[FEAT] Add back to project btn, extract GIFS into frames.

This commit is contained in:
2026-01-05 01:15:01 +01:00
parent 8e71d7379a
commit 77ae4bb429
8 changed files with 117 additions and 9 deletions

View File

@@ -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);

View File

@@ -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>

View File

@@ -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();

View File

@@ -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,
};

View File

@@ -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;
}
}

View File

@@ -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 {

View File

@@ -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>