import { ref, watch, onUnmounted } from 'vue'; import type { Sprite } from '../types/sprites'; export const useSprites = () => { const sprites = ref([]); const columns = ref(4); // Clamp and coerce columns to a safe range [1..10] watch(columns, val => { const num = typeof val === 'number' ? val : parseInt(String(val)); const safe = Number.isFinite(num) && num >= 1 ? Math.min(num, 10) : 1; if (safe !== columns.value) columns.value = safe; }); const updateSpritePosition = (id: string, x: number, y: number) => { const i = sprites.value.findIndex(s => s.id === id); if (i !== -1) { sprites.value[i].x = Math.floor(x); sprites.value[i].y = Math.floor(y); } }; const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => { if (!sprites.value.length) return; const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); sprites.value = sprites.value.map(sprite => { let x = sprite.x; let y = sprite.y; switch (position) { case 'left': x = 0; break; case 'center': x = Math.floor((maxWidth - sprite.width) / 2); break; case 'right': x = Math.floor(maxWidth - sprite.width); break; case 'top': y = 0; break; case 'middle': y = Math.floor((maxHeight - sprite.height) / 2); break; case 'bottom': y = Math.floor(maxHeight - sprite.height); break; } return { ...sprite, x: Math.floor(x), y: Math.floor(y) }; }); triggerForceRedraw(); }; const updateSpriteCell = (id: string, newIndex: number) => { const currentIndex = sprites.value.findIndex(s => s.id === id); if (currentIndex === -1 || currentIndex === newIndex) return; const next = [...sprites.value]; if (newIndex < sprites.value.length) { const moving = { ...next[currentIndex] }; const target = { ...next[newIndex] }; next[currentIndex] = target; next[newIndex] = moving; } else { const [moved] = next.splice(currentIndex, 1); next.splice(newIndex, 0, moved); } sprites.value = next; }; const removeSprite = (id: string) => { const i = sprites.value.findIndex(s => s.id === id); if (i === -1) return; const s = sprites.value[i]; revokeIfBlob(s.url); sprites.value.splice(i, 1); }; const replaceSprite = (id: string, file: File) => { const i = sprites.value.findIndex(s => s.id === id); if (i === -1) return; const old = sprites.value[i]; revokeIfBlob(old.url); const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { const next: Sprite = { id: old.id, file, img, url, width: img.width, height: img.height, x: old.x, y: old.y, }; const arr = [...sprites.value]; arr[i] = next; sprites.value = arr; }; img.onerror = () => { console.error('Failed to load replacement image:', file.name); URL.revokeObjectURL(url); }; img.src = url; }; const addSprite = (file: File) => { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { const s: Sprite = { id: crypto.randomUUID(), file, img, url, width: img.width, height: img.height, x: 0, y: 0, }; sprites.value = [...sprites.value, s]; }; img.onerror = () => { console.error('Failed to load new sprite image:', file.name); URL.revokeObjectURL(url); }; img.src = url; }; const addSpriteWithResize = (file: File) => { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); const newSprite: Sprite = { id: crypto.randomUUID(), file, img, url, width: img.width, height: img.height, x: 0, y: 0, }; const newMaxWidth = Math.max(maxWidth, img.width); const newMaxHeight = Math.max(maxHeight, img.height); if (img.width > maxWidth || img.height > maxHeight) { sprites.value = sprites.value.map(sprite => { let newX = sprite.x; let newY = sprite.y; if (img.width > maxWidth) { const relativeX = maxWidth > 0 ? sprite.x / maxWidth : 0; newX = Math.floor(relativeX * newMaxWidth); newX = Math.max(0, Math.min(newX, newMaxWidth - sprite.width)); } if (img.height > maxHeight) { const relativeY = maxHeight > 0 ? sprite.y / maxHeight : 0; newY = Math.floor(relativeY * newMaxHeight); newY = Math.max(0, Math.min(newY, newMaxHeight - sprite.height)); } return { ...sprite, x: newX, y: newY }; }); } sprites.value = [...sprites.value, newSprite]; triggerForceRedraw(); }; img.onerror = () => { console.error('Failed to load new sprite image:', file.name); URL.revokeObjectURL(url); }; img.src = url; }; const processImageFiles = (files: File[]) => { Promise.all( files.map( file => new Promise(resolve => { const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { resolve({ id: crypto.randomUUID(), file, img, url, width: img.width, height: img.height, x: 0, y: 0, }); }; img.src = url; }) ) ).then(newSprites => { sprites.value = [...sprites.value, ...newSprites]; }); }; onUnmounted(() => { sprites.value.forEach(s => revokeIfBlob(s.url)); }); return { sprites, columns, updateSpritePosition, alignSprites, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, }; }; export const getMaxDimensions = (arr: Sprite[] | Readonly): { maxWidth: number; maxHeight: number } => { let maxWidth = 0; let maxHeight = 0; arr.forEach(s => { if (s.width > maxWidth) maxWidth = s.width; if (s.height > maxHeight) maxHeight = s.height; }); return { maxWidth, maxHeight }; }; export const revokeIfBlob = (url?: string) => { if (url && url.startsWith('blob:')) { try { URL.revokeObjectURL(url); } catch {} } }; export const triggerForceRedraw = () => { setTimeout(() => { window.dispatchEvent(new Event('forceRedraw')); }, 0); };