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>
<div class="spritesheet-preview">
<!-- Preview Canvas -->
<div class="relative border border-gray-300 rounded-lg mb-4 overflow-hidden" :style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }">
<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="spritesheet-preview w-full">
<!-- Controls Container -->
<div class="bg-white rounded-lg border border-gray-200 p-4">
<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>
<!-- Controls -->
<div class="space-y-4">
<!-- Playback Controls -->
<div class="flex items-center gap-4">
<button @click="togglePlayback" class="bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded">
{{ isPlaying ? 'Pause' : 'Play' }}
</button>
<div class="flex items-center gap-2">
<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>
<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>
</div>
<div class="flex items-center">
<input id="sprite-position" type="checkbox" v-model="isDraggable" class="mr-2" />
<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 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">
<canvas
ref="previewCanvasRef"
@mousedown="startDrag"
@mousemove="drag"
@mouseup="stopDrag"
@mouseleave="stopDrag"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="stopDrag"
class="block"
:class="{ 'cursor-move': isDraggable }"
:style="{ transform: `scale(${zoom})`, transformOrigin: 'top left' }"
>
</canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { ref, onMounted, watch, onUnmounted, computed } from 'vue';
interface Sprite {
id: string;
@ -75,6 +121,7 @@
const fps = ref(12);
const zoom = ref(1);
const isDraggable = ref(false);
const showAllSprites = ref(false);
const animationFrameId = ref<number | null>(null);
const lastFrameTime = ref(0);
@ -86,7 +133,7 @@
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
// Computed properties
const totalFrames = () => props.sprites.length;
const totalFrames = computed(() => props.sprites.length);
// Canvas drawing
const calculateMaxDimensions = () => {
@ -104,8 +151,8 @@
const drawPreviewCanvas = () => {
if (!previewCanvasRef.value || !ctx.value || props.sprites.length === 0) return;
const sprite = props.sprites[currentFrameIndex.value];
if (!sprite) return;
const currentSprite = props.sprites[currentFrameIndex.value];
if (!currentSprite) return;
const { maxWidth, maxHeight } = calculateMaxDimensions();
@ -119,14 +166,40 @@
// Draw grid background (cell)
ctx.value.fillStyle = '#f9fafb';
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
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
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
if (isDraggable.value) {
@ -283,6 +356,7 @@
watch(currentFrameIndex, drawPreviewCanvas);
watch(zoom, drawPreviewCanvas);
watch(isDraggable, drawPreviewCanvas);
watch(showAllSprites, drawPreviewCanvas);
// Initial draw
if (props.sprites.length > 0) {
@ -291,8 +365,23 @@
</script>
<style scoped>
.spritesheet-preview {
width: 100%;
max-width: 100%;
/* Custom styling for range inputs */
input[type='range']::-webkit-slider-thumb {
-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>

View File

@ -13,7 +13,7 @@
class="bg-white rounded-2xl border-2 border-gray-300 shadow-xl flex flex-col"
>
<!-- 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>
<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" />