[FEAT] Improved UI
This commit is contained in:
@@ -1,326 +1,441 @@
|
||||
<template>
|
||||
<main class="flex flex-col flex-1 bg-white/80 dark:bg-gray-900/80 backdrop-blur-md rounded-3xl shadow-2xl border border-gray-200/50 dark:border-gray-700/50 transition-all duration-300" :class="{ 'lg:overflow-hidden': layers.some(l => l.sprites.length) }">
|
||||
<!-- Welcome state -->
|
||||
<div v-if="!layers.some(l => l.sprites.length)" class="p-6 sm:p-10">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 mb-1">
|
||||
<p class="text-2xl font-bold text-gray-800 dark:text-gray-100">Upload sprites or single image</p>
|
||||
<a href="https://ko-fi.com/X8X416D44P" target="_blank" rel="noopener noreferrer">
|
||||
<img height="36" style="border: 0px; height: 36px" src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" alt="Buy Me a Coffee at ko-fi.com" />
|
||||
</a>
|
||||
</div>
|
||||
<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" />
|
||||
<main class="flex flex-col flex-1 h-full min-h-0 relative">
|
||||
<!-- Welcome / Empty State -->
|
||||
<div v-if="!layers.some(l => l.sprites.length)" class="flex-1 flex flex-col items-center justify-center pb-12">
|
||||
<div class="w-full max-w-[90rem] px-4 sm:px-6 lg:px-8 flex flex-col gap-8 lg:gap-12 items-start pt-4 sm:pt-8 lg:pt-16">
|
||||
<!-- Top Row: Upload Field & Video Side by Side -->
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-6 lg:gap-8">
|
||||
<!-- File Uploader Component -->
|
||||
<div class="glass-panel p-2 rounded-2xl shadow-xl shadow-indigo-500/10 border border-indigo-50/50 dark:border-gray-700 h-full flex flex-col">
|
||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||
</div>
|
||||
|
||||
<div class="mt-10">
|
||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Welcome to Spritesheet generator</h1>
|
||||
<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>
|
||||
<h3 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-4">Key features of this sprite editor</h3>
|
||||
<ul class="text-gray-700 dark:text-gray-300 mb-6 space-y-2 list-disc">
|
||||
<li><strong>Free sprite editor</strong>: Edit, organize, and optimize your game sprites directly in your browser</li>
|
||||
<li><strong>Automatic spritesheet generation</strong>: Convert multiple PNG, JPG, or GIF images into efficient sprite atlases</li>
|
||||
<li><strong>Customizable grid layouts</strong>: Adjust spacing, padding, and arrangement for pixel-perfect results</li>
|
||||
<li><strong>Animation preview</strong>: Test your sprite animations before exporting</li>
|
||||
<li><strong>Cross-platform compatibility</strong>: Works with Unity, Godot, Phaser, Pygame, and other game engines</li>
|
||||
<li><strong>Zero installation required</strong>: No downloads - use our web-based sprite sheet maker instantly</li>
|
||||
<li><strong>Batch processing</strong>: Upload and process multiple sprites simultaneously</li>
|
||||
<li><strong>Export options</strong>: Download spritesheet as PNG, JPG, GIF, ZIP or JSON.</li>
|
||||
</ul>
|
||||
<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-gray-800 dark:text-gray-200"></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/demo.mp4" type="video/mp4" />
|
||||
</video>
|
||||
<!-- Video Showcase -->
|
||||
<div class="rounded-2xl overflow-hidden shadow-2xl border border-gray-200 dark:border-gray-800 bg-gray-900 relative group h-full min-h-[300px] lg:min-h-0">
|
||||
<video autoplay controls loop muted playsinline class="w-full h-full object-cover opacity-90 group-hover:opacity-100 transition-opacity duration-500">
|
||||
<source :src="tutVideo" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Section: Hero Text & Features -->
|
||||
<div class="w-full grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-12">
|
||||
<!-- Hero Text -->
|
||||
<div class="text-left">
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl font-extrabold tracking-tight mb-6 text-gray-900 dark:text-gray-50 leading-[1.1]">Welcome to <span class="text-indigo-600 dark:text-indigo-400">Spritesheet generator</span></h1>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 leading-relaxed">
|
||||
Create spritesheets for your game development and animation projects with our completely free, open-source Spritesheet generator. 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>
|
||||
|
||||
<!-- Key Features Grid -->
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2"><i class="fas fa-star text-yellow-500 text-sm"></i> Key features</h2>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 text-left">
|
||||
<!-- Features List -->
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-edit text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Free sprite editor</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Edit, organize, and optimize directly in browser.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-magic text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Automatic generation</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Convert images into efficient sprite atlases.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-th text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Customizable layouts</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Adjust spacing for pixel-perfect results.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-play-circle text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Animation preview</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Test animations before exporting.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-gamepad text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Cross-platform</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Works with Unity, Godot, etc.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-globe text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">No installation</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Use our web-based tool instantly.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-layer-group text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Batch processing</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Upload/process multiple sprites.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card p-4 hover:border-indigo-500/30 transition-colors flex gap-4 items-start">
|
||||
<div class="w-8 h-8 rounded-lg bg-indigo-100 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400 shrink-0">
|
||||
<i class="fas fa-file-export text-sm"></i>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-sm text-gray-900 dark:text-gray-100 mb-0.5">Export options</h3>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 leading-snug">Download PNG, JSON, ZIP, or GIF.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Two-column layout: Left controls, Right preview -->
|
||||
<div v-if="layers.some(l => l.sprites.length)" class="flex flex-col flex-1 lg:grid lg:grid-cols-[320px_1fr] xl:grid-cols-[360px_1fr] lg:overflow-hidden min-h-0">
|
||||
<!-- Left sidebar - Controls (TIGHT!) -->
|
||||
<div class="p-4 bg-gray-50/50 dark:bg-gray-900/30 border-b lg:border-b-0 lg:border-r border-gray-200 dark:border-gray-700 lg:overflow-y-auto lg:overflow-x-hidden lg:min-h-0">
|
||||
<div class="space-y-4">
|
||||
<!-- Add Sprites Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-plus-circle text-[10px]"></i>
|
||||
Add Sprites
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
class="flex-1 p-3 text-center border border-dashed rounded-lg transition-all cursor-pointer focus:outline-none group"
|
||||
:class="[isDragging ? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' : 'border-gray-300 dark:border-gray-600 hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50']"
|
||||
@click="openFileDialog"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<i class="fas fa-image text-lg mb-1 transition-colors" :class="[isDragging ? 'text-blue-500' : 'text-gray-400 group-hover:text-gray-600']"></i>
|
||||
<p class="text-xs font-medium" :class="[isDragging ? 'text-blue-600' : 'text-gray-500 group-hover:text-gray-700']">
|
||||
{{ isDragging ? 'Drop here' : 'Images' }}
|
||||
</p>
|
||||
</button>
|
||||
<button @click="openJSONImportDialog" class="flex-1 p-3 text-center border border-gray-300 dark:border-gray-600 rounded-lg hover:border-gray-500 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all cursor-pointer group" data-rybbit-event="import-json">
|
||||
<i class="fas fa-file-code text-lg mb-1 text-gray-400 group-hover:text-gray-600 transition-colors"></i>
|
||||
<p class="text-xs font-medium text-gray-500 group-hover:text-gray-700">Import JSON</p>
|
||||
</button>
|
||||
</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>
|
||||
<!-- Main Editor Interface -->
|
||||
<div v-else class="flex flex-col lg:flex-row gap-6 h-full min-h-[600px] lg:overflow-hidden">
|
||||
<!-- Sidebar Controls -->
|
||||
<aside class="flex flex-col w-full lg:w-[340px] gap-4 shrink-0 lg:overflow-hidden">
|
||||
<div class="glass-panel rounded-xl flex flex-col h-full lg:overflow-hidden border border-gray-200 dark:border-gray-700/60 shadow-lg">
|
||||
<!-- Sidebar Header -->
|
||||
<div class="px-5 py-4 border-b border-gray-200/50 dark:border-gray-700/50 flex items-center justify-between shrink-0 bg-gray-50/50 dark:bg-gray-800/10">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider text-gray-500 dark:text-gray-400">Editor Tools</h2>
|
||||
<button @click="openFileDialog" class="btn btn-primary btn-sm shadow-indigo-500/20" title="Add more sprites"><i class="fas fa-plus mr-1"></i> Add</button>
|
||||
</div>
|
||||
|
||||
<!-- Layers Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-layer-group text-[10px]"></i>
|
||||
Layers
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
<button @click="addLayer()" class="text-xs px-2 py-0.5 bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 rounded text-gray-600 dark:text-gray-300 transition-colors cursor-pointer" title="Add new layer">
|
||||
<i class="fas fa-plus text-[9px]"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div
|
||||
v-for="layer in layers"
|
||||
:key="layer.id"
|
||||
class="flex items-center gap-1.5 px-2 py-1.5 bg-white dark:bg-gray-800 border rounded-md transition-all text-sm"
|
||||
:class="[layer.id === activeLayerId ? 'border-gray-700 ring-1 ring-gray-700 dark:border-gray-400 dark:ring-gray-400' : 'border-gray-200 dark:border-gray-700', !layer.visible ? 'opacity-40' : '']"
|
||||
>
|
||||
<button @click.stop="layer.visible = !layer.visible" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" :title="layer.visible ? 'Hide' : 'Show'">
|
||||
<i :class="[layer.visible ? 'fas fa-eye text-gray-600 dark:text-gray-300' : 'fas fa-eye-slash text-gray-400', 'text-xs']"></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-1.5 py-0.5 text-xs border border-gray-700 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-100 rounded outline-none min-w-0"
|
||||
ref="layerNameInput"
|
||||
@click.stop
|
||||
/>
|
||||
<button v-else @click="activeLayerId = layer.id" class="flex-1 text-xs font-medium text-left truncate cursor-pointer min-w-0" :class="layer.id === activeLayerId ? 'text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'">
|
||||
{{ layer.name }}<span v-if="layer.sprites.length" class="ml-1 opacity-50">({{ layer.sprites.length }})</span>
|
||||
</button>
|
||||
<div class="flex items-center gap-0.5" v-if="editingLayerId !== layer.id">
|
||||
<button @click="startEditingLayer(layer.id, layer.name)" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Rename">
|
||||
<i class="fas fa-pen text-[9px] text-gray-400"></i>
|
||||
<!-- Scrollable Content -->
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden p-5 space-y-6 scrollbar-thin">
|
||||
<!-- Layers -->
|
||||
<section>
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">Layers</h3>
|
||||
<button @click="addLayer()" class="text-xs p-1 text-indigo-600 dark:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors" title="Add Layer"><i class="fas fa-layer-group mr-1"></i> New</button>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<div
|
||||
v-for="layer in layers"
|
||||
:key="layer.id"
|
||||
class="group flex items-center gap-2 p-2 rounded-lg border transition-all duration-200"
|
||||
:class="activeLayerId === layer.id ? 'bg-white dark:bg-gray-800 border-indigo-500 ring-1 ring-indigo-500 shadow-sm' : 'bg-gray-50 dark:bg-gray-800/40 border-transparent hover:border-gray-200 dark:hover:border-gray-700'"
|
||||
>
|
||||
<!-- Visibility Toggle -->
|
||||
<button @click.stop="layer.visible = !layer.visible" class="w-6 h-6 flex items-center justify-center rounded text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors">
|
||||
<i :class="layer.visible ? 'fas fa-eye' : 'fas fa-eye-slash'" class="text-xs"></i>
|
||||
</button>
|
||||
<button @click="moveLayer(layer.id, 'up')" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Move up">
|
||||
<i class="fas fa-chevron-up text-[9px] text-gray-400"></i>
|
||||
|
||||
<!-- Layer Name -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<input
|
||||
v-if="editingLayerId === layer.id"
|
||||
v-model="editingLayerName"
|
||||
@blur="finishEditingLayer"
|
||||
@keyup.enter="finishEditingLayer"
|
||||
@keyup.esc="cancelEditingLayer"
|
||||
ref="layerNameInput"
|
||||
class="w-full text-sm bg-transparent border-b border-indigo-500 outline-none p-0 text-gray-900 dark:text-gray-100"
|
||||
/>
|
||||
<button v-else @click="activeLayerId = layer.id" class="w-full text-left text-sm font-medium truncate" :class="activeLayerId === layer.id ? 'text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-400'">
|
||||
{{ layer.name }} <span class="text-xs opacity-50 font-normal ml-1">({{ layer.sprites.length }})</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button @click="startEditingLayer(layer.id, layer.name)" class="p-1.5 text-gray-400 hover:text-indigo-500 transition-colors" title="Rename"><i class="fas fa-pen text-[10px]"></i></button>
|
||||
<button @click="moveLayer(layer.id, 'up')" class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" title="Move Up"><i class="fas fa-chevron-up text-[10px]"></i></button>
|
||||
<button @click="moveLayer(layer.id, 'down')" class="p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors" title="Move Down"><i class="fas fa-chevron-down text-[10px]"></i></button>
|
||||
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="p-1.5 text-gray-400 hover:text-red-500 transition-colors" title="Delete"><i class="fas fa-trash text-[10px]"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Canvas Grid Settings -->
|
||||
<section>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Grid Layout</h3>
|
||||
<div class="card p-3 bg-gray-50/50 dark:bg-gray-800/40 space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Columns</label>
|
||||
<input type="number" v-model.number="columns" min="1" max="20" class="input-field w-16 text-center" />
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600 dark:text-gray-300">Force Size</label>
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" />
|
||||
</div>
|
||||
<div v-if="settingsStore.manualCellSizeEnabled" class="flex items-center gap-1">
|
||||
<input v-model.number="settingsStore.manualCellWidth" class="input-field w-14 text-center px-1" placeholder="W" />
|
||||
<span class="text-gray-400 text-xs">x</span>
|
||||
<input v-model.number="settingsStore.manualCellHeight" class="input-field w-14 text-center px-1" placeholder="H" />
|
||||
</div>
|
||||
<span v-else class="text-xs font-mono text-gray-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">{{ cellSize.width }}×{{ cellSize.height }}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- View Options -->
|
||||
<section>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">View Options</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 grid grid-cols-2 gap-2">
|
||||
<Tooltip text="Disable anti-aliasing for crisp pixel art rendering">
|
||||
<button
|
||||
@click="settingsStore.pixelPerfect = !settingsStore.pixelPerfect"
|
||||
:class="settingsStore.pixelPerfect ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-th mr-2"></i>Pixel
|
||||
</button>
|
||||
<button @click="moveLayer(layer.id, 'down')" class="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Move down">
|
||||
<i class="fas fa-chevron-down text-[9px] text-gray-400"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show checkerboard background">
|
||||
<button
|
||||
@click="settingsStore.checkerboardEnabled = !settingsStore.checkerboardEnabled"
|
||||
:class="settingsStore.checkerboardEnabled ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-chess-board mr-2"></i>Grid
|
||||
</button>
|
||||
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 transition-colors cursor-pointer" title="Delete">
|
||||
<i class="fas fa-trash text-[9px] text-red-400"></i>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show selection borders">
|
||||
<button
|
||||
@click="showActiveBorder = !showActiveBorder"
|
||||
:class="showActiveBorder ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-vector-square mr-2"></i>Borders
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Show sprite coordinates">
|
||||
<button
|
||||
@click="showOffsetLabels = !showOffsetLabels"
|
||||
:class="showOffsetLabels ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-tag mr-2"></i>Labels
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Compare with ghost overlays" class="col-span-2">
|
||||
<button
|
||||
@click="showAllSprites = !showAllSprites"
|
||||
:class="showAllSprites ? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-clone mr-2"></i>Ghost compare
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Tools -->
|
||||
<section>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Tools</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 space-y-2">
|
||||
<div class="flex gap-2">
|
||||
<Tooltip text="Select multiple sprites" class="flex-1">
|
||||
<button
|
||||
@click="isMultiSelectMode = !isMultiSelectMode"
|
||||
:class="isMultiSelectMode ? 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-object-group mr-2"></i>Multi
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Swap cell positions" class="flex-1">
|
||||
<button
|
||||
@click="allowCellSwap = !allowCellSwap"
|
||||
:class="allowCellSwap ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300' : 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700'"
|
||||
class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium transition-colors"
|
||||
>
|
||||
<i class="fas fa-exchange-alt mr-2"></i>Swap
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex items-center justify-between px-2 pt-1 border-t border-gray-100 dark:border-gray-700/50">
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">Negative Spacing</span>
|
||||
<button
|
||||
@click="settingsStore.negativeSpacingEnabled = !settingsStore.negativeSpacingEnabled"
|
||||
class="relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
:class="settingsStore.negativeSpacingEnabled ? 'bg-indigo-600' : 'bg-gray-200 dark:bg-gray-700'"
|
||||
>
|
||||
<span class="inline-block h-3 w-3 transform rounded-full bg-white transition-transform" :class="settingsStore.negativeSpacingEnabled ? 'translate-x-5' : 'translate-x-1'" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- Grid & Cell Size Section (Combined) -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-th text-[10px]"></i>
|
||||
Grid Layout
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-2.5 space-y-2">
|
||||
<!-- Columns -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label for="columns" class="text-xs font-medium text-gray-600 dark:text-gray-300">Columns</label>
|
||||
<input id="columns" type="number" v-model.number="columns" min="1" max="10" class="w-14 px-2 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 focus:ring-1 focus:ring-gray-500 outline-none" />
|
||||
<!-- Alignment Tools -->
|
||||
<section>
|
||||
<h3 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-3">Alignment</h3>
|
||||
<div class="card p-2 bg-gray-50/50 dark:bg-gray-800/40 grid grid-cols-3 gap-2">
|
||||
<Tooltip text="Align sprites to the left edge">
|
||||
<button @click="alignSprites('left')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-left"></i>Left</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Center sprites horizontally">
|
||||
<button @click="alignSprites('center')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrows-left-right"></i>Center</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the right edge">
|
||||
<button @click="alignSprites('right')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-right"></i>Right</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the top edge">
|
||||
<button @click="alignSprites('top')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-up"></i>Top</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Center sprites vertically">
|
||||
<button @click="alignSprites('middle')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrows-up-down"></i>Middle</button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Align sprites to the bottom edge">
|
||||
<button @click="alignSprites('bottom')" class="w-full flex items-center justify-start gap-2 p-2 rounded text-xs font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"><i class="fas fa-arrow-down"></i>Bottom</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<!-- Cell Size -->
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs font-medium text-gray-600 dark:text-gray-300 flex items-center gap-1.5">
|
||||
Cell Size
|
||||
<input type="checkbox" v-model="settingsStore.manualCellSizeEnabled" class="w-3 h-3 rounded" title="Manual override" />
|
||||
</label>
|
||||
<div v-if="settingsStore.manualCellSizeEnabled" class="flex items-center gap-1">
|
||||
<input type="number" v-model.number="settingsStore.manualCellWidth" min="1" max="2048" class="w-12 px-1.5 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 outline-none" placeholder="W" />
|
||||
<span class="text-gray-400 text-xs">×</span>
|
||||
<input type="number" v-model.number="settingsStore.manualCellHeight" min="1" max="2048" class="w-12 px-1.5 py-1 text-xs text-center border border-gray-300 dark:border-gray-600 rounded bg-gray-50 dark:bg-gray-700 dark:text-gray-100 outline-none" placeholder="H" />
|
||||
</div>
|
||||
<span v-else class="text-xs font-mono text-gray-400">{{ cellSize.width }}×{{ cellSize.height }}px</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Sprite Alignment Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-align-center text-[10px]"></i>
|
||||
Align All Sprites
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
<!-- Sidebar Footer (Export) -->
|
||||
<div class="p-4 border-t border-gray-200/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/10 space-y-3 shrink-0">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<button @click="downloadSpritesheet" class="btn btn-secondary btn-sm justify-start" title="Download PNG"><i class="fas fa-image text-indigo-500 w-4"></i> PNG</button>
|
||||
<button @click="exportSpritesheetJSON" class="btn btn-secondary btn-sm justify-start" title="Download JSON"><i class="fas fa-code text-indigo-500 w-4"></i> JSON</button>
|
||||
<button @click="openGifFpsModal" class="btn btn-secondary btn-sm justify-start" title="Export GIF"><i class="fas fa-film text-pink-500 w-4"></i> GIF</button>
|
||||
<button @click="downloadAsZip" class="btn btn-secondary btn-sm justify-start" title="Download ZIP"><i class="fas fa-file-archive text-yellow-500 w-4"></i> ZIP</button>
|
||||
</div>
|
||||
<div class="grid grid-cols-6 gap-1">
|
||||
<button @click="alignSprites('left')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Left">
|
||||
<i class="fas fa-arrow-left text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('center')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Center Horizontally">
|
||||
<i class="fas fa-arrows-left-right text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('right')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Right">
|
||||
<i class="fas fa-arrow-right text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('top')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Top">
|
||||
<i class="fas fa-arrow-up text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('middle')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Center Vertically">
|
||||
<i class="fas fa-arrows-up-down text-xs text-gray-500"></i>
|
||||
</button>
|
||||
<button @click="alignSprites('bottom')" class="p-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors cursor-pointer" title="Align Bottom">
|
||||
<i class="fas fa-arrow-down text-xs text-gray-500"></i>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Export Section -->
|
||||
<section>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<h3 class="flex items-center gap-1.5 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide">
|
||||
<i class="fas fa-download text-[10px]"></i>
|
||||
Download & Share
|
||||
</h3>
|
||||
<div class="flex-1 h-px bg-gray-200 dark:bg-gray-700"></div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-1.5 mb-2">
|
||||
<button
|
||||
@click="downloadSpritesheet"
|
||||
class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer"
|
||||
data-rybbit-event="download-spritesheet"
|
||||
title="Download as PNG image"
|
||||
>
|
||||
<i class="fas fa-image text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">PNG</span>
|
||||
</button>
|
||||
<button
|
||||
@click="exportSpritesheetJSON"
|
||||
class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer"
|
||||
data-rybbit-event="export-json"
|
||||
title="Export project data as JSON"
|
||||
>
|
||||
<i class="fas fa-file-code text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">JSON</span>
|
||||
</button>
|
||||
<button @click="openGifFpsModal" class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer" data-rybbit-event="download-gif" title="Export as animated GIF">
|
||||
<i class="fas fa-film text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">GIF</span>
|
||||
</button>
|
||||
<button @click="downloadAsZip" class="flex flex-col items-center justify-center p-2.5 bg-gray-700 hover:bg-gray-800 dark:bg-gray-600 dark:hover:bg-gray-500 rounded-lg text-white transition-colors cursor-pointer" data-rybbit-event="download-zip" title="Download all sprites as ZIP">
|
||||
<i class="fas fa-file-archive text-sm mb-0.5"></i>
|
||||
<span class="text-[10px] font-medium">ZIP</span>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
@click="openShareModal"
|
||||
class="w-full flex items-center justify-center gap-2 p-2.5 bg-gradient-to-r from-blue-500 to-purple-600 hover:from-blue-600 hover:to-purple-700 rounded-lg text-white text-sm font-medium transition-all shadow-sm hover:shadow-md cursor-pointer"
|
||||
data-rybbit-event="share-spritesheet"
|
||||
title="Generate shareable link"
|
||||
>
|
||||
<i class="fas fa-share-alt text-sm"></i>
|
||||
Share Online
|
||||
</button>
|
||||
</section>
|
||||
<button @click="openShareModal" class="btn btn-secondary w-full justify-start"><i class="fas fa-share-alt mr-2 text-indigo-500"></i> Share Project</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Right panel - Tabs -->
|
||||
<div class="flex flex-col overflow-hidden min-h-0">
|
||||
<!-- Tab Navigation -->
|
||||
<div class="bg-gray-50/50 dark:bg-gray-900/30 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between gap-1 px-3 py-2">
|
||||
<div class="flex gap-1">
|
||||
<!-- Main Canvas Area -->
|
||||
<div class="flex-1 flex flex-col min-h-0 glass-panel rounded-xl border border-gray-200 dark:border-gray-700/60 shadow-lg overflow-hidden">
|
||||
<!-- Tabs / Toolbar -->
|
||||
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-200 dark:border-gray-700 bg-gray-50/30 dark:bg-gray-800/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex p-1 bg-gray-100 dark:bg-gray-800 rounded-lg">
|
||||
<button
|
||||
@click="activeTab = 'canvas'"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer border',
|
||||
activeTab === 'canvas' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm border-gray-300 dark:border-gray-600' : 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50 border-transparent',
|
||||
]"
|
||||
:class="activeTab === 'canvas' ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<i class="fas fa-th text-xs"></i>
|
||||
<span>Canvas</span>
|
||||
<i class="fas fa-th mr-2"></i>Editor
|
||||
</button>
|
||||
<button
|
||||
@click="activeTab = 'preview'"
|
||||
:class="[
|
||||
'flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-all cursor-pointer border',
|
||||
activeTab === 'preview' ? 'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-sm border-gray-300 dark:border-gray-600' : 'text-gray-500 dark:text-gray-400 hover:bg-white/50 dark:hover:bg-gray-800/50 border-transparent',
|
||||
]"
|
||||
data-rybbit-event="preview-animation"
|
||||
:class="activeTab === 'preview' ? 'bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 shadow-sm' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
|
||||
class="px-4 py-1.5 rounded-md text-sm font-medium transition-all duration-200"
|
||||
>
|
||||
<i class="fas fa-play text-xs"></i>
|
||||
<span>Animation</span>
|
||||
<i class="fas fa-play mr-2"></i>Preview
|
||||
</button>
|
||||
</div>
|
||||
<button @click="openShareModal" class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md bg-gradient-to-r from-blue-500 to-purple-600 text-white shadow-sm hover:shadow-md transition-all cursor-pointer" data-rybbit-event="share-spritesheet-header">
|
||||
<i class="fas fa-share-alt text-[10px]"></i>
|
||||
<span>Share</span>
|
||||
</button>
|
||||
|
||||
<!-- Background Color (Compact) -->
|
||||
<div class="flex items-center gap-2 pl-3 border-l border-gray-200 dark:border-gray-700">
|
||||
<span class="text-xs font-medium text-gray-500 uppercase">Bg</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<select v-model="bgSelectValue" class="text-xs border-none bg-transparent dark:text-gray-300 focus:ring-0 cursor-pointer pr-8">
|
||||
<option value="transparent">None</option>
|
||||
<option value="#ffffff">White</option>
|
||||
<option value="#000000">Black</option>
|
||||
<option value="#f9fafb">Gray</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
<div v-if="bgSelectValue === 'custom'" class="relative w-5 h-5 rounded-full overflow-hidden border border-gray-300 dark:border-gray-600 shadow-sm">
|
||||
<input type="color" v-model="customColor" @input="updateCustomColor" class="absolute -top-1 -left-1 w-8 h-8 cursor-pointer p-0 border-0 opacity-0" />
|
||||
<div class="w-full h-full" :style="{ backgroundColor: customColor }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
||||
<button @click="zoomOut" class="p-1.5 hover:bg-white dark:hover:bg-gray-700 rounded text-gray-500 transition-colors"><i class="fas fa-minus text-xs"></i></button>
|
||||
<span class="text-xs font-mono w-12 text-center text-gray-600 dark:text-gray-300">{{ Math.round(zoom * 100) }}%</span>
|
||||
<button @click="zoomIn" class="p-1.5 hover:bg-white dark:hover:bg-gray-700 rounded text-gray-500 transition-colors"><i class="fas fa-plus text-xs"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
<div class="p-4 lg:flex-1 lg:overflow-auto lg:min-h-0">
|
||||
<div v-if="activeTab === 'canvas'" class="h-full">
|
||||
<sprite-canvas
|
||||
:layers="layers"
|
||||
:active-layer-id="activeLayerId"
|
||||
:columns="columns"
|
||||
@update-sprite="updateSpritePosition"
|
||||
@update-sprite-cell="updateSpriteCell"
|
||||
@remove-sprite="removeSprite"
|
||||
@remove-sprites="removeSprites"
|
||||
@replace-sprite="replaceSprite"
|
||||
@add-sprite="addSprite"
|
||||
@rotate-sprite="rotateSprite"
|
||||
@flip-sprite="flipSprite"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab === 'preview'" class="h-full">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" />
|
||||
<!-- Canvas Content -->
|
||||
<div class="flex-1 overflow-hidden relative bg-white dark:bg-gray-900/50">
|
||||
<!-- Grid Background Pattern -->
|
||||
<div
|
||||
class="absolute inset-0 opacity-5 pointer-events-none bg-gray-500"
|
||||
style="background-image: url('data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2220%22 height=%2220%22><circle cx=%2210%22 cy=%2210%22 r=%221%22 fill=%22%236b7280%22/></svg>'); background-size: 20px 20px"
|
||||
></div>
|
||||
|
||||
<div class="h-full overflow-auto custom-scrollbar p-4">
|
||||
<div v-if="activeTab === 'canvas'" class="h-full flex flex-col justify-center">
|
||||
<sprite-canvas
|
||||
:layers="layers"
|
||||
:active-layer-id="activeLayerId"
|
||||
:columns="columns"
|
||||
:zoom="zoom"
|
||||
:is-multi-select-mode="isMultiSelectMode"
|
||||
:show-active-border="showActiveBorder"
|
||||
:allow-cell-swap="allowCellSwap"
|
||||
:show-all-sprites="showAllSprites"
|
||||
:show-offset-labels="showOffsetLabels"
|
||||
@update-sprite="updateSpritePosition"
|
||||
@update-sprite-cell="updateSpriteCell"
|
||||
@remove-sprite="removeSprite"
|
||||
@remove-sprites="removeSprites"
|
||||
@replace-sprite="replaceSprite"
|
||||
@add-sprite="addSprite"
|
||||
@rotate-sprite="rotateSprite"
|
||||
@flip-sprite="flipSprite"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab === 'preview'" class="h-full flex items-center justify-center">
|
||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" @drop-sprite="handleDropSprite" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modals & Hidden Inputs -->
|
||||
<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" />
|
||||
<ShareModal :is-open="isShareModalOpen" :share-function="shareFunction" @close="closeShareModal" />
|
||||
|
||||
<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" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, toRef, computed } from 'vue';
|
||||
import { ref, toRef, computed, watch, nextTick } from 'vue';
|
||||
import FileUploader from '@/components/FileUploader.vue';
|
||||
import SpriteCanvas from '@/components/SpriteCanvas.vue';
|
||||
import SpritePreview from '@/components/SpritePreview.vue';
|
||||
import SpritesheetSplitter from '@/components/SpritesheetSplitter.vue';
|
||||
import GifFpsModal from '@/components/GifFpsModal.vue';
|
||||
import ShareModal from '@/components/ShareModal.vue';
|
||||
import Tooltip from '@/components/utilities/Tooltip.vue';
|
||||
import { useExportLayers } from '@/composables/useExportLayers';
|
||||
import { useShare } from '@/composables/useShare';
|
||||
import { useLayers } from '@/composables/useLayers';
|
||||
@@ -328,9 +443,10 @@
|
||||
import { useSettingsStore } from '@/stores/useSettingsStore';
|
||||
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
|
||||
import { useHomeViewSEO } from './HomeView.seo';
|
||||
import { useZoom } from '@/composables/useZoom';
|
||||
import type { SpriteFile } from '@/types/sprites';
|
||||
import tutVideo from '@/assets/tut2.mp4';
|
||||
|
||||
// Initialize SEO
|
||||
useHomeViewSEO();
|
||||
|
||||
const settingsStore = useSettingsStore();
|
||||
@@ -347,13 +463,69 @@
|
||||
toRef(settingsStore, 'manualCellHeight')
|
||||
);
|
||||
|
||||
// Zoom Control
|
||||
const {
|
||||
zoom,
|
||||
increase: zoomIn,
|
||||
decrease: zoomOut,
|
||||
reset: resetZoom,
|
||||
} = useZoom({
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
step: 0.25,
|
||||
initial: 1,
|
||||
});
|
||||
|
||||
// View Options & Tools
|
||||
const isMultiSelectMode = ref(false);
|
||||
const showActiveBorder = ref(true);
|
||||
const allowCellSwap = ref(false);
|
||||
const showAllSprites = ref(false);
|
||||
const showOffsetLabels = ref(false);
|
||||
const customColor = ref('#ffffff');
|
||||
const isCustomMode = ref(false);
|
||||
|
||||
// Background Color Logic
|
||||
const presetBgColors = ['transparent', '#ffffff', '#000000', '#f9fafb'] as const;
|
||||
const isHexColor = (val: string) => /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(val);
|
||||
|
||||
if (isHexColor(settingsStore.backgroundColor) && !presetBgColors.includes(settingsStore.backgroundColor as any)) {
|
||||
customColor.value = settingsStore.backgroundColor;
|
||||
isCustomMode.value = true;
|
||||
}
|
||||
|
||||
const bgSelectValue = computed<string>({
|
||||
get() {
|
||||
if (isCustomMode.value) return 'custom';
|
||||
const val = settingsStore.backgroundColor;
|
||||
if (presetBgColors.includes(val as any)) return val;
|
||||
if (isHexColor(val)) {
|
||||
customColor.value = val;
|
||||
isCustomMode.value = true;
|
||||
return 'custom';
|
||||
}
|
||||
return 'transparent';
|
||||
},
|
||||
set(v: string) {
|
||||
if (v === 'custom') {
|
||||
isCustomMode.value = true;
|
||||
settingsStore.setBackgroundColor(customColor.value);
|
||||
} else {
|
||||
isCustomMode.value = false;
|
||||
settingsStore.setBackgroundColor(v);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const updateCustomColor = () => {
|
||||
settingsStore.setBackgroundColor(customColor.value);
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -362,18 +534,21 @@
|
||||
|
||||
const cellSize = computed(getCellSize);
|
||||
const activeTab = ref<'canvas' | 'preview'>('canvas');
|
||||
|
||||
const isSpritesheetSplitterOpen = ref(false);
|
||||
const isGifFpsModalOpen = ref(false);
|
||||
const isShareModalOpen = ref(false);
|
||||
|
||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||
const spritesheetImageUrl = ref('');
|
||||
const spritesheetImageFile = ref<File | null>(null);
|
||||
|
||||
const editingLayerId = ref<string | null>(null);
|
||||
const editingLayerName = ref('');
|
||||
const layerNameInput = ref<HTMLInputElement | null>(null);
|
||||
const isDragging = ref(false);
|
||||
|
||||
// Upload Handlers
|
||||
const handleSpritesUpload = async (files: File[]) => {
|
||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||
|
||||
@@ -395,7 +570,6 @@
|
||||
isSpritesheetSplitterOpen.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
processImageFiles([file]);
|
||||
};
|
||||
img.onerror = () => {
|
||||
@@ -403,9 +577,6 @@
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
console.error('Failed to read image file:', file.name);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
return;
|
||||
}
|
||||
@@ -413,13 +584,6 @@
|
||||
processImageFiles(files);
|
||||
};
|
||||
|
||||
const handleDrop = (event: DragEvent) => {
|
||||
isDragging.value = false;
|
||||
if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
|
||||
handleSpritesUpload(Array.from(event.dataTransfer.files));
|
||||
}
|
||||
};
|
||||
|
||||
const handleJSONImport = async (jsonFile: File) => {
|
||||
try {
|
||||
await importSpritesheetJSON(jsonFile);
|
||||
@@ -448,13 +612,9 @@
|
||||
isGifFpsModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeGifFpsModal = () => {
|
||||
isGifFpsModalOpen.value = false;
|
||||
};
|
||||
const closeGifFpsModal = () => (isGifFpsModalOpen.value = false);
|
||||
|
||||
// Share functionality
|
||||
const { share } = useShare(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'), toRef(settingsStore, 'backgroundColor'), toRef(settingsStore, 'manualCellSizeEnabled'), toRef(settingsStore, 'manualCellWidth'), toRef(settingsStore, 'manualCellHeight'));
|
||||
|
||||
const shareFunction = () => share();
|
||||
|
||||
const openShareModal = () => {
|
||||
@@ -465,17 +625,13 @@
|
||||
isShareModalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeShareModal = () => {
|
||||
isShareModalOpen.value = false;
|
||||
};
|
||||
const closeShareModal = () => (isShareModalOpen.value = false);
|
||||
|
||||
const handleSplitSpritesheet = (spriteFiles: SpriteFile[]) => {
|
||||
processImageFiles(spriteFiles.map(s => s.file));
|
||||
};
|
||||
|
||||
const openJSONImportDialog = () => {
|
||||
jsonFileInput.value?.click();
|
||||
};
|
||||
const openFileDialog = () => uploadInput.value?.click();
|
||||
|
||||
const handleJSONFileChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
@@ -485,10 +641,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
const openFileDialog = () => {
|
||||
uploadInput.value?.click();
|
||||
};
|
||||
|
||||
const handleUploadChange = async (event: Event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files && input.files.length > 0) {
|
||||
@@ -497,10 +649,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Layer Editing
|
||||
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();
|
||||
@@ -510,9 +662,7 @@
|
||||
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();
|
||||
}
|
||||
if (layer) layer.name = editingLayerName.value.trim();
|
||||
}
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
@@ -522,4 +672,57 @@
|
||||
editingLayerId.value = null;
|
||||
editingLayerName.value = '';
|
||||
};
|
||||
|
||||
const handleDropSprite = (layerId: string, frameIndex: number, files: File[]) => {
|
||||
const layer = layers.value.find(l => l.id === layerId);
|
||||
if (!layer) return;
|
||||
|
||||
files.forEach((file, fileIdx) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
const url = e.target?.result as string;
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
const sprite = {
|
||||
id: crypto.randomUUID(),
|
||||
file,
|
||||
img,
|
||||
url,
|
||||
width: img.width,
|
||||
height: img.height,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
};
|
||||
const insertIndex = frameIndex + fileIdx;
|
||||
if (insertIndex < layer.sprites.length) {
|
||||
layer.sprites = [...layer.sprites.slice(0, insertIndex), sprite, ...layer.sprites.slice(insertIndex + 1)];
|
||||
} else {
|
||||
while (layer.sprites.length < insertIndex) {
|
||||
layer.sprites.push({
|
||||
id: crypto.randomUUID(),
|
||||
file: new File([], 'empty'),
|
||||
img: new Image(),
|
||||
url: '',
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
flipX: false,
|
||||
flipY: false,
|
||||
});
|
||||
}
|
||||
layer.sprites = [...layer.sprites, sprite];
|
||||
}
|
||||
};
|
||||
img.src = url;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
|
||||
Reference in New Issue
Block a user