Base model
This commit is contained in:
parent
100bacc471
commit
a23b1c8463
219
src/components/BaseModal.vue
Normal file
219
src/components/BaseModal.vue
Normal file
@ -0,0 +1,219 @@
|
||||
<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 overflow-auto shadow-lg pointer-events-auto relative" :class="[{ 'border border-gray-400': showBorder }, { 'max-w-2xl w-full max-h-[90vh]': !movable }, customClass]" :style="modalStyle" ref="modalRef">
|
||||
<!-- Modal header -->
|
||||
<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 content -->
|
||||
<div class="p-6">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- Resize handle (only if resizable is true) -->
|
||||
<div v-if="resizable" class="absolute bottom-0 right-0 w-4 h-4 cursor-se-resize" @mousedown="startResize">
|
||||
<i class="fas fa-grip-lines-diagonal text-gray-500 text-xs absolute bottom-1 right-1"></i>
|
||||
</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,91 +1,85 @@
|
||||
<template>
|
||||
<!-- Help Modal -->
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
||||
<!-- Modal backdrop with semi-transparent background -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
||||
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||
<template #header-icon>
|
||||
<i class="fas fa-question-circle text-blue-500"></i>
|
||||
</template>
|
||||
<template #header-title>Help</template>
|
||||
|
||||
<!-- 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>
|
||||
<span>Help</span>
|
||||
<div class="p-6">
|
||||
<!-- Help content tabs -->
|
||||
<div class="mb-6">
|
||||
<div class="flex border-b border-gray-600 mb-4">
|
||||
<button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" class="px-4 py-2 font-medium text-sm transition-colors" :class="activeTab === tab.id ? 'text-blue-500 border-b-2 border-blue-500' : 'text-gray-400 hover:text-gray-200'">
|
||||
<i :class="tab.icon" class="mr-2"></i>{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Help content tabs -->
|
||||
<div class="mb-6">
|
||||
<div class="flex border-b border-gray-600 mb-4">
|
||||
<button v-for="tab in tabs" :key="tab.id" @click="activeTab = tab.id" class="px-4 py-2 font-medium text-sm transition-colors" :class="activeTab === tab.id ? 'text-blue-500 border-b-2 border-blue-500' : 'text-gray-400 hover:text-gray-200'">
|
||||
<i :class="tab.icon" class="mr-2"></i>{{ tab.label }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts Tab -->
|
||||
<div v-if="activeTab === 'shortcuts'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Keyboard Shortcuts</h3>
|
||||
<div class="bg-gray-700 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="(shortcut, index) in shortcuts" :key="index" class="flex justify-between">
|
||||
<span class="font-medium">{{ shortcut.key }}</span>
|
||||
<span class="text-gray-300">{{ shortcut.description }}</span>
|
||||
</div>
|
||||
<!-- Keyboard Shortcuts Tab -->
|
||||
<div v-if="activeTab === 'shortcuts'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Keyboard Shortcuts</h3>
|
||||
<div class="bg-gray-700 rounded-lg p-4">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<div v-for="(shortcut, index) in shortcuts" :key="index" class="flex justify-between">
|
||||
<span class="font-medium">{{ shortcut.key }}</span>
|
||||
<span class="text-gray-300">{{ shortcut.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Guide Tab -->
|
||||
<div v-if="activeTab === 'guide'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Usage Guide</h3>
|
||||
<div class="bg-gray-700 rounded-lg p-4 space-y-3">
|
||||
<p>This tool helps you create spritesheets from individual sprite images.</p>
|
||||
<ol class="list-decimal pl-5 space-y-2">
|
||||
<li>Upload your sprite images using the upload area</li>
|
||||
<li>Arrange sprites by dragging them to desired positions</li>
|
||||
<li>Adjust settings like column count in the Settings panel</li>
|
||||
<li>Preview animation by clicking the Play button</li>
|
||||
<li>Download your spritesheet when ready</li>
|
||||
</ol>
|
||||
<p class="bg-blue-600 p-2 rounded">Questions? Add me on discord: <b>nu11ed</b></p>
|
||||
</div>
|
||||
<!-- Usage Guide Tab -->
|
||||
<div v-if="activeTab === 'guide'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Usage Guide</h3>
|
||||
<div class="bg-gray-700 rounded-lg p-4 space-y-3">
|
||||
<p>This tool helps you create spritesheets from individual sprite images.</p>
|
||||
<ol class="list-decimal pl-5 space-y-2">
|
||||
<li>Upload your sprite images using the upload area</li>
|
||||
<li>Arrange sprites by dragging them to desired positions</li>
|
||||
<li>Adjust settings like column count in the Settings panel</li>
|
||||
<li>Preview animation by clicking the Play button</li>
|
||||
<li>Download your spritesheet when ready</li>
|
||||
</ol>
|
||||
<p class="bg-blue-600 p-2 rounded">Questions? Add me on discord: <b>nu11ed</b></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Donation Tab -->
|
||||
<div v-if="activeTab === 'donate'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Buy me a coffee</h3>
|
||||
<p class="text-gray-300 mb-4">If you find this tool useful, please consider supporting its development with a donation.</p>
|
||||
<!-- Donation Tab -->
|
||||
<div v-if="activeTab === 'donate'" class="space-y-4">
|
||||
<h3 class="text-lg font-semibold mb-2">Buy me a coffee</h3>
|
||||
<p class="text-gray-300 mb-4">If you find this tool useful, please consider supporting its development with a donation.</p>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-for="wallet in wallets" :key="wallet.type" class="bg-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<i :class="wallet.icon" class="text-xl mr-2 bg-gray-800 p-1 px-2 rounded-lg" :style="{ color: wallet.color }"></i>
|
||||
<span class="font-medium">{{ wallet.name }}</span>
|
||||
</div>
|
||||
<button @click="copyToClipboard(wallet.address)" class="text-xs bg-gray-600 hover:bg-gray-500 px-2 py-1 rounded transition-colors">Copy</button>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-2 rounded text-xs font-mono break-all">
|
||||
{{ wallet.address }}
|
||||
<div class="space-y-4">
|
||||
<div v-for="wallet in wallets" :key="wallet.type" class="bg-gray-700 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center">
|
||||
<i :class="wallet.icon" class="text-xl mr-2 bg-gray-800 p-1 px-2 rounded-lg" :style="{ color: wallet.color }"></i>
|
||||
<span class="font-medium">{{ wallet.name }}</span>
|
||||
</div>
|
||||
<button @click="copyToClipboard(wallet.address)" class="text-xs bg-gray-600 hover:bg-gray-500 px-2 py-1 rounded transition-colors">Copy</button>
|
||||
</div>
|
||||
<div class="bg-gray-800 p-2 rounded text-xs font-mono break-all">
|
||||
{{ wallet.address }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
|
||||
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 tabs = [
|
||||
|
@ -1,169 +1,149 @@
|
||||
<template>
|
||||
<!-- Make the outer container always pointer-events-none so clicks pass through -->
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none">
|
||||
<!-- 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>
|
||||
<span>Animation Preview</span>
|
||||
</div>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-gray-200 text-xl">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true" :initialSize="modalSize" customClass="scrollbar-hide">
|
||||
<template #header-icon>
|
||||
<i class="fas fa-film text-blue-500"></i>
|
||||
</template>
|
||||
<template #header-title>Animation Preview</template>
|
||||
|
||||
<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 gap-2">
|
||||
<button
|
||||
@click="startAnimation"
|
||||
:disabled="sprites.length === 0"
|
||||
:class="{
|
||||
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': !animation.isPlaying,
|
||||
}"
|
||||
class="flex items-center gap-2 border rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
<i class="fas fa-play"></i> Play
|
||||
</button>
|
||||
<button
|
||||
@click="stopAnimation"
|
||||
:disabled="sprites.length === 0"
|
||||
:class="{
|
||||
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': animation.isPlaying,
|
||||
}"
|
||||
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
||||
>
|
||||
<i class="fas fa-pause"></i> Pause
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-grow">
|
||||
<div class="flex justify-between text-sm text-gray-400">
|
||||
<label for="frame-slider">Frame:</label>
|
||||
<span>{{ currentFrameDisplay }}</span>
|
||||
</div>
|
||||
<input type="range" id="frame-slider" v-model="currentFrame" :min="0" :max="Math.max(0, sprites.length - 1)" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-grow">
|
||||
<div class="flex justify-between text-sm text-gray-400">
|
||||
<label for="framerate">Frame Rate:</label>
|
||||
<span>{{ animation.frameRate }} FPS</span>
|
||||
</div>
|
||||
<input type="range" id="framerate" v-model.number="animation.frameRate" min="1" max="30" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameRateChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<!-- Zoom controls -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="zoomOut" :disabled="previewZoom <= 1" 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-minus"></i>
|
||||
</button>
|
||||
<span class="text-sm text-gray-300">{{ Math.round(previewZoom * 100) }}%</span>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Show all sprites toggle -->
|
||||
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" v-model="showAllSprites" class="form-checkbox h-4 w-4 text-blue-500 rounded border-gray-600 bg-gray-700 focus:ring-blue-500" />
|
||||
Show all frames
|
||||
</label>
|
||||
|
||||
<!-- Reset sprite position 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="Reset sprite to original position"
|
||||
>
|
||||
<i class="fas fa-crosshairs"></i>
|
||||
Reset Position
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<div class="text-xs text-gray-400 mb-2" v-if="hasSpriteOffset || sprites.length > 0">
|
||||
<span v-if="hasSpriteOffset">Sprite offset: {{ Math.round(spriteOffset.x) }}px, {{ Math.round(spriteOffset.y) }}px</span>
|
||||
<span v-else>Click and drag the sprite to move it within the cell</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="canvas-container relative transition-transform duration-100 flex items-center justify-center"
|
||||
:style="{
|
||||
minWidth: `${store.cellSize.width * previewZoom}px`,
|
||||
minHeight: `${store.cellSize.height * previewZoom}px`,
|
||||
cursor: previewZoom > 1 ? (isViewportDragging ? 'grabbing' : 'grab') : 'default',
|
||||
<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 gap-2">
|
||||
<button
|
||||
@click="startAnimation"
|
||||
:disabled="sprites.length === 0"
|
||||
:class="{
|
||||
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': !animation.isPlaying,
|
||||
}"
|
||||
@mousedown="startViewportDrag"
|
||||
@wheel="handleCanvasWheel"
|
||||
class="flex items-center gap-2 border rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
<div
|
||||
class="sprite-wrapper"
|
||||
:style="{
|
||||
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
|
||||
cursor: isCanvasDragging ? 'grabbing' : 'move',
|
||||
}"
|
||||
@mousedown.stop="startCanvasDrag"
|
||||
title="Drag to move sprite within cell"
|
||||
>
|
||||
<canvas ref="animCanvas" class="block pixel-art"></canvas>
|
||||
</div>
|
||||
<i class="fas fa-play"></i> Play
|
||||
</button>
|
||||
<button
|
||||
@click="stopAnimation"
|
||||
:disabled="sprites.length === 0"
|
||||
:class="{
|
||||
'bg-blue-500 border-blue-500 hover:bg-blue-600 hover:border-blue-600': animation.isPlaying,
|
||||
}"
|
||||
class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500"
|
||||
>
|
||||
<i class="fas fa-pause"></i> Pause
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-grow">
|
||||
<div class="flex justify-between text-sm text-gray-400">
|
||||
<label for="frame-slider">Frame:</label>
|
||||
<span>{{ currentFrameDisplay }}</span>
|
||||
</div>
|
||||
<input type="range" id="frame-slider" v-model="currentFrame" :min="0" :max="Math.max(0, sprites.length - 1)" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameChange" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-grow">
|
||||
<div class="flex justify-between text-sm text-gray-400">
|
||||
<label for="framerate">Frame Rate:</label>
|
||||
<span>{{ animation.frameRate }} FPS</span>
|
||||
</div>
|
||||
<input type="range" id="framerate" v-model.number="animation.frameRate" min="1" max="30" class="w-full h-1 bg-gray-600 rounded appearance-none focus:outline-none cursor-pointer" @input="handleFrameRateChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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 class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<!-- Zoom controls -->
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="zoomOut" :disabled="previewZoom <= 1" 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-minus"></i>
|
||||
</button>
|
||||
<span class="text-sm text-gray-300">{{ Math.round(previewZoom * 100) }}%</span>
|
||||
<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>
|
||||
</button>
|
||||
<button
|
||||
@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>
|
||||
</div>
|
||||
|
||||
<!-- Controls -->
|
||||
<div class="flex items-center gap-4">
|
||||
<!-- Show all sprites toggle -->
|
||||
<label class="flex items-center gap-2 text-sm text-gray-300 cursor-pointer">
|
||||
<input type="checkbox" v-model="showAllSprites" class="form-checkbox h-4 w-4 text-blue-500 rounded border-gray-600 bg-gray-700 focus:ring-blue-500" />
|
||||
Show all frames
|
||||
</label>
|
||||
|
||||
<!-- Reset sprite position 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="Reset sprite to original position"
|
||||
>
|
||||
<i class="fas fa-crosshairs"></i>
|
||||
Reset Position
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 -->
|
||||
<div class="text-xs text-gray-400 mb-2" v-if="hasSpriteOffset || sprites.length > 0">
|
||||
<span v-if="hasSpriteOffset">Sprite offset: {{ Math.round(spriteOffset.x) }}px, {{ Math.round(spriteOffset.y) }}px</span>
|
||||
<span v-else>Click and drag the sprite to move it within the cell</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="canvas-container relative transition-transform duration-100 flex items-center justify-center"
|
||||
:style="{
|
||||
minWidth: `${store.cellSize.width * previewZoom}px`,
|
||||
minHeight: `${store.cellSize.height * previewZoom}px`,
|
||||
cursor: previewZoom > 1 ? (isViewportDragging ? 'grabbing' : 'grab') : 'default',
|
||||
}"
|
||||
@mousedown="startViewportDrag"
|
||||
@wheel="handleCanvasWheel"
|
||||
>
|
||||
<div
|
||||
class="sprite-wrapper"
|
||||
:style="{
|
||||
transform: `scale(${previewZoom}) translate(${viewportOffset.x}px, ${viewportOffset.y}px)`,
|
||||
cursor: isCanvasDragging ? 'grabbing' : 'move',
|
||||
}"
|
||||
@mousedown.stop="startCanvasDrag"
|
||||
title="Drag to move sprite within cell"
|
||||
>
|
||||
<canvas ref="animCanvas" class="block pixel-art"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
|
||||
const store = useSpritesheetStore();
|
||||
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 animation = computed(() => store.animation);
|
||||
const previewBorder = computed(() => store.previewBorder);
|
||||
|
||||
const currentFrame = ref(0);
|
||||
const position = ref({ x: 0, y: 0 });
|
||||
const isDragging = ref(false);
|
||||
const dragOffset = ref({ x: 0, y: 0 });
|
||||
// Position is now handled by BaseModal
|
||||
|
||||
// New state for added features
|
||||
const previewZoom = ref(1);
|
||||
@ -197,11 +177,8 @@
|
||||
const isViewportDragging = ref(false);
|
||||
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 isResizing = ref(false);
|
||||
const initialSize = ref({ width: 0, height: 0 });
|
||||
const resizeStart = ref({ x: 0, y: 0 });
|
||||
|
||||
const currentFrameDisplay = computed(() => {
|
||||
if (sprites.value.length === 0) return '0 / 0';
|
||||
@ -395,77 +372,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Modal drag functionality
|
||||
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);
|
||||
};
|
||||
// Modal drag and resize functionality is now handled by BaseModal
|
||||
|
||||
const initializeCanvas = async () => {
|
||||
if (!animCanvas.value) {
|
||||
@ -699,12 +606,8 @@
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('mousemove', handleDrag);
|
||||
window.removeEventListener('mouseup', stopDrag);
|
||||
window.removeEventListener('mousemove', handleCanvasDrag);
|
||||
window.removeEventListener('mouseup', stopCanvasDrag);
|
||||
window.removeEventListener('mousemove', handleResize);
|
||||
window.removeEventListener('mouseup', stopResize);
|
||||
window.removeEventListener('mousemove', handleViewportDrag);
|
||||
window.removeEventListener('mouseup', stopViewportDrag);
|
||||
});
|
||||
|
@ -1,126 +1,120 @@
|
||||
<template>
|
||||
<!-- Settings Modal -->
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
||||
<!-- Modal backdrop with semi-transparent background -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
||||
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||
<template #header-icon>
|
||||
<i class="fas fa-cog text-blue-500"></i>
|
||||
</template>
|
||||
<template #header-title>Settings</template>
|
||||
|
||||
<!-- 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>
|
||||
<span>Settings</span>
|
||||
<div class="p-6">
|
||||
<!-- Tools Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-tools text-blue-500"></i> Tools</h3>
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button @click="autoArrangeSprites" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||
<i class="fas fa-th"></i> Auto arrange
|
||||
</button>
|
||||
<button @click="openPreviewModal" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-blue-600 hover:border-blue-600">
|
||||
<i class="fas fa-play"></i> Preview animation
|
||||
</button>
|
||||
<button @click="downloadSpritesheet" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button @click="confirmClearAll" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-red-700 hover:border-red-700">
|
||||
<i class="fas fa-trash-alt"></i> Clear all
|
||||
</button>
|
||||
</div>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Tools Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-tools text-blue-500"></i> Tools</h3>
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button @click="autoArrangeSprites" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||
<i class="fas fa-th"></i> Auto arrange
|
||||
</button>
|
||||
<button @click="openPreviewModal" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-blue-600 hover:border-blue-600">
|
||||
<i class="fas fa-play"></i> Preview animation
|
||||
</button>
|
||||
<button @click="downloadSpritesheet" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:border-blue-500">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</button>
|
||||
<button @click="confirmClearAll" :disabled="sprites.length === 0" class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors disabled:opacity-60 disabled:cursor-not-allowed hover:bg-red-700 hover:border-red-700">
|
||||
<i class="fas fa-trash-alt"></i> Clear all
|
||||
</button>
|
||||
<!-- Layout Controls Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-th text-blue-500"></i> Layout controls</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-2">Column Count</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="range" min="1" max="10" v-model.number="columnCount" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
<span class="text-gray-200 font-medium w-8 text-center">{{ columnCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="applyColumnCount" class="flex items-center gap-2 bg-blue-500 text-white border border-blue-500 rounded px-4 py-2 text-sm transition-colors hover:bg-blue-600 hover:border-blue-600"><i class="fas fa-check"></i> Apply layout</button>
|
||||
</div>
|
||||
|
||||
<!-- Layout Controls Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-th text-blue-500"></i> Layout controls</h3>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm text-gray-400 mb-2">Column Count</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<input type="range" min="1" max="10" v-model.number="columnCount" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
<span class="text-gray-200 font-medium w-8 text-center">{{ columnCount }}</span>
|
||||
<!-- Zoom Controls Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-search text-blue-500"></i> Zoom controls</h3>
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button @click="zoomIn" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-search-plus"></i> Zoom in</button>
|
||||
<button @click="zoomOut" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-search-minus"></i> Zoom out</button>
|
||||
<button @click="resetZoom" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-undo"></i> Reset zoom</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Border Settings Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-border-style text-blue-500"></i> Preview border</h3>
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-sm text-gray-400">Enable border (preview only)</label>
|
||||
<div class="relative inline-block w-10 align-middle select-none">
|
||||
<input type="checkbox" v-model="previewBorder.enabled" id="toggle-border" class="sr-only" />
|
||||
<label for="toggle-border" class="block h-6 rounded-full bg-gray-600 cursor-pointer"></label>
|
||||
<div :class="{ 'translate-x-4': previewBorder.enabled, 'translate-x-0': !previewBorder.enabled }" class="absolute left-0 top-0 w-6 h-6 rounded-full bg-white border border-gray-300 transform transition-transform duration-200 ease-in-out"></div>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="applyColumnCount" class="flex items-center gap-2 bg-blue-500 text-white border border-blue-500 rounded px-4 py-2 text-sm transition-colors hover:bg-blue-600 hover:border-blue-600"><i class="fas fa-check"></i> Apply layout</button>
|
||||
</div>
|
||||
|
||||
<!-- Zoom Controls Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-search text-blue-500"></i> Zoom controls</h3>
|
||||
<div class="flex flex-wrap gap-3 mb-6">
|
||||
<button @click="zoomIn" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-search-plus"></i> Zoom in</button>
|
||||
<button @click="zoomOut" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-search-minus"></i> Zoom out</button>
|
||||
<button @click="resetZoom" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-undo"></i> Reset zoom</button>
|
||||
<div class="grid grid-cols-2 gap-4 mt-4" v-if="previewBorder.enabled">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Border color</label>
|
||||
<input type="color" v-model="previewBorder.color" class="w-full h-8 bg-gray-700 rounded cursor-pointer" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Border width: {{ previewBorder.width }}px</label>
|
||||
<input type="range" v-model.number="previewBorder.width" min="1" max="10" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-400 mt-2"><i class="fas fa-info-circle mr-1"></i> Border will only be visible in the preview and won't be included in the downloaded spritesheet.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Border Settings Section -->
|
||||
<div class="mb-8">
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-border-style text-blue-500"></i> Preview border</h3>
|
||||
<div class="mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-sm text-gray-400">Enable border (preview only)</label>
|
||||
<div class="relative inline-block w-10 align-middle select-none">
|
||||
<input type="checkbox" v-model="previewBorder.enabled" id="toggle-border" class="sr-only" />
|
||||
<label for="toggle-border" class="block h-6 rounded-full bg-gray-600 cursor-pointer"></label>
|
||||
<div :class="{ 'translate-x-4': previewBorder.enabled, 'translate-x-0': !previewBorder.enabled }" class="absolute left-0 top-0 w-6 h-6 rounded-full bg-white border border-gray-300 transform transition-transform duration-200 ease-in-out"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mt-4" v-if="previewBorder.enabled">
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Border color</label>
|
||||
<input type="color" v-model="previewBorder.color" class="w-full h-8 bg-gray-700 rounded cursor-pointer" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-400 mb-2">Border width: {{ previewBorder.width }}px</label>
|
||||
<input type="range" v-model.number="previewBorder.width" min="1" max="10" class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-400 mt-2"><i class="fas fa-info-circle mr-1"></i> Border will only be visible in the preview and won't be included in the downloaded spritesheet.</div>
|
||||
<!-- Keyboard Shortcuts Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-keyboard text-blue-500"></i> Keyboard shortcuts</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Shift + Drag</kbd>
|
||||
<span>Fine-tune position</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Keyboard Shortcuts Section -->
|
||||
<div>
|
||||
<h3 class="text-lg font-medium text-gray-200 mb-4 flex items-center gap-2"><i class="fas fa-keyboard text-blue-500"></i> Keyboard shortcuts</h3>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Shift + Drag</kbd>
|
||||
<span>Fine-tune position</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Space</kbd>
|
||||
<span>Play/Pause animation</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Esc</kbd>
|
||||
<span>Close preview</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">←/→</kbd>
|
||||
<span>Navigate frames</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Space</kbd>
|
||||
<span>Play/Pause animation</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">Esc</kbd>
|
||||
<span>Close preview</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-sm text-gray-400">
|
||||
<kbd class="bg-gray-700 border border-gray-600 rounded px-2 py-1 text-xs font-mono">←/→</kbd>
|
||||
<span>Navigate frames</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
|
||||
const store = useSpritesheetStore();
|
||||
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);
|
||||
|
||||
// Column count control
|
||||
|
@ -1,59 +1,53 @@
|
||||
<template>
|
||||
<!-- Sprites Modal -->
|
||||
<div class="fixed inset-0 flex items-center justify-center z-50 pointer-events-none" v-if="isModalOpen">
|
||||
<!-- Modal backdrop with semi-transparent background -->
|
||||
<div class="absolute inset-0 bg-black bg-opacity-50 pointer-events-auto" @click="closeModal"></div>
|
||||
<BaseModal v-model="isModalOpen" @close="closeModal" :movable="true" :resizable="true" :showBackdrop="false" :showBorder="true">
|
||||
<template #header-icon>
|
||||
<i class="fas fa-images text-blue-500"></i>
|
||||
</template>
|
||||
<template #header-title>Sprites</template>
|
||||
|
||||
<!-- 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>
|
||||
<span>Sprites</span>
|
||||
</div>
|
||||
<button @click="closeModal" class="text-gray-400 hover:text-white">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="p-6">
|
||||
<!-- Sprites List -->
|
||||
<div v-if="sprites.length === 0" class="text-center text-gray-400 py-8">
|
||||
<i class="fas fa-image text-4xl mb-4 opacity-30"></i>
|
||||
<p>No sprites uploaded yet</p>
|
||||
<button @click="showUploadSection" class="mt-4 flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors mx-auto hover:bg-blue-600 hover:border-blue-600"><i class="fas fa-upload"></i> Upload sprites</button>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<!-- Sprites List -->
|
||||
<div v-if="sprites.length === 0" class="text-center text-gray-400 py-8">
|
||||
<i class="fas fa-image text-4xl mb-4 opacity-30"></i>
|
||||
<p>No sprites uploaded yet</p>
|
||||
<button @click="showUploadSection" class="mt-4 flex items-center gap-2 bg-blue-500 border border-blue-500 text-white rounded px-4 py-2 text-sm transition-colors mx-auto hover:bg-blue-600 hover:border-blue-600"><i class="fas fa-upload"></i> Upload sprites</button>
|
||||
<div v-else>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-200 flex items-center gap-2"><i class="fas fa-images text-blue-500"></i> Uploaded Sprites</h3>
|
||||
<span class="text-sm text-gray-400">{{ sprites.length }} sprites</span>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-200 flex items-center gap-2"><i class="fas fa-images text-blue-500"></i> Uploaded Sprites</h3>
|
||||
<span class="text-sm text-gray-400">{{ sprites.length }} sprites</span>
|
||||
<div class="grid grid-cols-3 gap-4 max-h-96 overflow-y-auto p-2">
|
||||
<div v-for="(sprite, index) in sprites" :key="sprite.id" @click="handleSpriteClick(sprite.id)" class="border border-gray-600 rounded bg-gray-700 p-3 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<img :src="sprite.img.src" :alt="sprite.name" class="max-w-full max-h-20 mx-auto mb-2 bg-black bg-opacity-20 rounded" />
|
||||
<div class="text-xs text-gray-400 truncate">{{ index + 1 }}. {{ truncateName(sprite.name) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 max-h-96 overflow-y-auto p-2">
|
||||
<div v-for="(sprite, index) in sprites" :key="sprite.id" @click="handleSpriteClick(sprite.id)" class="border border-gray-600 rounded bg-gray-700 p-3 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md">
|
||||
<img :src="sprite.img.src" :alt="sprite.name" class="max-w-full max-h-20 mx-auto mb-2 bg-black bg-opacity-20 rounded" />
|
||||
<div class="text-xs text-gray-400 truncate">{{ index + 1 }}. {{ truncateName(sprite.name) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button @click="showUploadSection" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-upload"></i> Upload more</button>
|
||||
<button @click="confirmClearAll" class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors hover:bg-red-700 hover:border-red-700"><i class="fas fa-trash-alt"></i> Clear all</button>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button @click="showUploadSection" class="flex items-center gap-2 bg-gray-700 text-gray-200 border border-gray-600 rounded px-4 py-2 text-sm transition-colors hover:border-blue-500"><i class="fas fa-upload"></i> Upload more</button>
|
||||
<button @click="confirmClearAll" class="flex items-center gap-2 bg-red-600 border border-red-600 text-white rounded px-4 py-2 text-sm transition-colors hover:bg-red-700 hover:border-red-700"><i class="fas fa-trash-alt"></i> Clear all</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
|
||||
import BaseModal from './BaseModal.vue';
|
||||
|
||||
const store = useSpritesheetStore();
|
||||
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 = () => {
|
||||
store.isSpritesModalOpen.value = false;
|
||||
|
Loading…
x
Reference in New Issue
Block a user