462 lines
21 KiB
Vue
462 lines
21 KiB
Vue
<template>
|
|
<div class="spritesheet-preview w-full">
|
|
<!-- Main Layout: Canvas Left, Controls Right -->
|
|
<div class="flex flex-col lg:flex-row gap-4">
|
|
<!-- Canvas Area (Left/Main) -->
|
|
<div class="flex-1 min-w-0">
|
|
<div class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto min-h-[300px] sm:min-h-[520px] shadow-sm hover:shadow-md transition-shadow duration-200">
|
|
<canvas
|
|
ref="previewCanvasRef"
|
|
@mousedown="startDrag"
|
|
@mousemove="drag"
|
|
@mouseup="stopDrag"
|
|
@mouseleave="stopDrag"
|
|
@touchstart="handleTouchStart"
|
|
@touchmove="handleTouchMove"
|
|
@touchend="stopDrag"
|
|
class="block touch-manipulation"
|
|
:class="{ 'cursor-move': isDraggable }"
|
|
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left', ...(settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}) }"
|
|
>
|
|
</canvas>
|
|
|
|
<!-- 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">
|
|
<button @click="increaseZoom" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
<button @click="decreaseZoom" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
|
|
<i class="fas fa-minus"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls Sidebar (Right) -->
|
|
<div class="lg:w-80 xl:w-96 flex-shrink-0">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 space-y-4">
|
|
<!-- Playback Controls -->
|
|
<div class="space-y-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Playback</h3>
|
|
<div class="flex items-center gap-2">
|
|
<button @click="togglePlayback" class="flex items-center justify-center gap-1.5 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition-colors cursor-pointer flex-1">
|
|
<span v-if="isPlaying" class="flex items-center gap-1.5">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
<path fill-rule="evenodd" d="M6.75 5.25a.75.75 0 01.75-.75H9a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H7.5a.75.75 0 01-.75-.75V5.25zm7.5 0A.75.75 0 0115 4.5h1.5a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H15a.75.75 0 01-.75-.75V5.25z" clip-rule="evenodd" />
|
|
</svg>
|
|
Pause
|
|
</span>
|
|
<span v-else class="flex items-center gap-1.5">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
|
|
<path fill-rule="evenodd" d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z" clip-rule="evenodd" />
|
|
</svg>
|
|
Play
|
|
</span>
|
|
</button>
|
|
<button @click="previousFrame" class="bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 p-2 rounded-md transition-colors duration-200 cursor-pointer" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 dark:text-gray-200">
|
|
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 01.75.75v12.06l4.72-4.72a.75.75 0 111.06 1.06l-6 6a.75.75 0 01-1.06 0l-6-6a.75.75 0 011.06-1.06l4.72 4.72V5.97a.75.75 0 01.75-.75z" clip-rule="evenodd" transform="rotate(90 12 12)" />
|
|
</svg>
|
|
</button>
|
|
<button @click="nextFrame" class="bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 p-2 rounded-md transition-colors duration-200 cursor-pointer" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 dark:text-gray-200">
|
|
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 01.75.75v12.06l4.72-4.72a.75.75 0 111.06 1.06l-6 6a.75.75 0 01-1.06 0l-6-6a.75.75 0 011.06-1.06l4.72 4.72V5.97a.75.75 0 01.75-.75z" clip-rule="evenodd" transform="rotate(-90 12 12)" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sliders -->
|
|
<div class="space-y-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Controls</h3>
|
|
|
|
<!-- Frame Navigation -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Frame</span>
|
|
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">{{ visibleFrameNumber }}/{{ visibleFramesCount }}</span>
|
|
</div>
|
|
<input type="range" min="0" :max="visibleFrames.length - 1" :value="visibleFrameIndex" @input="handleSliderInput" class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500" :disabled="isPlaying" :class="{ 'opacity-50': isPlaying }" />
|
|
</div>
|
|
|
|
<!-- FPS Control -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">FPS</span>
|
|
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">{{ fps }}</span>
|
|
</div>
|
|
<input type="range" min="1" max="60" v-model.number="fps" class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500" />
|
|
</div>
|
|
|
|
<!-- Zoom Control -->
|
|
<div class="space-y-1">
|
|
<div class="flex justify-between items-center">
|
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Zoom</span>
|
|
<span class="text-xs font-mono text-gray-700 dark:text-gray-300">{{ Math.round(zoom * 100) }}%</span>
|
|
</div>
|
|
<input type="range" min="0.5" max="5" step="0.1" v-model.number="zoom" class="w-full h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-blue-500" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Options -->
|
|
<div class="space-y-3">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Options</h3>
|
|
<div class="space-y-2">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" v-model="isDraggable" 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">Reposition</span>
|
|
</label>
|
|
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input type="checkbox" v-model="showAllSprites" 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">Compare sprites</span>
|
|
</label>
|
|
|
|
<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" />
|
|
<span class="text-sm dark:text-gray-200">Pixel perfect</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Current frame offset display -->
|
|
<div v-if="currentFrameSprite" class="p-2 bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-xs font-medium text-gray-600 dark:text-gray-400">Offset</span>
|
|
<span class="text-xs font-mono font-semibold text-cyan-600 dark:text-cyan-400">x: {{ currentFrameSprite.x }}, y: {{ currentFrameSprite.y }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Frame Selection (when Compare sprites is enabled) -->
|
|
<div v-if="showAllSprites" class="space-y-2">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Frames</h3>
|
|
<div class="flex gap-1">
|
|
<button @click="showAllFrames" class="px-2 py-1 text-xs bg-blue-500 hover:bg-blue-600 text-white rounded transition-colors">All</button>
|
|
<button @click="hideAllFrames" class="px-2 py-1 text-xs bg-gray-500 hover:bg-gray-600 text-white rounded transition-colors">None</button>
|
|
</div>
|
|
</div>
|
|
<div class="rounded-md border border-gray-300 dark:border-gray-600 dark:bg-gray-800">
|
|
<div class="max-h-[180px] overflow-y-auto">
|
|
<div class="space-y-0.5 p-1">
|
|
<div v-for="(sprite, index) in compositeFrames" :key="sprite.id" class="flex items-center gap-2 px-2 py-1.5 hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer rounded" @click="toggleHiddenFrame(index)">
|
|
<input type="checkbox" :checked="!hiddenFrames.includes(index)" class="w-3.5 h-3.5 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" @click.stop @change="toggleHiddenFrame(index)" />
|
|
<div class="w-8 h-8 bg-gray-100 dark:bg-gray-700 rounded flex items-center justify-center overflow-hidden flex-shrink-0">
|
|
<img :src="sprite.img.src" class="max-w-full max-h-full object-contain" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" />
|
|
</div>
|
|
<span class="text-xs dark:text-gray-200">{{ index + 1 }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
|
|
import { useSettingsStore } from '@/stores/useSettingsStore';
|
|
import type { Layer, Sprite } from '@/types/sprites';
|
|
import { getMaxDimensionsAcrossLayers } from '@/composables/useLayers';
|
|
import { useCanvas2D } from '@/composables/useCanvas2D';
|
|
import { useZoom } from '@/composables/useZoom';
|
|
import { useAnimationFrames } from '@/composables/useAnimationFrames';
|
|
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
|
|
|
const props = defineProps<{
|
|
layers: Layer[];
|
|
activeLayerId: string;
|
|
columns: number;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(e: 'updateSprite', id: string, x: number, y: number): void;
|
|
}>();
|
|
|
|
const previewCanvasRef = ref<HTMLCanvasElement | null>(null);
|
|
|
|
// Get settings from store
|
|
const settingsStore = useSettingsStore();
|
|
|
|
// Initialize composables
|
|
const canvas2D = useCanvas2D(previewCanvasRef);
|
|
|
|
const {
|
|
zoom,
|
|
increase: increaseZoom,
|
|
decrease: decreaseZoom,
|
|
} = useZoom({
|
|
allowedValues: [0.5, 1, 2, 3, 4, 5],
|
|
initial: 1,
|
|
});
|
|
|
|
const getVisibleLayers = () => props.layers.filter(l => l.visible);
|
|
const maxFrames = () => Math.max(0, ...getVisibleLayers().map(l => l.sprites.length));
|
|
|
|
const { currentFrameIndex, isPlaying, fps, hiddenFrames, visibleFrames, visibleFramesCount, visibleFrameIndex, visibleFrameNumber, togglePlayback, nextFrame, previousFrame, handleSliderInput, toggleHiddenFrame, showAllFrames, hideAllFrames, stopAnimation } = useAnimationFrames({
|
|
sprites: () => {
|
|
const len = maxFrames();
|
|
const frames: Sprite[] = [];
|
|
for (let i = 0; i < len; i++) {
|
|
const s = getVisibleLayers().find(l => l.sprites[i])?.sprites[i];
|
|
if (s) frames.push(s);
|
|
}
|
|
return frames;
|
|
},
|
|
onDraw: drawPreviewCanvas,
|
|
});
|
|
|
|
// Preview state
|
|
const isDraggable = ref(false);
|
|
const showAllSprites = ref(false);
|
|
|
|
const compositeFrames = computed<Sprite[]>(() => {
|
|
const v = getVisibleLayers();
|
|
const len = maxFrames();
|
|
const arr: Sprite[] = [];
|
|
for (let i = 0; i < len; i++) {
|
|
const s = v.find(l => l.sprites[i])?.sprites[i];
|
|
if (s) arr.push(s);
|
|
}
|
|
return arr;
|
|
});
|
|
|
|
const currentFrameSprite = computed<Sprite | null>(() => {
|
|
const layer = props.layers.find(l => l.id === props.activeLayerId);
|
|
if (!layer) return null;
|
|
return layer.sprites[currentFrameIndex.value] || null;
|
|
});
|
|
|
|
// Dragging state
|
|
const isDragging = ref(false);
|
|
const activeSpriteId = ref<string | null>(null);
|
|
const dragStartX = ref(0);
|
|
const dragStartY = ref(0);
|
|
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
|
|
|
|
// 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 { cellWidth, cellHeight, negativeSpacing } = getCellDimensions();
|
|
|
|
// Apply pixel art optimization
|
|
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) {
|
|
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);
|
|
});
|
|
}
|
|
}
|
|
|
|
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
|
|
const startDrag = (event: MouseEvent) => {
|
|
if (!isDraggable.value || !previewCanvasRef.value) return;
|
|
|
|
const rect = previewCanvasRef.value.getBoundingClientRect();
|
|
const scaleX = previewCanvasRef.value.width / (rect.width / zoom.value);
|
|
const scaleY = previewCanvasRef.value.height / (rect.height / zoom.value);
|
|
|
|
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
|
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
|
|
|
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
|
const { negativeSpacing } = getCellDimensions();
|
|
|
|
// Check if click is on sprite (accounting for negative spacing offset)
|
|
if (activeSprite) {
|
|
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 };
|
|
}
|
|
}
|
|
};
|
|
|
|
const drag = (event: MouseEvent) => {
|
|
if (!isDragging.value || !activeSpriteId.value || !previewCanvasRef.value) return;
|
|
|
|
const rect = previewCanvasRef.value.getBoundingClientRect();
|
|
const scaleX = previewCanvasRef.value.width / (rect.width / zoom.value);
|
|
const scaleY = previewCanvasRef.value.height / (rect.height / zoom.value);
|
|
|
|
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
|
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
|
|
|
const deltaX = Math.round(mouseX - dragStartX.value);
|
|
const deltaY = Math.round(mouseY - dragStartY.value);
|
|
|
|
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
|
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
|
|
|
|
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions();
|
|
|
|
// Calculate new position with constraints and round to integers
|
|
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
|
|
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
|
|
|
|
// Constrain movement within expanded cell (allow negative values up to -negativeSpacing)
|
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
|
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
|
|
|
|
emit('updateSprite', activeSpriteId.value, newX, newY);
|
|
drawPreviewCanvas();
|
|
};
|
|
|
|
const stopDrag = () => {
|
|
isDragging.value = false;
|
|
activeSpriteId.value = null;
|
|
};
|
|
|
|
const handleTouchStart = (event: TouchEvent) => {
|
|
if (!isDraggable.value) return;
|
|
|
|
if (event.touches.length === 1) {
|
|
const touch = event.touches[0];
|
|
const mouseEvent = new MouseEvent('mousedown', {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY,
|
|
});
|
|
startDrag(mouseEvent);
|
|
}
|
|
};
|
|
|
|
const handleTouchMove = (event: TouchEvent) => {
|
|
if (!isDraggable.value) return;
|
|
|
|
if (isDragging.value) {
|
|
event.preventDefault(); // Only prevent default when actually dragging
|
|
}
|
|
|
|
if (event.touches.length === 1) {
|
|
const touch = event.touches[0];
|
|
const mouseEvent = new MouseEvent('mousemove', {
|
|
clientX: touch.clientX,
|
|
clientY: touch.clientY,
|
|
});
|
|
drag(mouseEvent);
|
|
}
|
|
};
|
|
|
|
// Lifecycle hooks
|
|
onMounted(() => {
|
|
canvas2D.initContext();
|
|
drawPreviewCanvas();
|
|
|
|
// Listen for forceRedraw event from App.vue
|
|
window.addEventListener('forceRedraw', handleForceRedraw);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
stopAnimation();
|
|
window.removeEventListener('forceRedraw', handleForceRedraw);
|
|
});
|
|
|
|
// Handler for force redraw event
|
|
const handleForceRedraw = () => {
|
|
const allSprites = props.layers.flatMap(l => l.sprites);
|
|
canvas2D.ensureIntegerPositions(allSprites);
|
|
canvas2D.applySmoothing();
|
|
drawPreviewCanvas();
|
|
};
|
|
|
|
// Watchers
|
|
watch(() => props.layers, drawPreviewCanvas, { deep: true });
|
|
watch(currentFrameIndex, drawPreviewCanvas);
|
|
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>
|
|
|
|
<style scoped>
|
|
/* Custom styling for range inputs */
|
|
input[type='range']::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #3b82f6;
|
|
cursor: pointer;
|
|
}
|
|
|
|
input[type='range']::-moz-range-thumb {
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #3b82f6;
|
|
cursor: pointer;
|
|
border: none;
|
|
}
|
|
</style>
|