231 lines
6.8 KiB
Vue
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>
|