UI and application enhancements
This commit is contained in:
76
src/App.vue
76
src/App.vue
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-3 sm:p-6 bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 transition-colors duration-300">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<div class="max-w-7xl mx-auto">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center mb-4 sm:mb-8 gap-2">
|
||||
<h1 class="text-2xl sm:text-4xl font-bold text-gray-900 dark:text-white tracking-tight text-center sm:text-left">Spritesheet generator</h1>
|
||||
<dark-mode-toggle />
|
||||
@@ -33,7 +33,7 @@
|
||||
<input
|
||||
id="columns"
|
||||
type="number"
|
||||
v-model="columns"
|
||||
v-model.number="columns"
|
||||
min="1"
|
||||
max="10"
|
||||
class="w-20 px-3 py-2 border border-gray-200 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent outline-none transition-all text-base"
|
||||
@@ -108,7 +108,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { ref, watch, onUnmounted } from 'vue';
|
||||
import FileUploader from './components/FileUploader.vue';
|
||||
import SpriteCanvas from './components/SpriteCanvas.vue';
|
||||
import Modal from './components/utilities/Modal.vue';
|
||||
@@ -117,8 +117,8 @@
|
||||
import SpritesheetSplitter from './components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from './components/GifFpsModal.vue';
|
||||
import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
|
||||
import { useSettingsStore } from './stores/useSettingsStore';
|
||||
import GIF from 'gif.js';
|
||||
import gifWorkerUrl from 'gif.js/dist/gif.worker.js?url';
|
||||
import JSZip from 'jszip';
|
||||
|
||||
interface Sprite {
|
||||
@@ -142,6 +142,12 @@
|
||||
|
||||
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 isPreviewModalOpen = ref(false);
|
||||
const isHelpModalOpen = ref(false);
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
@@ -225,7 +231,10 @@
|
||||
};
|
||||
|
||||
const downloadSpritesheet = () => {
|
||||
if (sprites.value.length === 0) return;
|
||||
if (sprites.value.length === 0) {
|
||||
alert('Please upload or import sprites before downloading the spritesheet.');
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -268,9 +277,8 @@
|
||||
|
||||
// Preview modal control
|
||||
const openPreviewModal = () => {
|
||||
console.log('Opening preview modal');
|
||||
if (sprites.value.length === 0) {
|
||||
console.log('No sprites to preview');
|
||||
alert('Please upload or import sprites to preview an animation.');
|
||||
return;
|
||||
}
|
||||
isPreviewModalOpen.value = true;
|
||||
@@ -302,7 +310,10 @@
|
||||
|
||||
// GIF FPS modal control
|
||||
const openGifFpsModal = () => {
|
||||
if (sprites.value.length === 0) return;
|
||||
if (sprites.value.length === 0) {
|
||||
alert('Please upload or import sprites before generating a GIF.');
|
||||
return;
|
||||
}
|
||||
isGifFpsModalOpen.value = true;
|
||||
};
|
||||
|
||||
@@ -326,8 +337,8 @@
|
||||
url,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: spriteFile.x, // Use the position from the splitter
|
||||
y: spriteFile.y, // Use the position from the splitter
|
||||
x: 0, // Start at top-left of cell; ignore splitter bounding-box offset for display
|
||||
y: 0, // Start at top-left of cell; ignore splitter bounding-box offset for display
|
||||
});
|
||||
};
|
||||
img.src = url;
|
||||
@@ -340,7 +351,10 @@
|
||||
|
||||
// Export spritesheet as JSON with base64 images
|
||||
const exportSpritesheetJSON = async () => {
|
||||
if (sprites.value.length === 0) return;
|
||||
if (sprites.value.length === 0) {
|
||||
alert('Nothing to export. Please add sprites first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create an array to store sprite data with base64 images
|
||||
const spritesData = await Promise.all(
|
||||
@@ -426,6 +440,17 @@
|
||||
|
||||
// Process each sprite
|
||||
// Replace current sprites with imported ones
|
||||
// Revoke existing blob: URLs to avoid memory leaks
|
||||
if (sprites.value.length) {
|
||||
sprites.value.forEach(s => {
|
||||
if (s.url && s.url.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(s.url);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
sprites.value = await Promise.all(
|
||||
jsonData.sprites.map(async (spriteData: any) => {
|
||||
return new Promise<Sprite>(resolve => {
|
||||
@@ -548,7 +573,10 @@
|
||||
|
||||
// Download as GIF with specified FPS
|
||||
const downloadAsGif = (fps: number) => {
|
||||
if (sprites.value.length === 0) return;
|
||||
if (sprites.value.length === 0) {
|
||||
alert('Please upload or import sprites before generating a GIF.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find max dimensions
|
||||
let maxWidth = 0;
|
||||
@@ -577,7 +605,7 @@
|
||||
quality: 10,
|
||||
width: maxWidth,
|
||||
height: maxHeight,
|
||||
workerScript: '/gif.worker.js',
|
||||
workerScript: gifWorkerUrl,
|
||||
});
|
||||
|
||||
// Add each sprite as a frame
|
||||
@@ -612,9 +640,23 @@
|
||||
gif.render();
|
||||
};
|
||||
|
||||
// Revoke blob URLs on unmount to avoid memory leaks
|
||||
onUnmounted(() => {
|
||||
sprites.value.forEach(s => {
|
||||
if (s.url && s.url.startsWith('blob:')) {
|
||||
try {
|
||||
URL.revokeObjectURL(s.url);
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Download as ZIP with each cell individually
|
||||
const downloadAsZip = async () => {
|
||||
if (sprites.value.length === 0) return;
|
||||
if (sprites.value.length === 0) {
|
||||
alert('Please upload or import sprites before downloading a ZIP.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a new ZIP file
|
||||
const zip = new JSZip();
|
||||
@@ -664,8 +706,10 @@
|
||||
uint8Array[i] = binaryData.charCodeAt(i);
|
||||
}
|
||||
|
||||
// Add to ZIP file
|
||||
zip.file(`sprite_${index + 1}.png`, uint8Array);
|
||||
// Add to ZIP file with clearer naming
|
||||
const baseName = sprite.file?.name ? sprite.file.name.replace(/\s+/g, '_') : `sprite_${index + 1}.png`;
|
||||
const name = `${index + 1}_${baseName}`;
|
||||
zip.file(name, uint8Array);
|
||||
});
|
||||
|
||||
// Generate ZIP file
|
||||
|
||||
Reference in New Issue
Block a user