first pass at responsive layout
Hunter
Monday, October 20th, 2025 at 3:14 PM
-const MM_TO_PX = 96 / 25.4;
-const CELL_SIZE_MM = 4;
-const CELL_SIZE_PX = CELL_SIZE_MM * MM_TO_PX;
-const GRID_OFFSET_IN = 0.25;
-const GRID_OFFSET_PX = GRID_OFFSET_IN * 96;
1 const GRID_COLS = 50;
2 const GRID_ROWS = 66;
3
4 const grid = document.getElementById('grid');
5 const page = document.querySelector('.page');
6
7 +// Calculate cell size dynamically based on actual grid dimensions
8 +function getCellSize() {
9 + const gridRect = grid.getBoundingClientRect();
10 + return {
11 + width: gridRect.width / GRID_COLS,
12 + height: gridRect.height / GRID_ROWS
13 + };
14 +}
15 +
16 // Create grid cells
17 for (let i = 0; i < GRID_COLS * GRID_ROWS; i++) {
18 const cell = document.createElement('div');
40
41 // Get drop position relative to the grid
42 const gridRect = grid.getBoundingClientRect();
- const gridWidth = GRID_COLS * CELL_SIZE_PX;
- const gridHeight = GRID_ROWS * CELL_SIZE_PX;
43 + const cellSize = getCellSize();
44
45 // Clamp drop position to grid boundaries
46 let dropX = e.clientX - gridRect.left;
47 let dropY = e.clientY - gridRect.top;
- dropX = Math.max(0, Math.min(gridWidth, dropX));
- dropY = Math.max(0, Math.min(gridHeight, dropY));
48 + dropX = Math.max(0, Math.min(gridRect.width, dropX));
49 + dropY = Math.max(0, Math.min(gridRect.height, dropY));
50
51 // Load first image to get base dimensions
52 let firstImageDimensions = null;
104 heightCells = Math.min(heightCells, GRID_ROWS);
105
106 // Calculate position with first image's center under drop point
- let xCell = Math.round(dropX / CELL_SIZE_PX - firstImageDimensions.widthCells / 2) + (idx * 2);
- let yCell = Math.round(dropY / CELL_SIZE_PX - firstImageDimensions.heightCells / 2);
107 + let xCell = Math.round(dropX / cellSize.width - firstImageDimensions.widthCells / 2) + (idx * 2);
108 + let yCell = Math.round(dropY / cellSize.height - firstImageDimensions.heightCells / 2);
109
110 // Ensure image stays within grid bounds
111 xCell = Math.max(0, Math.min(GRID_COLS - widthCells, xCell));
175 imageData.naturalHeight = img.naturalHeight;
176
177 // Calculate base scale to cover container (mimics object-fit: cover)
- const containerWidth = imageData.widthCells * CELL_SIZE_PX;
- const containerHeight = imageData.heightCells * CELL_SIZE_PX;
178 + const cellSize = getCellSize();
179 + const containerWidth = imageData.widthCells * cellSize.width;
180 + const containerHeight = imageData.heightCells * cellSize.height;
181 const scaleX = containerWidth / img.naturalWidth;
182 const scaleY = containerHeight / img.naturalHeight;
183 imageData.baseScale = Math.max(scaleX, scaleY);
196
197 function calculatePanBounds(imageData) {
198 // Container dimensions in the current (possibly swapped) grid orientation
- const containerWidth = imageData.widthCells * CELL_SIZE_PX;
- const containerHeight = imageData.heightCells * CELL_SIZE_PX;
199 + const cellSize = getCellSize();
200 + const containerWidth = imageData.widthCells * cellSize.width;
201 + const containerHeight = imageData.heightCells * cellSize.height;
202
203 if (imageData.naturalWidth === 0 || imageData.naturalHeight === 0) {
204 return { maxPanX: 0, maxPanY: 0 };
236 }
237
238 function updateImagePosition(img) {
- img.container.style.left = img.xCell * CELL_SIZE_PX + 'px';
- img.container.style.top = img.yCell * CELL_SIZE_PX + 'px';
- img.container.style.width = img.widthCells * CELL_SIZE_PX + 'px';
- img.container.style.height = img.heightCells * CELL_SIZE_PX + 'px';
239 + const cellSize = getCellSize();
240 + img.container.style.left = img.xCell * cellSize.width + 'px';
241 + img.container.style.top = img.yCell * cellSize.height + 'px';
242 + img.container.style.width = img.widthCells * cellSize.width + 'px';
243 + img.container.style.height = img.heightCells * cellSize.height + 'px';
244 +
245 + // Store cell positions as CSS variables for print styles
246 + img.container.style.setProperty('--x-cell', img.xCell);
247 + img.container.style.setProperty('--y-cell', img.yCell);
248 + img.container.style.setProperty('--width-cells', img.widthCells);
249 + img.container.style.setProperty('--height-cells', img.heightCells);
250
251 // Update dimension labels
252 const widthLabel = img.container.querySelector('.dimension-label.width');
256
257 // Recalculate baseScale if container size changed
258 if (img.naturalWidth > 0 && img.naturalHeight > 0) {
- const containerWidth = img.widthCells * CELL_SIZE_PX;
- const containerHeight = img.heightCells * CELL_SIZE_PX;
259 + const containerWidth = img.widthCells * cellSize.width;
260 + const containerHeight = img.heightCells * cellSize.height;
261
262 // When rotated 90° or 270°, the image dimensions are effectively swapped
263 const isRotated90or270 = img.rotation % 180 !== 0;
279 // Transform: translate from center (-50%, -50%), scale, rotate, then pan
280 // Pan is applied after rotation so it stays relative to the image's rotated state
281 imgElement.style.transform = `translate(-50%, -50%) scale(${totalScale}) rotate(${img.rotation}deg) translate(${img.panX}px, ${img.panY}px)`;
282 +
283 + // Store transform parameters as CSS variables for print recalculation
284 + img.container.style.setProperty('--user-scale', img.userScale);
285 + img.container.style.setProperty('--rotation', img.rotation);
286 + img.container.style.setProperty('--pan-x', img.panX);
287 + img.container.style.setProperty('--pan-y', img.panY);
288 + img.container.style.setProperty('--natural-width', img.naturalWidth);
289 + img.container.style.setProperty('--natural-height', img.naturalHeight);
290 }
291 }
292
528 const dx = e.clientX - dragState.startX;
529 const dy = e.clientY - dragState.startY;
530
- const dxCells = Math.round(dx / CELL_SIZE_PX);
- const dyCells = Math.round(dy / CELL_SIZE_PX);
531 + const cellSize = getCellSize();
532 + const dxCells = Math.round(dx / cellSize.width);
533 + const dyCells = Math.round(dy / cellSize.height);
534
535 const newXCell = Math.max(0, Math.min(GRID_COLS - dragState.image.widthCells, dragState.startXCell + dxCells));
536 const newYCell = Math.max(0, Math.min(GRID_ROWS - dragState.image.heightCells, dragState.startYCell + dyCells));
544 const dx = e.clientX - resizeState.startX;
545 const dy = e.clientY - resizeState.startY;
546
- const dxCells = Math.round(dx / CELL_SIZE_PX);
- const dyCells = Math.round(dy / CELL_SIZE_PX);
547 + const cellSize = getCellSize();
548 + const dxCells = Math.round(dx / cellSize.width);
549 + const dyCells = Math.round(dy / cellSize.height);
550
551 const dir = resizeState.direction;
552 const img = resizeState.image;
607 document.body.classList.remove('resizing');
608 }
609 });
610 +
611 +// Update all image positions when window resizes (for responsive scaling)
612 +window.addEventListener('resize', () => {
613 + images.forEach(img => {
614 + updateImagePosition(img);
615 + });
616 +});
617 +
618 +// Store original transforms before print
619 +let beforePrintTransforms = [];
620 +
621 +// Recalculate image transforms for print
622 +window.addEventListener('beforeprint', () => {
623 + const PRINT_CELL_SIZE_MM = 4;
624 + const MM_TO_PX_PRINT = 96 / 25.4; // Conversion for print
625 + const printCellSizePx = PRINT_CELL_SIZE_MM * MM_TO_PX_PRINT;
626 +
627 + // Store current transforms and container styles
628 + beforePrintTransforms = images.map(img => {
629 + const imgElement = img.container.querySelector('img');
630 + return {
631 + container: img.container,
632 + transform: imgElement ? imgElement.style.transform : '',
633 + left: img.container.style.left,
634 + top: img.container.style.top,
635 + width: img.container.style.width,
636 + height: img.container.style.height
637 + };
638 + });
639 +
640 + images.forEach(img => {
641 + const imgElement = img.container.querySelector('img');
642 + if (imgElement && img.naturalWidth > 0 && img.naturalHeight > 0) {
643 + // Calculate container size at print (in px)
644 + const containerWidth = img.widthCells * printCellSizePx;
645 + const containerHeight = img.heightCells * printCellSizePx;
646 +
647 + // Calculate base scale for print
648 + const isRotated90or270 = img.rotation % 180 !== 0;
649 + const effectiveWidth = isRotated90or270 ? img.naturalHeight : img.naturalWidth;
650 + const effectiveHeight = isRotated90or270 ? img.naturalWidth : img.naturalHeight;
651 +
652 + const scaleX = containerWidth / effectiveWidth;
653 + const scaleY = containerHeight / effectiveHeight;
654 + const printBaseScale = Math.max(scaleX, scaleY);
655 + const printTotalScale = printBaseScale * img.userScale;
656 +
657 + // Apply transform for print
658 + imgElement.style.transform = `translate(-50%, -50%) scale(${printTotalScale}) rotate(${img.rotation}deg) translate(${img.panX}px, ${img.panY}px)`;
659 + }
660 + });
661 +});
662 +
663 +// Restore screen transforms after print
664 +window.addEventListener('afterprint', () => {
665 + // Restore the exact transforms and positions from before print
666 + beforePrintTransforms.forEach(saved => {
667 + const imgElement = saved.container.querySelector('img');
668 + if (imgElement) {
669 + imgElement.style.transform = saved.transform;
670 + }
671 + saved.container.style.left = saved.left;
672 + saved.container.style.top = saved.top;
673 + saved.container.style.width = saved.width;
674 + saved.container.style.height = saved.height;
675 + });
676 + beforePrintTransforms = [];
677 +});
5 --color-accent: #79aeea;
6 --color-white: white;
7 --shadow: #00000044;
8 +
9 + /* Grid configuration */
10 + --grid-cols: 50;
11 + --grid-rows: 66;
12 + --cell-size-mm: 4mm; /* Cell size for print */
13 +
14 + /* Page dimensions: 8.5in = 216mm, 11in = 279.4mm */
15 + /* Grid dimensions at print: 200mm x 264mm */
16 + /* Grid as percentage of page: 200/216 = 92.59%, 264/279.4 = 94.48% */
17 + --grid-width-percent: 92.59%;
18 + --grid-height-percent: 94.48%;
19 }
20
21 * {
31 align-items: flex-start;
32 min-height: 100vh;
33 padding: 20px;
34 + overflow-x: hidden;
35 }
36
37 .page {
- width: 8.5in;
- height: 11in;
38 + /* Scale down on narrow screens, but never larger than 8.5in */
39 + max-width: min(8.5in, calc(100vw - 40px));
40 + width: 100%;
41 + /* Maintain 8.5:11 aspect ratio (11/8.5 = 1.294117647) */
42 + aspect-ratio: 8.5 / 11;
43 background: var(--color-bg-page);
44 position: relative;
45 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- transform-origin: top center;
46 + display: flex;
47 + align-items: center;
48 + justify-content: center;
49 }
50
51 .grid {
- position: absolute;
- top: calc((11in - 264mm) / 2);
- left: calc((8.5in - 200mm) / 2);
52 + /* Grid dimensions as percentage of page to scale responsively */
53 + width: var(--grid-width-percent);
54 + height: var(--grid-height-percent);
55 display: grid;
- grid-template-columns: repeat(50, 4mm);
- grid-template-rows: repeat(66, 4mm);
56 + grid-template-columns: repeat(var(--grid-cols), 1fr);
57 + grid-template-rows: repeat(var(--grid-rows), 1fr);
58 gap: 0;
59 line-height: 0;
60 + position: relative; /* Image containers are positioned relative to grid */
61 + /* Grid is centered within .page by the parent's flexbox */
62 }
63
64 .grid-cell {
- width: 4mm;
- height: 4mm;
65 + width: 100%;
66 + height: 100%;
67 box-sizing: border-box;
68 border-right: 1px dashed var(--color-grid-line);
69 border-bottom: 1px dashed var(--color-grid-line);
243 }
244
245 .page {
- width: 8.5in;
- height: 11in;
246 + width: 8.5in !important;
247 + max-width: 8.5in !important;
248 + height: 11in !important;
249 box-shadow: none;
250 margin: 0;
251 padding: 0;
252 background: var(--color-white);
- transform: none !important;
- position: relative;
253 + display: flex !important;
254 + align-items: center !important;
255 + justify-content: center !important;
256 }
257
258 .grid {
- position: absolute;
- top: calc((11in - 264mm) / 2);
- left: calc((8.5in - 200mm) / 2);
259 + /* Use fixed dimensions for print: 50 cells * 4mm = 200mm, 66 cells * 4mm = 264mm */
260 + width: 200mm !important;
261 + height: 264mm !important;
262 display: grid;
- grid-template-columns: repeat(50, 4mm);
- grid-template-rows: repeat(66, 4mm);
263 + grid-template-columns: repeat(var(--grid-cols), 1fr) !important;
264 + grid-template-rows: repeat(var(--grid-rows), 1fr) !important;
265 + position: relative !important;
266 }
267
268 .grid-cell {
269 border-right: 1px dashed var(--color-grid-line) !important;
270 border-bottom: 1px dashed var(--color-grid-line) !important;
271 box-sizing: border-box !important;
- width: 4mm !important;
- height: 4mm !important;
272 + width: 100% !important;
273 + height: 100% !important;
274 margin: 0 !important;
275 padding: 0 !important;
276 }
287 outline: none !important;
288 position: absolute !important;
289 background: var(--color-white) !important;
290 + /* Recalculate positions using cell coordinates and print cell size */
291 + left: calc(var(--x-cell) * var(--cell-size-mm)) !important;
292 + top: calc(var(--y-cell) * var(--cell-size-mm)) !important;
293 + width: calc(var(--width-cells) * var(--cell-size-mm)) !important;
294 + height: calc(var(--height-cells) * var(--cell-size-mm)) !important;
295 }
296
297 .image-container .image-wrapper {