# `Cauldron2D.Atlas`
[🔗](https://github.com/jaman/cauldron/blob/v0.2.2/cauldron_2d/lib/cauldron_2d/atlas.ex#L1)

A named catalogue of tiles.

Each entry holds a sprite — or a run of frames shown in turn — a glyph and a colour,
which are what the pixel renderer and the text renderer respectively draw.

    Cauldron2D.Atlas.new(:my_game, tile: 16, void: {6, 7, 11})
    |> Cauldron2D.Atlas.put(:floor, floor_raster(), glyph: "· ", color: {60, 66, 88})
    |> Cauldron2D.Atlas.put(:player, player_raster(), glyph: "@ ", color: {245, 235, 200})
    |> Cauldron2D.Atlas.put(:torch, [flame_1(), flame_2(), flame_3()], fps: 8, glyph: "i ")
    |> Cauldron2D.Atlas.tint(:dim, fn {r, g, b, a} -> {div(r, 3), div(g, 3), div(b, 3), a} end)
    |> Cauldron2D.Atlas.install()

Or from a directory of pictures:

    {:ok, atlas} = Cauldron2D.Atlas.load(:my_game, "priv/art", tile: 16, arts: [floor: [glyph: "· "]])
    Cauldron2D.Atlas.install(atlas)

Rules a caller must respect:

  * Every base tile in one atlas is the atlas's tile size, as is every face; an overlay
    or a mover may be any size, all its frames alike, and stands on its tile. `put/4`
    raises on frames that disagree or a face that is not a tile.
  * `install/1` must be called before any renderer refers to the atlas by name. It publishes
    the atlas to `:persistent_term` and creates the ETS table named by `cache/1`.
  * Functions taking an atlas accept either the name or the struct; `fetch/1` resolves both.

A tint is a named function over RGBA. It is applied when a tile is composited, and the
result is cached under the tint's name.

An animated art has `frames` and an `fps`; `frame_at/3` is the frame showing at a time
in milliseconds, and `sprite` is its first frame. Every reader that does not take a
time draws the first frame.

An art may stand up: `height:` storeys tall with `faces:` for its top and sides, which
the isometric and first-person projections draw as a block; and a mover may have
`poses:`, pictures for the ways it can face the camera. `face/3` and `pose/4` read
them, falling back to the art's own picture.

`subscribe/1` delivers `{:cauldron_atlas, name, :changed}` to the calling process each
time a different atlas is installed under `name`, including the first.

# `art`

```elixir
@type art() :: term()
```

# `entry`

```elixir
@type entry() :: %{
  sprite: FrenchCurve.Sprite.t(),
  frames: [FrenchCurve.Sprite.t()],
  fps: pos_integer(),
  glyph: String.t(),
  color: rgb(),
  size: {pos_integer(), pos_integer()},
  height: non_neg_integer(),
  faces: %{required(face()) =&gt; FrenchCurve.Sprite.t()},
  poses: %{required(pose()) =&gt; [FrenchCurve.Sprite.t()]},
  stand: number() | nil,
  eye: number() | nil
}
```

# `face`

```elixir
@type face() :: :top | :side | :north | :east | :south | :west
```

# `name`

```elixir
@type name() :: atom()
```

# `pose`

```elixir
@type pose() :: :front | :back | :side
```

# `rgb`

```elixir
@type rgb() :: {0..255, 0..255, 0..255}
```

# `rgba`

```elixir
@type rgba() :: {0..255, 0..255, 0..255, 0..255}
```

# `t`

```elixir
@type t() :: %Cauldron2D.Atlas{
  entries: %{required(art()) =&gt; entry()},
  name: name(),
  tile: pos_integer(),
  tints: %{required(tint()) =&gt; (rgba() -&gt; rgba())},
  void: rgb()
}
```

# `tint`

```elixir
@type tint() :: atom()
```

# `animated?`

```elixir
@spec animated?(t() | name(), art()) :: boolean()
```

Whether `art` has more than one frame.

# `arts`

```elixir
@spec arts(t()) :: [art()]
```

Every art name in the atlas.

# `cache`

```elixir
@spec cache(name()) :: atom()
```

The name of the ETS table holding composited tiles for the atlas `name`.

The table exists only after `install/1`.

# `entry`

```elixir
@spec entry(t(), art()) :: entry()
```

The `t:entry/0` stored under `art`.

Raises `ArgumentError` when the atlas has no such art.

# `eye_height`

```elixir
@spec eye_height(t(), art()) :: float()
```

How far above the ground `art` looks from, in tiles: its `:eye` where it has one, else a
fixed fraction of how tall it stands.

# `face`

```elixir
@spec face(t() | name(), art(), face()) :: FrenchCurve.Sprite.t()
```

The picture of `art`'s `face`: `:top`, `:side`, or a compass side, which falls back to
`:side`; any face the art has no picture for is its own picture.

# `fetch`

```elixir
@spec fetch(name() | t()) :: t()
```

The atlas struct for `name`, or the struct itself when given one.

Raises `ArgumentError` when nothing is installed under `name`.

# `frame_at`

```elixir
@spec frame_at(t() | name(), art(), integer()) :: FrenchCurve.Sprite.t()
```

The sprite of `art` showing at `at_ms`.

# `frame_index`

```elixir
@spec frame_index(t() | name(), art(), integer()) :: non_neg_integer()
```

Which of `art`'s frames shows at `at_ms`; `0` for an art with one frame.

# `install`

```elixir
@spec install(t()) :: name()
```

Publish the atlas to `:persistent_term` and create its composited-tile cache table.

Idempotent, and safe to call again with a changed atlas. Installing the same atlas twice
keeps every tile already composited. Installing a *different* one under the same name empties
the cache first, so art or a tint that has been redefined is composited again rather than
served as it used to look, and tells every subscriber of the name.

The table itself is created once, owned by `Cauldron2D.Atlas.Tables` rather than the
caller, and kept, so a reference taken from `cache/1` stays valid across re-installs and
after the installing process has gone.

Returns the atlas name.

# `installed?`

```elixir
@spec installed?(name()) :: boolean()
```

Whether an atlas has been installed under this name.

# `load`

```elixir
@spec load(name(), Path.t(), keyword()) :: {:ok, t()} | {:error, term()}
```

An atlas from every `.png` and `.pic` file in `dir`, each art named by its file.

A `.pic` gives its frames as it draws them, at their own size; one smaller than a tile
is centred in one. A `.png` exactly one tile square is that art; one wider than a
tile, in whole tiles, is a strip of frames left to right; any other size is one art of
that size. A file that will not read is `{:error, {:art, file, reason}}`.

An `atlas.txt` in the directory (`Cauldron2D.Atlas.Manifest`) gives arts their glyph,
colour and rate; `:arts` lays over it. The glyph of an art named nowhere is the first
two characters of its name in capitals, and its colour the mean of its first frame's
opaque pixels. An art the manifest derives from another (`Linocut.Derive`) is built
after the pictures, from a `.pic` in the directory or another derived art; one that
cannot be made is `{:error, {:art, name, message}}`.

## Options

  * `:tile` — as `new/2`, required
  * `:void` — as `new/2`
  * `:arts` — `[art: [glyph: "..", color: {r, g, b}, fps: n]]`, per-art options over
    the defaults. Default `[]`
  * `:fps` — the rate of every strip not given one in `:arts`. Default `8`

# `new`

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

Start an empty atlas under `name`.

## Options

  * `:tile` — the pixel width and height of every tile, required
  * `:void` — the colour of a cell with nothing in it, default `{0, 0, 0}`

Raises `ArgumentError` when `:tile` is missing or is not a positive integer.

# `pose`

```elixir
@spec pose(t() | name(), art(), pose(), integer()) :: FrenchCurve.Sprite.t()
```

The frame of `art` showing at `at_ms` in `pose`, `:front`, `:back` or `:side`, or of
its own frames when it has no such pose.

# `put`

```elixir
@spec put(t(), art(), FrenchCurve.Raster.t() | [FrenchCurve.Raster.t()], keyword()) ::
  t()
```

Add the tile `art`, drawn from `raster` — or from a list of rasters shown in turn —
replacing any entry already under that name.

A raster may be any size, every frame the same; a face must be the atlas's tile size.
`put/4` raises `ArgumentError` otherwise, and on an empty list. An art larger than a
tile can only be an overlay or a mover, never a cell's base.

## Options

  * `:glyph` — what a text renderer draws for this tile, two columns wide because a tile
    occupies two terminal cells across. Default `"??"`
  * `:color` — the glyph's `{r, g, b}` colour before tinting, default `{255, 0, 255}`
  * `:fps` — frames a second when `raster` is a list. Default `8`
  * `:height` — how many storeys the art stands, `0` for something flat on the ground.
    Default `0`
  * `:faces` — `%{top: raster, side: raster}`, the pictures of a standing art's top and
    of each of its sides, either omitted, with `:north`, `:east`, `:south` or `:west`
    for one side that differs (`:north` faces `-y`); a face left out is the `:side`
    face, then the art's own picture. Default `%{}`
  * `:poses` — `%{front: raster | [raster], back: ..., side: ...}`, the pictures of a
    mover facing the camera, away from it, and across it (drawn mirrored for the other
    way); a pose left out is the art's own frames. Default `%{}`
  * `:stand` — how tall the art is in tiles when a projection draws it as a
    billboard, its width in proportion. Default `nil`: its picture's height over the
    tile size
  * `:eye` — how far above the ground the art looks from, in tiles, for a projection
    that puts the camera where it stands. Default `nil`: 90% of
    how tall it stands, so a taller art sees further over the world and a shorter one
    sees less

# `standing`

```elixir
@spec standing(t(), art()) :: float()
```

How tall `art` stands, in tiles: its `:stand` where it has one, else its picture height
over the tile size.

# `subscribe`

```elixir
@spec subscribe(name()) :: :ok
```

Receive `{:cauldron_atlas, name, :changed}` in the calling process whenever a different atlas is installed under `name`.

# `tallest`

```elixir
@spec tallest(t() | name()) :: pos_integer()
```

How tall the tallest art of the atlas stands, in pixels.

Counts an art's own picture and its poses, so a mover drawn taller than a tile is
included. A caller sizing a picture uses it to keep the tallest thing in view.

# `tint`

```elixir
@spec tint(t(), tint(), (rgba() -&gt; rgba())) :: t()
```

Register `fun`, a function from an RGBA tuple to an RGBA tuple, under `name`.

# `unsubscribe`

```elixir
@spec unsubscribe(name()) :: :ok
```

Stop receiving changes to `name`.

---

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