commit e657b0d19023b26ff34ee47a307ef74448910078
parent c063a5e974ce874b241f291266d63251c0b6a33f
Author: Hunter
Date: Mon, 20 Oct 2025 15:14:19 -0400
first pass at responsive layout
Diffstat:
| M | script.js | | | 140 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------- |
| M | style.css | | | 69 | ++++++++++++++++++++++++++++++++++++++++++++++++--------------------- |
2 files changed, 163 insertions(+), 46 deletions(-)
diff --git a/script.js b/script.js
@@ -1,14 +1,18 @@
-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;
const GRID_COLS = 50;
const GRID_ROWS = 66;
const grid = document.getElementById('grid');
const page = document.querySelector('.page');
+// Calculate cell size dynamically based on actual grid dimensions
+function getCellSize() {
+ const gridRect = grid.getBoundingClientRect();
+ return {
+ width: gridRect.width / GRID_COLS,
+ height: gridRect.height / GRID_ROWS
+ };
+}
+
// Create grid cells
for (let i = 0; i < GRID_COLS * GRID_ROWS; i++) {
const cell = document.createElement('div');
@@ -36,14 +40,13 @@ document.addEventListener('drop', async (e) => {
// Get drop position relative to the grid
const gridRect = grid.getBoundingClientRect();
- const gridWidth = GRID_COLS * CELL_SIZE_PX;
- const gridHeight = GRID_ROWS * CELL_SIZE_PX;
+ 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(gridWidth, dropX));
- dropY = Math.max(0, Math.min(gridHeight, dropY));
+ 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;
@@ -101,8 +104,8 @@ document.addEventListener('drop', async (e) => {
heightCells = Math.min(heightCells, GRID_ROWS);
// 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);
+ 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));
@@ -172,8 +175,9 @@ function addImage(src, xCell, yCell, widthCells, heightCells) {
imageData.naturalHeight = img.naturalHeight;
// Calculate base scale to cover container (mimics object-fit: cover)
- const containerWidth = imageData.widthCells * CELL_SIZE_PX;
- const containerHeight = imageData.heightCells * CELL_SIZE_PX;
+ const cellSize = getCellSize();
+ const containerWidth = imageData.widthCells * cellSize.width;
+ const containerHeight = imageData.heightCells * cellSize.height;
const scaleX = containerWidth / img.naturalWidth;
const scaleY = containerHeight / img.naturalHeight;
imageData.baseScale = Math.max(scaleX, scaleY);
@@ -192,8 +196,9 @@ function addImage(src, xCell, yCell, widthCells, heightCells) {
function calculatePanBounds(imageData) {
// Container dimensions in the current (possibly swapped) grid orientation
- const containerWidth = imageData.widthCells * CELL_SIZE_PX;
- const containerHeight = imageData.heightCells * CELL_SIZE_PX;
+ const cellSize = getCellSize();
+ const containerWidth = imageData.widthCells * cellSize.width;
+ const containerHeight = imageData.heightCells * cellSize.height;
if (imageData.naturalWidth === 0 || imageData.naturalHeight === 0) {
return { maxPanX: 0, maxPanY: 0 };
@@ -231,10 +236,17 @@ function clampPan(imageData) {
}
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';
+ const cellSize = getCellSize();
+ img.container.style.left = img.xCell * cellSize.width + 'px';
+ img.container.style.top = img.yCell * cellSize.height + 'px';
+ img.container.style.width = img.widthCells * cellSize.width + 'px';
+ img.container.style.height = img.heightCells * cellSize.height + 'px';
+
+ // Store cell positions as CSS variables for print styles
+ img.container.style.setProperty('--x-cell', img.xCell);
+ img.container.style.setProperty('--y-cell', img.yCell);
+ img.container.style.setProperty('--width-cells', img.widthCells);
+ img.container.style.setProperty('--height-cells', img.heightCells);
// Update dimension labels
const widthLabel = img.container.querySelector('.dimension-label.width');
@@ -244,8 +256,8 @@ function updateImagePosition(img) {
// Recalculate baseScale if container size changed
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
- const containerWidth = img.widthCells * CELL_SIZE_PX;
- const containerHeight = img.heightCells * CELL_SIZE_PX;
+ const containerWidth = img.widthCells * cellSize.width;
+ const containerHeight = img.heightCells * cellSize.height;
// When rotated 90° or 270°, the image dimensions are effectively swapped
const isRotated90or270 = img.rotation % 180 !== 0;
@@ -267,6 +279,14 @@ function updateImagePosition(img) {
// Transform: translate from center (-50%, -50%), scale, rotate, then pan
// Pan is applied after rotation so it stays relative to the image's rotated state
imgElement.style.transform = `translate(-50%, -50%) scale(${totalScale}) rotate(${img.rotation}deg) translate(${img.panX}px, ${img.panY}px)`;
+
+ // Store transform parameters as CSS variables for print recalculation
+ img.container.style.setProperty('--user-scale', img.userScale);
+ img.container.style.setProperty('--rotation', img.rotation);
+ img.container.style.setProperty('--pan-x', img.panX);
+ img.container.style.setProperty('--pan-y', img.panY);
+ img.container.style.setProperty('--natural-width', img.naturalWidth);
+ img.container.style.setProperty('--natural-height', img.naturalHeight);
}
}
@@ -508,8 +528,9 @@ document.addEventListener('mousemove', (e) => {
const dx = e.clientX - dragState.startX;
const dy = e.clientY - dragState.startY;
- const dxCells = Math.round(dx / CELL_SIZE_PX);
- const dyCells = Math.round(dy / CELL_SIZE_PX);
+ const cellSize = getCellSize();
+ const dxCells = Math.round(dx / cellSize.width);
+ const dyCells = Math.round(dy / cellSize.height);
const newXCell = Math.max(0, Math.min(GRID_COLS - dragState.image.widthCells, dragState.startXCell + dxCells));
const newYCell = Math.max(0, Math.min(GRID_ROWS - dragState.image.heightCells, dragState.startYCell + dyCells));
@@ -523,8 +544,9 @@ document.addEventListener('mousemove', (e) => {
const dx = e.clientX - resizeState.startX;
const dy = e.clientY - resizeState.startY;
- const dxCells = Math.round(dx / CELL_SIZE_PX);
- const dyCells = Math.round(dy / CELL_SIZE_PX);
+ const cellSize = getCellSize();
+ const dxCells = Math.round(dx / cellSize.width);
+ const dyCells = Math.round(dy / cellSize.height);
const dir = resizeState.direction;
const img = resizeState.image;
@@ -585,3 +607,71 @@ document.addEventListener('mouseup', () => {
document.body.classList.remove('resizing');
}
});
+
+// Update all image positions when window resizes (for responsive scaling)
+window.addEventListener('resize', () => {
+ images.forEach(img => {
+ updateImagePosition(img);
+ });
+});
+
+// Store original transforms before print
+let beforePrintTransforms = [];
+
+// Recalculate image transforms for print
+window.addEventListener('beforeprint', () => {
+ const PRINT_CELL_SIZE_MM = 4;
+ const MM_TO_PX_PRINT = 96 / 25.4; // Conversion for print
+ const printCellSizePx = PRINT_CELL_SIZE_MM * MM_TO_PX_PRINT;
+
+ // Store current transforms and container styles
+ beforePrintTransforms = images.map(img => {
+ const imgElement = img.container.querySelector('img');
+ return {
+ container: img.container,
+ transform: imgElement ? imgElement.style.transform : '',
+ left: img.container.style.left,
+ top: img.container.style.top,
+ width: img.container.style.width,
+ height: img.container.style.height
+ };
+ });
+
+ images.forEach(img => {
+ const imgElement = img.container.querySelector('img');
+ if (imgElement && img.naturalWidth > 0 && img.naturalHeight > 0) {
+ // Calculate container size at print (in px)
+ const containerWidth = img.widthCells * printCellSizePx;
+ const containerHeight = img.heightCells * printCellSizePx;
+
+ // Calculate base scale for print
+ const isRotated90or270 = img.rotation % 180 !== 0;
+ const effectiveWidth = isRotated90or270 ? img.naturalHeight : img.naturalWidth;
+ const effectiveHeight = isRotated90or270 ? img.naturalWidth : img.naturalHeight;
+
+ const scaleX = containerWidth / effectiveWidth;
+ const scaleY = containerHeight / effectiveHeight;
+ const printBaseScale = Math.max(scaleX, scaleY);
+ const printTotalScale = printBaseScale * img.userScale;
+
+ // Apply transform for print
+ imgElement.style.transform = `translate(-50%, -50%) scale(${printTotalScale}) rotate(${img.rotation}deg) translate(${img.panX}px, ${img.panY}px)`;
+ }
+ });
+});
+
+// Restore screen transforms after print
+window.addEventListener('afterprint', () => {
+ // Restore the exact transforms and positions from before print
+ beforePrintTransforms.forEach(saved => {
+ const imgElement = saved.container.querySelector('img');
+ if (imgElement) {
+ imgElement.style.transform = saved.transform;
+ }
+ saved.container.style.left = saved.left;
+ saved.container.style.top = saved.top;
+ saved.container.style.width = saved.width;
+ saved.container.style.height = saved.height;
+ });
+ beforePrintTransforms = [];
+});
diff --git a/style.css b/style.css
@@ -5,6 +5,17 @@
--color-accent: #79aeea;
--color-white: white;
--shadow: #00000044;
+
+ /* Grid configuration */
+ --grid-cols: 50;
+ --grid-rows: 66;
+ --cell-size-mm: 4mm; /* Cell size for print */
+
+ /* Page dimensions: 8.5in = 216mm, 11in = 279.4mm */
+ /* Grid dimensions at print: 200mm x 264mm */
+ /* Grid as percentage of page: 200/216 = 92.59%, 264/279.4 = 94.48% */
+ --grid-width-percent: 92.59%;
+ --grid-height-percent: 94.48%;
}
* {
@@ -20,31 +31,39 @@ body {
align-items: flex-start;
min-height: 100vh;
padding: 20px;
+ overflow-x: hidden;
}
.page {
- width: 8.5in;
- height: 11in;
+ /* Scale down on narrow screens, but never larger than 8.5in */
+ max-width: min(8.5in, calc(100vw - 40px));
+ width: 100%;
+ /* Maintain 8.5:11 aspect ratio (11/8.5 = 1.294117647) */
+ aspect-ratio: 8.5 / 11;
background: var(--color-bg-page);
position: relative;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- transform-origin: top center;
+ display: flex;
+ align-items: center;
+ justify-content: center;
}
.grid {
- position: absolute;
- top: calc((11in - 264mm) / 2);
- left: calc((8.5in - 200mm) / 2);
+ /* Grid dimensions as percentage of page to scale responsively */
+ width: var(--grid-width-percent);
+ height: var(--grid-height-percent);
display: grid;
- grid-template-columns: repeat(50, 4mm);
- grid-template-rows: repeat(66, 4mm);
+ grid-template-columns: repeat(var(--grid-cols), 1fr);
+ grid-template-rows: repeat(var(--grid-rows), 1fr);
gap: 0;
line-height: 0;
+ position: relative; /* Image containers are positioned relative to grid */
+ /* Grid is centered within .page by the parent's flexbox */
}
.grid-cell {
- width: 4mm;
- height: 4mm;
+ width: 100%;
+ height: 100%;
box-sizing: border-box;
border-right: 1px dashed var(--color-grid-line);
border-bottom: 1px dashed var(--color-grid-line);
@@ -224,31 +243,34 @@ body.dragging * {
}
.page {
- width: 8.5in;
- height: 11in;
+ width: 8.5in !important;
+ max-width: 8.5in !important;
+ height: 11in !important;
box-shadow: none;
margin: 0;
padding: 0;
background: var(--color-white);
- transform: none !important;
- position: relative;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
}
.grid {
- position: absolute;
- top: calc((11in - 264mm) / 2);
- left: calc((8.5in - 200mm) / 2);
+ /* Use fixed dimensions for print: 50 cells * 4mm = 200mm, 66 cells * 4mm = 264mm */
+ width: 200mm !important;
+ height: 264mm !important;
display: grid;
- grid-template-columns: repeat(50, 4mm);
- grid-template-rows: repeat(66, 4mm);
+ grid-template-columns: repeat(var(--grid-cols), 1fr) !important;
+ grid-template-rows: repeat(var(--grid-rows), 1fr) !important;
+ position: relative !important;
}
.grid-cell {
border-right: 1px dashed var(--color-grid-line) !important;
border-bottom: 1px dashed var(--color-grid-line) !important;
box-sizing: border-box !important;
- width: 4mm !important;
- height: 4mm !important;
+ width: 100% !important;
+ height: 100% !important;
margin: 0 !important;
padding: 0 !important;
}
@@ -265,6 +287,11 @@ body.dragging * {
outline: none !important;
position: absolute !important;
background: var(--color-white) !important;
+ /* Recalculate positions using cell coordinates and print cell size */
+ left: calc(var(--x-cell) * var(--cell-size-mm)) !important;
+ top: calc(var(--y-cell) * var(--cell-size-mm)) !important;
+ width: calc(var(--width-cells) * var(--cell-size-mm)) !important;
+ height: calc(var(--height-cells) * var(--cell-size-mm)) !important;
}
.image-container .image-wrapper {