add add images button on mobile; drop diagonally
Hunter
Wednesday, October 22nd, 2025 at 1:01 PM
13 <div class="grid" id="grid"></div>
14 </div>
15
16 + <input type="file" id="fileInput" accept="image/*" multiple style="display: none;">
17 + <button id="addImagesBtn" class="add-images-btn">Add images</button>
18 +
19 <script src="script.js"></script>
20 </body>
21 </html>
25 let resizeState = null;
26 let highestZIndex = 0;
27
28 +// Helper function to calculate image dimensions from aspect ratio
29 +function calculateImageDimensions(aspectRatio) {
30 + let widthCells, heightCells;
31 + if (aspectRatio >= 1) {
32 + heightCells = 5;
33 + widthCells = Math.round(heightCells * aspectRatio);
34 + } else {
35 + widthCells = 5;
36 + heightCells = Math.round(widthCells / aspectRatio);
37 + }
38 + return {
39 + widthCells: Math.min(widthCells, GRID_COLS),
40 + heightCells: Math.min(heightCells, GRID_ROWS)
41 + };
42 +}
43 +
44 +// Helper function to load an image and get its dimensions
45 +async function loadImageDimensions(file) {
46 + const reader = new FileReader();
47 + const dataUrl = await new Promise(resolve => {
48 + reader.onload = (e) => resolve(e.target.result);
49 + reader.readAsDataURL(file);
50 + });
51 +
52 + const img = new Image();
53 + const dimensions = await new Promise(resolve => {
54 + img.onload = () => {
55 + const aspectRatio = img.width / img.height;
56 + resolve(calculateImageDimensions(aspectRatio));
57 + };
58 + img.src = dataUrl;
59 + });
60 +
61 + return { dataUrl, ...dimensions };
62 +}
63 +
64 +// Shared function to process and add images to the grid
65 +async function processAndAddImages(files, dropX = 0, dropY = 0) {
66 + const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
67 + if (imageFiles.length === 0) return;
68 +
69 + const cellSize = getCellSize();
70 +
71 + // Load first image to get base dimensions for positioning
72 + const firstImageData = await loadImageDimensions(imageFiles[0]);
73 +
74 + // Calculate base drop position
75 + // For single images, center on cursor; for multiple images, place top-left at cursor
76 + let baseXCell, baseYCell;
77 + if (imageFiles.length === 1) {
78 + baseXCell = Math.round(dropX / cellSize.width - firstImageData.widthCells / 2);
79 + baseYCell = Math.round(dropY / cellSize.height - firstImageData.heightCells / 2);
80 + } else {
81 + baseXCell = Math.round(dropX / cellSize.width);
82 + baseYCell = Math.round(dropY / cellSize.height);
83 + }
84 +
85 + // Pre-allocate z-indexes to maintain drop order
86 + const baseZIndex = highestZIndex + 1;
87 + highestZIndex += imageFiles.length;
88 +
89 + // Load all images
90 + const imageDataArray = [];
91 + for (let idx = 0; idx < imageFiles.length; idx++) {
92 + const data = idx === 0 ? firstImageData : await loadImageDimensions(imageFiles[idx]);
93 + imageDataArray.push({ idx, ...data });
94 + }
95 +
96 + let wrappedOffset = 0;
97 +
98 + for (const { idx, dataUrl, widthCells, heightCells } of imageDataArray) {
99 + // Calculate position with diagonal offset
100 + let xCell = baseXCell + idx;
101 + let yCell = baseYCell + idx;
102 +
103 + // If out of bounds or would overlap with wrapped images, wrap to next diagonal position
104 + const outOfBounds = xCell < 0 || yCell < 0 ||
105 + xCell + widthCells > GRID_COLS ||
106 + yCell + heightCells > GRID_ROWS;
107 + const overlapsWrapped = xCell < wrappedOffset || yCell < wrappedOffset;
108 +
109 + if (outOfBounds || overlapsWrapped) {
110 + xCell = wrappedOffset;
111 + yCell = wrappedOffset;
112 + wrappedOffset++;
113 + }
114 +
115 + const imageData = addImage(dataUrl, xCell, yCell, widthCells, heightCells);
116 + imageData.container.style.zIndex = baseZIndex + idx;
117 + }
118 +}
119 +
120 // Prevent default drag behavior on entire document
121 document.addEventListener('dragover', (e) => {
122 e.preventDefault();
127 e.preventDefault();
128 e.stopPropagation();
129
- const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
- if (files.length === 0) return;
-
130 // Get drop position relative to the grid
131 const gridRect = grid.getBoundingClientRect();
- const cellSize = getCellSize();
-
- // Clamp drop position to grid boundaries
- let dropX = e.clientX - gridRect.left;
- let dropY = e.clientY - gridRect.top;
- dropX = Math.max(0, Math.min(gridRect.width, dropX));
- dropY = Math.max(0, Math.min(gridRect.height, dropY));
-
- // Load first image to get base dimensions
- let firstImageDimensions = null;
- if (files.length > 0) {
- const firstFile = files[0];
- const reader = new FileReader();
- const dataUrl = await new Promise(resolve => {
- reader.onload = (e) => resolve(e.target.result);
- reader.readAsDataURL(firstFile);
- });
-
- const img = new Image();
- await new Promise(resolve => {
- img.onload = () => {
- const aspectRatio = img.width / img.height;
- let widthCells, heightCells;
- if (aspectRatio >= 1) {
- heightCells = 5;
- widthCells = Math.round(heightCells * aspectRatio);
- } else {
- widthCells = 5;
- heightCells = Math.round(widthCells / aspectRatio);
- }
- widthCells = Math.min(widthCells, GRID_COLS);
- heightCells = Math.min(heightCells, GRID_ROWS);
- firstImageDimensions = { widthCells, heightCells };
- resolve();
- };
- img.src = dataUrl;
- });
- }
-
- // Now process all files
- files.forEach((file, idx) => {
- const reader = new FileReader();
- reader.onload = (event) => {
- const img = new Image();
- img.onload = () => {
- const aspectRatio = img.width / img.height;
-
- // Set smaller dimension to 5 cells, calculate larger dimension
- let widthCells, heightCells;
- if (aspectRatio >= 1) {
- // Width is larger or equal
- heightCells = 5;
- widthCells = Math.round(heightCells * aspectRatio);
- } else {
- // Height is larger
- widthCells = 5;
- heightCells = Math.round(widthCells / aspectRatio);
- }
-
- // Clamp to grid boundaries
- widthCells = Math.min(widthCells, GRID_COLS);
- heightCells = Math.min(heightCells, GRID_ROWS);
132 + const dropX = e.clientX - gridRect.left;
133 + const dropY = e.clientY - gridRect.top;
134
- // Calculate position with first image's center under drop point
- let xCell = Math.round(dropX / cellSize.width - firstImageDimensions.widthCells / 2) + (idx * 2);
- let yCell = Math.round(dropY / cellSize.height - firstImageDimensions.heightCells / 2);
-
- // Ensure image stays within grid bounds
- xCell = Math.max(0, Math.min(GRID_COLS - widthCells, xCell));
- yCell = Math.max(0, Math.min(GRID_ROWS - heightCells, yCell));
-
- addImage(event.target.result, xCell, yCell, widthCells, heightCells);
- };
- img.src = event.target.result;
- };
- reader.readAsDataURL(file);
- });
135 + await processAndAddImages(e.dataTransfer.files, dropX, dropY);
136 });
137
138 function addImage(src, xCell, yCell, widthCells, heightCells) {
377
378 // Cmd-click (or Ctrl-click on Windows/Linux) to duplicate
379 if (e.metaKey || e.ctrlKey) {
- // Calculate target position (2 cells to the right if there's room)
- let newXCell = imageData.xCell + 2;
- let newYCell = imageData.yCell;
380 + // Calculate target position (1 cell right and 1 cell down)
381 + let newXCell = imageData.xCell + 1;
382 + let newYCell = imageData.yCell + 1;
383
- // If there's not enough room to the right, try to place appropriately
- if (newXCell + imageData.widthCells > GRID_COLS) {
- // Try wrapping to next row
384 + // If there's not enough room, fall back to top-left
385 + if (newXCell + imageData.widthCells > GRID_COLS || newYCell + imageData.heightCells > GRID_ROWS) {
386 newXCell = 0;
- newYCell = imageData.yCell + imageData.heightCells;
-
- // If that goes off the bottom, place at origin
- if (newYCell + imageData.heightCells > GRID_ROWS) {
- newXCell = 0;
- newYCell = 0;
- }
387 + newYCell = 0;
388 }
389
390 // Create duplicate with the same image source and dimensions
720 return '';
721 }
722 });
723 +
724 +// Mobile file input handling
725 +const fileInput = document.getElementById('fileInput');
726 +const addImagesBtn = document.getElementById('addImagesBtn');
727 +
728 +addImagesBtn.addEventListener('click', () => {
729 + fileInput.click();
730 +});
731 +
732 +fileInput.addEventListener('change', async (e) => {
733 + await processAndAddImages(e.target.files, 0, 0);
734 +
735 + // Clear the input so the same files can be selected again
736 + fileInput.value = '';
737 +});
436 size: 8.5in 11in;
437 margin: 0;
438 }
439 +
440 +.add-images-btn {
441 + display: none;
442 + position: fixed;
443 + bottom: 24px;
444 + left: 50%;
445 + transform: translateX(-50%);
446 + background: var(--accent);
447 + color: var(--page);
448 + border: none;
449 + border-radius: 100px;
450 + padding: 14px 28px;
451 + font-size: 16px;
452 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
453 + font-weight: 600;
454 + cursor: pointer;
455 + box-shadow: 0 4px 12px rgba(0,0,0,0.2);
456 + z-index: 10000;
457 + -webkit-tap-highlight-color: transparent;
458 + user-select: none;
459 + -webkit-user-select: none;
460 + transition: transform 0.1s ease, box-shadow 0.1s ease;
461 +}
462 +
463 +.add-images-btn:active {
464 + transform: translateX(-50%) scale(0.95);
465 + box-shadow: 0 2px 8px rgba(0,0,0,0.2);
466 +}
467 +
468 +@media (pointer: coarse), (hover: none) {
469 + .add-images-btn {
470 + display: block;
471 + }
472 +}