# `RasterExRatatui.Raster`
[🔗](https://github.com/mcass19/raster_ex_ratatui/blob/v0.4.0/lib/raster_ex_ratatui/raster.ex#L1)

Turns `ExRatatui.CellSession` payloads into packed pixels for a panel.

A raster is a pure value: the panel geometry, a font, a pixel format, an integer scale, and the `RasterExRatatui.Grid` of the current frame. Feed it every snapshot or diff the session produces and it answers with the `RasterExRatatui.Patch` rectangles that changed, packed and ready to write.

    raster = Raster.new(size: {400, 300}, font: Default6x8, format: Mono)
    {cols, rows} = Raster.grid_size(raster)
    session = CellSession.new(cols, rows, font_size: Raster.font_size(raster))

    :ok = CellSession.draw(session, widgets)
    {raster, patches} = Raster.apply(raster, CellSession.take_cells_diff(session))

## Geometry

The effective cell is the font's cell times `scale:`; it is what the session must be created with as `font_size:`, so pixel-mode widgets render bitmaps at the panel's resolution. The grid is the panel size divided by the effective cell, rounded down; the leftover right and bottom strips (`margin/1`) are painted with the format's blank pixel.

## Patches

`apply/2` rasterises only what changed:

  * changed cells, one patch per contiguous run on a row
  * when the region list changed, one patch per region that is new or whose bitmap or rect changed (the bitmap scaled nearest-neighbour onto its cell rect, clipped to the grid, in one native pass by `ExRatatui.Pixels.pack_rgb8/5`), plus the cells a region that went away no longer covers. A region that is the same as in the previous payload costs nothing, so a still image next to an animated one is rasterised once. Overlapping regions stay correct: an unchanged region that touches a repainted area is repainted too, in list order
  * on a full payload (the first diff, a snapshot, a resize), every run of cells on every row, every region, and the margins

Cells under a pixel region are never rasterised, on full payloads or diffs: they arrive blank and the region paints over them.

`apply/2` also takes a **list** of payloads and rasterises them as one: the grid is folded through all of them first, so a cell or region that changed several times is drawn once, in its final state. A consumer that falls behind its app (a slow panel, a big pixel region) folds everything queued into one call instead of drawing frames nobody will see; the surface process does exactly that.

Patches must be written in list order. `frame/1` renders the whole panel as one buffer for panels that only take full frames (`render_frame/1` does the same and keeps the glyph cache it fills); writing `apply/2`'s patches over the previous `frame/1` gives the next one.

## Rotation

A panel mounted on its side keeps its native scan order: `/dev/fb0` on a portrait display is portrait however the stand holds it. `rotate: 90 | 180 | 270` turns the app's image clockwise on the way to the panel. `:size` stays the **physical** panel; the grid is computed from the **logical** size, which is the physical size with width and height swapped for 90 and 270 (`logical_size/1`); and every patch, `frame/1`, and `margin/1` come out in physical coordinates, so whatever writes the panel does not know the difference.

    rotate: 90 on a 720×1280 panel      logical (x, y) → physical
                                          90:  (W - 1 - y, x)
      logical 1280×720  ─┐               180:  (W - 1 - x, H - 1 - y)
      ┌───────────────┐  │ clockwise     270:  (y, H - 1 - x)
      │ A B C         │  │               W, H: the physical size
      └───────────────┘  ▼
                      ┌─────┐
                      │ A   │  physical 720×1280
                      │ B   │  (the app's top edge is on the right)
                      │ C   │
                      └─────┘

Glyphs are rotated once as they enter the cache; a run of cells becomes a vertical strip; a pixel region's bitmap is sampled, turned and packed in the same native pass as a flat one, so both cost the same. Dithering and checkerboards stay anchored to the panel's own pixel grid, which is what a 1-bit panel wants.

# `payload`

```elixir
@type payload() :: ExRatatui.CellSession.Snapshot.t() | ExRatatui.CellSession.Diff.t()
```

What `apply/2` folds in: a cell diff or a snapshot from an `ExRatatui.CellSession`.

# `rotation`

```elixir
@type rotation() :: 0 | 90 | 180 | 270
```

Clockwise rotation of the app's image on the panel, in degrees.

# `size`

```elixir
@type size() :: {pos_integer(), pos_integer()}
```

# `t`

```elixir
@type t() :: %RasterExRatatui.Raster{
  blank: binary(),
  bytes_per_pixel: pos_integer(),
  cache: map(),
  cell_size: size(),
  config: RasterExRatatui.PixelFormat.config(),
  font: RasterExRatatui.Font.t(),
  format: RasterExRatatui.PixelFormat.t(),
  grid: RasterExRatatui.Grid.t(),
  grid_size: size(),
  logical_size: size(),
  native_format: ExRatatui.Pixels.format() | nil,
  rotate: rotation(),
  scale: pos_integer(),
  size: size()
}
```

# `apply`

```elixir
@spec apply(t(), payload() | [payload()]) :: {t(), [RasterExRatatui.Patch.t()]}
```

Folds a snapshot or diff, or a list of them in order, into the raster and returns the patches that repaint what changed.

See the moduledoc for which patches are produced. A list produces the patches of its last state only, never of the states in between; an empty list produces none.

## Examples

    iex> alias ExRatatui.CellSession.{Cell, Diff}
    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> raster = Raster.new(size: {12, 8}, format: PixelFormat.Mono)
    iex> full = %Diff{width: 2, height: 1, ops: [%Cell{col: 0, symbol: "A"}, %Cell{col: 1}]}
    iex> {raster, [row]} = Raster.apply(raster, full)
    iex> {row.x, row.y, row.width, row.height, byte_size(row.data)}
    {0, 0, 12, 8, 96}
    iex> {_raster, [cell]} = Raster.apply(raster, %Diff{width: 2, height: 1, ops: [%Cell{col: 1, symbol: "B"}]})
    iex> {cell.x, cell.width}
    {6, 6}
    iex> twice = [%Diff{width: 2, height: 1, ops: [%Cell{col: 0, symbol: "C"}]}, %Diff{width: 2, height: 1, ops: [%Cell{col: 0, symbol: "D"}]}]
    iex> {_raster, [once]} = Raster.apply(raster, twice)
    iex> {once.x, once.width}
    {0, 6}

# `bytes_per_pixel`

```elixir
@spec bytes_per_pixel(t()) :: pos_integer()
```

Bytes per packed pixel, from the format.

## Examples

    iex> RasterExRatatui.Raster.new(size: {64, 64}, format: RasterExRatatui.PixelFormat.RGB565) |> RasterExRatatui.Raster.bytes_per_pixel()
    2

# `cell_at`

```elixir
@spec cell_at(t(), {integer(), integer()}) ::
  {non_neg_integer(), non_neg_integer()} | :outside
```

The cell under a physical pixel, `{col, row}`, or `:outside` for a pixel in the margins or off the panel.

The inverse of the rotation mapping in the moduledoc: what a touch controller, which reports the panel's own coordinates, needs to find the cell the app sees under a finger.

## Examples

A 12×16 panel with the 6×8 font: two columns and two rows flat, two columns and one row turned.

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> flat = Raster.new(size: {12, 16}, format: PixelFormat.Mono)
    iex> {Raster.cell_at(flat, {7, 9}), Raster.cell_at(flat, {12, 0}), Raster.cell_at(flat, {-1, 0})}
    {{1, 1}, :outside, :outside}

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> at = fn angle, point -> Raster.cell_at(Raster.new(size: {12, 16}, format: PixelFormat.Mono, rotate: angle), point) end
    iex> {at.(90, {5, 10}), at.(180, {7, 9}), at.(270, {5, 10})}
    {{1, 0}, {0, 0}, {0, 0}}

Turned by 90, the app's 4-pixel right margin is at the bottom of the panel and its bottom margin on the panel's left:

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> turned = Raster.new(size: {12, 16}, format: PixelFormat.Mono, rotate: 90)
    iex> {Raster.cell_at(turned, {4, 11}), Raster.cell_at(turned, {4, 12}), Raster.cell_at(turned, {3, 11})}
    {{1, 0}, :outside, :outside}

# `font_size`

```elixir
@spec font_size(t()) :: size()
```

Effective cell size in pixels (the font's cell times the scale): the `font_size:` for `ExRatatui.CellSession.new/3`.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono, scale: 2) |> RasterExRatatui.Raster.font_size()
    {12, 16}

# `frame`

```elixir
@spec frame(t()) :: binary()
```

Renders the whole panel as one row-major buffer of `width * height` packed pixels: cells, then regions, then margins.

## Examples

    iex> alias ExRatatui.CellSession.{Cell, Snapshot}
    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> raster = Raster.new(size: {8, 9}, format: PixelFormat.Mono)
    iex> {raster, _patches} = Raster.apply(raster, %Snapshot{width: 1, height: 1, cells: [%Cell{symbol: "█"}]})
    iex> frame = Raster.frame(raster)
    iex> {byte_size(frame), :binary.at(frame, 0), :binary.at(frame, 6), :binary.at(frame, 8 * 8)}
    {72, 0, 255, 255}

# `grid`

```elixir
@spec grid(t()) :: RasterExRatatui.Grid.t()
```

The raster's current grid.

# `grid_size`

```elixir
@spec grid_size(t()) :: size()
```

Grid size in cells, `{cols, rows}`: the size to create the `ExRatatui.CellSession` with.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono) |> RasterExRatatui.Raster.grid_size()
    {66, 37}

# `logical_size`

```elixir
@spec logical_size(t()) :: size()
```

The size of the app's image in pixels, `{width, height}`: the physical size, with width and height swapped when the raster rotates by 90 or 270.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono, rotate: 270) |> RasterExRatatui.Raster.logical_size()
    {300, 400}

# `margin`

```elixir
@spec margin(t()) :: {non_neg_integer(), non_neg_integer()}
```

The unused strips on the right and at the bottom of the app's image, in pixels, `{right, bottom}`. On a rotated raster they are on the right and at the bottom as the app sees them; on the panel they are wherever the rotation puts them.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono) |> RasterExRatatui.Raster.margin()
    {4, 4}

    iex> RasterExRatatui.Raster.new(size: {300, 400}, format: RasterExRatatui.PixelFormat.Mono, rotate: 90) |> RasterExRatatui.Raster.margin()
    {4, 4}

# `new`

```elixir
@spec new(keyword()) :: t()
```

Builds a raster for a panel.

## Options

  * `:size` (required) — the physical panel size in pixels, `{width, height}`
  * `:format` (required) — a `RasterExRatatui.PixelFormat` module
  * `:font` — a `RasterExRatatui.Font` module (default `RasterExRatatui.Font.Default6x8`)
  * `:scale` — integer magnification of the font's cell (default `1`)
  * `:rotate` — `0`, `90`, `180`, or `270`: how far clockwise the app's image is turned on the panel (default `0`, see the moduledoc)
  * `:format_opts` — passed to the format's `c:RasterExRatatui.PixelFormat.init/1` (default `[]`)

Raises `ArgumentError` when `:size` or `:format` is missing, when `:size` is not a pair of positive integers, when `:scale` is not a positive integer, when `:rotate` is not one of the four angles, or when not even one cell fits on the panel.

## Examples

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> raster = Raster.new(size: {1920, 1080}, format: PixelFormat.XRGB8888, scale: 3)
    iex> {Raster.grid_size(raster), Raster.font_size(raster), Raster.margin(raster)}
    {{106, 45}, {18, 24}, {12, 0}}

A portrait panel on a landscape stand: the grid is that of the turned image.

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> raster = Raster.new(size: {720, 1280}, format: PixelFormat.RGB565, scale: 2, rotate: 90)
    iex> {Raster.size(raster), Raster.logical_size(raster), Raster.grid_size(raster)}
    {{720, 1280}, {1280, 720}, {106, 45}}

# `render_frame`

```elixir
@spec render_frame(t()) :: {t(), binary()}
```

Like `frame/1`, but also returns the raster with every glyph it rendered added to its cache. Use it when rendering frames repeatedly (a panel that only takes full frames) and keep the returned raster.

## Examples

    iex> alias ExRatatui.CellSession.{Cell, Snapshot}
    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> raster = Raster.new(size: {12, 8}, format: PixelFormat.Mono)
    iex> {raster, _patches} = Raster.apply(raster, %Snapshot{width: 2, height: 1, cells: [%Cell{symbol: "A"}]})
    iex> {rendered, frame} = Raster.render_frame(%{raster | cache: %{}})
    iex> {frame == Raster.frame(raster), map_size(rendered.cache) > 0}
    {true, true}

# `resize`

```elixir
@spec resize(t(), size()) :: t()
```

Rebuilds the geometry for a new physical panel size, keeping font, format, scale, rotation, and the glyph cache, and clears the grid.

After a resize the `ExRatatui.CellSession` must be resized to the new `grid_size/1`; its next diff is a full payload, so `apply/2` repaints the whole panel.

## Examples

    iex> alias RasterExRatatui.Raster
    iex> raster = Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono)
    iex> raster |> Raster.resize({120, 80}) |> Raster.grid_size()
    {20, 10}

    iex> alias RasterExRatatui.Raster
    iex> raster = Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono, rotate: 90)
    iex> raster |> Raster.resize({80, 120}) |> Raster.grid_size()
    {20, 10}

# `rotate`

```elixir
@spec rotate(t()) :: rotation()
```

How far clockwise the app's image is turned on the panel: `0`, `90`, `180`, or `270`.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono) |> RasterExRatatui.Raster.rotate()
    0

# `size`

```elixir
@spec size(t()) :: size()
```

The physical panel size in pixels, `{width, height}`: what patches and `frame/1` are laid out for.

## Examples

    iex> RasterExRatatui.Raster.new(size: {400, 300}, format: RasterExRatatui.PixelFormat.Mono) |> RasterExRatatui.Raster.size()
    {400, 300}

# `to_png`

```elixir
@spec to_png(t()) :: binary()
```

Renders the whole panel as a PNG image: 8-bit RGB, the panel's physical size, what `frame/1` holds turned back into colours by the format's `c:RasterExRatatui.PixelFormat.unpack_row/2`.

For tests and debugging: a snapshot of what the panel would show, written with `File.write!/2` and opened in any image viewer. Pure Elixir, no dependency.

Raises `ArgumentError` when the format does not implement `c:RasterExRatatui.PixelFormat.unpack_row/2`.

## Examples

    iex> alias RasterExRatatui.{Raster, PixelFormat}
    iex> png = Raster.to_png(Raster.new(size: {12, 8}, format: PixelFormat.RGB565))
    iex> <<137, "PNG", 13, 10, 26, 10, 13::32, "IHDR", width::32, height::32, 8, 2, _::binary>> = png
    iex> {width, height}
    {12, 8}

---

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