aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-31-timeline-face-prototype-6.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/2026-07-31-timeline-face-prototype-6.html')
-rw-r--r--docs/prototypes/2026-07-31-timeline-face-prototype-6.html151
1 files changed, 151 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-31-timeline-face-prototype-6.html b/docs/prototypes/2026-07-31-timeline-face-prototype-6.html
new file mode 100644
index 0000000..d3f1427
--- /dev/null
+++ b/docs/prototypes/2026-07-31-timeline-face-prototype-6.html
@@ -0,0 +1,151 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Timeline face — prototype 6</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a; --grid:#332f28; --grid-hi:#4a443a; --now:#c8452f;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ *{box-sizing:border-box}
+ html,body{margin:0;height:100%;background:var(--ground);overflow:hidden;
+ font-family:var(--mono)}
+ #face{position:fixed;inset:0;width:100vw;height:100vh;display:block}
+ #hud{position:fixed;left:10px;top:8px;z-index:5;display:flex;gap:8px;
+ align-items:center;font-size:11px}
+ button{font-family:var(--mono);font-size:11px;letter-spacing:.1em;
+ text-transform:uppercase;background:#16161699;color:var(--silver);
+ border:1px solid #2e2e2e;padding:5px 9px;cursor:pointer}
+ button.on{background:var(--gold);color:#111;border-color:var(--gold)}
+ select{font-family:var(--mono);font-size:11px;letter-spacing:.08em;
+ background:#161616;color:var(--silver);border:1px solid #2e2e2e;
+ padding:4px 6px}
+
+ /* One rule per primitive class the engine emits. All appearance lives here;
+ the engine decides what to draw and never how it looks. */
+ .meridian,.parallel{stroke:var(--grid);stroke-width:1;stroke-dasharray:7 7}
+ .meridian-anchor,.equator{stroke:var(--grid-hi);stroke-width:1}
+ .day-axis{stroke:var(--grid-hi);stroke-width:1.4}
+ line.now{stroke:var(--now);stroke-width:1.2;stroke-opacity:.6}
+ circle.now{fill:var(--now)}
+ text.now{fill:var(--now)}
+ .grid-label{fill:var(--steel)}
+ rect.event-next{fill:var(--gold-hi);fill-opacity:.95}
+ rect.event-future{fill:#ececec;fill-opacity:.5}
+ rect.event-past{fill:#686868;fill-opacity:.25}
+ text.event-next,text.event-next-above{fill:var(--gold-hi);font-weight:700}
+ text.event-future,text.event-future-above{fill:#ececec}
+ text.event-past,text.event-past-above{fill:#686868}
+ .leader{stroke:var(--grid-hi);stroke-width:1;stroke-opacity:.8}
+ .dot-home{fill:var(--gold)}
+ /* temporary: every city marked in red so their true positions are obvious */
+ .dot-city{fill:#e0392b}
+ .city-home{fill:var(--gold);font-weight:700}
+ .city-day{fill:var(--cream)}
+ .city-night{fill:var(--slate)}
+</style>
+</head>
+<body>
+<div id="hud">
+ <button id="bTrue" class="on">true latitude</button>
+ <button id="bRank">ranked</button>
+ <button id="bScrub">scrub</button>
+ <span style="width:14px"></span>
+ <span style="color:var(--steel)">home</span>
+ <select id="homes"></select>
+</div>
+<svg id="face"></svg>
+
+<script src="timeline-face-engine.js"></script>
+<script>
+// The renderer. It measures the font, asks the engine for a scene, and turns
+// primitives into SVG nodes. It holds no layout, no time and no geography --
+// all of that lives in the engine, where it is tested.
+"use strict";
+const NS = "http://www.w3.org/2000/svg";
+const svg = document.getElementById("face");
+let mode = "true", scrub = null, charRatio = 0.6, home = TF.HOME;
+
+const now = () => (scrub != null ? new Date(scrub) : new Date());
+
+/**
+ * Advance width of one glyph as a fraction of font size, measured against the
+ * font actually in use. Every width downstream is arithmetic from this, which
+ * is what lets the engine derive margins without touching the DOM.
+ */
+function measureChar() {
+ const probe = document.createElementNS(NS, "text");
+ probe.setAttribute("font-size", "100");
+ probe.setAttribute("x", "-9999");
+ probe.setAttribute("y", "-9999");
+ probe.textContent = "MMMMMMMMMM";
+ svg.appendChild(probe);
+ try {
+ const w = probe.getComputedTextLength();
+ if (w > 0) charRatio = w / 10 / 100;
+ } catch (e) { /* keep the default */ }
+ probe.remove();
+}
+
+const ATTRS = {
+ line: (p) => ({ x1: p.x1, y1: p.y1, x2: p.x2, y2: p.y2 }),
+ circle: (p) => ({ cx: p.cx, cy: p.cy, r: p.r }),
+ rect: (p) => ({ x: p.x, y: p.y, width: p.width, height: p.height }),
+ text: (p) => ({
+ x: p.x, y: p.y, "text-anchor": p.anchor,
+ "font-size": p.size, "letter-spacing": p.ls + "em",
+ }),
+};
+
+function toNode(p) {
+ const node = document.createElementNS(NS, p.kind);
+ const attrs = ATTRS[p.kind](p);
+ for (const k in attrs) if (attrs[k] != null) node.setAttribute(k, attrs[k]);
+ node.setAttribute("class", p.cls);
+ if (p.kind === "text") node.textContent = p.text;
+ return node;
+}
+
+function render() {
+ const width = svg.clientWidth || window.innerWidth;
+ const height = svg.clientHeight || window.innerHeight;
+ svg.setAttribute("viewBox", "0 0 " + width + " " + height);
+ const items = TF.scene({ width, height, ref: now(), mode, charRatio, home });
+ svg.replaceChildren(...items.map(toNode));
+}
+
+function pick(id, value) {
+ mode = value;
+ for (const b of ["bTrue", "bRank"]) {
+ document.getElementById(b).classList.toggle("on", b === id);
+ }
+ render();
+}
+document.getElementById("bTrue").onclick = () => pick("bTrue", "true");
+document.getElementById("bRank").onclick = () => pick("bRank", "rank");
+document.getElementById("bScrub").onclick = (e) => {
+ scrub = scrub == null ? Date.now() : null;
+ e.target.classList.toggle("on", scrub != null);
+};
+
+// One button per candidate home. Moving home moves the crossing with it, since
+// the roster reaches a fixed distance either side of wherever you are.
+const homeSel = document.getElementById("homes");
+TF.HOMES.forEach((h, i) => {
+ const o = document.createElement("option");
+ o.value = String(i);
+ o.textContent = h.label;
+ homeSel.appendChild(o);
+});
+homeSel.onchange = () => { home = TF.HOMES[+homeSel.value]; render(); };
+
+new ResizeObserver(render).observe(document.documentElement);
+setInterval(function () { if (scrub != null) scrub += 4 * 60000; render(); }, 250);
+measureChar();
+render();
+</script>
+</body>
+</html>