256 lines
6.7 KiB
TypeScript
256 lines
6.7 KiB
TypeScript
import { ref, watch, onUnmounted } from 'vue';
|
|
import type { Sprite } from '../types/sprites';
|
|
|
|
export const useSprites = () => {
|
|
const sprites = ref<Sprite[]>([]);
|
|
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<Sprite>(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<Sprite[]>): { 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);
|
|
};
|