Improvements

This commit is contained in:
Dennis Postma 2025-04-05 23:06:58 +02:00
parent d323355451
commit bc7af9dd8e
2 changed files with 140 additions and 51 deletions

View File

@ -1,52 +1,98 @@
<template> <template>
<div class="spritesheet-preview"> <div class="spritesheet-preview w-full">
<!-- Preview Canvas --> <!-- Controls Container -->
<div class="relative border border-gray-300 rounded-lg mb-4 overflow-hidden" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }"> <div class="bg-white rounded-lg border border-gray-200 p-4">
<canvas ref="previewCanvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="block" :class="{ 'cursor-move': isDraggable }"></canvas> <div class="flex flex-col gap-4 sm:flex-row sm:flex-wrap">
<!-- Playback Controls -->
<div class="flex items-center gap-2">
<div class="space-y-2">
<button @click="togglePlayback" class="flex items-center gap-1.5 bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-md transition-colors w-full">
<span v-if="isPlaying" class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M6.75 5.25a.75.75 0 01.75-.75H9a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H7.5a.75.75 0 01-.75-.75V5.25zm7.5 0A.75.75 0 0115 4.5h1.5a.75.75 0 01.75.75v13.5a.75.75 0 01-.75.75H15a.75.75 0 01-.75-.75V5.25z" clip-rule="evenodd" />
</svg>
<span class="hidden sm:inline">Pause</span>
</span>
<span v-else class="flex items-center gap-1.5">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5">
<path fill-rule="evenodd" d="M4.5 5.653c0-1.426 1.529-2.33 2.779-1.643l11.54 6.348c1.295.712 1.295 2.573 0 3.285L7.28 19.991c-1.25.687-2.779-.217-2.779-1.643V5.653z" clip-rule="evenodd" />
</svg>
<span class="hidden sm:inline">Play</span>
</span>
</button>
<div class="flex items-center gap-1">
<button @click="previousFrame" class="bg-gray-200 hover:bg-gray-300 p-2 rounded-md transition-colors duration-200 w-full" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 mx-auto">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 01.75.75v12.06l4.72-4.72a.75.75 0 111.06 1.06l-6 6a.75.75 0 01-1.06 0l-6-6a.75.75 0 011.06-1.06l4.72 4.72V5.97a.75.75 0 01.75-.75z" clip-rule="evenodd" transform="rotate(90 12 12)" />
</svg>
</button>
<button @click="nextFrame" class="bg-gray-200 hover:bg-gray-300 p-2 rounded-md transition-colors duration-200 w-full" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-5 h-5 mx-auto">
<path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 01.75.75v12.06l4.72-4.72a.75.75 0 111.06 1.06l-6 6a.75.75 0 01-1.06 0l-6-6a.75.75 0 011.06-1.06l4.72 4.72V5.97a.75.75 0 01.75-.75z" clip-rule="evenodd" transform="rotate(-90 12 12)" />
</svg>
</button>
</div>
<!-- Checkboxes -->
<div class="flex flex-wrap gap-4 items-center">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="isDraggable" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300" />
<span class="text-sm whitespace-nowrap">Reposition</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" v-model="showAllSprites" class="w-4 h-4 text-blue-500 focus:ring-blue-400 rounded border-gray-300" />
<span class="text-sm whitespace-nowrap">Show all sprites</span>
</label>
</div>
</div>
</div>
<!-- Frame Controls -->
<div class="flex-1 min-w-[200px] space-y-6">
<!-- Frame Navigation -->
<div class="flex items-center gap-2">
<span class="text-sm font-medium w-30">Frame {{ currentFrameIndex + 1 }}/{{ totalFrames }}</span>
<input type="range" min="0" :max="totalFrames - 1" v-model.number="currentFrameIndex" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500" :disabled="isPlaying" :class="{ 'opacity-50': isPlaying }" />
</div>
<!-- FPS Control -->
<div class="flex items-center gap-2">
<span class="text-sm font-medium w-30">FPS: {{ fps }}</span>
<input type="range" min="1" max="60" v-model.number="fps" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500" />
</div>
<!-- Zoom Control -->
<div class="flex items-center gap-2">
<span class="text-sm font-medium w-30">{{ Math.round(zoom * 100) }}%</span>
<input type="range" min="0.5" max="5" step="0.1" v-model.number="zoom" class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-blue-500" />
</div>
</div>
</div>
</div> </div>
<!-- Controls --> <div class="mt-3 relative bg-gray-50 border border-gray-300 rounded-lg mb-6 overflow-auto min-h-[520px] shadow-sm hover:shadow-md transition-shadow duration-200">
<div class="space-y-4"> <canvas
<!-- Playback Controls --> ref="previewCanvasRef"
<div class="flex items-center gap-4"> @mousedown="startDrag"
<button @click="togglePlayback" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"> @mousemove="drag"
{{ isPlaying ? 'Pause' : 'Play' }} @mouseup="stopDrag"
</button> @mouseleave="stopDrag"
@touchstart="handleTouchStart"
<div class="flex items-center gap-2"> @touchmove="handleTouchMove"
<button @click="previousFrame" class="bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">&lt;</button> @touchend="stopDrag"
<button @click="nextFrame" class="bg-gray-200 hover:bg-gray-300 px-3 py-1 rounded" :disabled="isPlaying" :class="{ 'opacity-50 cursor-not-allowed': isPlaying }">&gt;</button> class="block"
</div> :class="{ 'cursor-move': isDraggable }"
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }"
<div class="flex items-center"> >
<input id="sprite-position" type="checkbox" v-model="isDraggable" class="mr-2" /> </canvas>
<label for="sprite-position">Allow repositioning</label>
</div>
</div>
<!-- Frame Navigation Slider -->
<div class="flex items-center gap-2">
<label for="frame-slider" class="min-w-16">Frame {{ currentFrameIndex + 1 }}/{{ totalFrames }}</label>
<input id="frame-slider" type="range" min="0" :max="totalFrames - 1" v-model.number="currentFrameIndex" class="w-full" :disabled="isPlaying" :class="{ 'opacity-50': isPlaying }" />
</div>
<!-- FPS Slider -->
<div class="flex items-center gap-2">
<label for="fps-slider" class="min-w-16">FPS: {{ fps }}</label>
<input id="fps-slider" type="range" min="1" max="60" v-model.number="fps" class="w-full" />
</div>
<!-- Zoom Slider -->
<div class="flex items-center gap-2">
<label for="zoom-slider" class="min-w-16">Zoom: {{ Math.round(zoom * 100) }}%</label>
<input id="zoom-slider" type="range" min="0.5" max="4" step="0.1" v-model.number="zoom" class="w-full" />
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, watch, onUnmounted } from 'vue'; import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
interface Sprite { interface Sprite {
id: string; id: string;
@ -75,6 +121,7 @@
const fps = ref(12); const fps = ref(12);
const zoom = ref(1); const zoom = ref(1);
const isDraggable = ref(false); const isDraggable = ref(false);
const showAllSprites = ref(false);
const animationFrameId = ref<number | null>(null); const animationFrameId = ref<number | null>(null);
const lastFrameTime = ref(0); const lastFrameTime = ref(0);
@ -86,7 +133,7 @@
const spritePosBeforeDrag = ref({ x: 0, y: 0 }); const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Computed properties // Computed properties
const totalFrames = () => props.sprites.length; const totalFrames = computed(() => props.sprites.length);
// Canvas drawing // Canvas drawing
const calculateMaxDimensions = () => { const calculateMaxDimensions = () => {
@ -104,8 +151,8 @@
const drawPreviewCanvas = () => { const drawPreviewCanvas = () => {
if (!previewCanvasRef.value || !ctx.value || props.sprites.length === 0) return; if (!previewCanvasRef.value || !ctx.value || props.sprites.length === 0) return;
const sprite = props.sprites[currentFrameIndex.value]; const currentSprite = props.sprites[currentFrameIndex.value];
if (!sprite) return; if (!currentSprite) return;
const { maxWidth, maxHeight } = calculateMaxDimensions(); const { maxWidth, maxHeight } = calculateMaxDimensions();
@ -119,14 +166,40 @@
// Draw grid background (cell) // Draw grid background (cell)
ctx.value.fillStyle = '#f9fafb'; ctx.value.fillStyle = '#f9fafb';
ctx.value.fillRect(0, 0, maxWidth, maxHeight); ctx.value.fillRect(0, 0, maxWidth, maxHeight);
ctx.value.strokeStyle = '#e5e7eb';
ctx.value.strokeRect(0, 0, maxWidth, maxHeight); // Draw background grid (checkerboard pattern for transparency)
const cellSize = 10;
ctx.value.fillStyle = '#e5e7eb';
for (let x = 0; x < maxWidth; x += cellSize) {
for (let y = 0; y < maxHeight; y += cellSize) {
if ((x / cellSize + y / cellSize) % 2 === 0) {
ctx.value.fillRect(x, y, cellSize, cellSize);
}
}
}
// Apply pixel art optimization // Apply pixel art optimization
ctx.value.imageSmoothingEnabled = false; ctx.value.imageSmoothingEnabled = false;
// Draw all sprites with transparency if enabled
if (showAllSprites.value && props.sprites.length > 1) {
ctx.value.globalAlpha = 0.3;
props.sprites.forEach((sprite, index) => {
if (index !== currentFrameIndex.value) {
ctx.value.drawImage(sprite.img, sprite.x, sprite.y);
}
});
ctx.value.globalAlpha = 1.0;
}
// Draw current sprite // Draw current sprite
ctx.value.drawImage(sprite.img, sprite.x, sprite.y); ctx.value.drawImage(currentSprite.img, currentSprite.x, currentSprite.y);
// Draw cell border
ctx.value.strokeStyle = '#e5e7eb';
ctx.value.lineWidth = 1;
ctx.value.strokeRect(0, 0, maxWidth, maxHeight);
// Highlight current frame if draggable // Highlight current frame if draggable
if (isDraggable.value) { if (isDraggable.value) {
@ -283,6 +356,7 @@
watch(currentFrameIndex, drawPreviewCanvas); watch(currentFrameIndex, drawPreviewCanvas);
watch(zoom, drawPreviewCanvas); watch(zoom, drawPreviewCanvas);
watch(isDraggable, drawPreviewCanvas); watch(isDraggable, drawPreviewCanvas);
watch(showAllSprites, drawPreviewCanvas);
// Initial draw // Initial draw
if (props.sprites.length > 0) { if (props.sprites.length > 0) {
@ -291,8 +365,23 @@
</script> </script>
<style scoped> <style scoped>
.spritesheet-preview { /* Custom styling for range inputs */
width: 100%; input[type='range']::-webkit-slider-thumb {
max-width: 100%; -webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
}
input[type='range']::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #3b82f6;
cursor: pointer;
border: none;
} }
</style> </style>

View File

@ -13,7 +13,7 @@
class="bg-white rounded-2xl border-2 border-gray-300 shadow-xl flex flex-col" class="bg-white rounded-2xl border-2 border-gray-300 shadow-xl flex flex-col"
> >
<!-- Header with drag handle --> <!-- Header with drag handle -->
<div class="flex justify-between items-center p-4 border-b border-gray-100 cursor-move" @mousedown="startDrag" @touchstart="startDrag"> <div class="flex justify-between items-center p-4 border-b border-gray-200 cursor-move" @mousedown="startDrag" @touchstart="startDrag">
<h3 class="text-2xl font-semibold text-gray-900">{{ title }}</h3> <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"> <button @click="close" class="p-2 hover:bg-gray-100 rounded-lg transition-colors">
<img src="@/assets/images/close-icon.svg" class="w-5 h-5" alt="Close" /> <img src="@/assets/images/close-icon.svg" class="w-5 h-5" alt="Close" />