Skip to content

Pinterest-quality cell detail lightbox transitions

jedmund requested to merge jedmund/masonry-zoom into main

Created by: jedmund

Summary

Replaces SwiftUI's .navigationTransition(.zoom) (which flashed black, jumped aspect ratios, and offered no interactive dismiss) with an owned floating-image transition pattern across both transitions in the detail flow:

  • Cell → detail: FloatingHeroCoordinator + FloatingHeroOverlay render a single PixivImage at the top z-index, animated via activeFrame from the cell's window-frame to the detail hero's pedestal frame. No matched-geometry cross-fade artifacts.
  • Detail → lightbox: Same pattern applied to the hero lightbox pair (FloatingLightboxCoordinator + FloatingLightboxOverlay). Replaces the previous matchedGeometryEffect.
  • Pull-to-dismiss everywhere: detail dismiss rides on onScrollPhaseChange + onScrollGeometryChange (so it doesn't fight the inner ScrollView's pan recognizer); lightbox dismiss bridges UIKit pan data from ZoomableImageView to the coordinator's dragOffsetY / dragScale so the image follows the finger 1:1 in both cases.
  • Spring tuner moved from a permanent floating panel to Settings → Developer.
  • Lightbox preload routed through a dedicated ImagePipeline.pixivLightbox (~400 MB cache) so the original-resolution image doesn't evict the hero from the main pipeline.
  • Multi-resolution image fallback in PixivImage so the lift always has something to show on a fresh-cell tap (cell's 300pt variant under the loading 1200pt), with the upgrade suppressed during the spring so it doesn't flash mid-flight.

Architectural pieces

  • DetailPresenter + PresentedDetail stack (designed for v3 nested related-illust overlays; v1 ever holds a single layer).
  • Composite source IDs (<feedID>/<illustID>) so the same illust in multiple feeds doesn't cross-bind.
  • CellFrameReporter + HeroFrameReporter (async, refines on scroll/rotation) AND synchronous frame capture in PressableCellStyle on press-down (eliminates the first-tap-doesn't-zoom race).
  • HeroFrameComputer + LightboxFrameComputer pure functions matching the actual SwiftUI layouts.
  • Three velocity / settle-handler safeguards: disablesAnimations transaction in handleStackChange (kills inherited spring velocity on rapid re-tap), gated lift/dismiss settle handlers (a stale settle can't clobber the next transition), holdFallbackDuringLoad so a 1200pt arriving mid-spring doesn't flash.

Plus a few orthogonal wins along the way:

  • iPad correctly distinguishes portrait vs landscape column counts (was always using portraitColumns because verticalSizeClass is .regular in both iPad orientations).
  • Cells fade in only on genuine network loads — cached cells render directly, no flash on re-entry into the viewport.
  • Spring Tuner relocated to Settings → Developer.

What's NOT in this PR

  • v2 horizontal swipe through the feed (architecture supports it; not yet implemented).
  • v3 nested related-illusts overlays (architecture supports it; RelatedIllustsSection.swift is a stub).
  • Dominant-color placeholders (planned, shelved as future exploration).
  • Removing the temporary 🟥 debug logs in ZoomableImageView / FloatingLightboxOverlay for diagnosing silent CDN failures.

Test plan

  • Cold scroll Home — cells fade in on first appearance, no fade on re-scroll into viewport.
  • Tap fresh cell (one not yet tapped this session) — clean lift, image visible the entire time. Repeat with several different cells.
  • Re-tap same cell — no exaggerated bounce / velocity inheritance from previous dismiss.
  • Pull-down dismiss — image follows finger 1:1, chrome and pedestal fade with the pull, release at any positive offset commits, scroll-into-content rescues.
  • Close button dismiss — same dismiss spring, no jump.
  • Multi-page illust — swipe carousel, tap close, dismiss returns from page 0 (carousel resets first). Tap hero → lightbox shows current page.
  • Lightbox lift / dismiss — single floating image throughout, no cross-fade. Pinch to zoom; pan-down at min zoom commits dismiss with finger tracking.
  • Tag pill / user byline in detail — overlay dismisses cleanly, then NavigationStack pushes destination.
  • Settings → Developer → Spring Tuner — sliders adjust springs in real time. Reset restores defaults.
  • iPad portrait + landscape rotation — column count switches per the user's chosen settings.
  • Memory — open lightbox on 5+ different illusts; main pipeline cache (heroes/cells) untouched, lightbox pipeline holds ≤6 entries.

🤖 Generated with Claude Code

Merge request reports

Loading