Cauldron2D.Atlas (Cauldron2D v0.2.2)

Copy Markdown View Source

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.

Summary

Functions

Whether art has more than one frame.

Every art name in the atlas.

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

The entry/0 stored under art.

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.

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.

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

The sprite of art showing at at_ms.

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

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

Whether an atlas has been installed under this name.

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

Start an empty atlas under name.

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.

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

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

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

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

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

Stop receiving changes to name.

Types

art()

@type art() :: term()

entry()

@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()) => FrenchCurve.Sprite.t()},
  poses: %{required(pose()) => [FrenchCurve.Sprite.t()]},
  stand: number() | nil,
  eye: number() | nil
}

face()

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

name()

@type name() :: atom()

pose()

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

rgb()

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

rgba()

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

t()

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

tint()

@type tint() :: atom()

Functions

animated?(atlas, art)

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

Whether art has more than one frame.

arts(atlas)

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

Every art name in the atlas.

cache(name)

@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(atlas, art)

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

The entry/0 stored under art.

Raises ArgumentError when the atlas has no such art.

eye_height(atlas, art)

@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(atlas, art, face)

@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(atlas)

@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(atlas, art, at_ms)

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

The sprite of art showing at at_ms.

frame_index(atlas, art, at_ms)

@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(atlas)

@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?(name)

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

Whether an atlas has been installed under this name.

load(name, dir, opts)

@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(name, opts)

@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(atlas, art, pose, at_ms)

@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(atlas, art, raster, opts \\ [])

@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(atlas, art)

@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(name)

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

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

tallest(atlas)

@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(atlas, name, fun)

@spec tint(t(), tint(), (rgba() -> rgba())) :: t()

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

unsubscribe(name)

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

Stop receiving changes to name.