Multiple fixes

This commit is contained in:
Dennis Postma 2025-04-03 03:20:47 +02:00
parent ca28f66997
commit 7cb5605b54
7 changed files with 270 additions and 124 deletions

View File

@ -1,6 +1,11 @@
<template>
<div class="min-h-screen bg-gray-900 text-gray-200 font-sans">
<app-header @toggle-help="showHelpModal" />
<div class="max-w-7xl mx-auto px-6">
<div class="bg-blue-500 bg-opacity-10 border-l-4 border-blue-500 p-4 mt-6 rounded-r">
<p>Container size will adjust to fit the largest sprite. All sprites will be placed in cells of the same size.</p>
</div>
</div>
<div class="max-w-7xl mx-auto p-6 grid grid-cols-1 lg:grid-cols-4 gap-6">
<sidebar class="lg:col-span-1" />
<main-content class="lg:col-span-3" />

View File

@ -47,9 +47,7 @@
};
const handleFiles = async (files: FileList) => {
console.log('Handling files:', files.length);
const imageFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
console.log('Image files filtered:', imageFiles.length);
if (imageFiles.length === 0) {
store.showNotification('Please upload image files only', 'error');
@ -61,12 +59,10 @@
for (let i = 0; i < imageFiles.length; i++) {
const file = imageFiles[i];
console.log(`Processing file ${i+1}/${imageFiles.length}:`, file.name, file.type, file.size);
try {
const sprite = await createSpriteFromFile(file, i);
newSprites.push(sprite);
console.log(`Successfully processed ${file.name}`);
} catch (error) {
errorCount++;
console.error('Error loading sprite:', error);
@ -75,7 +71,6 @@
}
if (newSprites.length > 0) {
console.log('Adding sprites to store:', newSprites.length);
store.addSprites(newSprites);
emit('files-uploaded', newSprites);
store.showNotification(`Added ${newSprites.length} sprites successfully`);
@ -86,56 +81,48 @@
const createSpriteFromFile = (file: File, index: number): Promise<Sprite> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
// Create a URL for the file
const objectUrl = URL.createObjectURL(file);
reader.onload = e => {
if (!e.target?.result) {
console.error('Failed to read file:', file.name);
reject(new Error(`Failed to read file: ${file.name}`));
const img = new Image();
// Set up event handlers
img.onload = () => {
// Verify the image has loaded properly
if (img.width === 0 || img.height === 0) {
console.error('Image loaded with invalid dimensions:', file.name, img.width, img.height);
URL.revokeObjectURL(objectUrl);
reject(new Error(`Image has invalid dimensions: ${file.name}`));
return;
}
const img = new Image();
// Set crossOrigin to anonymous to handle CORS issues
img.crossOrigin = 'anonymous';
img.onload = () => {
// Verify the image has loaded properly
if (img.width === 0 || img.height === 0) {
console.error('Image loaded with invalid dimensions:', file.name, img.width, img.height);
reject(new Error(`Image has invalid dimensions: ${file.name}`));
return;
}
console.log('Sprite created successfully:', file.name, img.width, img.height);
resolve({
img,
width: img.width,
height: img.height,
x: 0,
y: 0,
name: file.name,
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
uploadOrder: index,
});
// Create the sprite object
const sprite: Sprite = {
img,
width: img.width,
height: img.height,
x: 0,
y: 0,
name: file.name,
id: `sprite-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
uploadOrder: index,
};
img.onerror = (error) => {
console.error('Error loading image:', file.name, error);
reject(new Error(`Failed to load image: ${file.name}`));
};
// Keep the objectUrl reference and don't revoke it yet
// The image is still needed for rendering later
// URL.revokeObjectURL(objectUrl); - Don't do this anymore
// Set the source after setting up event handlers
img.src = e.target.result as string;
resolve(sprite);
};
reader.onerror = (error) => {
console.error('Error reading file:', file.name, error);
reject(new Error(`Error reading file: ${file.name}`));
img.onerror = error => {
console.error('Error loading image:', file.name, error);
URL.revokeObjectURL(objectUrl);
reject(new Error(`Failed to load image: ${file.name}`));
};
reader.readAsDataURL(file);
// Set the source to the object URL
img.src = objectUrl;
});
};
</script>

View File

@ -39,17 +39,24 @@
}));
const setupCheckerboardPattern = () => {
if (!canvasEl.value) return;
if (!canvasEl.value) {
console.error('MainContent: Canvas element not available for checkerboard pattern');
return;
}
// This will be done with CSS using Tailwind's bg utilities
canvasEl.value.style.backgroundImage = `
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
`;
canvasEl.value.style.backgroundSize = '20px 20px';
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
try {
// This will be done with CSS using Tailwind's bg utilities
canvasEl.value.style.backgroundImage = `
linear-gradient(45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(-45deg, #1a1a1a 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #1a1a1a 75%),
linear-gradient(-45deg, transparent 75%, #1a1a1a 75%)
`;
canvasEl.value.style.backgroundSize = '20px 20px';
canvasEl.value.style.backgroundPosition = '0 0, 0 10px, 10px -10px, -10px 0px';
} catch (error) {
console.error('MainContent: Error setting up checkerboard pattern:', error);
}
};
const handleMouseDown = (e: MouseEvent) => {
@ -85,8 +92,9 @@
if (canvasEl.value && cellX >= 0 && cellX < canvasEl.value.width / store.cellSize.width && cellY >= 0 && cellY < canvasEl.value.height / store.cellSize.height) {
isTooltipVisible.value = true;
tooltipText.value = `Cell: (${cellX}, ${cellY})`;
tooltipPosition.value.x = e.clientX;
tooltipPosition.value.y = e.clientY;
// Use pageX and pageY instead of clientX and clientY
tooltipPosition.value.x = e.pageX;
tooltipPosition.value.y = e.pageY;
} else {
isTooltipVisible.value = false;
}
@ -106,12 +114,15 @@
// Calculate cell boundaries
const cellLeft = cellX * store.cellSize.width;
const cellTop = cellY * store.cellSize.height;
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.width;
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.height;
const cellRight = cellLeft + store.cellSize.width - store.draggedSprite.value.img.width;
const cellBottom = cellTop + store.cellSize.height - store.draggedSprite.value.img.height;
// Constrain position to stay within the cell
store.draggedSprite.value.x = Math.max(cellLeft, Math.min(newX, cellRight));
store.draggedSprite.value.y = Math.max(cellTop, Math.min(newY, cellBottom));
// Trigger a re-render
store.renderSpritesheetPreview();
} else {
// Calculate new position based on grid cells (snap to grid)
const newCellX = Math.floor((x - store.dragOffset.x) / store.cellSize.width);
@ -127,6 +138,9 @@
store.draggedSprite.value.x = boundedCellX * store.cellSize.width;
store.draggedSprite.value.y = boundedCellY * store.cellSize.height;
// Trigger a re-render
store.renderSpritesheetPreview();
}
}
}
@ -154,31 +168,80 @@
};
const setupCanvasEvents = () => {
if (!canvasEl.value) return;
if (!canvasEl.value) {
console.error('MainContent: Canvas element not available for event setup');
return;
}
canvasEl.value.addEventListener('mousedown', handleMouseDown);
canvasEl.value.addEventListener('mousemove', handleMouseMove);
canvasEl.value.addEventListener('mouseup', handleMouseUp);
canvasEl.value.addEventListener('mouseout', handleMouseOut);
try {
canvasEl.value.addEventListener('mousedown', handleMouseDown);
canvasEl.value.addEventListener('mousemove', handleMouseMove);
canvasEl.value.addEventListener('mouseup', handleMouseUp);
canvasEl.value.addEventListener('mouseout', handleMouseOut);
} catch (error) {
console.error('MainContent: Error setting up canvas events:', error);
}
};
onMounted(() => {
if (canvasEl.value) {
// Initialize canvas immediately
initializeCanvas();
// Also set up a MutationObserver to ensure the canvas is properly initialized
// even if there are DOM changes
const observer = new MutationObserver(mutations => {
initializeCanvas();
});
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });
// Clean up the observer after a short time
setTimeout(() => {
observer.disconnect();
}, 2000);
});
const initializeCanvas = () => {
if (!canvasEl.value) {
console.error('MainContent: Canvas element not found');
return;
}
try {
// Get the 2D context
const context = canvasEl.value.getContext('2d');
if (!context) {
console.error('MainContent: Failed to get 2D context from canvas');
return;
}
// Set the store references
store.canvas.value = canvasEl.value;
store.ctx.value = canvasEl.value.getContext('2d');
store.ctx.value = context;
// Initialize canvas size
canvasEl.value.width = 400;
canvasEl.value.height = 300;
// Setup the canvas
setupCheckerboardPattern();
setupCanvasEvents();
// Setup keyboard events for modifiers
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
// Check if we have sprites that need rendering
if (store.sprites.value.length > 0) {
store.updateCellSize();
store.autoArrangeSprites();
store.renderSpritesheetPreview();
}
} catch (error) {
console.error('MainContent: Error initializing canvas:', error);
}
});
};
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeyDown);

View File

@ -1,8 +1,8 @@
<template>
<div class="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50 transition-all duration-300" :class="{ 'opacity-0 invisible': !isModalOpen, 'opacity-100 visible': isModalOpen }" @click.self="closeModal">
<div class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto shadow-lg transform transition-transform duration-300" :class="{ '-translate-y-5': !isModalOpen, 'translate-y-0': isModalOpen }">
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600">
<div class="flex items-center gap-2 text-lg font-semibold">
<div class="fixed inset-0 flex items-center justify-center z-50" :class="{ 'pointer-events-none': !isModalOpen }">
<div class="bg-gray-800 rounded-lg max-w-4xl max-h-[90vh] overflow-auto scrollbar-hide shadow-lg pointer-events-auto" :class="{ invisible: !isModalOpen, visible: isModalOpen }" :style="{ transform: `translate3d(${position.x}px, ${position.y + (isModalOpen ? 0 : -20)}px, 0)` }">
<div class="flex items-center justify-between p-4 bg-gray-700 border-b border-gray-600 cursor-move" @mousedown="startDrag">
<div class="flex items-center gap-2 text-lg font-semibold select-none">
<i class="fas fa-film text-blue-500"></i>
<span>Animation Preview</span>
</div>
@ -104,17 +104,37 @@
}
};
const openModal = () => {
const openModal = async () => {
if (sprites.value.length === 0) {
store.showNotification('Please add sprites first', 'error');
return;
}
// Reset position when opening
position.value = { x: 0, y: 0 };
// Reset to first frame
currentFrame.value = 0;
animation.value.currentFrame = 0;
// Make sure the canvas is initialized
await initializeCanvas();
// Set modal open state
store.isModalOpen.value = true;
// Show the current frame
if (!animation.value.isPlaying && sprites.value.length > 0) {
store.renderAnimationFrame(currentFrame.value);
// Wait for next frame to ensure DOM is updated
await new Promise(resolve => requestAnimationFrame(resolve));
// Set proper canvas size before rendering
if (animCanvas.value && store.cellSize.width && store.cellSize.height) {
animCanvas.value.width = store.cellSize.width;
animCanvas.value.height = store.cellSize.height;
}
// Force render the first frame
if (sprites.value.length > 0) {
store.renderAnimationFrame(0);
}
};
@ -159,23 +179,72 @@
};
onMounted(() => {
if (animCanvas.value) {
store.animation.canvas = animCanvas.value;
store.animation.ctx = animCanvas.value.getContext('2d');
initializeCanvas();
// Initialize canvas size
animCanvas.value.width = 200;
animCanvas.value.height = 200;
// Setup keyboard shortcuts for the modal
window.addEventListener('keydown', handleKeyDown);
}
// Setup keyboard shortcuts for the modal
window.addEventListener('keydown', handleKeyDown);
});
const initializeCanvas = async () => {
if (!animCanvas.value) {
console.error('PreviewModal: Animation canvas not found');
return;
}
try {
// Get the 2D context
const context = animCanvas.value.getContext('2d');
if (!context) {
console.error('PreviewModal: Failed to get 2D context from animation canvas');
return;
}
// Set the store references
store.animation.canvas = animCanvas.value;
store.animation.ctx = context;
} catch (error) {
console.error('PreviewModal: Error initializing animation canvas:', error);
}
};
onBeforeUnmount(() => {
window.removeEventListener('keydown', handleKeyDown);
});
// Add these new refs for dragging functionality
const position = ref({ x: 0, y: 0 });
const isDragging = ref(false);
const dragOffset = ref({ x: 0, y: 0 });
const startDrag = (e: MouseEvent) => {
isDragging.value = true;
dragOffset.value = {
x: e.clientX - position.value.x,
y: e.clientY - position.value.y,
};
// Add temporary event listeners
window.addEventListener('mousemove', handleDrag);
window.addEventListener('mouseup', stopDrag);
};
const handleDrag = (e: MouseEvent) => {
if (!isDragging.value) return;
requestAnimationFrame(() => {
position.value = {
x: e.clientX - dragOffset.value.x,
y: e.clientY - dragOffset.value.y,
};
});
};
const stopDrag = () => {
isDragging.value = false;
window.removeEventListener('mousemove', handleDrag);
window.removeEventListener('mouseup', stopDrag);
};
// Keep currentFrame in sync with animation.currentFrame
watch(
() => animation.value.currentFrame,
@ -205,4 +274,19 @@
border-radius: 50%;
cursor: pointer;
}
/* Optional: Prevent text selection while dragging */
.cursor-move {
user-select: none;
}
/* Add these new styles */
.scrollbar-hide {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.scrollbar-hide::-webkit-scrollbar {
display: none; /* Chrome, Safari and Opera */
}
</style>

View File

@ -10,10 +10,6 @@
</div>
<div class="p-6">
<drop-zone @files-uploaded="handleUpload" />
<div class="bg-blue-500 bg-opacity-10 border-l-4 border-blue-500 p-4 mt-6 rounded-r">
<p>Container size will adjust to fit the largest sprite. All sprites will be placed in cells of the same size.</p>
</div>
</div>
</div>
@ -75,14 +71,14 @@
<script setup lang="ts">
import { computed } from 'vue';
import { useSpritesheetStore } from '../composables/useSpritesheetStore';
import { type Sprite, useSpritesheetStore } from '../composables/useSpritesheetStore';
import DropZone from './DropZone.vue';
import SpriteList from './SpriteList.vue';
const store = useSpritesheetStore();
const sprites = computed(() => store.sprites.value);
const handleUpload = () => {
const handleUpload = (sprites: Sprite[]) => {
// The dropzone component handles adding sprites to the store
// This is just for event handling if needed
};

View File

@ -1,7 +1,7 @@
<template>
<div v-if="sprites.length === 0" class="text-center text-gray-400 py-8">No sprites uploaded yet</div>
<div v-else class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3 max-h-72 overflow-y-auto pr-2">
<div v-else class="grid grid-cols-2 gap-3 max-h-72 overflow-y-auto pr-2">
<div v-for="(sprite, index) in sprites" :key="sprite.id" @click="$emit('spriteClicked', sprite.id)" class="border border-gray-600 rounded bg-gray-700 p-2 text-center transition-all cursor-pointer hover:border-blue-500 hover:-translate-y-0.5 hover:shadow-md">
<img :src="sprite.img.src" :alt="sprite.name" class="max-w-full max-h-16 mx-auto mb-2 bg-black bg-opacity-20 rounded" />
<div class="text-xs text-gray-400 truncate">{{ index + 1 }}. {{ truncateName(sprite.name) }}</div>

View File

@ -28,17 +28,17 @@ export interface AnimationState {
manualUpdate: boolean;
}
export function useSpritesheetStore() {
const sprites = ref<Sprite[]>([]);
const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
const cellSize = reactive<CellSize>({ width: 0, height: 0 });
const columns = ref(4); // Default number of columns
const draggedSprite = ref<Sprite | null>(null);
const dragOffset = reactive({ x: 0, y: 0 });
const isShiftPressed = ref(false);
const isModalOpen = ref(false);
const sprites = ref<Sprite[]>([]);
const canvas = ref<HTMLCanvasElement | null>(null);
const ctx = ref<CanvasRenderingContext2D | null>(null);
const cellSize = reactive<CellSize>({ width: 0, height: 0 });
const columns = ref(4); // Default number of columns
const draggedSprite = ref<Sprite | null>(null);
const dragOffset = reactive({ x: 0, y: 0 });
const isShiftPressed = ref(false);
const isModalOpen = ref(false);
export function useSpritesheetStore() {
const animation = reactive<AnimationState>({
canvas: null,
ctx: null,
@ -58,7 +58,6 @@ export function useSpritesheetStore() {
});
function addSprites(newSprites: Sprite[]) {
console.log('Store: Adding sprites', newSprites.length);
if (newSprites.length === 0) {
console.warn('Store: Attempted to add empty sprites array');
return;
@ -79,21 +78,17 @@ export function useSpritesheetStore() {
return;
}
console.log('Store: Adding valid sprites', validSprites.length);
sprites.value.push(...validSprites);
sprites.value.sort((a, b) => a.uploadOrder - b.uploadOrder);
updateCellSize();
autoArrangeSprites();
console.log('Store: Sprites added successfully, total count:', sprites.value.length);
} catch (error) {
console.error('Store: Error adding sprites:', error);
}
}
function updateCellSize() {
console.log('Store: Updating cell size');
if (sprites.value.length === 0) {
console.log('Store: No sprites to update cell size');
return;
}
@ -115,7 +110,6 @@ export function useSpritesheetStore() {
return;
}
console.log('Store: Cell size calculated', maxWidth, maxHeight);
cellSize.width = maxWidth;
cellSize.height = maxHeight;
@ -126,14 +120,11 @@ export function useSpritesheetStore() {
}
function updateCanvasSize() {
console.log('Store: Updating canvas size');
if (!canvas.value) {
console.error('Store: Canvas not available for size update');
return;
}
if (sprites.value.length === 0) {
console.log('Store: No sprites to determine canvas size');
return;
}
@ -150,7 +141,6 @@ export function useSpritesheetStore() {
const newWidth = cols * cellSize.width;
const newHeight = rows * cellSize.height;
console.log('Store: Setting canvas size to', newWidth, newHeight);
canvas.value.width = newWidth;
canvas.value.height = newHeight;
} catch (error) {
@ -159,9 +149,7 @@ export function useSpritesheetStore() {
}
function autoArrangeSprites() {
console.log('Store: Auto-arranging sprites');
if (sprites.value.length === 0) {
console.log('Store: No sprites to arrange');
return;
}
@ -177,52 +165,75 @@ export function useSpritesheetStore() {
sprite.x = column * cellSize.width;
sprite.y = row * cellSize.height;
console.log(`Store: Positioned sprite ${index} at (${sprite.x}, ${sprite.y})`);
});
// Check if the canvas is ready before attempting to render
if (!ctx.value || !canvas.value) {
return;
}
renderSpritesheetPreview();
if (!animation.isPlaying && animation.manualUpdate && isModalOpen.value) {
renderAnimationFrame(animation.currentFrame);
}
console.log('Store: Sprites arranged successfully');
} catch (error) {
console.error('Store: Error auto-arranging sprites:', error);
}
}
function renderSpritesheetPreview(showGrid = true) {
console.log('Store: Rendering spritesheet preview');
if (!ctx.value || !canvas.value) {
console.error('Store: Canvas or context not available for rendering');
console.error('Store: Canvas or context not available for rendering, will retry when ready');
// Set up a small delay and retry when elements might be ready
setTimeout(() => {
if (ctx.value && canvas.value) {
renderSpritesheetPreview(showGrid);
}
}, 100);
return;
}
if (sprites.value.length === 0) {
console.log('Store: No sprites to render');
return;
}
try {
// Make sure canvas dimensions are set correctly
updateCanvasSize();
// Clear the canvas
ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height);
if (showGrid) {
drawGrid();
}
// Draw each sprite
sprites.value.forEach((sprite, index) => {
try {
if (!sprite.img || sprite.img.width === 0 || sprite.img.height === 0) {
console.warn(`Store: Invalid sprite at index ${index}, skipping render`);
if (!sprite.img) {
console.warn(`Store: Sprite at index ${index} has no image, skipping render`);
return;
}
ctx.value!.drawImage(sprite.img, sprite.x, sprite.y);
if (sprite.img.complete && sprite.img.naturalWidth !== 0) {
ctx.value!.drawImage(sprite.img, sprite.x, sprite.y);
} else {
console.warn(`Store: Sprite image ${index} not fully loaded, setting onload handler`);
// If image isn't loaded yet, set an onload handler
sprite.img.onload = () => {
if (ctx.value && canvas.value) {
ctx.value.drawImage(sprite.img, sprite.x, sprite.y);
}
};
}
} catch (spriteError) {
console.error(`Store: Error rendering sprite at index ${index}:`, spriteError);
}
});
console.log('Store: Spritesheet preview rendered successfully');
} catch (error) {
console.error('Store: Error rendering spritesheet preview:', error);
}
@ -314,7 +325,7 @@ export function useSpritesheetStore() {
});
const link = document.createElement('a');
link.download = 'noxious-spritesheet.png';
link.download = 'spritesheet.png';
link.href = tempCanvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();