mirror of
https://github.com/sbuffkin/hexmaker.git
synced 2026-07-22 14:30:24 +00:00
Map grid: the per-hex render loop called getTerrainFromFile + getIconOverrideFromFile + getGmIconsFromFile, each re-running getAbstractFileByPath + getFileCache — 3 identical cache lookups per hex (~11.5k redundant ops on a 3843-hex map). Fetch frontmatter once and feed three new pure extractors (terrainFromFm/iconOverrideFromFm/gmIconsFromFm). Reuse the bg-image layer across renders (bgLayerEl/bgLayerSrc) so renderGrid()'s viewportEl.empty() no longer re-decodes the image each time. Tag painted hexes with .duckmage-hex-painted (setHexColor) and switch the bg-image empty-hex rule to a class selector instead of a slower :not([style*="background-color"]) attribute-substring match. Table view: memoise the name→entry palette Map per region instead of rebuilding it per row, and re-filter only the rows just filled in each batch (applyFilters(rows?)) instead of re-scanning all rows after every batch. Adds dev/bg-hex-recalc-bench + unit tests for the new pure extractors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4.9 KiB
HTML
125 lines
4.9 KiB
HTML
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<style>
|
||
body { margin: 0; font-family: monospace; }
|
||
.viewport { position: relative; will-change: transform; }
|
||
.bg-layer {
|
||
position: absolute; top: 0; left: 0; transform-origin: 0 0;
|
||
transform: scale(4.84); z-index: 0; pointer-events: none;
|
||
}
|
||
.bg-layer img { display: block; max-width: none; max-height: none; }
|
||
.grid { position: relative; display: flex; flex-direction: row; align-items: flex-start; }
|
||
.col { display: flex; flex-direction: column; margin-right: -1.1em; }
|
||
.col-offset { margin-top: 1.9em; }
|
||
.hex {
|
||
width: 4.4em; height: 3.81em; font-size: 8px;
|
||
clip-path: polygon(25% 0%, 75% 0%, 100% 50%, 75% 100%, 25% 100%, 0% 50%);
|
||
background-color: #2a4d3a;
|
||
position: relative;
|
||
}
|
||
.hex:hover::after { content: ""; position: absolute; inset: 0; background: rgba(255,255,255,.2); }
|
||
|
||
/* ── The two competing rules — only ONE is active at a time ── */
|
||
/* A: attribute-substring selector (current code) */
|
||
.mode-attr.has-bg-image .hex:not([style*="background-color"]) { background-color: transparent; }
|
||
/* B: class selector (proposed) */
|
||
.mode-class.has-bg-image .hex:not(.has-terrain) { background-color: transparent; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<script>
|
||
const COLS = 63, ROWS = 61; // chult map dimensions
|
||
const TERRAIN_FRACTION = 0.55; // ~half painted, rest show the image
|
||
const HEX_COUNT = COLS * ROWS;
|
||
const root = document.getElementById("root");
|
||
|
||
// A 1107×1499 image analogue (canvas data URL — comparable paint cost, no fetch).
|
||
const c = document.createElement("canvas");
|
||
c.width = 1107; c.height = 1499;
|
||
const ctx = c.getContext("2d");
|
||
const grd = ctx.createLinearGradient(0, 0, 1107, 1499);
|
||
grd.addColorStop(0, "#3b5"); grd.addColorStop(1, "#247");
|
||
ctx.fillStyle = grd; ctx.fillRect(0, 0, 1107, 1499);
|
||
const IMG_URL = c.toDataURL("image/png");
|
||
|
||
let painted = 0;
|
||
// Build the full map subtree once, fresh — this is what renderGrid() does on
|
||
// open: empty() the viewport then recreate everything. `config`:
|
||
// "none" → no bg image, no has-bg-image class (the "fast" baseline)
|
||
// "attr" → bg image + has-bg-image + the current [style*=…] empty-hex rule
|
||
// "class" → bg image + has-bg-image + the proposed .has-terrain rule
|
||
function build(config) {
|
||
root.className = "";
|
||
if (config !== "none") root.classList.add("has-bg-image", config === "attr" ? "mode-attr" : "mode-class");
|
||
root.replaceChildren();
|
||
const viewport = document.createElement("div");
|
||
viewport.className = "viewport";
|
||
if (config !== "none") {
|
||
const bg = document.createElement("div");
|
||
bg.className = "bg-layer";
|
||
const img = document.createElement("img");
|
||
img.src = IMG_URL;
|
||
bg.appendChild(img);
|
||
viewport.appendChild(bg);
|
||
}
|
||
const grid = document.createElement("div");
|
||
grid.className = "grid";
|
||
painted = 0;
|
||
for (let x = 0; x < COLS; x++) {
|
||
const col = document.createElement("div");
|
||
col.className = "col" + (x % 2 ? " col-offset" : "");
|
||
for (let y = 0; y < ROWS; y++) {
|
||
const hex = document.createElement("div");
|
||
hex.className = "hex";
|
||
if (((x * 31 + y * 17) % 100) / 100 < TERRAIN_FRACTION) {
|
||
hex.style.backgroundColor = "#2a4d3a"; // inline → matches [style*=…]
|
||
hex.classList.add("has-terrain"); // class → matches :not(.has-terrain)
|
||
painted++;
|
||
}
|
||
col.appendChild(hex);
|
||
}
|
||
grid.appendChild(col);
|
||
}
|
||
viewport.appendChild(grid);
|
||
root.appendChild(viewport);
|
||
}
|
||
|
||
// Time a full rebuild + forced style/layout flush — the open-time cost.
|
||
function bench(config, iters) {
|
||
for (let i = 0; i < 3; i++) { build(config); void document.body.offsetHeight; } // warm
|
||
const samples = [];
|
||
for (let i = 0; i < iters; i++) {
|
||
const t0 = performance.now();
|
||
build(config);
|
||
void document.body.offsetHeight; // force recalc + layout
|
||
samples.push(performance.now() - t0);
|
||
}
|
||
samples.sort((a, b) => a - b);
|
||
return {
|
||
median: samples[Math.floor(samples.length / 2)],
|
||
mean: samples.reduce((s, v) => s + v, 0) / samples.length,
|
||
p95: samples[Math.floor(samples.length * 0.95)],
|
||
};
|
||
}
|
||
|
||
window.__runBench = (iters = 40) => {
|
||
const none1 = bench("none", iters);
|
||
const attr1 = bench("attr", iters);
|
||
const cls1 = bench("class", iters);
|
||
const none2 = bench("none", iters);
|
||
const attr2 = bench("attr", iters);
|
||
const cls2 = bench("class", iters);
|
||
const avg = (a, b, k) => (a[k] + b[k]) / 2;
|
||
const pack = (a, b) => ({ median: avg(a, b, "median"), mean: avg(a, b, "mean"), p95: avg(a, b, "p95") });
|
||
return {
|
||
hexCount: HEX_COUNT, painted, empty: HEX_COUNT - painted, iters,
|
||
none: pack(none1, none2), attr: pack(attr1, attr2), cls: pack(cls1, cls2),
|
||
};
|
||
};
|
||
document.title = "ready";
|
||
</script>
|
||
</body>
|
||
</html>
|