231 lines
6.8 KiB
Vue

<template>
<div class="space-y-4">
<div class="flex justify-between items-center">
<div class="flex items-center">
<input id="pixel-perfect" type="checkbox" v-model="pixelPerfect" class="mr-2" @change="drawCanvas" />
<label for="pixel-perfect">Pixel perfect rendering (for pixel art)</label>
</div>
</div>
<div class="relative border border-gray-300 rounded-lg">
<canvas ref="canvasRef" @mousedown="startDrag" @mousemove="drag" @mouseup="stopDrag" @mouseleave="stopDrag" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="stopDrag" class="w-full"></canvas>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch, computed } from 'vue';
interface Sprite {
id: string;
img: HTMLImageElement;
width: number;
height: number;
x: number;
y: number;
}
const props = defineProps<{
sprites: Sprite[];
columns: number;
}>();
const emit = defineEmits<{
(e: 'updateSprite', id: string, x: number, y: number): void;
}>();
// Pixel art optimization
const pixelPerfect = ref(true);
const canvasRef = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
// Dragging state
const isDragging = ref(false);
const activeSpriteId = ref<string | null>(null);
const dragStartX = ref(0);
const dragStartY = ref(0);
const spritePosBeforeDrag = ref({ x: 0, y: 0 });
const spritePositions = computed(() => {
if (!canvasRef.value) return [];
const { maxWidth, maxHeight } = calculateMaxDimensions();
return props.sprites.map((sprite, index) => {
const col = index % props.columns;
const row = Math.floor(index / props.columns);
return {
id: sprite.id,
canvasX: col * maxWidth + sprite.x,
canvasY: row * maxHeight + sprite.y,
cellX: col * maxWidth,
cellY: row * maxHeight,
width: sprite.width,
height: sprite.height,
maxWidth,
maxHeight,
};
});
});
const calculateMaxDimensions = () => {
let maxWidth = 0;
let maxHeight = 0;
props.sprites.forEach(sprite => {
maxWidth = Math.max(maxWidth, sprite.width);
maxHeight = Math.max(maxHeight, sprite.height);
});
return { maxWidth, maxHeight };
};
const startDrag = (event: MouseEvent) => {
if (!canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const clickedSprite = findSpriteAtPosition(mouseX, mouseY);
if (clickedSprite) {
isDragging.value = true;
activeSpriteId.value = clickedSprite.id;
dragStartX.value = mouseX;
dragStartY.value = mouseY;
// Find current sprite position
const sprite = props.sprites.find(s => s.id === clickedSprite.id);
if (sprite) {
spritePosBeforeDrag.value = { x: sprite.x, y: sprite.y };
}
}
};
const drag = (event: MouseEvent) => {
if (!isDragging.value || !activeSpriteId.value || !canvasRef.value) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
const mouseX = (event.clientX - rect.left) * scaleX;
const mouseY = (event.clientY - rect.top) * scaleY;
const deltaX = mouseX - dragStartX.value;
const deltaY = mouseY - dragStartY.value;
// Calculate new position with constraints
const spriteIndex = props.sprites.findIndex(s => s.id === activeSpriteId.value);
if (spriteIndex === -1) return;
const position = spritePositions.value.find(pos => pos.id === activeSpriteId.value);
if (!position) return;
let newX = spritePosBeforeDrag.value.x + deltaX;
let newY = spritePosBeforeDrag.value.y + deltaY;
// Constrain movement strictly within cell
newX = Math.max(0, Math.min(position.maxWidth - props.sprites[spriteIndex].width, newX));
newY = Math.max(0, Math.min(position.maxHeight - props.sprites[spriteIndex].height, newY));
emit('updateSprite', activeSpriteId.value, newX, newY);
drawCanvas();
};
const stopDrag = () => {
isDragging.value = false;
activeSpriteId.value = null;
};
const handleTouchStart = (event: TouchEvent) => {
if (event.touches.length === 1) {
const touch = event.touches[0];
const mouseEvent = new MouseEvent('mousedown', {
clientX: touch.clientX,
clientY: touch.clientY,
});
startDrag(mouseEvent);
}
};
const handleTouchMove = (event: TouchEvent) => {
if (event.touches.length === 1) {
const touch = event.touches[0];
const mouseEvent = new MouseEvent('mousemove', {
clientX: touch.clientX,
clientY: touch.clientY,
});
drag(mouseEvent);
}
};
const findSpriteAtPosition = (x: number, y: number) => {
// Search in reverse order to get the topmost sprite first
for (let i = spritePositions.value.length - 1; i >= 0; i--) {
const pos = spritePositions.value[i];
const sprite = props.sprites.find(s => s.id === pos.id);
if (!sprite) continue;
if (x >= pos.canvasX && x <= pos.canvasX + sprite.width && y >= pos.canvasY && y <= pos.canvasY + sprite.height) {
return sprite;
}
}
return null;
};
const drawCanvas = () => {
if (!canvasRef.value || !ctx.value) return;
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Set canvas size
const rows = Math.ceil(props.sprites.length / props.columns);
canvasRef.value.width = maxWidth * props.columns;
canvasRef.value.height = maxHeight * rows;
// Clear canvas
ctx.value.clearRect(0, 0, canvasRef.value.width, canvasRef.value.height);
// Disable image smoothing
ctx.value.imageSmoothingEnabled = false;
// Draw grid
ctx.value.strokeStyle = '#e5e7eb';
for (let col = 0; col < props.columns; col++) {
for (let row = 0; row < rows; row++) {
ctx.value.strokeRect(Math.floor(col * maxWidth), Math.floor(row * maxHeight), maxWidth, maxHeight);
}
}
// Draw sprites
props.sprites.forEach((sprite, index) => {
const col = index % props.columns;
const row = Math.floor(index / props.columns);
const cellX = Math.floor(col * maxWidth);
const cellY = Math.floor(row * maxHeight);
// Draw sprite using integer positions
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
});
};
onMounted(() => {
if (canvasRef.value) {
ctx.value = canvasRef.value.getContext('2d');
drawCanvas();
}
});
watch(() => props.sprites, drawCanvas, { deep: true });
watch(() => props.columns, drawCanvas);
</script>