UI and application enhancements

This commit is contained in:
2025-08-09 15:46:41 +02:00
parent 6953a41ca4
commit f8d9d2ae83
7 changed files with 371 additions and 89 deletions

View File

@@ -92,8 +92,6 @@
const showAllSprites = ref(false);
const spritePositions = computed(() => {
if (!canvasRef.value) return [];
const { maxWidth, maxHeight } = calculateMaxDimensions();
return props.sprites.map((sprite, index) => {
@@ -119,22 +117,38 @@
});
});
// Cache last known max dimensions to avoid collapsing cells while images are loading
const lastMaxWidth = ref(1);
const lastMaxHeight = ref(1);
const calculateMaxDimensions = () => {
let maxWidth = 0;
let maxHeight = 0;
props.sprites.forEach(sprite => {
maxWidth = Math.max(maxWidth, sprite.width);
maxHeight = Math.max(maxHeight, sprite.height);
const img = sprite.img as HTMLImageElement | undefined;
const w = Math.max(0, sprite.width || (img ? img.naturalWidth || img.width || 0 : 0));
const h = Math.max(0, sprite.height || (img ? img.naturalHeight || img.height || 0 : 0));
maxWidth = Math.max(maxWidth, w);
maxHeight = Math.max(maxHeight, h);
});
// Add some padding to ensure sprites have room to move
return { maxWidth: maxWidth, maxHeight: maxHeight };
// Keep dimensions at least as large as last known to prevent temporary collapse during loading
maxWidth = Math.max(1, maxWidth, lastMaxWidth.value);
maxHeight = Math.max(1, maxHeight, lastMaxHeight.value);
lastMaxWidth.value = maxWidth;
lastMaxHeight.value = maxHeight;
return { maxWidth, maxHeight };
};
const startDrag = (event: MouseEvent) => {
if (!canvasRef.value) return;
// Ignore non-left mouse buttons (but allow touch-generated events without a button prop)
if ('button' in event && (event as MouseEvent).button !== 0) return;
const rect = canvasRef.value.getBoundingClientRect();
const scaleX = canvasRef.value.width / rect.width;
const scaleY = canvasRef.value.height / rect.height;
@@ -287,11 +301,8 @@
const handleTouchStart = (event: TouchEvent) => {
// Don't prevent default to allow scrolling
if (event.touches.length === 1) {
if (!canvasRef.value) return;
const touch = event.touches[0];
const rect = canvasRef.value?.getBoundingClientRect();
if (!rect) return;
// Adjust for zoom
const mouseEvent = {
clientX: touch.clientX,
clientY: touch.clientY,
@@ -322,12 +333,9 @@
// 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;
if (x >= pos.canvasX && x <= pos.canvasX + pos.width && y >= pos.canvasY && y <= pos.canvasY + pos.height) {
return props.sprites.find(s => s.id === pos.id) || null;
}
}
return null;
@@ -339,7 +347,7 @@
const { maxWidth, maxHeight } = calculateMaxDimensions();
// Set canvas size
const rows = Math.ceil(props.sprites.length / props.columns);
const rows = Math.max(1, Math.ceil(props.sprites.length / props.columns));
canvasRef.value.width = maxWidth * props.columns;
canvasRef.value.height = maxHeight * rows;
@@ -380,7 +388,7 @@
props.sprites.forEach((sprite, spriteIndex) => {
if (spriteIndex !== cellIndex) {
// Don't draw the cell's own sprite with transparency
ctx.value?.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
}
});
ctx.value.globalAlpha = 1.0;
@@ -401,7 +409,7 @@
const cellY = Math.floor(row * maxHeight);
// Draw sprite using integer positions for pixel-perfect rendering
ctx.value?.drawImage(sprite.img, cellX + sprite.x, cellY + sprite.y);
ctx.value?.drawImage(sprite.img, Math.floor(cellX + sprite.x), Math.floor(cellY + sprite.y));
});
// Draw ghost sprite if we're dragging between cells
@@ -430,12 +438,32 @@
}
};
// Track which images already have listeners
const imagesWithListeners = new WeakSet<HTMLImageElement>();
const attachImageListeners = () => {
props.sprites.forEach(sprite => {
const img = sprite.img as HTMLImageElement | undefined;
if (img && !imagesWithListeners.has(img)) {
imagesWithListeners.add(img);
if (!img.complete) {
// Redraw when the image loads or errors (to reflect updated dimensions)
img.addEventListener('load', handleForceRedraw, { once: true } as AddEventListenerOptions);
img.addEventListener('error', handleForceRedraw, { once: true } as AddEventListenerOptions);
}
}
});
};
onMounted(() => {
if (canvasRef.value) {
ctx.value = canvasRef.value.getContext('2d');
drawCanvas();
}
// Attach listeners for current sprites
attachImageListeners();
// Listen for forceRedraw event from App.vue
window.addEventListener('forceRedraw', handleForceRedraw);
});
@@ -459,17 +487,18 @@
}
};
watch(() => props.sprites, drawCanvas, { deep: true });
watch(
() => props.sprites,
() => {
attachImageListeners();
drawCanvas();
},
{ deep: true }
);
watch(() => props.columns, drawCanvas);
watch(() => settingsStore.pixelPerfect, drawCanvas);
watch(() => settingsStore.darkMode, drawCanvas);
watch(showAllSprites, drawCanvas);
// Add scale computed property
const scale = computed(() => {
if (!canvasRef.value) return 1;
const rect = canvasRef.value.getBoundingClientRect();
return rect.width / canvasRef.value.width;
});
</script>
<style scoped></style>