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:
- Copy existing canvas content shifted by scroll offset
- Only render newly exposed data in the exposed regions
- 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 dataexposed_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
Functions
@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})
trueA 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
@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).
@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 pixelscanvas_size-{width, height}of the canvasregion_elements- List of{bounds, element}tuples for each exposed regionopts-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).
@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}]
Filters data points to only those visible in the given regions.
Parameters
data- List of data pointsregions- 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}]
@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