use options api

This commit is contained in:
Dennis Postma 2025-04-03 02:46:50 +02:00
parent a989c8719f
commit 22781b1883
9 changed files with 421 additions and 527 deletions

View File

@ -2,11 +2,11 @@
<header class="flex items-center justify-between bg-gray-800 p-3 shadow-md sticky top-0 z-50"> <header class="flex items-center justify-between bg-gray-800 p-3 shadow-md sticky top-0 z-50">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<i class="fas fa-gamepad text-blue-500 text-2xl"></i> <i class="fas fa-gamepad text-blue-500 text-2xl"></i>
<h1 class="text-xl font-semibold text-gray-200">Noxious Spritesheet Creator</h1> <h1 class="text-xl font-semibold text-gray-200">Spritesheet Creator</h1>
</div> </div>
<div class="flex gap-3"> <div class="flex gap-3">
<button <button
@click="$emit('toggleHelp')" @click="emit('toggleHelp')"
class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors" class="p-2 bg-gray-700 border border-gray-600 rounded hover:border-blue-500 transition-colors"
title="Keyboard Shortcuts" title="Keyboard Shortcuts"
> >
@ -16,11 +16,8 @@
</header> </header>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; const emit = defineEmits<{
(e: 'toggleHelp'): void
export default defineComponent({ }>()
name: 'AppHeader',
emits: ['toggleHelp']
});
</script> </script>

View File

@ -22,112 +22,99 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref } from 'vue'; import { ref } from 'vue';
import { Sprite } from '../composables/useSpritesheetStore'; import { type Sprite, useSpritesheetStore } from '../composables/useSpritesheetStore';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
export default defineComponent({ const emit = defineEmits<{
name: 'DropZone', 'files-uploaded': [sprites: Sprite[]]
emits: ['files-uploaded'], }>();
setup(props, { emit }) {
const store = useSpritesheetStore();
const fileInput = ref<HTMLInputElement | null>(null);
const isDragOver = ref(false);
const openFileDialog = () => { const store = useSpritesheetStore();
if (fileInput.value) { const fileInput = ref<HTMLInputElement | null>(null);
fileInput.value.click(); const isDragOver = ref(false);
}
};
const onDragOver = () => { const openFileDialog = () => {
isDragOver.value = true; if (fileInput.value) {
}; fileInput.value.click();
const onDragLeave = () => {
isDragOver.value = false;
};
const onDrop = (e: DragEvent) => {
isDragOver.value = false;
if (e.dataTransfer?.files.length) {
handleFiles(e.dataTransfer.files);
}
};
const onFileChange = (e: Event) => {
const input = e.target as HTMLInputElement;
if (input.files?.length) {
handleFiles(input.files);
}
};
const handleFiles = async (files: FileList) => {
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) {
store.showNotification('Please upload image files only', 'error');
return;
}
const newSprites: Sprite[] = [];
for (let i = 0; i < imageFiles.length; i++) {
const file = imageFiles[i];
try {
const sprite = await createSpriteFromFile(file, i);
newSprites.push(sprite);
} catch (error) {
console.error('Error loading sprite:', error);
}
}
if (newSprites.length > 0) {
store.addSprites(newSprites);
emit('files-uploaded', newSprites);
store.showNotification(`Added ${newSprites.length} sprites successfully`);
}
};
const createSpriteFromFile = (file: File, index: number): Promise<Sprite> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
resolve({
img,
width: img.width,
height: img.height,
x: 0,
y: 0,
name: file.name,
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
uploadOrder: index
});
};
img.onerror = reject;
img.src = e.target?.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
return {
fileInput,
isDragOver,
openFileDialog,
onDragOver,
onDragLeave,
onDrop,
onFileChange
};
} }
}); };
const onDragOver = () => {
isDragOver.value = true;
};
const onDragLeave = () => {
isDragOver.value = false;
};
const onDrop = (e: DragEvent) => {
isDragOver.value = false;
if (e.dataTransfer?.files.length) {
handleFiles(e.dataTransfer.files);
}
};
const onFileChange = (e: Event) => {
const input = e.target as HTMLInputElement;
if (input.files?.length) {
handleFiles(input.files);
}
};
const handleFiles = async (files: FileList) => {
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
if (imageFiles.length === 0) {
store.showNotification('Please upload image files only', 'error');
return;
}
const newSprites: Sprite[] = [];
for (let i = 0; i < imageFiles.length; i++) {
const file = imageFiles[i];
try {
const sprite = await createSpriteFromFile(file, i);
newSprites.push(sprite);
} catch (error) {
console.error('Error loading sprite:', error);
}
}
if (newSprites.length > 0) {
store.addSprites(newSprites);
emit('files-uploaded', newSprites);
store.showNotification(`Added ${newSprites.length} sprites successfully`);
}
};
const createSpriteFromFile = (file: File, index: number): Promise<Sprite> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
resolve({
img,
width: img.width,
height: img.height,
x: 0,
y: 0,
name: file.name,
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
uploadOrder: index
});
};
img.onerror = reject;
img.src = e.target?.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
};
</script> </script>

View File

@ -1,17 +1,14 @@
<template> <template>
<button <button
@click="$emit('showHelp')" @click="emit('showHelp')"
class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40" class="fixed bottom-5 right-5 w-12 h-12 bg-blue-500 text-white rounded-full flex items-center justify-center text-xl shadow-lg cursor-pointer transition-all hover:bg-blue-600 hover:-translate-y-1 z-40"
> >
<i class="fas fa-question"></i> <i class="fas fa-question"></i>
</button> </button>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; const emit = defineEmits<{
showHelp: []
export default defineComponent({ }>();
name: 'HelpButton',
emits: ['showHelp']
});
</script> </script>

View File

@ -26,203 +26,181 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, onMounted, computed, onBeforeUnmount } from 'vue'; import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore'; import { useSpritesheetStore } from '../composables/useSpritesheetStore';
export default defineComponent({ const store = useSpritesheetStore();
name: 'MainContent', const canvasEl = ref<HTMLCanvasElement | null>(null);
setup() {
const store = useSpritesheetStore();
const canvasEl = ref<HTMLCanvasElement | null>(null);
// Tooltip state // Tooltip state
const isTooltipVisible = ref(false); const isTooltipVisible = ref(false);
const tooltipText = ref(''); const tooltipText = ref('');
const tooltipPosition = ref({ x: 0, y: 0 }); const tooltipPosition = ref({ x: 0, y: 0 });
const tooltipStyle = computed(() => ({ const tooltipStyle = computed(() => ({
left: `${tooltipPosition.value.x + 15}px`, left: `${tooltipPosition.value.x + 15}px`,
top: `${tooltipPosition.value.y + 15}px` top: `${tooltipPosition.value.y + 15}px`
})); }));
onMounted(() => { const setupCheckerboardPattern = () => {
if (!canvasEl.value) return;
// This will be done with CSS using Tailwind's bg utilities
canvasEl.value.style.backgroundImage = `
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
`;
canvasEl.value.style.backgroundSize = '20px 20px';
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
};
const handleMouseDown = (e: MouseEvent) => {
if (!canvasEl.value || store.sprites.value.length === 0) return;
const rect = canvasEl.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Find which sprite was clicked
for (let i = store.sprites.value.length - 1; i >= 0; i--) {
const sprite = store.sprites.value[i];
if (
x >= sprite.x &&
x <= sprite.x + store.cellSize.width &&
y >= sprite.y &&
y <= sprite.y + store.cellSize.height
) {
store.draggedSprite.value = sprite;
store.dragOffset.x = x - sprite.x;
store.dragOffset.y = y - sprite.y;
break;
}
}
};
const handleMouseMove = (e: MouseEvent) => {
if (!canvasEl.value) return;
const rect = canvasEl.value.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Update tooltip
const cellX = Math.floor(x / store.cellSize.width);
const cellY = Math.floor(y / store.cellSize.height);
if (canvasEl.value &&
cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width &&
cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
isTooltipVisible.value = true;
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
tooltipPosition.value.x = e.clientX;
tooltipPosition.value.y = e.clientY;
} else {
isTooltipVisible.value = false;
}
// Move the sprite if we're dragging one
if (store.draggedSprite.value) {
if (store.isShiftPressed.value) {
// Free positioning within the cell bounds when shift is pressed
// First determine which cell we're in
const cellX = Math.floor(store.draggedSprite.value.x / store.cellSize.width);
const cellY = Math.floor(store.draggedSprite.value.y / store.cellSize.height);
// Calculate new position with constraints to stay within the cell
const newX = x - store.dragOffset.x;
const newY = y - store.dragOffset.y;
// Calculate cell boundaries
const cellLeft = cellX * store.cellSize.width;
const cellTop = cellY * store.cellSize.height;
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.width;
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.height;
// Constrain position to stay within the cell
store.draggedSprite.value.x = Math.max(cellLeft, Math.min(newX, cellRight));
store.draggedSprite.value.y = Math.max(cellTop, Math.min(newY, cellBottom));
} else {
// Calculate new position based on grid cells (snap to grid)
const newCellX = Math.floor((x - store.dragOffset.x) / store.cellSize.width);
const newCellY = Math.floor((y - store.dragOffset.y) / store.cellSize.height);
// Make sure we stay within bounds
if (canvasEl.value) { if (canvasEl.value) {
store.canvas.value = canvasEl.value; const maxCellX = Math.floor(canvasEl.value.width / store.cellSize.width) - 1;
store.ctx.value = canvasEl.value.getContext('2d'); const maxCellY = Math.floor(canvasEl.value.height / store.cellSize.height) - 1;
const boundedCellX = Math.max(0, Math.min(newCellX, maxCellX));
const boundedCellY = Math.max(0, Math.min(newCellY, maxCellY));
// Initialize canvas size store.draggedSprite.value.x = boundedCellX * store.cellSize.width;
canvasEl.value.width = 400; store.draggedSprite.value.y = boundedCellY * store.cellSize.height;
canvasEl.value.height = 300;
// Set up checkerboard background pattern
setupCheckerboardPattern();
setupCanvasEvents();
// Setup keyboard events for modifiers
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
} }
}); }
}
};
onBeforeUnmount(() => { const handleMouseUp = () => {
window.removeEventListener('keydown', handleKeyDown); store.draggedSprite.value = null;
window.removeEventListener('keyup', handleKeyUp); };
if (canvasEl.value) { const handleMouseOut = () => {
canvasEl.value.removeEventListener('mousedown', handleMouseDown); isTooltipVisible.value = false;
canvasEl.value.removeEventListener('mousemove', handleMouseMove); store.draggedSprite.value = null;
canvasEl.value.removeEventListener('mouseup', handleMouseUp); };
canvasEl.value.removeEventListener('mouseout', handleMouseOut);
}
});
const setupCheckerboardPattern = () => { const handleKeyDown = (e: KeyboardEvent) => {
if (!canvasEl.value) return; if (e.key === 'Shift') {
store.isShiftPressed.value = true;
}
};
// This will be done with CSS using Tailwind's bg utilities const handleKeyUp = (e: KeyboardEvent) => {
canvasEl.value.style.backgroundImage = ` if (e.key === 'Shift') {
linear-gradient(45deg, #1a1a1a 25%, transparent 25%), store.isShiftPressed.value = false;
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%), }
linear-gradient(45deg, transparent 75%, #1a1a1a 75%), };
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
`;
canvasEl.value.style.backgroundSize = '20px 20px';
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
};
const setupCanvasEvents = () => { const setupCanvasEvents = () => {
if (!canvasEl.value) return; if (!canvasEl.value) return;
canvasEl.value.addEventListener('mousedown', handleMouseDown); canvasEl.value.addEventListener('mousedown', handleMouseDown);
canvasEl.value.addEventListener('mousemove', handleMouseMove); canvasEl.value.addEventListener('mousemove', handleMouseMove);
canvasEl.value.addEventListener('mouseup', handleMouseUp); canvasEl.value.addEventListener('mouseup', handleMouseUp);
canvasEl.value.addEventListener('mouseout', handleMouseOut); canvasEl.value.addEventListener('mouseout', handleMouseOut);
}; };
const handleMouseDown = (e: MouseEvent) => { onMounted(() => {
if (!canvasEl.value || store.sprites.value.length === 0) return; if (canvasEl.value) {
store.canvas.value = canvasEl.value;
store.ctx.value = canvasEl.value.getContext('2d');
const rect = canvasEl.value.getBoundingClientRect(); // Initialize canvas size
const x = e.clientX - rect.left; canvasEl.value.width = 400;
const y = e.clientY - rect.top; canvasEl.value.height = 300;
// Find which sprite was clicked setupCheckerboardPattern();
for (let i = store.sprites.value.length - 1; i >= 0; i--) { setupCanvasEvents();
const sprite = store.sprites.value[i];
if (
x >= sprite.x &&
x <= sprite.x + store.cellSize.width &&
y >= sprite.y &&
y <= sprite.y + store.cellSize.height
) {
store.draggedSprite.value = sprite;
store.dragOffset.x = x - sprite.x;
store.dragOffset.y = y - sprite.y;
break;
}
}
};
const handleMouseMove = (e: MouseEvent) => { // Setup keyboard events for modifiers
if (!canvasEl.value) return; window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
}
});
const rect = canvasEl.value.getBoundingClientRect(); onBeforeUnmount(() => {
const x = e.clientX - rect.left; window.removeEventListener('keydown', handleKeyDown);
const y = e.clientY - rect.top; window.removeEventListener('keyup', handleKeyUp);
// Update tooltip if (canvasEl.value) {
const cellX = Math.floor(x / store.cellSize.width); canvasEl.value.removeEventListener('mousedown', handleMouseDown);
const cellY = Math.floor(y / store.cellSize.height); canvasEl.value.removeEventListener('mousemove', handleMouseMove);
canvasEl.value.removeEventListener('mouseup', handleMouseUp);
if (canvasEl.value && canvasEl.value.removeEventListener('mouseout', handleMouseOut);
cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width &&
cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
isTooltipVisible.value = true;
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
tooltipPosition.value.x = e.clientX;
tooltipPosition.value.y = e.clientY;
} else {
isTooltipVisible.value = false;
}
// Move the sprite if we're dragging one
if (store.draggedSprite.value) {
if (store.isShiftPressed.value) {
// Free positioning within the cell bounds when shift is pressed
// First determine which cell we're in
const cellX = Math.floor(store.draggedSprite.value.x / store.cellSize.width);
const cellY = Math.floor(store.draggedSprite.value.y / store.cellSize.height);
// Calculate new position with constraints to stay within the cell
const newX = x - store.dragOffset.x;
const newY = y - store.dragOffset.y;
// Calculate cell boundaries
const cellLeft = cellX * store.cellSize.width;
const cellTop = cellY * store.cellSize.height;
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.width;
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.height;
// Constrain position to stay within the cell
store.draggedSprite.value.x = Math.max(cellLeft, Math.min(newX, cellRight));
store.draggedSprite.value.y = Math.max(cellTop, Math.min(newY, cellBottom));
} else {
// Calculate new position based on grid cells (snap to grid)
const newCellX = Math.floor((x - store.dragOffset.x) / store.cellSize.width);
const newCellY = Math.floor((y - store.dragOffset.y) / store.cellSize.height);
// Make sure we stay within bounds
if (canvasEl.value) {
const maxCellX = Math.floor(canvasEl.value.width / store.cellSize.width) - 1;
const maxCellY = Math.floor(canvasEl.value.height / store.cellSize.height) - 1;
const boundedCellX = Math.max(0, Math.min(newCellX, maxCellX));
const boundedCellY = Math.max(0, Math.min(newCellY, maxCellY));
// Update sprite position to snap to grid
store.draggedSprite.value.x = boundedCellX * store.cellSize.width;
store.draggedSprite.value.y = boundedCellY * store.cellSize.height;
}
}
store.renderSpritesheetPreview();
// Update animation preview if paused
if (!store.animation.isPlaying && store.sprites.value.length > 0 && store.isModalOpen.value) {
store.renderAnimationFrame(store.animation.currentFrame);
}
}
};
const handleMouseUp = () => {
store.draggedSprite.value = null;
};
const handleMouseOut = () => {
store.draggedSprite.value = null;
isTooltipVisible.value = false;
};
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
store.isShiftPressed.value = true;
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
store.isShiftPressed.value = false;
}
};
return {
canvasEl,
isTooltipVisible,
tooltipText,
tooltipStyle
};
} }
}); });
</script> </script>

View File

@ -25,25 +25,15 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed } from 'vue'; import { computed } from 'vue'
import { useSpritesheetStore } from '../composables/useSpritesheetStore'; import { useSpritesheetStore } from '../composables/useSpritesheetStore'
export default defineComponent({ const store = useSpritesheetStore()
name: 'Notification',
setup() {
const store = useSpritesheetStore();
const notification = computed(() => store.notification); const notification = computed(() => store.notification)
const closeNotification = () => { const closeNotification = () => {
store.notification.isVisible = false; store.notification.isVisible = false
}; }
return {
notification,
closeNotification
};
}
});
</script> </script>

View File

@ -80,147 +80,128 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, ref, onMounted, computed, watch, onBeforeUnmount } from 'vue'; import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'
import { useSpritesheetStore } from '../composables/useSpritesheetStore'; import { useSpritesheetStore } from '../composables/useSpritesheetStore'
export default defineComponent({ const store = useSpritesheetStore()
name: 'PreviewModal', const animCanvas = ref<HTMLCanvasElement | null>(null)
setup() {
const store = useSpritesheetStore();
const animCanvas = ref<HTMLCanvasElement | null>(null);
const isModalOpen = computed(() => store.isModalOpen.value); const isModalOpen = computed(() => store.isModalOpen.value)
const sprites = computed(() => store.sprites.value); const sprites = computed(() => store.sprites.value)
const animation = computed(() => store.animation); const animation = computed(() => store.animation)
const currentFrame = ref(0); const currentFrame = ref(0)
const currentFrameDisplay = computed(() => { const currentFrameDisplay = computed(() => {
const totalFrames = Math.max(1, sprites.value.length); const totalFrames = Math.max(1, sprites.value.length)
const frame = Math.min(currentFrame.value + 1, totalFrames); const frame = Math.min(currentFrame.value + 1, totalFrames)
return `${frame} / ${totalFrames}`; return `${frame} / ${totalFrames}`
}); })
onMounted(() => { const handleKeyDown = (e: KeyboardEvent) => {
if (animCanvas.value) { if (!isModalOpen.value) return
store.animation.canvas = animCanvas.value;
store.animation.ctx = animCanvas.value.getContext('2d');
// Initialize canvas size if (e.key === 'Escape') {
animCanvas.value.width = 200; closeModal()
animCanvas.value.height = 200; } else if (e.key === ' ' || e.key === 'Spacebar') {
// Toggle play/pause
// Setup keyboard shortcuts for the modal if (animation.value.isPlaying) {
window.addEventListener('keydown', handleKeyDown); stopAnimation()
} } else if (sprites.value.length > 0) {
}); startAnimation()
}
onBeforeUnmount(() => { e.preventDefault()
window.removeEventListener('keydown', handleKeyDown); } else if (e.key === 'ArrowRight' && !animation.value.isPlaying && sprites.value.length > 0) {
}); // Next frame
currentFrame.value = (currentFrame.value + 1) % sprites.value.length
const handleKeyDown = (e: KeyboardEvent) => { updateFrame()
if (!isModalOpen.value) return; } else if (e.key === 'ArrowLeft' && !animation.value.isPlaying && sprites.value.length > 0) {
// Previous frame
if (e.key === 'Escape') { currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length
closeModal(); updateFrame()
} else if (e.key === ' ' || e.key === 'Spacebar') {
// Toggle play/pause
if (animation.value.isPlaying) {
stopAnimation();
} else if (sprites.value.length > 0) {
startAnimation();
}
e.preventDefault();
} else if (e.key === 'ArrowRight' && !animation.value.isPlaying && sprites.value.length > 0) {
// Next frame
currentFrame.value = (currentFrame.value + 1) % sprites.value.length;
updateFrame();
} else if (e.key === 'ArrowLeft' && !animation.value.isPlaying && sprites.value.length > 0) {
// Previous frame
currentFrame.value = (currentFrame.value - 1 + sprites.value.length) % sprites.value.length;
updateFrame();
}
};
const openModal = () => {
if (sprites.value.length === 0) {
store.showNotification('Please add sprites first', 'error');
return;
}
store.isModalOpen.value = true;
// Show the current frame
if (!animation.value.isPlaying && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value);
}
};
const closeModal = () => {
store.isModalOpen.value = false;
// Stop animation if it's playing
if (animation.value.isPlaying) {
stopAnimation();
}
};
const startAnimation = () => {
if (sprites.value.length === 0) return;
store.startAnimation();
};
const stopAnimation = () => {
store.stopAnimation();
};
const handleFrameChange = () => {
// Stop any running animation
if (animation.value.isPlaying) {
stopAnimation();
}
updateFrame();
};
const updateFrame = () => {
animation.value.currentFrame = currentFrame.value;
animation.value.manualUpdate = true;
store.renderAnimationFrame(currentFrame.value);
};
const handleFrameRateChange = () => {
// If animation is currently playing, restart it with the new frame rate
if (animation.value.isPlaying) {
stopAnimation();
startAnimation();
}
};
// Keep currentFrame in sync with animation.currentFrame
watch(() => animation.value.currentFrame, (newVal) => {
currentFrame.value = newVal;
});
return {
animCanvas,
isModalOpen,
sprites,
animation,
currentFrame,
currentFrameDisplay,
openModal,
closeModal,
startAnimation,
stopAnimation,
handleFrameChange,
handleFrameRateChange
};
} }
}); }
const openModal = () => {
if (sprites.value.length === 0) {
store.showNotification('Please add sprites first', 'error')
return
}
store.isModalOpen.value = true
// Show the current frame
if (!animation.value.isPlaying && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value)
}
}
const closeModal = () => {
store.isModalOpen.value = false
// Stop animation if it's playing
if (animation.value.isPlaying) {
stopAnimation()
}
}
const startAnimation = () => {
if (sprites.value.length === 0) return
store.startAnimation()
}
const stopAnimation = () => {
store.stopAnimation()
}
const handleFrameChange = () => {
// Stop any running animation
if (animation.value.isPlaying) {
stopAnimation()
}
updateFrame()
}
const updateFrame = () => {
animation.value.currentFrame = currentFrame.value
animation.value.manualUpdate = true
store.renderAnimationFrame(currentFrame.value)
}
const handleFrameRateChange = () => {
// If animation is currently playing, restart it with the new frame rate
if (animation.value.isPlaying) {
stopAnimation()
startAnimation()
}
}
onMounted(() => {
if (animCanvas.value) {
store.animation.canvas = animCanvas.value
store.animation.ctx = animCanvas.value.getContext('2d')
// Initialize canvas size
animCanvas.value.width = 200
animCanvas.value.height = 200
// Setup keyboard shortcuts for the modal
window.addEventListener('keydown', handleKeyDown)
}
})
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeyDown)
})
// Keep currentFrame in sync with animation.currentFrame
watch(() => animation.value.currentFrame, (newVal) => {
currentFrame.value = newVal
})
// Expose openModal for external use
defineExpose({ openModal })
</script> </script>
<style scoped> <style scoped>

View File

@ -89,55 +89,40 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, computed } from 'vue'; import { computed } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore'; import { useSpritesheetStore } from '../composables/useSpritesheetStore';
import DropZone from './DropZone.vue'; import DropZone from './DropZone.vue';
import SpriteList from './SpriteList.vue'; import SpriteList from './SpriteList.vue';
export default defineComponent({ const store = useSpritesheetStore();
name: 'Sidebar', const sprites = computed(() => store.sprites.value);
components: {
DropZone,
SpriteList
},
setup() {
const store = useSpritesheetStore();
const handleUpload = () => { const handleUpload = () => {
// The dropzone component handles adding sprites to the store // The dropzone component handles adding sprites to the store
// This is just for event handling if needed // This is just for event handling if needed
}; };
const handleSpriteClick = (spriteId: string) => { const handleSpriteClick = (spriteId: string) => {
store.highlightSprite(spriteId); store.highlightSprite(spriteId);
}; };
const openPreviewModal = () => { const openPreviewModal = () => {
if (store.sprites.value.length === 0) { if (store.sprites.value.length === 0) {
store.showNotification('Please add sprites first', 'error'); store.showNotification('Please add sprites first', 'error');
return; return;
}
store.isModalOpen.value = true;
};
const confirmClearAll = () => {
if (confirm('Are you sure you want to clear all sprites?')) {
store.clearAllSprites();
store.showNotification('All sprites cleared');
}
};
return {
sprites: computed(() => store.sprites.value),
autoArrangeSprites: store.autoArrangeSprites,
downloadSpritesheet: store.downloadSpritesheet,
confirmClearAll,
handleUpload,
handleSpriteClick,
openPreviewModal
};
} }
});
store.isModalOpen.value = true;
};
const confirmClearAll = () => {
if (confirm('Are you sure you want to clear all sprites?')) {
store.clearAllSprites();
store.showNotification('All sprites cleared');
}
};
// Expose store methods directly
const { autoArrangeSprites, downloadSpritesheet } = store;
</script> </script>

View File

@ -7,7 +7,7 @@
<div <div
v-for="(sprite, index) in sprites" v-for="(sprite, index) in sprites"
:key="sprite.id" :key="sprite.id"
@click="$emit('sprite-clicked', sprite.id)" @click="$emit('spriteClicked', sprite.id)"
class="border border-gray-600 rounded bg-gray-700 p-2 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md" class="border border-gray-600 rounded bg-gray-700 p-2 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md"
> >
<img <img
@ -22,27 +22,18 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import { defineComponent, type PropType } from 'vue'; import type { Sprite } from '../composables/useSpritesheetStore'
import { type Sprite } from '../composables/useSpritesheetStore';
export default defineComponent({ defineProps<{
name: 'SpriteList', sprites: Sprite[]
props: { }>()
sprites: {
type: Array as PropType<Sprite[]>,
required: true
}
},
emits: ['sprite-clicked'],
setup() {
const truncateName = (name: string) => {
return name.length > 10 ? `${name.substring(0, 10)}...` : name;
};
return { defineEmits<{
truncateName spriteClicked: [id: string]
}; }>()
}
}); const truncateName = (name: string) => {
return name.length > 10 ? `${name.substring(0, 10)}...` : name
}
</script> </script>

View File

@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})