Ascii.Player (Ascii v0.3.0)

Copy Markdown View Source

Plays a piece in real time: a process that draws a frame every 1/fps seconds and hands it on.

{:ok, player} = Ascii.Player.start_link(piece: "donut")

receive do
  {:ascii_frame, _ref, frame} -> IO.write(Ascii.Render.ANSI.render(frame))
end

Where frames go

Started with start_link/1 or start/1, a player sends its frames to the process that started it (or :to) as {:ascii_frame, ref, %Ascii.Frame{}}, and stops when that process, its owner, exits. Given a :callback, it calls that instead, in the player's process, and has no owner.

Any process can also subscribe/1 to a player and receive the same messages until it unsubscribes or exits, and frame/1 returns the latest frame. That is how one player serves many viewers:

children = [
  {Ascii.Player, piece: "elixir", name: MyApp.Logo}
]

# in each LiveView
def mount(_params, _session, socket) do
  if connected?(socket), do: Ascii.Player.subscribe(MyApp.Logo)
  {:ok, assign(socket, frame: Ascii.Player.frame(MyApp.Logo))}
end

def handle_info({:ascii_frame, _ref, frame}, socket),
  do: {:noreply, assign(socket, frame: frame)}

Under a supervisor (child_spec/1) a player has no owner unless given :to: it sends nothing to the supervisor, only to its callback and its subscribers. A player with no one to send to holds its frame and does no work, and play time waits with it, until someone subscribes.

Time

Play time advances as upstream's mount advances it: by the real time since the last frame, at most 100 ms a frame, and not at all while paused. Ticks are scheduled against the monotonic clock, so they keep their pace rather than drift by the time each frame takes to draw.

start_link/1 checks the piece and its options, and returns their error if there is one, without starting a process. It returns as soon as the player has started; the player then sets the piece up (a scene takes up to a second, more on a small board) before it does anything else, so a call made meanwhile waits for it. A supervisor starting a scene's player is not held up.

A piece that closes a loop (Ascii.Meta's loop), played with its default options and frame rate, is drawn once a frame of its cycle, as play first reaches each, and then repeated, so after one cycle it costs nothing to play: the frames are kept, up to a megabyte. Frames come at the cycle's places rather than at the exact play time, which is every 1/fps of a second anyway; a frame's t is still the play time.

It needs nothing from Phoenix, so it serves a terminal app (see mix ascii.play) or a device: on Nerves, start it under your own supervisor and draw frame.lines and frame.colors to the display in the callback.

Options

  • :piece (required) - a slug or module.
  • :to - the owner: the pid to send frames to, which the player stops with. The caller by default, or no one under a supervisor; nil for no one.
  • :callback - a function of one frame, instead of messages to an owner.
  • :ref - the tag on each message (a fresh reference by default; see ref/1).
  • :cache - keep and repeat the frames of a loop (default true).
  • :options, :fps, :paper, :color, :ink, :gradient, :parts - as for Ascii.new/2.
  • :name - registers the player. Under a supervisor it is also the child's id, so one supervisor can hold several named players.

Summary

Functions

A child spec for a supervisor: {Ascii.Player, piece: "donut", name: MyApp.Donut}.

The latest frame drawn. Frame 0 until the player has played; while the piece is still being set up, this waits for it, up to timeout ms.

A key or a click for the piece (see Ascii.handle_event/2): its new options if it took the event, the new picture drawn and sent at once; :ignore if it had no use for it.

Holds the current frame; play time stops.

The tag on this player's frame messages.

Plays on from where it paused.

Changes the frame rate, 0 to 60; 0 holds the picture.

New options for the piece, merged over its own (see Ascii.put_options/2): for live data, a reading or the latest samples. The new picture is drawn and sent at once.

Starts a player without a link.

Starts a player linked to the caller. See the module docs for options.

Stops the player.

Sends the caller every frame from now on, as the owner gets them, until it calls unsubscribe/1 or exits. Subscribing twice changes nothing.

Stops sending the caller frames.

Functions

child_spec(init_arg)

@spec child_spec(opts :: keyword()) :: Supervisor.child_spec()

A child spec for a supervisor: {Ascii.Player, piece: "donut", name: MyApp.Donut}.

The player sends frames to no one by default, rather than to the supervisor that starts it; see the module docs.

frame(player, timeout \\ 5000)

@spec frame(player :: GenServer.server(), timeout :: timeout()) :: Ascii.Frame.t()

The latest frame drawn. Frame 0 until the player has played; while the piece is still being set up, this waits for it, up to timeout ms.

handle_event(player, event)

@spec handle_event(player :: GenServer.server(), event :: Ascii.event()) ::
  {:ok, options :: map()} | :ignore

A key or a click for the piece (see Ascii.handle_event/2): its new options if it took the event, the new picture drawn and sent at once; :ignore if it had no use for it.

pause(player)

@spec pause(player :: GenServer.server()) :: :ok

Holds the current frame; play time stops.

ref(player)

@spec ref(player :: GenServer.server()) :: term()

The tag on this player's frame messages.

resume(player)

@spec resume(player :: GenServer.server()) :: :ok

Plays on from where it paused.

set_fps(player, fps)

@spec set_fps(player :: GenServer.server(), fps :: number()) ::
  :ok | {:error, {:invalid_option, :fps}}

Changes the frame rate, 0 to 60; 0 holds the picture.

set_options(player, options)

@spec set_options(player :: GenServer.server(), options :: map() | keyword()) ::
  :ok | Ascii.error()

New options for the piece, merged over its own (see Ascii.put_options/2): for live data, a reading or the latest samples. The new picture is drawn and sent at once.

start(opts)

@spec start(opts :: keyword()) :: GenServer.on_start() | Ascii.error()

Starts a player without a link.

start_link(opts)

@spec start_link(opts :: keyword()) :: GenServer.on_start() | Ascii.error()

Starts a player linked to the caller. See the module docs for options.

stop(player)

@spec stop(player :: GenServer.server()) :: :ok

Stops the player.

subscribe(player, timeout \\ 5000)

@spec subscribe(player :: GenServer.server(), timeout :: timeout()) :: :ok

Sends the caller every frame from now on, as the owner gets them, until it calls unsubscribe/1 or exits. Subscribing twice changes nothing.

unsubscribe(player)

@spec unsubscribe(player :: GenServer.server()) :: :ok

Stops sending the caller frames.