This commit is contained in:
2025-11-22 02:52:36 +01:00
parent 5cc4eb8731
commit 097df1f5de
8 changed files with 726 additions and 198 deletions

View File

@@ -120,10 +120,10 @@
</div>
<!-- Current frame offset display -->
<div v-if="props.sprites[currentFrameIndex]" class="p-2 bg-gray-100 dark:bg-gray-700 rounded-md border border-gray-200 dark:border-gray-600">
<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: {{ props.sprites[currentFrameIndex].x }}, y: {{ props.sprites[currentFrameIndex].y }}</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>
@@ -139,7 +139,7 @@
<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 props.sprites" :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)">
<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' } : {}" />
@@ -157,17 +157,18 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from '@/composables/useSprites';
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<{
sprites: Sprite[];
layers: Layer[];
activeLayerId: string;
columns: number;
}>();
@@ -192,8 +193,19 @@
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: () => props.sprites,
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,
});
@@ -201,6 +213,23 @@
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);
@@ -211,13 +240,13 @@
// Canvas drawing
function drawPreviewCanvas() {
if (!previewCanvasRef.value || !canvas2D.ctx.value || props.sprites.length === 0) return;
if (!previewCanvasRef.value || !canvas2D.ctx.value) return;
const visibleLayers = getVisibleLayers();
if (!visibleLayers.length || !visibleLayers.some(l => l.sprites.length)) return;
const currentSprite = props.sprites[currentFrameIndex.value];
if (!currentSprite) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
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;
@@ -233,17 +262,25 @@
// Draw grid background (cell)
canvas2D.fillRect(0, 0, cellWidth, cellHeight, '#f9fafb');
// Draw all sprites with transparency if enabled
if (showAllSprites.value && props.sprites.length > 1) {
props.sprites.forEach((sprite, index) => {
if (index !== currentFrameIndex.value && !hiddenFrames.value.includes(index)) {
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);
}
});
});
}
}
// Draw current sprite with negative spacing offset
canvas2D.drawImage(currentSprite.img, negativeSpacing + currentSprite.x, negativeSpacing + currentSprite.y);
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);
@@ -260,18 +297,22 @@
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
const sprite = props.sprites[currentFrameIndex.value];
const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
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);
// Check if click is on sprite (accounting for negative spacing offset)
const spriteCanvasX = negativeSpacing + sprite.x;
const spriteCanvasY = negativeSpacing + sprite.y;
if (sprite && mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + sprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + sprite.height) {
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 = sprite.id;
activeSpriteId.value = activeSprite.id;
dragStartX.value = mouseX;
dragStartY.value = mouseY;
spritePosBeforeDrag.value = { x: sprite.x, y: sprite.y };
spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y };
}
}
};
@@ -288,11 +329,13 @@
const deltaX = Math.round(mouseX - dragStartX.value);
const deltaY = Math.round(mouseY - dragStartY.value);
const sprite = props.sprites[currentFrameIndex.value];
if (!sprite || sprite.id !== activeSpriteId.value) return;
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
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;
@@ -301,8 +344,8 @@
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 - sprite.width, newX));
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
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();
@@ -359,13 +402,14 @@
// Handler for force redraw event
const handleForceRedraw = () => {
canvas2D.ensureIntegerPositions(props.sprites);
const allSprites = props.layers.flatMap(l => l.sprites);
canvas2D.ensureIntegerPositions(allSprites);
canvas2D.applySmoothing();
drawPreviewCanvas();
};
// Watchers
watch(() => props.sprites, drawPreviewCanvas, { deep: true });
watch(() => props.layers, drawPreviewCanvas, { deep: true });
watch(currentFrameIndex, drawPreviewCanvas);
watch(zoom, drawPreviewCanvas);
watch(isDraggable, drawPreviewCanvas);
@@ -375,7 +419,7 @@
watch(() => settingsStore.negativeSpacingEnabled, drawPreviewCanvas);
// Initial draw
if (props.sprites.length > 0) {
if (props.layers.some(l => l.sprites.length > 0)) {
drawPreviewCanvas();
}
</script>