aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/dashboard/dashboard.html71
1 files 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 @@
</div>
<div class="legend" id="legend"></div>
<div class="legend">
- <span class="hint">drag to pan, wheel or pinch to zoom, double-click to reset; drag a node to
- pin it, double-click it to let go</span>
+ <span class="hint">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</span>
</div>
</div>
<svg id="graph"><g id="view"></g></svg>
@@ -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); });