aboutsummaryrefslogtreecommitdiff
path: root/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html
diff options
context:
space:
mode:
Diffstat (limited to 'docs/prototypes/2026-07-23-world-clock-vertical-longitude.html')
-rw-r--r--docs/prototypes/2026-07-23-world-clock-vertical-longitude.html177
1 files changed, 177 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html b/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html
new file mode 100644
index 0000000..a897755
--- /dev/null
+++ b/docs/prototypes/2026-07-23-world-clock-vertical-longitude.html
@@ -0,0 +1,177 @@
+<!doctype html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>Dupre World — vertical, true longitude</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:34px; left:64px;
+ color:var(--steel); font-size:12px; letter-spacing:0.35em;
+ text-transform:uppercase; z-index:3;
+ }
+ .masthead b { color:var(--gold); font-weight:400; }
+ 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 .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>
+ <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 parts = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, timeZoneName: "longGeneric" }).formatToParts(new Date());
+ const p = parts.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;
+
+/* Geometry: longitude maps to y (east/high-lon at top, west/low-lon at bottom).
+ Dots sit at the true longitude position on a vertical line. Labels are
+ staggered into columns so near-longitude cities don't overwrite each other;
+ a horizontal leader (>= MIN_LEAD px) runs from each dot to its label. */
+const LINE_X = 84, TOP = 104, BOT = 104, MIN_LEAD = 7;
+const COL_W = 216, ROW_H = 68; // label footprint (3 lines + breathing room)
+const lons = ROSTER.map((c) => c.lon);
+const maxLon = Math.max(...lons), minLon = Math.min(...lons);
+const wires = document.getElementById("wires");
+const labels = document.getElementById("labels");
+let nodes = [];
+
+function layout() {
+ const H = window.innerHeight, W = window.innerWidth;
+ const drawH = H - TOP - BOT;
+ wires.innerHTML = ""; labels.innerHTML = ""; nodes = [];
+ const svgNS = "http://www.w3.org/2000/svg";
+
+ // the spine
+ const spine = document.createElementNS(svgNS, "line");
+ spine.setAttribute("x1", LINE_X); spine.setAttribute("x2", LINE_X);
+ spine.setAttribute("y1", TOP - 18); spine.setAttribute("y2", H - BOT + 18);
+ spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2");
+ wires.appendChild(spine);
+
+ // true y per city, sorted east->west (already the roster order)
+ const placed = []; // per column: last y used
+ ROSTER.forEach((c) => {
+ const y = TOP + (maxLon - c.lon) / (maxLon - minLon) * drawH;
+ // leftmost column whose last label is >= ROW_H away
+ let col = 0;
+ while (placed[col] !== undefined && y - placed[col] < ROW_H) col++;
+ placed[col] = y;
+ const x = LINE_X + MIN_LEAD + col * COL_W;
+
+ // leader (horizontal from dot to label)
+ const lead = document.createElementNS(svgNS, "line");
+ lead.setAttribute("x1", LINE_X); lead.setAttribute("x2", x);
+ lead.setAttribute("y1", y); lead.setAttribute("y2", y);
+ lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4");
+ wires.appendChild(lead);
+ // dot on the spine
+ const dot = document.createElementNS(svgNS, "circle");
+ dot.setAttribute("cx", LINE_X); 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";
+ lbl.style.left = (x + 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: true, weekday: "short",
+ day: "numeric", hour: "numeric", minute: "2-digit",
+ }).formatToParts(now);
+ const g = (t) => (p.find((x) => x.type === t) || {}).value || "";
+ const h24 = new Intl.DateTimeFormat("en-US",
+ { timeZone: tz, hour12: false, hour: "2-digit" }).formatToParts(now)
+ .find((x) => x.type === "hour").value;
+ return {
+ time: g("hour") + ":" + g("minute") + " " + g("dayPeriod"),
+ date: g("weekday") + " " + g("day"),
+ night: (parseInt(h24, 10) % 24) >= 19 || (parseInt(h24, 10) % 24) < 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);
+ }
+}
+layout(); tick();
+setInterval(tick, 5000);
+let rt; window.addEventListener("resize", () => {
+ clearTimeout(rt); rt = setTimeout(() => { layout(); tick(); }, 150);
+});
+</script>
+</body>
+</html>