Skip to content

feat(renderer): focal-point preservation for double-tap zoom #70

@LeslieOA

Description

@LeslieOA

Background

Split out from the cinematic double-tap zoom umbrella (#36, item 3 of the zoom-rendering audit). The audit called this "the most impactful cinematic improvement" — but a first implementation was attempted and deliberately reverted, so read the retro on #36 before picking this up.

Original intent (from the audit)

Double-tap zoom centres the target node; it does not keep the tap location stationary in screen space. Pinch keeps the focal point stationary, which is the canonical infinite-canvas convention (Figma, Miro, Freeform). The proposed maths — the animateCamera interpolator already lerps tx/ty/scale independently:

// world coord under tap before animation
const wx = (tapX - txStart) / scaleStart;
const wy = (tapY - tyStart) / scaleStart;
// post-animation tx/ty that keeps (wx, wy) under (tapX, tapY)
const txEnd = tapX - wx * scaleEnd;
const tyEnd = tapY - wy * scaleEnd;

What happened when it was tried

Implemented on feat/cinematic-double-tap-zoom (PR #42, closed unmerged) and reverted. The retro on #36 diagnosed a category error: Safari Smart Zoom treats the tap as a spatial anchor inside content being read; a node canvas treats it as a selector of a discrete object. Pinning the tap point placed the selected node arbitrarily relative to the viewport depending on which pixel was tapped — tap near a node's edge and the node ends up offset toward a corner instead of centred. User-visible failure: "I picked this node, why isn't it centred?"

Remaining live scope

Centring is correct when the tap hits a node — that behaviour must not change. If this is revived, the open design question is where focal anchoring is the right model. Candidates:

  • Zoom transitions not aimed at a node (keyboard/menu zoom steps), where there is no selected object to centre
  • A hybrid "anchor-then-settle": hold the tap point stationary through the animation, then settle to centred
  • Exposing the policy via a prop so embedding apps choose

Acceptance criteria

  • A documented decision on where (if anywhere) focal-point anchoring applies, recorded on this issue before implementation
  • Node-targeted double-tap still centres the node (regression guard for the PR perf(renderer): scale-aware tween duration for camera animations #42 failure mode)
  • Any anchoring path keeps the chosen focal point stationary in screen space through the whole animation
  • All existing animation tests still pass

Refs: #36

Agent prompt

Repo: workspace-sh/react-native-jsoncanvas. Read issue #36 in full, including the
two retro comments (PR #42 attempted this and was reverted), then this issue.

Task: focal-point preservation for double-tap zoom — but NOT the naive version.
The naive version (anchor the tap point whenever a node is double-tapped) was
implemented in PR #42 and reverted: on a node canvas the tap is a selector, not
an anchor, so node-targeted double-tap must keep centring the node.

Steps:
1. Read src/renderer/CanvasView.tsx (double-tap handler, around the fitToViewport
   fallback near line 454) and the animateCamera interpolator it calls. Note the
   interpolator is a JS-thread requestAnimationFrame tween — Reanimated withTiming
   is unusable here (corrupts shared values on Fabric + react-native-macos).
2. Decide and document (as a comment on this issue) where focal anchoring applies:
   non-node-targeted zoom transitions, an anchor-then-settle hybrid, or a
   configurable prop. Default behaviour for node-targeted double-tap stays
   "centre the node".
3. Implement using the world-coordinate maths from the issue body.
4. Do not modify pan/pinch/scroll-wheel gesture handlers — they are out of bounds.
5. Verify in the harness app; run the existing animation tests.

Branch + PR (Conventional Commits), PR body includes "Refs: #<this issue>".

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions