Improvements
This commit is contained in:
parent
d323355451
commit
bc7af9dd8e
@ -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 }"><</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 }">></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>
|
||||
|
@ -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" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user