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, columns: Ref, negativeSpacingEnabled: Ref) => { // 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(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 }; };