diff options
| author | tv <tv@krebsco.de> | 2026-11-07 00:14:08 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-07 00:14:08 +0100 |
| commit | 06dca76de30422a9cfdb45a7879c83b3f91cc1e4 (patch) | |
| tree | 80ca470bfe3cd458d1c6943f85247d0de488d71f /src/api/dashboard | |
| parent | df40d6d2aed2d1a3b5c02a0a2e14c7c234d035ef (diff) | |
dashboard: polish remake rings
Diffstat (limited to 'src/api/dashboard')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 55 |
1 files changed, 42 insertions, 13 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 771b49b..06bf267 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -1371,12 +1371,12 @@ function renderGraph() { // In a remake (`inRemakes`): amber; its coordinator also ringed. const voting = amber.has(id); if (coordinating.has(id)) { - // Counting down once committed (`countdowns`): from the top, as a clock's hand. + // Filling while it settles, counting down once committed (`countdowns`): both + // from the top of the screen, however the map is turned. const dash = countdownDash(id, radius + 4); children.push(drawing("coordinator " + id, "circle", { cx: p.x, cy: p.y, r: radius + 4, fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none", - ...(dash ? { "stroke-dasharray": dash, - transform: "rotate(-90 " + p.x + " " + p.y + ")" } : {}) })); + ...(dash ? { "stroke-dasharray": dash, transform: countdownTurn(id, p) } : {}) })); if (dash && countdownFrame === null) countdownFrame = requestAnimationFrame(tickCountdowns); } const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: radius, @@ -2253,12 +2253,22 @@ function remakesArrived() { countdowns.clear(); for (const remake of remakes) { seen.add(remake.id); - // Committed: this node's wait to switch, as of now (re-anchored with every update). - if (remake.phase === "committed" && typeof remake.switchInMs === "number" && - remake.switchDelayMs > 0) { - countdowns.set(remake.coordinator, { until: performance.now() + remake.switchInMs, - total: remake.switchDelayMs }); + // Settling: how long its votes are collected; committed: this node's wait to switch. + // Anchored when first seen, not with every update (each moved it by however old the + // update was: jumps, with short waits), and from there on whole: it starts and ends + // at the top, even where most of the wait was over before this page saw it. + const [kind, left] = remake.phase === "proposed" && typeof remake.settleInMs === "number" + ? ["settle", remake.settleInMs] + : remake.phase === "committed" && typeof remake.switchInMs === "number" && + remake.switchDelayMs > 0 ? ["switch", remake.switchInMs] : [null, 0]; + const key = remake.id + " " + kind; + if (kind && !anchors.has(key)) { + anchors.set(key, { until: performance.now() + left, total: Math.max(1, left) }); } + // Committed and over (it switches any moment): an empty ring, not a whole one again. + const anchor = anchors.get(key) || + (remake.phase === "committed" && anchors.get(remake.id + " switch")); + if (anchor) countdowns.set(remake.coordinator, { ...anchor, kind: kind || "switch" }); const was = knownRemakes.get(remake.id); if (remakesKnown) { const breath = id => breathe(id, { fill: AMBER, rim: AMBER, to: 20, ms: 900, opacity: .45 }); @@ -2287,20 +2297,39 @@ function remakesArrived() { } knownRemakes.delete(id); } + for (const key of anchors.keys()) if (!seen.has(key.split(" ")[0])) anchors.delete(key); remakesKnown = true; } -// Committed remakes' coordinators, by node ID: when this node switches (by -// `performance.now()`) and the whole wait (ms). Their ring counts down: a full circle -// from the top, going counterclockwise as the wait runs out, gone as the epoch switches. +// Open remakes' coordinators, by node ID: when the wait ends (by `performance.now()`), +// the whole wait (ms), and which: `settle` (votes collected) or `switch` (committed, +// this node's wait to switch). The ring of one settling fills: empty at first, growing +// counterclockwise from the top; once committed it counts down: a full circle from the +// top, its end going back counterclockwise as the wait runs out, gone as the epoch +// switches. Both meet at the top, so the one hands over to the other there. const countdowns = new Map(); +// When each wait was first seen (`remakesArrived`), by remake ID and kind. +const anchors = new Map(); let countdownFrame = null; -// The ring's dashes for `id` (radius `r`), if it counts down: the part left. +// The ring's dashes for `id` (radius `r`), if it counts: the part filled, or left. function countdownDash(id, r) { const countdown = countdowns.get(id); if (!countdown) return null; const left = Math.min(1, Math.max(0, (countdown.until - performance.now()) / countdown.total)); const around = 2 * Math.PI * r; - return (around * left).toFixed(1) + " " + around.toFixed(1); + const shown = countdown.kind === "settle" ? 1 - left : left; + return (around * shown).toFixed(1) + " " + around.toFixed(1); +} +// Where the ring of `id` (at `p`) starts, and which way it goes: a circle's stroke +// starts at its right and goes clockwise; turned a quarter back (and back from the +// map's own turn, `view`), it starts at the top of the screen. Settling, mirrored +// about the upright through its centre: counterclockwise from there. +function countdownTurn(id, p) { + const turn = "rotate(" + (-90 - degrees(view.turn)) + " " + p.x + " " + p.y + ")"; + if ((countdowns.get(id) || {}).kind !== "settle") return turn; + const a = degrees(view.turn); + // The mirror about the screen's upright through p, in the map's turned frame. + return "rotate(" + -a + " " + p.x + " " + p.y + ") translate(" + 2 * p.x + " 0) scale(-1 1) rotate(" + + a + " " + p.x + " " + p.y + ") " + turn; } // Every frame while a ring counts down: its dashes, and nothing else drawn anew. function tickCountdowns() { |
