# `Cauldron2D.Drafter.Surface`
[🔗](https://github.com/jaman/cauldron/blob/v0.1.3/cauldron_2d_drafter/lib/cauldron_2d/drafter/surface.ex#L1)

A drafter widget that draws a `Cauldron2D` surface.

    {:cauldron_surface,
     [
       id: :map,
       atlas: :my_game,
       focus: {x, y},
       cell: &MyGame.cell/1,
       movers: [{:ball, {7.42, 3.15}}],
       flex: 1
     ]}

Register it before use, after loading it:

    Code.ensure_loaded!(Cauldron2D.Drafter.Surface)
    Drafter.Widget.Registry.register(Cauldron2D.Drafter.Surface)

Where `Cauldron2D.Renderer` finds a pixel protocol, `image/3` hands drafter one composited
image and `render/2` fills the pane with blank cells beneath it. Otherwise `image/3` returns
`nil` and `render/2` draws two-cell glyphs. Both read the same camera view, sized from the
pane by `Cauldron2D.Camera.columns_for/1` and `Cauldron2D.Camera.rows_for/1`.

Until both `:focus` and `:cell` are set, `render/2` fills the pane with black and `image/3`
returns `nil`.

## Options

  * `:atlas` — the name an atlas was installed under with `Cauldron2D.Atlas.install/1`
  * `:focus`, `:cell` — passed to `Cauldron2D.Camera.view/1`
  * `:movers` — `[{art, {x, y}}]` in world tile coordinates, which may be fractional.
    Default `[]`. In pixel mode a mover is drawn at its exact pixel position; in text mode
    it replaces the glyph of the nearest whole cell
  * `:labels` — `[{text, {x, y}, colour}]` or `[{text, {x, y}, colour, opts}]` in world
    tile coordinates, as `Cauldron2D.Surface.compose/4` takes them: names, numbers,
    notes drawn over everything. Default `[]`. In pixel mode a label is drawn in
    `Linocut.Font` at its exact pixel position, with placements as a stored image of its
    own; in text mode its characters overlay the glyphs from the nearest cell
  * `:zoom` — how many times its native size a tile is drawn: `2` twice as large,
    `0.5` half, so a pane shows half or twice the tiles. Default `1`. Levels a
    surface draws exactly are `zoom_levels/2`'s; another is drawn at the nearest of
    them. Zoomed in, a pixel terminal that can fit an image to cells is sent the
    smaller picture to scale, sixels are scaled first; zoomed out, the frame is
    composed at the lower resolution. In text mode a tile zoomed in covers that many
    more cells; zoomed out, every that-manyeth tile is drawn. Movers are placed by
    the terminal (placements) only at native size
  * `:mode` — a `Cauldron2D.Renderer` mode to force instead of detecting one, or
    `:braille`: the frame composed at four dots per tile side, pixels at or above a
    luminance of 64 lit, drawn as braille text in the colour of each cell's lit pixels.
    Default `nil`
  * `:compress` — zlib the kitty transmit. Default `true`
  * `:placements` — `:auto` (default), `true` or `false`. Whether movers are drawn as
    stored sprites placed at pixel offsets rather than composited into the frame; see
    below
  * `:on_geometry` — an event name sent to the application whenever the pane's rect changes,
    see `on_rect_change/2`. Default `nil`
  * `:image_throttle`, `:image_priority` — how often a new frame is composed and at
    what scheduler priority, as drafter's widget server takes them. Defaults: every
    second application frame, `:low`. A game animating at the application's frame rate
    passes `image_throttle: {1, :tick}, image_priority: :normal`

## Scrolling

In both pixel renderings the focus sits at the exact centre of the pane: the grid is
composed one tile wider and taller than the pane and shown through a window shifted by
the fraction of a tile the focus is past a tile boundary, and movers are placed
relative to that same fractional origin. Text mode scrolls by whole tiles.

## Animation

An art with frames (`Cauldron2D.Atlas.put/4` with a list) is drawn at the frame showing
on the session's monotonic clock, in every rendering but glyphs. The grid image of the
placement rendering is sent again when a frame changes; a mover with frames is placed
by the frame's own stored image.

## Placements

On a kitty terminal whose cell size the session knows
(`Drafter.Session.Context.cell_size/0`, asked for with `use Drafter.App, cell_size: true`),
the grid is one stored image, fitted to the pane by the terminal and sent again only
when its tiles or the atlas change, and each mover is a placement of a sprite stored by
`Cauldron2D.Drafter.Sprites.transmit/2` — a few dozen bytes per mover per frame, no
pixels. The surface stores the sprites itself, once per atlas and cell size, resampled
to the two cells by one row a tile occupies, so the terminal draws a sprite pixel for
pixel wherever it is placed. Elsewhere the movers are composited into one image per
frame.

The protocol is the one the session's terminal answered the startup probe with, or
`Cauldron2D.Renderer`'s detection when there was no probe.

# `t`

```elixir
@type t() :: %Cauldron2D.Drafter.Surface{
  atlas: term(),
  cell: term(),
  compress: term(),
  focus: term(),
  labels: term(),
  mode: term(),
  movers: term(),
  on_geometry: term(),
  placements: term(),
  rect: term(),
  widget_id: term(),
  zoom: term()
}
```

# `component_tag`

```elixir
@spec component_tag() :: atom()
```

The tag this widget answers to in a component tree: `:cauldron_surface`.

# `focused`

# `handle_event`

# `image`

```elixir
@spec image(t(), map(), term()) :: {iodata(), iodata(), map()} | nil
```

Produce the frame as terminal-graphics bytes, or `nil` when there is nothing to draw.

Returns `nil` when `Cauldron2D.Renderer` finds no pixel protocol, when `:focus` or `:cell`
is unset, or when composing the frame raises.

A frame that raises is logged with its stacktrace before `nil` is returned, so a missing
atlas or a bad mover shows up as an error rather than as a pane that is silently blank.
Frames are drawn many times a second, so the same message is logged once per widget process
rather than on every frame.

Otherwise returns `{paint, clear, region}`:

  * `paint` — the bytes that draw this frame, which already begin with `clear`
  * `clear` — the bytes that remove this frame, empty for protocols that leave no stored
    image behind
  * `region` — `%{dx: 0, dy: 0, cols: cols, rows: rows, place: place}` covering the pane.
    `place` re-asserts an already-transmitted image's placement, and is `nil` for protocols
    whose bytes are the pixels themselves

`id` identifies the image to the terminal across frames, so successive calls for the same
widget must pass the same `id`.

`FrenchCurve.frame/3` builds the bytes, which matters because not every terminal detected as
`:kitty` keeps images under an id — iTerm does not, and a surface animated as though it did
would never move.

# `mount`

```elixir
@spec mount(map() | keyword()) :: t()
```

Build the widget's state from its props.

Props may be a map or a keyword list. Every key falls back to its documented default when
absent, so `compress: false` means false rather than the default.

# `on_rect_change`

```elixir
@spec on_rect_change(map(), t()) :: t()
```

Record the pane's rect, and report it when it differs from the one already held.

With `:on_geometry` set and a drafter application running, the application is sent
`%{rect: rect, size: {columns, rows}}`, where `size` is the viewport in tiles. Those two,
with `Cauldron2D.Camera.origin/3` and `Cauldron2D.Camera.tile_at/2`, are what maps a screen
position back to a world tile.

Returns the state carrying the new rect.

# `preferred_height`

# `protocol`

```elixir
@spec protocol(t()) :: :kitty | :iterm2 | :sixel | nil
```

The pixel protocol this surface draws with, or `nil` for glyphs.

A forced `:mode` wins; then the protocol the session's terminal answered the probe
with; then, when the terminal was not probed or named no protocol,
`Cauldron2D.Renderer.protocol/1`.

# `unmount`

# `update`

```elixir
@spec update(map() | keyword(), t()) :: t()
```

Fold re-rendered props into the widget state.

Every option the widget takes can be changed by a re-render, not only the camera ones. A key
present in `props` is used even when its value is `nil` or `false`, so a mode that was forced
can be unforced and `compress` can be turned off. A key absent from `props` leaves that part
of the state as it was.

# `update_props_from_mount`

# `zoom_levels`

```elixir
@spec zoom_levels(Cauldron2D.Atlas.name(), atom() | nil) :: [number()]
```

The zoom levels a surface draws exactly, largest first: in `:text` mode and the pixel
modes (`:kitty`, `:iterm2`, `:sixel`, or `nil` for whichever is detected) 4 down to a
sixteenth, kept to the divisors of the atlas's tile size zoomed out; in `:braille` mode
those where a tile is a whole number of dots, from the tile's size in dots down to one.

---

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