Compare commits

...

2 Commits

Author SHA1 Message Date
a0d2b2994b [FEAT] Allow to toggle checker bg 2025-11-23 15:42:02 +01:00
7f8e26255f [FEAT] Canvas > IMG 2025-11-23 15:40:04 +01:00
3 changed files with 348 additions and 382 deletions

View File

@@ -35,7 +35,7 @@
</h3> </h3>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all"> <label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
<input id="pixel-perfect" type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 rounded" @change="requestDraw" /> <input id="pixel-perfect" type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 rounded" />
<span class="text-sm font-medium dark:text-gray-200">Pixel Perfect</span> <span class="text-sm font-medium dark:text-gray-200">Pixel Perfect</span>
</label> </label>
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all"> <label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
@@ -50,6 +50,10 @@
<input id="negative-spacing" type="checkbox" v-model="settingsStore.negativeSpacingEnabled" class="w-4 h-4 rounded" /> <input id="negative-spacing" type="checkbox" v-model="settingsStore.negativeSpacingEnabled" class="w-4 h-4 rounded" />
<span class="text-sm font-medium dark:text-gray-200">Negative Spacing</span> <span class="text-sm font-medium dark:text-gray-200">Negative Spacing</span>
</label> </label>
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
<input id="checkerboard" type="checkbox" v-model="settingsStore.checkerboardEnabled" class="w-4 h-4 rounded" />
<span class="text-sm font-medium dark:text-gray-200">Checkerboard</span>
</label>
<div class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600"> <div class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600">
<label for="bg-color" class="text-sm font-medium dark:text-gray-200">Background:</label> <label for="bg-color" class="text-sm font-medium dark:text-gray-200">Background:</label>
<select id="bg-color" v-model="bgSelectValue" class="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 dark:text-gray-200 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all"> <select id="bg-color" v-model="bgSelectValue" class="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 dark:text-gray-200 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all">
@@ -82,37 +86,124 @@
<div class="relative bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-2xl shadow-lg overflow-auto max-h-[calc(100vh-400px)] min-h-[500px] w-full"> <div class="relative bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-2xl shadow-lg overflow-auto max-h-[calc(100vh-400px)] min-h-[500px] w-full">
<div class="canvas-container touch-manipulation relative inline-block min-w-full"> <div class="canvas-container touch-manipulation relative inline-block min-w-full">
<div :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }" class="inline-block"> <div
<canvas ref="gridContainerRef"
ref="canvasRef" :style="{
@mousedown="startDrag" transform: `scale(${zoom})`,
@mousemove="drag" transformOrigin: 'top left',
@mouseup="stopDrag" width: `${gridDimensions.width}px`,
@mouseleave="stopDrag" height: `${gridDimensions.height}px`,
@touchstart="handleTouchStart" position: 'relative',
@touchmove="handleTouchMove" }"
@touchend="stopDrag" class="inline-block"
@contextmenu.prevent @mousedown="startDrag"
@dragover="handleDragOver" @mousemove="drag"
@dragenter="handleDragEnter" @mouseup="stopDrag"
@dragleave="onDragLeave" @mouseleave="stopDrag"
@drop="handleDrop" @touchstart="handleTouchStart"
class="block transition-all" @touchmove="handleTouchMove"
:class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }" @touchend="stopDrag"
:style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" @contextmenu.prevent
></canvas> @dragover="handleDragOver"
</div> @dragenter="handleDragEnter"
@dragleave="onDragLeave"
<!-- Offset labels in corners (not scaled with zoom) --> @drop="handleDrop"
<div v-if="canvasRef && showOffsetLabels" class="absolute inset-0 pointer-events-none"> :class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }"
>
<!-- Grid cells -->
<div <div
v-for="position in spritePositions" v-for="cellIndex in totalCells"
:key="position.id" :key="`cell-${cellIndex - 1}`"
class="absolute text-[23px] leading-none font-mono text-cyan-600 dark:text-cyan-400 bg-white/90 dark:bg-gray-900/90 px-1 py-0.5 rounded-sm" class="absolute"
:style="{ :style="{
// Position at bottom-right corner of each cell, scaled by zoom left: `${getCellPosition(cellIndex - 1).x}px`,
left: `${(position.cellX + position.maxWidth) * zoom - 2}px`, top: `${getCellPosition(cellIndex - 1).y}px`,
top: `${(position.cellY + position.maxHeight) * zoom - 2}px`, width: `${gridMetrics.maxWidth}px`,
height: `${gridMetrics.maxHeight}px`,
backgroundColor: getCellBackground(),
backgroundImage: getCellBackgroundImage(),
backgroundSize: getCellBackgroundSize(),
backgroundPosition: getCellBackgroundPosition(),
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
}"
:class="{
'bg-blue-500 bg-opacity-20': highlightCell && highlightCell.col === (cellIndex - 1) % columns && highlightCell.row === Math.floor((cellIndex - 1) / columns),
}"
></div>
<!-- Background sprites (for compare mode) -->
<template v-if="showAllSprites">
<template v-for="layer in visibleLayers" :key="`bg-layer-${layer.id}`">
<template v-for="(sprite, spriteIndex) in layer.sprites" :key="`bg-${sprite.id}`">
<template v-for="cellIndex in totalCells" :key="`bg-${sprite.id}-${cellIndex}`">
<img
v-if="spriteIndex !== cellIndex - 1 && !(activeSpriteId === sprite.id && ghostSprite)"
:src="sprite.img.src"
class="absolute pointer-events-none"
:style="{
left: `${getCellPosition(cellIndex - 1).x + gridMetrics.negativeSpacing + sprite.x}px`,
top: `${getCellPosition(cellIndex - 1).y + gridMetrics.negativeSpacing + sprite.y}px`,
width: `${sprite.width}px`,
height: `${sprite.height}px`,
opacity: '0.25',
imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto',
}"
draggable="false"
/>
</template>
</template>
</template>
</template>
<!-- Layer sprites -->
<template v-for="layer in layers" :key="layer.id">
<template v-if="layer.visible">
<template v-for="(sprite, index) in layer.sprites" :key="sprite.id">
<img
v-if="!(activeSpriteId === sprite.id && ghostSprite)"
:src="sprite.img.src"
class="absolute cursor-move"
:style="{
left: `${getCellPosition(index).x + gridMetrics.negativeSpacing + sprite.x}px`,
top: `${getCellPosition(index).y + gridMetrics.negativeSpacing + sprite.y}px`,
width: `${sprite.width}px`,
height: `${sprite.height}px`,
opacity: layer.id === activeLayerId ? '1' : '0.85',
imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto',
}"
:data-sprite-id="sprite.id"
:data-layer-id="layer.id"
draggable="false"
/>
</template>
</template>
</template>
<!-- Ghost sprite (while dragging) -->
<img
v-if="ghostSprite && activeSpriteId"
:src="activeSpriteSprite?.img.src"
class="absolute pointer-events-none"
:style="{
left: `${ghostSprite.x}px`,
top: `${ghostSprite.y}px`,
width: `${activeSpriteSprite?.width}px`,
height: `${activeSpriteSprite?.height}px`,
opacity: '0.6',
imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto',
}"
draggable="false"
/>
<!-- Offset labels -->
<div
v-if="showOffsetLabels"
v-for="position in spritePositions"
:key="`label-${position.id}`"
class="absolute text-[23px] leading-none font-mono text-cyan-600 dark:text-cyan-400 bg-white/90 dark:bg-gray-900/90 px-1 py-0.5 rounded-sm pointer-events-none"
:style="{
left: `${position.cellX + position.maxWidth - 2}px`,
top: `${position.cellY + position.maxHeight - 2}px`,
transform: 'translate(-100%, -100%)', transform: 'translate(-100%, -100%)',
}" }"
> >
@@ -130,7 +221,6 @@
import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue'; import { ref, onMounted, watch, onUnmounted, toRef, computed, nextTick } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore'; import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites'; import type { Sprite } from '@/types/sprites';
import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom'; import { useZoom } from '@/composables/useZoom';
import { useDragSprite } from '@/composables/useDragSprite'; import { useDragSprite } from '@/composables/useDragSprite';
import { useFileDrop } from '@/composables/useFileDrop'; import { useFileDrop } from '@/composables/useFileDrop';
@@ -155,21 +245,7 @@
// Get settings from store // Get settings from store
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const canvasRef = ref<HTMLCanvasElement | null>(null); const gridContainerRef = ref<HTMLDivElement | null>(null);
// rAF-based draw scheduler to coalesce multiple draw requests into a single frame
// Define before usage to avoid TDZ issues when passing into composables during setup
let rafId: number | null = null;
function requestDraw() {
if (rafId !== null) return;
rafId = requestAnimationFrame(() => {
rafId = null;
drawCanvas();
});
}
// Initialize composables
const canvas2D = useCanvas2D(canvasRef);
const { const {
zoom, zoom,
@@ -185,6 +261,17 @@
const allowCellSwap = ref(false); const allowCellSwap = ref(false);
const getMousePosition = (event: MouseEvent, z: number) => {
if (!gridContainerRef.value) return null;
const rect = gridContainerRef.value.getBoundingClientRect();
const scaleX = gridContainerRef.value.offsetWidth / (rect.width / z);
const scaleY = gridContainerRef.value.offsetHeight / (rect.height / z);
return {
x: ((event.clientX - rect.left) / z) * scaleX,
y: ((event.clientY - rect.top) / z) * scaleY,
};
};
const { const {
isDragging, isDragging,
activeSpriteId, activeSpriteId,
@@ -208,13 +295,15 @@
manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'), manualCellSizeEnabled: toRef(settingsStore, 'manualCellSizeEnabled'),
manualCellWidth: toRef(settingsStore, 'manualCellWidth'), manualCellWidth: toRef(settingsStore, 'manualCellWidth'),
manualCellHeight: toRef(settingsStore, 'manualCellHeight'), manualCellHeight: toRef(settingsStore, 'manualCellHeight'),
getMousePosition: (event, z) => canvas2D.getMousePosition(event, z), getMousePosition,
onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y), onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y),
onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex), onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex),
onDraw: requestDraw, onDraw: () => {},
}); });
const activeSprites = computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []); const activeSprites = computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []);
const visibleLayers = computed(() => props.layers.filter(l => l.visible));
const activeSpriteSprite = computed(() => activeSprites.value.find(s => s.id === activeSpriteId.value));
const { isDragOver, handleDragOver, handleDragEnter, handleDragLeave, handleDrop } = useFileDrop({ const { isDragOver, handleDragOver, handleDragEnter, handleDragLeave, handleDrop } = useFileDrop({
sprites: activeSprites, sprites: activeSprites,
@@ -232,24 +321,79 @@
const fileInput = ref<HTMLInputElement | null>(null); const fileInput = ref<HTMLInputElement | null>(null);
const customColor = ref('#ffffff'); const customColor = ref('#ffffff');
// Background select handling: keep the select stable on "custom" // Grid metrics
const gridMetrics = computed(() => calculateMaxDimensions());
const totalCells = computed(() => {
const maxLen = Math.max(1, ...props.layers.map(l => (l.visible ? l.sprites.length : 1)));
return Math.max(1, Math.ceil(maxLen / props.columns)) * props.columns;
});
const gridDimensions = computed(() => {
const maxLen = Math.max(1, ...props.layers.map(l => (l.visible ? l.sprites.length : 1)));
const rows = Math.max(1, Math.ceil(maxLen / props.columns));
return {
width: gridMetrics.value.maxWidth * props.columns,
height: gridMetrics.value.maxHeight * rows,
};
});
const getCellPosition = (index: number) => {
const col = index % props.columns;
const row = Math.floor(index / props.columns);
return {
x: col * gridMetrics.value.maxWidth,
y: row * gridMetrics.value.maxHeight,
};
};
const getCellBackground = () => {
const bg = settingsStore.backgroundColor;
if (bg === 'transparent') {
return 'transparent';
}
return bg;
};
const getCellBackgroundImage = () => {
const bg = settingsStore.backgroundColor;
if (bg === 'transparent' && settingsStore.checkerboardEnabled) {
// Checkerboard pattern for transparent backgrounds (dark mode friendly)
const color = settingsStore.darkMode ? '#4b5563' : '#d1d5db';
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
}
return 'none';
};
const getCellBackgroundSize = () => {
const bg = settingsStore.backgroundColor;
if (bg === 'transparent') {
return '20px 20px';
}
return 'auto';
};
const getCellBackgroundPosition = () => {
const bg = settingsStore.backgroundColor;
if (bg === 'transparent') {
return '0 0, 0 10px, 10px -10px, -10px 0px';
}
return '0 0';
};
// Background select handling
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const; const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val); const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
// Ensure customColor mirrors the current stored custom color (only when needed)
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) { if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
customColor.value = settingsStore.backgroundColor; customColor.value = settingsStore.backgroundColor;
} }
// Track if user is in custom mode to keep picker visible
// Initialize to true if current color is custom
const isCustomMode = ref(isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)); const isCustomMode = ref(isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any));
const bgSelectValue = computed<string>({ const bgSelectValue = computed<string>({
get() { get() {
// If user explicitly entered custom mode, stay in it regardless of color value
if (isCustomMode.value) { if (isCustomMode.value) {
// Keep color picker synced
const val = settingsStore.backgroundColor; const val = settingsStore.backgroundColor;
if (isHexColor(val)) { if (isHexColor(val)) {
customColor.value = val; customColor.value = val;
@@ -260,18 +404,15 @@
const val = settingsStore.backgroundColor; const val = settingsStore.backgroundColor;
if (presetBgColors.includes(val as any)) return val; if (presetBgColors.includes(val as any)) return val;
if (isHexColor(val)) { if (isHexColor(val)) {
// Keep the color picker open and sync its swatch
customColor.value = val; customColor.value = val;
isCustomMode.value = true; // Auto-enable custom mode for non-preset hex colors isCustomMode.value = true;
return 'custom'; return 'custom';
} }
// Fallback
return 'transparent'; return 'transparent';
}, },
set(v: string) { set(v: string) {
if (v === 'custom') { if (v === 'custom') {
isCustomMode.value = true; isCustomMode.value = true;
// Switch UI to custom mode but keep the stored value as a color
const fallback = '#ffffff'; const fallback = '#ffffff';
const current = settingsStore.backgroundColor; const current = settingsStore.backgroundColor;
const fromStore = isHexColor(current) ? current : null; const fromStore = isHexColor(current) ? current : null;
@@ -286,20 +427,8 @@
}, },
}); });
// Ensure canvas redraw and UI flush after background changes
watch(
() => settingsStore.backgroundColor,
async () => {
await nextTick();
requestDraw();
}
);
// Grid metrics used to position offset labels relative to cell size
const gridMetrics = computed(() => calculateMaxDimensions());
const startDrag = (event: MouseEvent) => { const startDrag = (event: MouseEvent) => {
if (!canvasRef.value) return; if (!gridContainerRef.value) return;
// Hide context menu if open // Hide context menu if open
showContextMenu.value = false; showContextMenu.value = false;
@@ -307,13 +436,12 @@
// Handle right-click for context menu // Handle right-click for context menu
if ('button' in event && (event as MouseEvent).button === 2) { if ('button' in event && (event as MouseEvent).button === 2) {
event.preventDefault(); event.preventDefault();
const pos = canvas2D.getMousePosition(event, zoom.value); const pos = getMousePosition(event, zoom.value);
if (!pos) return; if (!pos) return;
const clickedSprite = findSpriteAtPosition(pos.x, pos.y); const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
contextMenuSpriteId.value = clickedSprite?.id || null; contextMenuSpriteId.value = clickedSprite?.id || null;
// Position context menu at cursor - use clientX/clientY with fixed positioning
contextMenuX.value = event.clientX; contextMenuX.value = event.clientX;
contextMenuY.value = event.clientY; contextMenuY.value = event.clientY;
@@ -321,14 +449,13 @@
return; return;
} }
// Ignore non-left mouse buttons (but allow touch-generated events without a button prop) // Ignore non-left mouse buttons
if ('button' in event && (event as MouseEvent).button !== 0) return; if ('button' in event && (event as MouseEvent).button !== 0) return;
// Delegate to composable for actual drag handling // Delegate to composable for actual drag handling
dragStart(event); dragStart(event);
}; };
// Wrapper for drag move
const drag = (event: MouseEvent) => { const drag = (event: MouseEvent) => {
dragMove(event); dragMove(event);
}; };
@@ -343,10 +470,8 @@
const replaceSprite = () => { const replaceSprite = () => {
if (contextMenuSpriteId.value && fileInput.value) { if (contextMenuSpriteId.value && fileInput.value) {
// Store the sprite ID separately so it persists after context menu closes
replacingSpriteId.value = contextMenuSpriteId.value; replacingSpriteId.value = contextMenuSpriteId.value;
fileInput.value.click(); fileInput.value.click();
// Hide context menu immediately since we've stored the ID
showContextMenu.value = false; showContextMenu.value = false;
contextMenuSpriteId.value = null; contextMenuSpriteId.value = null;
} }
@@ -355,7 +480,6 @@
const addSprite = () => { const addSprite = () => {
if (fileInput.value) { if (fileInput.value) {
fileInput.value.click(); fileInput.value.click();
// Hide context menu immediately
showContextMenu.value = false; showContextMenu.value = false;
contextMenuSpriteId.value = null; contextMenuSpriteId.value = null;
} }
@@ -370,14 +494,12 @@
if (replacingSpriteId.value) { if (replacingSpriteId.value) {
emit('replaceSprite', replacingSpriteId.value, file); emit('replaceSprite', replacingSpriteId.value, file);
} else { } else {
// Adding new sprite
emit('addSprite', file); emit('addSprite', file);
} }
} else { } else {
alert('Please select an image file.'); alert('Please select an image file.');
} }
} }
// Clean up after file selection
replacingSpriteId.value = null; replacingSpriteId.value = null;
input.value = ''; input.value = '';
}; };
@@ -387,161 +509,26 @@
contextMenuSpriteId.value = null; contextMenuSpriteId.value = null;
}; };
// Wrapper for drag leave to pass canvasRef
const onDragLeave = (event: DragEvent) => { const onDragLeave = (event: DragEvent) => {
handleDragLeave(event, canvasRef.value); handleDragLeave(event, gridContainerRef.value);
};
function drawCanvas() {
if (!canvasRef.value || !canvas2D.ctx.value) return;
const { maxWidth, maxHeight, negativeSpacing } = calculateMaxDimensions();
// Set canvas size
const maxLen = Math.max(1, ...props.layers.map(l => (l.visible ? l.sprites.length : 1)));
const rows = Math.max(1, Math.ceil(maxLen / props.columns));
const newCanvasWidth = maxWidth * props.columns;
const newCanvasHeight = maxHeight * rows;
canvas2D.setCanvasSize(newCanvasWidth, newCanvasHeight);
// Clear canvas
canvas2D.clear();
// Apply pixel art optimization
canvas2D.applySmoothing();
// Draw background for each cell
for (let col = 0; col < props.columns; col++) {
for (let row = 0; row < rows; row++) {
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
// Draw cell background
canvas2D.fillCellBackground(cellX, cellY, maxWidth, maxHeight);
// Highlight the target cell if specified
if (highlightCell.value && highlightCell.value.col === col && highlightCell.value.row === row) {
canvas2D.fillRect(cellX, cellY, maxWidth, maxHeight, 'rgba(59, 130, 246, 0.2)');
}
}
}
// If showing all sprites, overlay all sprites from all visible layers ghosted in each cell
if (showAllSprites.value) {
const visibleLayers = props.layers.filter(l => l.visible);
const maxSprites = Math.max(...visibleLayers.map(l => l.sprites.length), 0);
for (let cellIndex = 0; cellIndex < maxSprites; cellIndex++) {
const cellCol = cellIndex % props.columns;
const cellRow = Math.floor(cellIndex / props.columns);
const cellX = Math.floor(cellCol * maxWidth);
const cellY = Math.floor(cellRow * maxHeight);
// Draw all sprites from all visible layers ghosted in this cell
visibleLayers.forEach(layer => {
layer.sprites.forEach((sprite, spriteIndex) => {
// Skip drawing the sprite that belongs in this cell (it's already drawn below)
if (spriteIndex === cellIndex) return;
// Skip the active sprite if we're dragging it
if (activeSpriteId.value === sprite.id && ghostSprite.value) return;
if (sprite && sprite.img) {
canvas2D.drawImage(sprite.img, cellX + negativeSpacing + sprite.x, cellY + negativeSpacing + sprite.y, 0.25);
}
});
});
}
}
// Draw layers in order; active layer will be interactable
props.layers.forEach(layer => {
if (!layer.visible) return;
layer.sprites.forEach((sprite, index) => {
// Skip the active sprite if we're showing a ghost instead
if (activeSpriteId.value === sprite.id && ghostSprite.value) return;
const col = index % props.columns;
const row = Math.floor(index / props.columns);
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
const alpha = layer.id === props.activeLayerId ? 1 : 0.85;
canvas2D.drawImage(sprite.img, cellX + negativeSpacing + sprite.x, cellY + negativeSpacing + sprite.y, alpha);
});
});
// Draw ghost sprite if we're dragging between cells
if (ghostSprite.value && activeSpriteId.value) {
const sprite = activeSprites.value.find(s => s.id === activeSpriteId.value);
if (sprite) {
canvas2D.drawImage(sprite.img, ghostSprite.value.x, ghostSprite.value.y, 0.6);
}
}
// Draw grid lines on top of everything
for (let col = 0; col < props.columns; col++) {
for (let row = 0; row < rows; row++) {
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
canvas2D.strokeGridCell(cellX, cellY, maxWidth, maxHeight);
}
}
}
// Track which images already have listeners
const imagesWithListeners = new WeakSet<HTMLImageElement>();
const attachImageListeners = () => {
const sprites = props.layers.flatMap(l => l.sprites);
canvas2D.attachImageListeners(sprites, handleForceRedraw, imagesWithListeners);
}; };
onMounted(() => { onMounted(() => {
canvas2D.initContext();
requestDraw();
// Attach listeners for current sprites
attachImageListeners();
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
// Hide context menu when clicking elsewhere // Hide context menu when clicking elsewhere
document.addEventListener('click', hideContextMenu); document.addEventListener('click', hideContextMenu);
}); });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('forceRedraw', handleForceRedraw);
document.removeEventListener('click', hideContextMenu); document.removeEventListener('click', hideContextMenu);
}); });
// Handler for force redraw event // Watch for background color changes
const handleForceRedraw = () => {
// Ensure integer positioning for crisp rendering on the active layer
canvas2D.ensureIntegerPositions(activeSprites.value);
canvas2D.applySmoothing();
requestDraw();
};
// Re-attach listeners and redraw whenever layers/sprites change
watch( watch(
() => props.layers, () => settingsStore.backgroundColor,
() => { async () => {
attachImageListeners(); await nextTick();
requestDraw(); }
},
{ deep: true }
); );
watch(() => props.columns, requestDraw);
watch(() => settingsStore.pixelPerfect, requestDraw);
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);
</script> </script>
<style scoped></style> <style scoped></style>

View File

@@ -2,21 +2,73 @@
<div class="spritesheet-preview w-full h-full"> <div class="spritesheet-preview w-full h-full">
<div class="flex flex-col lg:flex-row gap-4 h-full"> <div class="flex flex-col lg:flex-row gap-4 h-full">
<div class="flex-1 min-w-0 flex flex-col"> <div class="flex-1 min-w-0 flex flex-col">
<div class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto flex-1 min-h-[300px] max-h-[calc(100vh-12rem)] shadow-sm hover:shadow-md transition-shadow duration-200"> <div
<canvas class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto flex-1 min-h-[300px] max-h-[calc(100vh-12rem)] shadow-sm hover:shadow-md transition-shadow duration-200"
ref="previewCanvasRef" @mousemove="drag"
@mousedown="startDrag" @mouseup="stopDrag"
@mousemove="drag" @mouseleave="stopDrag"
@mouseup="stopDrag" @touchmove="handleTouchMove"
@mouseleave="stopDrag" @touchend="stopDrag"
@touchstart="handleTouchStart" >
@touchmove="handleTouchMove" <div
@touchend="stopDrag" ref="previewContainerRef"
class="block touch-manipulation" class="relative touch-manipulation inline-block"
:class="{ 'cursor-move': isDraggable }" :style="{
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left', ...(settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}) }" transform: `scale(${zoom})`,
transformOrigin: 'top left',
width: `${cellDimensions.cellWidth}px`,
height: `${cellDimensions.cellHeight}px`,
backgroundColor: '#f9fafb',
backgroundImage: getPreviewBackgroundImage(),
backgroundSize: settingsStore.backgroundColor === 'transparent' ? '20px 20px' : 'auto',
backgroundPosition: settingsStore.backgroundColor === 'transparent' ? '0 0, 0 10px, 10px -10px, -10px 0px' : '0 0',
border: `1px solid ${settingsStore.darkMode ? '#4b5563' : '#e5e7eb'}`,
}"
> >
</canvas> <!-- Background sprites (dimmed for comparison) -->
<template v-if="showAllSprites">
<template v-for="i in maxFrames()" :key="`bg-${i}`">
<template v-if="i !== currentFrameIndex && !hiddenFrames.includes(i)">
<template v-for="layer in getVisibleLayers()" :key="`${layer.id}-${i}`">
<img
v-if="layer.sprites[i]"
:src="layer.sprites[i].img.src"
class="absolute pointer-events-none"
:style="{
left: `${cellDimensions.negativeSpacing + layer.sprites[i].x}px`,
top: `${cellDimensions.negativeSpacing + layer.sprites[i].y}px`,
width: `${layer.sprites[i].width}px`,
height: `${layer.sprites[i].height}px`,
opacity: '0.3',
imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto',
}"
draggable="false"
/>
</template>
</template>
</template>
</template>
<!-- Current frame sprites -->
<template v-for="layer in getVisibleLayers()" :key="layer.id">
<img
v-if="layer.sprites[currentFrameIndex]"
:src="layer.sprites[currentFrameIndex].img.src"
class="absolute"
:class="{ 'cursor-move': isDraggable }"
:style="{
left: `${cellDimensions.negativeSpacing + layer.sprites[currentFrameIndex].x}px`,
top: `${cellDimensions.negativeSpacing + layer.sprites[currentFrameIndex].y}px`,
width: `${layer.sprites[currentFrameIndex].width}px`,
height: `${layer.sprites[currentFrameIndex].height}px`,
imageRendering: settingsStore.pixelPerfect ? 'pixelated' : 'auto',
}"
@mousedown="startDrag($event, layer.sprites[currentFrameIndex], layer.id)"
@touchstart="handleTouchStart($event, layer.sprites[currentFrameIndex], layer.id)"
draggable="false"
/>
</template>
</div>
<!-- Mobile zoom controls --> <!-- Mobile zoom controls -->
<div class="absolute bottom-3 right-3 flex space-x-2 lg:hidden bg-white/80 dark:bg-gray-800/80 p-2 rounded-lg shadow-md"> <div class="absolute bottom-3 right-3 flex space-x-2 lg:hidden bg-white/80 dark:bg-gray-800/80 p-2 rounded-lg shadow-md">
@@ -114,7 +166,7 @@
</label> </label>
<label class="flex items-center gap-2 cursor-pointer"> <label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" @change="drawPreviewCanvas" /> <input type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" />
<span class="text-sm dark:text-gray-200">Pixel perfect</span> <span class="text-sm dark:text-gray-200">Pixel perfect</span>
</label> </label>
</div> </div>
@@ -162,7 +214,6 @@
import { useSettingsStore } from '@/stores/useSettingsStore'; import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Layer, Sprite } from '@/types/sprites'; import type { Layer, Sprite } from '@/types/sprites';
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers'; import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom'; import { useZoom } from '@/composables/useZoom';
import { useAnimationFrames } from '@/composables/useAnimationFrames'; import { useAnimationFrames } from '@/composables/useAnimationFrames';
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing'; import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
@@ -178,14 +229,11 @@
(e: 'updateSpriteInLayer', layerId: string, spriteId: string, x: number, y: number): void; (e: 'updateSpriteInLayer', layerId: string, spriteId: string, x: number, y: number): void;
}>(); }>();
const previewCanvasRef = ref<HTMLCanvasElement | null>(null); const previewContainerRef = ref<HTMLDivElement | null>(null);
// Get settings from store // Get settings from store
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
// Initialize composables
const canvas2D = useCanvas2D(previewCanvasRef);
const { const {
zoom, zoom,
increase: increaseZoom, increase: increaseZoom,
@@ -208,7 +256,7 @@
} }
return frames; return frames;
}, },
onDraw: drawPreviewCanvas, onDraw: () => {}, // No longer needed for canvas drawing
}); });
// Preview state // Preview state
@@ -239,17 +287,8 @@
return layer.sprites[currentFrameIndex.value] || null; return layer.sprites[currentFrameIndex.value] || null;
}); });
// Dragging state // Computed cell dimensions
const isDragging = ref(false); const cellDimensions = computed(() => {
const activeSpriteId = ref<string | null>(null);
const dragStartX = ref(0);
const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
// Canvas drawing
const getCellDimensions = () => {
const visibleLayers = getVisibleLayers(); const visibleLayers = getVisibleLayers();
// If manual cell size is enabled, use manual values // If manual cell size is enabled, use manual values
if (settingsStore.manualCellSizeEnabled) { if (settingsStore.manualCellSizeEnabled) {
@@ -269,115 +308,68 @@
cellHeight: maxHeight + negativeSpacing, cellHeight: maxHeight + negativeSpacing,
negativeSpacing, negativeSpacing,
}; };
});
// Helper for background image (dark mode friendly)
const getPreviewBackgroundImage = () => {
if (settingsStore.backgroundColor === 'transparent' && settingsStore.checkerboardEnabled) {
const color = settingsStore.darkMode ? '#4b5563' : '#d1d5db';
return `linear-gradient(45deg, ${color} 25%, transparent 25%), linear-gradient(-45deg, ${color} 25%, transparent 25%), linear-gradient(45deg, transparent 75%, ${color} 75%), linear-gradient(-45deg, transparent 75%, ${color} 75%)`;
}
return 'none';
}; };
function drawPreviewCanvas() { // Dragging state
if (!previewCanvasRef.value || !canvas2D.ctx.value) return; const isDragging = ref(false);
const visibleLayers = getVisibleLayers(); const activeSpriteId = ref<string | null>(null);
if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) return; const activeLayerId = ref<string | null>(null);
const dragStartX = ref(0);
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Apply pixel art optimization const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
canvas2D.applySmoothing();
// Set canvas size to fit one sprite cell (expanded with negative spacing)
canvas2D.setCanvasSize(cellWidth, cellHeight);
// Clear canvas
canvas2D.clear();
// Draw grid background (cell)
canvas2D.fillRect(0, 0, cellWidth, cellHeight, '#f9fafb');
const frameIndex = currentFrameIndex.value;
if (showAllSprites.value) {
// When comparing sprites, show all frames from all visible layers (dimmed)
const len = maxFrames();
for (let i = 0; i < len; i++) {
if (i === frameIndex || hiddenFrames.value.includes(i)) continue;
visibleLayers.forEach(layer => {
const sprite = layer.sprites[i];
if (!sprite) return;
canvas2D.drawImage(sprite.img, negativeSpacing + sprite.x, negativeSpacing + sprite.y, 0.3);
});
}
}
// Always draw current frame from all visible layers
visibleLayers.forEach(layer => {
const sprite = layer.sprites[frameIndex];
if (!sprite) return;
canvas2D.drawImage(sprite.img, negativeSpacing + sprite.x, negativeSpacing + sprite.y);
});
// Draw cell border
canvas2D.strokeRect(0, 0, cellWidth, cellHeight, '#e5e7eb', 1);
}
// Drag functionality // Drag functionality
const startDrag = (event: MouseEvent) => { const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => {
if (!isDraggable.value || !previewCanvasRef.value) return; if (!isDraggable.value || !previewContainerRef.value) return;
const rect = previewCanvasRef.value.getBoundingClientRect(); const rect = previewContainerRef.value.getBoundingClientRect();
const scaleX = previewCanvasRef.value.width / (rect.width / zoom.value); const scaleX = previewContainerRef.value.offsetWidth / (rect.width / zoom.value);
const scaleY = previewCanvasRef.value.height / (rect.height / zoom.value); const scaleY = previewContainerRef.value.offsetHeight / (rect.height / zoom.value);
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX; const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY; const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
const { negativeSpacing } = getCellDimensions();
if (repositionAllLayers.value) { if (repositionAllLayers.value) {
// Check if click is on any sprite from any visible layer isDragging.value = true;
activeSpriteId.value = 'ALL_LAYERS'; // Special marker for all layers
dragStartX.value = mouseX;
dragStartY.value = mouseY;
// Store initial positions for all sprites in this frame from all visible layers
allSpritesPosBeforeDrag.value.clear();
const visibleLayers = getVisibleLayers(); const visibleLayers = getVisibleLayers();
for (const layer of visibleLayers) { visibleLayers.forEach(layer => {
const sprite = layer.sprites[currentFrameIndex.value]; const s = layer.sprites[currentFrameIndex.value];
if (!sprite) continue; if (s) {
allSpritesPosBeforeDrag.value.set(s.id, { x: s.x, y: s.y });
const spriteCanvasX = negativeSpacing + sprite.x;
const spriteCanvasY = negativeSpacing + sprite.y;
if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + sprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + sprite.height) {
isDragging.value = true;
activeSpriteId.value = 'ALL_LAYERS'; // Special marker for all layers
dragStartX.value = mouseX;
dragStartY.value = mouseY;
// Store initial positions for all sprites in this frame from all visible layers
allSpritesPosBeforeDrag.value.clear();
visibleLayers.forEach(layer => {
const s = layer.sprites[currentFrameIndex.value];
if (s) {
allSpritesPosBeforeDrag.value.set(s.id, { x: s.x, y: s.y });
}
});
return;
} }
} });
} else { } else {
// Only check active layer sprite isDragging.value = true;
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value]; activeSpriteId.value = sprite.id;
if (activeSprite) { activeLayerId.value = layerId;
const spriteCanvasX = negativeSpacing + activeSprite.x; dragStartX.value = mouseX;
const spriteCanvasY = negativeSpacing + activeSprite.y; dragStartY.value = mouseY;
if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + activeSprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + activeSprite.height) { spritePosBeforeDrag.value = { x: sprite.x, y: sprite.y };
isDragging.value = true;
activeSpriteId.value = activeSprite.id;
dragStartX.value = mouseX;
dragStartY.value = mouseY;
spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y };
}
}
} }
}; };
const drag = (event: MouseEvent) => { const drag = (event: MouseEvent) => {
if (!isDragging.value || !activeSpriteId.value || !previewCanvasRef.value) return; if (!isDragging.value || !activeSpriteId.value || !previewContainerRef.value) return;
const rect = previewCanvasRef.value.getBoundingClientRect(); const rect = previewContainerRef.value.getBoundingClientRect();
const scaleX = previewCanvasRef.value.width / (rect.width / zoom.value); const scaleX = previewContainerRef.value.offsetWidth / (rect.width / zoom.value);
const scaleY = previewCanvasRef.value.height / (rect.height / zoom.value); const scaleY = previewContainerRef.value.offsetHeight / (rect.height / zoom.value);
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX; const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY; const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
@@ -385,7 +377,7 @@
const deltaX = Math.round(mouseX - dragStartX.value); const deltaX = Math.round(mouseX - dragStartX.value);
const deltaY = Math.round(mouseY - dragStartY.value); const deltaY = Math.round(mouseY - dragStartY.value);
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
if (activeSpriteId.value === 'ALL_LAYERS') { if (activeSpriteId.value === 'ALL_LAYERS') {
// Move all sprites in current frame from all visible layers // Move all sprites in current frame from all visible layers
@@ -407,10 +399,9 @@
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY); emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
}); });
drawPreviewCanvas();
} else { } else {
// Move only the active layer sprite // Move only the active layer sprite
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value]; const activeSprite = props.layers.find(l => l.id === activeLayerId.value)?.sprites[currentFrameIndex.value];
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return; if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
// Calculate new position with constraints and round to integers // Calculate new position with constraints and round to integers
@@ -422,16 +413,16 @@
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY)); newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
emit('updateSprite', activeSpriteId.value, newX, newY); emit('updateSprite', activeSpriteId.value, newX, newY);
drawPreviewCanvas();
} }
}; };
const stopDrag = () => { const stopDrag = () => {
isDragging.value = false; isDragging.value = false;
activeSpriteId.value = null; activeSpriteId.value = null;
activeLayerId.value = null;
}; };
const handleTouchStart = (event: TouchEvent) => { const handleTouchStart = (event: TouchEvent, sprite: Sprite, layerId: string) => {
if (!isDraggable.value) return; if (!isDraggable.value) return;
if (event.touches.length === 1) { if (event.touches.length === 1) {
@@ -440,7 +431,7 @@
clientX: touch.clientX, clientX: touch.clientX,
clientY: touch.clientY, clientY: touch.clientY,
}); });
startDrag(mouseEvent); startDrag(mouseEvent, sprite, layerId);
} }
}; };
@@ -463,44 +454,25 @@
// Lifecycle hooks // Lifecycle hooks
onMounted(() => { onMounted(() => {
canvas2D.initContext(); // No longer need to initialize canvas or draw
drawPreviewCanvas();
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
}); });
onUnmounted(() => { onUnmounted(() => {
stopAnimation(); stopAnimation();
window.removeEventListener('forceRedraw', handleForceRedraw);
}); });
// Handler for force redraw event // Watchers - most canvas-related watchers removed
const handleForceRedraw = () => { // Keep layer watchers to ensure reactivity
const allSprites = props.layers.flatMap(l => l.sprites); watch(
canvas2D.ensureIntegerPositions(allSprites); () => props.layers,
canvas2D.applySmoothing(); () => {},
drawPreviewCanvas(); { deep: true }
}; );
watch(
// Watchers () => props.activeLayerId,
watch(() => props.layers, drawPreviewCanvas, { deep: true }); () => {}
watch(() => props.activeLayerId, drawPreviewCanvas); );
watch(currentFrameIndex, drawPreviewCanvas); watch(currentFrameIndex, () => {});
watch(zoom, drawPreviewCanvas);
watch(isDraggable, drawPreviewCanvas);
watch(showAllSprites, drawPreviewCanvas);
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)) {
drawPreviewCanvas();
}
</script> </script>
<style scoped> <style scoped>

View File

@@ -8,6 +8,7 @@ const backgroundColor = ref('transparent');
const manualCellSizeEnabled = ref(false); const manualCellSizeEnabled = ref(false);
const manualCellWidth = ref(64); const manualCellWidth = ref(64);
const manualCellHeight = ref(64); const manualCellHeight = ref(64);
const checkerboardEnabled = ref(true);
// Initialize dark mode from localStorage or system preference // Initialize dark mode from localStorage or system preference
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -81,6 +82,10 @@ export const useSettingsStore = defineStore('settings', () => {
setManualCellHeight(height); setManualCellHeight(height);
} }
function toggleCheckerboard() {
checkerboardEnabled.value = !checkerboardEnabled.value;
}
return { return {
pixelPerfect, pixelPerfect,
darkMode, darkMode,
@@ -89,6 +94,7 @@ export const useSettingsStore = defineStore('settings', () => {
manualCellSizeEnabled, manualCellSizeEnabled,
manualCellWidth, manualCellWidth,
manualCellHeight, manualCellHeight,
checkerboardEnabled,
togglePixelPerfect, togglePixelPerfect,
setPixelPerfect, setPixelPerfect,
toggleDarkMode, toggleDarkMode,
@@ -99,5 +105,6 @@ export const useSettingsStore = defineStore('settings', () => {
setManualCellWidth, setManualCellWidth,
setManualCellHeight, setManualCellHeight,
setManualCellSize, setManualCellSize,
toggleCheckerboard,
}; };
}); });