Skip to content

feat(renderer): empty-space double-tap behaviour — de-zoom step or configurable policy #72

@LeslieOA

Description

@LeslieOA

Background

Split out from the cinematic double-tap zoom umbrella (#36, item 10 of the zoom-rendering audit). One of the three candidate behaviours was attempted and deliberately reverted — read the retro on #36 before picking this up.

Original intent (from the audit)

CanvasView.tsx:454-462 — if the user double-taps empty space, the camera always runs fitToViewport, even at extreme zoom levels. Sometimes you want a small zoom-out, not "show the entire world". Three candidate behaviours:

  1. Scale to 1× at the current focal point (Safari Smart Zoom default)
  2. "De-zoom by one step" — if scale > 1, go to 1×; if scale < 1, go to fit; if scale ≈ 1, no-op
  3. Fit-all (current behaviour)

What happened when it was tried

Option 1 was implemented on feat/cinematic-double-tap-zoom (PR #42, closed unmerged) and reverted. At typical fit-view scales (~0.5×), tapping empty space pulled the camera in to 1.0× — the opposite of the "I'm lost, take me home" intent. The retro's diagnosis: Safari's 1× is meaningful because it is the page's natural design scale; on a canvas, nodes live in world space with no canonical "100% view", so a scale-based reset is the wrong primitive. Fit-all was confirmed the right default.

Remaining live scope

  • Option 2 (de-zoom by one step) is untested and remains plausible: it only diverges from fit-all when zoomed in past 1×, which is exactly the case where fit-all feels like a teleport.
  • Alternatively (or additionally), expose the policy as a CanvasView prop so embedding apps choose, with fit-all as the default.
  • Option 1 (bare "reset to 1×") should not be re-attempted as a default — that decision is settled.

Acceptance criteria

  • Decision documented on this issue: option 2, a configurable prop, or "keep fit-all unconditionally"
  • Default behaviour at scale ≤ 1 remains fit-all (regression guard for the PR perf(renderer): scale-aware tween duration for camera animations #42 failure mode)
  • If option 2 is adopted: zoomed-in (>1×) empty-space double-tap goes to 1× preserving the current focal point; near-1× is a no-op; ≤1× fits all
  • All existing animation tests still pass

Refs: #36

Agent prompt

Repo: workspace-sh/react-native-jsoncanvas. Read issue #36 in full (especially the
two retro comments — option 1 below was implemented in PR #42 and reverted), then
this issue.

Task: improve empty-space double-tap behaviour without regressing the confirmed
default. Constraints from the retro: a bare "reset to 1x" default is settled as
wrong (at fit scales ~0.5x it zooms IN, opposite of user intent); fit-all is the
correct default whenever scale <= 1.

Steps:
1. Read src/renderer/CanvasView.tsx — the double-tap handler's empty-space branch
   (fitToViewport fallback, around lines 454-462) and the animateCamera tween.
2. Implement option 2 ("de-zoom by one step"): if scale > 1, animate to 1x while
   preserving the current focal point; if scale is within ~2% of 1, no-op; if
   scale < 1, fitToViewport as today. Consider exposing the policy as an optional
   CanvasView prop (e.g. emptySpaceDoubleTap: 'fit' | 'step') defaulting to the
   current fit-all if a prop feels cleaner — document the choice on the issue.
3. Do not modify pan/pinch/scroll-wheel gesture handlers — they are out of bounds.
4. Verify in the harness app at ~0.5x, ~1x, and ~3x starting scales; 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