Cauldron2D.Drafter.Surface (Cauldron2D.Drafter v0.1.3)

Copy Markdown View Source

A drafter widget that draws a Cauldron2D surface.

{:cauldron_surface,
 [
   id: :map,
   atlas: :my_game,
   focus: {x, y},
   cell: &MyGame.cell/1,
   movers: [{:ball, {7.42, 3.15}}],
   flex: 1
 ]}

Register it before use, after loading it:

Code.ensure_loaded!(Cauldron2D.Drafter.Surface)
Drafter.Widget.Registry.register(Cauldron2D.Drafter.Surface)

Where Cauldron2D.Renderer finds a pixel protocol, image/3 hands drafter one composited image and render/2 fills the pane with blank cells beneath it. Otherwise image/3 returns nil and render/2 draws two-cell glyphs. Both read the same camera view, sized from the pane by Cauldron2D.Camera.columns_for/1 and Cauldron2D.Camera.rows_for/1.

Until both :focus and :cell are set, render/2 fills the pane with black and image/3 returns nil.

Options

  • :atlas — the name an atlas was installed under with Cauldron2D.Atlas.install/1
  • :focus, :cell — passed to Cauldron2D.Camera.view/1
  • :movers[{art, {x, y}}] in world tile coordinates, which may be fractional. Default []. In pixel mode a mover is drawn at its exact pixel position; in text mode it replaces the glyph of the nearest whole cell
  • :labels[{text, {x, y}, colour}] or [{text, {x, y}, colour, opts}] in world tile coordinates, as Cauldron2D.Surface.compose/4 takes them: names, numbers, notes drawn over everything. Default []. In pixel mode a label is drawn in Linocut.Font at its exact pixel position, with placements as a stored image of its own; in text mode its characters overlay the glyphs from the nearest cell
  • :zoom — how many times its native size a tile is drawn: 2 twice as large, 0.5 half, so a pane shows half or twice the tiles. Default 1. Levels a surface draws exactly are zoom_levels/2's; another is drawn at the nearest of them. Zoomed in, a pixel terminal that can fit an image to cells is sent the smaller picture to scale, sixels are scaled first; zoomed out, the frame is composed at the lower resolution. In text mode a tile zoomed in covers that many more cells; zoomed out, every that-manyeth tile is drawn. Movers are placed by the terminal (placements) only at native size
  • :mode — a Cauldron2D.Renderer mode to force instead of detecting one, or :braille: the frame composed at four dots per tile side, pixels at or above a luminance of 64 lit, drawn as braille text in the colour of each cell's lit pixels. Default nil
  • :compress — zlib the kitty transmit. Default true
  • :placements:auto (default), true or false. Whether movers are drawn as stored sprites placed at pixel offsets rather than composited into the frame; see below
  • :on_geometry — an event name sent to the application whenever the pane's rect changes, see on_rect_change/2. Default nil
  • :image_throttle, :image_priority — how often a new frame is composed and at what scheduler priority, as drafter's widget server takes them. Defaults: every second application frame, :low. A game animating at the application's frame rate passes image_throttle: {1, :tick}, image_priority: :normal

Scrolling

In both pixel renderings the focus sits at the exact centre of the pane: the grid is composed one tile wider and taller than the pane and shown through a window shifted by the fraction of a tile the focus is past a tile boundary, and movers are placed relative to that same fractional origin. Text mode scrolls by whole tiles.

Animation

An art with frames (Cauldron2D.Atlas.put/4 with a list) is drawn at the frame showing on the session's monotonic clock, in every rendering but glyphs. The grid image of the placement rendering is sent again when a frame changes; a mover with frames is placed by the frame's own stored image.

Placements

On a kitty terminal whose cell size the session knows (Drafter.Session.Context.cell_size/0, asked for with use Drafter.App, cell_size: true), the grid is one stored image, fitted to the pane by the terminal and sent again only when its tiles or the atlas change, and each mover is a placement of a sprite stored by Cauldron2D.Drafter.Sprites.transmit/2 — a few dozen bytes per mover per frame, no pixels. The surface stores the sprites itself, once per atlas and cell size, resampled to the two cells by one row a tile occupies, so the terminal draws a sprite pixel for pixel wherever it is placed. Elsewhere the movers are composited into one image per frame.

The protocol is the one the session's terminal answered the startup probe with, or Cauldron2D.Renderer's detection when there was no probe.

Summary

Functions

The tag this widget answers to in a component tree: :cauldron_surface.

Produce the frame as terminal-graphics bytes, or nil when there is nothing to draw.

Build the widget's state from its props.

Record the pane's rect, and report it when it differs from the one already held.

The pixel protocol this surface draws with, or nil for glyphs.

Callback implementation for Drafter.Widget.unmount/1.

Fold re-rendered props into the widget state.

The zoom levels a surface draws exactly, largest first: in :text mode and the pixel modes (:kitty, :iterm2, :sixel, or nil for whichever is detected) 4 down to a sixteenth, kept to the divisors of the atlas's tile size zoomed out; in :braille mode those where a tile is a whole number of dots, from the tile's size in dots down to one.

Types

t()

@type t() :: %Cauldron2D.Drafter.Surface{
  atlas: term(),
  cell: term(),
  compress: term(),
  focus: term(),
  labels: term(),
  mode: term(),
  movers: term(),
  on_geometry: term(),
  placements: term(),
  rect: term(),
  widget_id: term(),
  zoom: term()
}

Functions

component_tag()

@spec component_tag() :: atom()

The tag this widget answers to in a component tree: :cauldron_surface.

focused(state)

handle_event(event, state)

Callback implementation for Drafter.Widget.handle_event/2.

image(state, rect, id)

@spec image(t(), map(), term()) :: {iodata(), iodata(), map()} | nil

Produce the frame as terminal-graphics bytes, or nil when there is nothing to draw.

Returns nil when Cauldron2D.Renderer finds no pixel protocol, when :focus or :cell is unset, or when composing the frame raises.

A frame that raises is logged with its stacktrace before nil is returned, so a missing atlas or a bad mover shows up as an error rather than as a pane that is silently blank. Frames are drawn many times a second, so the same message is logged once per widget process rather than on every frame.

Otherwise returns {paint, clear, region}:

  • paint — the bytes that draw this frame, which already begin with clear
  • clear — the bytes that remove this frame, empty for protocols that leave no stored image behind
  • region%{dx: 0, dy: 0, cols: cols, rows: rows, place: place} covering the pane. place re-asserts an already-transmitted image's placement, and is nil for protocols whose bytes are the pixels themselves

id identifies the image to the terminal across frames, so successive calls for the same widget must pass the same id.

FrenchCurve.frame/3 builds the bytes, which matters because not every terminal detected as :kitty keeps images under an id — iTerm does not, and a surface animated as though it did would never move.

mount(props)

@spec mount(map() | keyword()) :: t()

Build the widget's state from its props.

Props may be a map or a keyword list. Every key falls back to its documented default when absent, so compress: false means false rather than the default.

on_rect_change(rect, state)

@spec on_rect_change(map(), t()) :: t()

Record the pane's rect, and report it when it differs from the one already held.

With :on_geometry set and a drafter application running, the application is sent %{rect: rect, size: {columns, rows}}, where size is the viewport in tiles. Those two, with Cauldron2D.Camera.origin/3 and Cauldron2D.Camera.tile_at/2, are what maps a screen position back to a world tile.

Returns the state carrying the new rect.

preferred_height(args, opts)

protocol(surface)

@spec protocol(t()) :: :kitty | :iterm2 | :sixel | nil

The pixel protocol this surface draws with, or nil for glyphs.

A forced :mode wins; then the protocol the session's terminal answered the probe with; then, when the terminal was not probed or named no protocol, Cauldron2D.Renderer.protocol/1.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

@spec update(map() | keyword(), t()) :: t()

Fold re-rendered props into the widget state.

Every option the widget takes can be changed by a re-render, not only the camera ones. A key present in props is used even when its value is nil or false, so a mode that was forced can be unforced and compress can be turned off. A key absent from props leaves that part of the state as it was.

update_props_from_mount(mount_props, existing_state, opts)

zoom_levels(atlas, mode)

@spec zoom_levels(Cauldron2D.Atlas.name(), atom() | nil) :: [number()]

The zoom levels a surface draws exactly, largest first: in :text mode and the pixel modes (:kitty, :iterm2, :sixel, or nil for whichever is detected) 4 down to a sixteenth, kept to the divisors of the atlas's tile size zoomed out; in :braille mode those where a tile is a whole number of dots, from the tile's size in dots down to one.