diff options
Diffstat (limited to 'docs')
4 files changed, 686 insertions, 0 deletions
diff --git a/docs/prototypes/2026-07-23-world-clock-horizontal.html b/docs/prototypes/2026-07-23-world-clock-horizontal.html new file mode 100644 index 0000000..ef45657 --- /dev/null +++ b/docs/prototypes/2026-07-23-world-clock-horizontal.html @@ -0,0 +1,190 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<title>Dupre World — horizontal, 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:30px; left:50%; transform:translateX(-50%); + color:var(--steel); font-size:12px; letter-spacing:0.35em; + text-transform:uppercase; z-index:3; + } + .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; } + /* Each label anchors at the leader's end; above blocks sit up, below sit down. */ + .lbl { position:absolute; z-index:2; text-align:center; + transform:translateX(-50%); white-space:nowrap; } + .lbl.up { transform:translate(-50%,-100%); } + .lbl .city { color:var(--steel); font-size:11px; letter-spacing:0.18em; + text-transform:uppercase; } + .lbl .time { color:var(--cream); font-size:24px; letter-spacing:0.04em; + line-height:1.1; } + .lbl .meta { color:var(--dim); font-size:11px; letter-spacing:0.1em; } + .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 · WEST → EAST</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 across the centre; longitude maps to x (west left, east right). Labels + alternate above and below so near-longitude cities split across the line. */ +const SIDE = 150, MIN_LEAD = 7, ROW_H = 92, COL_W = 176; +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, cy = Math.round(H / 2); + const drawW = W - SIDE * 2; + wires.innerHTML = ""; labels.innerHTML = ""; nodes = []; + + const spine = document.createElementNS(svgNS, "line"); + spine.setAttribute("y1", cy); spine.setAttribute("y2", cy); + spine.setAttribute("x1", SIDE - 24); spine.setAttribute("x2", W - SIDE + 24); + spine.setAttribute("stroke", "var(--line)"); spine.setAttribute("stroke-width", "2"); + wires.appendChild(spine); + + const lastX = {}; // slot "side:row" -> last x placed + // west->east order (left to right) for stable slotting + const ordered = [...ROSTER].sort((a, b) => a.lon - b.lon); + ordered.forEach((c, i) => { + const x = SIDE + (c.lon - minLon) / (maxLon - minLon) * drawW; + const order = []; + for (let row = 0; row < 4; row++) { + const first = (i % 2 === 0) ? "up" : "dn"; + const second = first === "up" ? "dn" : "up"; + order.push(first + ":" + row, second + ":" + row); + } + let slot = order.find((s) => lastX[s] === undefined || x - lastX[s] >= COL_W) + || order[order.length - 1]; + lastX[slot] = x; + const [side, rowStr] = slot.split(":"); + const row = parseInt(rowStr, 10); + const labelY = side === "up" + ? cy - MIN_LEAD - row * ROW_H + : cy + MIN_LEAD + row * ROW_H; + + const lead = document.createElementNS(svgNS, "line"); + lead.setAttribute("x1", x); lead.setAttribute("x2", x); + lead.setAttribute("y1", cy); lead.setAttribute("y2", labelY); + lead.setAttribute("stroke", "var(--line)"); lead.setAttribute("stroke-width", "1.4"); + wires.appendChild(lead); + const dot = document.createElementNS(svgNS, "circle"); + dot.setAttribute("cx", x); dot.setAttribute("cy", cy); + 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 = x + "px"; + lbl.style.top = (side === "up" ? labelY - 6 : labelY + 6) + "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 + " · <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> 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 · 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 + " · <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> diff --git a/docs/prototypes/2026-07-23-world-clock-vertical-even.html b/docs/prototypes/2026-07-23-world-clock-vertical-even.html new file mode 100644 index 0000000..58d24a1 --- /dev/null +++ b/docs/prototypes/2026-07-23-world-clock-vertical-even.html @@ -0,0 +1,128 @@ +<!doctype html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<title>Dupre World — vertical, even spacing</title> +<style> + :root{ + --ground:#050505; --cream:#f3e7c5; --silver:#bfc4d0; --steel:#969385; + --dim:#7c838a; --gold:#dab53d; --gold-hi:#ffd75f; --slate:#54677d; + --night:#39435a; + --mono:"BerkeleyMono Nerd Font","Berkeley Mono",ui-monospace,monospace; + } + * { box-sizing: border-box; } + body { + margin:0; min-height:100vh; background:var(--ground); + display:flex; flex-direction:column; justify-content:center; + padding:48px 64px; gap:22px; + font-family:var(--mono); color:var(--silver); + cursor:none; overflow:hidden; + } + .masthead { + color:var(--steel); font-size:12px; letter-spacing:0.35em; + text-transform:uppercase; margin-bottom:8px; + } + .masthead b { color:var(--gold); font-weight:400; } + .row { display:flex; align-items:baseline; gap:16px; } + .city { color:var(--steel); font-size:13px; letter-spacing:0.22em; + text-transform:uppercase; min-width:150px; } + .time { color:var(--cream); font-size:30px; letter-spacing:0.05em; + min-width:150px; } + .meta { color:var(--dim); font-size:12px; letter-spacing:0.12em; } + .meta .tz { color:var(--steel); } + .row.night .time { color:var(--slate); } + .row.night .city::after { content:" ☾"; color:var(--night); } + .row.home .time { color:var(--gold-hi); } + .row.home .city { color:var(--gold); } +</style> +</head> +<body> + <div class="masthead"><b>DUPRE</b> · WORLD WATCH</div> + <div id="stack"></div> +<script> +/* Roster mirrors ~/.config/waybar/worldclock.conf (tz|label|lat|lon). The + real face receives this as ?cities=<json>; embedded here for the prototype. */ +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}, +]; +/* Region names in Craig's "US Central" style, not the tz's major city. + Intl longGeneric is the fallback for a zone not in the map. */ +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; +const stack = document.getElementById("stack"); +stack.className = ""; +const rows = ROSTER.map((c) => { + const row = document.createElement("div"); + row.className = "row"; + row.style.marginBottom = "18px"; + const city = document.createElement("div"); city.className = "city"; + city.textContent = c.label; + const time = document.createElement("div"); time.className = "time"; + const meta = document.createElement("div"); meta.className = "meta"; + row.append(city, time, meta); + stack.appendChild(row); + return { c, row, time, 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 rows) { + 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.row.classList.toggle("home", isHome); + e.row.classList.toggle("night", f.night && !isHome); + } +} +tick(); +setInterval(tick, 5000); +</script> +</body> +</html> 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> |
