Gamepad UI: multi-tone palettes, and a UI that takes its colours from them #74

Merged
enricobuehler merged 1 commits from worktree-gamepad-ui-polish into main 2026-08-06 14:48:27 +00:00
Owner

Follow-up to #66, on the same branch. Two pieces of feedback: the palettes read as uniform and unattractive, and the violet UI clashed with every background except the default.

The palettes were one hue, rotated

That was the actual defect. #66 built each palette by rotating a single colour field's hue, so every option was one tone at several brightnesses — flat next to any real gradient.

A palette is now an ordered ramp of DISTINCT hues. The 4×4 mesh samples that ramp along the diagonal 0.5·(x + y) plus a fixed per-cell offset table, so neighbouring cells land on different parts of the ramp and colours pool instead of banding; the control points' existing drift then moves those pools around. Android spreads four blobs across the same ramp. Violet keeps its explicit sixteen colours, so the default is untouched.

Twelve now, dark first then pale:

Dark Violet · Nebula · Abyss · Ember · Moss · Graphite
Pale Holo · Sunset · Bloom · Dawn · Mint · Opal

Holo and Sunset are straight takes on the two reference gradients (the foil and the poster).

every_palette_is_multi_tone measures the hue spread across all sixteen cells and fails under 45° — 20° for Graphite and Opal, which are meant to be restrained. It immediately caught Ember at 35° (all reds and oranges, exactly the flatness this rework removes) and Graphite at 3°, despite a comment claiming it drifted cool to warm. Both were rebuilt until the numbers matched the prose.

The UI now takes its colours from the palette

Each palette carries an accent and a light flag. An Ink derived from them — foreground, accent, on-accent, glass, scrim + strength — is published to the whole tree: a thread-local in the console, a \.gamepadInk environment value on Apple, LocalGamepadInk on Android. Ember's pill and focus wash are amber, Mint's are deep teal; Violet is unchanged.

Pale palettes flip the ink: dark text on white frost, with materials, tray scrims and every wash that sits under text following suit (ConsoleGlass and GamepadTrayScrim were force-.dark).

Three things only the renders could have told us

  • Additive blending blows out over a pale ground. Android's blobs and Apple's legacy field composite with Plus/plusLighter; over near-white that saturates every blob to white and Holo rendered as a grey wash. Pale palettes blend normally.
  • A white scrim at the dark field's strength BLEACHES the gradient. Mixing toward black at 0.4 reads as depth; toward white at 0.4 destroys the chroma it's drawn over. The scrim carries a per-palette strength now (u_scrim.a in the shader).
  • White glass over a bright field has far less separating it from its backdrop than dark glass over a dark one, and needed more body.

A new Roborazzi light-palette scene is what exposed the first of those — the existing scene bypasses App, so it was rendering every palette with the default dark ink and proving nothing.

Verification

Surface Result
console cargo build + clippy --all-targets -D warnings + 173 tests (pf-lxcheck2, amd64)
Apple swift build + 200 tests + a real arm64-apple-ios17.0 typecheck
Android :app:compileDebugKotlin + 62 tests
fmt cargo fmt --all clean

Plus eyeball passes on real renders at both ends of the range — console CPU rasters of the launcher and settings under Violet, Ember, Abyss, Holo, Sunset and Mint, and Roborazzi shots of the Android settings screen dark and pale.

Not done

  • On-glass pass owed, and it matters more now: the pale palettes have only been seen in CPU renders and Roborazzi shots, never on a real panel.
  • tvOS #if branches stay typecheck-unverified from a Mac (needs a tvOS Rust slice: nightly + -Zbuild-std, tier 3).
Follow-up to #66, on the same branch. Two pieces of feedback: the palettes read as *uniform* and unattractive, and the violet UI clashed with every background except the default. ## The palettes were one hue, rotated That was the actual defect. #66 built each palette by rotating a single colour field's hue, so every option was one tone at several brightnesses — flat next to any real gradient. **A palette is now an ordered ramp of DISTINCT hues.** The 4×4 mesh samples that ramp along the diagonal `0.5·(x + y)` plus a fixed per-cell offset table, so neighbouring cells land on different parts of the ramp and colours *pool* instead of banding; the control points' existing drift then moves those pools around. Android spreads four blobs across the same ramp. **Violet keeps its explicit sixteen colours**, so the default is untouched. Twelve now, dark first then pale: | | | | --- | --- | | **Dark** | Violet · Nebula · Abyss · Ember · Moss · Graphite | | **Pale** | Holo · Sunset · Bloom · Dawn · Mint · Opal | **Holo** and **Sunset** are straight takes on the two reference gradients (the foil and the poster). `every_palette_is_multi_tone` measures the hue spread across all sixteen cells and fails under 45° — 20° for Graphite and Opal, which are meant to be restrained. It immediately caught **Ember at 35°** (all reds and oranges, exactly the flatness this rework removes) and **Graphite at 3°**, despite a comment claiming it drifted cool to warm. Both were rebuilt until the numbers matched the prose. ## The UI now takes its colours from the palette Each palette carries an `accent` and a `light` flag. An **Ink** derived from them — foreground, accent, on-accent, glass, scrim + strength — is published to the whole tree: a thread-local in the console, a `\.gamepadInk` environment value on Apple, `LocalGamepadInk` on Android. Ember's pill and focus wash are amber, Mint's are deep teal; Violet is unchanged. **Pale palettes flip the ink**: dark text on white frost, with materials, tray scrims and every wash that sits under text following suit (`ConsoleGlass` and `GamepadTrayScrim` were force-`.dark`). ## Three things only the renders could have told us - **Additive blending blows out over a pale ground.** Android's blobs and Apple's legacy field composite with `Plus`/`plusLighter`; over near-white that saturates every blob to white and Holo rendered as a **grey wash**. Pale palettes blend normally. - **A white scrim at the dark field's strength BLEACHES the gradient.** Mixing toward black at 0.4 reads as depth; toward white at 0.4 destroys the chroma it's drawn over. The scrim carries a per-palette strength now (`u_scrim.a` in the shader). - **White glass over a bright field** has far less separating it from its backdrop than dark glass over a dark one, and needed more body. A new Roborazzi **light-palette** scene is what exposed the first of those — the existing scene bypasses `App`, so it was rendering every palette with the default dark ink and proving nothing. ## Verification | Surface | Result | | --- | --- | | console | `cargo build` + `clippy --all-targets -D warnings` + **173 tests** (`pf-lxcheck2`, amd64) | | Apple | `swift build` + **200 tests** + a real `arm64-apple-ios17.0` typecheck | | Android | `:app:compileDebugKotlin` + **62 tests** | | fmt | `cargo fmt --all` clean | Plus eyeball passes on real renders at both ends of the range — console CPU rasters of the launcher and settings under Violet, Ember, Abyss, Holo, Sunset and Mint, and Roborazzi shots of the Android settings screen dark and pale. ## Not done - **On-glass pass owed**, and it matters more now: the pale palettes have only been seen in CPU renders and Roborazzi shots, never on a real panel. - **tvOS `#if` branches stay typecheck-unverified** from a Mac (needs a tvOS Rust slice: nightly + `-Zbuild-std`, tier 3).
enricobuehler added 1 commit 2026-08-06 14:01:51 +00:00
feat(clients/gamepad-ui): multi-tone palettes, and a UI that takes its colours from them
ci / docs-site (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 1m18s
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m59s
android / android (pull_request) Successful in 5m53s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 6m36s
ci / rust (pull_request) Successful in 9m57s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 4m41s
4a9a1c3ed4
The first pass built each palette by rotating ONE colour field's hue, and it
showed: every option was a single tone at several brightnesses, which reads flat
next to any real gradient. A palette is now an ordered ramp of DISTINCT hues.
The 4×4 mesh samples that ramp along the diagonal with a fixed per-cell offset
table, so neighbouring cells land on different parts of it and the colours pool
and swirl instead of banding; the control points' existing drift then moves the
pools around. Violet keeps its explicit sixteen colours, so the default is
untouched.

Twelve of them now, dark first then pale: Violet, Nebula, Abyss, Ember, Moss,
Graphite, then Holo, Sunset, Bloom, Dawn, Mint, Opal. Holo and Sunset are
straight takes on the two reference gradients — foil and poster.

`every_palette_is_multi_tone` measures the hue spread across all sixteen cells
and fails under 45° (20° for Graphite and Opal, which are meant to be
restrained). It caught Ember at 35°, all reds and oranges — the very flatness
this rework exists to remove — and Graphite at 3° despite a comment claiming it
drifted cool to warm. Both were rebuilt until the numbers matched the prose.

The UI follows the palette now, rather than wearing brand violet over whatever
happens to be behind it. Each palette carries an accent and a light flag, and an
Ink derived from those (foreground, accent, on-accent, glass, scrim and its
strength) is published to the whole tree — a thread-local in the console, an
environment value on Apple, a CompositionLocal on Android. Pale palettes flip
the ink: dark text on white frost, with the materials, tray scrims and every
wash that sits under text following suit.

Three things only the renders could have told us:

  - Additive blending blows out over a pale ground. Android's blobs and Apple's
    legacy field composite with Plus/plusLighter, which over near-white
    saturates every blob to white — Holo rendered as a grey wash. Pale palettes
    blend normally.
  - A white scrim at the dark field's strength BLEACHES the gradient. Mixing
    toward black at 0.4 reads as depth; toward white at 0.4 destroys the chroma
    it is drawn over. The scrim now carries a per-palette strength.
  - White glass over a bright field has far less separating it from its backdrop
    than dark glass over a dark one, and needed more body.

Verified: console build + clippy -D warnings + 173 tests, Apple build + 200
tests + an iOS-triple typecheck, Android compile + 62 tests, and eyeball passes
on real renders of both the vivid and the pale ends (console CPU rasters; a new
Roborazzi light-palette scene, which is what exposed the blend-mode bug).
enricobuehler force-pushed worktree-gamepad-ui-polish from a9aa5357cf to 4a9a1c3ed4 2026-08-06 14:01:51 +00:00 Compare
enricobuehler merged commit 453b9850fa into main 2026-08-06 14:48:27 +00:00
enricobuehler deleted branch worktree-gamepad-ui-polish 2026-08-06 14:48:33 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#74