Fresco.Canvas (fresco v0.5.1)

Copy Markdown View Source

<Fresco.canvas> — a layered scene of N images positioned at absolute coordinates on a virtual canvas, plus an open extensions map for annotation tools (future Etcher), ML overlays, and other peer packages.

Single-image is just the N=1 case — use Fresco.Viewer when you want the bare "pan/zoom one image" component without the scene-document overhead.

The .fresco file format

Serializing a canvas yields a JSON document keyed by:

{
  "version": "1",
  "canvas": { "width": 4000, "height": 3000, "background": null },
  "images": [
    {
      "id": "img-1",
      "src": "/uploads/a.jpg",
      "x": 0, "y": 0,
      "width": 2000,
      "z_index": 0,
      "natural_width": 2000,
      "natural_height": 1500
    }
  ],
  "extensions": {
    "etcher":     { "version": "1", "annotations": [...] },
    "ml-overlay": { ... }
  }
}
  • canvas.width / canvas.height — the virtual canvas extent in canvas pixels. Reset-view fits this rectangle to the viewport.
  • Images are positioned at absolute canvas-pixel (x, y) with width in canvas pixels. Height is derived from natural aspect ratio if natural_width and natural_height are present (and saved into the file for forward compatibility).
  • extensions is an open map keyed by package name. Fresco never inspects the inside — each extension owns its own shape and version. Unknown extension keys are preserved verbatim across read/write so you can load → edit → save without losing data the current version doesn't understand.
  • Read-time forward-compatibility: any unknown top-level or per-image key is preserved through a private __extra__ map and re-merged on write. A v1 reader of a future v2 file keeps the v2 fields it doesn't understand and writes them back unchanged.

Building a canvas

iex> canvas =
...>   Fresco.Canvas.new(width: 4000, height: 3000)
...>   |> Fresco.Canvas.add_image(%{src: "/a.jpg", x: 0, y: 0, width: 2000})
...>   |> Fresco.Canvas.add_image(%{src: "/b.jpg", x: 2100, y: 0, width: 1800})
...>   |> Fresco.Canvas.put_extension("etcher", %{"version" => "1", "annotations" => []})
iex> Enum.map(canvas.images, & &1.id)
["img-1", "img-2"]

File I/O

Fresco.Canvas.write!("/tmp/scene.fresco", canvas)
canvas = Fresco.Canvas.read!("/tmp/scene.fresco")

Writes are atomic: write/2 writes to <path>.tmp then renames, so an interrupted save can't corrupt an existing file.

Extension contract — passive Fresco

Fresco is passive with respect to extensions. The file is the source of truth; updates flow consumer LiveView → %Fresco.Canvas{} in assigns → re-render. A peer package like the future Etcher reads its initial state via handle.getExtension("etcher") at mount, pushes edits to its own LiveView, which calls Fresco.Canvas.put_extension(canvas, "etcher", new_data) and re-assigns. Fresco's handle is intentionally read-only for extensions — no setExtension method exists, so save timing is never racing with annotation updates over channels.

See Fresco.Viewer for the simpler single-image component, and Fresco.ScrollStrip for the long-scroll reader counterpart.

Summary

Functions

Append an image to the canvas.

Renders a Fresco canvas — N images positioned at absolute coordinates on a virtual canvas, with pan/zoom/fit/fullscreen identical to Fresco.viewer. Hooks the FrescoCanvas JS controller.

Parse a JSON string into a canvas struct. Returns {:ok, canvas} or {:error, %Fresco.Canvas.SchemaError{}}.

Parse a JSON string into a canvas struct. Raises on invalid input.

Build a new empty canvas.

Put or overwrite an extension blob keyed by name (binary or atom).

Read a canvas from disk. Returns {:ok, canvas} or {:error, reason}.

Read a canvas from disk. Raises on failure.

Serialize a canvas to a JSON string. Returns {:ok, json} or {:error, reason} (only on Jason encode failure — schema is validated at struct-construction time).

Serialize a canvas to a JSON string. Raises on encode failure.

Write a canvas to disk atomically. Writes to <path>.tmp first then renames so an interrupted save can't corrupt the existing file.

Write a canvas to disk atomically. Raises on failure.

Types

image()

@type image() :: %{
  :id => String.t(),
  :src => String.t(),
  :x => number(),
  :y => number(),
  :width => number(),
  optional(:z_index) => integer(),
  optional(:natural_width) => number(),
  optional(:natural_height) => number(),
  optional(:__extra__) => map()
}

t()

@type t() :: %Fresco.Canvas{
  __extra__: map(),
  canvas: %{
    :width => number(),
    :height => number(),
    optional(:background) => String.t() | nil
  },
  extensions: %{optional(String.t()) => any()},
  images: [image()],
  version: String.t()
}

Functions

add_image(canvas, attrs)

Append an image to the canvas.

Required attrs: :src (string URL/path), :x, :y, :width (numbers, width > 0). Optional: :id (auto-assigned img-N when omitted), :z_index, :natural_width, :natural_height.

Raises ArgumentError on invalid attrs.

canvas(assigns)

Renders a Fresco canvas — N images positioned at absolute coordinates on a virtual canvas, with pan/zoom/fit/fullscreen identical to Fresco.viewer. Hooks the FrescoCanvas JS controller.

Attributes

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

  • canvas (Fresco.Canvas) (required) - A %Fresco.Canvas{} struct describing the scene: virtual canvas extent, the list of images with their canvas-pixel positions, and an open extensions map. Build one via Fresco.Canvas.new/1 + Fresco.Canvas.add_image/2, or load one from a .fresco file via Fresco.Canvas.read!/1.

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

  • infinite_canvas (:boolean) - When true, drops the default "canvas must cover viewport" clamp so the user can pan freely beyond the canvas edges and zoom out until the whole layout is a thumbnail in the middle of an empty workspace.

    Defaults to false.

  • theme (:atom) - Color scheme. Same semantics as Fresco.viewer's :theme. Defaults to :system. Must be one of :system, :light, :dark, or :inherit.

  • zoom_floor (:float) - Optional minimum zoom scale, in engine units (screen-px-per-canvas-px). When set, the engine clamps every zoom path — wheel, pinch, double-click, fitBounds — at this floor. nil (default) falls through to the engine's normal floor (sFit clamped mode, sFit * 0.05 infinite-canvas mode).

    Most often set at runtime via handle.setZoomFloor(scale) — paged readers recompute it each time the user navigates to a new page so the floor tracks the current page's fit-to-viewport scale, not the whole-canvas fit.

    Defaults to nil.

  • zoom_ceiling (:float) - Optional maximum zoom scale. Symmetric to :zoom_floor. nil (default) uses the engine's default ceiling (8× canvas-pixel ratio capped by the 8192-px raster safety limit).

    Defaults to nil.

  • pan_locked (:boolean) - When true, single-pointer pan gestures (mouse drag, touch drag, arrow keys) are suppressed. Two-pointer pinch still works for zoom. Toggle at runtime via handle.setPanLocked(true|false).

    Defaults to false.

  • Global attributes are accepted.

from_json(json)

Parse a JSON string into a canvas struct. Returns {:ok, canvas} or {:error, %Fresco.Canvas.SchemaError{}}.

Unknown top-level and per-image keys are preserved via a private __extra__ map; round-tripping through to_json/from_json keeps them intact so v1 readers of a future v2 file don't lose v2-only data.

from_json!(json)

Parse a JSON string into a canvas struct. Raises on invalid input.

new(opts \\ [])

Build a new empty canvas.

Options:

  • :width — virtual canvas width in canvas pixels (default 0)
  • :height — virtual canvas height (default 0)
  • :background — optional CSS color string for the stage background

put_extension(canvas, name, value)

Put or overwrite an extension blob keyed by name (binary or atom).

Fresco never inspects the contents — each peer package owns the inner shape and self-versions inside its own blob.

read(path)

Read a canvas from disk. Returns {:ok, canvas} or {:error, reason}.

read!(path)

Read a canvas from disk. Raises on failure.

to_json(canvas)

Serialize a canvas to a JSON string. Returns {:ok, json} or {:error, reason} (only on Jason encode failure — schema is validated at struct-construction time).

to_json!(canvas)

Serialize a canvas to a JSON string. Raises on encode failure.

write(path, canvas)

Write a canvas to disk atomically. Writes to <path>.tmp first then renames so an interrupted save can't corrupt the existing file.

Returns {:ok, path} or {:error, reason}.

write!(path, canvas)

Write a canvas to disk atomically. Raises on failure.