-
Notifications
You must be signed in to change notification settings - Fork 1
Canvas Ops Reference
Every drawing op the Matrix Gateway's POST /api/canvas/ops accepts, in detail. This is
the companion page to Canvas API (the endpoint-level view) and
Writing Matrix Apps (the app author's view — the canvas.*
helpers there emit exactly these ops).
An ops request is a JSON array of op objects, applied in order, then presented:
[ {"op":"clear","color":[0,0,40]},
{"op":"arc","x":64,"y":16,"r":14,"t":3,"start":0,"end":270,"color":[0,200,120]},
{"op":"text","x":64,"y":11,"s":"75%","align":"center"} ]The same array, byte for byte, rides the stream channel's ops record
(PUT /api/canvas/stream, type 0x03) — everything below applies there too.
-
Coordinates: pixels, origin top-left,
xright /ydown. Everything is clipped to the panel (and to theclipwindow when one is set) — off-panel drawing is dropped, never an error. -
Colours:
"color": [r, g, b](0–255 each). Omitted, it defaults to white for drawing ops and black forclear/scrollvacancy. The panel quantises to its running bit depth. -
Batching: ops draw into the back buffer; nothing appears until a present. The
batch auto-presents when it ends unless it contained an explicit
show— include{"op":"show"}mid-batch only when you want an intermediate present. (On the stream channel there is no auto-present: sendshowops or a0x05record.) -
Batch-scoped state:
originandclipaffect only later ops in the same batch and reset between batches.atlasbinding is sticky across batches by design. -
Unknown ops and params are ignored (the op is skipped / the param unread, never a
400) — old and new clients interoperate with old and new firmware in any combination.
GET /api/capabilities→canvas.opsis the authoritative list of what the wall you're talking to understands. -
The reply is
{"ok":true,"applied":N}—appliedcounts ops that did something (unknown ops and out-of-range sprites don't count). It answers before the present, so the ~14 ms present overlaps your next frame's build.
{"op":"clear","color":[0,0,0]}| param | type | default | |
|---|---|---|---|
color |
[r,g,b] |
black | fill colour |
Fills the whole panel (a fast whole-buffer fill, ~5 ms on 256×64). Almost every frame
starts here. Ignores origin; respects clip.
{"op":"pixel","x":10,"y":4,"color":[255,0,0]}| param | type | default |
|---|---|---|
x, y
|
int | 0 |
color |
[r,g,b] |
white |
{"op":"hline","x":0,"y":31,"w":128,"color":[80,80,80]}
{"op":"vline","x":64,"y":0,"h":32}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | start |
w (hline) / h (vline) |
int | 0 | run length |
color |
[r,g,b] |
white |
For a thick rule use rect with fill:true — it's one fast fill.
{"op":"line","x":0,"y":0,"x1":127,"y1":31,"t":3,"color":[255,200,0]}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | one end |
x1, y1
|
int | 0 | other end |
t |
int | 1 | stroke thickness (fw 3.5) |
color |
[r,g,b] |
white |
Bresenham at t:1; thicker strokes sweep a t×t square brush along the line, which
reads as a solid stroke at panel resolutions.
{"op":"polyline","points":[[0,30],[20,10],[40,25],[60,5]],"t":2,"color":[0,255,255]}| param | type | default | |
|---|---|---|---|
points |
[[x,y],…] |
— | vertices, drawn in order, not closed |
t |
int | 1 | stroke thickness (fw 3.5) |
color |
[r,g,b] |
white |
The graphing primitive. For a closed shape use poly.
All shape ops share: color (default white), fill (default false — outline).
Outlines take t thickness since fw 3.5 where noted.
{"op":"rect","x":4,"y":4,"w":40,"h":24,"t":2,"color":[255,255,255]}
{"op":"rect","x":4,"y":4,"w":40,"h":24,"fill":true}| param | type | default |
|---|---|---|
x, y, w, h
|
int | 0 |
fill |
bool | false |
t |
int | 1 (outline only) |
A t-thick outline is four filled bars, drawn inside the w×h box.
{"op":"roundrect","x":2,"y":2,"w":60,"h":28,"r":6,"fill":true,"color":[30,30,60]}| param | type | default | |
|---|---|---|---|
x, y, w, h
|
int | 0 | box |
r |
int | 0 | corner radius |
fill |
bool | false |
The card/badge primitive. (Outline is 1 px; for a thick rounded frame draw two filled roundrects, inner in the background colour.)
{"op":"circle","x":16,"y":16,"r":10,"t":3,"color":[255,0,128]}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | centre |
r |
int | 0 | radius |
fill |
bool | false | disc |
t |
int | 1 | ring thickness (outline; fw 3.5 renders a true annulus) |
{"op":"ellipse","x":64,"y":16,"rx":30,"ry":12,"fill":true,"color":[0,60,120]}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | centre |
rx, ry
|
int | 0 | semi-axes |
fill |
bool | false | |
t |
int | 1 | outline thickness (nested rings) |
{"op":"triangle","x":10,"y":28,"x1":30,"y1":4,"x2":50,"y2":28,"fill":true}| param | type | default |
|---|---|---|
x,y, x1,y1, x2,y2
|
int | 0 |
fill |
bool | false |
color |
[r,g,b] |
white |
{"op":"poly","points":[[10,2],[20,12],[16,26],[4,26],[0,12]],"color":[255,180,0]}
{"op":"poly","points":[[10,2],[20,12],[4,26]],"fill":false,"t":2}| param | type | default | |
|---|---|---|---|
points |
[[x,y],…] |
— | up to 16 vertices, closed automatically |
fill |
bool | true | even-odd scanline fill |
t |
int | 1 | outline thickness when fill:false
|
color |
[r,g,b] |
white |
Even-odd fill means self-intersecting outlines (stars) render with holes, HTML-canvas style. Beyond 16 vertices the extras are ignored.
{"op":"arc","x":64,"y":32,"r":20,"t":4,"start":0,"end":270,"color":[0,220,100]}
{"op":"arc","x":64,"y":32,"r":20,"start":120,"end":200,"fill":true}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | centre |
r |
int | 0 | outer radius |
t |
int | 2 | ring thickness (ignored when fill) |
start, end
|
int deg | 0, 360 | sweep, 0° = 12 o'clock, clockwise |
fill |
bool | false | pie slice instead of ring segment |
color |
[r,g,b] |
white |
The gauge primitive. start:0, end: value*360/max is a progress ring;
t:r makes the ring a full disc segment. Sweeps may cross 0° (start:300, end:420 or
equivalently end:60, which the firmware unwraps). A full 0→360 with t is exactly a
thick circle outline.
{"op":"clip","x":10,"y":0,"w":50,"h":32}
{"op":"clip"}| param | type | default | |
|---|---|---|---|
x, y, w, h
|
int | 0 | window; w/h ≤ 0 (or the bare form) clears
|
Every later op in the batch — including full clear fills, images, sprites and text —
draws only inside the window. Enforced in the firmware's pixel/fill/blit fast paths, so
there's no op that escapes it. Reset automatically when the batch ends.
{"op":"origin","x":40,"y":8}
{"op":"origin"}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | added to every later coordinate; bare form resets |
Applies to all coordinate params, including x1/y1/x2/y2 and points arrays. Absolute
(not cumulative): a second origin replaces the first. The component pattern: draw a
widget's ops with coordinates relative to (0,0), prefix them with origin (and usually
clip) to place as many copies as you like:
[ {"op":"origin","x":0,"y":0}, {"op":"clip","x":0,"y":0,"w":64,"h":32}, …widget ops…,
{"op":"origin","x":64,"y":0}, {"op":"clip","x":64,"y":0,"w":64,"h":32}, …same ops… ](Note clip coordinates are themselves origin-translated — set origin first with the
window in mind, or set clip before origin as above.)
{"op":"text","x":64,"y":2,"s":"21° café","align":"center","size":13,"color":[255,220,0]}
{"op":"text","x":64,"y":2,"s":"BIG","aa":true,"size":34,"align":"center"}
{"op":"text","x":2,"y":2,"s":"Score","outline":[0,0,0]}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | anchor; y is the glyph top
|
s |
string | "" | UTF-8; transcoded to CP1252 on device (°, é, ß … all fine) |
size |
int | 10 | bitmap faces: 8, 9, 10, 13, 18, 20 px (nearest wins) |
font |
string | — | an uploaded font by name (PUT /api/canvas/font); unknown names keep the built-in face |
align |
string | left |
left | center | right, about x
|
aa |
bool | false |
fw 3.5: anti-aliased Orbitron instead of the bitmap face — size ≥ 30 → 34 px, ≥ 18 → 24 px, else 13 px; charset A–Z 0–9 :.-+%/ (input folded to uppercase, other glyphs skipped) |
outline |
[r,g,b] |
— | fw 3.5: 1 px ring under the text (bitmap path) |
shadow |
[r,g,b] |
— |
fw 3.5: +1,+1 drop shadow (bitmap path; outline wins if both) |
color |
[r,g,b] |
white |
Bitmap glyphs draw transparent (background shows between strokes) — text over gradients/images works directly. The AA path is the one for big numerals: clocks, temperatures, scores.
{"op":"textbox","x":4,"y":4,"w":120,"h":24,"s":"Multi-line text wraps by word and stays inside the box","size":8,"align":"center","valign":"middle"}| param | type | default | |
|---|---|---|---|
x, y, w, h
|
int | 0 | the box |
s |
string | "" | UTF-8; explicit \n forces a line break |
size / font
|
10 / — | as text (bitmap faces only) |
|
align |
string | left | per-line: left | center | right
|
valign |
string | top | block: top | middle | bottom
|
color |
[r,g,b] |
white |
Greedy word-wrap; a word longer than the box hard-breaks; everything is clipped to the box (up to 10 lines). The message-card primitive.
{"op":"image","x":8,"y":8,"w":16,"h":16,"fmt":"rgb565","data":"<base64>"}| param | type | default | |
|---|---|---|---|
x, y
|
int | 0 | top-left |
w, h
|
int | 0 | pixel dimensions of the data |
fmt |
string | rgb888 |
rgb888 | rgb565 (big-endian) |
data |
string | — | base64 of w*h*bpp bytes |
One-off small graphics. For anything reused, upload a sprite sheet once
(Atlas) and sprite
it — base64-in-JSON is ~33 % bigger and re-sent every time.
{"op":"atlas","name":"weather"}Binds the named sheet for subsequent sprite ops. Sticky across batches (rebind
per batch if you use content-fingerprint names). Lazy-loads a persisted sheet from
flash on first bind; an unknown name binds nothing and later sprites no-op.
{"op":"sprite","i":4,"x":10,"y":6}
{"op":"sprite","i":4,"x":10,"y":6,"flip":"h","rot":90,"scale":2}| param | type | default | |
|---|---|---|---|
i |
int | — | tile index in the bound sheet |
x, y
|
int | 0 | destination top-left |
flip |
string | — |
h | v | hv — applied first, in tile space (fw 3.5) |
rot |
int | 0 |
90 | 180 | 270, clockwise, after the flip (fw 3.5) |
scale |
int | 1 | 1–4, integer pixel scaling (fw 3.5) |
Magenta (255,0,255 / 0xF81F) is transparent. With transforms, one sheet serves
every orientation — a game ships one spaceship, not eight. Rotation pivots the tile
about its own top-left destination box (a 90°-rotated w×h tile occupies h×w).
{"op":"gradient","x":0,"y":0,"w":128,"h":32,"from":[10,16,44],"to":[3,5,16],"dir":"v"}| param | type | default | |
|---|---|---|---|
x, y, w, h
|
int | 0 | box |
from, to
|
[r,g,b] |
— | end colours |
dir |
string | v |
v (top→bottom) | h (left→right) |
The backdrop primitive (skies, cards).
{"op":"scroll","dx":-1,"dy":0,"color":[0,0,0]}| param | type | default | |
|---|---|---|---|
dx, dy
|
int | 0 | shift, pixels (negative = left/up) |
color |
[r,g,b] |
black | fill for the vacated strip |
Shift the whole back buffer and repaint only the fresh edge — the cheap way to pan or tick a chart along.
{"op":"show"}Presents the back buffer (double-buffered; the firmware's tear-guard paces the swap).
A batch without any show auto-presents at the end — reach for explicit show
only for multi-present batches or on the stream channel (which never auto-presents).
| you want | use |
|---|---|
| shapes, gauges, text, sprites | ops (this page) |
| photographic / Pillow-rendered frames |
PUT /api/canvas/frame (or QOI) |
| small changed regions of a frame |
PUT /api/canvas/rects deltas |
| animation-rate updates, no per-frame HTTP | the stream channel (ops record 0x03 carries this page's ops) |
| panel-side animation with zero network |
effects (POST /api/canvas/effect) |
| scrolling text | the ticker |
Full endpoint semantics: Canvas API. Machine-readable: the device's own
GET /openapi.yaml.
Start
Build it
- Hardware
- Module Firmware
- Provisioning
- Calibration
- Flaps & Character Sets
- SplitFlap Gateway
- Matrix Gateway
- LCD Gateway
Drive it
- Companion
- Built-in Apps
- Standalone & Docker
- Multiple Displays
- Home Assistant
- Vestaboard API
- MCP Server
- Using splitflap-os
Extend it
Reference
Hardware © Adam G Makes