[FEAT] Add back to project btn, extract GIFS into frames.
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [2.5.0] - 2026-01-05
|
||||
- Uploading GIFS will ask you if you want to extract them to individual frames
|
||||
|
||||
## [2.4.0] - 2026-01-03
|
||||
- Add pixel editor
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300"> Detection Mode </label>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-2 gap-3" :class="{ 'grid-cols-3': isGif }">
|
||||
<button @click="detectionMode = 'grid'" :class="['px-4 py-3 rounded-lg border-2 text-left transition-all', detectionMode === 'grid' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500']">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">Grid</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Split by cell size</div>
|
||||
@@ -19,6 +19,10 @@
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">Auto-detect</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Find individual sprites</div>
|
||||
</button>
|
||||
<button v-if="isGif" @click="detectionMode = 'gif'" :class="['px-4 py-3 rounded-lg border-2 text-left transition-all', detectionMode === 'gif' ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/30' : 'border-gray-300 dark:border-gray-600 hover:border-gray-400 dark:hover:border-gray-500']">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">Frame-by-frame</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Extract GIF frames</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,6 +133,8 @@
|
||||
|
||||
const gridRows = computed(() => (imageElement.value && cellHeight.value > 0 ? Math.floor(imageElement.value.height / cellHeight.value) : 0));
|
||||
|
||||
const isGif = computed(() => props.imageFile?.type === 'image/gif' || props.imageUrl.toLowerCase().endsWith('.gif'));
|
||||
|
||||
watch(
|
||||
() => props.imageUrl,
|
||||
url => {
|
||||
@@ -142,6 +148,12 @@
|
||||
cellWidth.value = suggested.width;
|
||||
cellHeight.value = suggested.height;
|
||||
|
||||
if (isGif.value) {
|
||||
detectionMode.value = 'gif';
|
||||
} else if (detectionMode.value === 'gif') {
|
||||
detectionMode.value = 'grid';
|
||||
}
|
||||
|
||||
generatePreview();
|
||||
};
|
||||
img.src = url;
|
||||
@@ -171,11 +183,13 @@
|
||||
preserveCellSize: preserveCellSize.value,
|
||||
removeEmpty: removeEmpty.value,
|
||||
});
|
||||
} else {
|
||||
} else if (detectionMode.value === 'auto') {
|
||||
previewSprites.value = await splitter.detectSprites(img, {
|
||||
sensitivity: sensitivity.value,
|
||||
removeEmpty: removeEmpty.value,
|
||||
});
|
||||
} else if (detectionMode.value === 'gif' && props.imageFile) {
|
||||
previewSprites.value = await splitter.extractGifFrames(props.imageFile);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error generating preview:', error);
|
||||
|
||||
@@ -7,8 +7,14 @@
|
||||
<NavbarLogo />
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center">
|
||||
<div class="hidden md:flex items-center gap-4">
|
||||
<NavbarLinks />
|
||||
|
||||
<!-- Back to Editor Button -->
|
||||
<button v-if="showBackToEditor" @click="goBackToEditor" class="btn btn-sm btn-primary shadow-indigo-500/20 shadow-lg">
|
||||
<i class="fas fa-arrow-left mr-2"></i>
|
||||
Back to Editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -54,6 +60,7 @@
|
||||
@open-new-project-modal="isNewProjectModalOpen = true"
|
||||
@open-project-list="isProjectListOpen = true"
|
||||
@open-auth-modal="isAuthModalOpen = true"
|
||||
@back-to-editor="goBackToEditor"
|
||||
/>
|
||||
</nav>
|
||||
<AuthModal :is-open="isAuthModalOpen" @close="isAuthModalOpen = false" />
|
||||
@@ -64,6 +71,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import DarkModeToggle from '../utilities/DarkModeToggle.vue';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import AuthModal from '@/components/auth/AuthModal.vue';
|
||||
@@ -91,11 +99,17 @@
|
||||
const saveMode = ref<'save' | 'save-as'>('save');
|
||||
const isSaving = ref(false);
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const authStore = useAuthStore();
|
||||
const projectStore = useProjectStore();
|
||||
const { createProject, openProject, saveProject, saveAsProject } = useProjectManager();
|
||||
const { addToast } = useToast();
|
||||
|
||||
const showBackToEditor = computed(() => {
|
||||
return route.name !== 'editor' && !!projectStore.currentProject;
|
||||
});
|
||||
|
||||
const handleOpenProject = async (project: Project) => {
|
||||
await openProject(project);
|
||||
};
|
||||
@@ -151,4 +165,8 @@
|
||||
const handleCreateNewProject = (config: { width: number; height: number; columns: number; rows: number }) => {
|
||||
createProject(config);
|
||||
};
|
||||
|
||||
const goBackToEditor = () => {
|
||||
router.push({ name: 'editor' });
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,20 @@
|
||||
Login / Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Back to Editor Button -->
|
||||
<button
|
||||
v-if="showBackToEditor"
|
||||
@click="
|
||||
$emit('back-to-editor');
|
||||
$emit('close');
|
||||
"
|
||||
class="mx-3 mb-3 btn btn-primary w-auto flex items-center gap-2"
|
||||
>
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
Back to Editor
|
||||
</button>
|
||||
|
||||
<router-link
|
||||
v-for="link in links"
|
||||
:key="link.path"
|
||||
@@ -127,6 +141,13 @@
|
||||
const route = useRoute();
|
||||
const isEditorActive = computed(() => route.name === 'editor');
|
||||
|
||||
import { useProjectStore } from '@/stores/useProjectStore';
|
||||
const projectStore = useProjectStore();
|
||||
|
||||
const showBackToEditor = computed(() => {
|
||||
return route.name !== 'editor' && !!projectStore.currentProject;
|
||||
});
|
||||
|
||||
const links = [
|
||||
{ name: 'Home', path: '/', icon: 'fas fa-home' },
|
||||
{ name: 'Blog', path: '/blog', icon: 'fas fa-newspaper' },
|
||||
@@ -147,7 +168,7 @@
|
||||
isSaving?: boolean;
|
||||
}>();
|
||||
|
||||
defineEmits(['close', 'open-help', 'save-project', 'open-save-modal', 'open-new-project-modal', 'open-project-list', 'open-auth-modal']);
|
||||
defineEmits(['close', 'open-help', 'save-project', 'open-save-modal', 'open-new-project-modal', 'open-project-list', 'open-auth-modal', 'back-to-editor']);
|
||||
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
const authStore = useAuthStore();
|
||||
|
||||
@@ -300,11 +300,65 @@ export function useSpritesheetSplitter() {
|
||||
return { width: cellWidth, height: cellHeight };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract frames from a GIF file using ImageDecoder API
|
||||
*/
|
||||
async function extractGifFrames(file: File): Promise<SpritePreview[]> {
|
||||
if (!('ImageDecoder' in window)) {
|
||||
console.warn('ImageDecoder API not supported');
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const decoder = new ImageDecoder({ data: new DataView(arrayBuffer), type: 'image/gif' });
|
||||
const sprites: SpritePreview[] = [];
|
||||
|
||||
let frameIndex = 0;
|
||||
// eslint-disable-next-line no-constant-condition
|
||||
while (true) {
|
||||
try {
|
||||
const result = await decoder.decode({ frameIndex });
|
||||
const frame = result.image;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = frame.displayWidth;
|
||||
canvas.height = frame.displayHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (ctx) {
|
||||
ctx.drawImage(frame, 0, 0);
|
||||
sprites.push({
|
||||
url: canvas.toDataURL('image/png'),
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
isEmpty: false,
|
||||
});
|
||||
}
|
||||
|
||||
frame.close(); // Important to release resources
|
||||
frameIndex++;
|
||||
} catch (err) {
|
||||
// End of frames (RangeError) or other error
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sprites;
|
||||
} catch (error) {
|
||||
console.error('Error extracting GIF frames:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isProcessing,
|
||||
previewSprites,
|
||||
splitByGrid,
|
||||
detectSprites,
|
||||
extractGifFrames,
|
||||
getSuggestedCellSize,
|
||||
cleanup,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ if (typeof window !== 'undefined') {
|
||||
if (storedDarkMode !== null) {
|
||||
darkMode.value = storedDarkMode === 'true';
|
||||
} else {
|
||||
darkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
darkMode.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SpritePreview {
|
||||
}
|
||||
|
||||
/** Detection mode for sprite splitting */
|
||||
export type DetectionMode = 'grid' | 'auto';
|
||||
export type DetectionMode = 'grid' | 'auto' | 'gif';
|
||||
|
||||
/** Options for grid-based splitting */
|
||||
export interface GridSplitOptions {
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
<div class="px-5 py-4 border-b border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between shrink-0 bg-gray-50/50 dark:bg-gray-800/10">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Editor Tools</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="closeProject" class="btn btn-secondary btn-sm w-8 h-8 p-0 justify-center border-gray-200 dark:border-gray-700 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20 dark:hover:text-red-400 dark:text-gray-400" title="Close Project">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<button @click="closeProject" class="btn btn-secondary btn-sm border-gray-200 dark:border-gray-700 hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-900/20 dark:hover:text-red-400 dark:text-gray-400" title="Close Project"><i class="fas fa-times mr-1"></i>Close</button>
|
||||
<button @click="openFileDialog" class="btn btn-primary btn-sm shadow-indigo-500/20" title="Add more sprites"><i class="fas fa-plus mr-1"></i> Add</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user