Continuation of separting logic into domain specific composables

This commit is contained in:
2025-11-18 20:11:36 +01:00
parent d571cb51cb
commit 404ca9ce88
7 changed files with 942 additions and 560 deletions

View File

@@ -48,7 +48,7 @@
@contextmenu.prevent
@dragover="handleDragOver"
@dragenter="handleDragEnter"
@dragleave="handleDragLeave"
@dragleave="onDragLeave"
@drop="handleDrop"
class="w-full transition-all"
:class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }"
@@ -95,16 +95,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed, onUnmounted } from 'vue';
import { ref, onMounted, watch, onUnmounted, toRef } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from '@/composables/useSprites';
interface CellPosition {
col: number;
row: number;
index: number;
}
import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom';
import { useDragSprite } from '@/composables/useDragSprite';
import { useFileDrop } from '@/composables/useFileDrop';
const props = defineProps<{
sprites: Sprite[];
@@ -124,22 +121,48 @@
const settingsStore = useSettingsStore();
const canvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
// State for tracking drag operations
const isDragging = ref(false);
const activeSpriteId = ref<string | null>(null);
const activeSpriteCellIndex = ref<number | null>(null);
const dragStartX = ref(0);
const dragStartY = ref(0);
const dragOffsetX = ref(0);
const dragOffsetY = ref(0);
// Initialize composables
const canvas2D = useCanvas2D(canvasRef);
const { zoom, increase: zoomIn, decrease: zoomOut, reset: resetZoom } = useZoom({
min: 0.5,
max: 3,
step: 0.25,
initial: 1,
});
const allowCellSwap = ref(false);
const currentHoverCell = ref<CellPosition | null>(null);
// Visual feedback refs
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
const highlightCell = ref<CellPosition | null>(null);
const {
isDragging,
activeSpriteId,
ghostSprite,
highlightCell,
spritePositions,
startDrag: dragStart,
drag: dragMove,
stopDrag,
handleTouchStart,
handleTouchMove,
findSpriteAtPosition,
calculateMaxDimensions,
} = useDragSprite({
sprites: toRef(props, 'sprites'),
columns: toRef(props, 'columns'),
zoom,
allowCellSwap,
getMousePosition: (event, z) => canvas2D.getMousePosition(event, z),
onUpdateSprite: (id, x, y) => emit('updateSprite', id, x, y),
onUpdateSpriteCell: (id, newIndex) => emit('updateSpriteCell', id, newIndex),
onDraw: drawCanvas,
});
const { isDragOver, handleDragOver, handleDragEnter, handleDragLeave, handleDrop } = useFileDrop({
sprites: props.sprites,
onAddSprite: (file) => emit('addSprite', file),
onAddSpriteWithResize: (file) => emit('addSpriteWithResize', file),
});
const showAllSprites = ref(false);
const showContextMenu = ref(false);
@@ -148,47 +171,7 @@
const contextMenuSpriteId = ref<string | null>(null);
const replacingSpriteId = ref<string | null>(null);
const fileInput = ref<HTMLInputElement | null>(null);
const isDragOver = ref(false);
const spritePositions = computed(() => {
const { maxWidth, maxHeight } = calculateMaxDimensions();
return props.sprites.map((sprite, index) => {
const col = index % props.columns;
const row = Math.floor(index / props.columns);
return {
id: sprite.id,
canvasX: col * maxWidth + sprite.x,
canvasY: row * maxHeight + sprite.y,
cellX: col * maxWidth,
cellY: row * maxHeight,
width: sprite.width,
height: sprite.height,
maxWidth,
maxHeight,
col,
row,
index,
x: sprite.x,
y: sprite.y,
};
});
});
// Cache last known max dimensions to avoid collapsing cells while images are loading
const lastMaxWidth = ref(1);
const lastMaxHeight = ref(1);
const calculateMaxDimensions = () => {
// Use shared utility for max dimensions, then apply local caching to avoid visual collapse
const base = getMaxDimensions(props.sprites);
const maxWidth = Math.max(1, base.maxWidth, lastMaxWidth.value);
const maxHeight = Math.max(1, base.maxHeight, lastMaxHeight.value);
lastMaxWidth.value = maxWidth;
lastMaxHeight.value = maxHeight;
return { maxWidth, maxHeight };
};
const startDrag = (event: MouseEvent) => {
if (!canvasRef.value) return;
@@ -199,14 +182,10 @@
// Handle right-click for context menu
if ('button' in event && (event as MouseEvent).button === 2) {
event.preventDefault();
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const pos = canvas2D.getMousePosition(event, zoom.value);
if (!pos) return;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const clickedSprite = findSpriteAtPosition(mouseX, mouseY);
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
contextMenuSpriteId.value = clickedSprite?.id || null;
contextMenuX.value = event.clientX;
contextMenuY.value = event.clientY;
@@ -217,167 +196,13 @@
// Ignore non-left mouse buttons (but allow touch-generated events without a button prop)
if ('button' in event && (event as MouseEvent).button !== 0) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const clickedSprite = findSpriteAtPosition(mouseX, mouseY);
if (clickedSprite) {
isDragging.value = true;
activeSpriteId.value = clickedSprite.id;
dragStartX.value = mouseX;
dragStartY.value = mouseY;
// Find the sprite's position to calculate offset from mouse to sprite origin
const spritePosition = spritePositions.value.find(pos => pos.id === clickedSprite.id);
if (spritePosition) {
dragOffsetX.value = mouseX - spritePosition.canvasX;
dragOffsetY.value = mouseY - spritePosition.canvasY;
activeSpriteCellIndex.value = spritePosition.index;
// Store the starting cell position
const startCell = findCellAtPosition(mouseX, mouseY);
if (startCell) {
currentHoverCell.value = startCell;
highlightCell.value = null; // No highlight at the start
}
}
}
};
const findCellAtPosition = (x: number, y: number): CellPosition | null => {
const { maxWidth, maxHeight } = calculateMaxDimensions();
const col = Math.floor(x / maxWidth);
const row = Math.floor(y / maxHeight);
// Check if the cell position is valid
const totalRows = Math.ceil(props.sprites.length / props.columns);
if (col >= 0 && col < props.columns && row >= 0 && row < totalRows) {
const index = row * props.columns + col;
if (index < props.sprites.length) {
return { col, row, index };
}
}
return null;
// Delegate to composable for actual drag handling
dragStart(event);
};
// Wrapper for drag move
const drag = (event: MouseEvent) => {
if (!isDragging.value || !activeSpriteId.value || !canvasRef.value || activeSpriteCellIndex.value === null) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const spriteIndex = props.sprites.findIndex(s => s.id === activeSpriteId.value);
if (spriteIndex === -1) return;
// Find the cell the mouse is currently over
const hoverCell = findCellAtPosition(mouseX, mouseY);
currentHoverCell.value = hoverCell;
if (allowCellSwap.value && hoverCell) {
// If we're hovering over a different cell than the sprite's current cell
if (hoverCell.index !== activeSpriteCellIndex.value) {
// Show a highlight for the target cell
highlightCell.value = hoverCell;
// Create a ghost sprite that follows the mouse
ghostSprite.value = {
id: activeSpriteId.value,
x: mouseX - dragOffsetX.value,
y: mouseY - dragOffsetY.value,
};
drawCanvas();
} else {
// Same cell as the sprite's origin, just do regular movement
highlightCell.value = null;
ghostSprite.value = null;
handleInCellMovement(mouseX, mouseY, spriteIndex);
}
} else {
// Regular in-cell movement
handleInCellMovement(mouseX, mouseY, spriteIndex);
}
};
const handleInCellMovement = (mouseX: number, mouseY: number, spriteIndex: number) => {
if (!activeSpriteId.value) return;
const position = spritePositions.value.find(pos => pos.id === activeSpriteId.value);
if (!position) return;
// Calculate new position based on mouse position and initial click offset
const newX = mouseX - position.cellX - dragOffsetX.value;
const newY = mouseY - position.cellY - dragOffsetY.value;
// Constrain within cell boundaries and ensure integer positions
const constrainedX = Math.floor(Math.max(0, Math.min(position.maxWidth - props.sprites[spriteIndex].width, newX)));
const constrainedY = Math.floor(Math.max(0, Math.min(position.maxHeight - props.sprites[spriteIndex].height, newY)));
emit('updateSprite', activeSpriteId.value, constrainedX, constrainedY);
drawCanvas();
};
const stopDrag = () => {
if (isDragging.value && allowCellSwap.value && activeSpriteId.value && activeSpriteCellIndex.value !== null && currentHoverCell.value && activeSpriteCellIndex.value !== currentHoverCell.value.index) {
// We've dragged from one cell to another
// Tell parent component to update the sprite's cell index
emit('updateSpriteCell', activeSpriteId.value, currentHoverCell.value.index);
// Also reset the sprite's position within the cell to 0,0
emit('updateSprite', activeSpriteId.value, 0, 0);
}
// Reset all drag state
isDragging.value = false;
activeSpriteId.value = null;
activeSpriteCellIndex.value = null;
currentHoverCell.value = null;
highlightCell.value = null;
ghostSprite.value = null;
// Redraw without highlights
drawCanvas();
};
// Add zoom functionality for mobile
const zoom = ref(1);
const minZoom = 0.5;
const maxZoom = 3;
const zoomStep = 0.25;
const zoomIn = () => {
zoom.value = Math.min(maxZoom, zoom.value + zoomStep);
};
const zoomOut = () => {
zoom.value = Math.max(minZoom, zoom.value - zoomStep);
};
const resetZoom = () => {
zoom.value = 1;
};
// Improved touch handling
const handleTouchStart = (event: TouchEvent) => {
// Don't prevent default to allow scrolling
if (event.touches.length === 1) {
if (!canvasRef.value) return;
const touch = event.touches[0];
const mouseEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
preventDefault: () => {},
} as unknown as MouseEvent;
startDrag(mouseEvent);
}
dragMove(event);
};
const removeSprite = () => {
@@ -434,132 +259,25 @@
contextMenuSpriteId.value = null;
};
// Drag and drop handlers
const handleDragOver = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) {
event.dataTransfer.dropEffect = 'copy';
}
isDragOver.value = true;
// Wrapper for drag leave to pass canvasRef
const onDragLeave = (event: DragEvent) => {
handleDragLeave(event, canvasRef.value);
};
const handleDragEnter = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
isDragOver.value = true;
};
const handleDragLeave = (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
// Only set to false if we're leaving the canvas entirely
const rect = canvasRef.value?.getBoundingClientRect();
if (rect && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) {
isDragOver.value = false;
}
};
const handleDrop = async (event: DragEvent) => {
event.preventDefault();
event.stopPropagation();
isDragOver.value = false;
if (!event.dataTransfer?.files || event.dataTransfer.files.length === 0) {
return;
}
const files = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('image/'));
if (files.length === 0) {
alert('Please drop image files only.');
return;
}
// Process each dropped file
for (const file of files) {
await processDroppedImage(file);
}
};
const processDroppedImage = (file: File): Promise<void> => {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = e => {
if (e.target?.result) {
img.src = e.target.result as string;
}
};
img.onload = () => {
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Check if the dropped image is larger than current cells
if (img.naturalWidth > maxWidth || img.naturalHeight > maxHeight) {
// Emit event with resize flag
emit('addSpriteWithResize', file);
} else {
// Normal add
emit('addSprite', file);
}
resolve();
};
img.onerror = () => {
console.error('Failed to load image:', file.name);
reject(new Error('Failed to load image'));
};
reader.readAsDataURL(file);
});
};
const handleTouchMove = (event: TouchEvent) => {
// Only prevent default when we're actually dragging
if (isDragging.value) {
event.preventDefault();
}
if (event.touches.length === 1) {
const touch = event.touches[0];
const mouseEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
preventDefault: () => {},
} as unknown as MouseEvent;
drag(mouseEvent);
}
};
const findSpriteAtPosition = (x: number, y: number) => {
// Search in reverse order to get the topmost sprite first
for (let i = spritePositions.value.length - 1; i >= 0; i--) {
const pos = spritePositions.value[i];
if (x >= pos.canvasX && x <= pos.canvasX + pos.width && y >= pos.canvasY && y <= pos.canvasY + pos.height) {
return props.sprites.find(s => s.id === pos.id) || null;
}
}
return null;
};
const drawCanvas = () => {
if (!canvasRef.value || !ctx.value) return;
function drawCanvas() {
if (!canvasRef.value || !canvas2D.ctx.value) return;
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Set canvas size
const rows = Math.max(1, Math.ceil(props.sprites.length / props.columns));
canvasRef.value.width = maxWidth * props.columns;
canvasRef.value.height = maxHeight * rows;
canvas2D.setCanvasSize(maxWidth * props.columns, maxHeight * rows);
// Clear canvas
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
canvas2D.clear();
// Disable image smoothing based on pixel perfect setting
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Apply pixel art optimization
canvas2D.applySmoothing();
// Draw background for each cell
for (let col = 0; col < props.columns; col++) {
@@ -568,13 +286,11 @@
const cellY = Math.floor(row * maxHeight);
// Draw cell background
ctx.value.fillStyle = settingsStore.darkMode ? '#1F2937' : '#f9fafb';
ctx.value.fillRect(cellX, cellY, maxWidth, maxHeight);
canvas2D.fillCellBackground(cellX, cellY, maxWidth, maxHeight);
// Highlight the target cell if specified
if (highlightCell.value && highlightCell.value.col === col && highlightCell.value.row === row) {
ctx.value.fillStyle = 'rgba(59, 130, 246, 0.2)'; // Light blue highlight
ctx.value.fillRect(cellX, cellY, maxWidth, maxHeight);
canvas2D.fillRect(cellX, cellY, maxWidth, maxHeight, 'rgba(59, 130, 246, 0.2)');
}
}
}
@@ -588,14 +304,11 @@
const cellY = Math.floor(cellRow * maxHeight);
// Draw all sprites with transparency in this cell
ctx.value.globalAlpha = 0.3;
props.sprites.forEach((sprite, spriteIndex) => {
if (spriteIndex !== cellIndex) {
// Don't draw the cell's own sprite with transparency
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
canvas2D.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y, 0.3);
}
});
ctx.value.globalAlpha = 1.0;
}
}
@@ -612,58 +325,38 @@
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
// Draw sprite using integer positions for pixel-perfect rendering
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
// Draw sprite
canvas2D.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
});
// Draw ghost sprite if we're dragging between cells
if (ghostSprite.value && activeSpriteId.value) {
const sprite = props.sprites.find(s => s.id === activeSpriteId.value);
if (sprite) {
// Semi-transparent ghost
ctx.value.globalAlpha = 0.6;
ctx.value.drawImage(sprite.img, Math.floor(ghostSprite.value.x), Math.floor(ghostSprite.value.y));
ctx.value.globalAlpha = 1.0;
canvas2D.drawImage(sprite.img, ghostSprite.value.x, ghostSprite.value.y, 0.6);
}
}
// Draw grid lines on top of everything
ctx.value.strokeStyle = settingsStore.darkMode ? '#4B5563' : '#e5e7eb';
ctx.value.lineWidth = 1;
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 grid lines
ctx.value.strokeRect(cellX, cellY, maxWidth, maxHeight);
canvas2D.strokeGridCell(cellX, cellY, maxWidth, maxHeight);
}
}
};
}
// Track which images already have listeners
const imagesWithListeners = new WeakSet<HTMLImageElement>();
const attachImageListeners = () => {
props.sprites.forEach(sprite => {
const img = sprite.img as HTMLImageElement | undefined;
if (img && !imagesWithListeners.has(img)) {
imagesWithListeners.add(img);
if (!img.complete) {
// Redraw when the image loads or errors (to reflect updated dimensions)
img.addEventListener('load', handleForceRedraw, { once: true } as AddEventListenerOptions);
img.addEventListener('error', handleForceRedraw, { once: true } as AddEventListenerOptions);
}
}
});
canvas2D.attachImageListeners(props.sprites, handleForceRedraw, imagesWithListeners);
};
onMounted(() => {
if (canvasRef.value) {
ctx.value = canvasRef.value.getContext('2d');
drawCanvas();
}
canvas2D.initContext();
drawCanvas();
// Attach listeners for current sprites
attachImageListeners();
@@ -682,17 +375,9 @@
// Handler for force redraw event
const handleForceRedraw = () => {
// Ensure we're using integer positions for pixel-perfect rendering
props.sprites.forEach(sprite => {
sprite.x = Math.floor(sprite.x);
sprite.y = Math.floor(sprite.y);
});
// Force a redraw with the correct image smoothing settings
if (ctx.value) {
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
drawCanvas();
}
canvas2D.ensureIntegerPositions(props.sprites);
canvas2D.applySmoothing();
drawCanvas();
};
watch(

View File

@@ -157,10 +157,13 @@
</template>
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { useSettingsStore } from '@/stores/useSettingsStore';
import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from '@/composables/useSprites';
import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom';
import { useAnimationFrames } from '@/composables/useAnimationFrames';
const props = defineProps<{
sprites: Sprite[];
@@ -172,17 +175,43 @@
}>();
const previewCanvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | 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 {
currentFrameIndex,
isPlaying,
fps,
hiddenFrames,
visibleFrames,
visibleFramesCount,
visibleFrameIndex,
visibleFrameNumber,
togglePlayback,
nextFrame,
previousFrame,
handleSliderInput,
toggleHiddenFrame,
showAllFrames,
hideAllFrames,
stopAnimation,
} = useAnimationFrames({
sprites: props.sprites,
onDraw: drawPreviewCanvas,
});
// Preview state
const currentFrameIndex = ref(0);
const isPlaying = ref(false);
const fps = ref(12);
const zoom = ref(1);
const isDraggable = ref(false);
const showAllSprites = ref(false);
const animationFrameId = ref<number | null>(null);
const lastFrameTime = ref(0);
// Dragging state
const isDragging = ref(false);
@@ -191,128 +220,44 @@
const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Add this after other refs
const hiddenFrames = ref<number[]>([]);
// Get settings from store
const settingsStore = useSettingsStore();
// Add these computed properties
const visibleFrames = computed(() => props.sprites.filter((_, index) => !hiddenFrames.value.includes(index)));
const visibleFramesCount = computed(() => visibleFrames.value.length);
const visibleFrameIndex = computed(() => {
return visibleFrames.value.findIndex((_, idx) => idx === visibleFrames.value.findIndex(s => s === props.sprites[currentFrameIndex.value]));
});
const visibleFrameNumber = computed(() => visibleFrameIndex.value + 1);
// Canvas drawing
const drawPreviewCanvas = () => {
if (!previewCanvasRef.value || !ctx.value || props.sprites.length === 0) return;
function drawPreviewCanvas() {
if (!previewCanvasRef.value || !canvas2D.ctx.value || props.sprites.length === 0) return;
const currentSprite = props.sprites[currentFrameIndex.value];
if (!currentSprite) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
// Apply pixel art optimization consistently from store
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
// Apply pixel art optimization
canvas2D.applySmoothing();
// Set canvas size to just fit one sprite cell
previewCanvasRef.value.width = maxWidth;
previewCanvasRef.value.height = maxHeight;
canvas2D.setCanvasSize(maxWidth, maxHeight);
// Clear canvas
ctx.value.clearRect(0, 0, previewCanvasRef.value.width, previewCanvasRef.value.height);
canvas2D.clear();
// Draw grid background (cell)
ctx.value.fillStyle = '#f9fafb';
ctx.value.fillRect(0, 0, maxWidth, maxHeight);
// Keep pixel art optimization consistent throughout all drawing operations
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
canvas2D.fillRect(0, 0, maxWidth, maxHeight, '#f9fafb');
// Draw all sprites with transparency if enabled
if (showAllSprites.value && props.sprites.length > 1) {
ctx.value.globalAlpha = 0.3;
props.sprites.forEach((sprite, index) => {
if (index !== currentFrameIndex.value && !hiddenFrames.value.includes(index)) {
// Use Math.floor for pixel-perfect positioning
ctx.value?.drawImage(sprite.img, Math.floor(sprite.x), Math.floor(sprite.y));
canvas2D.drawImage(sprite.img, sprite.x, sprite.y, 0.3);
}
});
ctx.value.globalAlpha = 1.0;
}
// Draw current sprite with integer positions for pixel-perfect rendering
ctx.value.drawImage(currentSprite.img, Math.floor(currentSprite.x), Math.floor(currentSprite.y));
// Draw current sprite
canvas2D.drawImage(currentSprite.img, currentSprite.x, currentSprite.y);
// Draw cell border
ctx.value.strokeStyle = '#e5e7eb';
ctx.value.lineWidth = 1;
ctx.value.strokeRect(0, 0, maxWidth, maxHeight);
};
canvas2D.strokeRect(0, 0, maxWidth, maxHeight, '#e5e7eb', 1);
}
// Animation control
const togglePlayback = () => {
isPlaying.value = !isPlaying.value;
if (isPlaying.value) {
startAnimation();
} else {
stopAnimation();
}
};
const startAnimation = () => {
lastFrameTime.value = performance.now();
animateFrame();
};
const stopAnimation = () => {
if (animationFrameId.value !== null) {
cancelAnimationFrame(animationFrameId.value);
animationFrameId.value = null;
}
};
const animateFrame = () => {
const now = performance.now();
const elapsed = now - lastFrameTime.value;
const frameTime = 1000 / fps.value;
if (elapsed >= frameTime) {
lastFrameTime.value = now - (elapsed % frameTime);
nextFrame();
}
animationFrameId.value = requestAnimationFrame(animateFrame);
};
const nextFrame = () => {
if (visibleFrames.value.length === 0) return;
const currentVisibleIndex = visibleFrameIndex.value;
const nextVisibleIndex = (currentVisibleIndex + 1) % visibleFrames.value.length;
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[nextVisibleIndex]);
drawPreviewCanvas();
};
const previousFrame = () => {
if (visibleFrames.value.length === 0) return;
const currentVisibleIndex = visibleFrameIndex.value;
const prevVisibleIndex = (currentVisibleIndex - 1 + visibleFrames.value.length) % visibleFrames.value.length;
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[prevVisibleIndex]);
drawPreviewCanvas();
};
// Add this method to handle slider input
const handleSliderInput = (event: Event) => {
const target = event.target as HTMLInputElement;
const index = parseInt(target.value);
currentFrameIndex.value = props.sprites.indexOf(visibleFrames.value[index]);
};
// Drag functionality
const startDrag = (event: MouseEvent) => {
@@ -372,22 +317,6 @@
activeSpriteId.value = null;
};
// Add helper methods for mobile zoom controls
const increaseZoom = () => {
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex < zoomValues.length - 1) {
zoom.value = zoomValues[currentIndex + 1];
}
};
const decreaseZoom = () => {
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex > 0) {
zoom.value = zoomValues[currentIndex - 1];
}
};
const handleTouchStart = (event: TouchEvent) => {
if (!isDraggable.value) return;
@@ -421,10 +350,8 @@
// Lifecycle hooks
onMounted(() => {
if (previewCanvasRef.value) {
ctx.value = previewCanvasRef.value.getContext('2d');
drawPreviewCanvas();
}
canvas2D.initContext();
drawPreviewCanvas();
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
@@ -437,17 +364,9 @@
// Handler for force redraw event
const handleForceRedraw = () => {
// Ensure we're using integer positions for pixel-perfect rendering
props.sprites.forEach(sprite => {
sprite.x = Math.floor(sprite.x);
sprite.y = Math.floor(sprite.y);
});
// Force a redraw with the correct image smoothing settings
if (ctx.value) {
ctx.value.imageSmoothingEnabled = !settingsStore.pixelPerfect;
drawPreviewCanvas();
}
canvas2D.ensureIntegerPositions(props.sprites);
canvas2D.applySmoothing();
drawPreviewCanvas();
};
// Watchers
@@ -464,41 +383,6 @@
drawPreviewCanvas();
}
const toggleHiddenFrame = (index: number) => {
const currentIndex = hiddenFrames.value.indexOf(index);
if (currentIndex === -1) {
// Adding to hidden frames
hiddenFrames.value.push(index);
// If we're hiding the current frame, switch to the next visible frame
if (index === currentFrameIndex.value) {
const nextVisibleSprite = props.sprites.findIndex((_, i) => i !== index && !hiddenFrames.value.includes(i));
if (nextVisibleSprite !== -1) {
currentFrameIndex.value = nextVisibleSprite;
}
}
} else {
// Removing from hidden frames
hiddenFrames.value.splice(currentIndex, 1);
}
// Force a redraw
drawPreviewCanvas();
};
const showAllFrames = () => {
hiddenFrames.value = [];
drawPreviewCanvas();
};
const hideAllFrames = () => {
hiddenFrames.value = props.sprites.map((_, index) => index);
// Keep at least one frame visible
if (hiddenFrames.value.length > 0) {
hiddenFrames.value.splice(currentFrameIndex.value, 1);
}
drawPreviewCanvas();
};
</script>
<style scoped>