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 @@
@@ -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 @@
Negative spacing
+
+
+ Background:
+
+ Transparent
+ White
+ Black
+ Light Gray
+ Custom...
+
+
+
@@ -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,
};
});