Compare commits

2 Commits

Author SHA1 Message Date
5cc4eb8731 [FEAT] negative spacing in JSON export logic, show cell size 2025-11-18 22:18:38 +01:00
57d62db219 Fix for negative spacing 2025-11-18 21:11:26 +01:00
6 changed files with 57 additions and 43 deletions

View File

@@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
## [1.6.0] - 2025-11-18 ## [1.6.0] - 2025-11-18
- Improved animation preview modal - Improved animation preview modal
- Add toggle for negative spacing in cells - Add toggle for negative spacing in cells
- Show cell size
## [1.5.0] - 2025-11-17 ## [1.5.0] - 2025-11-17
- Show offset values in sprite cells and in preview modal - Show offset values in sprite cells and in preview modal

View File

@@ -51,6 +51,11 @@
/> />
</div> </div>
<div class="flex items-center space-x-1">
<span class="text-gray-700 dark:text-gray-200 font-medium">Cell size:</span>
<span class="text-gray-600 dark:text-gray-300">{{ cellSize.width }} × {{ cellSize.height }}px</span>
</div>
<!-- Add mass position buttons --> <!-- Add mass position buttons -->
<div class="flex flex-wrap items-center justify-center gap-2"> <div class="flex flex-wrap items-center justify-center gap-2">
<button @click="alignSprites('left')" class="p-3 sm:p-2 bg-gray-100 hover:bg-gray-200 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-100 rounded-lg transition-colors" title="Align Left" data-rybbit-event="align-left"> <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">
@@ -135,7 +140,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, toRef } from 'vue'; import { ref, onMounted, toRef, computed } from 'vue';
import FileUploader from './components/FileUploader.vue'; import FileUploader from './components/FileUploader.vue';
import SpriteCanvas from './components/SpriteCanvas.vue'; import SpriteCanvas from './components/SpriteCanvas.vue';
import Modal from './components/utilities/Modal.vue'; import Modal from './components/utilities/Modal.vue';
@@ -145,15 +150,26 @@
import SpritesheetSplitter from './components/SpritesheetSplitter.vue'; import SpritesheetSplitter from './components/SpritesheetSplitter.vue';
import GifFpsModal from './components/GifFpsModal.vue'; import GifFpsModal from './components/GifFpsModal.vue';
import DarkModeToggle from './components/utilities/DarkModeToggle.vue'; import DarkModeToggle from './components/utilities/DarkModeToggle.vue';
import { useSprites } from './composables/useSprites'; import { useSprites, getMaxDimensions } from './composables/useSprites';
import { useExport } from './composables/useExport'; import { useExport } from './composables/useExport';
import { useSettingsStore } from './stores/useSettingsStore'; import { useSettingsStore } from './stores/useSettingsStore';
import { calculateNegativeSpacing } from './composables/useNegativeSpacing';
import type { SpriteFile } from './types/sprites'; import type { SpriteFile } from './types/sprites';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const { sprites, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites } = useSprites(); const { sprites, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites } = useSprites();
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExport(sprites, columns, toRef(settingsStore, 'negativeSpacingEnabled')); const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExport(sprites, 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,
};
});
const isPreviewModalOpen = ref(false); const isPreviewModalOpen = ref(false);
const isHelpModalOpen = ref(false); const isHelpModalOpen = ref(false);
const isFeedbackModalOpen = ref(false); const isFeedbackModalOpen = ref(false);

View File

@@ -164,6 +164,7 @@
import { useCanvas2D } from '@/composables/useCanvas2D'; import { useCanvas2D } from '@/composables/useCanvas2D';
import { useZoom } from '@/composables/useZoom'; import { useZoom } from '@/composables/useZoom';
import { useAnimationFrames } from '@/composables/useAnimationFrames'; import { useAnimationFrames } from '@/composables/useAnimationFrames';
import { calculateNegativeSpacing } from '@/composables/useNegativeSpacing';
const props = defineProps<{ const props = defineProps<{
sprites: Sprite[]; sprites: Sprite[];
@@ -209,18 +210,6 @@
// Canvas drawing // Canvas drawing
// Calculate negative spacing based on sprite dimensions
function calculateNegativeSpacing(): number {
if (!settingsStore.negativeSpacingEnabled || props.sprites.length === 0) return 0;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
const minWidth = Math.min(...props.sprites.map(s => s.width));
const minHeight = Math.min(...props.sprites.map(s => s.height));
const widthDiff = maxWidth - minWidth;
const heightDiff = maxHeight - minHeight;
return Math.max(widthDiff, heightDiff);
}
function drawPreviewCanvas() { function drawPreviewCanvas() {
if (!previewCanvasRef.value || !canvas2D.ctx.value || props.sprites.length === 0) return; if (!previewCanvasRef.value || !canvas2D.ctx.value || props.sprites.length === 0) return;
@@ -228,7 +217,7 @@
if (!currentSprite) return; if (!currentSprite) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites); const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
const cellWidth = maxWidth + negativeSpacing; const cellWidth = maxWidth + negativeSpacing;
const cellHeight = maxHeight + negativeSpacing; const cellHeight = maxHeight + negativeSpacing;
@@ -272,7 +261,7 @@
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY; const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
const sprite = props.sprites[currentFrameIndex.value]; const sprite = props.sprites[currentFrameIndex.value];
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
// Check if click is on sprite (accounting for negative spacing offset) // Check if click is on sprite (accounting for negative spacing offset)
const spriteCanvasX = negativeSpacing + sprite.x; const spriteCanvasX = negativeSpacing + sprite.x;
@@ -303,7 +292,7 @@
if (!sprite || sprite.id !== activeSpriteId.value) return; if (!sprite || sprite.id !== activeSpriteId.value) return;
const { maxWidth, maxHeight } = getMaxDimensions(props.sprites); const { maxWidth, maxHeight } = getMaxDimensions(props.sprites);
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(props.sprites, settingsStore.negativeSpacingEnabled);
const cellWidth = maxWidth + negativeSpacing; const cellWidth = maxWidth + negativeSpacing;
const cellHeight = maxHeight + negativeSpacing; const cellHeight = maxHeight + negativeSpacing;

View File

@@ -1,6 +1,7 @@
import { ref, computed, type Ref, type ComputedRef } from 'vue'; import { ref, computed, type Ref, type ComputedRef } from 'vue';
import type { Sprite } from '@/types/sprites'; import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from './useSprites'; import { getMaxDimensions } from './useSprites';
import { calculateNegativeSpacing } from './useNegativeSpacing';
export interface CellPosition { export interface CellPosition {
col: number; col: number;
@@ -74,17 +75,8 @@ export function useDragSprite(options: DragSpriteOptions) {
lastMaxWidth.value = baseMaxWidth; lastMaxWidth.value = baseMaxWidth;
lastMaxHeight.value = baseMaxHeight; lastMaxHeight.value = baseMaxHeight;
// Calculate negative spacing based on sprite size differences // Calculate negative spacing using shared composable
let negativeSpacing = 0; const negativeSpacing = calculateNegativeSpacing(sprites, negativeSpacingEnabled);
if (negativeSpacingEnabled && sprites.length > 0) {
// Find the smallest sprite dimensions
const minWidth = Math.min(...sprites.map(s => s.width));
const minHeight = Math.min(...sprites.map(s => s.height));
// Negative spacing is the difference between max and min dimensions
const widthDiff = baseMaxWidth - minWidth;
const heightDiff = baseMaxHeight - minHeight;
negativeSpacing = Math.max(widthDiff, heightDiff);
}
// Add negative spacing to expand each cell // Add negative spacing to expand each cell
const maxWidth = baseMaxWidth + negativeSpacing; const maxWidth = baseMaxWidth + negativeSpacing;

View File

@@ -4,19 +4,9 @@ import gifWorkerUrl from 'gif.js/dist/gif.worker.js?url';
import JSZip from 'jszip'; import JSZip from 'jszip';
import type { Sprite } from '../types/sprites'; import type { Sprite } from '../types/sprites';
import { getMaxDimensions } from './useSprites'; import { getMaxDimensions } from './useSprites';
import { calculateNegativeSpacing } from './useNegativeSpacing';
export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>) => { export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negativeSpacingEnabled: Ref<boolean>) => {
// Calculate negative spacing based on sprite dimensions
const calculateNegativeSpacing = (): number => {
if (!negativeSpacingEnabled.value || sprites.value.length === 0) return 0;
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
const minWidth = Math.min(...sprites.value.map(s => s.width));
const minHeight = Math.min(...sprites.value.map(s => s.height));
const widthDiff = maxWidth - minWidth;
const heightDiff = maxHeight - minHeight;
return Math.max(widthDiff, heightDiff);
};
const downloadSpritesheet = () => { const downloadSpritesheet = () => {
if (!sprites.value.length) { if (!sprites.value.length) {
alert('Please upload or import sprites before downloading the spritesheet.'); alert('Please upload or import sprites before downloading the spritesheet.');
@@ -24,7 +14,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
} }
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value);
const cellWidth = maxWidth + negativeSpacing; const cellWidth = maxWidth + negativeSpacing;
const cellHeight = maxHeight + negativeSpacing; const cellHeight = maxHeight + negativeSpacing;
const rows = Math.ceil(sprites.value.length / columns.value); const rows = Math.ceil(sprites.value.length / columns.value);
@@ -78,7 +68,11 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
}) })
); );
const jsonData = { columns: columns.value, sprites: spritesData.filter(Boolean) }; const jsonData = {
columns: columns.value,
negativeSpacingEnabled: negativeSpacingEnabled.value,
sprites: spritesData.filter(Boolean),
};
const jsonString = JSON.stringify(jsonData, null, 2); const jsonString = JSON.stringify(jsonData, null, 2);
const blob = new Blob([jsonString], { type: 'application/json' }); const blob = new Blob([jsonString], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -96,6 +90,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
if (!jsonData.sprites || !Array.isArray(jsonData.sprites)) throw new Error('Invalid JSON format: missing sprites array'); if (!jsonData.sprites || !Array.isArray(jsonData.sprites)) throw new Error('Invalid JSON format: missing sprites array');
if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns; if (jsonData.columns && typeof jsonData.columns === 'number') columns.value = jsonData.columns;
if (typeof jsonData.negativeSpacingEnabled === 'boolean') negativeSpacingEnabled.value = jsonData.negativeSpacingEnabled;
// revoke existing blob urls // revoke existing blob urls
if (sprites.value.length) { if (sprites.value.length) {
@@ -147,7 +142,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
} }
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value);
const cellWidth = maxWidth + negativeSpacing; const cellWidth = maxWidth + negativeSpacing;
const cellHeight = maxHeight + negativeSpacing; const cellHeight = maxHeight + negativeSpacing;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -187,7 +182,7 @@ export const useExport = (sprites: Ref<Sprite[]>, columns: Ref<number>, negative
const zip = new JSZip(); const zip = new JSZip();
const { maxWidth, maxHeight } = getMaxDimensions(sprites.value); const { maxWidth, maxHeight } = getMaxDimensions(sprites.value);
const negativeSpacing = calculateNegativeSpacing(); const negativeSpacing = calculateNegativeSpacing(sprites.value, negativeSpacingEnabled.value);
const cellWidth = maxWidth + negativeSpacing; const cellWidth = maxWidth + negativeSpacing;
const cellHeight = maxHeight + negativeSpacing; const cellHeight = maxHeight + negativeSpacing;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');

View File

@@ -0,0 +1,21 @@
import type { Sprite } from '@/types/sprites';
import { getMaxDimensions } from './useSprites';
/**
* Calculate negative spacing to add to top-left of cells.
* Uses half the available space so spacing is equal on all sides.
*/
export function calculateNegativeSpacing(sprites: Sprite[], enabled: boolean): number {
if (!enabled || sprites.length === 0) return 0;
const { maxWidth, maxHeight } = getMaxDimensions(sprites);
const minWidth = Math.min(...sprites.map(s => s.width));
const minHeight = Math.min(...sprites.map(s => s.height));
// Available space is the gap between cell size and smallest sprite
const availableWidth = maxWidth - minWidth;
const availableHeight = maxHeight - minHeight;
// Use half to balance spacing equally on all sides
return Math.floor(Math.min(availableWidth, availableHeight) / 2);
}