diff options
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.html | 177 |
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 · 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 + " · <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> |
