aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-23-world-clock-vertical-centered.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/2026-07-23-world-clock-vertical-centered.html')
-rw-r--r--docs/prototypes/2026-07-23-world-clock-vertical-centered.html191
1 files changed, 191 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-centered.html b/docs/prototypes/2026-07-23-world-clock-vertical-centered.html
new file mode 100644
index 0000000..114c4b8
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-vertical-centered.html
@@ -0,0 +1,191 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — vertical, centered spine, both sides</title>
+<style>
+ :root{
+ --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385;
+ --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d;
+ --night:#39435a; --line:#2c2823;
+ --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace;
+ }
+ * { box-sizing: border-box; }
+ html, body { margin:0; height:100%; }
+ body {
+ background:var(--ground); font-family:var(--mono); color:var(--silver);
+ cursor:none; overflow:hidden; position:relative;
+ }
+ .masthead {
+ position:absolute; top:30px; left:50%; transform:translateX(-50%);
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; z-index:3; text-align:center;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ .mode { position:absolute; top:30px; right:34px; color:var(--dim);
+ font-size:11px; letter-spacing:0.2em; z-index:3; }
+ svg { position:absolute; inset:0; width:100%; height:100%; z-index:1; }
+ .lbl { position:absolute; z-index:2; transform:translateY(-50%);
+ white-space:nowrap; }
+ .lbl.r { text-align:left; }
+ .lbl.l { text-align:right; transform:translate(-100%,-50%); }
+ .lbl .city { color:var(--steel); font-size:12px; letter-spacing:0.2em;
+ text-transform:uppercase; }
+ .lbl .time { color:var(--cream); font-size:26px; letter-spacing:0.04em;
+ line-height:1.05; }
+ .lbl .meta { color:var(--dim); font-size:11px; letter-spacing:0.11em; }
+ .lbl .meta .tz { color:var(--steel); }
+ .lbl.night .time { color:var(--slate); }
+ .lbl.night .city::after { content:" ☾"; color:var(--night); }
+ .lbl.home .time { color:var(--gold-hi); }
+ .lbl.home .city { color:var(--gold); }
+</style>
+</head>
+<body>
+ <div class="masthead"><b>DUPRE</b> · WORLD WATCH &nbsp;·&nbsp; EAST → WEST</div>
+ <div class="mode" id="mode">12H · click to toggle</div>
+ <svg id="wires"></svg>
+ <div id="labels"></div>
+<script>
+const ROSTER = [
+ { tz:"Pacific/Auckland", label:"Wellington", lon:174.78 },
+ { tz:"Australia/Sydney", label:"Sydney", lon:151.21 },
+ { tz:"Asia/Tokyo", label:"Tokyo", lon:139.69 },
+ { tz:"Asia/Shanghai", label:"Shanghai", lon:121.47 },
+ { tz:"Asia/Kolkata", label:"Delhi", lon:77.21 },
+ { tz:"Asia/Yerevan", label:"Yerevan", lon:44.51 },
+ { tz:"Europe/Istanbul", label:"Istanbul", lon:28.98 },
+ { tz:"Europe/Athens", label:"Athens", lon:23.73 },
+ { tz:"Europe/Paris", label:"Paris", lon:2.35 },
+ { tz:"Europe/London", label:"London", lon:-0.13 },
+ { tz:"America/New_York", label:"New York", lon:-74.01 },
+ { tz:"America/Chicago", label:"New Orleans", lon:-90.07 },
+ { tz:"America/Los_Angeles", label:"Berkeley", lon:-122.27},
+ { tz:"America/Anchorage", label:"Anchorage", lon:-149.90},
+ { tz:"Pacific/Honolulu", label:"Honolulu", lon:-157.86},
+];
+const TZNAME = {
+ "Pacific/Honolulu":"US Hawaii", "America/Anchorage":"US Alaska",
+ "America/Los_Angeles":"US Pacific", "America/Chicago":"US Central",
+ "America/New_York":"US Eastern", "Europe/London":"UK",
+ "Europe/Paris":"Central Europe", "Europe/Athens":"Eastern Europe",
+ "Europe/Istanbul":"Türkiye", "Asia/Yerevan":"Armenia",
+ "Asia/Kolkata":"India", "Asia/Shanghai":"China", "Asia/Tokyo":"Japan",
+ "Australia/Sydney":"Australia East", "Pacific/Auckland":"New Zealand",
+};
+function tzName(tz) {
+ if (TZNAME[tz]) return TZNAME[tz];
+ try {
+ const p = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date())
+ .find((x) => x.type === "timeZoneName");
+ return p ? p.value : tz.split("/").pop().replace(/_/g, " ");
+ } catch (e) { return tz.split("/").pop().replace(/_/g, " "); }
+}
+const home = Intl.DateTimeFormat().resolvedOptions().timeZone;
+let hour12 = true;
+
+/* Spine down the centre; labels alternate to left and right so near-longitude
+ cities split across the line instead of piling into one far column. */
+const TOP = 104, BOT = 104, MIN_LEAD = 7, COL_W = 224, ROW_H = 68;
+const lons = ROSTER.map((c) => c.lon);
+const maxLon = Math.max(...lons), minLon = Math.min(...lons);
+const svgNS = "http://www.w3.org/2000/svg";
+const wires = document.getElementById("wires");
+const labels = document.getElementById("labels");
+let nodes = [];
+
+function layout() {
+ const H = window.innerHeight, W = window.innerWidth, cx = Math.round(W / 2);
+ const drawH = H - TOP - BOT;
+ wires.innerHTML = ""; labels.innerHTML = ""; nodes = [];
+
+ const spine = document.createElementNS(svgNS, "line");
+ spine.setAttribute("x1", cx); spine.setAttribute("x2", cx);
+ spine.setAttribute("y1", TOP - 18); spine.setAttribute("y2", H - BOT + 18);
+ spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2");
+ wires.appendChild(spine);
+
+ // slots keyed "side:col" -> last y placed there
+ const lastY = {};
+ ROSTER.forEach((c, i) => {
+ const y = TOP + (maxLon - c.lon) / (maxLon - minLon) * drawH;
+ // try sides in an order that alternates by index, then widen columns
+ const order = [];
+ for (let col = 0; col < 6; col++) {
+ const first = (i % 2 === 0) ? "r" : "l";
+ const second = first === "r" ? "l" : "r";
+ order.push(first + ":" + col, second + ":" + col);
+ }
+ let slot = order.find((s) => lastY[s] === undefined || y - lastY[s] >= ROW_H)
+ || order[order.length - 1];
+ lastY[slot] = y;
+ const [side, colStr] = slot.split(":");
+ const col = parseInt(colStr, 10);
+
+ const labelX = side === "r"
+ ? cx + MIN_LEAD + col * COL_W
+ : cx - MIN_LEAD - col * COL_W;
+
+ const lead = document.createElementNS(svgNS, "line");
+ lead.setAttribute("x1", cx); lead.setAttribute("x2", labelX);
+ lead.setAttribute("y1", y); lead.setAttribute("y2", y);
+ lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4");
+ wires.appendChild(lead);
+ const dot = document.createElementNS(svgNS, "circle");
+ dot.setAttribute("cx", cx); dot.setAttribute("cy", y);
+ dot.setAttribute("r", "3.5");
+ dot.setAttribute("fill", c.tz === home ? "var(--gold)" : "var(--steel)");
+ wires.appendChild(dot);
+
+ const lbl = document.createElement("div");
+ lbl.className = "lbl " + side;
+ lbl.style.left = (side === "r" ? labelX + 8 : labelX - 8) + "px";
+ lbl.style.top = y + "px";
+ lbl.innerHTML = "<div class='city'>" + c.label + "</div>"
+ + "<div class='time'></div><div class='meta'></div>";
+ labels.appendChild(lbl);
+ nodes.push({ c, lbl,
+ time: lbl.querySelector(".time"), meta: lbl.querySelector(".meta") });
+ });
+}
+function fmt(tz) {
+ const now = new Date();
+ const p = new Intl.DateTimeFormat("en-US", {
+ timeZone: tz, hour12, weekday: "short",
+ day: "numeric", hour: hour12 ? "numeric" : "2-digit", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = parseInt(new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value, 10) % 24;
+ const time = hour12
+ ? g("hour") + ":" + g("minute") + " " + g("dayPeriod")
+ : g("hour") + ":" + g("minute");
+ return { time, date: g("weekday") + " " + g("day"), night: h24 >= 19 || h24 < 7 };
+}
+function tick() {
+ for (const e of nodes) {
+ const f = fmt(e.c.tz);
+ e.time.textContent = f.time;
+ e.meta.innerHTML = f.date + " &nbsp;·&nbsp; <span class='tz'>"
+ + tzName(e.c.tz) + "</span>";
+ const isHome = e.c.tz === home;
+ e.lbl.classList.toggle("home", isHome);
+ e.lbl.classList.toggle("night", f.night && !isHome);
+ }
+}
+document.body.addEventListener("click", () => {
+ hour12 = !hour12;
+ document.getElementById("mode").textContent =
+ (hour12 ? "12H" : "24H") + " · click to toggle";
+ tick();
+});
+layout(); tick();
+setInterval(tick, 5000);
+let rt; window.addEventListener("resize", () => {
+ clearTimeout(rt); rt = setTimeout(() => { layout(); tick(); }, 150);
+});
+</script>
+</body>
+</html>