sbuffkin_hexmaker/dev/coord-slip-sandbox.html
isaprettycoolguy@protonmail.com 0e65565e25 fix: coord labels no longer slip up-left during zoom bake
The bug the user has been chasing for several sessions. Sandbox
repro at dev/coord-slip-sandbox.html, snapshot-coord-slip.mjs
captures the half-baked frame so we can SEE the slip.

Root cause: bakeZoom mutates the viewport font-size mid-function,
which triggers a layout reflow growing every em-based hex
dimension. The path-overlay SVG holds coord labels at pixel
positions computed against the PRE-bake hex sizes, so any paint
frame between the font-size change and the SVG rebuild shows the
labels at where the hexes USED to be — visibly shifted up-left of
the now-larger hexes. updatePathOverlay() further down in bakeZoom
eventually fixes it ("snap back into place"), but the intermediate
slip-then-snap is what the user reported across multiple zoom
sessions.

Fix: tear down the path / faction / region SVGs at the START of
bakeZoom, before the font-size mutation. The CSS rule
`.duckmage-svg-labels-active .duckmage-hex-label { visibility:
hidden }` is keyed off a class on the viewport — that class gets
removed alongside the SVG, so the intermediate paint falls back to
the HTML `.duckmage-hex-label` spans. Those live inside each hex's
own DOM tree, so they always track the hex's layout perfectly
across font-size changes. No slip.

The overlays are then re-rendered later in the function via the
existing updatePathOverlay() / updateFactionOverlay() /
updateRegionOverlay() / updateTokenLayer() calls. Net result: zero
extra rebuild work, just earlier tear-down.

Sandbox confirms:
  buggy-mid: SVG labels visibly shifted up-left of small hexes
  fixed-mid: HTML labels inside hexes, tracking correctly

351 unit tests / 18 e2e pass.
2026-06-21 11:09:12 -04:00

197 lines
6.6 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Coord-slip during bakeZoom — repro</title>
<style>
:root { --background-secondary: #2a2a2a; }
body { background: #1e1e1e; color: #ddd; font-family: system-ui, sans-serif; margin: 0; padding: 8px; }
h1 { font-size: 0.95em; margin: 0 0 8px; }
.controls { display: flex; gap: 8px; margin: 0 0 8px; }
.controls button { background: #3b82f6; color: white; border: none; border-radius: 4px; padding: 6px 14px; cursor: pointer; }
#log { font-family: monospace; font-size: 0.85em; color: #999; padding: 6px; background: #0c111a; border-radius: 3px; margin: 0 0 8px; min-height: 36px; white-space: pre; }
.viewport {
position: relative;
width: 700px;
height: 480px;
background: #0c111a;
overflow: hidden;
}
.grid-wrap {
position: absolute;
top: 0;
left: 0;
padding: 1em;
transform-origin: 0 0;
transform: scale(var(--vp-scale, 1));
}
.grid {
position: relative;
display: flex;
flex-direction: column;
}
.row { display: flex; flex-wrap: nowrap; margin-bottom: calc(var(--hex-size, 2.2em) * -0.5); }
.row.odd { margin-left: calc(var(--hex-size, 2.2em) * 1.732 / 2); }
.hex {
--hex-size: 2.2em;
width: calc(var(--hex-size) * 1.732);
height: calc(var(--hex-size) * 2);
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background: rgba(120, 200, 80, 0.5);
margin: 0.15em;
position: relative;
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 11px; color: #1a1a1a;
}
.hex-label {
pointer-events: none;
font-weight: 600;
/* Hidden when SVG labels are active (path SVG present) — same rule
as the plugin uses. */
}
.grid-wrap.svg-active .hex-label { visibility: hidden; }
.path-svg {
position: absolute;
top: 0; left: 0;
pointer-events: none;
z-index: 5;
}
.svg-coord {
fill: #fff;
stroke: rgba(0,0,0,0.85);
stroke-width: 2;
paint-order: stroke;
font-size: 11px;
font-weight: 600;
font-family: system-ui, sans-serif;
}
</style>
</head>
<body>
<h1>Coord-slip during bakeZoom — buggy vs fixed order</h1>
<div class="controls">
<button id="buggy-bake">Bake (buggy: SVG torn down LAST)</button>
<button id="fixed-bake">Bake (fixed: SVG torn down FIRST)</button>
<button id="reset">Reset</button>
</div>
<div id="log">click "Bake" then look at the screenshot for the half-baked frame</div>
<div class="viewport" id="viewport">
<div class="grid-wrap svg-active" id="gridWrap">
<div class="grid" id="grid"></div>
</div>
</div>
<script>
const ROWS = 4, COLS = 5;
let zoom = 2; // start "zoomed in"
let fontSize = 16; // pre-bake font-size
const grid = document.getElementById("grid");
const gridWrap = document.getElementById("gridWrap");
const viewport = document.getElementById("viewport");
const log = document.getElementById("log");
function buildGrid() {
grid.replaceChildren();
for (let r = 0; r < ROWS; r++) {
const row = document.createElement("div");
row.className = "row" + (r % 2 === 1 ? " odd" : "");
for (let c = 0; c < COLS; c++) {
const hex = document.createElement("div");
hex.className = "hex";
hex.dataset.x = c; hex.dataset.y = r;
const label = document.createElement("span");
label.className = "hex-label";
label.textContent = `${c},${r}`;
hex.appendChild(label);
row.appendChild(hex);
}
grid.appendChild(row);
}
}
function applyTransform() {
gridWrap.style.setProperty("--vp-scale", String(zoom));
gridWrap.style.fontSize = `${fontSize}px`;
}
function renderPathSvg() {
grid.querySelector(".path-svg")?.remove();
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.classList.add("path-svg");
svg.setAttribute("width", String(grid.offsetWidth));
svg.setAttribute("height", String(grid.offsetHeight));
grid.querySelectorAll(".hex").forEach((hex) => {
let ox = hex.offsetWidth / 2, oy = hex.offsetHeight / 2;
let cur = hex;
while (cur && cur !== grid) {
ox += cur.offsetLeft; oy += cur.offsetTop;
cur = cur.offsetParent;
}
const t = document.createElementNS("http://www.w3.org/2000/svg", "text");
t.setAttribute("x", String(ox));
t.setAttribute("y", String(oy + hex.offsetHeight * 0.28));
t.setAttribute("text-anchor", "middle");
t.setAttribute("class", "svg-coord");
t.textContent = `${hex.dataset.x},${hex.dataset.y}`;
svg.appendChild(t);
});
grid.appendChild(svg);
gridWrap.classList.add("svg-active");
}
function reset() {
zoom = 2; fontSize = 16;
applyTransform();
buildGrid();
renderPathSvg();
log.textContent = "reset — zoomed in 2× via CSS transform, SVG labels active";
}
// BUGGY bake: change font-size first, rebuild SVG at the end.
document.getElementById("buggy-bake").addEventListener("click", () => {
const F = zoom;
// 1. font-size mutated (hexes grow in layout)
fontSize *= F;
gridWrap.style.fontSize = `${fontSize}px`;
// 2. transform reset to scale 1
zoom = 1;
gridWrap.style.setProperty("--vp-scale", "1");
// Force a paint here to simulate the worst case
void grid.offsetWidth;
// 3. SVG rebuilt (now with new sizes)
setTimeout(() => {
renderPathSvg();
log.textContent = "buggy bake done. Between font-size change and SVG rebuild, OLD SVG labels were at OLD positions → slip visible.";
}, 300);
});
// FIXED bake: tear down SVG FIRST so HTML labels take over during the transition.
document.getElementById("fixed-bake").addEventListener("click", () => {
const F = zoom;
// 1. TEAR DOWN old SVG + remove the class so HTML labels become visible.
grid.querySelector(".path-svg")?.remove();
gridWrap.classList.remove("svg-active");
// 2. font-size mutated (hexes grow). HTML labels are inside hexes — they
// track perfectly. User sees correctly-placed HTML labels during the
// in-between frame.
fontSize *= F;
gridWrap.style.fontSize = `${fontSize}px`;
// 3. transform reset to scale 1.
zoom = 1;
gridWrap.style.setProperty("--vp-scale", "1");
void grid.offsetWidth;
// 4. SVG rebuilt with new positions.
setTimeout(() => {
renderPathSvg();
log.textContent = "fixed bake done. SVG was torn down before font-size change → HTML labels took over for the transition → no slip.";
}, 300);
});
document.getElementById("reset").addEventListener("click", reset);
reset();
</script>
</body>
</html>