diff --git a/src/App.vue b/src/App.vue index 1d59502..95c6293 100644 --- a/src/App.vue +++ b/src/App.vue @@ -66,8 +66,30 @@
- Cell size: - {{ cellSize.width }} × {{ cellSize.height }}px + + + × + + {{ cellSize.width }} × {{ cellSize.height }}px
@@ -183,10 +205,26 @@ const settingsStore = useSettingsStore(); 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'), activeLayerId, toRef(settingsStore, 'backgroundColor')); + const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers( + layers, + columns, + toRef(settingsStore, 'negativeSpacingEnabled'), + activeLayerId, + toRef(settingsStore, 'backgroundColor'), + toRef(settingsStore, 'manualCellSizeEnabled'), + toRef(settingsStore, 'manualCellWidth'), + toRef(settingsStore, 'manualCellHeight') + ); const cellSize = computed(() => { if (!layers.value.length) return { width: 0, height: 0 }; + + // If manual cell size is enabled, use the manual values + if (settingsStore.manualCellSizeEnabled) { + return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight }; + } + + // Otherwise, calculate based on max dimensions const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value); const allSprites = visibleLayers.value.flatMap(l => l.sprites); const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index f178d64..683bb42 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -190,6 +190,9 @@ zoom, allowCellSwap, negativeSpacingEnabled: toRef(settingsStore, 'negativeSpacingEnabled'), + manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'), + manualCellWidth: toRef(settingsStore, 'manualCellWidth'), + manualCellHeight: toRef(settingsStore, 'manualCellHeight'), getMousePosition: (event, z) => canvas2D.getMousePosition(event, z), onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y), onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex), @@ -513,6 +516,9 @@ watch(() => settingsStore.darkMode, requestDraw); watch(() => settingsStore.negativeSpacingEnabled, requestDraw); watch(() => settingsStore.backgroundColor, requestDraw); + watch(() => settingsStore.manualCellSizeEnabled, requestDraw); + watch(() => settingsStore.manualCellWidth, requestDraw); + watch(() => settingsStore.manualCellHeight, requestDraw); watch(showAllSprites, requestDraw); diff --git a/src/components/SpritePreview.vue b/src/components/SpritePreview.vue index 25bf28c..76eea19 100644 --- a/src/components/SpritePreview.vue +++ b/src/components/SpritePreview.vue @@ -239,16 +239,34 @@ // Canvas drawing + const getCellDimensions = () => { + const visibleLayers = getVisibleLayers(); + // If manual cell size is enabled, use manual values + if (settingsStore.manualCellSizeEnabled) { + return { + cellWidth: settingsStore.manualCellWidth, + cellHeight: settingsStore.manualCellHeight, + negativeSpacing: 0, + }; + } + + // Otherwise, calculate from sprite dimensions + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); + const allSprites = visibleLayers.flatMap(l => l.sprites); + const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); + return { + cellWidth: maxWidth + negativeSpacing, + cellHeight: maxHeight + negativeSpacing, + negativeSpacing, + }; + }; + function drawPreviewCanvas() { if (!previewCanvasRef.value || !canvas2D.ctx.value) return; const visibleLayers = getVisibleLayers(); if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) return; - const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); - const allSprites = visibleLayers.flatMap(l => l.sprites); - const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); // Apply pixel art optimization canvas2D.applySmoothing(); @@ -298,9 +316,7 @@ const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY; const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value]; - const vLayers = getVisibleLayers(); - const allSprites = vLayers.flatMap(l => l.sprites); - const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); + const { negativeSpacing } = getCellDimensions(); // Check if click is on sprite (accounting for negative spacing offset) if (activeSprite) { @@ -332,12 +348,7 @@ const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value]; if (!activeSprite || activeSprite.id !== activeSpriteId.value) return; - const vLayers = getVisibleLayers(); - const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(vLayers); - const allSprites = vLayers.flatMap(l => l.sprites); - const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); // Calculate new position with constraints and round to integers let newX = Math.round(spritePosBeforeDrag.value.x + deltaX); @@ -417,6 +428,9 @@ watch(hiddenFrames, drawPreviewCanvas); watch(() => settingsStore.pixelPerfect, drawPreviewCanvas); watch(() => settingsStore.negativeSpacingEnabled, drawPreviewCanvas); + watch(() => settingsStore.manualCellSizeEnabled, drawPreviewCanvas); + watch(() => settingsStore.manualCellWidth, drawPreviewCanvas); + watch(() => settingsStore.manualCellHeight, drawPreviewCanvas); // Initial draw if (props.layers.some(l => l.sprites.length > 0)) { diff --git a/src/composables/useDragSprite.ts b/src/composables/useDragSprite.ts index cfbd6ff..5c1e353 100644 --- a/src/composables/useDragSprite.ts +++ b/src/composables/useDragSprite.ts @@ -32,6 +32,9 @@ export interface DragSpriteOptions { zoom?: Ref; allowCellSwap?: Ref; negativeSpacingEnabled?: Ref; + manualCellSizeEnabled?: Ref; + manualCellWidth?: Ref; + manualCellHeight?: Ref; getMousePosition: (event: MouseEvent, zoom?: number) => { x: number; y: number } | null; onUpdateSprite: (id: string, x: number, y: number) => void; onUpdateSpriteCell?: (id: string, newIndex: number) => void; @@ -47,6 +50,9 @@ export function useDragSprite(options: DragSpriteOptions) { const getZoom = () => options.zoom?.value ?? 1; const getAllowCellSwap = () => options.allowCellSwap?.value ?? false; const getNegativeSpacingEnabled = () => options.negativeSpacingEnabled?.value ?? false; + const getManualCellSizeEnabled = () => options.manualCellSizeEnabled?.value ?? false; + const getManualCellWidth = () => options.manualCellWidth?.value ?? 64; + const getManualCellHeight = () => options.manualCellHeight?.value ?? 64; // Drag state const isDragging = ref(false); @@ -69,9 +75,23 @@ export function useDragSprite(options: DragSpriteOptions) { const calculateMaxDimensions = () => { const sprites = getSprites(); const negativeSpacingEnabled = getNegativeSpacingEnabled(); + const manualCellSizeEnabled = getManualCellSizeEnabled(); + + // If manual cell size is enabled, use manual dimensions + if (manualCellSizeEnabled) { + const maxWidth = getManualCellWidth(); + const maxHeight = getManualCellHeight(); + // When manual cell size is used, negative spacing is not applied + const negativeSpacing = 0; + // Don't update lastMaxWidth/lastMaxHeight when in manual mode + return { maxWidth, maxHeight, negativeSpacing }; + } + + // Otherwise, calculate based on sprite dimensions const base = getMaxDimensions(sprites); - const baseMaxWidth = Math.max(1, base.maxWidth, lastMaxWidth.value); - const baseMaxHeight = Math.max(1, base.maxHeight, lastMaxHeight.value); + // When switching back from manual mode, reset to actual sprite dimensions + const baseMaxWidth = Math.max(1, base.maxWidth); + const baseMaxHeight = Math.max(1, base.maxHeight); lastMaxWidth.value = baseMaxWidth; lastMaxHeight.value = baseMaxHeight; diff --git a/src/composables/useExport.ts b/src/composables/useExport.ts index ae84853..ec0ea8c 100644 --- a/src/composables/useExport.ts +++ b/src/composables/useExport.ts @@ -6,17 +6,34 @@ import type { Sprite } from '../types/sprites'; import { getMaxDimensions } from './useSprites'; import { calculateNegativeSpacing } from './useNegativeSpacing'; -export const useExport = (sprites: Ref, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref) => { +export const useExport = (sprites: Ref, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref) => { + const getCellDimensions = () => { + // If manual cell size is enabled, use manual values + if (manualCellSizeEnabled?.value) { + return { + cellWidth: manualCellWidth?.value ?? 64, + cellHeight: manualCellHeight?.value ?? 64, + negativeSpacing: 0, + }; + } + + // Otherwise, calculate from sprite dimensions + const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); + const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value); + return { + cellWidth: maxWidth + negativeSpacing, + cellHeight: maxHeight + negativeSpacing, + negativeSpacing, + }; + }; + const downloadSpritesheet = () => { if (!sprites.value.length) { alert('Please upload or import sprites before downloading the spritesheet.'); return; } - const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); - const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const rows = Math.ceil(sprites.value.length / columns.value); const canvas = document.createElement('canvas'); @@ -78,6 +95,9 @@ export const useExport = (sprites: Ref, columns: Ref, negative columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, backgroundColor: backgroundColor?.value || 'transparent', + manualCellSizeEnabled: manualCellSizeEnabled?.value || false, + manualCellWidth: manualCellWidth?.value || 64, + manualCellHeight: manualCellHeight?.value || 64, sprites: spritesData.filter(Boolean), }; const jsonString = JSON.stringify(jsonData, null, 2); @@ -99,6 +119,9 @@ export const useExport = (sprites: Ref, columns: Ref, negative if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns; if (typeof jsonData.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = jsonData.negativeSpacingEnabled; if (typeof jsonData.backgroundColor === 'string' && backgroundColor) backgroundColor.value = jsonData.backgroundColor; + if (typeof jsonData.manualCellSizeEnabled === 'boolean' && manualCellSizeEnabled) manualCellSizeEnabled.value = jsonData.manualCellSizeEnabled; + if (typeof jsonData.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = jsonData.manualCellWidth; + if (typeof jsonData.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = jsonData.manualCellHeight; // revoke existing blob urls if (sprites.value.length) { @@ -149,10 +172,7 @@ export const useExport = (sprites: Ref, columns: Ref, negative return; } - const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); - const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; @@ -192,10 +212,7 @@ export const useExport = (sprites: Ref, columns: Ref, negative } const zip = new JSZip(); - const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); - const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return; diff --git a/src/composables/useExportLayers.ts b/src/composables/useExportLayers.ts index d348e1d..72fcd26 100644 --- a/src/composables/useExportLayers.ts +++ b/src/composables/useExportLayers.ts @@ -6,10 +6,31 @@ import type { Layer, Sprite } from '@/types/sprites'; import { getMaxDimensionsAcrossLayers } from './useLayers'; import { calculateNegativeSpacing } from './useNegativeSpacing'; -export const useExportLayers = (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref, activeLayerId?: Ref, backgroundColor?: Ref) => { +export const useExportLayers = (layersRef: Ref, columns: Ref, negativeSpacingEnabled: Ref, activeLayerId?: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref) => { const getVisibleLayers = () => layersRef.value.filter(l => l.visible); const getAllVisibleSprites = () => getVisibleLayers().flatMap(l => l.sprites); + const getCellDimensions = () => { + // If manual cell size is enabled, use manual values + if (manualCellSizeEnabled?.value) { + return { + cellWidth: manualCellWidth?.value ?? 64, + cellHeight: manualCellHeight?.value ?? 64, + negativeSpacing: 0, + }; + } + + // Otherwise, calculate from sprite dimensions + const visibleLayers = getVisibleLayers(); + const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); + const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); + return { + cellWidth: maxWidth + negativeSpacing, + cellHeight: maxHeight + negativeSpacing, + negativeSpacing, + }; + }; + const drawCompositeCell = (ctx: CanvasRenderingContext2D, cellIndex: number, cellWidth: number, cellHeight: number, negativeSpacing: number) => { ctx.clearRect(0, 0, cellWidth, cellHeight); // Apply background color if not transparent @@ -32,10 +53,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n return; } - const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); - const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const maxLen = Math.max(...visibleLayers.map(l => l.sprites.length)); const rows = Math.ceil(maxLen / columns.value); @@ -104,6 +122,9 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, backgroundColor: backgroundColor?.value || 'transparent', + manualCellSizeEnabled: manualCellSizeEnabled?.value || false, + manualCellWidth: manualCellWidth?.value || 64, + manualCellHeight: manualCellHeight?.value || 64, layers: layersData, }; const jsonString = JSON.stringify(json, null, 2); @@ -140,6 +161,9 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n if (typeof data.columns === 'number') columns.value = data.columns; if (typeof data.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = data.negativeSpacingEnabled; if (typeof data.backgroundColor === 'string' && backgroundColor) backgroundColor.value = data.backgroundColor; + if (typeof data.manualCellSizeEnabled === 'boolean' && manualCellSizeEnabled) manualCellSizeEnabled.value = data.manualCellSizeEnabled; + if (typeof data.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = data.manualCellWidth; + if (typeof data.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = data.manualCellHeight; if (Array.isArray(data.layers)) { const newLayers: Layer[] = []; @@ -186,10 +210,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n return; } - const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); - const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); @@ -224,10 +245,7 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n } const zip = new JSZip(); - const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers); - const negativeSpacing = calculateNegativeSpacing(getAllVisibleSprites(), negativeSpacingEnabled.value); - const cellWidth = maxWidth + negativeSpacing; - const cellHeight = maxHeight + negativeSpacing; + const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); @@ -263,6 +281,9 @@ export const useExportLayers = (layersRef: Ref, columns: Ref, n columns: columns.value, negativeSpacingEnabled: negativeSpacingEnabled.value, backgroundColor: backgroundColor?.value || 'transparent', + manualCellSizeEnabled: manualCellSizeEnabled?.value || false, + manualCellWidth: manualCellWidth?.value || 64, + manualCellHeight: manualCellHeight?.value || 64, layers: layersPayload, }; const metaStr = JSON.stringify(meta, null, 2); diff --git a/src/composables/useLayers.ts b/src/composables/useLayers.ts index 8694050..ecc0864 100644 --- a/src/composables/useLayers.ts +++ b/src/composables/useLayers.ts @@ -1,6 +1,7 @@ import { computed, ref, watch } from 'vue'; import type { Layer, Sprite } from '@/types/sprites'; import { getMaxDimensions as getMaxDimensionsSingle, useSprites as useSpritesSingle } from './useSprites'; +import { useSettingsStore } from '@/stores/useSettingsStore'; export const createEmptyLayer = (name: string): Layer => ({ id: crypto.randomUUID(), @@ -14,6 +15,7 @@ export const useLayers = () => { const layers = ref([createEmptyLayer('Base')]); const activeLayerId = ref(layers.value[0].id); const columns = ref(4); + const settingsStore = useSettingsStore(); watch(columns, val => { const num = typeof val === 'number' ? val : parseInt(String(val)); @@ -38,7 +40,22 @@ export const useLayers = () => { const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => { const l = activeLayer.value; if (!l || !l.sprites.length) return; - const { maxWidth, maxHeight } = getMaxDimensions(l.sprites); + + // Determine the cell dimensions to align within + let cellWidth: number; + let cellHeight: number; + + if (settingsStore.manualCellSizeEnabled) { + // Use manual cell size (without negative spacing) + cellWidth = settingsStore.manualCellWidth; + cellHeight = settingsStore.manualCellHeight; + } else { + // Use auto-calculated dimensions based on sprite sizes + const { maxWidth, maxHeight } = getMaxDimensions(l.sprites); + cellWidth = maxWidth; + cellHeight = maxHeight; + } + l.sprites = l.sprites.map(sprite => { let x = sprite.x; let y = sprite.y; @@ -47,19 +64,19 @@ export const useLayers = () => { x = 0; break; case 'center': - x = Math.floor((maxWidth - sprite.width) / 2); + x = Math.floor((cellWidth - sprite.width) / 2); break; case 'right': - x = Math.floor(maxWidth - sprite.width); + x = Math.floor(cellWidth - sprite.width); break; case 'top': y = 0; break; case 'middle': - y = Math.floor((maxHeight - sprite.height) / 2); + y = Math.floor((cellHeight - sprite.height) / 2); break; case 'bottom': - y = Math.floor(maxHeight - sprite.height); + y = Math.floor(cellHeight - sprite.height); break; } return { ...sprite, x: Math.floor(x), y: Math.floor(y) }; diff --git a/src/stores/useSettingsStore.ts b/src/stores/useSettingsStore.ts index 75a4178..a4c4508 100644 --- a/src/stores/useSettingsStore.ts +++ b/src/stores/useSettingsStore.ts @@ -5,6 +5,9 @@ const pixelPerfect = ref(true); const darkMode = ref(false); const negativeSpacingEnabled = ref(false); const backgroundColor = ref('transparent'); +const manualCellSizeEnabled = ref(false); +const manualCellWidth = ref(64); +const manualCellHeight = ref(64); // Initialize dark mode from localStorage or system preference if (typeof window !== 'undefined') { @@ -61,16 +64,40 @@ export const useSettingsStore = defineStore('settings', () => { backgroundColor.value = color; } + function toggleManualCellSize() { + manualCellSizeEnabled.value = !manualCellSizeEnabled.value; + } + + function setManualCellWidth(width: number) { + manualCellWidth.value = Math.max(1, Math.floor(width)); + } + + function setManualCellHeight(height: number) { + manualCellHeight.value = Math.max(1, Math.floor(height)); + } + + function setManualCellSize(width: number, height: number) { + setManualCellWidth(width); + setManualCellHeight(height); + } + return { pixelPerfect, darkMode, negativeSpacingEnabled, backgroundColor, + manualCellSizeEnabled, + manualCellWidth, + manualCellHeight, togglePixelPerfect, setPixelPerfect, toggleDarkMode, setDarkMode, toggleNegativeSpacing, setBackgroundColor, + toggleManualCellSize, + setManualCellWidth, + setManualCellHeight, + setManualCellSize, }; });