# `Cauldron2D.Drafter.Surface`
[🔗](https://github.com/jaman/cauldron/blob/v0.1.2/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. In the pixel modes a tile is drawn at the whole multiple of its pixels
    nearest to `zoom` rows of cells, or shrunk by a divisor of its size below half
    a tile, so the terminal scales nothing (`Cauldron2D.Drafter.Surface.Scale`);
    where the session does not know the cell size the frame is fitted to the
    pane's cells by the terminal instead. In text mode a tile zoomed in covers that
    many more cells; zoomed out, every that-manyeth tile is drawn
  * `: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 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, at the size they are drawn, once per size and atlas. Zoomed out below half a
tile, and 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*
