diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 9e34de2..87a309f 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -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 diff --git a/src/components/SpritesheetSplitter.vue b/src/components/SpritesheetSplitter.vue index 4dbb235..bf1d2ed 100644 --- a/src/components/SpritesheetSplitter.vue +++ b/src/components/SpritesheetSplitter.vue @@ -10,7 +10,7 @@
-
+
+
@@ -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); diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index a40660d..9bdbd46 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -7,8 +7,14 @@ - @@ -54,6 +60,7 @@ @open-new-project-modal="isNewProjectModalOpen = true" @open-project-list="isProjectListOpen = true" @open-auth-modal="isAuthModalOpen = true" + @back-to-editor="goBackToEditor" /> @@ -64,6 +71,7 @@ diff --git a/src/components/layout/navbar/NavbarMobileMenu.vue b/src/components/layout/navbar/NavbarMobileMenu.vue index 6149270..0e2378e 100644 --- a/src/components/layout/navbar/NavbarMobileMenu.vue +++ b/src/components/layout/navbar/NavbarMobileMenu.vue @@ -34,6 +34,20 @@ Login / Register
+ + + + 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(); diff --git a/src/composables/useSpritesheetSplitter.ts b/src/composables/useSpritesheetSplitter.ts index 5df8db2..d8a5a07 100644 --- a/src/composables/useSpritesheetSplitter.ts +++ b/src/composables/useSpritesheetSplitter.ts @@ -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 { + 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, }; diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts index 739b089..1869be7 100644 --- a/src/stores/useSettingsStore.ts +++ b/src/stores/useSettingsStore.ts @@ -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; } } diff --git a/src/types/spritesheet.ts b/src/types/spritesheet.ts index c256d18..285419b 100644 --- a/src/types/spritesheet.ts +++ b/src/types/spritesheet.ts @@ -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 { diff --git a/src/views/EditorView.vue b/src/views/EditorView.vue index 3083cd5..00f0de0 100644 --- a/src/views/EditorView.vue +++ b/src/views/EditorView.vue @@ -9,9 +9,7 @@

Editor Tools

- +