Skip to content

feat(shaders): add PaletteMap, a dithered palette Effect - #279

Merged
hunterbecton merged 6 commits into
mainfrom
t3/sha-308
Nov 3, 2026
Merged

hunterbecton merged 6 commits into
mainfrom
t3/sha-308

Conversation

@hunterbecton

Copy link
Copy Markdown
Member

Why

Composition 2 maps a gray heat field onto six colors, and scatters single cells across each band edge instead of drawing a line. No existing part could do that. Dither quantizes each RGB channel on its own, and FractalNoise's stops land on chosen colors but cut clean contours.

What changes

PaletteMap

A new Effect that reads the OKLab lightness of the image beneath it and picks one of its colors, spaced evenly from darkest to lightest. A blue-noise threshold per cell decides whether a cell between two colors rounds up or down. spread sets how wide that scatter runs and defaults to 0.5. pixelSize sets the cell, and matching it to LedWall's spacing gives each dot one color.

Color-only resamples

PaletteMap registers as a resample rather than a color transform, so it runs inside LedWall's cell snap once per cell. ADR 0006 records the rule. It also records what follows: CursorTrail (SHA-169) must be a resample too, or its heat never reaches the map. The glossary gains a Color transform entry, and useResample's JSDoc no longer says a resample reads only what Sources drew.

Docs and tests

The component page demos PaletteMap over a gray FractalNoise with contrast={3.5}, because at the default contrast the darkest of the four default colors barely shows. A visual spec pins the page on the WebGL2 fallback. Two pixel checks on /dev/palette-map-probe cover composition 2's setup: every LED dot holds one palette color, and spread scatters cells across band edges. The canvas readback they use moves out of the FlutedGlass spec into helpers.ts.

Known limitations

CI checks only the WebGL2 fallback. WebGPU was checked by eye in the dev server only.

Closes SHA-308

PaletteMap maps the lightness of the image beneath it onto a few chosen
colors. A blue-noise threshold per cell scatters the two colors at each
band edge, and spread sets how wide that scatter runs. It registers as a
resample, so under LedWall it runs once per cell and each dot takes
exactly one palette color. A dev probe at /dev/palette-map-probe shows it
over a gray FractalNoise.

Refs SHA-308
An Effect that changes only color registers as a resample when a UV warp
mounted after it must move it. PaletteMap is the first, and CursorTrail
follows the same rule so its heat reaches the palette map. The glossary
gains a Color transform entry to name the other case, and useResample's
JSDoc now says a resample reads what the resamples beneath it changed.

Refs SHA-308
The page demos PaletteMap over a gray FractalNoise with its contrast at
3.5, so the noise's lightness covers the full range and all four default
colors show. At the default contrast the first color barely appears. The
panel edits pixel size, spread, and the palette.

A visual spec pins the default story on the WebGL2 fallback. Two pixel
checks on the dev probe cover the composition 2 setup under LedWall:
each dot holds one palette color, and spread scatters cells across band
edges. The canvas readback they use moves out of the FlutedGlass spec
into helpers.ts so both specs share it.

Refs SHA-308
Copilot AI balanced review requested due to automatic review settings November 3, 2026 01:46
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

React Doctor found no new issues. 🎉

Reviewed by React Doctor for commit 3f1bb10.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Palette mapping can select incorrect colors for wide-gamut sources, leave static scenes showing stale palettes after color changes, and introduce stray cells at spread 1.

3 open issues
What changed in this PR

Adds PaletteMap, a lightness-based palette Effect for composition 2’s dithered heat field. It runs as a resample so a later LedWall can display one palette color per dot.

Changes:

  • Adds the public component with palette, cell-size, and scatter controls.
  • Integrates the docs demo and records the color-only resample rule.
  • Adds visual and pixel checks and shares canvas-readback helpers.
File Description
scripts/​build-posters.sh Adds PaletteMap poster generation.
packages/​shaders/​src/​react/​hooks/​use-resample/​use-resample.ts Clarifies resample inputs in JSDoc.
packages/​shaders/​src/​components/​palette-map/​shader.tsx Implements lightness mapping and per-cell dithering.
packages/​shaders/​src/​components/​palette-map/​palette-map.tsx Defines public props and defaults.
packages/​shaders/​src/​components/​index.ts Exports PaletteMap and its props.
GLOSSARY.md Clarifies resamples and defines color transforms.
docs/​adr/​0006-color-only-effects-under-a-uv-warp-register-as-resamples.md Records the color-only resample decision.
apps/​docs/​src/​content/​components.ts Adds the component’s catalog entry.
apps/​docs/​src/​compositions/​component-map.ts Makes PaletteMap available to compositions.
apps/​docs/​src/​app/​dev/​palette-map-probe/​probe-scene.tsx Adds a configurable LED-wall probe.
apps/​docs/​src/​app/​dev/​palette-map-probe/​page.dev.tsx Loads the probe client-side.
apps/​docs/​src/​app/​components/​palette-map/​scene.tsx Demonstrates mapping a grayscale noise field.
apps/​docs/​src/​app/​components/​palette-map/​params.ts Defines demo defaults and palette limits.
apps/​docs/​src/​app/​components/​palette-map/​demo.tsx Adds interactive controls and preview.
apps/​docs/​src/​app/​components/​demo-registry.tsx Registers the demo, controls, and usage guidance.
apps/​docs-tests/​visual/​palette-map.spec.ts Adds screenshot, dot-color, and scatter checks.
apps/​docs-tests/​visual/​helpers.ts Extracts reusable canvas pixel readback.
apps/​docs-tests/​visual/​fluted-glass.spec.ts Uses the shared readback helper.
.changeset/​palette-map.md Announces the minor release addition and usage constraints.

Review effort: Balanced

Copilot AI balanced review requested due to automatic review settings November 3, 2026 02:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

RGB clamping distorts wide-gamut lightness and selects incorrect palette colors, while blue-noise thresholds reaching 1 introduce stray colors in flat areas at spread=1.

3 open issues

Review effort: Balanced

- Measure lightness without clamping negative channels first. The clamp
  lifted the L of wide-gamut colors and could pick the next palette color.
- Center the blue-noise bytes inside 0..1. A byte of 255 read back as 1,
  so at spread 1 some cells in flat black areas took the second color.
- Request a frame after a palette change, so a static scene shows it.
Copilot AI balanced review requested due to automatic review settings November 3, 2026 02:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Removing PaletteMap from a static scene leaves the last palette-mapped frame visible because unmount cleanup does not request a render after the output graph is rebuilt.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Request a render when removing the palette map

packages/​shaders/​src/​components/​palette-map/​shader.tsx:213

Removing PaletteMap from an otherwise unchanged static scene (for example, over <LinearGradient speed={0} />) leaves the last palette-mapped frame visible. useResample unregisters and rebuilds the output graph, but no unmount cleanup requests a frame, so the parked scheduler never draws the image without the map. Return a cleanup here that requests a render after the resample has been removed.

3 resolved since last review

Review effort: Balanced

Unmounting removes the resample and rebuilds the output material, but
nothing asked for a frame, so a static scene kept showing the mapped image.
Copilot AI balanced review requested due to automatic review settings November 3, 2026 02:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The PaletteMap change has no active review issues requiring resolution.

No open issues 🎉

Review effort: Balanced

@hunterbecton
hunterbecton merged commit 3b47dad into main Nov 3, 2026
12 checks passed
@hunterbecton
hunterbecton deleted the t3/sha-308 branch November 3, 2026 02:32
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.

2 participants