Compare commits
3 Commits
f8b4e98f9c
...
9fa2bbd15d
| Author | SHA1 | Date | |
|---|---|---|---|
| 9fa2bbd15d | |||
| b3f530870e | |||
| 56858701ef |
10
index.html
10
index.html
@@ -14,24 +14,24 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<!-- Primary Meta Tags -->
|
||||
<title>Spritesheet Generator - Create Game Spritesheets Online</title>
|
||||
<meta name="title" content="Spritesheet Generator - Create Game Spritesheets Online">
|
||||
<title>Spritesheet generator - Create Game Spritesheets Online</title>
|
||||
<meta name="title" content="Spritesheet generator - Create Game Spritesheets Online">
|
||||
<meta name="description" content="Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.">
|
||||
<meta name="keywords" content="spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools">
|
||||
<meta name="keywords" content="Spritesheet generator, sprite sheet maker, game development, pixel art, sprite animation, game assets, 2D game tools">
|
||||
<meta name="author" content="nu11ed">
|
||||
<meta name="robots" content="index, follow">
|
||||
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:type" content="website">
|
||||
<meta property="og:url" content="https://spritesheetgenerator.online/">
|
||||
<meta property="og:title" content="Spritesheet Generator - Create Game Spritesheets Online">
|
||||
<meta property="og:title" content="Spritesheet generator - Create Game Spritesheets Online">
|
||||
<meta property="og:description" content="Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.">
|
||||
<meta property="og:image" content="/og-image.png">
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content="https://spritesheetgenerator.online/">
|
||||
<meta property="twitter:title" content="Spritesheet Generator - Create Game Spritesheets Online">
|
||||
<meta property="twitter:title" content="Spritesheet generator - Create Game Spritesheets Online">
|
||||
<meta property="twitter:description" content="Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.">
|
||||
<meta property="twitter:image" content="/og-image.png">
|
||||
|
||||
|
||||
459
src/App.vue
459
src/App.vue
@@ -1,164 +1,272 @@
|
||||
<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-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 />
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-4 mb-4 sm:mb-8">
|
||||
<a href="https://gitea.adhd.sh/root/spritesheet-generator" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="source-link"> <i class="fab fa-github"></i> Source </a>
|
||||
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="discord-link"> <i class="fab fa-discord"></i> Discord </a>
|
||||
<a href="#" @click.prevent="openHelpModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="help-link"> <i class="fas fa-question-circle"></i> Help </a>
|
||||
<a href="#" @click.prevent="openFeedbackModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="feedback-link"> <i class="fas fa-comment-dots"></i> Feedback </a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-soft dark:shadow-gray-900/30 p-4 sm:p-8 transition-colors duration-300">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-center mb-4 sm:mb-6 gap-3">
|
||||
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-100">Upload sprites</h2>
|
||||
<button
|
||||
@click="openJSONImportDialog"
|
||||
class="w-full sm:w-auto px-4 py-2 bg-indigo-500 hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center sm:justify-start space-x-2"
|
||||
data-rybbit-event="import-json"
|
||||
>
|
||||
<i class="fas fa-file-import"></i>
|
||||
<span>Import JSON</span>
|
||||
</button>
|
||||
</div>
|
||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
|
||||
<div v-if="!visibleLayers.some(l => l.sprites.length)" class="mt-8">
|
||||
<div class="mt-2 leading-relaxed space-y-2">
|
||||
<p>Create spritesheets for your game development and animation projects with our completely free, open-source spritesheet generator.</p>
|
||||
<p>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>
|
||||
<p class="font-bold text-2xl pb-3 pt-2">How it works:</p>
|
||||
<video controls playsinline class="w-full h-full object-contain rounded-lg shadow-md" title="Spritesheet generator tutorial" aria-label="Spritesheet generator tutorial">
|
||||
<source src="@/assets/tut2.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<div class="min-h-screen p-4 sm:p-8 bg-gradient-to-br from-slate-50 via-blue-50 to-slate-50 dark:from-gray-950 dark:via-gray-900 dark:to-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 bg-gradient-to-r from-blue-600 to-indigo-600 dark:from-blue-400 dark:to-indigo-400 bg-clip-text text-transparent 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>
|
||||
|
||||
<div v-if="visibleLayers.some(l => l.sprites.length)" class="mt-8">
|
||||
<div class="flex flex-col gap-3 mb-4">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="text-gray-700 dark:text-gray-200 font-medium">Layers</span>
|
||||
<button @click="addLayer()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-100 rounded">Add</button>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div v-for="layer in layers" :key="layer.id" class="flex items-center gap-2 px-2 py-1 rounded border border-gray-200 dark:border-gray-600" :class="{ 'ring-2 ring-blue-500': layer.id === activeLayerId }">
|
||||
<button @click="activeLayerId = layer.id" class="px-2 py-0.5 rounded bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200">{{ layer.name }}</button>
|
||||
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300"> <input type="checkbox" v-model="layer.visible" /> Visible </label>
|
||||
<button @click="moveLayer(layer.id, 'up')" class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">↑</button>
|
||||
<button @click="moveLayer(layer.id, 'down')" class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded">↓</button>
|
||||
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="text-xs px-1.5 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-200 rounded">Remove</button>
|
||||
<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" />
|
||||
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
|
||||
|
||||
<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 class="flex flex-wrap items-center justify-center sm:justify-start gap-3 sm:gap-6 mb-6 sm:mb-8">
|
||||
<div class="flex items-center space-x-1">
|
||||
<label for="columns" class="text-gray-700 dark:text-gray-200 font-medium">Columns:</label>
|
||||
<input
|
||||
id="columns"
|
||||
type="number"
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-1">
|
||||
<label class="text-gray-700 dark:text-gray-200 font-medium flex items-center gap-1">
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="w-4 h-4" />
|
||||
Cell size:
|
||||
</label>
|
||||
<input
|
||||
v-if="settingsStore.manualCellSizeEnabled"
|
||||
type="number"
|
||||
v-model.number="settingsStore.manualCellWidth"
|
||||
min="1"
|
||||
max="2048"
|
||||
class="w-20 px-2 py-1 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-sm"
|
||||
placeholder="Width"
|
||||
/>
|
||||
<span v-if="settingsStore.manualCellSizeEnabled" class="text-gray-600 dark:text-gray-300">×</span>
|
||||
<input
|
||||
v-if="settingsStore.manualCellSizeEnabled"
|
||||
type="number"
|
||||
v-model.number="settingsStore.manualCellHeight"
|
||||
min="1"
|
||||
max="2048"
|
||||
class="w-20 px-2 py-1 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-sm"
|
||||
placeholder="Height"
|
||||
/>
|
||||
<span v-if="!settingsStore.manualCellSizeEnabled" class="text-gray-600 dark:text-gray-300">{{ cellSize.width }} × {{ cellSize.height }}px</span>
|
||||
</div>
|
||||
|
||||
<!-- Add mass position buttons -->
|
||||
<div class="flex flex-wrap items-center justify-center gap-2">
|
||||
<button @click="alignSprites('left')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Left" data-rybbit-event="align-left">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('center')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Center" data-rybbit-event="align-center">
|
||||
<i class="fas fa-arrows-left-right"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('right')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Right" data-rybbit-event="align-right">
|
||||
<i class="fas fa-arrow-right"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('top')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Top" data-rybbit-event="align-top">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('middle')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Middle" data-rybbit-event="align-middle">
|
||||
<i class="fas fa-arrows-up-down"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('bottom')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Bottom" data-rybbit-event="align-bottom">
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="downloadSpritesheet" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-spritesheet">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>Download spritesheet</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@click="exportSpritesheetJSON"
|
||||
class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-purple-500 hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2"
|
||||
data-rybbit-event="export-json"
|
||||
>
|
||||
<i class="fas fa-file-code"></i>
|
||||
<span>Export as JSON</span>
|
||||
</button>
|
||||
|
||||
<button @click="openGifFpsModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-gif">
|
||||
<i class="fas fa-film"></i>
|
||||
<span>Download as GIF</span>
|
||||
</button>
|
||||
|
||||
<button @click="downloadAsZip" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-teal-500 hover:bg-teal-600 dark:bg-teal-600 dark:hover:bg-teal-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-zip">
|
||||
<i class="fas fa-file-archive"></i>
|
||||
<span>Download as ZIP</span>
|
||||
</button>
|
||||
|
||||
<button @click="openPreviewModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="preview-animation">
|
||||
<i class="fas fa-play"></i>
|
||||
<span>Preview animation</span>
|
||||
</button>
|
||||
</div>
|
||||
<sprite-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"
|
||||
@add-sprite-with-resize="addSpriteWithResize"
|
||||
/>
|
||||
</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">
|
||||
<button
|
||||
@click="openPreviewModal"
|
||||
class="col-span-2 px-3 py-2 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 text-white rounded-lg transition-all text-xs font-medium flex items-center justify-center gap-1.5 cursor-pointer"
|
||||
data-rybbit-event="preview-animation"
|
||||
>
|
||||
<i class="fas fa-play"></i>
|
||||
<span>Preview animation</span>
|
||||
</button>
|
||||
<!-- 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-2 mt-2">
|
||||
<input type="number" v-model.number="settingsStore.manualCellWidth" min="1" max="2048" class="flex-1 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">×</span>
|
||||
<input type="number" v-model.number="settingsStore.manualCellHeight" min="1" max="2048" class="flex-1 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">{{ 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 - Canvas Preview -->
|
||||
<div class="p-6 overflow-y-auto overflow-x-auto max-h-[calc(100vh-200px)]">
|
||||
<sprite-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"
|
||||
@add-sprite-with-resize="addSpriteWithResize"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" title="Animation preview">
|
||||
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" title="Animation Preview">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" />
|
||||
</Modal>
|
||||
|
||||
@@ -173,10 +281,10 @@
|
||||
<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>
|
||||
<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">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">Share feedback</button>
|
||||
<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>
|
||||
@@ -236,9 +344,13 @@
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const uploadInput = 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'));
|
||||
@@ -363,4 +475,43 @@
|
||||
openFeedbackModal();
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const handleUploadChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
const files = Array.from(input.files);
|
||||
await handleSpritesUpload(files);
|
||||
if (uploadInput.value) uploadInput.value.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>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<div
|
||||
class="border-2 border-dashed rounded-xl p-4 sm:p-8 text-center transition-all duration-200"
|
||||
class="relative border-3 border-dashed rounded-2xl p-8 sm:p-12 text-center transition-all duration-300 cursor-pointer group overflow-hidden"
|
||||
:class="{
|
||||
'border-blue-300 bg-blue-50 dark:border-blue-500 dark:bg-blue-900/30': isDragging,
|
||||
'border-gray-200 hover:border-blue-300 hover:bg-gray-50 dark:border-gray-600 dark:hover:border-blue-500 dark:hover:bg-gray-700/50': !isDragging,
|
||||
'border-blue-400 bg-gradient-to-br from-blue-50 to-indigo-50 dark:border-blue-400 dark:from-blue-900/40 dark:to-indigo-900/40 scale-[1.02]': isDragging,
|
||||
'border-gray-300 bg-gradient-to-br from-gray-50/50 to-slate-50/50 hover:border-blue-400 hover:from-blue-50/80 hover:to-indigo-50/80 dark:border-gray-600 dark:from-gray-800/30 dark:to-gray-700/30 dark:hover:border-blue-400 dark:hover:from-blue-900/30 dark:hover:to-indigo-900/30': !isDragging,
|
||||
}"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@@ -12,19 +12,38 @@
|
||||
@click="openFileDialog"
|
||||
data-rybbit-event="file-upload-area"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-400/0 to-indigo-400/0 group-hover:from-blue-400/5 group-hover:to-indigo-400/5 transition-all duration-300"></div>
|
||||
|
||||
<input ref="fileInput" type="file" multiple accept="image/*,.json" class="hidden" @change="handleFileChange" />
|
||||
|
||||
<div class="mb-4 sm:mb-6">
|
||||
<img src="@/assets/images/file.svg" alt="File upload" class="w-16 h-16 sm:w-20 sm:h-20 mx-auto mb-2 sm:mb-4 opacity-75 dark:invert" />
|
||||
<div class="relative z-10">
|
||||
<div class="mb-6 transform transition-transform duration-300" :class="isDragging ? 'scale-110' : 'group-hover:scale-105'">
|
||||
<div class="w-20 h-20 sm:w-24 sm:h-24 mx-auto mb-4 bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50 rounded-2xl flex items-center justify-center shadow-lg">
|
||||
<i class="fas fa-cloud-upload-alt text-4xl sm:text-5xl text-blue-600 dark:text-blue-400"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="text-xl sm:text-2xl font-bold text-gray-800 dark:text-gray-100 mb-3">
|
||||
<span v-if="isDragging">Drop your files here</span>
|
||||
<span v-else>Upload Your Sprites</span>
|
||||
</h3>
|
||||
<p class="text-base sm:text-lg text-gray-600 dark:text-gray-300 mb-2">Drag and drop sprite images or JSON files</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-8">Supports PNG, JPG, GIF, and JSON</p>
|
||||
|
||||
<div class="flex items-center justify-center gap-4 mb-6">
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-gray-600"></div>
|
||||
<span class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase">or</span>
|
||||
<div class="h-px flex-1 bg-gray-300 dark:bg-gray-600"></div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-8 py-3.5 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 dark:from-blue-600 dark:to-indigo-700 dark:hover:from-blue-700 dark:hover:to-indigo-800 text-white font-semibold rounded-xl shadow-lg hover:shadow-xl transition-all transform hover:scale-105 flex items-center justify-center gap-3 mx-auto"
|
||||
data-rybbit-event="select-files"
|
||||
>
|
||||
<i class="fas fa-folder-open text-lg"></i>
|
||||
<span>Browse Files</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-lg sm:text-xl font-medium text-gray-700 dark:text-gray-200 mb-2">Drag and drop your sprite images or JSON file here</p>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4 sm:mb-6">or</p>
|
||||
|
||||
<button class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors flex sm:inline-flex items-center justify-center space-x-2 cursor-pointer" data-rybbit-event="select-files">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span>Select files</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,87 +1,100 @@
|
||||
<template>
|
||||
<div class="p-2 bg-cyan-600 rounded w-full my-4">
|
||||
<p>Developer's tip: Right click a sprite to open the context menu and add, replace or remove sprites.</p>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-3 sm:gap-0">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3 sm:gap-4 w-full sm:w-auto">
|
||||
<div class="flex items-center">
|
||||
<input id="pixel-perfect" type="checkbox" v-model="settingsStore.pixelPerfect" class="mr-2 w-4 h-4" @change="requestDraw" />
|
||||
<label for="pixel-perfect" class="dark:text-gray-200 text-sm sm:text-base">Pixel perfect rendering</label>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input id="allow-cell-swap" type="checkbox" v-model="allowCellSwap" class="mr-2 w-4 h-4" />
|
||||
<label for="allow-cell-swap" class="dark:text-gray-200 text-sm sm:text-base">Allow moving between cells</label>
|
||||
</div>
|
||||
<!-- Add new checkbox for showing all sprites -->
|
||||
<div class="flex items-center">
|
||||
<input id="show-all-sprites" type="checkbox" v-model="showAllSprites" class="mr-2" />
|
||||
<label for="show-all-sprites" class="dark:text-gray-200">Compare sprites</label>
|
||||
</div>
|
||||
<!-- Negative spacing control -->
|
||||
<div class="flex items-center">
|
||||
<input id="negative-spacing" type="checkbox" v-model="settingsStore.negativeSpacingEnabled" class="mr-2 w-4 h-4" />
|
||||
<label for="negative-spacing" class="dark:text-gray-200 text-sm sm:text-base">Negative spacing</label>
|
||||
</div>
|
||||
<!-- Background color picker -->
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="bg-color" class="dark:text-gray-200 text-sm sm:text-base">Background:</label>
|
||||
<select id="bg-color" v-model="bgSelectValue" class="px-2 py-1 border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 dark:text-gray-200 text-sm">
|
||||
<option value="transparent">Transparent</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Light Gray</option>
|
||||
<option value="custom">Custom...</option>
|
||||
</select>
|
||||
<input v-if="bgSelectValue === 'custom'" type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="w-8 h-8 border border-gray-300 dark:border-gray-600 rounded cursor-pointer" />
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gradient-to-r from-cyan-500 to-blue-500 dark:from-cyan-600 dark:to-blue-600 rounded-xl p-4 shadow-lg border border-cyan-400/50 dark:border-cyan-500/50">
|
||||
<div class="flex items-start gap-3">
|
||||
<i class="fas fa-lightbulb text-yellow-300 text-xl mt-0.5"></i>
|
||||
<div>
|
||||
<h4 class="font-semibold text-white mb-1">Pro Tip</h4>
|
||||
<p class="text-cyan-50 text-sm">Right-click any sprite to open the context menu for quick actions: add, replace, or remove sprites.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative border border-gray-300 dark:border-gray-600 rounded-lg overflow-auto">
|
||||
<!-- Zoom controls - visible on all screen sizes and positioned outside cells -->
|
||||
<div class="relative flex space-x-2 bg-white/90 dark:bg-gray-800/90 p-2 rounded-lg shadow-md z-20">
|
||||
<button @click="zoomIn" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
|
||||
<i class="fas fa-plus"></i>
|
||||
</button>
|
||||
<button @click="zoomOut" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
|
||||
<i class="fas fa-minus"></i>
|
||||
</button>
|
||||
<button @click="resetZoom" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
|
||||
<i class="fas fa-expand"></i>
|
||||
</button>
|
||||
<section>
|
||||
<h3 class="text-lg font-bold text-gray-800 dark:text-gray-100 mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-cog text-blue-600 dark:text-blue-400"></i>
|
||||
Canvas Options
|
||||
</h3>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
|
||||
<input id="pixel-perfect" type="checkbox" v-model="settingsStore.pixelPerfect" class="w-4 h-4 rounded" @change="requestDraw" />
|
||||
<span class="text-sm font-medium dark:text-gray-200">Pixel Perfect</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
|
||||
<input id="allow-cell-swap" type="checkbox" v-model="allowCellSwap" class="w-4 h-4 rounded" />
|
||||
<span class="text-sm font-medium dark:text-gray-200">Cell Swapping</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
|
||||
<input id="show-all-sprites" type="checkbox" v-model="showAllSprites" class="w-4 h-4 rounded" />
|
||||
<span class="text-sm font-medium dark:text-gray-200">Compare Sprites</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
|
||||
<input id="negative-spacing" type="checkbox" v-model="settingsStore.negativeSpacingEnabled" class="w-4 h-4 rounded" />
|
||||
<span class="text-sm font-medium dark:text-gray-200">Negative Spacing</span>
|
||||
</label>
|
||||
<div class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600">
|
||||
<label for="bg-color" class="text-sm font-medium dark:text-gray-200">Background:</label>
|
||||
<select id="bg-color" v-model="bgSelectValue" class="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 dark:text-gray-200 text-sm focus:ring-2 focus:ring-blue-500 outline-none transition-all">
|
||||
<option value="transparent">Transparent</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Light Gray</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
<input v-if="bgSelectValue === 'custom'" type="color" v-model="customColor" @input="settingsStore.setBackgroundColor(customColor)" class="w-10 h-10 border-2 border-gray-300 dark:border-gray-600 rounded-lg cursor-pointer" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600">
|
||||
<span class="text-sm font-medium dark:text-gray-200 mr-1">Zoom:</span>
|
||||
<button @click="zoomIn" class="p-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded transition-all" title="Zoom In">
|
||||
<i class="fas fa-plus text-xs"></i>
|
||||
</button>
|
||||
<button @click="zoomOut" class="p-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded transition-all" title="Zoom Out">
|
||||
<i class="fas fa-minus text-xs"></i>
|
||||
</button>
|
||||
<button @click="resetZoom" class="p-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded transition-all" title="Reset Zoom">
|
||||
<i class="fas fa-expand text-xs"></i>
|
||||
</button>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 px-4 py-2.5 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-600 cursor-pointer hover:border-blue-400 dark:hover:border-blue-500 transition-all">
|
||||
<input id="show-offset-labels" type="checkbox" v-model="showOffsetLabels" class="w-4 h-4 rounded" />
|
||||
<span class="text-sm font-medium dark:text-gray-200">Show Offset Labels</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="canvas-container touch-manipulation relative" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="stopDrag"
|
||||
@mouseleave="stopDrag"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="stopDrag"
|
||||
@contextmenu.prevent
|
||||
@dragover="handleDragOver"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="handleDrop"
|
||||
class="w-full transition-all"
|
||||
:class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }"
|
||||
:style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"
|
||||
></canvas>
|
||||
<div class="relative bg-white dark:bg-gray-800 border-2 border-gray-200 dark:border-gray-700 rounded-2xl shadow-lg overflow-auto">
|
||||
<div class="canvas-container touch-manipulation relative inline-block min-w-full">
|
||||
<div :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }" class="inline-block">
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
@mousedown="startDrag"
|
||||
@mousemove="drag"
|
||||
@mouseup="stopDrag"
|
||||
@mouseleave="stopDrag"
|
||||
@touchstart="handleTouchStart"
|
||||
@touchmove="handleTouchMove"
|
||||
@touchend="stopDrag"
|
||||
@contextmenu.prevent
|
||||
@dragover="handleDragOver"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="onDragLeave"
|
||||
@drop="handleDrop"
|
||||
class="block transition-all"
|
||||
:class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }"
|
||||
:style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<!-- Offset labels in corners -->
|
||||
<div v-if="canvasRef" class="absolute inset-0 pointer-events-none">
|
||||
<!-- Offset labels in corners (not scaled with zoom) -->
|
||||
<div v-if="canvasRef && showOffsetLabels" class="absolute inset-0 pointer-events-none">
|
||||
<div
|
||||
v-for="position in spritePositions"
|
||||
:key="position.id"
|
||||
class="absolute text-[23px] leading-none font-mono text-cyan-600 dark:text-cyan-400 bg-white/90 dark:bg-gray-900/90 px-1 py-0.5 rounded-sm"
|
||||
:style="{
|
||||
// Use the global cell size so labels line up with the actual grid cells
|
||||
left: `calc(${(position.cellX / canvasWidth) * 100}% + ${(gridMetrics.maxWidth / canvasWidth) * 100}% - 2px)`,
|
||||
top: `calc(${(position.cellY / canvasHeight) * 100}% + ${(gridMetrics.maxHeight / canvasHeight) * 100}% - 2px)`,
|
||||
// Position at bottom-right corner of each cell, scaled by zoom
|
||||
left: `${(position.cellX + position.maxWidth) * zoom - 2}px`,
|
||||
top: `${(position.cellY + position.maxHeight) * zoom - 2}px`,
|
||||
transform: 'translate(-100%, -100%)',
|
||||
}"
|
||||
>
|
||||
@@ -91,23 +104,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Context Menu -->
|
||||
<div v-if="showContextMenu" class="fixed bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg z-50 py-1" :style="{ left: contextMenuX + 'px', top: contextMenuY + 'px' }">
|
||||
<button @click="addSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 flex items-center gap-2">
|
||||
<i class="fas fa-plus"></i>
|
||||
Add sprite
|
||||
<div v-if="showContextMenu" class="fixed bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-xl shadow-2xl z-50 py-2 min-w-[200px] overflow-hidden" :style="{ left: contextMenuX + 'px', top: contextMenuY + 'px' }">
|
||||
<button @click="addSprite" class="w-full px-5 py-3 text-left hover:bg-blue-50 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-200 flex items-center gap-3 transition-colors font-medium">
|
||||
<i class="fas fa-plus text-blue-600 dark:text-blue-400"></i>
|
||||
<span>Add Sprite</span>
|
||||
</button>
|
||||
<button v-if="contextMenuSpriteId" @click="replaceSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 flex items-center gap-2">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
Replace sprite
|
||||
<button v-if="contextMenuSpriteId" @click="replaceSprite" class="w-full px-5 py-3 text-left hover:bg-purple-50 dark:hover:bg-purple-900/30 text-gray-700 dark:text-gray-200 flex items-center gap-3 transition-colors font-medium">
|
||||
<i class="fas fa-exchange-alt text-purple-600 dark:text-purple-400"></i>
|
||||
<span>Replace Sprite</span>
|
||||
</button>
|
||||
<button v-if="contextMenuSpriteId" @click="removeSprite" class="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-red-600 dark:text-red-400 flex items-center gap-2">
|
||||
<div v-if="contextMenuSpriteId" class="h-px bg-gray-200 dark:bg-gray-600 my-1"></div>
|
||||
<button v-if="contextMenuSpriteId" @click="removeSprite" class="w-full px-5 py-3 text-left hover:bg-red-50 dark:hover:bg-red-900/30 text-red-600 dark:text-red-400 flex items-center gap-3 transition-colors font-medium">
|
||||
<i class="fas fa-trash"></i>
|
||||
Remove sprite
|
||||
<span>Remove Sprite</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Hidden file input for replace functionality -->
|
||||
<input ref="fileInput" type="file" accept="image/*" class="hidden" @change="handleFileChange" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -186,6 +198,7 @@
|
||||
calculateMaxDimensions,
|
||||
} = useDragSprite({
|
||||
sprites: computed(() => props.layers.find(l => l.id === props.activeLayerId)?.sprites ?? []),
|
||||
layers: toRef(props, 'layers'),
|
||||
columns: toRef(props, 'columns'),
|
||||
zoom,
|
||||
allowCellSwap,
|
||||
@@ -208,6 +221,7 @@
|
||||
});
|
||||
|
||||
const showAllSprites = ref(false);
|
||||
const showOffsetLabels = ref(true);
|
||||
const showContextMenu = ref(false);
|
||||
const contextMenuX = ref(0);
|
||||
const contextMenuY = ref(0);
|
||||
@@ -282,10 +296,6 @@
|
||||
// Grid metrics used to position offset labels relative to cell size
|
||||
const gridMetrics = computed(() => calculateMaxDimensions());
|
||||
|
||||
// Reactive canvas dimensions to ensure label positions update when canvas size changes
|
||||
const canvasWidth = ref(0);
|
||||
const canvasHeight = ref(0);
|
||||
|
||||
const startDrag = (event: MouseEvent) => {
|
||||
if (!canvasRef.value) return;
|
||||
|
||||
@@ -388,9 +398,6 @@
|
||||
const newCanvasWidth = maxWidth * props.columns;
|
||||
const newCanvasHeight = maxHeight * rows;
|
||||
canvas2D.setCanvasSize(newCanvasWidth, newCanvasHeight);
|
||||
// Update reactive dimensions for template-driven elements (like labels)
|
||||
if (canvasWidth.value !== newCanvasWidth) canvasWidth.value = newCanvasWidth;
|
||||
if (canvasHeight.value !== newCanvasHeight) canvasHeight.value = newCanvasHeight;
|
||||
|
||||
// Clear canvas
|
||||
canvas2D.clear();
|
||||
@@ -414,19 +421,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
// If showing all sprites, draw all sprites with transparency in each cell
|
||||
// If showing all sprites, overlay all sprites from all visible layers ghosted in each cell
|
||||
if (showAllSprites.value) {
|
||||
const total = Math.max(...props.layers.map(l => (l.visible ? l.sprites.length : 0)));
|
||||
for (let cellIndex = 0; cellIndex < total; cellIndex++) {
|
||||
const visibleLayers = props.layers.filter(l => l.visible);
|
||||
const maxSprites = Math.max(...visibleLayers.map(l => l.sprites.length), 0);
|
||||
|
||||
for (let cellIndex = 0; cellIndex < maxSprites; cellIndex++) {
|
||||
const cellCol = cellIndex % props.columns;
|
||||
const cellRow = Math.floor(cellIndex / props.columns);
|
||||
const cellX = Math.floor(cellCol * maxWidth);
|
||||
const cellY = Math.floor(cellRow * maxHeight);
|
||||
|
||||
props.layers.forEach(layer => {
|
||||
if (!layer.visible) return;
|
||||
const sprite = layer.sprites[cellIndex];
|
||||
if (sprite) canvas2D.drawImage(sprite.img, cellX + negativeSpacing + sprite.x, cellY + negativeSpacing + sprite.y, 0.35);
|
||||
// Draw all sprites from all visible layers ghosted in this cell
|
||||
visibleLayers.forEach(layer => {
|
||||
layer.sprites.forEach((sprite, spriteIndex) => {
|
||||
// Skip drawing the sprite that belongs in this cell (it's already drawn below)
|
||||
if (spriteIndex === cellIndex) return;
|
||||
// Skip the active sprite if we're dragging it
|
||||
if (activeSpriteId.value === sprite.id && ghostSprite.value) return;
|
||||
|
||||
if (sprite && sprite.img) {
|
||||
canvas2D.drawImage(sprite.img, cellX + negativeSpacing + sprite.x, cellY + negativeSpacing + sprite.y, 0.25);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<div class="spritesheet-preview w-full">
|
||||
<!-- Main Layout: Canvas Left, Controls Right -->
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<!-- Canvas Area (Left/Main) -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="relative bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg overflow-auto min-h-[300px] sm:min-h-[520px] shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||
<canvas
|
||||
@@ -32,10 +30,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Controls Sidebar (Right) -->
|
||||
<div class="lg:w-80 xl:w-96 flex-shrink-0">
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 space-y-4">
|
||||
<!-- Playback Controls -->
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide">Playback</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -214,14 +210,20 @@
|
||||
const showAllSprites = ref(false);
|
||||
|
||||
const compositeFrames = computed<Sprite[]>(() => {
|
||||
const v = getVisibleLayers();
|
||||
const len = maxFrames();
|
||||
const arr: Sprite[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
const s = v.find(l => l.sprites[i])?.sprites[i];
|
||||
if (s) arr.push(s);
|
||||
// Show frames from the active layer for the thumbnail list
|
||||
const activeLayer = props.layers.find(l => l.id === props.activeLayerId);
|
||||
if (!activeLayer) {
|
||||
// Fallback to first visible layer if no active layer
|
||||
const v = getVisibleLayers();
|
||||
const len = maxFrames();
|
||||
const arr: Sprite[] = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
const s = v.find(l => l.sprites[i])?.sprites[i];
|
||||
if (s) arr.push(s);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
return arr;
|
||||
return activeLayer.sprites;
|
||||
});
|
||||
|
||||
const currentFrameSprite = computed<Sprite | null>(() => {
|
||||
@@ -283,6 +285,7 @@
|
||||
const frameIndex = currentFrameIndex.value;
|
||||
|
||||
if (showAllSprites.value) {
|
||||
// When comparing sprites, show all frames from all visible layers (dimmed)
|
||||
const len = maxFrames();
|
||||
for (let i = 0; i < len; i++) {
|
||||
if (i === frameIndex || hiddenFrames.value.includes(i)) continue;
|
||||
@@ -294,6 +297,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Always draw current frame from all visible layers
|
||||
visibleLayers.forEach(layer => {
|
||||
const sprite = layer.sprites[frameIndex];
|
||||
if (!sprite) return;
|
||||
@@ -421,6 +425,7 @@
|
||||
|
||||
// Watchers
|
||||
watch(() => props.layers, drawPreviewCanvas, { deep: true });
|
||||
watch(() => props.activeLayerId, drawPreviewCanvas);
|
||||
watch(currentFrameIndex, drawPreviewCanvas);
|
||||
watch(zoom, drawPreviewCanvas);
|
||||
watch(isDraggable, drawPreviewCanvas);
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
<template>
|
||||
<button @click="settingsStore.toggleDarkMode()" class="p-2 rounded-lg transition-colors" :class="settingsStore.darkMode ? 'text-yellow-400 hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'" aria-label="Toggle dark mode" data-rybbit-event="toggle-dark-mode">
|
||||
<i :class="settingsStore.darkMode ? 'fas fa-sun' : 'fas fa-moon'"></i>
|
||||
<button
|
||||
@click="settingsStore.toggleDarkMode()"
|
||||
class="relative p-3 rounded-xl transition-all duration-300 shadow-lg hover:shadow-xl group"
|
||||
:class="settingsStore.darkMode ? 'bg-gradient-to-br from-indigo-600 to-purple-700 text-yellow-300' : 'bg-gradient-to-br from-blue-500 to-indigo-600 text-white'"
|
||||
aria-label="Toggle dark mode"
|
||||
data-rybbit-event="toggle-dark-mode"
|
||||
>
|
||||
<div class="relative w-6 h-6 flex items-center justify-center">
|
||||
<i :class="settingsStore.darkMode ? 'fas fa-sun' : 'fas fa-moon'" class="text-xl transition-all duration-300 group-hover:scale-110"></i>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ref, computed, type Ref, type ComputedRef } from 'vue';
|
||||
import type { Sprite } from '@/types/sprites';
|
||||
import type { Sprite, Layer } from '@/types/sprites';
|
||||
import { getMaxDimensions } from './useSprites';
|
||||
import { calculateNegativeSpacing } from './useNegativeSpacing';
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface SpritePosition {
|
||||
|
||||
export interface DragSpriteOptions {
|
||||
sprites: Ref<Sprite[]> | ComputedRef<Sprite[]> | Sprite[];
|
||||
layers?: Ref<Layer[]> | ComputedRef<Layer[]> | Layer[];
|
||||
columns: Ref<number> | number;
|
||||
zoom?: Ref<number>;
|
||||
allowCellSwap?: Ref<boolean>;
|
||||
@@ -46,6 +47,7 @@ export function useDragSprite(options: DragSpriteOptions) {
|
||||
|
||||
// Helper to get reactive values
|
||||
const getSprites = () => (Array.isArray(options.sprites) ? options.sprites : options.sprites.value);
|
||||
const getLayers = () => (options.layers ? (Array.isArray(options.layers) ? options.layers : options.layers.value) : null);
|
||||
const getColumns = () => (typeof options.columns === 'number' ? options.columns : options.columns.value);
|
||||
const getZoom = () => options.zoom?.value ?? 1;
|
||||
const getAllowCellSwap = () => options.allowCellSwap?.value ?? false;
|
||||
@@ -73,7 +75,6 @@ export function useDragSprite(options: DragSpriteOptions) {
|
||||
const lastMaxHeight = ref(1);
|
||||
|
||||
const calculateMaxDimensions = () => {
|
||||
const sprites = getSprites();
|
||||
const negativeSpacingEnabled = getNegativeSpacingEnabled();
|
||||
const manualCellSizeEnabled = getManualCellSizeEnabled();
|
||||
|
||||
@@ -87,8 +88,13 @@ export function useDragSprite(options: DragSpriteOptions) {
|
||||
return { maxWidth, maxHeight, negativeSpacing };
|
||||
}
|
||||
|
||||
// Otherwise, calculate based on sprite dimensions
|
||||
const base = getMaxDimensions(sprites);
|
||||
// Get all sprites to calculate dimensions from
|
||||
// If layers are provided, use all visible layers; otherwise use current sprites
|
||||
const layers = getLayers();
|
||||
const spritesToMeasure = layers ? layers.filter(l => l.visible).flatMap(l => l.sprites) : getSprites();
|
||||
|
||||
// Otherwise, calculate based on sprite dimensions across all visible layers
|
||||
const base = getMaxDimensions(spritesToMeasure);
|
||||
// When switching back from manual mode, reset to actual sprite dimensions
|
||||
const baseMaxWidth = Math.max(1, base.maxWidth);
|
||||
const baseMaxHeight = Math.max(1, base.maxHeight);
|
||||
@@ -96,7 +102,7 @@ export function useDragSprite(options: DragSpriteOptions) {
|
||||
lastMaxHeight.value = baseMaxHeight;
|
||||
|
||||
// Calculate negative spacing using shared composable
|
||||
const negativeSpacing = calculateNegativeSpacing(sprites, negativeSpacingEnabled);
|
||||
const negativeSpacing = calculateNegativeSpacing(spritesToMeasure, negativeSpacingEnabled);
|
||||
|
||||
// Add negative spacing to expand each cell
|
||||
const maxWidth = baseMaxWidth + negativeSpacing;
|
||||
|
||||
@@ -50,8 +50,8 @@ export const useLayers = () => {
|
||||
cellWidth = settingsStore.manualCellWidth;
|
||||
cellHeight = settingsStore.manualCellHeight;
|
||||
} else {
|
||||
// Use auto-calculated dimensions based on sprite sizes
|
||||
const { maxWidth, maxHeight } = getMaxDimensions(l.sprites);
|
||||
// Use auto-calculated dimensions based on ALL visible layers (not just active layer)
|
||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||
cellWidth = maxWidth;
|
||||
cellHeight = maxHeight;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user