The piece contract, upstream's in Elixir.
Upstream, a piece's default function takes the options, precomputes, and
returns a closure frame(t, env). Here init/1 is the outer function: it
receives every option (the defaults merged with what was asked for) and
returns a state. frame/3 is the closure: given the state, the play time
t in seconds and the Ascii.Env, it returns the picture as iodata,
the colours, and the next state.
The picture is rows lines of exactly cols characters joined by \n.
Colours are nil unless the piece has a palette and env.color? is set;
then they are a binary of cols * rows palette indices, row by row.
State is threaded because upstream closures keep state between frames: a
simulation that steps by how far t moved, a cache of the last minute
drawn. A piece behaves exactly as its upstream closure does when called with
the same sequence of t, including t going backwards.
Example
defmodule MySpinner do
@behaviour Ascii.Piece
@meta %Ascii.Meta{
name: "spinner",
category: :ui,
note: "a line turning while something loads",
cols: 10,
rows: 1,
fps: 6
}
@impl true
def meta, do: @meta
@impl true
def init(_options), do: nil
@impl true
def frame(state, t, _env) do
{[Enum.at(~w(| / - \\), rem(floor(t * 3), 4)), " loading "], nil, state}
end
end
Summary
Callbacks
What the piece shows, in words, for a screen reader: read off the
options, the data a component draws ("load: 72%", "boot log: 9 ok, 1
warning"). nil when the options hold nothing to tell (the piece plays
its own script), for Ascii.describe/1 to say what the piece is.
The picture at t seconds, its colours, and the next state.
The options after an event, for a piece that can be used: a key or a
click (see Ascii.event/0) moving a cursor, opening a folder,
ticking a box. What it changes is in the options it returns, so an app
reads what was chosen from them. :ignore for an event it has no use
for.
Precomputes from every option, merged with the defaults.
The piece's Ascii.Meta.
The parts of a frame in one ink, for a piece whose meta lists parts:
each row as runs of {name, text} making up the row (see
Ascii.Parts). Given the frame as drawn, and the options it was drawn
with; called only when parts are to be coloured.
The frame's size for these options, {cols, rows}, for a piece whose size
follows its options (a width to fit a terminal, a row a series). Without
it, every frame is the meta's size.
The state for new options while playing (see Ascii.put_options/2),
keeping what it can of state: a needle swinging to a new reading, a
scroll carrying on. Without it, init/1 starts afresh.
Callbacks
What the piece shows, in words, for a screen reader: read off the
options, the data a component draws ("load: 72%", "boot log: 9 ok, 1
warning"). nil when the options hold nothing to tell (the piece plays
its own script), for Ascii.describe/1 to say what the piece is.
@callback frame(state :: term(), t :: float(), env :: Ascii.Env.t()) :: {iodata(), colors :: binary() | nil, state :: term()}
The picture at t seconds, its colours, and the next state.
@callback handle_event(event :: Ascii.event(), options :: map()) :: {:ok, options :: map()} | :ignore
The options after an event, for a piece that can be used: a key or a
click (see Ascii.event/0) moving a cursor, opening a folder,
ticking a box. What it changes is in the options it returns, so an app
reads what was chosen from them. :ignore for an event it has no use
for.
Precomputes from every option, merged with the defaults.
@callback meta() :: Ascii.Meta.t()
The piece's Ascii.Meta.
@callback parts(frame :: Ascii.Frame.t(), options :: map()) :: Ascii.Parts.t()
The parts of a frame in one ink, for a piece whose meta lists parts:
each row as runs of {name, text} making up the row (see
Ascii.Parts). Given the frame as drawn, and the options it was drawn
with; called only when parts are to be coloured.
@callback size(options :: map()) :: {cols :: pos_integer(), rows :: pos_integer()}
The frame's size for these options, {cols, rows}, for a piece whose size
follows its options (a width to fit a terminal, a row a series). Without
it, every frame is the meta's size.
The state for new options while playing (see Ascii.put_options/2),
keeping what it can of state: a needle swinging to a new reading, a
scroll carrying on. Without it, init/1 starts afresh.