diff --git a/src/App.vue b/src/App.vue index 8da39f4..3e78d5e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -103,7 +103,7 @@ - + @@ -688,6 +688,79 @@ img.src = url; }; + const addSpriteWithResize = (file: File) => { + const url = URL.createObjectURL(file); + const img = new Image(); + img.onload = () => { + // Find current max dimensions + let maxWidth = 0; + let maxHeight = 0; + sprites.value.forEach(sprite => { + maxWidth = Math.max(maxWidth, sprite.width); + maxHeight = Math.max(maxHeight, sprite.height); + }); + + // Create new sprite + const newSprite: Sprite = { + id: crypto.randomUUID(), + file, + img, + url, + width: img.width, + height: img.height, + x: 0, + y: 0, + }; + + // Calculate new max dimensions after adding the new sprite + const newMaxWidth = Math.max(maxWidth, img.width); + const newMaxHeight = Math.max(maxHeight, img.height); + + // Resize existing sprites if the new image is larger + if (img.width > maxWidth || img.height > maxHeight) { + // Update all existing sprites to center them in the new larger cells + sprites.value = sprites.value.map(sprite => { + let newX = sprite.x; + let newY = sprite.y; + + // Adjust x position if width increased + if (img.width > maxWidth) { + const widthDiff = newMaxWidth - maxWidth; + // Try to keep the sprite in the same relative position + const relativeX = maxWidth > 0 ? sprite.x / maxWidth : 0; + newX = Math.floor(relativeX * newMaxWidth); + // Make sure it doesn't go out of bounds + newX = Math.max(0, Math.min(newX, newMaxWidth - sprite.width)); + } + + // Adjust y position if height increased + if (img.height > maxHeight) { + const heightDiff = newMaxHeight - maxHeight; + const relativeY = maxHeight > 0 ? sprite.y / maxHeight : 0; + newY = Math.floor(relativeY * newMaxHeight); + newY = Math.max(0, Math.min(newY, newMaxHeight - sprite.height)); + } + + return { ...sprite, x: newX, y: newY }; + }); + } + + // Add the new sprite + sprites.value = [...sprites.value, newSprite]; + + // Force redraw of the canvas + setTimeout(() => { + const event = new Event('forceRedraw'); + window.dispatchEvent(event); + }, 0); + }; + img.onerror = () => { + console.error('Failed to load new sprite image:', file.name); + URL.revokeObjectURL(url); + }; + img.src = url; + }; + // Download as GIF with specified FPS const downloadAsGif = (fps: number) => { if (sprites.value.length === 0) { diff --git a/src/components/SpriteCanvas.vue b/src/components/SpriteCanvas.vue index fc23845..7e84a5e 100644 --- a/src/components/SpriteCanvas.vue +++ b/src/components/SpriteCanvas.vue @@ -46,7 +46,12 @@ @touchmove="handleTouchMove" @touchend="stopDrag" @contextmenu.prevent - class="w-full" + @dragover="handleDragOver" + @dragenter="handleDragEnter" + @dragleave="handleDragLeave" + @drop="handleDrop" + class="w-full transition-all" + :class="{ 'ring-4 ring-blue-500 ring-opacity-50': isDragOver }" :style="settingsStore.pixelPerfect ? { 'image-rendering': 'pixelated' } : {}" > @@ -119,6 +124,7 @@ (e: 'removeSprite', id: string): void; (e: 'replaceSprite', id: string, file: File): void; (e: 'addSprite', file: File): void; + (e: 'addSpriteWithResize', file: File): void; }>(); // Get settings from store @@ -149,6 +155,7 @@ const contextMenuSpriteId = ref(null); const replacingSpriteId = ref(null); const fileInput = ref(null); + const isDragOver = ref(false); const spritePositions = computed(() => { const { maxWidth, maxHeight } = calculateMaxDimensions(); @@ -446,6 +453,88 @@ contextMenuSpriteId.value = null; }; + // Drag and drop handlers + const handleDragOver = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + if (event.dataTransfer) { + event.dataTransfer.dropEffect = 'copy'; + } + isDragOver.value = true; + }; + + const handleDragEnter = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + isDragOver.value = true; + }; + + const handleDragLeave = (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + // Only set to false if we're leaving the canvas entirely + const rect = canvasRef.value?.getBoundingClientRect(); + if (rect && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) { + isDragOver.value = false; + } + }; + + const handleDrop = async (event: DragEvent) => { + event.preventDefault(); + event.stopPropagation(); + isDragOver.value = false; + + if (!event.dataTransfer?.files || event.dataTransfer.files.length === 0) { + return; + } + + const files = Array.from(event.dataTransfer.files).filter(file => file.type.startsWith('image/')); + + if (files.length === 0) { + alert('Please drop image files only.'); + return; + } + + // Process each dropped file + for (const file of files) { + await processDroppedImage(file); + } + }; + + const processDroppedImage = (file: File): Promise => { + return new Promise((resolve, reject) => { + const img = new Image(); + const reader = new FileReader(); + + reader.onload = e => { + if (e.target?.result) { + img.src = e.target.result as string; + } + }; + + img.onload = () => { + const { maxWidth, maxHeight } = calculateMaxDimensions(); + + // Check if the dropped image is larger than current cells + if (img.naturalWidth > maxWidth || img.naturalHeight > maxHeight) { + // Emit event with resize flag + emit('addSpriteWithResize', file); + } else { + // Normal add + emit('addSprite', file); + } + resolve(); + }; + + img.onerror = () => { + console.error('Failed to load image:', file.name); + reject(new Error('Failed to load image')); + }; + + reader.readAsDataURL(file); + }); + }; + const handleTouchMove = (event: TouchEvent) => { // Only prevent default when we're actually dragging if (isDragging.value) {