diff --git a/public/CHANGELOG.md b/public/CHANGELOG.md index 0f0bdbe..fdb245f 100644 --- a/public/CHANGELOG.md +++ b/public/CHANGELOG.md @@ -1,5 +1,10 @@ All notable changes to this project will be documented in this file. +## [1.7.0] - 2025-11-22 +- Add layer support +- Add background color picker +- Improved UI + ## [1.6.0] - 2025-11-18 - Improved animation preview modal - Add toggle for negative spacing in cells @@ -34,30 +39,30 @@ All notable changes to this project will be documented in this file. ## [1.7.0] - 2025-05-02 ### Removed -- 🪟 Checkerboard pattern inside sprite cells as it could conflict with the sprite. (Thanks Rivers) +- Checkerboard pattern inside sprite cells as it could conflict with the sprite. (Thanks Rivers) ## [1.6.0] - 2025-04-30 ### Added -- 🎨 Dark mode support -- ⭐ Preview other sprites inside cells from overview +- Dark mode support +- Preview other sprites inside cells from overview ## [1.5.0] - 2025-04-30 ### Added -- 📏 Offset indicators for better sprite alignment +- Offset indicators for better sprite alignment - Set base offset for offset indicators ## [1.4.0] - 2025-04-06 ### Added -- 🎥 Download as GIF functionality -- 🗂 Download as ZIP functionality +- Download as GIF functionality +- Download as ZIP functionality ## [1.3.0] - 2025-04-06 ### Fixed -- 📄 When importing a spritesheet, the tool will now remove transparent from the edges of each sprite so you can move them inside their cells. +- When importing a spritesheet, the tool will now remove transparent from the edges of each sprite so you can move them inside their cells. ## [1.2.0] - 2025-04-06 @@ -67,22 +72,22 @@ All notable changes to this project will be documented in this file. ## [1.1.0] - 2025-04-06 ### Added -- 📝 Help modal with instructions and tips -- 🎨 Pixel perfect mode for better sprite alignment +- Help modal with instructions and tips +- Pixel perfect mode for better sprite alignment ## [1.0.0] - 2025-04-06 ### Added - 🎉 Initial release -- ✨ Basic spritesheet generation functionality - - Drag and drop image upload - - Grid-based sprite arrangement - - Custom grid size configuration -- 🎮 Animation preview functionality - - Real-time animation preview - - Adjustable animation speed - - Frame-by-frame navigation -- 💾 JSON import/export support - - Save sprite arrangements - - Load previous projects - - Export configuration files \ No newline at end of file +- Basic spritesheet generation functionality +- Drag and drop image upload +- Grid-based sprite arrangement +- Custom grid size configuration +- Animation preview functionality +- Real-time animation preview +- Adjustable animation speed +- Frame-by-frame navigation +- JSON import/export support +- Save sprite arrangements +- Load previous projects +- Export configuration files \ No newline at end of file diff --git a/src/App.vue b/src/App.vue index 8e1228d..1d59502 100644 --- a/src/App.vue +++ b/src/App.vue @@ -45,9 +45,7 @@
- + @@ -123,7 +121,17 @@ Preview animation
- +
@@ -173,26 +181,9 @@ import type { SpriteFile } from './types/sprites'; const settingsStore = useSettingsStore(); - const { - layers, - visibleLayers, - activeLayer, - activeLayerId, - columns, - updateSpritePosition, - updateSpriteCell, - removeSprite, - replaceSprite, - addSprite, - addSpriteWithResize, - processImageFiles, - alignSprites, - addLayer, - removeLayer, - moveLayer, - } = useLayers(); + const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers(); - const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled')); + const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'), activeLayerId, toRef(settingsStore, 'backgroundColor')); const cellSize = computed(() => { if (!layers.value.length) return { width: 0, height: 0 }; diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index 74ec52c..de036e1 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -23,6 +23,18 @@ + +
+ + + +
@@ -172,7 +184,7 @@ findSpriteAtPosition, calculateMaxDimensions, } = useDragSprite({ - sprites: computed(() => (props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? [])), + sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []), columns: toRef(props, 'columns'), zoom, allowCellSwap, @@ -198,6 +210,7 @@ const contextMenuSpriteId = ref(null); const replacingSpriteId = ref(null); const fileInput = ref(null); + const customColor = ref('#ffffff'); const startDrag = (event: MouseEvent) => { if (!canvasRef.value) return; @@ -423,6 +436,7 @@ watch(() => settingsStore.pixelPerfect, requestDraw); watch(() => settingsStore.darkMode, requestDraw); watch(() => settingsStore.negativeSpacingEnabled, requestDraw); + watch(() => settingsStore.backgroundColor, requestDraw); watch(showAllSprites, requestDraw); diff --git a/src/components/SpritePreview.vue b/src/components/SpritePreview.vue index 90c068e..25bf28c 100644 --- a/src/components/SpritePreview.vue +++ b/src/components/SpritePreview.vue @@ -307,12 +307,12 @@ const spriteCanvasX = negativeSpacing + activeSprite.x; const spriteCanvasY = negativeSpacing + activeSprite.y; if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + activeSprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + activeSprite.height) { - isDragging.value = true; - activeSpriteId.value = activeSprite.id; - dragStartX.value = mouseX; - dragStartY.value = mouseY; - spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y }; - } + isDragging.value = true; + activeSpriteId.value = activeSprite.id; + dragStartX.value = mouseX; + dragStartY.value = mouseY; + spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y }; + } } }; diff --git a/src/composables/useCanvas2D.ts b/src/composables/useCanvas2D.ts index 38dbe98..3243f3e 100644 --- a/src/composables/useCanvas2D.ts +++ b/src/composables/useCanvas2D.ts @@ -116,9 +116,10 @@ export function useCanvas2D(canvasRef: Ref, options?: }); }; - // Fill cell background with theme-aware color + // Fill cell background with selected color or transparent const fillCellBackground = (x: number, y: number, width: number, height: number) => { - const color = settingsStore.darkMode ? '#1F2937' : '#f9fafb'; + if (settingsStore.backgroundColor === 'transparent') return; + const color = settingsStore.backgroundColor === 'custom' ? settingsStore.backgroundColor : settingsStore.backgroundColor; fillRect(x, y, width, height, color); }; diff --git a/src/composables/useExport.ts b/src/composables/useExport.ts index 688ec59..f5e9050 100644 --- a/src/composables/useExport.ts +++ b/src/composables/useExport.ts @@ -156,8 +156,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative sprites.value.forEach(sprite => { ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = '#f9fafb'; - ctx.fillRect(0, 0, cellWidth, cellHeight); ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y)); gif.addFrame(ctx, { copy: true, delay: 1000 / fps }); }); @@ -194,8 +192,6 @@ export const useExport = (sprites: Ref, columns: Ref, negative sprites.value.forEach((sprite, index) => { ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.fillStyle = '#f9fafb'; - ctx.fillRect(0, 0, cellWidth, cellHeight); ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y)); const dataURL = canvas.toDataURL('image/png'); const binary = atob(dataURL.split(',')[1]); diff --git a/src/composables/useExportLayers.ts b/src/composables/useExportLayers.ts index beae08b..f77049b 100644 --- a/src/composables/useExportLayers.ts +++ b/src/composables/useExportLayers.ts @@ -6,14 +6,17 @@ import type { Layer, Sprite } from '@/types/sprites'; import { getMaxDimensionsAcrossLayers } from './useLayers'; import { calculateNegativeSpacing } from './useNegativeSpacing'; -export const useExportLayers = (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref) => { +export const useExportLayers = (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref, activeLayerId?: Ref, backgroundColor?: Ref) => { const getVisibleLayers = () => layersRef.value.filter(l => l.visible); const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites); const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => { ctx.clearRect(0, 0, cellWidth, cellHeight); - ctx.fillStyle = '#f9fafb'; - ctx.fillRect(0, 0, cellWidth, cellHeight); + // Apply background color if not transparent + if (backgroundColor?.value && backgroundColor.value !== 'transparent') { + ctx.fillStyle = backgroundColor.value; + ctx.fillRect(0, 0, cellWidth, cellHeight); + } const vLayers = getVisibleLayers(); vLayers.forEach(layer => { const sprite = layer.sprites[cellIndex]; @@ -130,16 +133,32 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n const sprites: Sprite[] = await Promise.all(layerData.sprites.map((s: any) => loadSprite(s))); newLayers.push({ id: layerData.id || crypto.randomUUID(), name: layerData.name || 'Layer', visible: layerData.visible !== false, locked: !!layerData.locked, sprites }); } + // Ensure at least one layer with sprites is visible + if (newLayers.length > 0 && !newLayers.some(l => l.visible && l.sprites.length > 0)) { + const firstLayerWithSprites = newLayers.find(l => l.sprites.length > 0); + if (firstLayerWithSprites) { + firstLayerWithSprites.visible = true; + } + } layersRef.value = newLayers; + // Set active layer to the first layer with sprites + if (activeLayerId && newLayers.length > 0) { + const firstWithSprites = newLayers.find(l => l.sprites.length > 0); + activeLayerId.value = firstWithSprites ? firstWithSprites.id : newLayers[0].id; + } return; } if (Array.isArray(data.sprites)) { const sprites: Sprite[] = await Promise.all(data.sprites.map((s: any) => loadSprite(s))); + const baseLayerId = crypto.randomUUID(); layersRef.value = [ - { id: crypto.randomUUID(), name: 'Base', visible: true, locked: false, sprites }, - { id: crypto.randomUUID(), name: 'Clothes', visible: true, locked: false, sprites: [] }, + { id: baseLayerId, name: 'Base', visible: true, locked: false, sprites }, + { id: crypto.randomUUID(), name: 'Other', visible: true, locked: false, sprites: [] }, ]; + if (activeLayerId) { + activeLayerId.value = baseLayerId; + } return; } @@ -222,9 +241,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n name: layer.name, visible: layer.visible, locked: layer.locked, - sprites: await Promise.all( - layer.sprites.map(async s => ({ id: s.id, width: s.width, height: s.height, x: s.x, y: s.y, name: s.file.name })) - ), + sprites: await Promise.all(layer.sprites.map(async s => ({ id: s.id, width: s.width, height: s.height, x: s.x, y: s.y, name: s.file.name }))), })) ); const meta = { version: 2, columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, layers: layersPayload }; diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts index 466a0fa..8694050 100644 --- a/src/composables/useLayers.ts +++ b/src/composables/useLayers.ts @@ -200,6 +200,6 @@ export const useLayers = () => { }; export const getMaxDimensionsAcrossLayers = (layers: Layer[]) => { - const sprites = layers.flatMap(l => l.visible ? l.sprites : []); + const sprites = layers.flatMap(l => (l.visible ? l.sprites : [])); return getMaxDimensionsSingle(sprites); -}; \ No newline at end of file +}; diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts index 4029b36..75a4178 100644 --- a/src/stores/useSettingsStore.ts +++ b/src/stores/useSettingsStore.ts @@ -4,6 +4,7 @@ import { ref, watch } from 'vue'; const pixelPerfect = ref(true); const darkMode = ref(false); const negativeSpacingEnabled = ref(false); +const backgroundColor = ref('transparent'); // Initialize dark mode from localStorage or system preference if (typeof window !== 'undefined') { @@ -56,14 +57,20 @@ export const useSettingsStore = defineStore('settings', () => { negativeSpacingEnabled.value = !negativeSpacingEnabled.value; } + function setBackgroundColor(color: string) { + backgroundColor.value = color; + } + return { pixelPerfect, darkMode, negativeSpacingEnabled, + backgroundColor, togglePixelPerfect, setPixelPerfect, toggleDarkMode, setDarkMode, toggleNegativeSpacing, + setBackgroundColor, }; });