# `Cauldron2D.Drafter.Surface`
[🔗](https://github.com/jaman/cauldron/blob/v0.2.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}}]` or `[{art, {x, y}, heading}]` in world tile
    coordinates, which may be fractional, `heading` in radians. Default `[]`. In pixel
    mode a mover the size of a tile is drawn at its exact pixel position and a larger one
    stands in that cell, centred across it with its bottom edge on the cell's bottom, as
    `Cauldron2D.Surface` stands a tall overlay; in text mode it replaces the
    glyph of the nearest whole cell; every projection but the plain glyph one poses it
    by its heading, showing the art's `:front`, `:back` or `:side` and mirroring it when
    it faces left
  * `:volumes` — `[%{at: {x, y}, shape: shape, colour: {red, green, blue}, strength: s}]`
    in world tile coordinates: coloured regions of space tinting what is seen through
    them, as `Cauldron2D.Projection.Volume` describes them. Every projection draws
    them, and in text mode they tint the glyphs they cover. Default `[]`
  * `: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;
    `: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;
    or a `FrenchCurve.Backend.Cells` glyph set, `:half`, `:quad`, `:sextant` or
    `:octant`: the frame composed at twice the set's pixels per cell across a tile,
    drawn as block characters in two colours per cell. Default `nil`
  * `:compress` — zlib the kitty transmit. Default `true`
  * `:placements` — `:auto` (default), `true` or `false`. Whether the pane is drawn
    from images the terminal stores between frames rather than one image composited
    every  frame; see below
  * `:projection` — `:top` (default), `:iso` or `:first_person`; see below
  * `:picture` — `fn {width, height}, aspect -> raster end`: a picture the game draws
    itself, in place of the atlas world, at the pane's size in pixels; `aspect` is how
    wide each pixel is on screen against its height. Given, the surface draws it in
    every mode — through the terminal's pixel protocol, or as block glyphs — and
    `:projection` is `:picture`; `:focus`, `:cell` and `:movers` are not read
  * `:native` — `{width, height}`, the picture's own size in pixels. Given, on a kitty or
    iTerm2 terminal the picture is asked for at that size times a whole number
    (`:native_scale`), widened or heightened to the pane's shape, and the terminal
    scales it to the pane, in place of the pane's cells times the density. Default
    `nil`
  * `:native_scale` — `:fit`, the largest whole number at which the picture fits the
    pane's pixels (the terminal's cell size times the pane's cells, where the terminal
    reports one, else the cells times the density), or a positive integer. Default
    `:fit`
  * `:picture_format` — `:rgba` (default), deflated raw pixels; or `:png`, a PNG,
    palette when the frame has at most 256 colours
    (`FrenchCurve.Backend.Kitty.encode/2`), on a kitty terminal that keeps images
  * `:swap` — `true` stores each frame under the one of two kitty image ids not on
    screen, places it and only then deletes the other, as one synchronized update, so
    the pane is never blank between frames, on a kitty terminal that keeps images.
    Default `false`
  * `:facing` — which step of a turn the isometric view is at, `0` to `facings - 1`.
    A fraction is allowed and turns the view by part of a step. Default `0`
  * `:facings` — how many steps make a full turn, as a number for the isometric view or
    a map per projection, `%{iso: 8, oblique: 4}`. The oblique view is fixed at its
    default of one step, and takes `1` or `4`. For the isometric view, at least `4`.
    At `4` the view is drawn by blitting one stamp per art, which is the cheapest
    path; any other number, or a fractional `:facing`, draws the ground and the
    standing arts by sampling, which costs several times as much and is seamless at
    every angle. Default `4`
  * `:tilt` — the isometric diamond's height as a fraction of the tile, `0.5..1.5`.
    Default `1.0`
  * `:heading` — radians, where the first-person eye looks, `0` along `+x`. Default `0.0`
  * `:eye` — how far above the ground the camera sits, in tiles, where the projection
    puts it in the world: first person and mode 7. An art gives its own with
    `Cauldron2D.Atlas.eye_height/2`, so a taller figure sees further over the world.
    Default `nil`, the projection's own
  * `:pitch` — `-1.0..1.0`, how far the first-person horizon is moved. Default `0.0`
  * `:density` — how many pixels a terminal cell is drawn with in a projected view on a
    terminal that scales pictures to cells, before the zoom divides it. More pixels show
    more of a sprite's own detail; what that costs depends on the projection, because the
    isometric and oblique views blit whole sprites while first person and mode 7 sample
    every pixel. A map from projection to number sets each separately, which is how a game
    keeps a crisp isometric view without paying for it in first person. Default: half the
    atlas's tile size, and `4` in first person and mode 7
  * `: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`

## Projections

`:iso` draws `Cauldron2D.Projection.Iso` with the focus at the pane's centre: floors as
diamonds, arts with a `height` as blocks, movers and flat overlays upright, turned by
`:facing` and tilted by `:tilt`; zoomed out it shrinks the whole picture. `:first_person`
draws `Cauldron2D.Projection.FirstPerson` from the focus along `:heading`, with
`:pitch`. Both are drawn as an image on a pixel terminal and as block characters or
braille otherwise; plain glyphs (`:text`) stay top-down. Placements are not used.
`world_at/3` maps a pane cell back to the ground for the top and isometric views.

## 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 isometric and oblique panes are placed the same way while no volume is up: the pane
is stored once at a quarter again its size, and a frame that only moved the camera
shows that same image through a shifted source rectangle. Everything that moves is a
patch — a small picture of the pane's own pixels around each mover, composed the way the
whole frame is, so a mover behind a pillar is behind it in the patch too. Patches that
touch are sent as one, and a frame whose moving parts cover too much of the pane is
composited whole instead.

The protocol is `protocol/1`'s; whether the terminal places is
`FrenchCurve.Capability.placements?/1` asked of the session's environment, so an iTerm2
drawing with kitty graphics, which stores but never places, is composited every frame.

# `t`

```elixir
@type t() :: %Cauldron2D.Drafter.Surface{
  atlas: term(),
  cell: term(),
  compress: term(),
  density: term(),
  eye: term(),
  facing: term(),
  facings: term(),
  focus: term(),
  frame: term(),
  heading: term(),
  labels: term(),
  layers: term(),
  mode: term(),
  movers: term(),
  native: term(),
  native_scale: term(),
  on_geometry: term(),
  picture: term(),
  picture_format: term(),
  pitch: term(),
  placements: term(),
  projection: term(),
  rect: term(),
  swap: term(),
  tilt: term(),
  volumes: term(),
  widget_id: term(),
  zoom: term()
}
```

# `cell_at`

```elixir
@spec cell_at(t(), map(), {number(), number()}) :: {float(), float()} | nil
```

The pane cell a world point is drawn at, as `world_at/3` reads one back.

`{column, row}` relative to the pane's top-left corner, fractional, so a caller can see
where in the cell the point falls. `nil` in first person, and for a point the
projection cannot show. Round-trips with `world_at/3`.

# `component_tag`

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

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

# `drawn_mode`

```elixir
@spec drawn_mode(t()) :: atom()
```

The mode the surface actually draws in: a cell set, `:braille`, a pixel protocol, or
`:text` where there is no picture to draw.

# `fitted`

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

The surface as a picture protocol draws it: zooming in is fewer pixels for the same
cells, and zooming out, where the projection can draw its own world smaller, is the
same pixels with a smaller world in them.

A terminal that scales pictures to cells has no other way to zoom in than to hand it a
smaller picture. Zooming out that way costs four times the pixels a step, which is a
picture nobody can transmit thirty times a second; a projection that takes a `:shrink`
draws more world into the picture it already had instead.

# `fitting_zoom`

```elixir
@spec fitting_zoom(t(), pos_integer()) :: number()
```

The zoom a projection is actually drawn at in a picture `height` pixels tall.

The zoom asked for, rounded to a level the projection draws exactly, then pulled back
until the tallest art stands in the half of the picture above its feet with a tile to
spare, so a figure at the middle of the picture has room over its head. In the cell and
braille modes a picture is only a few dozen pixels tall, so the closest zoom would
otherwise show a figure's boots and nothing above them.

# `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.

# `image_active?`

```elixir
@spec image_active?(t()) :: boolean()
```

Whether `image/3` draws anything: a pixel protocol is in use and the view is set.

# `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/2` and `Cauldron2D.Camera.tile_at/2`, are what maps a screen
position back to a world tile.

Returns the state carrying the new rect.

# `picture`

```elixir
@spec picture(t(), map()) ::
  {{number(), number()}, {number(), number()}, number()} | nil
```

The picture a projection is drawn into for `rect`, in the mode the surface is in.

`{{width, height}, {across, down}, aspect}`: the picture's size in pixels, how many of
those pixels a terminal cell and a terminal row cover, and the pixel aspect the
projection is given. `nil` in the plain glyph mode, which draws the grid from above
whatever the projection is.

# `preferred_height`

# `projections`

```elixir
@spec projections() :: [atom()]
```

The projections a surface draws the atlas world in: `:top`, `:oblique`, `:iso`, `:mode7`
and `:first_person`. A surface given a `:picture` draws that instead, as `:picture`.

# `protocol`

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

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

A forced `:mode` naming a protocol or a glyph rendering wins; then `CAULDRON_MODE` and
the application configuration, through `Cauldron2D.Renderer.resolved/1`; and where
those leave it to the terminal, `Drafter.Widget.Chart.Pixel.terminal/0`: what the
session's terminal answered the startup probe, or its environment when it was not
probed, with iTerm2 always judged by its environment.

# `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`

# `world_at`

```elixir
@spec world_at(t(), map(), {number(), number()}) :: {float(), float()} | nil
```

The world point under pane cell `{column, row}`, on the ground, for the view the
surface draws; `nil` in first person, where a cell is not a place. A `:picture` surface
gives the point of its picture there instead — the cell's centre for whole cells, the
point itself for fractional ones (a pixel-precise pointer) — as fractions of the
picture's width and height (`{0.0, 0.0}` the top-left corner); with a `:frame` it gives
the point of the frame `Cauldron2D.Fit.letterbox/3` places in the picture, `nil` on
the bars around it.

# `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.

Those are the top-down projection's, and `zoom_levels/3` gives another projection's:
isometric, 2 down to a quarter; oblique, 2 down to a half; first person and mode 7,
`1` alone. The isometric view draws its own world smaller to zoom out, so its steps
out cost nothing; see `fitted/1`.

# `zoom_levels`

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

The zoom levels the surface draws `projection` at exactly, largest first; see `zoom_levels/2`.

---

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