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'; import { calculateNegativeSpacing } from './useNegativeSpacing'; export const useExport = (sprites: Ref, columns: Ref, negativeSpacingEnabled: Ref, backgroundColor?: Ref, manualCellSizeEnabled?: Ref, manualCellWidth?: Ref, manualCellHeight?: Ref) => { const getCellDimensions = () => { // If manual cell size is enabled, use manual values if (manualCellSizeEnabled?.value) { return { cellWidth: manualCellWidth?.value ?? 64, cellHeight: manualCellHeight?.value ?? 64, negativeSpacing: 0, }; } // Otherwise, calculate from sprite dimensions const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value); return { cellWidth: maxWidth + negativeSpacing, cellHeight: maxHeight + negativeSpacing, negativeSpacing, }; }; const downloadSpritesheet = () => { if (!sprites.value.length) { alert('Please upload or import sprites before downloading the spritesheet.'); return; } const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); 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; // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); } 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, negativeSpacingEnabled: negativeSpacingEnabled.value, backgroundColor: backgroundColor?.value || 'transparent', manualCellSizeEnabled: manualCellSizeEnabled?.value || false, manualCellWidth: manualCellWidth?.value || 64, manualCellHeight: manualCellHeight?.value || 64, 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; if (typeof jsonData.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = jsonData.negativeSpacingEnabled; if (typeof jsonData.backgroundColor === 'string' && backgroundColor) backgroundColor.value = jsonData.backgroundColor; if (typeof jsonData.manualCellSizeEnabled === 'boolean' && manualCellSizeEnabled) manualCellSizeEnabled.value = jsonData.manualCellSizeEnabled; if (typeof jsonData.manualCellWidth === 'number' && manualCellWidth) manualCellWidth.value = jsonData.manualCellWidth; if (typeof jsonData.manualCellHeight === 'number' && manualCellHeight) manualCellHeight.value = jsonData.manualCellHeight; // 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 { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); 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); // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); } 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 { cellWidth, cellHeight, negativeSpacing } = getCellDimensions(); 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); // Apply background color if not transparent if (backgroundColor?.value && backgroundColor.value !== 'transparent') { ctx.fillStyle = backgroundColor.value; ctx.fillRect(0, 0, canvas.width, canvas.height); } 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 }; };