@@ -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 s tate -- >
< 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 S tate -- >
< 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 = "p rose 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 > : U pload a nd 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 u pload i ndividual s prite 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 s idebar - 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" >
<!-- S idebar 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-8 00 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-6 00 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-4 00 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/5 0 dark:bg -gray-8 00/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 p anel - 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 ga p-1" >
<!-- Main C anvas 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 c ursor-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 d uration-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 c ursor-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 d uration-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 >
< / butto n>
<!-- 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 < / spa n>
< 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/5 0" >
<!-- Grid Background Pattern -- >
< div
class = "absolute inset-0 opacity-5 pointer-events-none bg-gray-500 "
style = "b ackground-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 - 9 a - fA - F ] { 3 } | [ 0 - 9 a - fA - F ] { 6 } | [ 0 - 9 a - 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 openJSONImport Dialog = ( ) => {
jsonFileInput . value ? . click ( ) ;
} ;
const openFile Dialog = ( ) => 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 >