150 lines
4.8 KiB
Vue
150 lines
4.8 KiB
Vue
<template>
|
|
<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 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>
|
|
|
|
<sprite-canvas :sprites="sprites" :columns="columns" @update-sprite="updateSpritePosition" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import FileUploader from './components/FileUploader.vue';
|
|
import SpriteCanvas from './components/SpriteCanvas.vue';
|
|
import Modal from './components/utilities/Modal.vue';
|
|
import SpritePreview from './components/SpritePreview.vue';
|
|
|
|
interface Sprite {
|
|
id: string;
|
|
file: File;
|
|
img: HTMLImageElement;
|
|
url: string;
|
|
width: number;
|
|
height: number;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
const sprites = ref<Sprite[]>([]);
|
|
const columns = ref(4);
|
|
const isPreviewModalOpen = ref(false);
|
|
|
|
const handleSpritesUpload = (files: File[]) => {
|
|
Promise.all(
|
|
files.map(file => {
|
|
return new Promise<Sprite>(resolve => {
|
|
const url = URL.createObjectURL(file);
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
resolve({
|
|
id: crypto.randomUUID(),
|
|
file,
|
|
img,
|
|
url,
|
|
width: img.width,
|
|
height: img.height,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
};
|
|
img.src = url;
|
|
});
|
|
})
|
|
).then(newSprites => {
|
|
sprites.value = [...sprites.value, ...newSprites];
|
|
});
|
|
};
|
|
|
|
const updateSpritePosition = (id: string, x: number, y: number) => {
|
|
const spriteIndex = sprites.value.findIndex(sprite => sprite.id === id);
|
|
if (spriteIndex !== -1) {
|
|
sprites.value[spriteIndex].x = x;
|
|
sprites.value[spriteIndex].y = y;
|
|
}
|
|
};
|
|
|
|
const downloadSpritesheet = () => {
|
|
if (sprites.value.length === 0) return;
|
|
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
if (!ctx) return;
|
|
|
|
// Find max dimensions
|
|
let maxWidth = 0;
|
|
let maxHeight = 0;
|
|
|
|
sprites.value.forEach(sprite => {
|
|
if (sprite.width > maxWidth) maxWidth = sprite.width;
|
|
if (sprite.height > maxHeight) maxHeight = sprite.height;
|
|
});
|
|
|
|
// Set canvas size
|
|
const rows = Math.ceil(sprites.value.length / columns.value);
|
|
canvas.width = maxWidth * columns.value;
|
|
canvas.height = maxHeight * rows;
|
|
|
|
// Apply pixel art optimization for the export canvas
|
|
ctx.imageSmoothingEnabled = false;
|
|
|
|
// Draw sprites
|
|
sprites.value.forEach((sprite, index) => {
|
|
const col = index % columns.value;
|
|
const row = Math.floor(index / columns.value);
|
|
|
|
const cellX = col * maxWidth;
|
|
const cellY = row * maxHeight;
|
|
|
|
ctx.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
|
|
});
|
|
|
|
// Create download link
|
|
const link = document.createElement('a');
|
|
link.download = 'spritesheet.png';
|
|
link.href = canvas.toDataURL('image/png');
|
|
link.click();
|
|
};
|
|
|
|
// Preview modal control
|
|
const openPreviewModal = () => {
|
|
console.log('Opening preview modal');
|
|
if (sprites.value.length === 0) {
|
|
console.log('No sprites to preview');
|
|
return;
|
|
}
|
|
isPreviewModalOpen.value = true;
|
|
};
|
|
|
|
const closePreviewModal = () => {
|
|
isPreviewModalOpen.value = false;
|
|
};
|
|
</script>
|