diff --git a/src/App.vue b/src/App.vue index f8658ac..bdb7e3d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,79 +1,117 @@ diff --git a/src/application/animationController.ts b/src/application/animationController.ts deleted file mode 100644 index f83137a..0000000 --- a/src/application/animationController.ts +++ /dev/null @@ -1,160 +0,0 @@ -import { sprites, animation, cellSize, previewBorder } from '@/application/state'; -import { getSpriteOffset } from '@/application/utilities'; - -/** - * Start the animation - */ -export function startAnimation() { - if (sprites.value.length === 0 || !animation.canvas) return; - - animation.isPlaying = true; - animation.lastFrameTime = performance.now(); - animation.manualUpdate = false; - - animation.canvas.width = cellSize.width; - animation.canvas.height = cellSize.height; - - // Start the animation loop without resetting sprite offset - animationLoop(); -} - -/** - * Stop the animation - */ -export function stopAnimation() { - animation.isPlaying = false; - - if (animation.animationId) { - cancelAnimationFrame(animation.animationId); - animation.animationId = null; - } -} - -/** - * Render a specific frame of the animation - */ -export function renderAnimationFrame(frameIndex: number, showAllSprites = false, spriteOffset = { x: 0, y: 0 }) { - if (sprites.value.length === 0 || !animation.canvas || !animation.ctx) return; - - // Resize the animation canvas to match the cell size if needed - if (animation.canvas.width !== cellSize.width || animation.canvas.height !== cellSize.height) { - animation.canvas.width = cellSize.width; - animation.canvas.height = cellSize.height; - } - - // Clear the canvas - animation.ctx.clearRect(0, 0, animation.canvas.width, animation.canvas.height); - - // Draw background (transparent by default) - animation.ctx.fillStyle = 'transparent'; - animation.ctx.fillRect(0, 0, animation.canvas.width, animation.canvas.height); - - // Keep pixel art sharp - animation.ctx.imageSmoothingEnabled = false; - - // Draw background sprites with reduced opacity if requested - if (showAllSprites && sprites.value.length > 1) { - renderBackgroundSprites(frameIndex); - } - - // Get the current sprite and render it - renderCurrentSprite(frameIndex, spriteOffset); - - // Draw border around the cell if enabled - renderPreviewBorder(); -} - -/** - * Draw all sprites except the current one with reduced opacity - */ -function renderBackgroundSprites(frameIndex: number) { - if (!animation.ctx) return; - - // Save the current context state - animation.ctx.save(); - - // Set global alpha for background sprites - animation.ctx.globalAlpha = 0.3; - - // Draw all sprites except the current one - sprites.value.forEach((sprite, index) => { - if (index !== frameIndex) { - const spriteCellX = Math.floor(sprite.x / cellSize.width); - const spriteCellY = Math.floor(sprite.y / cellSize.height); - - // Calculate precise offset for pixel-perfect rendering - const spriteOffsetX = Math.round(sprite.x - spriteCellX * cellSize.width); - const spriteOffsetY = Math.round(sprite.y - spriteCellY * cellSize.height); - - // Draw the sprite with transparency - animation.ctx?.drawImage(sprite.img, spriteOffsetX, spriteOffsetY); - } - }); - - // Restore the context to full opacity - animation.ctx.restore(); -} - -/** - * Render the current sprite at full opacity - */ -function renderCurrentSprite(frameIndex: number, spriteOffset: { x: number; y: number }) { - if (!animation.ctx) return; - - // Get the current sprite - const currentSprite = sprites.value[frameIndex % sprites.value.length]; - - // Draw the current sprite at full opacity at the specified position - animation.ctx.drawImage(currentSprite.img, spriteOffset.x, spriteOffset.y); -} - -/** - * Render a border around the animation preview if enabled - */ -function renderPreviewBorder() { - if (!animation.ctx || !animation.canvas || !previewBorder.enabled) return; - - animation.ctx.strokeStyle = previewBorder.color; - animation.ctx.lineWidth = previewBorder.width; - - // Use pixel-perfect coordinates for the border (0.5 offset for crisp lines) - const x = 0.5; - const y = 0.5; - const width = animation.canvas.width - 1; - const height = animation.canvas.height - 1; - - animation.ctx.strokeRect(x, y, width, height); -} - -/** - * Animation loop for continuous playback - */ -export function animationLoop(timestamp?: number) { - if (!animation.isPlaying) return; - - const currentTime = timestamp || performance.now(); - const elapsed = currentTime - animation.lastFrameTime; - const frameInterval = 1000 / animation.frameRate; - - if (elapsed >= frameInterval) { - animation.lastFrameTime = currentTime; - - if (sprites.value.length > 0) { - // Get the stored offset for the current frame - const frameOffset = getSpriteOffset(animation.currentFrame); - - // Render the current frame with its offset - renderAnimationFrame(animation.currentFrame, false, frameOffset); - - // Move to the next frame - animation.currentFrame = (animation.currentFrame + 1) % sprites.value.length; - - // Update the slider position if available - if (animation.slider) { - animation.slider.value = animation.currentFrame.toString(); - } - } - } - - animation.animationId = requestAnimationFrame(animationLoop); -} diff --git a/src/application/canvasOperations.ts b/src/application/canvasOperations.ts deleted file mode 100644 index 49e8f59..0000000 --- a/src/application/canvasOperations.ts +++ /dev/null @@ -1,332 +0,0 @@ -// canvasOperations.ts -import { sprites, canvas, ctx, cellSize, columns, zoomLevel, previewBorder } from '@/application/state'; -import { logger, getSpriteOffset, isImageReady, getPixelPerfectCoordinate, showNotification } from '@/application/utilities'; - -/** - * Update the canvas size based on sprites and cell size - */ -export function updateCanvasSize() { - if (!canvas.value) { - logger.warn('Canvas not available for size update'); - return; - } - - if (sprites.value.length === 0) { - return; - } - - try { - const totalSprites = sprites.value.length; - const cols = columns.value; - const rows = Math.ceil(totalSprites / cols); - - if (cellSize.width <= 0 || cellSize.height <= 0) { - logger.error('Invalid cell size for canvas update', cellSize); - return; - } - - const newWidth = cols * cellSize.width; - const newHeight = rows * cellSize.height; - - // Ensure the canvas is large enough to display all sprites - if (canvas.value.width !== newWidth || canvas.value.height !== newHeight) { - canvas.value.width = newWidth; - canvas.value.height = newHeight; - - // Emit an event to update the wrapper dimensions - window.dispatchEvent( - new CustomEvent('canvas-size-updated', { - detail: { width: newWidth, height: newHeight }, - }) - ); - } - } catch (error) { - logger.error('Error updating canvas size:', error); - } -} - -/** - * Draw the grid on the canvas - */ -export function drawGrid() { - if (!ctx.value || !canvas.value) return; - - ctx.value.strokeStyle = '#333'; - ctx.value.lineWidth = 1 / zoomLevel.value; // Adjust line width based on zoom level - - // Calculate the visible area based on zoom level - const visibleWidth = canvas.value.width / zoomLevel.value; - const visibleHeight = canvas.value.height / zoomLevel.value; - - // Draw vertical lines - ensure pixel-perfect grid lines - for (let x = 0; x <= visibleWidth; x += cellSize.width) { - const pixelX = getPixelPerfectCoordinate(x); - ctx.value.beginPath(); - ctx.value.moveTo(pixelX, 0); - ctx.value.lineTo(pixelX, visibleHeight); - ctx.value.stroke(); - } - - // Draw horizontal lines - ensure pixel-perfect grid lines - for (let y = 0; y <= visibleHeight; y += cellSize.height) { - const pixelY = getPixelPerfectCoordinate(y); - ctx.value.beginPath(); - ctx.value.moveTo(0, pixelY); - ctx.value.lineTo(visibleWidth, pixelY); - ctx.value.stroke(); - } -} - -/** - * Render the spritesheet preview on the canvas - */ -export function renderSpritesheetPreview(showGrid = true) { - if (!ctx.value || !canvas.value) { - logger.error('Canvas or context not available for rendering, will retry when ready'); - setTimeout(() => { - if (ctx.value && canvas.value) { - renderSpritesheetPreview(showGrid); - } - }, 100); - return; - } - - if (sprites.value.length === 0) return; - - try { - // Make sure the canvas size is correct before rendering - updateCanvasSize(); - - // Clear the canvas - ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height); - - if (showGrid) { - drawGrid(); - } - - // First, collect all occupied cells - const occupiedCells = new Set(); - sprites.value.forEach(sprite => { - const cellX = Math.floor(sprite.x / cellSize.width); - const cellY = Math.floor(sprite.y / cellSize.height); - occupiedCells.add(`${cellX},${cellY}`); - }); - - // Draw each sprite - remove the zoom scaling from context - sprites.value.forEach((sprite, index) => { - try { - if (!sprite.img) { - logger.warn(`Sprite at index ${index} has no image, skipping render`); - return; - } - - // Calculate the cell coordinates for this sprite - const cellX = Math.floor(sprite.x / cellSize.width); - const cellY = Math.floor(sprite.y / cellSize.height); - - // Calculate cell boundaries - const cellLeft = cellX * cellSize.width; - const cellTop = cellY * cellSize.height; - - // Get the frame-specific offset for this sprite - const frameOffset = getSpriteOffset(index); - - // Calculate the maximum allowed offset based on sprite and cell size - const maxOffsetX = Math.max(0, cellSize.width - sprite.width); - const maxOffsetY = Math.max(0, cellSize.height - sprite.height); - - // Constrain the offset to prevent out-of-bounds positioning - const constrainedOffsetX = Math.max(0, Math.min(maxOffsetX, frameOffset.x)); - const constrainedOffsetY = Math.max(0, Math.min(maxOffsetY, frameOffset.y)); - - // Calculate final position ensuring sprite stays within cell bounds - const finalX = Math.max(cellLeft, Math.min(cellLeft + maxOffsetX, cellLeft + constrainedOffsetX)); - const finalY = Math.max(cellTop, Math.min(cellTop + maxOffsetY, cellTop + constrainedOffsetY)); - - // Update sprite position to stay within bounds - sprite.x = finalX; - sprite.y = finalY; - - // Update the frame offset with the constrained values - frameOffset.x = finalX - cellLeft; - frameOffset.y = finalY - cellTop; - - // Draw the image at its final position with pixel-perfect rendering - if (isImageReady(sprite.img)) { - ctx.value.imageSmoothingEnabled = false; // Keep pixel art sharp - ctx.value.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height); - } else { - logger.warn(`Sprite image ${index} not fully loaded, setting onload handler`); - sprite.img.onload = () => { - if (ctx.value && canvas.value) { - renderSpriteOnCanvas(sprite, index); - } - }; - } - } catch (spriteError) { - logger.error(`Error rendering sprite at index ${index}:`, spriteError); - } - }); - - // Draw borders around occupied cells if enabled (preview only) - drawPreviewBorders(occupiedCells); - } catch (error) { - logger.error('Error in renderSpritesheetPreview:', error); - } -} - -/** - * Draw a single sprite on the canvas - */ -function renderSpriteOnCanvas(sprite: any, index: number) { - if (!ctx.value || !canvas.value) return; - - if (isImageReady(sprite.img)) { - // For pixel art, ensure we're drawing at exact pixel boundaries - const x = Math.round(sprite.x); - const y = Math.round(sprite.y); - - // Get the frame-specific offset for this sprite - const frameOffset = getSpriteOffset(index); - - // Calculate the maximum allowed offset based on sprite and cell size - const maxOffsetX = Math.max(0, cellSize.width - sprite.width); - const maxOffsetY = Math.max(0, cellSize.height - sprite.height); - - // Constrain the offset to prevent out-of-bounds positioning - const constrainedOffsetX = Math.max(0, Math.min(maxOffsetX, frameOffset.x)); - const constrainedOffsetY = Math.max(0, Math.min(maxOffsetY, frameOffset.y)); - - // Update the frame offset with the constrained values - frameOffset.x = constrainedOffsetX; - frameOffset.y = constrainedOffsetY; - - // Apply the constrained offset to the sprite position - const finalX = x + constrainedOffsetX; - const finalY = y + constrainedOffsetY; - - // Draw the image at its final position with pixel-perfect rendering - ctx.value.imageSmoothingEnabled = false; // Keep pixel art sharp - ctx.value.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height); - } else { - logger.warn(`Sprite image ${index} not fully loaded, setting onload handler`); - sprite.img.onload = () => { - if (ctx.value && canvas.value) { - renderSpriteOnCanvas(sprite, index); - } - }; - } -} - -/** - * Draw preview borders around occupied cells if enabled - */ -function drawPreviewBorders(occupiedCells: Set) { - if (!ctx.value || !canvas.value || !previewBorder.enabled || occupiedCells.size === 0) return; - - ctx.value.strokeStyle = previewBorder.color; - ctx.value.lineWidth = previewBorder.width / zoomLevel.value; // Adjust for zoom - - // Draw borders around each occupied cell - occupiedCells.forEach(cellKey => { - const [cellX, cellY] = cellKey.split(',').map(Number); - - // Calculate pixel-perfect coordinates for the cell - // Add 0.5 to align with pixel boundaries for crisp lines - const x = getPixelPerfectCoordinate(cellX * cellSize.width); - const y = getPixelPerfectCoordinate(cellY * cellSize.height); - - // Adjust width and height to ensure the border is inside the cell - const width = cellSize.width - 1; - const height = cellSize.height - 1; - - ctx.value!.strokeRect(x, y, width, height); - }); -} - -/** - * Download the spritesheet as a PNG - */ -export function downloadSpritesheet() { - if (sprites.value.length === 0 || !canvas.value) { - showNotification('No sprites to download', 'error'); - return; - } - - const tempCanvas = document.createElement('canvas'); - tempCanvas.width = canvas.value.width; - tempCanvas.height = canvas.value.height; - const tempCtx = tempCanvas.getContext('2d'); - - if (!tempCtx) { - showNotification('Failed to create download context', 'error'); - return; - } - - tempCtx.clearRect(0, 0, tempCanvas.width, tempCanvas.height); - - // Ensure pixel art remains sharp in the downloaded file - tempCtx.imageSmoothingEnabled = false; - - sprites.value.forEach((sprite, index) => { - // Get the frame-specific offset for this sprite - const frameOffset = getSpriteOffset(index); - - // Calculate the cell coordinates for this sprite - const cellX = Math.floor(sprite.x / cellSize.width); - const cellY = Math.floor(sprite.y / cellSize.height); - - // Calculate the base position within the cell - const baseX = cellX * cellSize.width; - const baseY = cellY * cellSize.height; - - // Calculate the maximum allowed offset based on sprite and cell size - // This prevents sprites from going out of bounds - const maxOffsetX = Math.max(0, cellSize.width - sprite.width); - const maxOffsetY = Math.max(0, cellSize.height - sprite.height); - - // Constrain the offset to prevent out-of-bounds positioning - const constrainedOffsetX = Math.max(0, Math.min(maxOffsetX, frameOffset.x)); - const constrainedOffsetY = Math.max(0, Math.min(maxOffsetY, frameOffset.y)); - - // Apply the constrained offset to the base position - const finalX = baseX + constrainedOffsetX; - const finalY = baseY + constrainedOffsetY; - - // Draw the sprite at the calculated position - tempCtx.drawImage(sprite.img, finalX, finalY, sprite.width, sprite.height); - }); - - const link = document.createElement('a'); - link.download = 'spritesheet.png'; - link.href = tempCanvas.toDataURL('image/png'); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - showNotification('Spritesheet downloaded successfully'); -} - -/** - * Zoom-related operations - */ -export function zoomIn() { - // Increase zoom level by 0.1, max 3.0 (300%) - zoomLevel.value = Math.min(3.0, zoomLevel.value + 0.1); - renderSpritesheetPreview(); - showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`); -} - -export function zoomOut() { - // Decrease zoom level by 0.1, min 0.5 (50%) - zoomLevel.value = Math.max(0.5, zoomLevel.value - 0.1); - renderSpritesheetPreview(); - showNotification(`Zoom: ${Math.round(zoomLevel.value * 100)}%`); -} - -export function resetZoom() { - // Reset to default zoom level (100%) - zoomLevel.value = 1; - renderSpritesheetPreview(); - showNotification('Zoom reset to 100%'); -} diff --git a/src/application/spriteOperations.ts b/src/application/spriteOperations.ts deleted file mode 100644 index 6552d6f..0000000 --- a/src/application/spriteOperations.ts +++ /dev/null @@ -1,241 +0,0 @@ -// spriteOperations.ts -import { type Sprite } from '@/application/types'; -import { sprites, cellSize, canvas, ctx, columns } from '@/application/state'; -import { logger, getSpriteOffset } from '@/application/utilities'; -import { updateCanvasSize, renderSpritesheetPreview } from '@/application/canvasOperations'; - -/** - * Add new sprites to the spritesheet - */ -export function addSprites(newSprites: Sprite[]) { - if (newSprites.length === 0) { - logger.warn('Attempted to add empty sprites array'); - return; - } - - try { - // Validate sprites before adding them - const validSprites = newSprites.filter(sprite => { - if (!sprite.img || sprite.width <= 0 || sprite.height <= 0) { - logger.error('Invalid sprite detected', sprite); - return false; - } - return true; - }); - - if (validSprites.length === 0) { - logger.error('No valid sprites to add'); - return; - } - - // Log the number of valid sprites being added - logger.info(`Adding ${validSprites.length} valid sprites`); - - sprites.value.push(...validSprites); - sprites.value.sort((a, b) => a.uploadOrder - b.uploadOrder); - - // Update cell size before arranging sprites - logger.info('Updating cell size after adding sprites'); - updateCellSize(); - - // Only auto-arrange if cell size is valid - if (cellSize && typeof cellSize.width === 'number' && typeof cellSize.height === 'number' && cellSize.width > 0 && cellSize.height > 0) { - logger.info('Auto-arranging sprites'); - autoArrangeSprites(); - } else { - logger.warn('Skipping auto-arrange due to invalid cell size'); - } - } catch (error) { - logger.error('Error adding sprites:', error); - } -} - -/** - * Update the cell size based on the largest sprite dimensions - */ -export function updateCellSize() { - if (sprites.value.length === 0) { - logger.warn('Cannot update cell size: no sprites available'); - return; - } - - try { - let maxWidth = 0; - let maxHeight = 0; - let validSpriteCount = 0; - - // Find the maximum dimensions across all sprites - sprites.value.forEach(sprite => { - if (!sprite.img || sprite.width <= 0 || sprite.height <= 0) { - logger.warn('Sprite with invalid dimensions detected', sprite); - return; - } - maxWidth = Math.max(maxWidth, sprite.width); - maxHeight = Math.max(maxHeight, sprite.height); - validSpriteCount++; - }); - - if (maxWidth === 0 || maxHeight === 0 || validSpriteCount === 0) { - logger.error('Failed to calculate valid cell size - no valid sprites found'); - return; - } - - // Add a small buffer to ensure sprites fit completely (optional) - const buffer = 0; // Increase if you want padding between sprites - - // Set cell size with validation - const newWidth = maxWidth + buffer; - const newHeight = maxHeight + buffer; - - if (newWidth <= 0 || newHeight <= 0) { - logger.error(`Invalid calculated cell dimensions: ${newWidth}x${newHeight}`); - return; - } - - logger.info(`Updating cell size to ${newWidth}x${newHeight}`); - cellSize.width = newWidth; - cellSize.height = newHeight; - - // Ensure all sprites are within their cell bounds after resize - sprites.value.forEach((sprite, index) => { - const column = index % columns.value; - const row = Math.floor(index / columns.value); - - // Calculate base position for the sprite's cell - const cellX = column * cellSize.width; - const cellY = row * cellSize.height; - - // Center the sprite within its cell if smaller than cell size - const offsetX = Math.floor((cellSize.width - sprite.width) / 2); - const offsetY = Math.floor((cellSize.height - sprite.height) / 2); - - sprite.x = cellX + offsetX; - sprite.y = cellY + offsetY; - }); - - updateCanvasSize(); - renderSpritesheetPreview(); - } catch (error) { - logger.error('Error updating cell size:', error); - } -} - -/** - * Automatically arrange sprites in a grid - */ -export function autoArrangeSprites() { - if (sprites.value.length === 0) { - logger.warn('No sprites to arrange'); - return; - } - - try { - // Ensure cell size is valid before proceeding - if (!cellSize || typeof cellSize.width !== 'number' || typeof cellSize.height !== 'number' || cellSize.width <= 0 || cellSize.height <= 0) { - logger.error('Invalid cell size for auto-arranging', cellSize); - - // Try to update cell size first - updateCellSize(); - - // Check again after update attempt - if (!cellSize || typeof cellSize.width !== 'number' || typeof cellSize.height !== 'number' || cellSize.width <= 0 || cellSize.height <= 0) { - logger.error('Still invalid cell size after update attempt', cellSize); - return; - } - - logger.warn('Cell size was updated and is now valid'); - } - - // First update the canvas size to ensure it's large enough - updateCanvasSize(); - - // Then position each sprite in its grid cell - sprites.value.forEach((sprite, index) => { - const column = index % columns.value; - const row = Math.floor(index / columns.value); - - sprite.x = column * cellSize.width; - sprite.y = row * cellSize.height; - }); - - // Check if the canvas is ready before attempting to render - if (!ctx.value || !canvas.value) { - logger.warn('Canvas or context not available for rendering after auto-arrange'); - return; - } - - renderSpritesheetPreview(); - } catch (error) { - logger.error('Error auto-arranging sprites:', error); - } -} - -/** - * Highlight a specific sprite by its ID - */ -export function highlightSprite(spriteId: string) { - if (!ctx.value || !canvas.value) return; - - const sprite = sprites.value.find(s => s.id === spriteId); - if (!sprite) return; - - // Calculate the cell coordinates - const cellX = Math.floor(sprite.x / cellSize.width); - const cellY = Math.floor(sprite.y / cellSize.height); - - // Briefly flash the cell - ctx.value.save(); - ctx.value.fillStyle = 'rgba(0, 150, 255, 0.3)'; - ctx.value.fillRect(cellX * cellSize.width, cellY * cellSize.height, cellSize.width, cellSize.height); - ctx.value.restore(); - - // Reset after a short delay - setTimeout(() => { - renderSpritesheetPreview(); - }, 500); -} - -/** - * Clear all sprites and reset the canvas - */ -export function clearAllSprites(animation: any) { - if (!canvas.value || !ctx.value) return; - - sprites.value = []; - canvas.value.width = 400; - canvas.value.height = 300; - ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height); - - if (animation.canvas && animation.ctx) { - animation.canvas.width = 200; - animation.canvas.height = 200; - animation.ctx.clearRect(0, 0, animation.canvas.width, animation.canvas.height); - } - - animation.currentFrame = 0; -} - -/** - * Apply frame-specific offsets to the main sprite positions - */ -export function applyOffsetsToMainView(currentSpriteOffset: any) { - sprites.value.forEach((sprite, index) => { - const frameOffset = getSpriteOffset(index); - if (frameOffset.x !== 0 || frameOffset.y !== 0) { - // Update the sprite's position to include the offset - sprite.x += frameOffset.x; - sprite.y += frameOffset.y; - - // Reset the offset - frameOffset.x = 0; - frameOffset.y = 0; - } - }); - - // Reset current offset - currentSpriteOffset.x = 0; - currentSpriteOffset.y = 0; - - // Re-render the main view - renderSpritesheetPreview(); -} diff --git a/src/application/state.ts b/src/application/state.ts deleted file mode 100644 index 3721dab..0000000 --- a/src/application/state.ts +++ /dev/null @@ -1,54 +0,0 @@ -// state.ts -import { ref, reactive } from 'vue'; -import { type Sprite, type CellSize, type AnimationState, type NotificationState, type PreviewBorderSettings } from '@/application/types'; - -// Core state -export const sprites = ref([]); -export const canvas = ref(null); -export const ctx = ref(null); -export const cellSize = reactive({ width: 0, height: 0 }); -export const columns = ref(4); // Default number of columns - -// UI state -export const draggedSprite = ref(null); -export const dragOffset = reactive({ x: 0, y: 0 }); -export const isShiftPressed = ref(false); -export const isModalOpen = ref(false); -export const isSettingsModalOpen = ref(false); -export const isSpritesModalOpen = ref(false); -export const isHelpModalOpen = ref(false); -export const zoomLevel = ref(1); // Default zoom level (1 = 100%) - -// Preview border settings -export const previewBorder = reactive({ - enabled: false, - color: '#ff0000', // Default red color - width: 2, // Default width in pixels -}); - -// Animation state -export const animation = reactive({ - canvas: null, - ctx: null, - currentFrame: 0, - isPlaying: false, - frameRate: 10, - lastFrameTime: 0, - animationId: null, - slider: null, - manualUpdate: false, -}); - -// Notification state -export const notification = reactive({ - isVisible: false, - message: '', - type: 'success', -}); - -// Store the current sprite offset for animation playback -// We'll use a Map to store offsets for each frame, so they're preserved when switching frames -export const spriteOffsets = reactive(new Map()); - -// Current sprite offset is a reactive object that will be used for the current frame -export const currentSpriteOffset = reactive({ x: 0, y: 0 }); diff --git a/src/application/types.ts b/src/application/types.ts deleted file mode 100644 index 7893d39..0000000 --- a/src/application/types.ts +++ /dev/null @@ -1,45 +0,0 @@ -// types.ts -export interface Sprite { - img: HTMLImageElement; - width: number; - height: number; - x: number; - y: number; - name: string; - id: string; - uploadOrder: number; -} - -export interface CellSize { - width: number; - height: number; -} - -export interface AnimationState { - canvas: HTMLCanvasElement | null; - ctx: CanvasRenderingContext2D | null; - currentFrame: number; - isPlaying: boolean; - frameRate: number; - lastFrameTime: number; - animationId: number | null; - slider: HTMLInputElement | null; - manualUpdate: boolean; -} - -export interface SpriteOffset { - x: number; - y: number; -} - -export interface NotificationState { - isVisible: boolean; - message: string; - type: 'success' | 'error'; -} - -export interface PreviewBorderSettings { - enabled: boolean; - color: string; - width: number; -} diff --git a/src/application/utilities.ts b/src/application/utilities.ts deleted file mode 100644 index 543108c..0000000 --- a/src/application/utilities.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { spriteOffsets, notification } from '@/application/state'; -import { type Sprite } from '@/application/types'; - -/** - * Logger utility with consistent error format - */ -export const logger = { - info: (message: string, details?: any) => { - console.log(`Spritesheet: ${message}`, details || ''); - }, - warn: (message: string, details?: any) => { - console.warn(`Spritesheet: ${message}`, details || ''); - }, - error: (message: string, error?: any) => { - console.error(`Spritesheet: ${message}`, error || ''); - }, -}; - -/** - * Safely execute a function with error handling - */ -export function safeExecute(fn: () => T, errorMessage: string): T | undefined { - try { - return fn(); - } catch (error) { - logger.error(errorMessage, error); - return undefined; - } -} - -/** - * Show a notification - */ -export function showNotification(message: string, type: 'success' | 'error' = 'success') { - notification.message = message; - notification.type = type; - notification.isVisible = true; - - setTimeout(() => { - notification.isVisible = false; - }, 3000); -} - -/** - * Get the offset for a specific frame - */ -export function getSpriteOffset(frameIndex: number) { - if (!spriteOffsets.has(frameIndex)) { - spriteOffsets.set(frameIndex, { x: 0, y: 0 }); - } - return spriteOffsets.get(frameIndex)!; -} - -/** - * Check if image is fully loaded and ready to use - */ -export function isImageReady(img: HTMLImageElement): boolean { - return img.complete && img.naturalWidth !== 0; -} - -/** - * Get pixel-perfect coordinates aligned to pixel boundaries - */ -export function getPixelPerfectCoordinate(value: number): number { - return Math.floor(value) + 0.5; -} - -/** - * Create a sprite object from a file - */ -export function createSpriteFromFile(file: File, index: number): Promise { - return new Promise((resolve, reject) => { - // Create a URL for the file - const objectUrl = URL.createObjectURL(file); - - const img = new Image(); - - // Set up event handlers - img.onload = () => { - // Verify the image has loaded properly - if (img.width === 0 || img.height === 0) { - logger.error('Image loaded with invalid dimensions:', { name: file.name, width: img.width, height: img.height }); - URL.revokeObjectURL(objectUrl); - reject(new Error(`Image has invalid dimensions: ${file.name}`)); - return; - } - - // Create the sprite object - const sprite: Sprite = { - img, - width: img.width, - height: img.height, - x: 0, - y: 0, - name: file.name, - id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, - uploadOrder: index, - }; - - // Keep the objectUrl reference and don't revoke it yet - // The image is still needed for rendering later - resolve(sprite); - }; - - img.onerror = error => { - logger.error('Error loading image:', { name: file.name, error }); - URL.revokeObjectURL(objectUrl); - reject(new Error(`Failed to load image: ${file.name}`)); - }; - - // Set the source to the object URL - img.src = objectUrl; - }); -} - -/** - * Process multiple files and create sprites - */ -export async function processImageFiles(files: FileList): Promise<{ newSprites: Sprite[]; errorCount: number }> { - const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/')); - - if (imageFiles.length === 0) { - return { newSprites: [], errorCount: 0 }; - } - - const newSprites: Sprite[] = []; - let errorCount = 0; - - for (let i = 0; i < imageFiles.length; i++) { - const file = imageFiles[i]; - - try { - const sprite = await createSpriteFromFile(file, i); - newSprites.push(sprite); - } catch (error) { - errorCount++; - logger.error('Error loading sprite:', error); - } - } - - return { newSprites, errorCount }; -} - -/** - * Truncate text to a specific length and add ellipsis if needed - */ -export function truncateText(text: string, maxLength: number = 15): string { - return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text; -} diff --git a/src/components/AnimationControls.vue b/src/components/AnimationControls.vue deleted file mode 100644 index b53f60a..0000000 --- a/src/components/AnimationControls.vue +++ /dev/null @@ -1,96 +0,0 @@ - - - - - diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue deleted file mode 100644 index 97cd724..0000000 --- a/src/components/AppHeader.vue +++ /dev/null @@ -1,59 +0,0 @@ - - - diff --git a/src/components/BaseModal.vue b/src/components/BaseModal.vue deleted file mode 100644 index a974bef..0000000 --- a/src/components/BaseModal.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - diff --git a/src/components/DropZone.vue b/src/components/DropZone.vue deleted file mode 100644 index 43a84b9..0000000 --- a/src/components/DropZone.vue +++ /dev/null @@ -1,82 +0,0 @@ - - - diff --git a/src/components/FileUploader.vue b/src/components/FileUploader.vue new file mode 100644 index 0000000..09014f0 --- /dev/null +++ b/src/components/FileUploader.vue @@ -0,0 +1,55 @@ + + + diff --git a/src/components/HelpButton.vue b/src/components/HelpButton.vue deleted file mode 100644 index 8af3bf6..0000000 --- a/src/components/HelpButton.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/src/components/HelpModal.vue b/src/components/HelpModal.vue deleted file mode 100644 index 55d56a4..0000000 --- a/src/components/HelpModal.vue +++ /dev/null @@ -1,144 +0,0 @@ - - - diff --git a/src/components/MainContent.vue b/src/components/MainContent.vue deleted file mode 100644 index 69146b4..0000000 --- a/src/components/MainContent.vue +++ /dev/null @@ -1,513 +0,0 @@ - - - - - diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue deleted file mode 100644 index 8b59625..0000000 --- a/src/components/Navigation.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - diff --git a/src/components/Notification.vue b/src/components/Notification.vue deleted file mode 100644 index 4ac35d5..0000000 --- a/src/components/Notification.vue +++ /dev/null @@ -1,36 +0,0 @@ - - - diff --git a/src/components/PreviewModal.vue b/src/components/PreviewModal.vue deleted file mode 100644 index e65cbd1..0000000 --- a/src/components/PreviewModal.vue +++ /dev/null @@ -1,311 +0,0 @@ - - - - - - - diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue deleted file mode 100644 index 43be648..0000000 --- a/src/components/SettingsModal.vue +++ /dev/null @@ -1,199 +0,0 @@ - - - - - diff --git a/src/components/Sidebar.vue b/src/components/Sidebar.vue deleted file mode 100644 index 6a4d786..0000000 --- a/src/components/Sidebar.vue +++ /dev/null @@ -1,106 +0,0 @@ - - - diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue new file mode 100644 index 0000000..f890cf0 --- /dev/null +++ b/src/components/SpriteCanvas.vue @@ -0,0 +1,234 @@ + + + diff --git a/src/components/SpriteList.vue b/src/components/SpriteList.vue deleted file mode 100644 index 220144e..0000000 --- a/src/components/SpriteList.vue +++ /dev/null @@ -1,29 +0,0 @@ - - - diff --git a/src/components/SpritesModal.vue b/src/components/SpritesModal.vue deleted file mode 100644 index d6a173a..0000000 --- a/src/components/SpritesModal.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - diff --git a/src/components/Tooltip.vue b/src/components/Tooltip.vue deleted file mode 100644 index 2228655..0000000 --- a/src/components/Tooltip.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - diff --git a/src/components/ViewControls.vue b/src/components/ViewControls.vue deleted file mode 100644 index f2ae55f..0000000 --- a/src/components/ViewControls.vue +++ /dev/null @@ -1,62 +0,0 @@ - - - - - diff --git a/src/composables/useAnimation.ts b/src/composables/useAnimation.ts deleted file mode 100644 index c1bd6a8..0000000 --- a/src/composables/useAnimation.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { ref, computed, watch, type Ref } from 'vue'; -import { type Sprite, type AnimationState } from '@/application/types'; - -export function useAnimation(sprites: Ref, animation: Ref, onUpdateFrame: (frameIndex: number, offset: { x: number; y: number }) => void, getSpriteOffset: (frameIndex: number) => { x: number; y: number }) { - // State - const currentFrame = ref(0); - const showAllSprites = ref(false); - - // Computed - const currentFrameDisplay = computed(() => { - if (sprites.value.length === 0) return '0 / 0'; - return `${currentFrame.value + 1} / ${sprites.value.length}`; - }); - - // Methods - const startAnimation = () => { - if (sprites.value.length === 0) return; - animation.value.isPlaying = true; - animation.value.lastFrameTime = performance.now(); - animation.value.manualUpdate = false; - - if (animation.value.canvas) { - animation.value.canvas.width = animation.value.canvas.width; // Reset canvas - } - - // Start animation loop - if (!animation.value.animationId) { - animationLoop(); - } - }; - - const stopAnimation = () => { - animation.value.isPlaying = false; - - if (animation.value.animationId) { - cancelAnimationFrame(animation.value.animationId); - animation.value.animationId = null; - } - }; - - const animationLoop = (timestamp?: number) => { - if (!animation.value.isPlaying) return; - - const currentTime = timestamp || performance.now(); - const elapsed = currentTime - animation.value.lastFrameTime; - const frameInterval = 1000 / animation.value.frameRate; - - if (elapsed >= frameInterval) { - animation.value.lastFrameTime = currentTime; - - if (sprites.value.length > 0) { - // Get the stored offset for the current frame - const frameOffset = getSpriteOffset(animation.value.currentFrame); - - // Update frame with offset - onUpdateFrame(animation.value.currentFrame, frameOffset); - - // Move to the next frame - animation.value.currentFrame = (animation.value.currentFrame + 1) % sprites.value.length; - currentFrame.value = animation.value.currentFrame; - - // Update slider if available - if (animation.value.slider) { - animation.value.slider.value = animation.value.currentFrame.toString(); - } - } - } - - animation.value.animationId = requestAnimationFrame(animationLoop); - }; - - const handleFrameChange = () => { - if (animation.value.isPlaying) { - stopAnimation(); - } - - // Ensure frame is within bounds - currentFrame.value = Math.max(0, Math.min(currentFrame.value, sprites.value.length - 1)); - animation.value.currentFrame = currentFrame.value; - updateCurrentFrame(); - }; - - const handleFrameRateChange = () => { - // If animation is currently playing, restart it with the new frame rate - if (animation.value.isPlaying) { - stopAnimation(); - startAnimation(); - } - }; - - const updateCurrentFrame = () => { - // Ensure frame is within bounds - currentFrame.value = Math.max(0, Math.min(currentFrame.value, sprites.value.length - 1)); - animation.value.currentFrame = currentFrame.value; - animation.value.manualUpdate = true; - - // Get the offset for the current frame - const offset = getSpriteOffset(currentFrame.value); - - // Update frame with the current offset - onUpdateFrame(currentFrame.value, offset); - }; - - const nextFrame = () => { - if (sprites.value.length === 0) return; - currentFrame.value = (currentFrame.value + 1) % sprites.value.length; - updateCurrentFrame(); - }; - - const prevFrame = () => { - if (sprites.value.length === 0) return; - currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length; - updateCurrentFrame(); - }; - - // Keep currentFrame in sync with animation.currentFrame - watch( - () => animation.value.currentFrame, - newVal => { - currentFrame.value = newVal; - } - ); - - return { - currentFrame, - showAllSprites, - currentFrameDisplay, - startAnimation, - stopAnimation, - handleFrameChange, - handleFrameRateChange, - updateCurrentFrame, - nextFrame, - prevFrame, - }; -} diff --git a/src/composables/useCanvasInitialization.ts b/src/composables/useCanvasInitialization.ts deleted file mode 100644 index 77a0655..0000000 --- a/src/composables/useCanvasInitialization.ts +++ /dev/null @@ -1,59 +0,0 @@ -// composables/useCanvasInitialization.ts -import { ref, nextTick, type Ref } from 'vue'; -import { type CellSize, type AnimationState } from '@/application/types'; - -export function useCanvasInitialization(animation: Ref, cellSize: CellSize) { - const animCanvas = ref(null); - - const initializeCanvas = async (): Promise => { - // Wait for the next tick to ensure the canvas element is rendered - await nextTick(); - - if (!animCanvas.value) { - console.error('PreviewModal: Animation canvas not found'); - return false; - } - - try { - const context = animCanvas.value.getContext('2d'); - if (!context) { - console.error('PreviewModal: Failed to get 2D context from animation canvas'); - return false; - } - - animation.value.canvas = animCanvas.value; - animation.value.ctx = context; - return true; - } catch (error) { - console.error('PreviewModal: Error initializing animation canvas:', error); - return false; - } - }; - - const updateCanvasSize = () => { - if (!animCanvas.value) { - console.warn('PreviewModal: Cannot update canvas size - canvas not found'); - return; - } - - // More robust check for valid cell dimensions - if (cellSize && typeof cellSize.width === 'number' && typeof cellSize.height === 'number' && cellSize.width > 0 && cellSize.height > 0) { - animCanvas.value.width = cellSize.width; - animCanvas.value.height = cellSize.height; - } else { - console.warn('PreviewModal: Cannot update canvas size - invalid cell dimensions', cellSize ? `width: ${cellSize.width}, height: ${cellSize.height}` : 'cellSize is undefined'); - - // Set a default minimum size to prevent rendering errors - if (animCanvas.value.width === 0 || animCanvas.value.height === 0) { - animCanvas.value.width = animCanvas.value.width || 100; - animCanvas.value.height = animCanvas.value.height || 100; - } - } - }; - - return { - animCanvas, - initializeCanvas, - updateCanvasSize, - }; -} diff --git a/src/composables/useKeyboardShortcuts.ts b/src/composables/useKeyboardShortcuts.ts deleted file mode 100644 index 542cba2..0000000 --- a/src/composables/useKeyboardShortcuts.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { onMounted, onBeforeUnmount, type Ref } from 'vue'; -import { type Sprite, type AnimationState } from '@/application/types'; - -export function useKeyboardShortcuts( - isModalOpen: Ref, - sprites: Ref, - animation: Ref, - closeModal: () => void, - startAnimation: () => void, - stopAnimation: () => void, - nextFrame: () => void, - prevFrame: () => void, - zoomIn: () => void, - zoomOut: () => void, - resetZoom: () => void, - resetSpritePosition: () => void, - panViewport: (direction: 'left' | 'right' | 'up' | 'down', amount?: number) => void, - showNotification: (message: string, type?: 'success' | 'error') => void -) { - const handleKeyDown = (e: KeyboardEvent) => { - if (!isModalOpen.value) return; - - // Modal control - if (e.key === 'Escape') { - closeModal(); - return; - } - - // Animation control - if (e.key === ' ' || e.key === 'Spacebar') { - // Toggle play/pause - if (animation.value.isPlaying) { - stopAnimation(); - } else if (sprites.value.length > 0) { - startAnimation(); - } - e.preventDefault(); - return; - } - - // Frame navigation - if (e.key === 'ArrowRight' && !animation.value.isPlaying && sprites.value.length > 0) { - nextFrame(); - e.preventDefault(); - return; - } - - if (e.key === 'ArrowLeft' && !animation.value.isPlaying && sprites.value.length > 0) { - prevFrame(); - e.preventDefault(); - return; - } - - // Zoom controls - if (e.key === '+' || e.key === '=') { - zoomIn(); - e.preventDefault(); - return; - } - - if (e.key === '-' || e.key === '_') { - zoomOut(); - e.preventDefault(); - return; - } - - if (e.key === '0') { - resetZoom(); - e.preventDefault(); - return; - } - - // Reset sprite position - if (e.key === 'r') { - if (e.shiftKey) { - // Shift+R: Reset sprite position only - resetSpritePosition(); - } else { - // R: Reset both sprite position and viewport - resetSpritePosition(); - resetZoom(); - showNotification('View and position reset'); - } - e.preventDefault(); - return; - } - - // Viewport panning when animation is playing - if (animation.value.isPlaying) { - if (e.key === 'ArrowLeft') { - panViewport('left'); - e.preventDefault(); - } else if (e.key === 'ArrowRight') { - panViewport('right'); - e.preventDefault(); - } else if (e.key === 'ArrowUp') { - panViewport('up'); - e.preventDefault(); - } else if (e.key === 'ArrowDown') { - panViewport('down'); - e.preventDefault(); - } - } - }; - - onMounted(() => { - window.addEventListener('keydown', handleKeyDown); - }); - - onBeforeUnmount(() => { - window.removeEventListener('keydown', handleKeyDown); - }); - - return { - handleKeyDown, - }; -} diff --git a/src/composables/useSpritePosition.ts b/src/composables/useSpritePosition.ts deleted file mode 100644 index bef6b66..0000000 --- a/src/composables/useSpritePosition.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { ref, computed, type Ref } from 'vue'; -import { type Sprite, type CellSize } from '@/application/types'; - -export function useSpritePosition(sprites: Ref, currentFrame: Ref, cellSize: Ref, getSpriteOffset: (frameIndex: number) => { x: number; y: number }, onUpdateFrame: () => void, showNotification: (message: string, type?: 'success' | 'error') => void) { - // State - const isCanvasDragging = ref(false); - const canvasDragStart = ref({ x: 0, y: 0 }); - - // Computed - const spriteOffset = computed({ - get: () => { - // Get the offset for the current frame - return getSpriteOffset(currentFrame.value); - }, - set: val => { - // Update the frame-specific offset directly - const frameOffset = getSpriteOffset(currentFrame.value); - frameOffset.x = val.x; - frameOffset.y = val.y; - - // Re-render the frame - onUpdateFrame(); - }, - }); - - const hasSpriteOffset = computed(() => { - return spriteOffset.value.x !== 0 || spriteOffset.value.y !== 0; - }); - - // Methods - const resetSpritePosition = () => { - // Get current sprite - const sprite = sprites.value[currentFrame.value]; - if (!sprite) return; - - // Check if cellSize is properly defined - if (!cellSize.value || typeof cellSize.value.width !== 'number' || typeof cellSize.value.height !== 'number') { - console.warn('Invalid cell dimensions during position reset'); - showNotification('Could not reset position - invalid cell dimensions', 'error'); - return; - } - - // Calculate center position - const centerX = Math.max(0, Math.floor((cellSize.value.width - sprite.width) / 2)); - const centerY = Math.max(0, Math.floor((cellSize.value.height - sprite.height) / 2)); - - // Update the sprite offset - spriteOffset.value = { x: centerX, y: centerY }; - - // Update the frame - onUpdateFrame(); - - // Show a notification - showNotification('Sprite position reset to center'); - }; - - const startCanvasDrag = (e: MouseEvent, isViewportDragging: Ref, previewZoom: Ref) => { - if (sprites.value.length === 0) return; - if (isViewportDragging.value) return; - - // Validate cell size before starting drag - if (!cellSize.value || typeof cellSize.value.width !== 'number' || typeof cellSize.value.height !== 'number' || cellSize.value.width <= 0 || cellSize.value.height <= 0) { - console.warn('Cannot start drag - invalid cell dimensions', cellSize.value ? `width: ${cellSize.value.width}, height: ${cellSize.value.height}` : 'cellSize is undefined'); - showNotification('Cannot drag sprite - invalid cell dimensions', 'error'); - return; - } - - isCanvasDragging.value = true; - - // Store initial position - canvasDragStart.value = { - x: e.clientX, - y: e.clientY, - }; - - // Add event listeners with passive: false to ensure preventDefault works - const boundHandleCanvasDrag = (e: MouseEvent) => handleCanvasDrag(e, previewZoom); - window.addEventListener('mousemove', boundHandleCanvasDrag, { capture: true, passive: false }); - window.addEventListener('mouseup', stopCanvasDrag, { capture: true, passive: false }); - - // Store the bound function for later removal - (window as any).__boundHandleCanvasDrag = boundHandleCanvasDrag; - - e.preventDefault(); - e.stopPropagation(); - }; - - const handleCanvasDrag = (e: MouseEvent, previewZoom: Ref) => { - if (!isCanvasDragging.value) return; - - requestAnimationFrame(() => { - // Get current sprite - const sprite = sprites.value[currentFrame.value]; - if (!sprite) return; - - // More robust check for cellSize validity - if (!cellSize.value || typeof cellSize.value.width !== 'number' || typeof cellSize.value.height !== 'number' || cellSize.value.width <= 0 || cellSize.value.height <= 0) { - console.warn('Invalid cell dimensions during drag operation', cellSize.value ? `width: ${cellSize.value.width}, height: ${cellSize.value.height}` : 'cellSize is undefined'); - showNotification('Cannot drag sprite - invalid cell dimensions', 'error'); - isCanvasDragging.value = false; - return; - } - - // Calculate delta from last position - const deltaX = e.clientX - canvasDragStart.value.x; - const deltaY = e.clientY - canvasDragStart.value.y; - - // Only move when delta exceeds the threshold for one pixel movement at current zoom - const pixelThreshold = previewZoom.value; // One pixel at current zoom level - - // Calculate the maximum allowed offset - const maxOffsetX = Math.max(0, cellSize.value.width - sprite.width); - const maxOffsetY = Math.max(0, cellSize.value.height - sprite.height); - - // Move one pixel at a time when threshold is reached - if (Math.abs(deltaX) >= pixelThreshold) { - const pixelsToMove = Math.sign(deltaX); - const newX = spriteOffset.value.x + pixelsToMove; - spriteOffset.value.x = Math.max(0, Math.min(maxOffsetX, newX)); - - // Reset the start X position for next pixel move - canvasDragStart.value.x = e.clientX; - } - - if (Math.abs(deltaY) >= pixelThreshold) { - const pixelsToMove = Math.sign(deltaY); - const newY = spriteOffset.value.y + pixelsToMove; - spriteOffset.value.y = Math.max(0, Math.min(maxOffsetY, newY)); - - // Reset the start Y position for next pixel move - canvasDragStart.value.y = e.clientY; - } - - // Update the frame - onUpdateFrame(); - }); - }; - - const stopCanvasDrag = (e: MouseEvent) => { - if (!isCanvasDragging.value) return; - - isCanvasDragging.value = false; - // Use the stored bound function for removal - window.removeEventListener('mousemove', (window as any).__boundHandleCanvasDrag, { capture: true }); - window.removeEventListener('mouseup', stopCanvasDrag, { capture: true }); - - // Clean up the reference - delete (window as any).__boundHandleCanvasDrag; - - e.preventDefault(); - e.stopPropagation(); - }; - - return { - isCanvasDragging, - canvasDragStart, - spriteOffset, - hasSpriteOffset, - resetSpritePosition, - startCanvasDrag, - handleCanvasDrag, - stopCanvasDrag, - }; -} diff --git a/src/composables/useSpritesheetStore.ts b/src/composables/useSpritesheetStore.ts deleted file mode 100644 index 9c3c0bf..0000000 --- a/src/composables/useSpritesheetStore.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { sprites, canvas, ctx, cellSize, columns, draggedSprite, dragOffset, isShiftPressed, isModalOpen, isSettingsModalOpen, isSpritesModalOpen, isHelpModalOpen, zoomLevel, previewBorder, animation, notification, currentSpriteOffset, spriteOffsets } from '@/application/state'; - -import { getSpriteOffset, showNotification, createSpriteFromFile, processImageFiles, truncateText } from '@/application/utilities'; - -import { addSprites, updateCellSize, autoArrangeSprites, highlightSprite, clearAllSprites, applyOffsetsToMainView } from '@/application/spriteOperations'; - -import { updateCanvasSize, renderSpritesheetPreview, drawGrid, downloadSpritesheet, zoomIn, zoomOut, resetZoom } from '@/application/canvasOperations'; - -import { startAnimation, stopAnimation, renderAnimationFrame, animationLoop } from '@/application/animationController'; - -/** - * Main store function that provides access to all spritesheet functionality - */ -export function useSpritesheetStore() { - return { - // State - sprites, - canvas, - ctx, - cellSize, - columns, - draggedSprite, - dragOffset, - isShiftPressed, - isModalOpen, - isSettingsModalOpen, - isSpritesModalOpen, - isHelpModalOpen, - animation, - notification, - zoomLevel, - previewBorder, - currentSpriteOffset, - spriteOffsets, - - // Utils - getSpriteOffset, - showNotification, - createSpriteFromFile, - processImageFiles, - truncateText, - - // Sprite operations - addSprites, - updateCellSize, - autoArrangeSprites, - highlightSprite, - clearAllSprites: () => clearAllSprites(animation), - applyOffsetsToMainView: () => applyOffsetsToMainView(currentSpriteOffset), - - // Canvas operations - updateCanvasSize, - renderSpritesheetPreview, - drawGrid, - downloadSpritesheet, - zoomIn, - zoomOut, - resetZoom, - - // Animation - startAnimation, - stopAnimation, - renderAnimationFrame, - }; -} - -// Re-export types -export { type Sprite, type CellSize, type AnimationState } from '@/application/types'; diff --git a/src/composables/useViewport.ts b/src/composables/useViewport.ts deleted file mode 100644 index 4c854b6..0000000 --- a/src/composables/useViewport.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { ref, nextTick, type Ref } from 'vue'; -import { type AnimationState } from '@/application/types'; - -export function useViewport(animation: Ref, onUpdateFrame: () => void, showNotification: (message: string, type?: 'success' | 'error') => void) { - // State - const previewZoom = ref(1); - const viewportOffset = ref({ x: 0, y: 0 }); - const isViewportDragging = ref(false); - const viewportDragStart = ref({ x: 0, y: 0 }); - - // Methods - const zoomIn = () => { - if (previewZoom.value < 5) { - previewZoom.value = Math.min(5, previewZoom.value + 0.5); - - // Adjust container size after zoom change - nextTick(() => { - updateCanvasContainerSize(); - }); - } - }; - - const zoomOut = () => { - if (previewZoom.value > 1) { - previewZoom.value = Math.max(1, previewZoom.value - 0.5); - - // Adjust container size after zoom change - nextTick(() => { - updateCanvasContainerSize(); - }); - } - }; - - const resetZoom = () => { - previewZoom.value = 1; - viewportOffset.value = { x: 0, y: 0 }; - - // Adjust container size after zoom change - nextTick(() => { - updateCanvasContainerSize(); - }); - }; - - const updateCanvasContainerSize = () => { - // This is a no-op in the composable, but can be implemented if needed - // The actual container size is managed through reactive bindings in the template - }; - - const startViewportDrag = (e: MouseEvent, isCanvasDragging: Ref) => { - // Only enable viewport dragging when zoomed in - if (previewZoom.value <= 1 || isCanvasDragging.value) return; - - isViewportDragging.value = true; - viewportDragStart.value = { - x: e.clientX, - y: e.clientY, - }; - - // Add temporary event listeners with passive: false to ensure preventDefault works - window.addEventListener('mousemove', handleViewportDrag, { passive: false }); - window.addEventListener('mouseup', stopViewportDrag, { passive: false }); - - // Prevent default to avoid text selection - e.preventDefault(); - }; - - const handleViewportDrag = (e: MouseEvent) => { - if (!isViewportDragging.value) return; - - // Prevent default browser behavior - e.preventDefault(); - e.stopPropagation(); - - const deltaX = e.clientX - viewportDragStart.value.x; - const deltaY = e.clientY - viewportDragStart.value.y; - - // Update viewport offset with the delta, scaled by zoom level - viewportOffset.value = { - x: viewportOffset.value.x + deltaX / previewZoom.value, - y: viewportOffset.value.y + deltaY / previewZoom.value, - }; - - // Reset drag start position - viewportDragStart.value = { - x: e.clientX, - y: e.clientY, - }; - }; - - const stopViewportDrag = (e: MouseEvent) => { - if (!isViewportDragging.value) return; - - isViewportDragging.value = false; - window.removeEventListener('mousemove', handleViewportDrag); - window.removeEventListener('mouseup', stopViewportDrag); - - // Prevent default browser behavior - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - }; - - const handleCanvasWheel = (e: WheelEvent) => { - // Prevent the default scroll behavior - e.preventDefault(); - - if (e.ctrlKey) { - // Ctrl + wheel = zoom in/out - if (e.deltaY < 0) { - zoomIn(); - } else { - zoomOut(); - } - } else { - // Just wheel = pan when zoomed in - if (previewZoom.value > 1) { - // Adjust pan amount by zoom level - const panFactor = 0.5 / previewZoom.value; - - if (e.shiftKey) { - // Shift + wheel = horizontal pan - viewportOffset.value.x -= e.deltaY * panFactor; - } else { - // Regular wheel = vertical pan - viewportOffset.value.y -= e.deltaY * panFactor; - } - } - } - }; - - const panViewport = (direction: 'left' | 'right' | 'up' | 'down', amount: number = 10) => { - const panAmount = amount / previewZoom.value; - - switch (direction) { - case 'left': - viewportOffset.value.x += panAmount; - break; - case 'right': - viewportOffset.value.x -= panAmount; - break; - case 'up': - viewportOffset.value.y += panAmount; - break; - case 'down': - viewportOffset.value.y -= panAmount; - break; - } - }; - - return { - previewZoom, - viewportOffset, - isViewportDragging, - zoomIn, - zoomOut, - resetZoom, - updateCanvasContainerSize, - startViewportDrag, - handleViewportDrag, - stopViewportDrag, - handleCanvasWheel, - panViewport, - }; -}