Skip to content

feat: scaleY pinch-to-zoom with counter-scaling and zoomScale persistence#20

Draft
anton-patrushev wants to merge 7 commits into
fix/remove-header-bottom-borderfrom
fix/pinch-to-zoom-for-perf-improvements_v2
Draft

feat: scaleY pinch-to-zoom with counter-scaling and zoomScale persistence#20
anton-patrushev wants to merge 7 commits into
fix/remove-header-bottom-borderfrom
fix/pinch-to-zoom-for-perf-improvements_v2

Conversation

@anton-patrushev

Copy link
Copy Markdown
Owner

Summary

  • Rewrites pinch-to-zoom to use GPU-accelerated transform: [{ scaleY: zoomScale }]timeIntervalHeight stays constant, only zoomScale changes per frame
  • Counter-scales hour labels, now indicator, event text, drag dots, horizontal grid lines, and draggable/dragging event components
  • Fixes onZoomChange to clamp zoomScale before computing zoomPercent, preventing spring overshoot from reporting out-of-range values
  • Adds initialZoomScale prop for consumer-provided initial zoom, minZoomScale/maxZoomScale on context
  • Exposes counterScaleStyle via BodyContext for shared counter-scale animated style

Architecture

ScrollView
  └─ Animated.View { height: timelineHeight * zoomScale }     ← scroll spacer
      └─ Animated.View { height: timelineHeight,               ← GPU scaling
                         transform: [{ scaleY: zoomScale }],
                         transformOrigin: '0% 0%' }
            └─ content (TimeColumn, Grid, Events, NowIndicator, Drag overlay)

Performance model

  • During pinch: 1 SharedValue write (zoomScale) + 1 outer spacer height update. GPU handles visual scaling. No content re-layout.
  • Before: timeIntervalHeight changes → minuteHeight derives → timelineHeight derives → ALL event heights derive → full layout recalc

Stacked on

  • fix/remove-header-bottom-border

Test plan

  • Pinch gesture: zoom in/out, focal point stays stable, no scroll jumping
  • Counter-scaling: hour labels, now indicator, event text all remain unwarped
  • Drag to create/edit: correct time snapping at all zoom levels
  • Tap on background: correct time reported at all zoom levels
  • goToHour(), goToDate(), zoom() work at all zoom levels
  • Overscroll spring: rubber-band feel at min/max bounds
  • Web ctrl+wheel: zoom works on web

🤖 Generated with Claude Code

anton-patrushev and others added 5 commits March 12, 2026 16:16
Adds a `children` prop to CalendarBodyProps, rendered inside
BodyContext.Provider after the calendar grid. This allows mounting
components that need BodyContext access (e.g. SharedValue capture,
zoom persistence) without depending on NowIndicatorComponent's
lifecycle — which only mounts when today is visible.

Also fixes positionUtils CalendarData import.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
During pinch gesture, only updates `zoomScale` SharedValue and applies
`transform: [{ scaleY }]` to the content container — O(1) per frame
instead of O(N) per-event height recalculations.

On gesture end, commits `timeIntervalHeight *= zoomScale; zoomScale = 1`
for a one-time O(N) recalc at the final zoom level.

Consumers receive `zoomScale` via SizeAnimation and BodyContext to
optionally counter-scale text or make discrete layout decisions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Counter-scale drag dots and default text in DraggableEvent and
  DraggingEvent (View → Animated.View with counterScaleStyle)
- Counter-scale horizontal grid lines in HorizontalLine
  (stays 1px regardless of zoom)
- Clamp zoomScale to [min, max] before computing zoomPercent in
  onZoomChange reaction — prevents spring overshoot during rubber-band
  snap-back from reporting out-of-range values

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On Android, RNGH's transformPoint computes focalY using a cached scroll
offset that doesn't update after setNativeProps. The per-frame anchor
calculation accumulated this error, causing the viewport to drift during
pinch-to-zoom.

Fix: capture focalY, offsetY, and zoomScale once at gesture start and
derive the scroll offset purely from the zoom ratio change. This makes
the anchor computation independent of per-frame focalY accuracy.

Also fix scrollTo fallback to use animated:false (instant) instead of
animated:true which caused visible lag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues caused the viewport to drift on Android during pinch:

1. setNativeProps({ contentOffset }) doesn't reliably scroll on Android
   when the ref targets RNGH's ScrollView wrapper. Replaced with
   scrollTo() from Reanimated which calls the native scrollTo method
   directly on the underlying scrollable view.

2. The ScrollView's native pan gesture fires simultaneously with the
   pinch (via simultaneousHandlers). As fingers spread/squeeze, their
   midpoint naturally moves — Android interprets this as a pan, scrolling
   the view and fighting our programmatic offset. Fix: disable
   scrollEnabled on the native ScrollView during pinch (onBegin) and
   re-enable on onFinalize. scrollTo() still works with scrollEnabled
   disabled since it only blocks touch-initiated scrolls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
anton-patrushev and others added 2 commits March 13, 2026 15:20
Root cause: transformOrigin: '0% 0%' on the inner scale container was
ignored on Android when split across static + animated style objects.
scaleY defaulted to center-origin, pushing content both up and down
instead of only down — causing visible drift despite correct scroll math.

Fix: replace transformOrigin with an explicit translateY that simulates
top-left origin: translateY = (height/2) * (zoomScale - 1). This works
identically on both platforms.

Also:
- Add isPinching SharedValue + onScroll guard on Android as safety net
- Use scrollTo instead of setNativeProps for scroll position updates
- Use gesture-start snapshot for focal-point anchoring
- Remove all debug logging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…write

The scaleY pinch-to-zoom rewrite accidentally dropped:
- dayEndLineStyle prop (dashed border between days in multi-provider scroll)
- hapticService.selection() on day change (both CalendarContainer and useSyncedList)
- isDayEnd/isDayStart forwarding through _renderResourceItem -> BodyResourceItem -> ResourceBoard -> VerticalLine

Restores full parity with patch 018 (day-end-line-and-haptic) from the
GlossGenius core-mobile patch chain.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants