Fresco.Viewer (fresco v0.3.1)

Copy Markdown View Source

Phoenix LiveView function component that mounts a Fresco viewer.

Renders a <div> with phx-hook="FrescoViewer". The companion JS hook in priv/static/fresco.js lazy-loads OpenSeadragon from jsDelivr, initializes the viewer with sensible defaults (smooth animations, viewport clamped, Heroicons nav overlay), and publishes a handle to window.Fresco.viewerFor(id) so peer extensions can attach.

Usage

<Fresco.viewer
  id="photo"
  src={~p"/uploads/photo.jpg"}
  class="w-full h-[80vh] rounded"
/>

Source detection

The default behavior treats src as a plain image URL. Extensions can register source providers via window.Fresco.registerSourceProvider/2 to handle other formats (e.g., a DZI manifest URL via Tessera).

Interactions

Wheel-zoom, pinch-zoom, click-drag pan, double-click zoom, Heroicons nav buttons (zoom in / zoom out / reset / fullscreen). All work out of the box; no parent configuration needed.

Parent app setup

Import the JS hook and spread FrescoHooks into your LiveSocket hooks:

import "../../deps/fresco/priv/static/fresco.js"

let liveSocket = new LiveSocket("/live", Socket, {
  hooks: { ...window.FrescoHooks, ...colocatedHooks }
})

Summary

Functions

Renders a Fresco viewer for the given image source(s).

Functions

viewer(assigns)

Renders a Fresco viewer for the given image source(s).

Companion JS hook lazy-loads OpenSeadragon, mounts the viewer, attaches the nav overlay, and publishes the handle for peer extensions.

Attributes

  • id (:string) (required) - DOM id; must be unique on the page.

  • src (:string) - URL of a single image to display — shortcut for one-image viewers. Treated as a plain image (.jpg, .png, .webp, etc.) by default; source providers registered via window.Fresco.registerSourceProvider/2 can intercept specific URL patterns (e.g., Tessera handles .dzi manifests).

    Exactly one of :src or :sources is required. If both are given, :sources wins and :src is ignored.

    Defaults to nil.

  • sources (:list) - List of images to lay out on a shared canvas. Each entry is a map:

    %{
      src: "/uploads/a.jpg",   # required — image URL
      x: 0.0,                  # optional — horizontal offset in viewport units (default 0)
      y: 0.0,                  # optional — vertical offset in viewport units (default 0)
      width: 1.0               # optional — width in viewport units (default 1)
    }

    Viewport units: the first image is conventionally placed at x: 0, y: 0 with width: 1. So x: 1.1 puts the next image just to the right with a 10% gap. Height is derived from the image's natural aspect ratio — you don't specify it.

    Each entry's src runs through the same source-provider chain as the single-image :src, so a multi-image viewer can mix plain images with DZI tile pyramids handled by Tessera.

    Typically paired with :infinite_canvas so the user can pan freely across the layout. Without it, OSD will fit-to-viewport over the bounding box of all sources at mount.

    Note: handle.imageToScreen / screenToImage currently operate on the first source only. Multi-image coordinate disambiguation is planned but not yet implemented.

    Defaults to [].

  • class (:string) - CSS classes for the viewer container. Defaults to "w-full h-96".

  • infinite_canvas (:boolean) - When true, drops OSD's "keep the image filling the viewport" clamps so the user can pan freely beyond the image edges and zoom out until the image is a thumbnail in the middle of an empty canvas. The viewer background picks up a subtle dot-grid pattern in the void so it reads as "canvas," not "broken layout." Default false preserves the stock single-image viewer behavior — every existing call site keeps working unchanged.

    Layered overlays (e.g. Etcher) can draw annotations in the void around the image because their coordinate math already supports out-of-bounds image-pixel values.

    Pairs naturally with :sources to lay multiple images out on the same canvas, Figma/Miro style.

    Defaults to false.

  • rotate (:boolean) - When true, appends a fifth button to the nav overlay that rotates the image 90° clockwise on each click. Rotation persists across "Reset view" — it's tracked independently of zoom/pan. Default false keeps the four-button stock nav layout. Opt-in like :infinite_canvas so existing consumers aren't surprised by an extra button.

    Defaults to false.

  • pan_optimized (:boolean) - When true, applies a CSS-transform fast path during pure-pan motion so the canvas doesn't repaint per frame. Drops per-frame cost from ~10–20ms to <1ms on iOS Safari — measurably smoother for long-scroll reading (manhwa, comics, document viewers) where the user is scrolling, not zooming.

    How it works: on pan-start, Fresco swaps OSD's drawer for a no-op and starts emitting a synthetic fast-pan event in three phases (start, delta, end). Per frame, instead of redrawing the canvas, the hook applies a GPU-composited transform: translate3d to OSD's canvas element so the existing pixels glide. On pan-end (or zoom-change / overscan bail), the transform is cleared and OSD's drawer is restored, repainting once at the new position.

    Opt-in (defaults false) because the synthetic fast-pan event is a new public surface that overlay extensions need to handle in order to stay aligned with the canvas during the transform window. Etcher >= 0.2.8 listens automatically (its SVG overlay transforms in lockstep); older Etcher versions will see annotations visibly drift during pan. Consumers without overlays can opt in unconditionally.

    Bails to the normal redraw path if zoom changes mid-pan or the rotation feature (:rotate) is active — both invalidate the simple translate math.

    Defaults to false.

  • theme (:atom) - Color scheme for the viewer host background, dot grid, and nav buttons.

    • :system (default) — follow the OS / browser prefers-color-scheme.
    • :light — force light palette regardless of OS preference.
    • :dark — force dark palette regardless of OS preference.
    • :inherit — emit only the host structure; the parent app's CSS supplies the six --fresco-* custom properties. Use this to wire Fresco to a parent theme system (daisyUI, custom palettes, …) so its background, grid, and nav follow the parent theme. The variables flip automatically as the parent theme changes.

    Theming is implemented as CSS custom properties on .fresco-viewer (--fresco-bg, --fresco-grid-dot, --fresco-nav-bg, --fresco-nav-bg-hover, --fresco-nav-fg, --fresco-nav-focus). With :system/:light/:dark, Fresco supplies the values. With :inherit, the parent app does — see the Theming section of the README for the daisyUI mapping example.

    Defaults to :system. Must be one of :system, :light, :dark, or :inherit.

  • Global attributes are accepted.