Drafter.Draw.Canvas (drafter v0.3.2)

Copy Markdown View Source

Coordinate-based drawing surface for terminal primitives.

A canvas stores a sparse map of {x, y} cells, each holding a character and style. Drawing operations (draw_hline/6, draw_vline/6, draw_line/6, draw_rect/7, fill_rect/7, draw_text/5) are composable and return an updated canvas. Call to_strips/1 to convert the canvas to a list of Drafter.Draw.Strip structs for rendering, or merge/4 to composite one canvas onto another at an offset.

iex> alias Drafter.Draw.{Canvas, Strip}
iex> Canvas.new(5, 3)
...> |> Canvas.draw_rect(0, 0, 5, 3)
...> |> Canvas.draw_text(1, 1, "hi")
...> |> Canvas.to_strips()
...> |> Enum.map(&Strip.to_plain_text/1)
["┌───┐", "│hi │", "└───┘"]

Summary

Functions

Unset every cell, keeping the canvas dimensions.

Unset every cell in the width by height rectangle at x, y.

Draw length horizontal line characters rightwards from x, y.

Draw a straight line of from x1, y1 to x2, y2 inclusive.

Draw the outline of a width by height rectangle with its top-left at x, y.

Draw text rightwards from x, y, one grapheme per cell.

Draw length vertical line characters downwards from x, y.

Fill every cell of the width by height rectangle at x, y with char.

The character at x, y, or a single space if that cell is unset.

Copy the set cells of overlay_canvas onto base_canvas, offset by offset_x, offset_y.

A canvas width columns by height rows, with no cells set.

Put char with style in the cell at x, y.

The canvas as one Drafter.Draw.Strip per row, height strips in all.

Types

cell()

@type cell() :: %{char: String.t(), style: style()}

coordinate()

@type coordinate() :: {non_neg_integer(), non_neg_integer()}

style()

@type style() :: Drafter.Draw.Segment.style()

t()

@type t() :: %Drafter.Draw.Canvas{
  cells: %{required(coordinate()) => cell()},
  height: pos_integer(),
  width: pos_integer()
}

Functions

clear(canvas)

@spec clear(t()) :: t()

Unset every cell, keeping the canvas dimensions.

Examples

iex> Drafter.Draw.Canvas.new(3, 1)
...> |> Drafter.Draw.Canvas.draw_text(0, 0, "abc")
...> |> Drafter.Draw.Canvas.clear()
%Drafter.Draw.Canvas{width: 3, height: 1, cells: %{}}

clear_rect(canvas, x, y, width, height)

@spec clear_rect(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer()
) :: t()

Unset every cell in the width by height rectangle at x, y.

Examples

iex> Drafter.Draw.Canvas.new(3, 1)
...> |> Drafter.Draw.Canvas.draw_text(0, 0, "abc")
...> |> Drafter.Draw.Canvas.clear_rect(1, 0, 1, 1)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["a c"]

draw_hline(canvas, x, y, length, line_style \\ :light, style \\ %{})

Draw length horizontal line characters rightwards from x, y.

line_style defaults to :light and style to %{}. A length of zero or less draws nothing.

Examples

iex> Drafter.Draw.Canvas.new(3, 1)
...> |> Drafter.Draw.Canvas.draw_hline(0, 0, 3, :heavy)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["━━━"]

draw_line(canvas, x1, y1, x2, y2, style \\ %{})

@spec draw_line(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  style()
) :: t()

Draw a straight line of from x1, y1 to x2, y2 inclusive.

Cells are chosen by Bresenham's algorithm, so a diagonal line is one cell per step along its longer axis. style defaults to %{}.

Examples

iex> Drafter.Draw.Canvas.new(3, 3)
...> |> Drafter.Draw.Canvas.draw_line(0, 0, 2, 2)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["█  ", " █ ", "  █"]

draw_rect(canvas, x, y, width, height, line_style \\ :light, style \\ %{})

Draw the outline of a width by height rectangle with its top-left at x, y.

The interior is left untouched. line_style defaults to :light and style to %{}. Returns the canvas unchanged when width or height is below 2.

Examples

iex> Drafter.Draw.Canvas.new(3, 3)
...> |> Drafter.Draw.Canvas.draw_rect(0, 0, 3, 3)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["┌─┐", "│ │", "└─┘"]

iex> Drafter.Draw.Canvas.new(2, 2) |> Drafter.Draw.Canvas.draw_rect(0, 0, 1, 1)
%Drafter.Draw.Canvas{width: 2, height: 2, cells: %{}}

draw_text(canvas, x, y, text, style \\ %{})

@spec draw_text(t(), non_neg_integer(), non_neg_integer(), String.t(), style()) :: t()

Draw text rightwards from x, y, one grapheme per cell.

Double-width graphemes still occupy a single cell, so text containing them renders wider than the cells it was written into. style defaults to %{}.

Examples

iex> Drafter.Draw.Canvas.new(4, 1)
...> |> Drafter.Draw.Canvas.draw_text(1, 0, "hi")
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
[" hi "]

draw_vline(canvas, x, y, length, line_style \\ :light, style \\ %{})

Draw length vertical line characters downwards from x, y.

line_style defaults to :light and style to %{}. A length of zero or less draws nothing.

Examples

iex> Drafter.Draw.Canvas.new(1, 3)
...> |> Drafter.Draw.Canvas.draw_vline(0, 0, 3)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["│", "│", "│"]

fill_rect(canvas, x, y, width, height, char \\ " ", style \\ %{})

@spec fill_rect(
  t(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  non_neg_integer(),
  String.t(),
  style()
) :: t()

Fill every cell of the width by height rectangle at x, y with char.

char defaults to a single space and style to %{}. A width or height of zero or less fills nothing.

Examples

iex> Drafter.Draw.Canvas.new(3, 2)
...> |> Drafter.Draw.Canvas.fill_rect(0, 0, 2, 2, "#")
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["## ", "## "]

get_char(canvas, x, y)

@spec get_char(t(), non_neg_integer(), non_neg_integer()) :: String.t()

The character at x, y, or a single space if that cell is unset.

A coordinate outside the canvas is also unset, so it too gives a space.

Examples

iex> canvas = Drafter.Draw.Canvas.new(3, 1) |> Drafter.Draw.Canvas.draw_text(0, 0, "hi")
iex> {Drafter.Draw.Canvas.get_char(canvas, 0, 0), Drafter.Draw.Canvas.get_char(canvas, 2, 0)}
{"h", " "}

merge(base_canvas, overlay_canvas, offset_x \\ 0, offset_y \\ 0)

@spec merge(t(), t(), non_neg_integer(), non_neg_integer()) :: t()

Copy the set cells of overlay_canvas onto base_canvas, offset by offset_x, offset_y.

Overlay cells replace base cells; unset overlay cells leave the base showing. Cells falling at or past the base canvas's width or height are dropped. Both offsets default to 0.

Examples

iex> base = Drafter.Draw.Canvas.new(3, 1) |> Drafter.Draw.Canvas.draw_text(0, 0, "abc")
iex> overlay = Drafter.Draw.Canvas.new(1, 1) |> Drafter.Draw.Canvas.draw_text(0, 0, "X")
iex> Drafter.Draw.Canvas.merge(base, overlay, 1, 0)
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["aXc"]

new(width, height)

@spec new(pos_integer(), pos_integer()) :: t()

A canvas width columns by height rows, with no cells set.

Both must be greater than zero; anything else raises FunctionClauseError.

Examples

iex> Drafter.Draw.Canvas.new(3, 2)
%Drafter.Draw.Canvas{width: 3, height: 2, cells: %{}}

set_char(canvas, x, y, char, style \\ %{})

@spec set_char(t(), non_neg_integer(), non_neg_integer(), String.t(), style()) :: t()

Put char with style in the cell at x, y.

Coordinates are zero-based. A position at or past width or height leaves the canvas unchanged. style is a Drafter.Draw.Segment.style/0 map, %{} by default.

Examples

iex> Drafter.Draw.Canvas.new(2, 2) |> Drafter.Draw.Canvas.set_char(0, 1, "x")
%Drafter.Draw.Canvas{width: 2, height: 2, cells: %{{0, 1} => %{char: "x", style: %{}}}}

iex> Drafter.Draw.Canvas.new(2, 2) |> Drafter.Draw.Canvas.set_char(5, 5, "x")
%Drafter.Draw.Canvas{width: 2, height: 2, cells: %{}}

to_strips(canvas)

@spec to_strips(t()) :: [Drafter.Draw.Strip.t()]

The canvas as one Drafter.Draw.Strip per row, height strips in all.

Every row is width cells wide, unset cells becoming spaces, and adjacent cells sharing a style are merged into one segment.

Examples

iex> Drafter.Draw.Canvas.new(3, 2)
...> |> Drafter.Draw.Canvas.draw_text(0, 0, "hi")
...> |> Drafter.Draw.Canvas.to_strips()
...> |> Enum.map(&Drafter.Draw.Strip.to_plain_text/1)
["hi ", "   "]