Fresco.Viewer (fresco v0.2.0)

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.

  • 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.