# `Ascii.Player`
[🔗](https://github.com/houllette/ascii_art/blob/main/lib/ascii/player.ex#L1)

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.

# `child_spec`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

Holds the current frame; play time stops.

# `ref`

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

The tag on this player's frame messages.

# `resume`

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

Plays on from where it paused.

# `set_fps`

```elixir
@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`

```elixir
@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`

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

Starts a player without a link.

# `start_link`

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

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

# `stop`

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

Stops the player.

# `subscribe`

```elixir
@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`

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

Stops sending the caller frames.

---

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