226 lines
8.2 KiB
TypeScript
226 lines
8.2 KiB
TypeScript
import type { Ref } from 'vue';
|
|
import GIF from 'gif.js';
|
|
import gifWorkerUrl from 'gif.js/dist/gif.worker.js?url';
|
|
import JSZip from 'jszip';
|
|
import type { Sprite } from '../types/sprites';
|
|
import { getMaxDimensions } from './useSprites';
|
|
|
|
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>) => {
|
|
// Calculate negative spacing based on sprite dimensions
|
|
const calculateNegativeSpacing = (): number => {
|
|
if (!negativeSpacingEnabled.value || sprites.value.length === 0) return 0;
|
|
|
|
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
|
|
const minWidth = Math.min(...sprites.value.map(s => s.width));
|
|
const minHeight = Math.min(...sprites.value.map(s => s.height));
|
|
const widthDiff = maxWidth - minWidth;
|
|
const heightDiff = maxHeight - minHeight;
|
|
return Math.max(widthDiff, heightDiff);
|
|
};
|
|
const downloadSpritesheet = () => {
|
|
if (!sprites.value.length) {
|
|
alert('Please upload or import sprites before downloading the spritesheet.');
|
|
return;
|
|
}
|
|
|
|
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
|
|
const negativeSpacing = calculateNegativeSpacing();
|
|
const cellWidth = maxWidth + negativeSpacing;
|
|
const cellHeight = maxHeight + negativeSpacing;
|
|
const rows = Math.ceil(sprites.value.length / columns.value);
|
|
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
canvas.width = cellWidth * columns.value;
|
|
canvas.height = cellHeight * rows;
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
sprites.value.forEach((sprite, index) => {
|
|
const col = index % columns.value;
|
|
const row = Math.floor(index / columns.value);
|
|
const cellX = Math.floor(col * cellWidth);
|
|
const cellY = Math.floor(row * cellHeight);
|
|
ctx.drawImage(sprite.img, Math.floor(cellX + negativeSpacing + sprite.x), Math.floor(cellY + negativeSpacing + sprite.y));
|
|
});
|
|
|
|
const link = document.createElement('a');
|
|
link.download = 'spritesheet.png';
|
|
link.href = canvas.toDataURL('image/png', 1.0);
|
|
link.click();
|
|
};
|
|
|
|
const exportSpritesheetJSON = async () => {
|
|
if (!sprites.value.length) {
|
|
alert('Nothing to export. Please add sprites first.');
|
|
return;
|
|
}
|
|
|
|
const spritesData = await Promise.all(
|
|
sprites.value.map(async sprite => {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return null;
|
|
canvas.width = sprite.width;
|
|
canvas.height = sprite.height;
|
|
ctx.drawImage(sprite.img, 0, 0);
|
|
const base64Data = canvas.toDataURL('image/png');
|
|
return {
|
|
id: sprite.id,
|
|
width: sprite.width,
|
|
height: sprite.height,
|
|
x: sprite.x,
|
|
y: sprite.y,
|
|
base64: base64Data,
|
|
name: sprite.file.name,
|
|
};
|
|
})
|
|
);
|
|
|
|
const jsonData = { columns: columns.value, sprites: spritesData.filter(Boolean) };
|
|
const jsonString = JSON.stringify(jsonData, null, 2);
|
|
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.download = 'spritesheet.json';
|
|
link.href = url;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const importSpritesheetJSON = async (jsonFile: File) => {
|
|
const jsonText = await jsonFile.text();
|
|
const jsonData = JSON.parse(jsonText);
|
|
|
|
if (!jsonData.sprites || !Array.isArray(jsonData.sprites)) throw new Error('Invalid JSON format: missing sprites array');
|
|
|
|
if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns;
|
|
|
|
// revoke existing blob urls
|
|
if (sprites.value.length) {
|
|
sprites.value.forEach(s => {
|
|
if (s.url && s.url.startsWith('blob:')) {
|
|
try {
|
|
URL.revokeObjectURL(s.url);
|
|
} catch {}
|
|
}
|
|
});
|
|
}
|
|
|
|
const imported = await Promise.all(
|
|
jsonData.sprites.map((spriteData: any) => {
|
|
return new Promise<Sprite>(resolve => {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const byteString = atob(spriteData.base64.split(',')[1]);
|
|
const mimeType = spriteData.base64.split(',')[0].split(':')[1].split(';')[0];
|
|
const ab = new ArrayBuffer(byteString.length);
|
|
const ia = new Uint8Array(ab);
|
|
for (let i = 0; i < byteString.length; i++) ia[i] = byteString.charCodeAt(i);
|
|
const blob = new Blob([ab], { type: mimeType });
|
|
const fileName = spriteData.name || `sprite-${spriteData.id}.png`;
|
|
const file = new File([blob], fileName, { type: mimeType });
|
|
resolve({
|
|
id: spriteData.id || crypto.randomUUID(),
|
|
file,
|
|
img,
|
|
url: spriteData.base64,
|
|
width: spriteData.width,
|
|
height: spriteData.height,
|
|
x: spriteData.x || 0,
|
|
y: spriteData.y || 0,
|
|
});
|
|
};
|
|
img.src = spriteData.base64;
|
|
});
|
|
})
|
|
);
|
|
|
|
sprites.value = imported;
|
|
};
|
|
|
|
const downloadAsGif = (fps: number) => {
|
|
if (!sprites.value.length) {
|
|
alert('Please upload or import sprites before generating a GIF.');
|
|
return;
|
|
}
|
|
|
|
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
|
|
const negativeSpacing = calculateNegativeSpacing();
|
|
const cellWidth = maxWidth + negativeSpacing;
|
|
const cellHeight = maxHeight + negativeSpacing;
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
canvas.width = cellWidth;
|
|
canvas.height = cellHeight;
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
const gif = new GIF({ workers: 2, quality: 10, width: cellWidth, height: cellHeight, workerScript: gifWorkerUrl });
|
|
|
|
sprites.value.forEach(sprite => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = '#f9fafb';
|
|
ctx.fillRect(0, 0, cellWidth, cellHeight);
|
|
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
|
|
gif.addFrame(ctx, { copy: true, delay: 1000 / fps });
|
|
});
|
|
|
|
gif.on('finished', (blob: Blob) => {
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.download = 'animation.gif';
|
|
link.href = url;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
gif.render();
|
|
};
|
|
|
|
const downloadAsZip = async () => {
|
|
if (!sprites.value.length) {
|
|
alert('Please upload or import sprites before downloading a ZIP.');
|
|
return;
|
|
}
|
|
|
|
const zip = new JSZip();
|
|
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
|
|
const negativeSpacing = calculateNegativeSpacing();
|
|
const cellWidth = maxWidth + negativeSpacing;
|
|
const cellHeight = maxHeight + negativeSpacing;
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
canvas.width = cellWidth;
|
|
canvas.height = cellHeight;
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
sprites.value.forEach((sprite, index) => {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
ctx.fillStyle = '#f9fafb';
|
|
ctx.fillRect(0, 0, cellWidth, cellHeight);
|
|
ctx.drawImage(sprite.img, Math.floor(negativeSpacing + sprite.x), Math.floor(negativeSpacing + sprite.y));
|
|
const dataURL = canvas.toDataURL('image/png');
|
|
const binary = atob(dataURL.split(',')[1]);
|
|
const buf = new ArrayBuffer(binary.length);
|
|
const view = new Uint8Array(buf);
|
|
for (let i = 0; i < binary.length; i++) view[i] = binary.charCodeAt(i);
|
|
const baseName = sprite.file?.name ? sprite.file.name.replace(/\s+/g, '_') : `sprite_${index + 1}.png`;
|
|
const name = `${index + 1}_${baseName}`;
|
|
zip.file(name, view);
|
|
});
|
|
|
|
const content = await zip.generateAsync({ type: 'blob' });
|
|
const url = URL.createObjectURL(content);
|
|
const link = document.createElement('a');
|
|
link.download = 'sprites.zip';
|
|
link.href = url;
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
return { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip };
|
|
};
|