doxa-maps/library/scripts/posters/build-hd-gallery.mjs
Doxa Maps 2c397d25ef Source comments state design history and measurements as facts, without quoting or naming anyone
Comments in library/ and internal/ that quoted a person, attributed a decision to one, used first person,
named fleet agents or carried private host names now state the same technical fact in neutral words:
poster-render (including the comments it writes into generated posters), packet-render, the poster and
research scripts, PosterWall, DataTable, the legends, the layouts and the gates. The network-policy note
states the policy itself. Behaviour is unchanged: a few printed gate messages and reason labels drop an
agent name, the poster-render integrity pin follows its comment-only edit, and series.json changes only in
its _meta and _why notes. The historical poster design snapshots (library/scripts/posters/snapshots/),
which nothing reads, are removed.
2026-10-01 00:37:17 +00:00

150 lines
7.1 KiB
JavaScript
Raw Permalink 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.

// build-hd-gallery.mjs — an HD review page for the 28 posters.
//
// One page shows every poster's HD version, each with a copy-to-clipboard action.
//
// What it gives you per poster:
// • the FULL-RESOLUTION PNG (3000x3000), lazily loaded so the page opens fast
// • Copy image -> the actual PNG bitmap on the clipboard, ready to paste
// • Copy link -> its absolute URL
// • Open PNG / Open HTML -> the raw artefacts
//
// Clipboard note: navigator.clipboard.write() needs a SECURE CONTEXT. Browsers
// treat http://localhost as secure but NOT a bare LAN IP, so over
// http://<ip>:8090 the image copy is unavailable — the page detects that and
// says so rather than silently failing. Copy-link always works.
//
// node build-hd-gallery.mjs -> out/hd.html
import { readFileSync, writeFileSync, existsSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { regionFor, sectionsOf } from './poster-regions.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const OUT = join(__dirname, 'out');
const manifest = JSON.parse(readFileSync(join(OUT, 'manifest.json'), 'utf8'));
// Sections come from the SHARED region map (poster-regions.mjs), which the
// preview gallery uses too — three copies of this mapping would drift.
const posters = manifest.posters.map((p) => {
const png = join(OUT, p.png);
const kb = existsSync(png) ? Math.round(statSync(png).size / 1024) : 0;
return { ...p, region: regionFor(p.zoneKey), kb };
});
const ordered = sectionsOf(posters);
const esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
const cards = ordered.map(({ name, list, total }) => `
<section class="sec">
<h2>${esc(name)} <span class="meta">${list.length} maps · ${total} people groups</span></h2>
<div class="grid">
${list.map((p) => `
<figure class="card" data-png="${esc(p.png)}">
<a class="shot" href="${esc(p.png)}" target="_blank" rel="noopener">
<img loading="lazy" decoding="async" src="${esc(p.png)}" alt="${esc(p.title)}">
</a>
<figcaption>
<div class="ttl">${esc(p.title)}</div>
<div class="sub">${p.count} UPG · ${p.kb} KB · 3000×3000</div>
<div class="btns">
<button class="btn copy-img">Copy image</button>
<button class="btn copy-link">Copy link</button>
<a class="btn ghost" href="${esc(p.png)}" download>PNG</a>
<a class="btn ghost" href="${esc(p.html)}" target="_blank" rel="noopener">HTML</a>
</div>
</figcaption>
</figure>`).join('')}
</div>
</section>`).join('');
const html = `<!doctype html><html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>UPG-100 posters — HD review</title>
<style>
:root{--paper:#f7f4ec;--ink:#1c1c17;--dim:#8a8470;--zc:#6B4A78;--line:#e2dccc}
*{box-sizing:border-box}
body{margin:0;background:var(--paper);color:var(--ink);
font:15px/1.5 Lato,'Segoe UI',system-ui,sans-serif}
header{position:sticky;top:0;z-index:5;background:rgba(247,244,236,.96);
backdrop-filter:blur(8px);border-bottom:1px solid var(--line);padding:14px 22px}
h1{margin:0;font:700 20px/1.2 Georgia,serif;letter-spacing:-.01em}
.hint{margin-top:4px;font-size:12.5px;color:var(--dim)}
.warn{margin-top:6px;font-size:12.5px;color:#8a5a2a;display:none}
.sec{padding:26px 22px 6px}
h2{margin:0 0 14px;font:700 17px/1.2 Georgia,serif;border-bottom:1px solid var(--line);padding-bottom:7px}
.meta{font:400 12.5px Lato,sans-serif;color:var(--dim);margin-left:8px}
.grid{display:grid;gap:22px;grid-template-columns:repeat(auto-fill,minmax(330px,1fr))}
.card{margin:0;background:#fff;border:1px solid var(--line);border-radius:10px;overflow:hidden;
box-shadow:0 1px 3px rgba(0,0,0,.05)}
.shot{display:block;aspect-ratio:1/1;background:#faf8f2}
.shot img{width:100%;height:100%;object-fit:contain;display:block}
figcaption{padding:11px 13px 13px;border-top:1px solid var(--line)}
.ttl{font-weight:700;font-size:14.5px;line-height:1.25}
.sub{font-size:12px;color:var(--dim);margin-top:3px;font-variant-numeric:tabular-nums}
.btns{display:flex;gap:7px;margin-top:10px;flex-wrap:wrap}
.btn{font:600 12px Lato,sans-serif;padding:6px 11px;border-radius:6px;cursor:pointer;
border:1px solid var(--zc);background:var(--zc);color:#fff;text-decoration:none;display:inline-block}
.btn.ghost{background:transparent;color:var(--zc)}
.btn[disabled]{opacity:.4;cursor:not-allowed}
.btn.ok{background:#2f7d4f;border-color:#2f7d4f}
.btn.err{background:#9b3b3b;border-color:#9b3b3b}
</style></head><body>
<header>
<h1>UPG-100 posters — HD review</h1>
<div class="hint">${posters.length} posters · 3000×3000 PNG · click a poster to open it full size</div>
<div class="warn" id="warn">Image-copy needs a secure context — open this page via <b>localhost</b>
(or HTTPS) to enable it. “Copy link” works anywhere.</div>
</header>
${cards}
<script>
// Image copy requires a secure context AND ClipboardItem support.
var CAN_COPY_IMG = window.isSecureContext && !!(navigator.clipboard && window.ClipboardItem);
if (!CAN_COPY_IMG) {
document.getElementById('warn').style.display = 'block';
document.querySelectorAll('.copy-img').forEach(function (b) { b.disabled = true; });
}
function flash(btn, text, cls) {
var old = btn.textContent;
btn.textContent = text; btn.classList.add(cls);
setTimeout(function () { btn.textContent = old; btn.classList.remove(cls); }, 1400);
}
document.addEventListener('click', function (e) {
var btn = e.target.closest('.btn'); if (!btn) return;
var card = e.target.closest('.card'); if (!card) return;
var png = card.getAttribute('data-png');
if (btn.classList.contains('copy-link')) {
var url = new URL(png, location.href).href;
// execCommand fallback: navigator.clipboard is also gated on secure context.
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(url).then(function(){ flash(btn,'Copied','ok'); },
function(){ flash(btn,'Failed','err'); });
} else {
var ta = document.createElement('textarea');
ta.value = url; ta.style.position='fixed'; ta.style.opacity='0';
document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); flash(btn,'Copied','ok'); }
catch (err) { flash(btn,'Failed','err'); }
document.body.removeChild(ta);
}
return;
}
if (btn.classList.contains('copy-img')) {
if (!CAN_COPY_IMG) return;
btn.textContent = 'Copying…';
// Fetch the real PNG bytes; the clipboard needs a Blob, not the <img>.
fetch(png).then(function (r) { return r.blob(); }).then(function (blob) {
return navigator.clipboard.write([ new ClipboardItem({ 'image/png': blob }) ]);
}).then(function () { flash(btn, 'Copied', 'ok'); })
.catch(function () { flash(btn, 'Failed', 'err'); });
}
});
</script>
</body></html>`;
writeFileSync(join(OUT, 'hd.html'), html);
console.log(`[hd-gallery] ${posters.length} posters -> out/hd.html`);