Improvements
This commit is contained in:
parent
66d1cf94b4
commit
2bbe6b0ff0
39
src/App.vue
39
src/App.vue
@ -1,26 +1,35 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gray-100 p-4">
|
||||
<h1 class="text-2xl font-bold text-center mb-6">Spritesheet Generator</h1>
|
||||
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow p-6">
|
||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||
<div class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 p-6">
|
||||
<div class="max-w-6xl mx-auto">
|
||||
<h1 class="text-4xl font-bold text-center mb-8 text-gray-900 tracking-tight">Spritesheet Generator</h1>
|
||||
|
||||
<div v-if="sprites.length > 0">
|
||||
<div class="mt-4 flex items-center gap-4">
|
||||
<div class="flex items-center">
|
||||
<label for="columns" class="mr-2">Columns:</label>
|
||||
<input id="columns" type="number" v-model="columns" min="1" max="10" class="border rounded px-2 py-1 w-16" />
|
||||
<div class="bg-white rounded-2xl shadow-soft p-8">
|
||||
<file-uploader @upload-sprites="handleSpritesUpload" />
|
||||
|
||||
<div v-if="sprites.length > 0" class="mt-8">
|
||||
<div class="flex flex-wrap items-center gap-6 mb-8">
|
||||
<div class="flex items-center space-x-3">
|
||||
<label for="columns" class="text-gray-700 font-medium">Columns:</label>
|
||||
<input id="columns" type="number" v-model="columns" min="1" max="10" class="w-20 px-3 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all" />
|
||||
</div>
|
||||
|
||||
<button @click="downloadSpritesheet" class="px-6 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
|
||||
<i class="fas fa-download"></i>
|
||||
<span>Download Spritesheet</span>
|
||||
</button>
|
||||
|
||||
<button @click="openPreviewModal" class="px-6 py-2.5 bg-green-500 hover:bg-green-600 text-white font-medium rounded-lg transition-colors flex items-center space-x-2">
|
||||
<i class="fas fa-play"></i>
|
||||
<span>Preview Animation</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button @click="downloadSpritesheet" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded">Download Spritesheet</button>
|
||||
|
||||
<button @click="openPreviewModal" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded">Preview</button>
|
||||
<sprite-canvas :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" />
|
||||
</div>
|
||||
|
||||
<sprite-canvas :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" :initial-width="800" :initial-height="600" title="Preview">
|
||||
<Modal :is-open="isPreviewModalOpen" @close="closePreviewModal" :initial-width="800" :initial-height="600" title="Animation Preview">
|
||||
<sprite-preview :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" />
|
||||
</Modal>
|
||||
</div>
|
||||
|
@ -1,15 +1,28 @@
|
||||
<template>
|
||||
<div class="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center" :class="{ 'bg-blue-50 border-blue-300': isDragging }" @dragenter.prevent="isDragging = true" @dragleave.prevent="isDragging = false" @dragover.prevent @drop.prevent="handleDrop">
|
||||
<div
|
||||
class="border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200"
|
||||
:class="{
|
||||
'border-blue-300 bg-blue-50': isDragging,
|
||||
'border-gray-200 hover:border-blue-300 hover:bg-gray-50': !isDragging,
|
||||
}"
|
||||
@dragenter.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@dragover.prevent
|
||||
@drop.prevent="handleDrop"
|
||||
>
|
||||
<input ref="fileInput" type="file" multiple accept="image/*" class="hidden" @change="handleFileChange" />
|
||||
|
||||
<div class="mb-4">
|
||||
<img src="@/assets/images/file.svg" alt="File upload" class="w-16 h-16 mx-auto mb-4" />
|
||||
<div class="mb-6">
|
||||
<img src="@/assets/images/file.svg" alt="File upload" class="w-20 h-20 mx-auto mb-4 opacity-75" />
|
||||
</div>
|
||||
|
||||
<p class="text-lg mb-2">Drag and drop your sprite images here</p>
|
||||
<p class="text-sm text-gray-500 mb-4">or</p>
|
||||
<p class="text-xl font-medium text-gray-700 mb-2">Drag and drop your sprite images here</p>
|
||||
<p class="text-sm text-gray-500 mb-6">or</p>
|
||||
|
||||
<button @click="openFileDialog" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Select Files</button>
|
||||
<button @click="openFileDialog" class="px-6 py-2.5 bg-blue-500 hover:bg-blue-600 text-white font-medium rounded-lg transition-colors inline-flex items-center space-x-2">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<span>Select Files</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-white border-gray-300 border-2 rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col">
|
||||
<!-- Header -->
|
||||
<div class="flex justify-between items-center p-4 border-b border-b-gray-400">
|
||||
<h3 class="text-xl font-semibold">{{ title }}</h3>
|
||||
<button @click="close" class="text-gray-500 hover:text-gray-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<div
|
||||
ref="modalRef"
|
||||
:style="{
|
||||
position: 'absolute',
|
||||
left: `${position.x}px`,
|
||||
top: `${position.y}px`,
|
||||
}"
|
||||
class="bg-white rounded-2xl border-2 border-gray-300 shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col"
|
||||
>
|
||||
<!-- Header - Make it the drag handle -->
|
||||
<div class="flex justify-between items-center p-6 border-b border-gray-100 cursor-move" @mousedown="startDrag" @touchstart="startDrag">
|
||||
<h3 class="text-2xl font-semibold text-gray-900">{{ title }}</h3>
|
||||
<button @click="close" class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="p-4 flex-1 overflow-auto">
|
||||
<div class="p-6 flex-1 overflow-auto">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
@ -23,7 +30,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
isOpen: boolean;
|
||||
@ -34,8 +41,78 @@
|
||||
(e: 'close'): void;
|
||||
}>();
|
||||
|
||||
const modalRef = ref<HTMLElement | null>(null);
|
||||
const position = ref({ x: 0, y: 0 });
|
||||
const isDragging = ref(false);
|
||||
const dragOffset = ref({ x: 0, y: 0 });
|
||||
|
||||
// Center the modal when it opens
|
||||
const centerModal = () => {
|
||||
if (!modalRef.value) return;
|
||||
|
||||
const modalRect = modalRef.value.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
position.value = {
|
||||
x: (viewportWidth - modalRect.width) / 2,
|
||||
y: (viewportHeight - modalRect.height) / 2,
|
||||
};
|
||||
};
|
||||
|
||||
const startDrag = (event: MouseEvent | TouchEvent) => {
|
||||
if (!modalRef.value) return;
|
||||
event.preventDefault();
|
||||
|
||||
isDragging.value = true;
|
||||
|
||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
dragOffset.value = {
|
||||
x: clientX - position.value.x,
|
||||
y: clientY - position.value.y,
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('touchmove', drag, { passive: false });
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
document.addEventListener('touchend', stopDrag);
|
||||
};
|
||||
|
||||
const drag = (event: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging.value || !modalRef.value) return;
|
||||
event.preventDefault();
|
||||
|
||||
const clientX = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||
const clientY = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
const modalRect = modalRef.value.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
// Calculate new position
|
||||
let newX = clientX - dragOffset.value.x;
|
||||
let newY = clientY - dragOffset.value.y;
|
||||
|
||||
// Constrain to viewport bounds
|
||||
newX = Math.max(0, Math.min(newX, viewportWidth - modalRect.width));
|
||||
newY = Math.max(0, Math.min(newY, viewportHeight - modalRect.height));
|
||||
|
||||
position.value = { x: newX, y: newY };
|
||||
};
|
||||
|
||||
const stopDrag = () => {
|
||||
isDragging.value = false;
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('touchmove', drag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
document.removeEventListener('touchend', stopDrag);
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
emit('close');
|
||||
position.value = { x: 0, y: 0 };
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
@ -45,11 +122,40 @@
|
||||
}
|
||||
};
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
if (!isDragging.value) {
|
||||
centerModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for modal opening
|
||||
watch(
|
||||
() => props.isOpen,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
// Use nextTick to ensure the modal is mounted
|
||||
Vue.nextTick(() => {
|
||||
centerModal();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('resize', handleResize);
|
||||
if (props.isOpen) {
|
||||
centerModal();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('resize', handleResize);
|
||||
document.removeEventListener('mousemove', drag);
|
||||
document.removeEventListener('touchmove', drag);
|
||||
document.removeEventListener('mouseup', stopDrag);
|
||||
document.removeEventListener('touchend', stopDrag);
|
||||
});
|
||||
</script>
|
||||
|
@ -1,29 +1,33 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
gray: {
|
||||
900: '#121212', // bg-primary
|
||||
800: '#1e1e1e', // bg-secondary
|
||||
700: '#252525', // bg-tertiary
|
||||
600: '#333333', // border
|
||||
400: '#a0a0a0', // text-secondary
|
||||
200: '#e0e0e0', // text-primary
|
||||
900: '#111827', // bg-primary
|
||||
800: '#1F2937', // bg-secondary
|
||||
700: '#374151', // bg-tertiary
|
||||
600: '#4B5563', // border
|
||||
400: '#9CA3AF', // text-secondary
|
||||
200: '#E5E7EB', // text-primary
|
||||
},
|
||||
blue: {
|
||||
500: '#0096ff', // accent
|
||||
600: '#0077cc', // accent-hover
|
||||
500: '#3B82F6', // accent
|
||||
600: '#2563EB', // accent-hover
|
||||
},
|
||||
red: {
|
||||
500: '#e53935', // danger
|
||||
600: '#c62828', // danger-hover
|
||||
500: '#EF4444', // danger
|
||||
600: '#DC2626', // danger-hover
|
||||
},
|
||||
green: {
|
||||
500: '#43a047', // success
|
||||
500: '#10B981', // success
|
||||
600: '#059669', // success-hover
|
||||
}
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user