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
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>".
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
animateCamerainterpolator already lerpstx/ty/scaleindependently: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:
Acceptance criteria
Refs: #36
Agent prompt