[FEAT] UX enhancements
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.8.0] - 2025-11-23
|
||||||
|
- Fix context menu location
|
||||||
|
- You can now reposition all sprites in current frame
|
||||||
|
|
||||||
## [1.7.0] - 2025-11-22
|
## [1.7.0] - 2025-11-22
|
||||||
- Add layer support
|
- Add layer support
|
||||||
- Add background color picker
|
- Add background color picker
|
||||||
|
|||||||
60
src/App.vue
60
src/App.vue
@@ -57,7 +57,6 @@
|
|||||||
<p class="text-sm text-gray-600 dark:text-gray-400">Drag and drop images or import from JSON</p>
|
<p class="text-sm text-gray-600 dark:text-gray-400">Drag and drop images or import from JSON</p>
|
||||||
</div>
|
</div>
|
||||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||||
<input ref="jsonFileInput" type="file" accept=".json,application/json" class="hidden" @change="handleJSONFileChange" />
|
|
||||||
|
|
||||||
<div class="mt-10">
|
<div class="mt-10">
|
||||||
<div class="prose prose-lg dark:prose-invert max-w-none">
|
<div class="prose prose-lg dark:prose-invert max-w-none">
|
||||||
@@ -250,24 +249,14 @@
|
|||||||
|
|
||||||
<!-- Right panel - Canvas Preview -->
|
<!-- Right panel - Canvas Preview -->
|
||||||
<div class="p-6 overflow-y-auto overflow-x-auto max-h-[calc(100vh-200px)]">
|
<div class="p-6 overflow-y-auto overflow-x-auto max-h-[calc(100vh-200px)]">
|
||||||
<sprite-canvas
|
<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" />
|
||||||
: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>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" title="Animation Preview">
|
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" title="Animation Preview">
|
||||||
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" />
|
<sprite-preview :layers="layers" :active-layer-id="activeLayerId" :columns="columns" @update-sprite="updateSpritePosition" @update-sprite-in-layer="updateSpriteInLayer" />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<HelpModal :is-open="isHelpModalOpen" @close="closeHelpModal" />
|
<HelpModal :is-open="isHelpModalOpen" @close="closeHelpModal" />
|
||||||
@@ -311,7 +300,7 @@
|
|||||||
import type { SpriteFile } from './types/sprites';
|
import type { SpriteFile } from './types/sprites';
|
||||||
|
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteCell, removeSprite, replaceSprite, addSprite, addSpriteWithResize, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();
|
const { layers, visibleLayers, activeLayer, activeLayerId, columns, updateSpritePosition, updateSpriteInLayer, updateSpriteCell, removeSprite, replaceSprite, addSprite, processImageFiles, alignSprites, addLayer, removeLayer, moveLayer } = useLayers();
|
||||||
|
|
||||||
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(
|
const { downloadSpritesheet, exportSpritesheetJSON, importSpritesheetJSON, downloadAsGif, downloadAsZip } = useExportLayers(
|
||||||
layers,
|
layers,
|
||||||
@@ -324,27 +313,27 @@
|
|||||||
toRef(settingsStore, 'manualCellHeight')
|
toRef(settingsStore, 'manualCellHeight')
|
||||||
);
|
);
|
||||||
|
|
||||||
const cellSize = computed(() => {
|
const getCellSize = () => {
|
||||||
if (!layers.value.length) return { width: 0, height: 0 };
|
if (!visibleLayers.value.length) return { width: 0, height: 0 };
|
||||||
|
|
||||||
// If manual cell size is enabled, use the manual values
|
|
||||||
if (settingsStore.manualCellSizeEnabled) {
|
if (settingsStore.manualCellSizeEnabled) {
|
||||||
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
|
return { width: settingsStore.manualCellWidth, height: settingsStore.manualCellHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, calculate based on max dimensions
|
|
||||||
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
const { maxWidth, maxHeight } = getMaxDimensionsAcrossLayers(visibleLayers.value);
|
||||||
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
|
const allSprites = visibleLayers.value.flatMap(l => l.sprites);
|
||||||
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
|
const negativeSpacing = calculateNegativeSpacing(allSprites, settingsStore.negativeSpacingEnabled);
|
||||||
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
|
return { width: maxWidth + negativeSpacing, height: maxHeight + negativeSpacing };
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const cellSize = computed(getCellSize);
|
||||||
const isPreviewModalOpen = ref(false);
|
const isPreviewModalOpen = ref(false);
|
||||||
const isHelpModalOpen = ref(false);
|
const isHelpModalOpen = ref(false);
|
||||||
const isFeedbackModalOpen = ref(false);
|
const isFeedbackModalOpen = ref(false);
|
||||||
const isSpritesheetSplitterOpen = ref(false);
|
const isSpritesheetSplitterOpen = ref(false);
|
||||||
const isGifFpsModalOpen = ref(false);
|
const isGifFpsModalOpen = ref(false);
|
||||||
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
|
||||||
const uploadInput = ref<HTMLInputElement | null>(null);
|
const uploadInput = ref<HTMLInputElement | null>(null);
|
||||||
|
const jsonFileInput = ref<HTMLInputElement | null>(null);
|
||||||
const spritesheetImageUrl = ref('');
|
const spritesheetImageUrl = ref('');
|
||||||
const spritesheetImageFile = ref<File | null>(null);
|
const spritesheetImageFile = ref<File | null>(null);
|
||||||
const showFeedbackPopup = ref(false);
|
const showFeedbackPopup = ref(false);
|
||||||
@@ -356,12 +345,7 @@
|
|||||||
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
const jsonFile = files.find(file => file.type === 'application/json' || file.name.endsWith('.json'));
|
||||||
|
|
||||||
if (jsonFile) {
|
if (jsonFile) {
|
||||||
try {
|
await handleJSONImport(jsonFile);
|
||||||
await importSpritesheetJSON(jsonFile);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error importing JSON:', error);
|
|
||||||
alert('Failed to import JSON file. Please check the file format.');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,6 +371,15 @@
|
|||||||
processImageFiles(files);
|
processImageFiles(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleJSONImport = async (jsonFile: File) => {
|
||||||
|
try {
|
||||||
|
await importSpritesheetJSON(jsonFile);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error importing JSON:', error);
|
||||||
|
alert('Failed to import JSON file. Please check the file format.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const openPreviewModal = () => {
|
const openPreviewModal = () => {
|
||||||
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
|
if (!visibleLayers.value.some(l => l.sprites.length > 0)) {
|
||||||
alert('Please upload or import sprites to preview an animation.');
|
alert('Please upload or import sprites to preview an animation.');
|
||||||
@@ -447,14 +440,8 @@
|
|||||||
const handleJSONFileChange = async (event: Event) => {
|
const handleJSONFileChange = async (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
const jsonFile = input.files[0];
|
await handleJSONImport(input.files[0]);
|
||||||
try {
|
input.value = '';
|
||||||
await importSpritesheetJSON(jsonFile);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error importing JSON:', error);
|
|
||||||
alert('Failed to import JSON file. Please check the file format.');
|
|
||||||
}
|
|
||||||
if (jsonFileInput.value) jsonFileInput.value.value = '';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -483,9 +470,8 @@
|
|||||||
const handleUploadChange = async (event: Event) => {
|
const handleUploadChange = async (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
if (input.files && input.files.length > 0) {
|
if (input.files && input.files.length > 0) {
|
||||||
const files = Array.from(input.files);
|
await handleSpritesUpload(Array.from(input.files));
|
||||||
await handleSpritesUpload(files);
|
input.value = '';
|
||||||
if (uploadInput.value) uploadInput.value.value = '';
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6 relative">
|
||||||
<div class="bg-cyan-500 dark:bg-cyan-600 rounded-xl p-4 shadow-lg border border-cyan-400/50 dark:border-cyan-500/50">
|
<div class="bg-cyan-500 dark:bg-cyan-600 rounded-xl p-4 shadow-lg border border-cyan-400/50 dark:border-cyan-500/50">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<i class="fas fa-lightbulb text-yellow-300 text-xl mt-0.5"></i>
|
<i class="fas fa-lightbulb text-yellow-300 text-xl mt-0.5"></i>
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showContextMenu" class="fixed bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-xl shadow-2xl z-50 py-2 min-w-[200px] overflow-hidden" :style="{ left: contextMenuX + 'px', top: contextMenuY + 'px' }">
|
<div v-if="showContextMenu" class="absolute bg-white dark:bg-gray-800 border-2 border-gray-300 dark:border-gray-600 rounded-xl shadow-2xl z-50 py-2 min-w-[200px] overflow-hidden" :style="{ left: contextMenuX + 'px', top: contextMenuY + 'px' }">
|
||||||
<button @click="addSprite" class="w-full px-5 py-3 text-left hover:bg-blue-50 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-200 flex items-center gap-3 transition-colors font-medium">
|
<button @click="addSprite" class="w-full px-5 py-3 text-left hover:bg-blue-50 dark:hover:bg-blue-900/30 text-gray-700 dark:text-gray-200 flex items-center gap-3 transition-colors font-medium">
|
||||||
<i class="fas fa-plus text-blue-600 dark:text-blue-400"></i>
|
<i class="fas fa-plus text-blue-600 dark:text-blue-400"></i>
|
||||||
<span>Add Sprite</span>
|
<span>Add Sprite</span>
|
||||||
@@ -310,8 +310,17 @@
|
|||||||
|
|
||||||
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
const clickedSprite = findSpriteAtPosition(pos.x, pos.y);
|
||||||
contextMenuSpriteId.value = clickedSprite?.id || null;
|
contextMenuSpriteId.value = clickedSprite?.id || null;
|
||||||
contextMenuX.value = event.clientX;
|
|
||||||
contextMenuY.value = event.clientY;
|
// Get the root component element to calculate offset
|
||||||
|
const rootElement = canvasRef.value.closest('.space-y-6') as HTMLElement;
|
||||||
|
if (!rootElement) return;
|
||||||
|
|
||||||
|
const rootRect = rootElement.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Position relative to the component root
|
||||||
|
contextMenuX.value = event.clientX - rootRect.left;
|
||||||
|
contextMenuY.value = event.clientY - rootRect.top;
|
||||||
|
|
||||||
showContextMenu.value = true;
|
showContextMenu.value = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,11 @@
|
|||||||
<span class="text-sm dark:text-gray-200">Reposition</span>
|
<span class="text-sm dark:text-gray-200">Reposition</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer ml-4" :class="{ 'opacity-50 cursor-not-allowed': !isDraggable }">
|
||||||
|
<input type="checkbox" v-model="repositionAllLayers" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" :disabled="!isDraggable" />
|
||||||
|
<span class="text-sm dark:text-gray-200">All layers</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label class="flex items-center gap-2 cursor-pointer">
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
<input type="checkbox" v-model="showAllSprites" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" />
|
<input type="checkbox" v-model="showAllSprites" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300 dark:border-gray-600 dark:bg-gray-700" />
|
||||||
<span class="text-sm dark:text-gray-200">Compare sprites</span>
|
<span class="text-sm dark:text-gray-200">Compare sprites</span>
|
||||||
@@ -170,6 +175,7 @@
|
|||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'updateSprite', id: string, x: number, y: number): void;
|
(e: 'updateSprite', id: string, x: number, y: number): void;
|
||||||
|
(e: 'updateSpriteInLayer', layerId: string, spriteId: string, x: number, y: number): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const previewCanvasRef = ref<HTMLCanvasElement | null>(null);
|
const previewCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||||
@@ -207,6 +213,7 @@
|
|||||||
|
|
||||||
// Preview state
|
// Preview state
|
||||||
const isDraggable = ref(false);
|
const isDraggable = ref(false);
|
||||||
|
const repositionAllLayers = ref(false);
|
||||||
const showAllSprites = ref(false);
|
const showAllSprites = ref(false);
|
||||||
|
|
||||||
const compositeFrames = computed<Sprite[]>(() => {
|
const compositeFrames = computed<Sprite[]>(() => {
|
||||||
@@ -238,6 +245,7 @@
|
|||||||
const dragStartX = ref(0);
|
const dragStartX = ref(0);
|
||||||
const dragStartY = ref(0);
|
const dragStartY = ref(0);
|
||||||
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
|
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
|
||||||
|
const allSpritesPosBeforeDrag = ref<Map<string, { x: number; y: number }>>(new Map());
|
||||||
|
|
||||||
// Canvas drawing
|
// Canvas drawing
|
||||||
|
|
||||||
@@ -319,19 +327,47 @@
|
|||||||
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
const mouseX = ((event.clientX - rect.left) / zoom.value) * scaleX;
|
||||||
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
const mouseY = ((event.clientY - rect.top) / zoom.value) * scaleY;
|
||||||
|
|
||||||
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
|
||||||
const { negativeSpacing } = getCellDimensions();
|
const { negativeSpacing } = getCellDimensions();
|
||||||
|
|
||||||
// Check if click is on sprite (accounting for negative spacing offset)
|
if (repositionAllLayers.value) {
|
||||||
if (activeSprite) {
|
// Check if click is on any sprite from any visible layer
|
||||||
const spriteCanvasX = negativeSpacing + activeSprite.x;
|
const visibleLayers = getVisibleLayers();
|
||||||
const spriteCanvasY = negativeSpacing + activeSprite.y;
|
for (const layer of visibleLayers) {
|
||||||
if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + activeSprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + activeSprite.height) {
|
const sprite = layer.sprites[currentFrameIndex.value];
|
||||||
isDragging.value = true;
|
if (!sprite) continue;
|
||||||
activeSpriteId.value = activeSprite.id;
|
|
||||||
dragStartX.value = mouseX;
|
const spriteCanvasX = negativeSpacing + sprite.x;
|
||||||
dragStartY.value = mouseY;
|
const spriteCanvasY = negativeSpacing + sprite.y;
|
||||||
spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y };
|
if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + sprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + sprite.height) {
|
||||||
|
isDragging.value = true;
|
||||||
|
activeSpriteId.value = 'ALL_LAYERS'; // Special marker for all layers
|
||||||
|
dragStartX.value = mouseX;
|
||||||
|
dragStartY.value = mouseY;
|
||||||
|
|
||||||
|
// Store initial positions for all sprites in this frame from all visible layers
|
||||||
|
allSpritesPosBeforeDrag.value.clear();
|
||||||
|
visibleLayers.forEach(layer => {
|
||||||
|
const s = layer.sprites[currentFrameIndex.value];
|
||||||
|
if (s) {
|
||||||
|
allSpritesPosBeforeDrag.value.set(s.id, { x: s.x, y: s.y });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only check active layer sprite
|
||||||
|
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
||||||
|
if (activeSprite) {
|
||||||
|
const spriteCanvasX = negativeSpacing + activeSprite.x;
|
||||||
|
const spriteCanvasY = negativeSpacing + activeSprite.y;
|
||||||
|
if (mouseX >= spriteCanvasX && mouseX <= spriteCanvasX + activeSprite.width && mouseY >= spriteCanvasY && mouseY <= spriteCanvasY + activeSprite.height) {
|
||||||
|
isDragging.value = true;
|
||||||
|
activeSpriteId.value = activeSprite.id;
|
||||||
|
dragStartX.value = mouseX;
|
||||||
|
dragStartY.value = mouseY;
|
||||||
|
spritePosBeforeDrag.value = { x: activeSprite.x, y: activeSprite.y };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -349,21 +385,45 @@
|
|||||||
const deltaX = Math.round(mouseX - dragStartX.value);
|
const deltaX = Math.round(mouseX - dragStartX.value);
|
||||||
const deltaY = Math.round(mouseY - dragStartY.value);
|
const deltaY = Math.round(mouseY - dragStartY.value);
|
||||||
|
|
||||||
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
|
||||||
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
|
|
||||||
|
|
||||||
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions();
|
const { cellWidth, cellHeight, negativeSpacing } = getCellDimensions();
|
||||||
|
|
||||||
// Calculate new position with constraints and round to integers
|
if (activeSpriteId.value === 'ALL_LAYERS') {
|
||||||
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
|
// Move all sprites in current frame from all visible layers
|
||||||
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
|
const visibleLayers = getVisibleLayers();
|
||||||
|
visibleLayers.forEach(layer => {
|
||||||
|
const sprite = layer.sprites[currentFrameIndex.value];
|
||||||
|
if (!sprite) return;
|
||||||
|
|
||||||
// Constrain movement within expanded cell (allow negative values up to -negativeSpacing)
|
const originalPos = allSpritesPosBeforeDrag.value.get(sprite.id);
|
||||||
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
|
if (!originalPos) return;
|
||||||
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
|
|
||||||
|
|
||||||
emit('updateSprite', activeSpriteId.value, newX, newY);
|
// Calculate new position with constraints
|
||||||
drawPreviewCanvas();
|
let newX = Math.round(originalPos.x + deltaX);
|
||||||
|
let newY = Math.round(originalPos.y + deltaY);
|
||||||
|
|
||||||
|
// Constrain movement within expanded cell
|
||||||
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - sprite.width, newX));
|
||||||
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - sprite.height, newY));
|
||||||
|
|
||||||
|
emit('updateSpriteInLayer', layer.id, sprite.id, newX, newY);
|
||||||
|
});
|
||||||
|
drawPreviewCanvas();
|
||||||
|
} else {
|
||||||
|
// Move only the active layer sprite
|
||||||
|
const activeSprite = props.layers.find(l => l.id === props.activeLayerId)?.sprites[currentFrameIndex.value];
|
||||||
|
if (!activeSprite || activeSprite.id !== activeSpriteId.value) return;
|
||||||
|
|
||||||
|
// Calculate new position with constraints and round to integers
|
||||||
|
let newX = Math.round(spritePosBeforeDrag.value.x + deltaX);
|
||||||
|
let newY = Math.round(spritePosBeforeDrag.value.y + deltaY);
|
||||||
|
|
||||||
|
// Constrain movement within expanded cell (allow negative values up to -negativeSpacing)
|
||||||
|
newX = Math.max(-negativeSpacing, Math.min(cellWidth - negativeSpacing - activeSprite.width, newX));
|
||||||
|
newY = Math.max(-negativeSpacing, Math.min(cellHeight - negativeSpacing - activeSprite.height, newY));
|
||||||
|
|
||||||
|
emit('updateSprite', activeSpriteId.value, newX, newY);
|
||||||
|
drawPreviewCanvas();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopDrag = () => {
|
const stopDrag = () => {
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ export const useLayers = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateSpriteInLayer = (layerId: string, spriteId: string, x: number, y: number) => {
|
||||||
|
const l = layers.value.find(layer => layer.id === layerId);
|
||||||
|
if (!l) return;
|
||||||
|
const i = l.sprites.findIndex(s => s.id === spriteId);
|
||||||
|
if (i !== -1) {
|
||||||
|
l.sprites[i].x = Math.floor(x);
|
||||||
|
l.sprites[i].y = Math.floor(y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => {
|
const alignSprites = (position: 'left' | 'center' | 'right' | 'top' | 'middle' | 'bottom') => {
|
||||||
const l = activeLayer.value;
|
const l = activeLayer.value;
|
||||||
if (!l || !l.sprites.length) return;
|
if (!l || !l.sprites.length) return;
|
||||||
@@ -138,9 +148,7 @@ export const useLayers = () => {
|
|||||||
img.src = url;
|
img.src = url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSprite = (file: File) => addSpriteWithResize(file);
|
const addSprite = (file: File) => {
|
||||||
|
|
||||||
const addSpriteWithResize = (file: File) => {
|
|
||||||
const l = activeLayer.value;
|
const l = activeLayer.value;
|
||||||
if (!l) return;
|
if (!l) return;
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
@@ -163,7 +171,7 @@ export const useLayers = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processImageFiles = async (files: File[]) => {
|
const processImageFiles = async (files: File[]) => {
|
||||||
for (const f of files) addSpriteWithResize(f);
|
for (const f of files) addSprite(f);
|
||||||
};
|
};
|
||||||
|
|
||||||
const addLayer = (name?: string) => {
|
const addLayer = (name?: string) => {
|
||||||
@@ -203,11 +211,11 @@ export const useLayers = () => {
|
|||||||
columns,
|
columns,
|
||||||
getMaxDimensions,
|
getMaxDimensions,
|
||||||
updateSpritePosition,
|
updateSpritePosition,
|
||||||
|
updateSpriteInLayer,
|
||||||
updateSpriteCell,
|
updateSpriteCell,
|
||||||
removeSprite,
|
removeSprite,
|
||||||
replaceSprite,
|
replaceSprite,
|
||||||
addSprite,
|
addSprite,
|
||||||
addSpriteWithResize,
|
|
||||||
processImageFiles,
|
processImageFiles,
|
||||||
alignSprites,
|
alignSprites,
|
||||||
addLayer,
|
addLayer,
|
||||||
|
|||||||
Reference in New Issue
Block a user