Compare commits

...

4 Commits

Author SHA1 Message Date
root
a54acf447b Updated links 2025-08-04 00:21:22 +02:00
a87da7ee38 npm update, dark mode fix 2025-05-20 22:51:12 +02:00
a76d195579 removed offset indicator 2025-05-18 17:31:43 +02:00
6cf29852e3 TS fix 2025-05-18 17:23:02 +02:00
12 changed files with 725 additions and 810 deletions

View File

@@ -23,14 +23,14 @@
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://sprite-gen.xvx.sh/">
<meta property="og:url" content="https://spritesheetgenerator.online/">
<meta property="og:title" content="Spritesheet Generator - Create Game Spritesheets Online">
<meta property="og:description" content="Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.">
<meta property="og:image" content="/og-image.png">
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image">
<meta property="twitter:url" content="https://sprite-gen.xvx.sh/">
<meta property="twitter:url" content="https://spritesheetgenerator.online/">
<meta property="twitter:title" content="Spritesheet Generator - Create Game Spritesheets Online">
<meta property="twitter:description" content="Free online tool to create spritesheets for game development. Upload sprites, arrange them, and export as a spritesheet with animation preview.">
<meta property="twitter:image" content="/og-image.png">
@@ -41,7 +41,13 @@
<title>Spritesheet generator</title>
<script defer src="https://stats.xvx.sh/script.js" data-website-id="fb1e3dec-3068-4384-b281-68d29f39c7fc"></script>
<script
src="https://a.adhd.sh/api/script.js"
data-site-id="7"
data-track-errors="true"
data-session-replay="true"
defer
></script>
</head>
<body>
<div id="app"></div>

1292
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,8 @@
All notable changes to this project will be documented in this file.
## [1.9.0] - 2025-05-20
- Fixed splitter UI in dark mode
## [1.8.0] - 2025-05-18
- Improved app for mobile users
- Added PayPal donation link

View File

@@ -6,9 +6,9 @@
<dark-mode-toggle />
</div>
<div class="flex flex-wrap justify-center gap-4 mb-4 sm:mb-8">
<a href="https://git.xvx.sh/root/spritesheet-generator" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-umami-event="source-link"> <i class="fab fa-github"></i> Source </a>
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-umami-event="discord-link"> <i class="fab fa-discord"></i> Discord </a>
<a href="#" @click.prevent="openHelpModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-umami-event="help-link"> <i class="fas fa-question-circle"></i> Help </a>
<a href="https://git.xvx.sh/root/spritesheet-generator" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="source-link"> <i class="fab fa-github"></i> Source </a>
<a href="https://discord.gg/JTev3nzeDa" target="_blank" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="discord-link"> <i class="fab fa-discord"></i> Discord </a>
<a href="#" @click.prevent="openHelpModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="help-link"> <i class="fas fa-question-circle"></i> Help </a>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-soft dark:shadow-gray-900/30 p-4 sm:p-8 transition-colors duration-300">
@@ -17,7 +17,7 @@
<button
@click="openJSONImportDialog"
class="w-full sm:w-auto px-4 py-2 bg-indigo-500 hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center sm:justify-start space-x-2"
data-umami-event="import-json"
data-rybbit-event="import-json"
>
<i class="fas fa-file-import"></i>
<span>Import JSON</span>
@@ -42,47 +42,51 @@
<!-- Add mass position buttons -->
<div class="flex flex-wrap items-center justify-center gap-2">
<button @click="alignSprites('left')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Left" data-umami-event="align-left">
<button @click="alignSprites('left')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Left" data-rybbit-event="align-left">
<i class="fas fa-arrow-left"></i>
</button>
<button @click="alignSprites('center')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Center" data-umami-event="align-center">
<button @click="alignSprites('center')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Center" data-rybbit-event="align-center">
<i class="fas fa-arrows-left-right"></i>
</button>
<button @click="alignSprites('right')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Right" data-umami-event="align-right">
<button @click="alignSprites('right')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Right" data-rybbit-event="align-right">
<i class="fas fa-arrow-right"></i>
</button>
<button @click="alignSprites('top')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Top" data-umami-event="align-top">
<button @click="alignSprites('top')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Top" data-rybbit-event="align-top">
<i class="fas fa-arrow-up"></i>
</button>
<button @click="alignSprites('middle')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Middle" data-umami-event="align-middle">
<button @click="alignSprites('middle')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Middle" data-rybbit-event="align-middle">
<i class="fas fa-arrows-up-down"></i>
</button>
<button @click="alignSprites('bottom')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Bottom" data-umami-event="align-bottom">
<button @click="alignSprites('bottom')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Bottom" data-rybbit-event="align-bottom">
<i class="fas fa-arrow-down"></i>
</button>
</div>
<button @click="downloadSpritesheet" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-umami-event="download-spritesheet">
<button @click="downloadSpritesheet" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 dark:bg-blue-600 dark:hover:bg-blue-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-spritesheet">
<i class="fas fa-download"></i>
<span>Download spritesheet</span>
</button>
<button @click="exportSpritesheetJSON" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-purple-500 hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-umami-event="export-json">
<button
@click="exportSpritesheetJSON"
class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-purple-500 hover:bg-purple-600 dark:bg-purple-600 dark:hover:bg-purple-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2"
data-rybbit-event="export-json"
>
<i class="fas fa-file-code"></i>
<span>Export as JSON</span>
</button>
<button @click="openGifFpsModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-umami-event="download-gif">
<button @click="openGifFpsModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-amber-500 hover:bg-amber-600 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-gif">
<i class="fas fa-film"></i>
<span>Download as GIF</span>
</button>
<button @click="downloadAsZip" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-teal-500 hover:bg-teal-600 dark:bg-teal-600 dark:hover:bg-teal-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-umami-event="download-zip">
<button @click="downloadAsZip" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-teal-500 hover:bg-teal-600 dark:bg-teal-600 dark:hover:bg-teal-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="download-zip">
<i class="fas fa-file-archive"></i>
<span>Download as ZIP</span>
</button>
<button @click="openPreviewModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-umami-event="preview-animation">
<button @click="openPreviewModal" class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-green-500 hover:bg-green-600 dark:bg-green-600 dark:hover:bg-green-700 text-white font-medium rounded-lg transition-colors flex items-center justify-center space-x-2" data-rybbit-event="preview-animation">
<i class="fas fa-play"></i>
<span>Preview animation</span>
</button>

View File

@@ -10,7 +10,7 @@
@dragover.prevent
@drop.prevent="handleDrop"
@click="openFileDialog"
data-umami-event="file-upload-area"
data-rybbit-event="file-upload-area"
>
<input ref="fileInput" type="file" multiple accept="image/*,.json" class="hidden" @change="handleFileChange" />
@@ -21,7 +21,7 @@
<p class="text-lg sm:text-xl font-medium text-gray-700 dark:text-gray-200 mb-2">Drag and drop your sprite images or JSON file here</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mb-4 sm:mb-6">or</p>
<button class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors flex sm:inline-flex items-center justify-center space-x-2 cursor-pointer" data-umami-event="select-files">
<button class="w-full sm:w-auto px-6 py-3 sm:py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors flex sm:inline-flex items-center justify-center space-x-2 cursor-pointer" data-rybbit-event="select-files">
<i class="fas fa-folder-open"></i>
<span>Select files</span>
</button>

View File

@@ -4,14 +4,14 @@
<div class="mb-6">
<label for="fps" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Frames Per Second (FPS)</label>
<div class="flex items-center">
<input id="fps" v-model="fpsValue" type="number" min="1" max="60" class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm text-base" data-umami-event="gif-fps-input" />
<input id="fps" v-model="fpsValue" type="number" min="1" max="60" class="block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm text-base" data-rybbit-event="gif-fps-input" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">Higher FPS will result in smoother animation but larger file size.</p>
</div>
<div class="flex flex-col-reverse sm:flex-row sm:justify-end space-y-3 space-y-reverse sm:space-y-0 sm:space-x-3">
<button @click="cancel" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium rounded-lg transition-colors" data-umami-event="gif-cancel">Cancel</button>
<button @click="confirm" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors" data-umami-event="gif-generate">Generate GIF</button>
<button @click="cancel" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 font-medium rounded-lg transition-colors" data-rybbit-event="gif-cancel">Cancel</button>
<button @click="confirm" class="px-4 py-2 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors" data-rybbit-event="gif-generate">Generate GIF</button>
</div>
</div>
</Modal>

View File

@@ -9,7 +9,7 @@
@click="activeTab = index"
class="px-3 sm:px-4 py-2 font-medium text-xs sm:text-sm transition-colors"
:class="activeTab === index ? 'text-blue-600 dark:text-blue-400 border-b-2 border-blue-500 dark:border-blue-400' : 'text-gray-600 dark:text-gray-400 hover:text-blue-500 dark:hover:text-blue-400'"
:data-umami-event="'help-tab-' + tab.name.toLowerCase().replace(/[^a-z0-9]/g, '-')"
:data-rybbit-event="'help-tab-' + tab.name.toLowerCase().replace(/[^a-z0-9]/g, '-')"
>
{{ tab.name }}
</button>
@@ -43,7 +43,9 @@
<div class="max-w-none text-gray-700 dark:text-gray-300">
<p class="mb-4 text-base leading-relaxed">Spritesheet generator is a free, open-source tool for creating spritesheets for game development and animation projects. This tool allows you to upload individual sprite images and arrange them into a spritesheet with customizable layout.</p>
<p class="mb-4 text-base leading-relaxed">
Matrix: root@adhd.sh
</p>
<h4 class="mt-6 mb-3 text-lg font-medium text-gray-900 dark:text-gray-100">How to use:</h4>
<ol class="list-decimal pl-6 space-y-2 mb-4">
<li>Upload your sprite images by dragging and dropping them or clicking the upload area</li>
@@ -68,7 +70,7 @@
<!-- Donations Tab -->
<div v-if="activeTab === 2" class="h-full overflow-y-auto">
<h3 class="text-lg font-semibold mb-4">Support my <a href="https://xvx.sh" title="XVX" target="_blank" class="text-blue-500 hover:text-blue-600 transition-colors">projects</a>.</h3>
<h3 class="text-lg font-semibold mb-4">Support my <a href="https://adhd.sh" title="XVX" target="_blank" class="text-blue-500 hover:text-blue-600 transition-colors">projects</a>.</h3>
<p class="mb-6">
Spritesheet generator is free and open-source software. If you find it useful, please consider supporting the project with a donation. Check out the source code on
@@ -82,8 +84,8 @@
<h4 class="text-md font-medium">PayPal</h4>
</div>
<div class="flex">
<a href="https://paypal.me/DennisPostma298" target="_blank" class="flex-1 p-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-l-md focus:outline-none dark:text-gray-200" data-umami-event="open-paypal">https://paypal.me/DennisPostma298</a>
<button @click="copyToClipboard('https://paypal.me/DennisPostma298')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-umami-event="copy-paypal-link">Copy</button>
<a href="https://paypal.me/DennisPostma298" target="_blank" class="flex-1 p-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-l-md focus:outline-none dark:text-gray-200" data-rybbit-event="open-paypal">https://paypal.me/DennisPostma298</a>
<button @click="copyToClipboard('https://paypal.me/DennisPostma298')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-rybbit-event="copy-paypal-link">Copy</button>
</div>
</div>
@@ -101,7 +103,7 @@
</div>
<div class="flex">
<input type="text" readonly value="bc1ql2a3nxnhfwft7qex0cclj5ar2lfsslvs0aygeq" class="flex-1 p-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-l-md focus:outline-none dark:text-gray-200" />
<button @click="copyToClipboard('bc1ql2a3nxnhfwft7qex0cclj5ar2lfsslvs0aygeq')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-umami-event="copy-btc-address">Copy</button>
<button @click="copyToClipboard('bc1ql2a3nxnhfwft7qex0cclj5ar2lfsslvs0aygeq')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-rybbit-event="copy-btc-address">Copy</button>
</div>
</div>
@@ -117,7 +119,7 @@
</div>
<div class="flex">
<input type="text" readonly value="ltc1qdkn46hpt39ppmhk25ed2eycu7m2pj5cdzuxw84" class="flex-1 p-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-l-md focus:outline-none dark:text-gray-200" />
<button @click="copyToClipboard('ltc1qdkn46hpt39ppmhk25ed2eycu7m2pj5cdzuxw84')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-umami-event="copy-ltc-address">Copy</button>
<button @click="copyToClipboard('ltc1qdkn46hpt39ppmhk25ed2eycu7m2pj5cdzuxw84')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-rybbit-event="copy-ltc-address">Copy</button>
</div>
</div>
@@ -133,7 +135,7 @@
</div>
<div class="flex">
<input type="text" readonly value="0x30843c72DF6E9A9226d967bf2403602f1C2aB67b" class="flex-1 p-2 text-sm bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-600 rounded-l-md focus:outline-none dark:text-gray-200" />
<button @click="copyToClipboard('0x30843c72DF6E9A9226d967bf2403602f1C2aB67b')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-umami-event="copy-eth-address">Copy</button>
<button @click="copyToClipboard('0x30843c72DF6E9A9226d967bf2403602f1C2aB67b')" class="px-3 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition-colors" data-rybbit-event="copy-eth-address">Copy</button>
</div>
</div>

View File

@@ -15,26 +15,12 @@
<input id="show-all-sprites" type="checkbox" v-model="showAllSprites" class="mr-2" />
<label for="show-all-sprites" class="dark:text-gray-200">Compare sprites</label>
</div>
<div class="flex items-center space-x-4">
<select v-model="offsetAnchor" class="px-2 py-1 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded">
<option value="top-left">Top Left</option>
<option value="top-right">Top Right</option>
<option value="bottom-left">Bottom Left</option>
<option value="bottom-right">Bottom Right</option>
</select>
<label class="dark:text-gray-200">Offset indicator position</label>
<button @click="setNewOffsetBase" class="px-3 py-1 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 rounded transition-colors">Set Current as Base</button>
</div>
</div>
</div>
<div class="relative border border-gray-300 dark:border-gray-600 rounded-lg overflow-auto">
<div class="canvas-container touch-manipulation" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }">
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"></canvas>
</div>
<!-- Mobile zoom controls -->
<div class="absolute bottom-3 right-3 flex space-x-2 sm:hidden bg-white/80 dark:bg-gray-800/80 p-2 rounded-lg shadow-md">
<!-- Zoom controls - visible on all screen sizes and positioned outside cells -->
<div class="relative flex space-x-2 bg-white/90 dark:bg-gray-800/90 p-2 rounded-lg shadow-md z-20">
<button @click="zoomIn" class="p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors">
<i class="fas fa-plus"></i>
</button>
@@ -46,11 +32,8 @@
</button>
</div>
<!-- Offset indicators overlay -->
<div v-if="canvasRef" class="absolute top-0 left-0 pointer-events-none w-full h-full">
<div v-for="pos in spritePositions" :key="pos.id" class="absolute" :style="getOffsetIndicatorStyle(pos)">
<div class="text-xs bg-black/75 dark:bg-white/75 text-white dark:text-gray-900 px-1 rounded whitespace-nowrap">x:{{ pos.x }}, y:{{ pos.y }}</div>
</div>
<div class="canvas-container touch-manipulation" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }">
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}"></canvas>
</div>
</div>
</div>
@@ -106,9 +89,6 @@
const ghostSprite = ref<{ id: string; x: number; y: number } | null>(null);
const highlightCell = ref<CellPosition | null>(null);
// Add these refs at the top with other refs
const baseOffsets = ref<Record<string, { x: number; y: number }>>({});
const showAllSprites = ref(false);
const spritePositions = computed(() => {
@@ -120,11 +100,6 @@
const col = index % props.columns;
const row = Math.floor(index / props.columns);
// Calculate relative offset from base
const baseOffset = baseOffsets.value[sprite.id] || { x: 0, y: 0 };
const relativeX = sprite.x - baseOffset.x;
const relativeY = sprite.y - baseOffset.y;
return {
id: sprite.id,
canvasX: col * maxWidth + sprite.x,
@@ -138,9 +113,8 @@
col,
row,
index,
// Use relative offsets for display
x: relativeX,
y: relativeY,
x: sprite.x,
y: sprite.y,
};
});
});
@@ -496,61 +470,6 @@
const rect = canvasRef.value.getBoundingClientRect();
return rect.width / canvasRef.value.width;
});
// Add new ref for offset anchor
const offsetAnchor = ref('top-left');
// Add new method to calculate indicator position
const getOffsetIndicatorStyle = (pos: any) => {
const baseX = pos.cellX * scale.value;
const baseY = pos.cellY * scale.value;
const cellWidth = pos.maxWidth * scale.value;
const cellHeight = pos.maxHeight * scale.value;
switch (offsetAnchor.value) {
case 'top-right':
return {
left: `${baseX + cellWidth}px`,
top: `${baseY}px`,
transform: 'translateX(-100%)',
};
case 'bottom-left':
return {
left: `${baseX}px`,
top: `${baseY + cellHeight}px`,
transform: 'translateY(-100%)',
};
case 'bottom-right':
return {
left: `${baseX + cellWidth}px`,
top: `${baseY + cellHeight}px`,
transform: 'translate(-100%, -100%)',
};
default: // top-left
return {
left: `${baseX}px`,
top: `${baseY}px`,
};
}
};
const setNewOffsetBase = () => {
// Store current positions as new base positions
props.sprites.forEach(sprite => {
baseOffsets.value[sprite.id] = {
x: Math.floor(sprite.x),
y: Math.floor(sprite.y),
};
});
// Force redraw to update offset indicators
drawCanvas();
};
</script>
<style scoped>
/* Add styles for offset indicators */
.pointer-events-none {
pointer-events: none;
}
</style>
<style scoped></style>

View File

@@ -367,7 +367,7 @@
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex < zoomValues.length - 1) {
zoom.value = zoomValues[currentIndex + 1].toString();
zoom.value = zoomValues[currentIndex + 1];
}
};
@@ -375,7 +375,7 @@
const zoomValues = [0.5, 1, 2, 3, 4];
const currentIndex = zoomValues.indexOf(Number(zoom.value));
if (currentIndex > 0) {
zoom.value = zoomValues[currentIndex - 1].toString();
zoom.value = zoomValues[currentIndex - 1];
}
};

View File

@@ -3,49 +3,68 @@
<div class="space-y-6">
<div class="flex flex-col space-y-4">
<div class="flex items-center justify-center mb-4">
<img :src="imageUrl" alt="Spritesheet" class="max-w-full max-h-48 sm:max-h-64 border border-gray-300 rounded-lg" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" />
<img :src="imageUrl" alt="Spritesheet" class="max-w-full max-h-48 sm:max-h-64 border border-gray-300 dark:border-gray-600 rounded-lg" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" />
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<label for="detection-method" class="block text-sm font-medium text-gray-700">Detection Method</label>
<select id="detection-method" v-model="detectionMethod" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" data-umami-event="spritesheet-detection-method">
<label for="detection-method" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Detection Method</label>
<select
id="detection-method"
v-model="detectionMethod"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400"
data-rybbit-event="spritesheet-detection-method"
>
<option value="manual">Manual (specify rows and columns)</option>
<option value="auto">Auto-detect (experimental)</option>
</select>
</div>
<div v-if="detectionMethod === 'auto'" class="space-y-2">
<label for="sensitivity" class="block text-sm font-medium text-gray-700">Detection Sensitivity</label>
<input type="range" id="sensitivity" v-model="sensitivity" min="1" max="100" class="w-full" data-umami-event="spritesheet-sensitivity" />
<div class="text-xs text-gray-500 flex justify-between">
<label for="sensitivity" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Detection Sensitivity</label>
<input type="range" id="sensitivity" v-model="sensitivity" min="1" max="100" class="w-full dark:accent-blue-400" data-rybbit-event="spritesheet-sensitivity" />
<div class="text-xs text-gray-500 dark:text-gray-400 flex justify-between">
<span>Low</span>
<span>High</span>
</div>
</div>
<div v-if="detectionMethod === 'manual'" class="space-y-2">
<label for="rows" class="block text-sm font-medium text-gray-700">Rows</label>
<input type="number" id="rows" v-model.number="rows" min="1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" data-umami-event="spritesheet-rows" />
<label for="rows" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Rows</label>
<input
type="number"
id="rows"
v-model.number="rows"
min="1"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400"
data-rybbit-event="spritesheet-rows"
/>
</div>
<div v-if="detectionMethod === 'manual'" class="space-y-2">
<label for="columns" class="block text-sm font-medium text-gray-700">Columns</label>
<input type="number" id="columns" v-model.number="columns" min="1" class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500" data-umami-event="spritesheet-columns" />
<label for="columns" class="block text-sm font-medium text-gray-700 dark:text-gray-300">Columns</label>
<input
type="number"
id="columns"
v-model.number="columns"
min="1"
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 dark:focus:ring-blue-400 dark:focus:border-blue-400"
data-rybbit-event="spritesheet-columns"
/>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center">
<input type="checkbox" id="remove-empty" v-model="removeEmpty" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded" data-umami-event="spritesheet-remove-empty" />
<label for="remove-empty" class="ml-2 block text-sm text-gray-700"> Remove empty sprites (transparent/background color) </label>
<input type="checkbox" id="remove-empty" v-model="removeEmpty" class="h-4 w-4 text-blue-600 dark:text-blue-400 focus:ring-blue-500 dark:focus:ring-blue-400 border-gray-300 dark:border-gray-600 rounded" data-rybbit-event="spritesheet-remove-empty" />
<label for="remove-empty" class="ml-2 block text-sm text-gray-700 dark:text-gray-300"> Remove empty sprites (transparent/background color) </label>
</div>
</div>
<div v-if="previewSprites.length > 0" class="space-y-2">
<h3 class="text-sm font-medium text-gray-700">Preview ({{ previewSprites.length }} sprites)</h3>
<div class="grid grid-cols-3 sm:grid-cols-6 md:grid-cols-8 gap-2 max-h-40 overflow-y-auto p-2 border border-gray-200 rounded-lg">
<div v-for="(sprite, index) in previewSprites" :key="index" class="relative border border-gray-300 rounded bg-gray-100 flex items-center justify-center" :style="{ width: '80px', height: '80px' }">
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-300">Preview ({{ previewSprites.length }} sprites)</h3>
<div class="grid grid-cols-3 sm:grid-cols-6 md:grid-cols-8 gap-2 max-h-40 overflow-y-auto p-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800">
<div v-for="(sprite, index) in previewSprites" :key="index" class="relative border border-gray-300 dark:border-gray-600 rounded bg-gray-100 dark:bg-gray-700 flex items-center justify-center" :style="{ width: '80px', height: '80px' }">
<img :src="sprite.url" alt="Sprite preview" class="max-w-full max-h-full" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" />
</div>
</div>
@@ -53,12 +72,18 @@
</div>
<div class="flex flex-col-reverse sm:flex-row sm:justify-end space-y-3 space-y-reverse sm:space-y-0 sm:space-x-3">
<button @click="cancel" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" data-umami-event="spritesheet-cancel">Cancel</button>
<button
@click="cancel"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"
data-rybbit-event="spritesheet-cancel"
>
Cancel
</button>
<button
@click="confirm"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-800"
:disabled="previewSprites.length === 0 || isProcessing"
data-umami-event="spritesheet-split"
data-rybbit-event="spritesheet-split"
>
Split Spritesheet
</button>

View File

@@ -1,5 +1,5 @@
<template>
<button @click="settingsStore.toggleDarkMode()" class="p-2 rounded-lg transition-colors" :class="settingsStore.darkMode ? 'text-yellow-400 hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'" aria-label="Toggle dark mode" data-umami-event="toggle-dark-mode">
<button @click="settingsStore.toggleDarkMode()" class="p-2 rounded-lg transition-colors" :class="settingsStore.darkMode ? 'text-yellow-400 hover:bg-gray-700' : 'text-gray-700 hover:bg-gray-100'" aria-label="Toggle dark mode" data-rybbit-event="toggle-dark-mode">
<i :class="settingsStore.darkMode ? 'fas fa-sun' : 'fas fa-moon'"></i>
</button>
</template>

View File

@@ -17,10 +17,10 @@
<div class="flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700" :class="{ 'cursor-move': !isFullScreen && !isMobile }">
<h3 class="text-xl sm:text-2xl font-semibold text-gray-900 dark:text-gray-100 truncate pr-2" @mousedown="startDrag" @touchstart="handleTouchStart">{{ title }}</h3>
<div class="flex items-center space-x-2">
<button @click="toggleFullScreen" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-umami-event="modal-fullscreen">
<button @click="toggleFullScreen" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-rybbit-event="modal-fullscreen">
<img src="@/assets/images/fullscreen-icon.svg" class="w-4 h-4 dark:invert" alt="Fullscreen" :class="{ 'rotate-180': isFullScreen }" />
</button>
<button @click="close" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-umami-event="modal-close">
<button @click="close" class="p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors" data-rybbit-event="modal-close">
<img src="@/assets/images/close-icon.svg" class="w-5 h-5 dark:invert" alt="Close" />
</button>
</div>