Files
spritesheet-generator/src/App.vue
2025-11-23 15:05:03 +01:00

520 lines
28 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="min-h-screen p-4 sm:p-8 bg-slate-50 dark:bg-gray-950 transition-colors duration-300">
<div class="max-w-[1600px] mx-auto">
<header class="mb-8 sm:mb-12">
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mb-6">
<div class="text-center sm:text-left">
<h1 class="text-3xl sm:text-5xl font-bold text-blue-600 dark:text-blue-400 tracking-tight mb-2">Spritesheet generator</h1>
<p class="text-sm sm:text-base text-gray-600 dark:text-gray-400">Create professional spritesheets for your game development projects</p>
</div>
<dark-mode-toggle />
</div>
<nav class="flex flex-wrap justify-center sm:justify-start items-center gap-2 sm:gap-3">
<a
href="https://gitea.adhd.sh/root/spritesheet-generator"
target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg transition-all hover:shadow-md"
data-rybbit-event="source-link"
>
<i class="fab fa-github"></i>
<span class="font-medium">Source</span>
</a>
<a
href="https://discord.gg/JTev3nzeDa"
target="_blank"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg transition-all hover:shadow-md"
data-rybbit-event="discord-link"
>
<i class="fab fa-discord"></i>
<span class="font-medium">Discord</span>
</a>
<a
href="#"
@click.prevent="openHelpModal"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg transition-all hover:shadow-md"
data-rybbit-event="help-link"
>
<i class="fas fa-question-circle"></i>
<span class="font-medium">Help</span>
</a>
<a
href="#"
@click.prevent="openFeedbackModal"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-800/80 hover:bg-white dark:hover:bg-gray-800 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 rounded-lg transition-all hover:shadow-md"
data-rybbit-event="feedback-link"
>
<i class="fas fa-comment-dots"></i>
<span class="font-medium">Feedback</span>
</a>
</nav>
</header>
<main class="bg-white/90 dark:bg-gray-800/90 backdrop-blur-sm rounded-3xl shadow-xl border border-gray-200/50 dark:border-gray-700/50 transition-all duration-300 overflow-hidden">
<!-- Welcome state -->
<div v-if="!visibleLayers.some(l => l.sprites.length)" class="p-6 sm:p-10">
<div class="mb-8">
<h2 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-1">Upload sprites</h2>
<p class="text-sm text-gray-600 dark:text-gray-400">Drag and drop images or import from JSON</p>
</div>
<file-uploader @upload-sprites="handleSpritesUpload" />
<div class="mt-10">
<div class="prose prose-lg dark:prose-invert max-w-none">
<div>
<h3 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Welcome to Spritesheet generator</h3>
<p class="text-gray-700 dark:text-gray-300 mb-4">Create spritesheets for your game development and animation projects with our completely free, open-source Spritesheet generator.</p>
<p class="text-gray-700 dark:text-gray-300 mb-6">This powerful online tool lets you upload individual sprite images and automatically arrange them into optimized sprite sheets with customizable layouts - perfect for indie developers, animators, and studios of any size.</p>
<div>
<h4 class="text-xl font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center gap-2">
<i class="fas fa-play-circle text-blue-600 dark:text-blue-400"></i>
How it works
</h4>
<video controls playsinline class="w-full rounded-lg shadow-lg border border-gray-200 dark:border-gray-700" title="Spritesheet generator tutorial" aria-label="Spritesheet generator tutorial">
<source src="@/assets/tut2.mp4" type="video/mp4" />
</video>
</div>
</div>
</div>
</div>
</div>
<!-- Two-column layout: Left controls, Right preview -->
<div v-if="visibleLayers.some(l => l.sprites.length)" class="grid lg:grid-cols-[380px_1fr] xl:grid-cols-[420px_1fr] min-h-[600px]">
<!-- Left sidebar - Controls -->
<div class="border-r border-gray-200 dark:border-gray-700 p-6 overflow-y-auto max-h-[calc(100vh-200px)] bg-gray-50/50 dark:bg-gray-900/30">
<div class="space-y-6">
<!-- Upload Section -->
<section>
<div class="flex items-center justify-between mb-3">
<h3 class="text-base font-bold text-gray-800 dark:text-gray-100 flex items-center gap-2">
<i class="fas fa-upload text-blue-600 dark:text-blue-400 text-sm"></i>
Upload
</h3>
<button @click="openJSONImportDialog" class="px-3 py-1.5 bg-indigo-500 hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700 text-white text-xs font-medium rounded-lg transition-all flex items-center gap-1.5 cursor-pointer" data-rybbit-event="import-json">
<i class="fas fa-file-import text-xs"></i>
<span>JSON</span>
</button>
</div>
<div class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-xl p-4 text-center hover:border-blue-400 dark:hover:border-blue-500 transition-colors cursor-pointer" @click="openFileDialog">
<i class="fas fa-plus-circle text-2xl text-blue-500 dark:text-blue-400 mb-2"></i>
<p class="text-xs text-gray-600 dark:text-gray-400">Add sprites</p>
</div>
<input ref="uploadInput" type="file" multiple accept="image/*" class="hidden" @change="handleUploadChange" />
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
</section>
<!-- Layers -->
<section>
<div class="flex items-center justify-between mb-3">
<h3 class="text-base font-bold text-gray-800 dark:text-gray-100 flex items-center gap-2">
<i class="fas fa-layer-group text-blue-600 dark:text-blue-400 text-sm"></i>
Layers
</h3>
<button @click="addLayer()" class="px-3 py-1.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white text-xs font-medium rounded-lg transition-all flex items-center gap-1.5 cursor-pointer">
<i class="fas fa-plus text-xs"></i>
<span>Add</span>
</button>
</div>
<div class="space-y-2">
<div
v-for="layer in layers"
:key="layer.id"
class="flex items-center gap-2 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border transition-all"
:class="[layer.id === activeLayerId ? 'border-blue-500 ring-1 ring-blue-500' : 'border-gray-200 dark:border-gray-700', !layer.visible ? 'opacity-50' : '']"
>
<button @click.stop="layer.visible = !layer.visible" class="p-1.5 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer" :title="layer.visible ? 'Hide layer' : 'Show layer'">
<i :class="layer.visible ? 'fas fa-eye text-blue-600 dark:text-blue-400' : 'fas fa-eye-slash text-gray-400 dark:text-gray-500'" class="text-sm"></i>
</button>
<input
v-if="editingLayerId === layer.id"
type="text"
v-model="editingLayerName"
@blur="finishEditingLayer"
@keyup.enter="finishEditingLayer"
@keyup.esc="cancelEditingLayer"
class="flex-1 px-2 py-1 border border-blue-500 dark:border-blue-400 dark:bg-gray-700 dark:text-gray-100 rounded text-sm focus:ring-2 focus:ring-blue-500 outline-none"
ref="layerNameInput"
@click.stop
/>
<button v-else @click="activeLayerId = layer.id" class="flex-1 text-left px-2 py-1 rounded text-sm font-medium transition-all cursor-pointer" :class="layer.id === activeLayerId ? 'text-blue-600 dark:text-blue-400' : 'text-gray-700 dark:text-gray-300'">
{{ layer.name }}
<span v-if="layer.sprites.length" class="text-xs opacity-60 ml-1">({{ layer.sprites.length }})</span>
</button>
<button v-if="editingLayerId !== layer.id" @click="startEditingLayer(layer.id, layer.name)" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer" title="Rename">
<i class="fas fa-pen text-xs text-gray-600 dark:text-gray-400"></i>
</button>
<button @click="moveLayer(layer.id, 'up')" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer" title="Move up">
<i class="fas fa-chevron-up text-xs text-gray-600 dark:text-gray-400"></i>
</button>
<button @click="moveLayer(layer.id, 'down')" class="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors cursor-pointer" title="Move down">
<i class="fas fa-chevron-down text-xs text-gray-600 dark:text-gray-400"></i>
</button>
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="p-1 hover:bg-red-100 dark:hover:bg-red-900/30 text-red-600 dark:text-red-400 rounded transition-colors cursor-pointer" title="Delete">
<i class="fas fa-trash text-xs"></i>
</button>
</div>
</div>
</section>
<!-- Grid Settings -->
<section>
<h3 class="text-base font-bold text-gray-800 dark:text-gray-100 mb-3 flex items-center gap-2">
<i class="fas fa-th text-blue-600 dark:text-blue-400 text-sm"></i>
Grid
</h3>
<div class="space-y-3">
<div class="flex items-center justify-between bg-white dark:bg-gray-800 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700">
<label for="columns" class="text-sm font-medium text-gray-700 dark:text-gray-200">Columns</label>
<input id="columns" type="number" v-model.number="columns" min="1" max="10" class="w-16 px-2 py-1 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded text-sm focus:ring-2 focus:ring-blue-500 outline-none" />
</div>
<div class="bg-white dark:bg-gray-800 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700">
<label class="flex items-center justify-between mb-2 cursor-pointer">
<span class="text-sm font-medium text-gray-700 dark:text-gray-200">Manual size</span>
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="w-4 h-4 rounded" />
</label>
<div v-if="settingsStore.manualCellSizeEnabled" class="flex items-center gap-1.5 mt-2">
<input
type="number"
v-model.number="settingsStore.manualCellWidth"
min="1"
max="2048"
class="w-full min-w-0 px-2 py-1 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded text-sm focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="W"
/>
<span class="text-gray-500 dark:text-gray-400 flex-shrink-0">×</span>
<input
type="number"
v-model.number="settingsStore.manualCellHeight"
min="1"
max="2048"
class="w-full min-w-0 px-2 py-1 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 rounded text-sm focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="H"
/>
</div>
<div v-else class="text-xs text-gray-500 dark:text-gray-400 font-mono mt-1 break-words">{{ cellSize.width }} × {{ cellSize.height }}px</div>
</div>
</div>
</section>
<!-- Alignment -->
<section>
<h3 class="text-base font-bold text-gray-800 dark:text-gray-100 mb-3 flex items-center gap-2">
<i class="fas fa-align-center text-blue-600 dark:text-blue-400 text-sm"></i>
Align
</h3>
<div class="grid grid-cols-3 gap-2">
<button @click="alignSprites('left')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Left">
<i class="fas fa-arrow-left"></i>
</button>
<button @click="alignSprites('center')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Center">
<i class="fas fa-arrows-left-right"></i>
</button>
<button @click="alignSprites('right')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Right">
<i class="fas fa-arrow-right"></i>
</button>
<button @click="alignSprites('top')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Top">
<i class="fas fa-arrow-up"></i>
</button>
<button @click="alignSprites('middle')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Middle">
<i class="fas fa-arrows-up-down"></i>
</button>
<button @click="alignSprites('bottom')" class="px-3 py-2 bg-white dark:bg-gray-800 hover:bg-blue-50 dark:hover:bg-blue-900/20 border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-100 rounded-lg transition-all text-xs font-medium cursor-pointer" title="Bottom">
<i class="fas fa-arrow-down"></i>
</button>
</div>
</section>
<!-- Export -->
<section>
<h3 class="text-base font-bold text-gray-800 dark:text-gray-100 mb-3 flex items-center gap-2">
<i class="fas fa-download text-blue-600 dark:text-blue-400 text-sm"></i>
Export
</h3>
<div class="grid grid-cols-2 gap-2">
<button @click="downloadSpritesheet" class="px-3 py-2 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white rounded-lg transition-all text-xs font-medium flex items-center justify-center gap-1.5 cursor-pointer" data-rybbit-event="download-spritesheet">
<i class="fas fa-image"></i>
<span>PNG</span>
</button>
<button @click="exportSpritesheetJSON" class="px-3 py-2 bg-purple-500 hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700 text-white rounded-lg transition-all text-xs font-medium flex items-center justify-center gap-1.5 cursor-pointer" data-rybbit-event="export-json">
<i class="fas fa-file-code"></i>
<span>JSON</span>
</button>
<button @click="openGifFpsModal" class="px-3 py-2 bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-700 text-white rounded-lg transition-all text-xs font-medium flex items-center justify-center gap-1.5 cursor-pointer" data-rybbit-event="download-gif">
<i class="fas fa-film"></i>
<span>GIF</span>
</button>
<button @click="downloadAsZip" class="px-3 py-2 bg-teal-500 hover:bg-teal-600 dark:bg-teal-600 dark:hover:bg-teal-700 text-white rounded-lg transition-all text-xs font-medium flex items-center justify-center gap-1.5 cursor-pointer" data-rybbit-event="download-zip">
<i class="fas fa-file-archive"></i>
<span>ZIP</span>
</button>
</div>
</section>
</div>
</div>
<!-- Right panel - Tabs -->
<div class="flex flex-col min-w-0">
<!-- Tab Navigation -->
<div class="border-b border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/30">
<div class="flex gap-1 p-2">
<button
@click="activeTab = 'canvas'"
:class="['flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium', activeTab === 'canvas' ? 'bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50']"
>
<i class="fas fa-th"></i>
<span>Canvas</span>
</button>
<button
@click="activeTab = 'preview'"
:class="['flex items-center gap-2 px-4 py-2 rounded-lg transition-all text-sm font-medium', activeTab === 'preview' ? 'bg-white dark:bg-gray-800 text-blue-600 dark:text-blue-400 shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50']"
data-rybbit-event="preview-animation"
>
<i class="fas fa-play"></i>
<span>Preview</span>
</button>
</div>
</div>
<!-- Tab Content -->
<div class="p-6 overflow-y-auto overflow-x-hidden max-h-[calc(100vh-260px)] min-w-0">
<div class="max-w-full">
<sprite-canvas v-show="activeTab === 'canvas'" :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-cell="updateSpriteCell" @remove-sprite="removeSprite" @replace-sprite="replaceSprite" @add-sprite="addSprite" />
<sprite-preview v-show="activeTab === 'preview'" :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" />
</div>
</div>
</div>
</div>
</main>
</div>
<HelpModal :is-open="isHelpModalOpen" @close="closeHelpModal" />
<FeedbackModal :is-open="isFeedbackModalOpen" @close="closeFeedbackModal" />
<SpritesheetSplitter :is-open="isSpritesheetSplitterOpen" :image-url="spritesheetImageUrl" :image-file="spritesheetImageFile" @close="closeSpritesheetSplitter" @split="handleSplitSpritesheet" />
<GifFpsModal :is-open="isGifFpsModalOpen" @close="closeGifFpsModal" @confirm="downloadAsGif" :default-fps="10" />
<!-- One-time feedback popup -->
<div v-if="showFeedbackPopup" class="fixed inset-0 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-md mx-4 shadow-xl border border-gray-600">
<div class="text-center">
<div class="text-4xl mb-4">💬</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Help us improve!</h3>
<p class="text-gray-600 dark:text-gray-300 mb-6">We'd love to hear your thoughts about the Spritesheet generator. Would you like to share your feedback?</p>
<div class="flex gap-3 justify-center">
<button @click="handleFeedbackPopupResponse(false)" class="px-4 py-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200 transition-colors cursor-pointer">Maybe later</button>
<button @click="handleFeedbackPopupResponse(true)" class="px-6 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors cursor-pointer">Share feedback</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, toRef, computed } from 'vue';
import FileUploader from './components/FileUploader.vue';
import SpriteCanvas from './components/SpriteCanvas.vue';
import SpritePreview from './components/SpritePreview.vue';
import HelpModal from './components/HelpModal.vue';
import FeedbackModal from './components/FeedbackModal.vue';
import SpritesheetSplitter from './components/SpritesheetSplitter.vue';
import GifFpsModal from './components/GifFpsModal.vue';
import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
import { useExportLayers } from './composables/useExportLayers';
import { useLayers } from './composables/useLayers';
import { getMaxDimensionsAcrossLayers } from './composables/useLayers';
import { useSettingsStore } from './stores/useSettingsStore';
import { calculateNegativeSpacing } from './composables/useNegativeSpacing';
import type { SpriteFile } from './types/sprites';
const settingsStore = useSettingsStore();
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(
layers,
columns,
toRef(settingsStore, 'negativeSpacingEnabled'),
activeLayerId,
toRef(settingsStore, 'backgroundColor'),
toRef(settingsStore, 'manualCellSizeEnabled'),
toRef(settingsStore, 'manualCellWidth'),
toRef(settingsStore, 'manualCellHeight')
);
const getCellSize = () => {
if (!visibleLayers.value.length) return { width: 0, height: 0 };
if (settingsStore.manualCellSizeEnabled) {
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
}
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
};
const cellSize = computed(getCellSize);
const activeTab = ref<'canvas' | 'preview'>('canvas');
const isHelpModalOpen = ref(false);
const isFeedbackModalOpen = ref(false);
const isSpritesheetSplitterOpen = ref(false);
const isGifFpsModalOpen = ref(false);
const uploadInput = ref<HTMLInputElement | null>(null);
const jsonFileInput = ref<HTMLInputElement | null>(null);
const spritesheetImageUrl = ref('');
const spritesheetImageFile = ref<File | null>(null);
const showFeedbackPopup = ref(false);
const editingLayerId = ref<string | null>(null);
const editingLayerName = ref('');
const layerNameInput = ref<HTMLInputElement | null>(null);
const handleSpritesUpload = async (files: File[]) => {
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
if (jsonFile) {
await handleJSONImport(jsonFile);
return;
}
if (files.length === 1 && files[0].type.startsWith('image/')) {
const file = files[0];
const url = URL.createObjectURL(file);
const img = new Image();
img.onload = () => {
if (confirm('This looks like it might be a spritesheet. Would you like to split it into individual sprites?')) {
spritesheetImageUrl.value = url;
spritesheetImageFile.value = file;
isSpritesheetSplitterOpen.value = true;
return;
}
processImageFiles([file]);
};
img.src = url;
return;
}
processImageFiles(files);
};
const handleJSONImport = async (jsonFile: File) => {
try {
await importSpritesheetJSON(jsonFile);
} catch (error) {
console.error('Error importing JSON:', error);
alert('Failed to import JSON file. Please check the file format.');
}
};
const openHelpModal = () => {
isHelpModalOpen.value = true;
};
const closeHelpModal = () => {
isHelpModalOpen.value = false;
};
const openFeedbackModal = () => {
isFeedbackModalOpen.value = true;
};
const closeFeedbackModal = () => {
isFeedbackModalOpen.value = false;
};
const closeSpritesheetSplitter = () => {
isSpritesheetSplitterOpen.value = false;
if (spritesheetImageUrl.value) {
URL.revokeObjectURL(spritesheetImageUrl.value);
spritesheetImageUrl.value = '';
}
spritesheetImageFile.value = null;
};
const openGifFpsModal = () => {
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
alert('Please upload or import sprites before generating a GIF.');
return;
}
isGifFpsModalOpen.value = true;
};
const closeGifFpsModal = () => {
isGifFpsModalOpen.value = false;
};
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
processImageFiles(spriteFiles.map(s => s.file));
};
const openJSONImportDialog = () => {
jsonFileInput.value?.click();
};
const handleJSONFileChange = async (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
await handleJSONImport(input.files[0]);
input.value = '';
}
};
onMounted(() => {
const hasShownFeedbackPopup = localStorage.getItem('hasShownFeedbackPopup');
if (!hasShownFeedbackPopup) {
setTimeout(() => {
showFeedbackPopup.value = true;
}, 3000);
}
});
const handleFeedbackPopupResponse = (showModal: boolean) => {
showFeedbackPopup.value = false;
localStorage.setItem('hasShownFeedbackPopup', 'true');
if (showModal) {
openFeedbackModal();
}
};
const openFileDialog = () => {
uploadInput.value?.click();
};
const handleUploadChange = async (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
await handleSpritesUpload(Array.from(input.files));
input.value = '';
}
};
const startEditingLayer = (layerId: string, currentName: string) => {
editingLayerId.value = layerId;
editingLayerName.value = currentName;
// Focus the input on next tick
setTimeout(() => {
layerNameInput.value?.focus();
layerNameInput.value?.select();
}, 0);
};
const finishEditingLayer = () => {
if (editingLayerId.value && editingLayerName.value.trim()) {
const layer = layers.value.find(l => l.id === editingLayerId.value);
if (layer) {
layer.name = editingLayerName.value.trim();
}
}
editingLayerId.value = null;
editingLayerName.value = '';
};
const cancelEditingLayer = () => {
editingLayerId.value = null;
editingLayerName.value = '';
};
</script>