first pass at responsive layout

Hunter
Monday, October 20th, 2025 at 3:14 PM

script.js+115-25
style.css+48-21

script.js
    -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 +});
style.css
  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 {