Better grid resizing behavior.

This commit is contained in:
Yaro Kasear 2025-12-12 12:10:14 -06:00
parent 3f4aee73a3
commit 0b85715c1e

View file

@ -2,22 +2,19 @@
{% set dot_size = [grid_size * 1.25, 32]|max|int %} {% set dot_size = [grid_size * 1.25, 32]|max|int %}
{% block style %} {% block style %}
:root {
--grid: {{ grid_size }}px;
}
#grid { #gridWrap {
cursor: crosshair;
height: 80vh;
width: 100%; width: 100%;
touch-action: none; height: 80vh;
position: relative;
} }
@supports (height: calc(round(nearest, 80vh, {{ grid_size }}px))) {
#grid { #grid {
height: calc(round(nearest, 80vh, var(--grid)) + 1px); position: relative;
width: calc(round(nearest, 100%, var(--grid)) + 1px); cursor: crosshair;
} width: 100%;
height: 100%;
touch-action: none;
} }
#toolBar { #toolBar {
@ -46,6 +43,7 @@
{% block main %} {% block main %}
<div class="container"> <div class="container">
<div id="gridWrap">
<div id="grid" class="position-relative overflow-hidden"> <div id="grid" class="position-relative overflow-hidden">
<div id="toolBar" <div id="toolBar"
class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1 align-items-center flex-nowrap overflow-auto"> class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1 align-items-center flex-nowrap overflow-auto">
@ -185,6 +183,7 @@
<canvas id="overlay" class="position-absolute w-100 h-100"></canvas> <canvas id="overlay" class="position-absolute w-100 h-100"></canvas>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}
{% block script %} {% block script %}
@ -199,6 +198,7 @@ const gridEl = document.getElementById('grid');
const importButtonEl = document.getElementById('importButton'); const importButtonEl = document.getElementById('importButton');
const importEl = document.getElementById('import'); const importEl = document.getElementById('import');
const gridSizeEl = document.getElementById('gridSize'); const gridSizeEl = document.getElementById('gridSize');
const gridWrapEl = document.getElementById('gridWrap');
let gridSize = Number(gridSizeEl.value) || {{ grid_size }}; let gridSize = Number(gridSizeEl.value) || {{ grid_size }};
let dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); let dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@ -210,8 +210,11 @@ let selectedColor;
let currentShape = null; let currentShape = null;
let shapes = loadShapes(); let shapes = loadShapes();
const ro = new ResizeObserver(() => resizeAndSetupCanvas()); let sizingRAF = 0;
ro.observe(gridEl); let lastApplied = { w: 0, h: 0 };
const ro = new ResizeObserver(scheduleSnappedGridSize);
ro.observe(gridWrapEl);
const savedTool = localStorage.getItem('gridTool'); const savedTool = localStorage.getItem('gridTool');
if (savedTool) { if (savedTool) {
@ -228,12 +231,43 @@ window.addEventListener('resize', resizeAndSetupCanvas);
setGrid(); setGrid();
function snapDown(n, step) {
return Math.floor(n / step) * step;
}
function applySnappedGridSize() {
sizingRAF = 0;
const grid = gridSize;
if (!Number.isFinite(grid) || grid < 1) return;
const w = gridWrapEl.clientWidth;
const h = gridWrapEl.clientHeight;
const snappedW = snapDown(w, grid);
const snappedH = snapDown(h, grid);
if (snappedW === lastApplied.w && snappedH === lastApplied.h) return;
lastApplied = { w: snappedW, h: snappedH };
gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`;
resizeAndSetupCanvas();
}
function scheduleSnappedGridSize() {
if (sizingRAF) return;
sizingRAF = requestAnimationFrame(applySnappedGridSize);
}
function applyGridSize(newSize) { function applyGridSize(newSize) {
const n = Number(newSize); const n = Number(newSize);
if (!Number.isFinite(n) || n < 1) return; if (!Number.isFinite(n) || n < 1) return;
gridSize = n; gridSize = n;
document.documentElement.style.setProperty('--grid', `${gridSize}px`); snapSizeToGrid();
dotSize = Math.floor(Math.max(gridSize * 1.25, 32)); dotSize = Math.floor(Math.max(gridSize * 1.25, 32));
@ -241,7 +275,7 @@ function applyGridSize(newSize) {
dotSVGEl.setAttribute('height', dotSize); dotSVGEl.setAttribute('height', dotSize);
setGrid(); setGrid();
resizeAndSetupCanvas(); scheduleSnappedGridSize();
} }
function pxToGrid(v) { function pxToGrid(v) {
@ -274,7 +308,29 @@ function setActiveType(typeId) {
} }
} }
function snapSizeToGrid() {
const grid = gridSize;
const rect = gridEl.getBoundingClientRect();
const targetW = rect.width;
const targetH = rect.height;
const snappedW = Math.floor(targetW / grid) * grid + 1;
const snappedH = Math.floor(targetH / grid) * grid + 1;
gridEl.style.width = `${snappedW}px`;
gridEl.style.height = `${snappedH}px`;
}
function snapToGrid(x, y) { function snapToGrid(x, y) {
/*
For portability, we do not allow pixel coordinates in the data model
and only use pixels for rendering. We display both spaces on the screen
to ensure no matter the mode, the user is reasoning in the same two
coordinate spaces as they need. Thus, snapping will happen even if
the tool doesn't use it.
*/
const rect = gridEl.getBoundingClientRect(); const rect = gridEl.getBoundingClientRect();
const clampedX = Math.min(Math.max(x, rect.left), rect.right); const clampedX = Math.min(Math.max(x, rect.left), rect.right);
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom); const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
@ -344,6 +400,7 @@ function normalizeLine(shape) {
} }
function resizeAndSetupCanvas() { function resizeAndSetupCanvas() {
snapSizeToGrid();
dpr = window.devicePixelRatio || 1; dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect(); const rect = canvasEl.getBoundingClientRect();
@ -581,9 +638,6 @@ document.addEventListener('keydown', (e) => {
}); });
gridEl.addEventListener('pointermove', (e) => { gridEl.addEventListener('pointermove', (e) => {
// Note to certain minds that keep thinking we want to mix grid and pixel coordinates.
// No. No we do not. Pixels are not portable. Stop it.
if (!ctx) return; if (!ctx) return;
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY); const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);