[FEAT] Canvas > IMG
This commit is contained in:
@@ -2,21 +2,66 @@
|
||||
<div class="spritesheet-preview w-full 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="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">
|
||||
<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' } : {}) }"
|
||||
<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" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchmove="handleTouchMove" @touchend="stopDrag">
|
||||
<div
|
||||
ref="previewContainerRef"
|
||||
class="relative touch-manipulation inline-block"
|
||||
:style="{
|
||||
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 -->
|
||||
<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 +159,7 @@
|
||||
</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" />
|
||||
<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>
|
||||
</label>
|
||||
</div>
|
||||
@@ -162,7 +207,6 @@
|
||||
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';
|
||||
@@ -178,14 +222,11 @@
|
||||
(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
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
// Initialize composables
|
||||
const canvas2D = useCanvas2D(previewCanvasRef);
|
||||
|
||||
const {
|
||||
zoom,
|
||||
increase: increaseZoom,
|
||||
@@ -208,7 +249,7 @@
|
||||
}
|
||||
return frames;
|
||||
},
|
||||
onDraw: drawPreviewCanvas,
|
||||
onDraw: () => {}, // No longer needed for canvas drawing
|
||||
});
|
||||
|
||||
// Preview state
|
||||
@@ -239,17 +280,8 @@
|
||||
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 });
|
||||
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
|
||||
|
||||
// Canvas drawing
|
||||
|
||||
const getCellDimensions = () => {
|
||||
// Computed cell dimensions
|
||||
const cellDimensions = computed(() => {
|
||||
const visibleLayers = getVisibleLayers();
|
||||
// If manual cell size is enabled, use manual values
|
||||
if (settingsStore.manualCellSizeEnabled) {
|
||||
@@ -269,115 +301,68 @@
|
||||
cellHeight: maxHeight + negativeSpacing,
|
||||
negativeSpacing,
|
||||
};
|
||||
});
|
||||
|
||||
// Helper for background image (dark mode friendly)
|
||||
const getPreviewBackgroundImage = () => {
|
||||
if (settingsStore.backgroundColor === 'transparent') {
|
||||
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() {
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
// Dragging state
|
||||
const isDragging = ref(false);
|
||||
const activeSpriteId = ref<string | null>(null);
|
||||
const activeLayerId = 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());
|
||||
|
||||
// Drag functionality
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
if (!isDraggable.value || !previewCanvasRef.value) return;
|
||||
const startDrag = (event: MouseEvent, sprite: Sprite, layerId: string) => {
|
||||
if (!isDraggable.value || !previewContainerRef.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 rect = previewContainerRef.value.getBoundingClientRect();
|
||||
const scaleX = previewContainerRef.value.offsetWidth / (rect.width / zoom.value);
|
||||
const scaleY = previewContainerRef.value.offsetHeight / (rect.height / zoom.value);
|
||||
|
||||
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
||||
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
||||
|
||||
const { negativeSpacing } = getCellDimensions();
|
||||
|
||||
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();
|
||||
for (const layer of visibleLayers) {
|
||||
const sprite = layer.sprites[currentFrameIndex.value];
|
||||
if (!sprite) continue;
|
||||
|
||||
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;
|
||||
visibleLayers.forEach(layer => {
|
||||
const s = layer.sprites[currentFrameIndex.value];
|
||||
if (s) {
|
||||
allSpritesPosBeforeDrag.value.set(s.id, { x: s.x, y: s.y });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Only check active layer sprite
|
||||
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
||||
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 };
|
||||
}
|
||||
}
|
||||
isDragging.value = true;
|
||||
activeSpriteId.value = sprite.id;
|
||||
activeLayerId.value = layerId;
|
||||
dragStartX.value = mouseX;
|
||||
dragStartY.value = mouseY;
|
||||
spritePosBeforeDrag.value = { x: sprite.x, y: sprite.y };
|
||||
}
|
||||
};
|
||||
|
||||
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 scaleX = previewCanvasRef.value.width / (rect.width / zoom.value);
|
||||
const scaleY = previewCanvasRef.value.height / (rect.height / zoom.value);
|
||||
const rect = previewContainerRef.value.getBoundingClientRect();
|
||||
const scaleX = previewContainerRef.value.offsetWidth / (rect.width / zoom.value);
|
||||
const scaleY = previewContainerRef.value.offsetHeight / (rect.height / zoom.value);
|
||||
|
||||
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
||||
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
||||
@@ -385,7 +370,7 @@
|
||||
const deltaX = Math.round(mouseX - dragStartX.value);
|
||||
const deltaY = Math.round(mouseY - dragStartY.value);
|
||||
|
||||
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions();
|
||||
const { cellWidth, cellHeight, negativeSpacing } = cellDimensions.value;
|
||||
|
||||
if (activeSpriteId.value === 'ALL_LAYERS') {
|
||||
// Move all sprites in current frame from all visible layers
|
||||
@@ -407,10 +392,9 @@
|
||||
|
||||
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
||||
});
|
||||
drawPreviewCanvas();
|
||||
} else {
|
||||
// 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;
|
||||
|
||||
// Calculate new position with constraints and round to integers
|
||||
@@ -422,16 +406,16 @@
|
||||
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;
|
||||
activeLayerId.value = null;
|
||||
};
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
const handleTouchStart = (event: TouchEvent, sprite: Sprite, layerId: string) => {
|
||||
if (!isDraggable.value) return;
|
||||
|
||||
if (event.touches.length === 1) {
|
||||
@@ -440,7 +424,7 @@
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
});
|
||||
startDrag(mouseEvent);
|
||||
startDrag(mouseEvent, sprite, layerId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -463,44 +447,18 @@
|
||||
|
||||
// Lifecycle hooks
|
||||
onMounted(() => {
|
||||
canvas2D.initContext();
|
||||
drawPreviewCanvas();
|
||||
|
||||
// Listen for forceRedraw event from App.vue
|
||||
window.addEventListener('forceRedraw', handleForceRedraw);
|
||||
// No longer need to initialize canvas or draw
|
||||
});
|
||||
|
||||
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(() => props.activeLayerId, drawPreviewCanvas);
|
||||
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();
|
||||
}
|
||||
// Watchers - most canvas-related watchers removed
|
||||
// Keep layer watchers to ensure reactivity
|
||||
watch(() => props.layers, () => {}, { deep: true });
|
||||
watch(() => props.activeLayerId, () => {});
|
||||
watch(currentFrameIndex, () => {});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user