All notable changes to Tessera are documented here. The format is based on Keep a Changelog and the project adheres to Semantic Versioning.
0.3.0 — 2026-06-14
Breaking change: rewritten for Fresco's own viewer engine. Fresco 0.5
dropped OpenSeadragon for a CSS-transform engine, so Tessera 0.2's
integration (a DZI source provider + OSD viewport.getZoom() /
swapSourcePreservingBounds against the OSD instance) no longer worked.
Tessera 0.3 is a Fresco peer layer, the same model as Etcher: it gets
the Fresco handle via window.Fresco.onReady/2 and reads the live
transform.
Changed
- Fresco constraint widened to
~> 0.5.9 or ~> 0.6.0 or ~> 0.7.0(was~> 0.1). <Tessera.layer>gains an optionaldzi_urlattribute.sourcesis unchanged (ordered low → high%{url, width}raster ladder); the hidden hook host now also carriesdata-dzi-url.- Progressive resolution swap now reads zoom from the Fresco handle
(
getTransform().s × getCanvasSize().width) instead of OSD'sviewport.getZoom()/getHomeZoom(), and swaps via the handle'sswapSourcePreservingBounds(falling back tosetImageSrc).
Added
- Debug HUD. A
debugattr on<Tessera.layer>(orlocalStorage.tesseraDebug = "1"at runtime) overlays a small panel reporting the active raster source, displayed width / zoom, DZI manifest info, current pyramid level, and live tile counts (loaded / shown / loading) — so you can confirm resolution swaps and tile streaming are actually happening. - DZI tile streaming. When
dzi_urlis set and the user zooms past the sharpest raster source, Tessera lazily fetches the.dzimanifest and streams the visible tiles at the matching pyramid level. The tile overlay is parented inside Fresco's stage and positioned in stage coordinates, so the tiles ride the same GPU transform as the base image and stay glued to it during pan/zoom (no focal-point drift). Tiles only activate when the DZI is genuinely higher-resolution than the top raster — otherwise they'd add no detail. Off-screen tiles and the manifest are fetched lazily; the overlay clears when the user zooms back below the threshold.
Fixed
- Tile generation now respects EXIF orientation (
-auto-orientbefore cropping), so tiles from a rotated source line up with the displayed image instead of being cut from the raw, mis-oriented pixels.
Unchanged
- Server-side DZI generation API (
generate/3,generate_manifest/3,generate_tile/4) and theTessera.Storagebehaviour.
0.2.1 — 2026-05-12
Patch release — package metadata polish only, no code or behavior changes.
Changed
mix docsextras now includeLICENSEandCHANGELOG.md, so the README's[LICENSE](LICENSE)reference resolves on HexDocs and the CHANGELOG gets a proper standalone page in the published docs.
0.2.0 — 2026-05-12
Breaking change: Tessera no longer hosts its own image viewer. It's now a layer that composes onto Fresco. The Fresco viewer owns the OpenSeadragon instance, the Heroicons nav overlay, animations, and viewport clamping. Tessera focuses on what makes Tessera distinctive: the DZI source provider and the multi-layer progressive-zoom logic.
What changed
- Removed:
<Tessera.viewer src=...>LiveView component. - Added:
<Tessera.layer fresco_id sources>— attaches Tessera's behavior to a named Fresco viewer. - Removed (moved to Fresco): the OSD lazy-loader, the Heroicons nav
overlay (
fresco-navnow), the animation tuning constants (animationTime: 0.3/springStiffness: 10defaults), the viewport clamp (visibilityRatio: 1.0/constrainDuringPan: true), theswapSourcePreservingBoundshelper. All available via Fresco's default viewer settings + viewer-handle API. - Added (client side): Tessera now registers a DZI source provider
with Fresco at load time, so any URL ending in
.dziis automatically treated as a tile pyramid. - Unchanged:
Tessera.generate/3,Tessera.generate_manifest/3,Tessera.generate_tile/4, theTessera.Storagebehaviour + the defaultTessera.Storage.Localadapter — the server-side generator API is identical to 0.1. Migration from 0.1 only affects the template.
Required dependency change
- Add
{:fresco, "~> 0.1"}alongside{:tessera, "~> 0.2"}in yourmix.exs. - In
app.js, importfresco.jsbeforetessera.js. Spread both hook namespaces into your LiveSocket hooks:{ ...window.FrescoHooks, ...window.TesseraHooks, ...colocatedHooks }.
Migration from 0.1
- <Tessera.viewer
- id="photo"
- src={~p"/uploads/photo-medium.jpg"}
- class="w-full h-[80vh] rounded"
- />
+ <Fresco.viewer
+ id="photo"
+ src={~p"/uploads/photo-medium.jpg"}
+ class="w-full h-[80vh] rounded"
+ />
+
+ <Tessera.layer
+ fresco_id="photo"
+ sources={[
+ %{url: ~p"/uploads/photo-medium.jpg", width: 1024},
+ %{url: ~p"/dzi/photo.dzi"}
+ ]}
+ />The sources list shape is unchanged from 0.1; it moves from a
<Tessera.viewer sources=...> attr to a <Tessera.layer sources=...> attr.
0.1.0 — 2026-05-11
Initial release. OpenSeadragon-backed deep zoom for Phoenix apps — generate DZI tile pyramids from images and render them with a LiveView component.
Server-side
Tessera.generate/3— eager full-pyramid DZI generator. Shells out to ImageMagick (magick convert -define dzi:tile-size=... input output.dzi) and writes the manifest plus the entire tile tree to a caller-supplied output directory.Tessera.generate_manifest/3+Tessera.generate_tile/4— lazy on-demand primitives. The manifest is just XML derived from the image's intrinsic width/height; individual tiles are cropped + resized per-request, so the pyramid grows organically as users zoom into the regions that actually matter.Tessera.Storagebehaviour with a defaultTessera.Storage.Localimplementation. Consumers can plug in any backend (S3, multi-bucket, CDN) by passingstorage: MyAdapter, storage_opts: [...]through to the generators.
Client-side
<Tessera.viewer sources={...}>— Phoenix LiveView function component. Accepts an ordered low → high qualitysourceslist. Each entry carries an intrinsic pixelwidth(omit for.dzisources); the JS hook computes thresholds dynamically and swaps between layers as the user zooms in or out. Downgrade has 15% hysteresis so the source can't flicker around a boundary.priv/static/tessera.js— companion JS hook (TesseraViewer) that lazy-loads OpenSeadragon from jsDelivr on first mount.- Self-injected Heroicons navigation overlay (zoom-in / zoom-out / reset
/ fullscreen) — replaces OSD's default PNG-sprite controls so the
viewer doesn't need a CDN
prefixUrl. - Snappy animation tuning (
animationTime: 0.3,springStiffness: 10) so pan/zoom track input directly instead of drifting into place. visibilityRatio: 1.0+constrainDuringPan: trueso the image stays clamped to the viewer rectangle — no off-screen drift.- Source swaps preserve the user's viewport rectangle (
fitBounds(_, true)after the new source'sopenevent) — the image just gets sharper or softer; no jump back to home.
Requirements
- ImageMagick (
magickbinary) on the hostPATH(used bygenerate*). phoenix_live_view ~> 1.1,phoenix_html ~> 4.0,jason ~> 1.4.