Visualize.Backend.CanvasIncremental (Visualize v0.2.35)

Copy Markdown View Source

Incremental rendering support for canvas binary format.

Provides utilities for calculating scroll regions, determining when copy optimization is beneficial, and encoding incremental updates.

Overview

Instead of clearing and redrawing the entire canvas each frame, this module enables:

  1. Copy existing canvas content shifted by scroll offset
  2. Only render newly exposed data in the exposed regions
  3. Fall back to full redraw when scroll is too large

Binary Format

Incremental updates use command 0x60:

[0x60]                  # header
[mode: u8]              # 0x01=scroll_x, 0x02=scroll_y, 0x03=scroll_xy, 0x00=full
[dx: f32]               # horizontal scroll (pixels)
[dy: f32]               # vertical scroll (pixels)
[vx: f32][vy: f32][vw: f32][vh: f32]  # the viewport: the rectangle that scrolls (D-106)
[region_count: u8]      # number of exposed regions (0-3)
[regions...]            # region data

exposed_regions/2 yields at most two regions (one strip, or an L of two); the record admits three and encode_incremental/4 raises on more (D-38). The decoder copies the viewport's pixels alone, clipped to it, so a plot that scrolls inside a margin leaves the margin — and the axes in it — untouched (#294).

Each region:

[x: f32][y: f32][w: f32][h: f32]  # bounds
[data_length: u32]                 # byte length of commands
[commands...]                      # standard binary commands

Summary

Functions

Determines if copy optimization is worthwhile.

Encodes a full redraw command (when scroll is too large for optimization).

Encodes an incremental update with scroll offset and region data.

Calculates the exposed regions after a scroll operation.

Filters data points to only those visible in the given regions.

Calculates the scroll mode based on scroll direction.

Types

bounds()

@type bounds() ::
  {x :: number(), y :: number(), width :: number(), height :: number()}

canvas_size()

@type canvas_size() :: {width :: number(), height :: number()}

scroll_offset()

@type scroll_offset() :: {dx :: number(), dy :: number()}

Functions

copy_beneficial?(arg1, arg2)

@spec copy_beneficial?(scroll_offset(), canvas_size()) :: boolean()

Determines if copy optimization is worthwhile.

Returns false if scroll distance > 50% of canvas dimension in either axis, as the overhead of copy+partial-render would exceed full redraw.

Examples

iex> alias Visualize.Backend.CanvasIncremental
iex> CanvasIncremental.copy_beneficial?({50, 0}, {800, 600})
true

A 500px scroll exceeds 400px, half of the 800px width, so a full redraw is cheaper:

iex> alias Visualize.Backend.CanvasIncremental
iex> CanvasIncremental.copy_beneficial?({500, 0}, {800, 600})
false

encode_full_redraw(element, canvas_size \\ nil)

@spec encode_full_redraw(Visualize.IR.Element.t(), canvas_size() | nil) :: binary()

Encodes a full redraw command (when scroll is too large for optimization).

This signals to the client to clear and redraw everything. The record's viewport is the canvas size when given, zeros otherwise; a full redraw does not consult it (D-106).

encode_incremental(arg1, arg2, region_elements, opts \\ [])

@spec encode_incremental(
  scroll_offset(),
  canvas_size(),
  [{bounds(), Visualize.IR.Element.t()}],
  keyword()
) :: binary()

Encodes an incremental update with scroll offset and region data.

Parameters

  • scroll - {dx, dy} scroll offset in pixels
  • canvas_size - {width, height} of the canvas
  • region_elements - List of {bounds, element} tuples for each exposed region
  • opts - viewport: {x, y, w, h}, the rectangle of the canvas that scrolls; the whole canvas when absent (D-106). The offset is classified against its size.

Returns

Binary data in incremental format that can be decoded by CanvasIncrementalChart hook.

Raises ArgumentError for more than three regions (the record's region_count is one byte and exposed_regions/2 never yields more than two) and for a zero offset, which is not an incremental update (scroll_mode/2 classifies it :none).

exposed_regions(arg1, arg2)

@spec exposed_regions(scroll_offset(), canvas_size()) :: [bounds()]

Calculates the exposed regions after a scroll operation.

Returns 0-2 regions depending on scroll direction:

  • No scroll: no regions
  • Horizontal only: 1 vertical strip (left or right edge)
  • Vertical only: 1 horizontal strip (top or bottom edge)
  • Diagonal: 2 regions forming an L-shape (avoiding overlap)

The regions are where new data needs to be rendered after the canvas content is copied/shifted.

Examples

Scrolling right by 20px on an 800 x 600 canvas exposes the right edge:

iex> alias Visualize.Backend.CanvasIncremental
iex> CanvasIncremental.exposed_regions({20, 0}, {800, 600})
[{780.0, 0.0, 20.0, 600.0}]

Scrolling left by 20px exposes the left edge:

iex> alias Visualize.Backend.CanvasIncremental
iex> CanvasIncremental.exposed_regions({-20, 0}, {800, 600})
[{0.0, 0.0, 20.0, 600.0}]

A diagonal scroll exposes an L of two strips, the second shortened by the first:

iex> alias Visualize.Backend.CanvasIncremental
iex> CanvasIncremental.exposed_regions({20, 30}, {800, 600})
[{780.0, 0.0, 20.0, 600.0}, {0.0, 570.0, 780.0, 30.0}]

filter_to_regions(data, regions, accessor)

@spec filter_to_regions(list(), [bounds()], (any() -> {number(), number()})) :: list()

Filters data points to only those visible in the given regions.

Parameters

  • data - List of data points
  • regions - List of bounds {x, y, width, height}
  • accessor - Function that extracts {x, y} from a data point

Example

data = [%{x: 10, y: 20}, %{x: 790, y: 300}]
regions = [{780.0, 0.0, 20.0, 600.0}]
accessor = fn d -> {d.x, d.y} end

filter_to_regions(data, regions, accessor)
# => [%{x: 790, y: 300}]

scroll_mode(arg1, arg2)

@spec scroll_mode(scroll_offset(), canvas_size()) ::
  :none | :scroll_x | :scroll_y | :scroll_xy | :full_redraw

Calculates the scroll mode based on scroll direction.

Returns:

  • :none - a zero offset: nothing moved and nothing is exposed (D-38)
  • :scroll_x - horizontal only
  • :scroll_y - vertical only
  • :scroll_xy - diagonal (both axes)
  • :full_redraw - scroll too large, use full redraw