Compare commits
5 Commits
f6c089ba58
...
293bed9135
Author | SHA1 | Date | |
---|---|---|---|
293bed9135 | |||
1a16457efc | |||
90a12fb6e0 | |||
a23b1c8463 | |||
100bacc471 |
223
src/components/BaseModal.vue
Normal file
223
src/components/BaseModal.vue
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
<template>
|
||||||
|
<!-- Base Modal Component -->
|
||||||
|
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="modelValue">
|
||||||
|
<!-- Modal backdrop with semi-transparent background (only if showBackdrop is true) -->
|
||||||
|
<div v-if="showBackdrop" class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
||||||
|
|
||||||
|
<!-- Modal content -->
|
||||||
|
<div class="bg-gray-800 rounded-lg shadow-lg pointer-events-auto relative flex flex-col" :class="[{ 'border border-gray-400': showBorder }, { 'max-w-2xl w-full max-h-[90vh]': !movable }, customClass]" :style="modalStyle" ref="modalRef">
|
||||||
|
<!-- Modal header - fixed -->
|
||||||
|
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600" :class="{ 'cursor-move': movable }" @mousedown="movable ? startDrag($event) : null">
|
||||||
|
<div class="flex items-center gap-2 text-lg font-semibold">
|
||||||
|
<slot name="header-icon">
|
||||||
|
<i class="fas fa-window-maximize text-blue-500"></i>
|
||||||
|
</slot>
|
||||||
|
<slot name="header-title">Modal</slot>
|
||||||
|
</div>
|
||||||
|
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal body - scrollable -->
|
||||||
|
<div class="flex-1 overflow-y-auto min-h-0">
|
||||||
|
<div class="p-6">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Resize handle (only if resizable is true) -->
|
||||||
|
<div v-if="resizable" class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize group" @mousedown="startResize">
|
||||||
|
<div class="absolute bottom-0 right-0 w-full h-full bg-gray-700 bg-opacity-0 group-hover:bg-opacity-50 transition-colors rounded-tl flex items-end justify-end">
|
||||||
|
<i class="fas fa-arrows-alt text-gray-400 group-hover:text-blue-500 transition-colors m-1"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Modal',
|
||||||
|
},
|
||||||
|
showBackdrop: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
showBorder: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
movable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
resizable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
initialPosition: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ x: 0, y: 0 }),
|
||||||
|
},
|
||||||
|
initialSize: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ width: 800, height: 600 }),
|
||||||
|
},
|
||||||
|
customClass: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(['update:modelValue', 'close']);
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
const modalRef = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
// State
|
||||||
|
const position = ref({
|
||||||
|
x: props.initialPosition.x,
|
||||||
|
y: props.initialPosition.y,
|
||||||
|
});
|
||||||
|
const size = ref({
|
||||||
|
width: props.initialSize.width,
|
||||||
|
height: props.initialSize.height,
|
||||||
|
});
|
||||||
|
const isDragging = ref(false);
|
||||||
|
const dragOffset = ref({ x: 0, y: 0 });
|
||||||
|
const isResizing = ref(false);
|
||||||
|
const resizeStart = ref({ x: 0, y: 0 });
|
||||||
|
const initialSize = ref({ width: 0, height: 0 });
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const modalStyle = computed(() => {
|
||||||
|
if (props.movable) {
|
||||||
|
return {
|
||||||
|
transform: `translate3d(${position.value.x}px, ${position.value.y}px, 0)`,
|
||||||
|
width: `${size.value.width}px`,
|
||||||
|
height: `${size.value.height}px`,
|
||||||
|
maxWidth: '90vw',
|
||||||
|
maxHeight: '90vh',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const closeModal = () => {
|
||||||
|
emit('update:modelValue', false);
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDrag = (e: MouseEvent) => {
|
||||||
|
// Only start dragging if it's the header (not a button in the header)
|
||||||
|
if ((e.target as HTMLElement).tagName === 'BUTTON' || (e.target as HTMLElement).closest('button')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDragging.value = true;
|
||||||
|
dragOffset.value = {
|
||||||
|
x: e.clientX - position.value.x,
|
||||||
|
y: e.clientY - position.value.y,
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleDrag);
|
||||||
|
document.addEventListener('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrag = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
|
||||||
|
position.value = {
|
||||||
|
x: e.clientX - dragOffset.value.x,
|
||||||
|
y: e.clientY - dragOffset.value.y,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
document.removeEventListener('mousemove', handleDrag);
|
||||||
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startResize = (e: MouseEvent) => {
|
||||||
|
isResizing.value = true;
|
||||||
|
resizeStart.value = { x: e.clientX, y: e.clientY };
|
||||||
|
initialSize.value = { ...size.value };
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', handleResize);
|
||||||
|
document.addEventListener('mouseup', stopResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleResize = (e: MouseEvent) => {
|
||||||
|
if (!isResizing.value) return;
|
||||||
|
|
||||||
|
const deltaX = e.clientX - resizeStart.value.x;
|
||||||
|
const deltaY = e.clientY - resizeStart.value.y;
|
||||||
|
|
||||||
|
size.value = {
|
||||||
|
width: Math.max(300, initialSize.value.width + deltaX),
|
||||||
|
height: Math.max(200, initialSize.value.height + deltaY),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopResize = () => {
|
||||||
|
isResizing.value = false;
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Center the modal in the viewport when it opens
|
||||||
|
const centerModal = () => {
|
||||||
|
if (props.movable && props.modelValue) {
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
|
||||||
|
position.value = {
|
||||||
|
x: (viewportWidth - size.value.width) / 2,
|
||||||
|
y: (viewportHeight - size.value.height) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle hooks
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.movable) {
|
||||||
|
centerModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
document.removeEventListener('mousemove', handleDrag);
|
||||||
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
|
document.removeEventListener('mousemove', handleResize);
|
||||||
|
document.removeEventListener('mouseup', stopResize);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for changes in modelValue to center the modal when it opens
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
newValue => {
|
||||||
|
if (newValue && props.movable) {
|
||||||
|
centerModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose methods
|
||||||
|
defineExpose({
|
||||||
|
closeModal,
|
||||||
|
centerModal,
|
||||||
|
});
|
||||||
|
</script>
|
@ -1,20 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Help Modal -->
|
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
<template #header-icon>
|
||||||
<!-- Modal backdrop with semi-transparent background -->
|
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
|
||||||
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div class="bg-gray-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-auto shadow-lg pointer-events-auto relative">
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<i class="fas fa-question-circle text-blue-500"></i>
|
<i class="fas fa-question-circle text-blue-500"></i>
|
||||||
<span>Help</span>
|
</template>
|
||||||
</div>
|
<template #header-title>Help</template>
|
||||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Help content tabs -->
|
<!-- Help content tabs -->
|
||||||
@ -76,16 +65,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseModal>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
import BaseModal from './BaseModal.vue';
|
||||||
|
|
||||||
const store = useSpritesheetStore();
|
const store = useSpritesheetStore();
|
||||||
const isModalOpen = computed(() => store.isHelpModalOpen.value);
|
const isModalOpen = computed({
|
||||||
|
get: () => store.isHelpModalOpen.value,
|
||||||
|
set: value => {
|
||||||
|
store.isHelpModalOpen.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
const activeTab = ref('shortcuts');
|
const activeTab = ref('shortcuts');
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
|
@ -11,14 +11,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<div ref="containerEl" class="relative overflow-auto bg-gray-700 rounded border border-gray-600 h-96" :class="{ 'cursor-grab': !isPanning, 'cursor-grabbing': isPanning }">
|
<div
|
||||||
|
ref="containerEl"
|
||||||
|
class="relative overflow-auto rounded border border-gray-600 h-96"
|
||||||
|
:class="{ 'cursor-grab': !isPanning, 'cursor-grabbing': isPanning }"
|
||||||
|
: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%)
|
||||||
|
`,
|
||||||
|
backgroundSize: '20px 20px',
|
||||||
|
backgroundPosition: '0 0, 0 10px, 10px -10px, -10px 0px',
|
||||||
|
backgroundColor: '#2d3748',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<canvas
|
<canvas
|
||||||
ref="canvasEl"
|
ref="canvasEl"
|
||||||
class="block"
|
class="block pixel-art"
|
||||||
:style="{
|
:style="{
|
||||||
transform: `scale(${store.zoomLevel.value})`,
|
transform: `scale(${store.zoomLevel.value})`,
|
||||||
transformOrigin: 'top left',
|
transformOrigin: 'top left',
|
||||||
imageRendering: 'pixelated', // Keep pixel art sharp when zooming
|
imageRendering: 'pixelated',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
}"
|
}"
|
||||||
></canvas>
|
></canvas>
|
||||||
</div>
|
</div>
|
||||||
@ -101,16 +117,7 @@
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setupCheckerboardPattern = () => {
|
const setupCheckerboardPattern = () => {
|
||||||
if (!canvasEl.value) return;
|
// Remove this function or leave it empty since we don't need it anymore
|
||||||
|
|
||||||
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 updateCanvasSize = () => {
|
const updateCanvasSize = () => {
|
||||||
|
@ -1,27 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Make the outer container always pointer-events-none so clicks pass through -->
|
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true" :initialSize="modalSize" customClass="scrollbar-hide">
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none">
|
<template #header-icon>
|
||||||
<!-- Apply pointer-events-auto ONLY to the modal itself so it can be interacted with -->
|
|
||||||
<div
|
|
||||||
class="bg-gray-800 rounded-lg overflow-auto scrollbar-hide shadow-lg pointer-events-auto relative border border-gray-400"
|
|
||||||
:class="{ invisible: !isModalOpen, visible: isModalOpen }"
|
|
||||||
:style="{
|
|
||||||
transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)`,
|
|
||||||
width: `${modalSize.width}px`,
|
|
||||||
height: `${modalSize.height}px`,
|
|
||||||
maxWidth: '90vw',
|
|
||||||
maxHeight: '90vh',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600 cursor-move" @mousedown="startDrag">
|
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold select-none">
|
|
||||||
<i class="fas fa-film text-blue-500"></i>
|
<i class="fas fa-film text-blue-500"></i>
|
||||||
<span>Animation Preview</span>
|
</template>
|
||||||
</div>
|
<template #header-title>Animation Preview</template>
|
||||||
<button @click="closeModal" class="text-gray-400 hover:text-gray-200 text-xl">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6 flex flex-col h-[calc(100%-64px)]">
|
<div class="p-6 flex flex-col h-[calc(100%-64px)]">
|
||||||
<div class="flex flex-wrap items-center gap-4 mb-6">
|
<div class="flex flex-wrap items-center gap-4 mb-6">
|
||||||
@ -75,15 +57,7 @@
|
|||||||
<button @click="zoomIn" :disabled="previewZoom >= 5" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
<button @click="zoomIn" :disabled="previewZoom >= 5" class="flex items-center justify-center w-8 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||||
<i class="fas fa-search-plus"></i>
|
<i class="fas fa-search-plus"></i>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<!-- Apply Offset button removed -->
|
||||||
@click="applyOffsetsToMainView"
|
|
||||||
:disabled="!hasSpriteOffset"
|
|
||||||
class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
|
||||||
title="Permanently apply offset to sprite position"
|
|
||||||
>
|
|
||||||
<i class="fas fa-save"></i>
|
|
||||||
Apply Offset
|
|
||||||
</button>
|
|
||||||
<button @click="resetZoom" :disabled="previewZoom === 1" class="flex items-center justify-center px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">Reset Zoom</button>
|
<button @click="resetZoom" :disabled="previewZoom === 1" class="flex items-center justify-center px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">Reset Zoom</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -96,23 +70,17 @@
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<!-- Reset sprite position button -->
|
<!-- Reset sprite position button -->
|
||||||
<button
|
<button @click="resetSpritePosition" :disabled="!hasSpriteOffset" class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500" title="Center sprite in cell">
|
||||||
@click="resetSpritePosition"
|
|
||||||
:disabled="!hasSpriteOffset"
|
|
||||||
class="flex items-center gap-1 px-2 h-8 bg-gray-700 text-gray-200 border border-gray-600 rounded text-xs transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
|
||||||
title="Reset sprite to original position"
|
|
||||||
>
|
|
||||||
<i class="fas fa-crosshairs"></i>
|
<i class="fas fa-crosshairs"></i>
|
||||||
Reset Position
|
Center Sprite
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-center items-center bg-gray-700 p-6 rounded mb-6 relative overflow-auto flex-grow">
|
<div class="flex flex-col justify-center items-center bg-gray-700 p-6 rounded mb-6 relative overflow-auto flex-grow">
|
||||||
<!-- Tooltip for dragging instructions -->
|
<!-- Tooltip for dragging instructions -->
|
||||||
<div class="text-xs text-gray-400 mb-2" v-if="hasSpriteOffset || sprites.length > 0">
|
<div class="text-xs text-gray-400 mb-2" v-if="sprites.length > 0">
|
||||||
<span v-if="hasSpriteOffset">Sprite offset: {{ Math.round(spriteOffset.x) }}px, {{ Math.round(spriteOffset.y) }}px</span>
|
<span>Position: {{ Math.round(spriteOffset.x) }}px, {{ Math.round(spriteOffset.y) }}px (drag to move within cell)</span>
|
||||||
<span v-else>Click and drag the sprite to move it within the cell</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -130,40 +98,49 @@
|
|||||||
:style="{
|
:style="{
|
||||||
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
|
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
|
||||||
cursor: isCanvasDragging ? 'grabbing' : 'move',
|
cursor: isCanvasDragging ? 'grabbing' : 'move',
|
||||||
|
width: `${store.cellSize.width}px`,
|
||||||
|
height: `${store.cellSize.height}px`,
|
||||||
|
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%)
|
||||||
|
`,
|
||||||
|
backgroundSize: '10px 10px',
|
||||||
|
backgroundPosition: '0 0, 0 5px, 5px -5px, -5px 0px',
|
||||||
|
backgroundColor: '#2d3748',
|
||||||
}"
|
}"
|
||||||
@mousedown.stop="startCanvasDrag"
|
@mousedown.stop="startCanvasDrag"
|
||||||
title="Drag to move sprite within cell"
|
title="Drag to move sprite within cell"
|
||||||
>
|
>
|
||||||
<canvas ref="animCanvas" class="block pixel-art"></canvas>
|
<canvas ref="animCanvas" class="block pixel-art absolute top-0 left-0"></canvas>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</BaseModal>
|
||||||
<!-- Resize handle - larger and more noticeable -->
|
|
||||||
<div class="absolute bottom-0 right-0 w-8 h-8 cursor-se-resize flex items-end justify-end bg-gradient-to-br from-transparent to-gray-700 hover:to-blue-500 transition-colors duration-200" @mousedown="startResize">
|
|
||||||
<i class="fas fa-grip-lines-diagonal text-gray-400 hover:text-white p-1"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
import BaseModal from './BaseModal.vue';
|
||||||
|
|
||||||
const store = useSpritesheetStore();
|
const store = useSpritesheetStore();
|
||||||
const animCanvas = ref<HTMLCanvasElement | null>(null);
|
const animCanvas = ref<HTMLCanvasElement | null>(null);
|
||||||
|
|
||||||
const isModalOpen = computed(() => store.isModalOpen.value);
|
const isModalOpen = computed({
|
||||||
|
get: () => store.isModalOpen.value,
|
||||||
|
set: value => {
|
||||||
|
store.isModalOpen.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
const sprites = computed(() => store.sprites.value);
|
const sprites = computed(() => store.sprites.value);
|
||||||
const animation = computed(() => store.animation);
|
const animation = computed(() => store.animation);
|
||||||
const previewBorder = computed(() => store.previewBorder);
|
const previewBorder = computed(() => store.previewBorder);
|
||||||
|
|
||||||
const currentFrame = ref(0);
|
const currentFrame = ref(0);
|
||||||
const position = ref({ x: 0, y: 0 });
|
// Position is now handled by BaseModal
|
||||||
const isDragging = ref(false);
|
|
||||||
const dragOffset = ref({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
// New state for added features
|
// New state for added features
|
||||||
const previewZoom = ref(1);
|
const previewZoom = ref(1);
|
||||||
@ -197,11 +174,8 @@
|
|||||||
const isViewportDragging = ref(false);
|
const isViewportDragging = ref(false);
|
||||||
const viewportDragStart = ref({ x: 0, y: 0 });
|
const viewportDragStart = ref({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Modal size state for resize functionality
|
// Modal size state - used by BaseModal
|
||||||
const modalSize = ref({ width: 800, height: 600 });
|
const modalSize = ref({ width: 800, height: 600 });
|
||||||
const isResizing = ref(false);
|
|
||||||
const initialSize = ref({ width: 0, height: 0 });
|
|
||||||
const resizeStart = ref({ x: 0, y: 0 });
|
|
||||||
|
|
||||||
const currentFrameDisplay = computed(() => {
|
const currentFrameDisplay = computed(() => {
|
||||||
if (sprites.value.length === 0) return '0 / 0';
|
if (sprites.value.length === 0) return '0 / 0';
|
||||||
@ -213,10 +187,7 @@
|
|||||||
return spriteOffset.x !== 0 || spriteOffset.y !== 0;
|
return spriteOffset.x !== 0 || spriteOffset.y !== 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const applyOffsetsToMainView = () => {
|
// applyOffsetsToMainView function removed
|
||||||
store.applyOffsetsToMainView();
|
|
||||||
store.showNotification('Offset permanently applied to sprite position');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!isModalOpen.value) return;
|
if (!isModalOpen.value) return;
|
||||||
@ -257,26 +228,11 @@
|
|||||||
resetSpritePosition();
|
resetSpritePosition();
|
||||||
} else {
|
} else {
|
||||||
// R: Reset both sprite position and viewport
|
// R: Reset both sprite position and viewport
|
||||||
// Reset the sprite offset for the current frame
|
resetSpritePosition();
|
||||||
const frameOffset = store.getSpriteOffset(currentFrame.value);
|
|
||||||
frameOffset.x = 0;
|
|
||||||
frameOffset.y = 0;
|
|
||||||
store.currentSpriteOffset.x = 0;
|
|
||||||
store.currentSpriteOffset.y = 0;
|
|
||||||
viewportOffset.value = { x: 0, y: 0 };
|
|
||||||
updateFrame();
|
updateFrame();
|
||||||
store.showNotification('View and position reset');
|
store.showNotification('View and position reset');
|
||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
} else if (previewZoom.value > 1) {
|
|
||||||
// Arrow key navigation for panning when zoomed in
|
|
||||||
const panAmount = 10;
|
|
||||||
if (e.key === 'ArrowUp') {
|
|
||||||
viewportOffset.value.y += panAmount / previewZoom.value;
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === 'ArrowDown') {
|
|
||||||
viewportOffset.value.y -= panAmount / previewZoom.value;
|
|
||||||
e.preventDefault();
|
|
||||||
} else if (e.key === 'ArrowLeft' && animation.value.isPlaying) {
|
} else if (e.key === 'ArrowLeft' && animation.value.isPlaying) {
|
||||||
viewportOffset.value.x += panAmount / previewZoom.value;
|
viewportOffset.value.x += panAmount / previewZoom.value;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -284,7 +240,6 @@
|
|||||||
viewportOffset.value.x -= panAmount / previewZoom.value;
|
viewportOffset.value.x -= panAmount / previewZoom.value;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openModal = async () => {
|
const openModal = async () => {
|
||||||
@ -293,27 +248,27 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Center the modal in the viewport
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
position.value = {
|
|
||||||
x: (viewportWidth - modalSize.value.width) / 2,
|
|
||||||
y: (viewportHeight - modalSize.value.height) / 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Reset zoom but keep sprite offset if it exists
|
// Reset zoom but keep sprite offset if it exists
|
||||||
previewZoom.value = 1;
|
previewZoom.value = 1;
|
||||||
viewportOffset.value = { x: 0, y: 0 };
|
viewportOffset.value = { x: 0, y: 0 };
|
||||||
|
|
||||||
// Reset modal size to default
|
// Reset modal size to default
|
||||||
modalSize.value = { width: 800, height: 600 };
|
modalSize.value = { width: 800, height: 600 };
|
||||||
|
// Note: Modal positioning is now handled by BaseModal
|
||||||
|
|
||||||
// Reset to first frame
|
// Reset to first frame
|
||||||
currentFrame.value = 0;
|
currentFrame.value = 0;
|
||||||
animation.value.currentFrame = 0;
|
animation.value.currentFrame = 0;
|
||||||
|
|
||||||
|
// Initialize canvas and retry if it fails
|
||||||
await initializeCanvas();
|
await initializeCanvas();
|
||||||
|
|
||||||
|
// If canvas is still not initialized, try again after a short delay
|
||||||
|
if (!animCanvas.value || !store.animation.canvas) {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
await initializeCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
// Set modal open state if not already open
|
// Set modal open state if not already open
|
||||||
if (!store.isModalOpen.value) {
|
if (!store.isModalOpen.value) {
|
||||||
store.isModalOpen.value = true;
|
store.isModalOpen.value = true;
|
||||||
@ -327,9 +282,22 @@
|
|||||||
|
|
||||||
// Force render the first frame
|
// Force render the first frame
|
||||||
if (sprites.value.length > 0) {
|
if (sprites.value.length > 0) {
|
||||||
|
// Get the current sprite
|
||||||
|
const currentSprite = sprites.value[0];
|
||||||
|
|
||||||
|
// Calculate center position
|
||||||
|
const centerX = Math.max(0, Math.floor((store.cellSize.width - currentSprite.width) / 2));
|
||||||
|
const centerY = Math.max(0, Math.floor((store.cellSize.height - currentSprite.height) / 2));
|
||||||
|
|
||||||
// Get the frame-specific offset for the first frame
|
// Get the frame-specific offset for the first frame
|
||||||
const frameOffset = store.getSpriteOffset(0);
|
const frameOffset = store.getSpriteOffset(0);
|
||||||
|
|
||||||
|
// If the offset is (0,0), center the sprite
|
||||||
|
if (frameOffset.x === 0 && frameOffset.y === 0) {
|
||||||
|
frameOffset.x = centerX;
|
||||||
|
frameOffset.y = centerY;
|
||||||
|
}
|
||||||
|
|
||||||
// Update the current offset for UI display
|
// Update the current offset for UI display
|
||||||
store.currentSpriteOffset.x = frameOffset.x;
|
store.currentSpriteOffset.x = frameOffset.x;
|
||||||
store.currentSpriteOffset.y = frameOffset.y;
|
store.currentSpriteOffset.y = frameOffset.y;
|
||||||
@ -340,12 +308,19 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateCanvasSize = () => {
|
const updateCanvasSize = () => {
|
||||||
if (animCanvas.value && store.cellSize.width && store.cellSize.height) {
|
if (!animCanvas.value) {
|
||||||
|
console.warn('PreviewModal: Cannot update canvas size - canvas not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store.cellSize.width && store.cellSize.height) {
|
||||||
animCanvas.value.width = store.cellSize.width;
|
animCanvas.value.width = store.cellSize.width;
|
||||||
animCanvas.value.height = store.cellSize.height;
|
animCanvas.value.height = store.cellSize.height;
|
||||||
|
|
||||||
// Also update container size
|
// Also update container size
|
||||||
updateCanvasContainerSize();
|
updateCanvasContainerSize();
|
||||||
|
} else {
|
||||||
|
console.warn('PreviewModal: Cannot update canvas size - invalid cell dimensions');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -395,79 +370,12 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modal drag functionality
|
// Modal drag and resize functionality is now handled by BaseModal
|
||||||
const startDrag = (e: MouseEvent) => {
|
|
||||||
// Don't allow drag if currently resizing
|
|
||||||
if (isResizing.value) return;
|
|
||||||
|
|
||||||
isDragging.value = true;
|
|
||||||
dragOffset.value = {
|
|
||||||
x: e.clientX - position.value.x,
|
|
||||||
y: e.clientY - position.value.y,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add temporary event listeners
|
|
||||||
window.addEventListener('mousemove', handleDrag);
|
|
||||||
window.addEventListener('mouseup', stopDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrag = (e: MouseEvent) => {
|
|
||||||
if (!isDragging.value) return;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
position.value = {
|
|
||||||
x: e.clientX - dragOffset.value.x,
|
|
||||||
y: e.clientY - dragOffset.value.y,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopDrag = () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
window.removeEventListener('mousemove', handleDrag);
|
|
||||||
window.removeEventListener('mouseup', stopDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
// NEW: Modal resize functionality
|
|
||||||
const startResize = (e: MouseEvent) => {
|
|
||||||
isResizing.value = true;
|
|
||||||
initialSize.value = { ...modalSize.value };
|
|
||||||
resizeStart.value = { x: e.clientX, y: e.clientY };
|
|
||||||
|
|
||||||
// Add temporary event listeners
|
|
||||||
window.addEventListener('mousemove', handleResize);
|
|
||||||
window.addEventListener('mouseup', stopResize);
|
|
||||||
|
|
||||||
// Prevent default to avoid text selection
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleResize = (e: MouseEvent) => {
|
|
||||||
if (!isResizing.value) return;
|
|
||||||
|
|
||||||
const deltaX = e.clientX - resizeStart.value.x;
|
|
||||||
const deltaY = e.clientY - resizeStart.value.y;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
// Calculate new size with minimum constraints
|
|
||||||
const newWidth = Math.max(400, initialSize.value.width + deltaX);
|
|
||||||
const newHeight = Math.max(400, initialSize.value.height + deltaY);
|
|
||||||
|
|
||||||
// Update modal size
|
|
||||||
modalSize.value = {
|
|
||||||
width: newWidth,
|
|
||||||
height: newHeight,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopResize = () => {
|
|
||||||
isResizing.value = false;
|
|
||||||
window.removeEventListener('mousemove', handleResize);
|
|
||||||
window.removeEventListener('mouseup', stopResize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeCanvas = async () => {
|
const initializeCanvas = async () => {
|
||||||
|
// Wait for the next tick to ensure the canvas element is rendered
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
if (!animCanvas.value) {
|
if (!animCanvas.value) {
|
||||||
console.error('PreviewModal: Animation canvas not found');
|
console.error('PreviewModal: Animation canvas not found');
|
||||||
return;
|
return;
|
||||||
@ -520,22 +428,30 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset sprite position to original
|
// Reset sprite position to center
|
||||||
const resetSpritePosition = () => {
|
const resetSpritePosition = () => {
|
||||||
// Reset the sprite offset for the current frame to zero
|
// Get current sprite
|
||||||
|
const currentSprite = sprites.value[currentFrame.value];
|
||||||
|
if (!currentSprite) return;
|
||||||
|
|
||||||
|
// Calculate center position
|
||||||
|
const centerX = Math.max(0, Math.floor((store.cellSize.width - currentSprite.width) / 2));
|
||||||
|
const centerY = Math.max(0, Math.floor((store.cellSize.height - currentSprite.height) / 2));
|
||||||
|
|
||||||
|
// Reset the sprite offset for the current frame to the center position
|
||||||
const frameOffset = store.getSpriteOffset(currentFrame.value);
|
const frameOffset = store.getSpriteOffset(currentFrame.value);
|
||||||
frameOffset.x = 0;
|
frameOffset.x = centerX;
|
||||||
frameOffset.y = 0;
|
frameOffset.y = centerY;
|
||||||
|
|
||||||
// Also update the current offset
|
// Also update the current offset
|
||||||
store.currentSpriteOffset.x = 0;
|
store.currentSpriteOffset.x = centerX;
|
||||||
store.currentSpriteOffset.y = 0;
|
store.currentSpriteOffset.y = centerY;
|
||||||
|
|
||||||
// Update the frame to reflect the change
|
// Update the frame to reflect the change
|
||||||
updateFrame();
|
updateFrame();
|
||||||
|
|
||||||
// Show a notification
|
// Show a notification
|
||||||
store.showNotification('Sprite position reset to original');
|
store.showNotification('Sprite position reset to center');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update canvas container size based on zoom level
|
// Update canvas container size based on zoom level
|
||||||
@ -549,74 +465,85 @@
|
|||||||
// Canvas drag functions for moving the sprite within its cell
|
// Canvas drag functions for moving the sprite within its cell
|
||||||
const startCanvasDrag = (e: MouseEvent) => {
|
const startCanvasDrag = (e: MouseEvent) => {
|
||||||
if (sprites.value.length === 0) return;
|
if (sprites.value.length === 0) return;
|
||||||
|
|
||||||
// Don't start sprite dragging if we're already dragging the viewport
|
|
||||||
if (isViewportDragging.value) return;
|
if (isViewportDragging.value) return;
|
||||||
|
|
||||||
isCanvasDragging.value = true;
|
isCanvasDragging.value = true;
|
||||||
|
|
||||||
|
// Store initial position
|
||||||
canvasDragStart.value = {
|
canvasDragStart.value = {
|
||||||
x: e.clientX,
|
x: e.clientX,
|
||||||
y: e.clientY,
|
y: e.clientY,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add temporary event listeners
|
window.addEventListener('mousemove', handleCanvasDrag, { capture: true });
|
||||||
window.addEventListener('mousemove', handleCanvasDrag);
|
window.addEventListener('mouseup', stopCanvasDrag, { capture: true });
|
||||||
window.addEventListener('mouseup', stopCanvasDrag);
|
|
||||||
|
|
||||||
// Prevent default to avoid text selection
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasDrag = (e: MouseEvent) => {
|
const handleCanvasDrag = (e: MouseEvent) => {
|
||||||
if (!isCanvasDragging.value) return;
|
if (!isCanvasDragging.value) return;
|
||||||
|
|
||||||
const deltaX = e.clientX - canvasDragStart.value.x;
|
|
||||||
const deltaY = e.clientY - canvasDragStart.value.y;
|
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
// Get the frame-specific offset
|
|
||||||
const frameOffset = store.getSpriteOffset(currentFrame.value);
|
|
||||||
|
|
||||||
// Calculate new position and round to nearest pixel
|
|
||||||
const newX = Math.round(frameOffset.x + deltaX / previewZoom.value);
|
|
||||||
const newY = Math.round(frameOffset.y + deltaY / previewZoom.value);
|
|
||||||
|
|
||||||
// Get current sprite
|
// Get current sprite
|
||||||
const currentSprite = sprites.value[currentFrame.value];
|
const currentSprite = sprites.value[currentFrame.value];
|
||||||
if (!currentSprite) return;
|
if (!currentSprite) return;
|
||||||
|
|
||||||
// Calculate maximum allowed offset based on sprite and cell size
|
// Calculate delta from last position
|
||||||
const maxOffsetX = Math.floor((store.cellSize.width - currentSprite.width) / 2);
|
const deltaX = e.clientX - canvasDragStart.value.x;
|
||||||
const maxOffsetY = Math.floor((store.cellSize.height - currentSprite.height) / 2);
|
const deltaY = e.clientY - canvasDragStart.value.y;
|
||||||
|
|
||||||
// Constrain movement to stay within cell boundaries, preventing negative offsets
|
// Only move when delta exceeds the threshold for one pixel movement at current zoom
|
||||||
const constrainedX = Math.max(0, Math.min(maxOffsetX, newX));
|
const pixelThreshold = previewZoom.value; // One pixel at current zoom level
|
||||||
const constrainedY = Math.max(0, Math.min(maxOffsetY, newY));
|
|
||||||
|
|
||||||
// Update both the current offset and the frame-specific offset
|
// Get the frame-specific offset
|
||||||
frameOffset.x = constrainedX;
|
const frameOffset = store.getSpriteOffset(currentFrame.value);
|
||||||
frameOffset.y = constrainedY;
|
|
||||||
store.currentSpriteOffset.x = constrainedX;
|
|
||||||
store.currentSpriteOffset.y = constrainedY;
|
|
||||||
|
|
||||||
// Reset drag start position
|
// Calculate the maximum allowed offset
|
||||||
canvasDragStart.value = {
|
const maxOffsetX = Math.max(0, store.cellSize.width - currentSprite.width);
|
||||||
x: e.clientX,
|
const maxOffsetY = Math.max(0, store.cellSize.height - currentSprite.height);
|
||||||
y: e.clientY,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the frame with the new offset
|
// Move one pixel at a time when threshold is reached
|
||||||
|
if (Math.abs(deltaX) >= pixelThreshold) {
|
||||||
|
const pixelsToMove = Math.sign(deltaX);
|
||||||
|
const newX = frameOffset.x + pixelsToMove;
|
||||||
|
frameOffset.x = Math.max(0, Math.min(maxOffsetX, newX));
|
||||||
|
|
||||||
|
// Reset the start X position for next pixel move
|
||||||
|
canvasDragStart.value.x = e.clientX;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(deltaY) >= pixelThreshold) {
|
||||||
|
const pixelsToMove = Math.sign(deltaY);
|
||||||
|
const newY = frameOffset.y + pixelsToMove;
|
||||||
|
frameOffset.y = Math.max(0, Math.min(maxOffsetY, newY));
|
||||||
|
|
||||||
|
// Reset the start Y position for next pixel move
|
||||||
|
canvasDragStart.value.y = e.clientY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the current offset to match
|
||||||
|
store.currentSpriteOffset.x = frameOffset.x;
|
||||||
|
store.currentSpriteOffset.y = frameOffset.y;
|
||||||
|
|
||||||
|
// Update the frame
|
||||||
updateFrame();
|
updateFrame();
|
||||||
|
|
||||||
// Re-render the main view to reflect the changes
|
// Re-render the main view
|
||||||
store.renderSpritesheetPreview();
|
store.renderSpritesheetPreview();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const stopCanvasDrag = () => {
|
const stopCanvasDrag = (e: MouseEvent) => {
|
||||||
|
if (!isCanvasDragging.value) return;
|
||||||
|
|
||||||
isCanvasDragging.value = false;
|
isCanvasDragging.value = false;
|
||||||
window.removeEventListener('mousemove', handleCanvasDrag);
|
window.removeEventListener('mousemove', handleCanvasDrag, { capture: true });
|
||||||
window.removeEventListener('mouseup', stopCanvasDrag);
|
window.removeEventListener('mouseup', stopCanvasDrag, { capture: true });
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Canvas viewport navigation functions
|
// Canvas viewport navigation functions
|
||||||
@ -692,19 +619,19 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
initializeCanvas();
|
// Initialize canvas with a slight delay to ensure DOM is ready
|
||||||
|
await nextTick();
|
||||||
|
await initializeCanvas();
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
window.addEventListener('keydown', handleKeyDown);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', handleKeyDown);
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
window.removeEventListener('mousemove', handleDrag);
|
|
||||||
window.removeEventListener('mouseup', stopDrag);
|
|
||||||
window.removeEventListener('mousemove', handleCanvasDrag);
|
window.removeEventListener('mousemove', handleCanvasDrag);
|
||||||
window.removeEventListener('mouseup', stopCanvasDrag);
|
window.removeEventListener('mouseup', stopCanvasDrag);
|
||||||
window.removeEventListener('mousemove', handleResize);
|
|
||||||
window.removeEventListener('mouseup', stopResize);
|
|
||||||
window.removeEventListener('mousemove', handleViewportDrag);
|
window.removeEventListener('mousemove', handleViewportDrag);
|
||||||
window.removeEventListener('mouseup', stopViewportDrag);
|
window.removeEventListener('mouseup', stopViewportDrag);
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Settings Modal -->
|
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
<template #header-icon>
|
||||||
<!-- Modal backdrop with semi-transparent background -->
|
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
|
||||||
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div class="bg-gray-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-auto shadow-lg pointer-events-auto relative">
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<i class="fas fa-cog text-blue-500"></i>
|
<i class="fas fa-cog text-blue-500"></i>
|
||||||
<span>Settings</span>
|
</template>
|
||||||
</div>
|
<template #header-title>Settings</template>
|
||||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Tools Section -->
|
<!-- Tools Section -->
|
||||||
@ -110,17 +99,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseModal>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
import BaseModal from './BaseModal.vue';
|
||||||
|
|
||||||
const store = useSpritesheetStore();
|
const store = useSpritesheetStore();
|
||||||
const sprites = computed(() => store.sprites.value);
|
const sprites = computed(() => store.sprites.value);
|
||||||
const isModalOpen = computed(() => store.isSettingsModalOpen.value);
|
const isModalOpen = computed({
|
||||||
|
get: () => store.isSettingsModalOpen.value,
|
||||||
|
set: value => {
|
||||||
|
store.isSettingsModalOpen.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
const previewBorder = computed(() => store.previewBorder);
|
const previewBorder = computed(() => store.previewBorder);
|
||||||
|
|
||||||
// Column count control
|
// Column count control
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<!-- Sprites Modal -->
|
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
<template #header-icon>
|
||||||
<!-- Modal backdrop with semi-transparent background -->
|
|
||||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
|
||||||
|
|
||||||
<!-- Modal content -->
|
|
||||||
<div class="bg-gray-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-auto shadow-lg pointer-events-auto relative">
|
|
||||||
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
|
|
||||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
|
||||||
<i class="fas fa-images text-blue-500"></i>
|
<i class="fas fa-images text-blue-500"></i>
|
||||||
<span>Sprites</span>
|
</template>
|
||||||
</div>
|
<template #header-title>Sprites</template>
|
||||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Sprites List -->
|
<!-- Sprites List -->
|
||||||
@ -43,17 +32,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</BaseModal>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||||
|
import BaseModal from './BaseModal.vue';
|
||||||
|
|
||||||
const store = useSpritesheetStore();
|
const store = useSpritesheetStore();
|
||||||
const sprites = computed(() => store.sprites.value);
|
const sprites = computed(() => store.sprites.value);
|
||||||
const isModalOpen = computed(() => store.isSpritesModalOpen.value);
|
const isModalOpen = computed({
|
||||||
|
get: () => store.isSpritesModalOpen.value,
|
||||||
|
set: value => {
|
||||||
|
store.isSpritesModalOpen.value = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
store.isSpritesModalOpen.value = false;
|
store.isSpritesModalOpen.value = false;
|
||||||
|
@ -527,8 +527,9 @@ export function useSpritesheetStore() {
|
|||||||
const originalOffsetY = Math.round(currentSprite.y - cellY * cellSize.height);
|
const originalOffsetY = Math.round(currentSprite.y - cellY * cellSize.height);
|
||||||
|
|
||||||
// Calculate precise offset for pixel-perfect rendering, including the user's drag offset
|
// Calculate precise offset for pixel-perfect rendering, including the user's drag offset
|
||||||
const offsetX = originalOffsetX + spriteOffset.x;
|
// Use the spriteOffset directly as the position within the cell
|
||||||
const offsetY = originalOffsetY + spriteOffset.y;
|
const offsetX = spriteOffset.x;
|
||||||
|
const offsetY = spriteOffset.y;
|
||||||
|
|
||||||
// Draw the current sprite at full opacity at the new position
|
// Draw the current sprite at full opacity at the new position
|
||||||
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
|
animation.ctx.drawImage(currentSprite.img, offsetX, offsetY);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user