v0.4.0
tagged this
17 May 22:29
New sibling component to Fresco.viewer for vertical-image-strip
reading (manhwa, long comics, IG-style feeds). Native DOM <img> +
browser scroll, no OpenSeadragon, no per-frame JS. Existing
Fresco.viewer is untouched.
The architectural rationale: OSD redraws the canvas per pan frame;
on iOS Safari that costs 10-20ms/frame and the pan_optimized
fast-path can't rescue large snaps (snaps move 30-50% of viewport,
beyond the painted area → blank pixels mid-snap). Native browser
scroll on DOM <img> is GPU-composited and effectively free per
frame. Strip mode delivers exactly that, plus memory windowing so
a 50-image chapter doesn't pin hundreds of MB of decoded pixels.
Public surface:
- New Phoenix.Component `<Fresco.scroll_strip>`. Attrs: `:id`,
`:sources` (required, list of `%{url, width, height}`), `:class`,
`:theme`, `:window_before` (default 1), `:window_after` (default
3), `:gap_px` (default 0), `:snap_to_image` (default :off).
`:width` + `:height` per source are mandatory so the component
can emit `aspect-ratio: <w> / <h>` on each <img> — what keeps
layout stable through memory-windowing evict/restore cycles
(removing `src` doesn't collapse the slot → no scroll jumps).
- New JS hook `FrescoScrollStrip` (priv/static/fresco.js). Wires
the scroll bridge (rAF-coalesced), the IntersectionObserver-
free memory-windowing loop (evict src outside the window,
restore on re-entry), and the handle registry. Routes
`phx:scroll-to` from the consumer LiveView straight to
`handle.scrollTo/1` for chapter-resume / programmatic snap.
- New strip handle with surface mirroring the viewer where it
makes sense (`container`, `on`, `_emit`, `appendNavButton` via
the lifted shared helpers) plus strip-specific
`scrollTo({imageIdx, y, behavior})`,
`scrollBy({dy, behavior})`,
`imageToScreen({imageIdx, x, y})`,
`screenToImage({x, y}) → {imageIdx, x, y}`,
`getScrollState()`. Events: `scroll`, `viewport-change`,
`image-loaded`, `image-evicted`, `open`.
- New `Fresco.scrollStripFor(domId)` registry lookup (alias of
`viewerFor`) and `Fresco.onReady` alias of `onViewerReady`.
- `handle.openSeadragon` on the strip handle is a throwing
getter — accessing it usually means an overlay was written for
the viewer host without a renderer adapter. Message points at
the fix (feature-detect via `"scrollTo" in handle`).
Internal refactor (no behavior change for viewer consumers):
- Extracted `createEventBus()` so both handle factories share the
same closure-based pub/sub.
- Extracted `attachNavButton(navEl, svg, title, onClick)` so both
handle factories share the same nav-button attach + setIcon/
setTitle/.el surface. Strip mode passes navEl=null and gets a
no-op unsubscribe back — callers can call appendNavButton
unconditionally.
Tests: 23/23 pass (was 12, added 11 render-assertion tests for
the new component: <img> per source, aspect-ratio inline,
loading=lazy/decoding=async, window/snap/gap attrs plumbed,
ArgumentError on missing :sources or missing per-source dims).
`mix format`, `mix compile --warnings-as-errors`, `mix test`,
`node --check`, `mix hex.build` — all clean. fresco-0.4.0.tar
ready.
Etcher 0.3 (paired release adding the renderer adapter for strip
mode) is queued as a separate PR in the etcher repo. Etcher 0.2.x
continues to work on viewer hosts unchanged; the strip handle's
throwing openSeadragon getter is the version-mismatch signal.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>