From 1de0775471930d6b13fad0f491d6c3023e50ee09 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 31 Oct 2026 06:20:05 +0100 Subject: dashboard: add two finger rotate --- src/api/dashboard/dashboard.html | 71 ++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 38a1a5c..1a11a25 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -152,8 +152,8 @@
- drag to pan, wheel or pinch to zoom, double-click to reset; drag a node to - pin it, double-click it to let go + drag to pan, wheel or pinch to zoom, turn with two fingers, + double-click to reset; drag a node to pin it, double-click it to let go
@@ -481,6 +481,13 @@ function drawing(key, tag, attrs, made) { } return node; } +// A label's `attrs`, turned back where the map is turned (`view`): upright, about where +// it is anchored. +function upright(attrs) { + if (!view.turn) return attrs; + return { ...attrs, transform: "rotate(" + -degrees(view.turn) + " " + attrs.x + " " + + attrs.y + ")" }; +} function text(node, content) { if (node.textContent !== content) node.textContent = content; } @@ -600,11 +607,11 @@ function renderGraph() { // The one in use at the middle, larger; the others further along, beside theirs. const t = line.used ? .5 : .3; const out = line.used ? -4 : Math.sign(side) * 9; - const label = drawing("rtt " + key + " " + i, "text", { + const label = drawing("rtt " + key + " " + i, "text", upright({ x: p.x + (q.x - p.x) * t + ox + nx * out, y: p.y + (q.y - p.y) * t + oy + ny * out + (line.used ? 0 : 3), "text-anchor": "middle", "font-size": line.used ? 11 : 9, - fill: line.used ? "var(--fg)" : "var(--muted)" }); + fill: line.used ? "var(--fg)" : "var(--muted)" })); text(label, ms(line.rttMs)); children.push(label); } @@ -636,8 +643,8 @@ function renderGraph() { "stroke-width": dormant ? 2.5 : 1.5 }, pressed); tooltip(circle, id + (reported ? "" : " (no links reported)") + (dormant ? " (dormant)" : "")); - const label = drawing("name " + id, "text", { x: p.x + 10, y: p.y + 4, "font-size": 12, - fill: "var(--fg)", cursor: "pointer" }, pressed); + const label = drawing("name " + id, "text", upright({ x: p.x + 10, y: p.y + 4, + "font-size": 12, fill: "var(--fg)", cursor: "pointer" }), pressed); text(label, name(id)); children.push(circle, label); } @@ -1015,13 +1022,21 @@ legendToggle.addEventListener("click", () => { if (state) renderGraph(); }); -// --- Pan and zoom: the map's group moved and scaled. Lines, labels and nodes scale -// with it, as on a map. --- +// --- Pan, zoom and turn: the map's group moved, scaled and turned (`turn`, in +// radians). Lines, labels and nodes scale with it, as on a map; labels stay upright +// (`upright`). --- const graph = document.getElementById("graph"); -const view = { x: 0, y: 0, k: 1 }; +const view = { x: 0, y: 0, k: 1, turn: 0 }; +const degrees = radians => radians * 180 / Math.PI; function moveView() { document.getElementById("view").setAttribute("transform", - "translate(" + view.x + " " + view.y + ") scale(" + view.k + ")"); + "translate(" + view.x + " " + view.y + ") rotate(" + degrees(view.turn) + ") scale(" + + view.k + ")"); +} +// `[x, y]` turned by `angle`. +function turned(x, y, angle) { + const c = Math.cos(angle), s = Math.sin(angle); + return [x * c - y * s, x * s + y * c]; } // Where the pointer is, in the map's own units (its `viewBox` is its size). function pointer(event) { @@ -1048,25 +1063,30 @@ function pressing(event, move, click) { graph.setPointerCapture(event.pointerId); } // Fingers on the map, by pointer. Two pinch: the map zooms around the point between -// them and moves with it. A press (`pressing`) is given up when the second comes. +// them, moves with it, and turns as they do. A press (`pressing`) is given up when the +// second comes. const touching = new Map(); let pinch = null; const between = (a, b) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }); // From the first two fingers as they are now. function pinchFrom() { - const [a, b] = touching.values(); - pinch = { view: { ...view }, at: between(a, b), - distance: Math.max(Math.hypot(b.x - a.x, b.y - a.y), 1) }; + const [one, two] = touching.values(); + pinch = { view: { ...view }, at: between(one, two), + distance: Math.max(Math.hypot(two.x - one.x, two.y - one.y), 1), + angle: Math.atan2(two.y - one.y, two.x - one.x) }; } function pinched() { - const [a, b] = touching.values(); - const at = between(a, b); - const k = zoomed(pinch.view.k * Math.hypot(b.x - a.x, b.y - a.y) / pinch.distance); - // What was between the fingers stays between them. - view.x = at.x - (pinch.at.x - pinch.view.x) * k / pinch.view.k; - view.y = at.y - (pinch.at.y - pinch.view.y) * k / pinch.view.k; - view.k = k; + const [one, two] = touching.values(); + const at = between(one, two); + const k = zoomed(pinch.view.k * Math.hypot(two.x - one.x, two.y - one.y) / pinch.distance); + const turn = pinch.view.turn + Math.atan2(two.y - one.y, two.x - one.x) - pinch.angle; + // What was between the fingers (on the map, unturned and unscaled) stays between them. + const [x, y] = turned(pinch.at.x - pinch.view.x, pinch.at.y - pinch.view.y, -pinch.view.turn); + const [dx, dy] = turned(x * k / pinch.view.k, y * k / pinch.view.k, turn); + Object.assign(view, { x: at.x - dx, y: at.y - dy, k, turn }); moveView(); + // Its labels upright again. + if (state) renderGraph(); } // Before the map's own handlers (a node's included): a second finger takes over. graph.addEventListener("pointerdown", event => { @@ -1123,8 +1143,10 @@ graph.addEventListener("pointerdown", event => { function drag(event, id) { const p = positions.get(id), from = { x: p.x, y: p.y }; pressing(event, (dx, dy) => { - p.x = from.x + dx / view.k; - p.y = from.y + dy / view.k; + // The pointer's move, on the map (unturned and unscaled). + const [x, y] = turned(dx, dy, -view.turn); + p.x = from.x + x / view.k; + p.y = from.y + y / view.k; pinned.add(id); renderGraph(); }, () => focus(id)); @@ -1138,8 +1160,9 @@ function unpin(id) { // A double-click beside the nodes: the whole map again. graph.addEventListener("dblclick", event => { if (event.target !== graph) return; - Object.assign(view, { x: 0, y: 0, k: 1 }); + Object.assign(view, { x: 0, y: 0, k: 1, turn: 0 }); moveView(); + if (state) renderGraph(); }); // Beside the nodes, or off the map: nothing hovered (the element left may be gone). graph.addEventListener("mousemove", event => { if (event.target === graph) hover(null); }); -- cgit v1.3.1