This commit is contained in:
2025-11-22 02:52:36 +01:00
parent 5cc4eb8731
commit 097df1f5de
8 changed files with 726 additions and 198 deletions

View File

@@ -11,7 +11,6 @@
<a href="#" @click.prevent="openHelpModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="help-link"> <i class="fas fa-question-circle"></i> Help </a>
<a href="#" @click.prevent="openFeedbackModal" class="text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300 transition-colors" data-rybbit-event="feedback-link"> <i class="fas fa-comment-dots"></i> Feedback </a>
</div>
<div class="bg-white dark:bg-gray-800 rounded-2xl shadow-soft dark:shadow-gray-900/30 p-4 sm:p-8 transition-colors duration-300">
<div class="flex flex-col sm:flex-row justify-between items-center mb-4 sm:mb-6 gap-3">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-100">Upload sprites</h2>
@@ -26,7 +25,7 @@
</div>
<file-uploader @upload-sprites="handleSpritesUpload" />
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
<div v-if="!sprites.length" class="mt-8">
<div v-if="!visibleLayers.some(l => l.sprites.length)" class="mt-8">
<div class="mt-2 leading-relaxed space-y-2">
<p>Create spritesheets for your game development and animation projects with our completely free, open-source spritesheet generator.</p>
<p>This powerful online tool lets you upload individual sprite images and automatically arrange them into optimized sprite sheets with customizable layouts - perfect for indie developers, animators, and studios of any size.</p>
@@ -37,7 +36,24 @@
</div>
</div>
<div v-if="sprites.length > 0" class="mt-8">
<div v-if="visibleLayers.some(l => l.sprites.length)" class="mt-8">
<div class="flex flex-col gap-3 mb-4">
<div class="flex items-center gap-2 flex-wrap">
<span class="text-gray-700 dark:text-gray-200 font-medium">Layers</span>
<button @click="addLayer()" class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-100 rounded">Add</button>
</div>
<div class="flex flex-wrap gap-2">
<div v-for="layer in layers" :key="layer.id" class="flex items-center gap-2 px-2 py-1 rounded border border-gray-200 dark:border-gray-600" :class="{ 'ring-2 ring-blue-500': layer.id === activeLayerId }">
<button @click="activeLayerId = layer.id" class="px-2 py-0.5 rounded bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-200">{{ layer.name }}</button>
<label class="flex items-center gap-1 text-xs text-gray-600 dark:text-gray-300">
<input type="checkbox" v-model="layer.visible" /> Visible
</label>
<button @click="moveLayer(layer.id, 'up')" class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded"></button>
<button @click="moveLayer(layer.id, 'down')" class="text-xs px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded"></button>
<button v-if="layers.length > 1" @click="removeLayer(layer.id)" class="text-xs px-1.5 py-0.5 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-200 rounded">Remove</button>
</div>
</div>
</div>
<div class="flex flex-wrap items-center justify-center sm:justify-start gap-3 sm:gap-6 mb-6 sm:mb-8">
<div class="flex items-center space-x-1">
<label for="columns" class="text-gray-700 dark:text-gray-200 font-medium">Columns:</label>
@@ -107,14 +123,13 @@
<span>Preview animation</span>
</button>
</div>
<sprite-canvas :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-cell="updateSpriteCell" @remove-sprite="removeSprite" @replace-sprite="replaceSprite" @add-sprite="addSprite" @add-sprite-with-resize="addSpriteWithResize" />
<sprite-canvas :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-cell="updateSpriteCell" @remove-sprite="removeSprite" @replace-sprite="replaceSprite" @add-sprite="addSprite" @add-sprite-with-resize="addSpriteWithResize" />
</div>
</div>
</div>
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" title="Animation preview">
<sprite-preview :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" />
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" />
</Modal>
<HelpModal :is-open="isHelpModalOpen" @close="closeHelpModal" />
@@ -150,25 +165,41 @@
import SpritesheetSplitter from './components/SpritesheetSplitter.vue';
import GifFpsModal from './components/GifFpsModal.vue';
import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
import { useSprites, getMaxDimensions } from './composables/useSprites';
import { useExport } from './composables/useExport';
import { useExportLayers } from './composables/useExportLayers';
import { useLayers } from './composables/useLayers';
import { getMaxDimensionsAcrossLayers } from './composables/useLayers';
import { useSettingsStore } from './stores/useSettingsStore';
import { calculateNegativeSpacing } from './composables/useNegativeSpacing';
import type { SpriteFile } from './types/sprites';
const settingsStore = useSettingsStore();
const { sprites, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites } = useSprites();
const {
layers,
visibleLayers,
activeLayer,
activeLayerId,
columns,
updateSpritePosition,
updateSpriteCell,
removeSprite,
replaceSprite,
addSprite,
addSpriteWithResize,
processImageFiles,
alignSprites,
addLayer,
removeLayer,
moveLayer,
} = useLayers();
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExport(sprites, columns, toRef(settingsStore, 'negativeSpacingEnabled'));
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(layers, columns, toRef(settingsStore, 'negativeSpacingEnabled'));
const cellSize = computed(() => {
if (!sprites.value.length) return { width: 0, height: 0 };
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
const negativeSpacing = calculateNegativeSpacing(sprites.value, settingsStore.negativeSpacingEnabled);
return {
width: maxWidth + negativeSpacing,
height: maxHeight + negativeSpacing,
};
if (!layers.value.length) return { width: 0, height: 0 };
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
});
const isPreviewModalOpen = ref(false);
const isHelpModalOpen = ref(false);
@@ -216,7 +247,7 @@
};
const openPreviewModal = () => {
if (sprites.value.length === 0) {
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
alert('Please upload or import sprites to preview an animation.');
return;
}
@@ -253,7 +284,7 @@
};
const openGifFpsModal = () => {
if (sprites.value.length === 0) {
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
alert('Please upload or import sprites before generating a GIF.');
return;
}