FrenchCurve (FrenchCurve v0.1.3)

Copy Markdown View Source

Draws pixel rasters to a terminal over whichever graphics protocol it supports.

A FrenchCurve.Raster is built with FrenchCurve.Draw or FrenchCurve.Chart, then handed to one of four backends: :kitty, :sixel, :iterm2 or :braille. FrenchCurve.Capability.detect/0 picks one from the environment when none is named.

raster =
  FrenchCurve.Raster.new(64, 32)
  |> FrenchCurve.Draw.circle({32, 16}, 12, {255, 0, 0, 255})

IO.write(FrenchCurve.to_terminal(raster, :kitty))

render/2 and render/3 return each backend's native form, which differs by protocol: the three pixel backends return an escape-sequence binary, :braille returns cell rows. to_terminal/3 returns a binary for every protocol and is what you write to the device.

For a region that is drawn over and over — a meter, a scope, anything animated — use frame/3 rather than building the sequence yourself. It picks between the two kitty dialects, which is not something a caller can tell from FrenchCurve.Capability.detect/0 alone.

Summary

Types

What it takes to draw a region and draw it again: {paint, clear, place}.

Functions

The sequences for drawing raster into a cell box, and for drawing it again.

The kitty image id for id, a positive integer inside the protocol's range.

Renders raster with the protocol given in opts, or with the detected one.

Renders raster with protocol, delegating to that backend's render/2.

Renders raster with protocol as a binary ready to write to the terminal.

Types

frame()

@type frame() :: {binary(), binary(), binary() | nil}

What it takes to draw a region and draw it again: {paint, clear, place}.

paint puts the picture on the screen and clear takes it off. place draws the picture already sent without sending it again, and is nil where the protocol has no such thing — redrawing then means sending paint once more.

Functions

frame(raster, id, opts \\ [])

@spec frame(FrenchCurve.Raster.t(), term(), keyword()) :: frame() | nil

The sequences for drawing raster into a cell box, and for drawing it again.

Use this rather than assembling a frame from FrenchCurve.Backend.Kitty's pieces. The kitty protocol can store an image under an id and place it repeatedly, which is the cheap way to animate — but not every terminal that speaks kitty implements that part, and one that does not draws nothing at all when sent a store and then a placement. FrenchCurve.Capability.placements?/0 is the question, and this function asks it.

Either way the image is named and clear really removes it. That matters more than it looks: a kitty image is an overlay rather than cells, so text drawn over it does not rub it out and leaving the alternate screen does not discard it. An unnamed one cannot be taken off at all, and is still on the screen after the program has exited.

id names the region across frames — any term; the same one must come back each time so the picture replaces itself rather than piling up. nil for a terminal with no pixels at all, which is the caller's cue to draw text instead.

Options

  • :fit{cols, rows}, the cell box to draw into. Omitted sizes from the pixels
  • :compress — deflate the payload where the protocol allows, default true
  • :protocol — force one instead of detecting. :placements forces the kitty dialect

Examples

iex> raster = FrenchCurve.Raster.new(8, 8)
iex> {paint, clear, place} =
...>   FrenchCurve.frame(raster, :meter, fit: {10, 2}, protocol: :kitty, placements: true)
iex> {paint =~ "a=t,", paint =~ "a=p", clear =~ "a=d", is_binary(place)}
{true, true, true, true}

iex> raster = FrenchCurve.Raster.new(8, 8)
iex> {paint, clear, place} =
...>   FrenchCurve.frame(raster, :meter, fit: {10, 2}, protocol: :kitty, placements: false)
iex> {paint =~ "a=T", paint =~ "a=p", clear =~ "a=d", place}
{true, false, true, nil}

iex> FrenchCurve.frame(FrenchCurve.Raster.new(8, 8), :meter, protocol: :braille)
nil

image_id(id)

@spec image_id(term()) :: pos_integer()

The kitty image id for id, a positive integer inside the protocol's range.

Any term becomes the same number every time, so a caller can name its regions however it likes and still have each one replace itself.

iex> FrenchCurve.image_id({:roll, 3}) == FrenchCurve.image_id({:roll, 3})
true
iex> FrenchCurve.image_id(:anything) > 0
true

render(raster, opts \\ [])

@spec render(
  FrenchCurve.Raster.t(),
  keyword()
) :: term()

Renders raster with the protocol given in opts, or with the detected one.

Options are passed through to the backend, plus:

Returns a binary for the pixel protocols and [[FrenchCurve.Backend.Braille.cell()]] for :braille.

render(raster, protocol, opts)

@spec render(FrenchCurve.Raster.t(), atom(), keyword()) :: term()

Renders raster with protocol, delegating to that backend's render/2.

protocol must be one of :kitty, :sixel, :iterm2 or :braille; any other atom raises FunctionClauseError from FrenchCurve.Capability.backend/1. opts are the options of the backend selected — see FrenchCurve.Backend.Kitty.render/2, FrenchCurve.Backend.Sixel.render/2, FrenchCurve.Backend.Iterm2.render/2 and FrenchCurve.Backend.Braille.render/2.

Returns a binary for the pixel protocols and cell rows for :braille.

to_terminal(raster, protocol, opts \\ [])

@spec to_terminal(FrenchCurve.Raster.t(), atom(), keyword()) :: binary()

Renders raster with protocol as a binary ready to write to the terminal.

Identical to render/3 except that :braille cell rows are converted to a newline-separated ANSI string. opts are the selected backend's options.