# `Cauldron2D.Atlas`
[🔗](https://github.com/jaman/cauldron/blob/v0.1.3/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 tile in one atlas is the same pixel size. `put/4` raises otherwise.
  * `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.

`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()
}
```

# `name`

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

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

# `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 picture exactly one tile square is that art. One wider than a tile, in whole tiles,
is a strip of frames left to right. A `.pic` smaller than a tile is centred in one.
Any other size is `{:error, {:art, file, :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.

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

Every raster must be exactly the atlas's tile size in both dimensions; `put/4` raises
`ArgumentError` otherwise, and on an empty list.

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

# `subscribe`

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

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

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