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) {