Skip to content

Graph view: each frame's maths computed once; pinch to zoom on a touch screen - #676

Merged
danbarua merged 2 commits into
mainfrom
graph-view-touch
Oct 15, 2026
Merged

danbarua merged 2 commits into
mainfrom
graph-view-touch

Conversation

@danbarua

@danbarua danbarua commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Problem

  • The graph view computed the same things several times each frame:
    • The camera rotation was written out three times: in the node projection, the compass axes and the framing's maths. The projection computed the cosines and sines of the yaw and pitch once per node.
    • The open node's neighbours were built by three separate loops over the edges. In the temporal colour overlay, each node's colour scanned every edge, so one frame cost nodes × edges steps.
    • The edge drawing and the label candidates each looked up both ends of every edge and each tested whether it joins the open or hovered node.
    • The palette read each record type's colour from the stylesheet once per node.
    • The starting pivot's depth came from a pass over every node, for each node projected before the pivot was set.
  • On a touch screen, one finger already orbited the camera, through pointer events and touch-action: none. Two fingers did not zoom. Pressing the second finger restarted the drag from that finger, and each finger's move then set the camera's turn from its own offset, so the camera jumped between them. A touch the browser cancelled (pointercancel) left the drag running.

Change

Two commits:

Commit What it does
Repeated computation rotationOf and onScreen are the one rotation; the projection, the compass and the framing use them, with the cosines and sines computed once per frame or per candidate yaw. neighboursOf replaces the three loops. sceneOf finds each frame's edges in front of the camera, and whether each is hot, for both drawing and labels. The palette looks up a type's colour once per frame. The starting pivot's depth is the newest node's. edgeKey is the one spelling of an edge's identity. No behaviour change.
Touch pressPointer, movePointer and releasePointer record the pointers pressed on the canvas. One pointer orbits, as before. Two pinch: the camera's distance scales with how much nearer or further apart they are than when the second was pressed, within the same 120 to 2200 as a scroll. Pressing or lifting a pointer restarts the gesture from where the pointers are, so the camera does not jump. A press that had two pointers opens no node. pointercancel releases a pointer as pointerup does.

What a person can see change

  • On a touch screen, two fingers moving apart zoom in and moving together zoom out. Lifting one finger hands the orbit to the other without a jump.
  • On a device whose main pointer cannot hover ((hover: none)), the hint reads "drag to orbit · pinch to zoom". Elsewhere it still reads "drag to orbit · scroll to zoom".
  • A drag cancelled by the browser no longer keeps turning the camera when the next pointer moves.

Not done

  • hitTest keeps its 18-pixel reach for a finger, as for a mouse: a 36-pixel target, above the 24-pixel minimum of WCAG 2.2's target size.

Not wanted

  • A card preview on a touch screen. A finger cannot hover, so a tap opens the node, and the corner shows a card only during playback.
  • A pan. The camera frames the open node by itself, which moves the view where a pan would.

Controls

  • tests/graph-view.test.ts: the existing 35 tests pass unchanged after the refactor, apart from labelCandidates taking a Scene built by sceneOf. Eight new tests cover orbiting, the 4-pixel slop, pinching in and out, the zoom limits, the click after a pinch, lifting one of two fingers, and a pointer that was not pressed on the canvas.
  • A new e2e test, run with hasTouch, sends a two-finger pinch through Chrome's touch input (Input.dispatchTouchEvent). It checks the hint and checks that the open node grows from at most 30 pixels across to fill a 41-pixel square. With the pinch's zoom line removed, the test fails.

Verification

  • bun run check: all 28 passed; bun test 1113 pass, 0 fail.
  • E2E_PORT_API=9019 E2E_PORT_DEV=8970 E2E_PORT_BUILT=8971 bun run e2e: 177 passed, 0 failed, 11 skipped. The 11 skipped tests are the development-only pages in the built project, as before; the new touch test passed in both projects.

Linked issues

None.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs

danbarua and others added 2 commits October 15, 2026 15:20
- One rotation (`rotationOf`, `onScreen`) serves the node projection, the
  compass axes, the framing's candidate yaws and `distanceToFit`. The cosines
  and sines of the yaw and pitch are computed once per frame for the nodes,
  and once per candidate yaw for the framing, instead of once per point.
- `neighboursOf` replaces three loops over the edges. In the temporal colour
  overlay, a node's colour no longer scans every edge: the frame looks the
  node up in the open node's neighbours.
- `sceneOf` finds the edges whose two nodes are in front of the camera, and
  whether each joins the open or hovered node, once per frame for both the
  edge drawing and the label candidates.
- The palette looks up each record type's colour in the stylesheet once per
  frame, not once per node.
- The starting pivot's depth is the newest node's depth, without a pass over
  every node: the canvas never removes a node.
- `edgeKey` is the one spelling of an edge's identity.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
…nds its drag

The canvas already orbited under one finger, through pointer events and
`touch-action: none`. Two fingers did not zoom: pressing the second finger
restarted the drag from that finger, and each finger's move then set the
camera's turn from its own offset, so the camera jumped between them. A touch
the browser cancelled left the drag running.

- `pressPointer`, `movePointer` and `releasePointer` record the pointers
  pressed on the canvas and what they do to the camera. One pointer orbits,
  as before. Two pinch: the camera's distance scales with how much nearer or
  further apart they are than when the second was pressed, within the same
  120 to 2200 as a scroll. Pressing or lifting a pointer restarts the gesture
  from where the pointers are, so the camera does not jump.
- A press that had two pointers opens no node when it ends.
- `pointercancel` releases a pointer as `pointerup` does.
- On a device whose main pointer cannot hover, the hint reads "drag to orbit
  · pinch to zoom".
- An e2e test sends a two-finger pinch through Chrome's touch input and
  checks the open node is drawn larger.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GVjyjkaADPAqLvtnf3xUNs
@github-actions github-actions Bot added the area:web The web app and shared UI: app-web, ui, design, records, view-model, acp-client label Oct 15, 2026
@danbarua
danbarua merged commit 9104fef into main Oct 15, 2026
10 checks passed
@danbarua
danbarua deleted the graph-view-touch branch October 15, 2026 14:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:web The web app and shared UI: app-web, ui, design, records, view-model, acp-client

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant