Skip to content

feat(board): LED bitmap layers for pixel canvases - #343

Merged
jeffredodd merged 1 commit into
mainfrom
feat/led-bitmap-layers
Oct 7, 2026
Merged

jeffredodd merged 1 commit into
mainfrom
feat/led-bitmap-layers

Conversation

@jeffredodd

Copy link
Copy Markdown
Member

What / why

FiestaBoard pages are getting pixel canvases (gradients, shapes, plugin art) on LED pages. FiestaBoard core does all canvas work (evaluation, rasterisation, placement in panel pixels) and hands the preview finished RGBA bitmaps. This PR adds the part FiestaUI owns: drawing those bitmaps (design §3 step 5, §4).

Stacks on #342 (feat/led-text-size). Until #342 merges, this diff includes its commit. Review only the top commit (a9b53128).

API

  • LedBitmapLayer = { x, y, width, height, rgba: Uint8ClampedArray | string /* base64 RGBA, row-major */ }, DecodedLedBitmapLayer, decodeLedBitmapLayer (normalises the layer: floors the geometry, pads or cuts rgba to w×h×4, and returns null for bad base64 or a layer with no area), decodeLedBitmapRgba, ledMonochromeLit. All are exported from the index.
  • LedLayoutOptions.layers is used by layoutLedMessage, layoutLedCellGrid and renderLedFrame. Each layer becomes one new LedDrawOp { kind: "bitmap", x, y, width, height, rgba }, appended after every cell op. LedLayout.layers is set only when layers exist.
  • How rasterizeLedOps paints a layer pixel: alpha > 0 overwrites with the RGB (no blending), alpha 0 leaves the pixel underneath, and the layer is clipped to the matrix.
  • Monochrome: a pixel with alpha > 0 lights in the panel colour when 299·r + 587·g + 114·b ≥ 127500 (PIL L weights, so the Python port can match it) and is unlit otherwise. The thresholded bytes are stored in the op.
  • ledBackgroundMask: opaque layer pixels over a block field are unmasked, so they glow like lit LEDs.
  • Transitions: per-pixel kinds work on frames, so layers come along automatically. For flip and cascade, mid-way layouts use the from layers in the first half and the to layers in the second half: flip frame f of F switches when 2f ≥ F−1, cascade slot n of N when 2n ≥ N. Layers are repainted over every half-flap. A change of layers alone snaps under per-cell kinds.
  • LedMatrixDisplay gets a layers prop (it comes from LedLayoutOptions) and reports data-layers. A board with layers but no text is not "empty". The previous layers come along through the shown layout, so a transition runs from what was on screen.
  • DisplayPreview gets a layers prop that it passes only to the LED renderer. Split-flap renderers ignore it.

Paint performance

paintLedFrame keeps one Path2D per colour up to 64 colours, so every existing frame paints exactly as before and VRT is unchanged. Past 64 colours (for example a photo-like 64×64 layer with 4,096 colours), the lit dots are traced into one path and filled once on a scratch canvas. The frame is then drawn over it at 1 px per LED, nearest-neighbour, with source-in, so each dot keeps its shape and takes its LED's colour. That is 2 fills plus a few drawImage calls, regardless of the colour count. I checked the result by eye in Storybook (Chromium): the Pixoo canvas, full-bleed plasma and MAX7219 stories all render correctly.

Stories

  • Pixoo64Canvas: a sky gradient, sun and hills canvas, bled to the top edge, above two text rows.
  • FullBleedArt: a 64×64 plasma.
  • MonochromeLayer: a MAX7219 with a grey ramp and a sun.

VRT should change for these new stories only.

Test evidence

All run in node:24 Docker:

  • vitest: 68 files, 1236 tests passed
  • prettier --check .: clean
  • eslint src --quiet: clean
  • tsc --noEmit: clean
  • node --test scripts/ci/tests/*.test.mjs: 335 passed

The tests were written first. The new lib tests failed 18 of 26 before the implementation, and the component tests failed 5. A mutation pass then broke each behaviour and confirmed a test fails:

Mutation Failing tests
threshold 127_500 → 127_000 1
flip rule 2f ≥ F−1 → f ≥ F−1 2 (unit + golden)
cascade rule 2n ≥ N → n ≥ N 1
skip repainting layers after a half-flap 2
alpha-0 pixels painted 2
no left clip (row wrap) 1, after adding a dedicated test (the first version survived this mutation)
bloom unmask reverted 1
MAX_COLOR_PATHS → 100 000 1 (photo paint test)
empty-label gating removed 1
DisplayPreview stops passing layers 1

Goldens: led-golden.json gains 2 layout and 2 transition layer cases (options.layers, fromLayers / toLayers). A JSON comparison confirms all 28 existing layout cases and 11 existing transition cases are byte-identical.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Visual regression report

0 changed · 12 added · 0 removed · 3140 unchanged

Status Screenshot Diff
✨ Added desktop/dark/app-board-ledmatrixdisplay--full-bleed-art.png —
✨ Added desktop/dark/app-board-ledmatrixdisplay--monochrome-layer.png —
✨ Added desktop/dark/app-board-ledmatrixdisplay--pixoo-64-canvas.png —
✨ Added desktop/light/app-board-ledmatrixdisplay--full-bleed-art.png —
✨ Added desktop/light/app-board-ledmatrixdisplay--monochrome-layer.png —
✨ Added desktop/light/app-board-ledmatrixdisplay--pixoo-64-canvas.png —
✨ Added mobile/dark/app-board-ledmatrixdisplay--full-bleed-art.png —
✨ Added mobile/dark/app-board-ledmatrixdisplay--monochrome-layer.png —
✨ Added mobile/dark/app-board-ledmatrixdisplay--pixoo-64-canvas.png —
✨ Added mobile/light/app-board-ledmatrixdisplay--full-bleed-art.png —
✨ Added mobile/light/app-board-ledmatrixdisplay--monochrome-layer.png —
✨ Added mobile/light/app-board-ledmatrixdisplay--pixoo-64-canvas.png —

📦 Download the full visual report (artifact vrt-report) · workflow run

Baseline from this run · commit ea5aa63

@jeffredodd
jeffredodd force-pushed the feat/led-bitmap-layers branch from a8a6251 to d020185 Compare October 7, 2026 04:35
FiestaBoard pages can hold pixel canvases; core rasterises them and hands
the preview finished RGBA bitmaps. FiestaUI only draws them.

- LedBitmapLayer {x, y, width, height, rgba: Uint8ClampedArray | base64}
  with decodeLedBitmapLayer / decodeLedBitmapRgba and ledMonochromeLit.
- Layout: `layers` on LedLayoutOptions (message-in and cells-in); one
  "bitmap" op per layer after every cell op. Raster: alpha > 0 overwrites,
  alpha 0 leaves, clipped. Monochrome: 299r+587g+114b >= 127500 lights in
  the panel colour, otherwise unlit. No layers = byte-identical layouts.
- Transitions: per-pixel kinds include layers via frames; flip/cascade use
  the old layers for the first half of frames and the new for the second,
  and repaint layers over half-flaps.
- LedMatrixDisplay / DisplayPreview take `layers` (LED only). Frames with
  more than 64 colours paint lit dots through one image (source-in) instead
  of a Path2D per colour.
- Golden fixtures gain layer cases; every existing case is unchanged.
- Stories: Pixoo64Canvas, FullBleedArt, MonochromeLayer.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant