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.
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).
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).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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:
Holo and Sunset are straight takes on the two reference gradients (the foil and the poster).
every_palette_is_multi_tonemeasures 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
accentand alightflag. 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\.gamepadInkenvironment value on Apple,LocalGamepadInkon 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 (
ConsoleGlassandGamepadTrayScrimwere force-.dark).Three things only the renders could have told us
Plus/plusLighter; over near-white that saturates every blob to white and Holo rendered as a grey wash. Pale palettes blend normally.u_scrim.ain the shader).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
cargo build+clippy --all-targets -D warnings+ 173 tests (pf-lxcheck2, amd64)swift build+ 200 tests + a realarm64-apple-ios17.0typecheck:app:compileDebugKotlin+ 62 testscargo fmt --allcleanPlus 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
#ifbranches stay typecheck-unverified from a Mac (needs a tvOS Rust slice: nightly +-Zbuild-std, tier 3).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).a9aa5357cfto4a9a1c3ed4