use options api
This commit is contained in:
parent
a989c8719f
commit
22781b1883
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
@ -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>
|
@ -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 }
|
|
||||||
})
|
|
Loading…
x
Reference in New Issue
Block a user