# `OpenFresco.Scene`
[🔗](https://github.com/alexdont/open_fresco/blob/v0.2.0/lib/open_fresco/scene.ex#L1)

The scene model: a fixed-size canvas plus a z-ordered list of elements.

A scene is a **design with holes**. Text and image values may be literals
or `placeholder/1` references resolved at render time against a `values`
map (see `OpenFresco.render_svg/3`). The struct is versioned and
serializes to/from JSON (`to_json!/1` / `from_json!/1`) so it can be
stored — e.g. inside a Fresco canvas's `extensions["open_fresco"]` blob —
and migrated forward (`migrate/1`).

## Elements

Elements are plain maps (mirroring `Fresco.Canvas`'s image maps) with a
`:type` tag and a common `:box` (`%{x, y, w, h}`), `:id`, and `:z`
(z-order, ascending). Build them with the constructors:

  * `text/2`   — a text block (`:value`, `:size`, `:weight`, `:font`,
    `:fill`, `:align`, `:line_height`)
  * `image/2`  — a positioned image (`:value`, `:fit`, `:radius`)
  * `shape/2`  — a filled (rounded) rectangle (`:fill`, `:radius`)
  * `button/2` — a shape + centered label composite (`:label`, `:preset`,
    `:fill`, `:text_fill`, `:radius`)

## Fills

Any fillable surface takes a `fill` built with `solid/1`, `gradient/2`, or
`image_fill/2`. Fills are tagged maps so a fill-type switch in an editor
can round-trip the inactive variants' values.

## Values

A value is either a literal binary or `placeholder(key)`. `resolve/2`
turns a value into `{:ok, resolved}` or `:unresolved` against a map.

# `box`

```elixir
@type box() :: %{x: number(), y: number(), w: number(), h: number()}
```

# `element`

```elixir
@type element() :: %{:id =&gt; String.t(), :type =&gt; atom(), optional(atom()) =&gt; any()}
```

# `fill`

```elixir
@type fill() ::
  %{type: :solid, color: String.t()}
  | %{type: :gradient, angle: number(), stops: [gradient_stop()]}
  | %{type: :image, value: value(), fit: :cover | :contain | :stretch}
```

# `gradient_stop`

```elixir
@type gradient_stop() :: %{offset: number(), color: String.t(), alpha: number()}
```

# `t`

```elixir
@type t() :: %OpenFresco.Scene{
  canvas: %{width: number(), height: number(), background: fill() | nil},
  elements: [element()],
  version: String.t()
}
```

# `value`

```elixir
@type value() :: String.t() | %{placeholder: String.t()}
```

# `add`

```elixir
@spec add(t(), element()) :: t()
```

Append an element (map from a constructor) to the scene.

# `anchor`

```elixir
@spec anchor(String.t(), atom(), keyword()) :: map()
```

An anchor: position an element relative to another (by id). `edge` is
`:below` / `:above` / `:left` / `:right`; `:gap` (default 16) is the space
between; `:align` (`:start` / `:center` / `:end`, default `:start`) aligns
the cross-axis. Pass as the `:anchor` option to an element builder. When
the target's rendered height changes (e.g. a title wraps), the anchored
element moves during layout. Chains resolve in order; cycles are rejected.

# `button`

```elixir
@spec button(
  String.t(),
  keyword()
) :: element()
```

A button composite (rounded shape + centered label). `:preset` is
`:solid` (fill), `:outline` (stroke only), or `:soft` (tinted fill).
`auto_width: true` sizes the box to the measured label plus `:padding`
(horizontal, default 24) during layout; otherwise the explicit `:box` wins.

# `from_json`

```elixir
@spec from_json(
  String.t(),
  keyword()
) :: {:ok, t()} | {:error, %{path: list(), reason: term()}}
```

Parse a scene from a JSON string without raising.

Returns `{:ok, scene}` or `{:error, %{path: path, reason: reason}}` — see
`from_map/2`. Invalid JSON reports `reason: {:invalid_json, message}` at
path `[]`.

# `from_json!`

```elixir
@spec from_json!(String.t()) :: t()
```

Parse a scene from a JSON string, running `migrate/1`. Raises on bad input.

# `from_map`

```elixir
@spec from_map(
  map(),
  keyword()
) :: {:ok, t()} | {:error, %{path: list(), reason: term()}}
```

Build a scene from a decoded JSON map (string keys) without raising.

Returns `{:ok, scene}` (after `migrate/1`) or
`{:error, %{path: path, reason: reason}}` where `path` locates the
offending value — e.g. `["elements", 3, "box"]` — so an admin UI can
highlight the corrupt part of a stored scene instead of silently
replacing the whole thing with a blank one.

Options:

  * `:unknown_fields` — `:keep` (default: unknown element fields ride
    along under their string keys, forward-compatible) or `:error`
    (strict mode: the first unknown field fails decoding, with its path).

# `from_map!`

```elixir
@spec from_map!(map()) :: t()
```

Build a scene from a decoded JSON map (string keys), running `migrate/1`. Raises on bad input.

# `gradient`

```elixir
@spec gradient(number(), [gradient_stop()]) :: fill()
```

A linear-gradient fill. `angle` is in degrees, measured clockwise from
"to the right" (0 = left→right, 90 = top→bottom). Each stop is
`%{offset: 0..1, color: "#rrggbb", alpha: 0..1}`.

# `image`

```elixir
@spec image(
  String.t(),
  keyword()
) :: element()
```

A positioned-image element. `:value` resolves to an href (or draws the
unresolved stand-in). Supports an `:underlay` for legibility, same as text.

# `image_fill`

```elixir
@spec image_fill(value(), :cover | :contain | :stretch, keyword()) :: fill()
```

An image fill. `fit` is `:cover`, `:contain`, or `:stretch`. Used as a
canvas `:background`, an optional `:overlay_color` (`:dark`/`:light`) +
`:overlay_opacity` tints the image (og's hero-darkening legibility trick).

# `migrate`

```elixir
@spec migrate(t()) :: t()
```

Migrate an older scene to the current version. Currently the identity for
`"1"`; the seam is here so stored scenes survive format changes.

# `new`

```elixir
@spec new(keyword()) :: t()
```

A new empty scene. `:width` / `:height` default to the 1200×630 OG size;
`:background` is an optional fill (default `nil` = transparent).

# `place`

```elixir
@spec place(keyword()) :: map()
```

A canvas-relative placement (CSS-inset style), resolved against the canvas
size at render time — so a template built with `:place` adapts to any
canvas dimensions (landscape, square, portrait). Pass as an element's
`:place` option; it overrides the static `:box` position on the axes it
specifies.

Keys (all optional, canvas px): `:left` / `:right` / `:top` / `:bottom`
insets from the edges, and `:w` / `:h` explicit sizes. Per axis:

  * both insets → stretches between them (`w = width - left - right`)
  * one inset + size → pinned to that edge at that size
  * size only → centered on that axis
  * neither → keeps the element's `:box` value

## Examples

    # A content strip 64px in from both sides, pinned 48px above the bottom:
    Scene.place(left: 64, right: 64, bottom: 48, h: 200)

    # A centered square badge:
    Scene.place(w: 120, h: 120, top: 40)

# `placeholder`

```elixir
@spec placeholder(String.t()) :: value()
```

A placeholder value, resolved at render time against the `values` map.

# `put_size`

```elixir
@spec put_size(t(), number(), number()) :: t()
```

Set the canvas size (elements with `:place` re-place at render time).

# `resolve`

```elixir
@spec resolve(value(), map()) :: {:ok, any()} | :unresolved
```

Resolve a value against a `values` map.

A literal binary resolves to itself. A placeholder resolves to
`values[key]` when present and non-nil, else `:unresolved`.

# `shape`

```elixir
@spec shape(
  String.t(),
  keyword()
) :: element()
```

A filled (rounded) rectangle element. `:fill` may be solid/gradient/image;
`:stroke` (+ `:stroke_width`) adds a border; `:underlay` a legibility box.

# `solid`

```elixir
@spec solid(String.t()) :: fill()
```

A solid-color fill.

# `stamp`

```elixir
@spec stamp(
  String.t(),
  keyword()
) :: element()
```

A preset-text element — `text/2` with `type: :stamp`. Mirrors og's `stamp`
(a reusable text snippet, e.g. a fixed byline); `:preset` is an alias for
`:value`. Renders identically to text.

# `switch_fill`

```elixir
@spec switch_fill(fill(), :solid | :gradient | :image) :: fill()
```

Switch a fill to another variant type **without data loss** — the schema
decision for fill-variant toggling: the scene retains inactive variants
by construction, so editors don't need side-channel draft state, and a
round-trip `solid → gradient → solid` restores the original color.

The outgoing variant's fields are stashed under the fill's `:variants`
key; the incoming variant restores from there (or from sensible
defaults on first switch). `:variants` is serialized with the scene and
ignored by rendering — cache keys that hash *render output* are
unaffected; hosts hashing raw scene JSON should treat it as part of the
document (it is: it's the author's undo-friendly state).

# `text`

```elixir
@spec text(
  String.t(),
  keyword()
) :: element()
```

A text-block element. Text word-wraps to `:box` width and vertically
aligns via `:valign` (`:top` / `:middle` / `:bottom`). An `:underlay`
(opacity > 0) draws a soft glow behind the text for legibility over busy
backgrounds — `:underlay_color` is `:dark` or `:light`.

# `to_json!`

```elixir
@spec to_json!(t()) :: String.t()
```

Serialize to a JSON string.

# `to_map`

```elixir
@spec to_map(t()) :: map()
```

Serialize to a JSON-ready map (string keys).

---

*Consult [api-reference.md](api-reference.md) for complete listing*
