palette: give the spectral mid and the envelope trace their own roles
Decouples the keyboard strip's mid stop from accent/secondary, which the deep-teal darkening had inverted. Adds overlay/trace (#816AA6), the first value to clear 3:1 against the waveform. Replaces the frozen-premise test.
This commit is contained in:
@@ -420,10 +420,26 @@ against the WCAG tests — §2.1):
|
||||
| `accent/tertiary` | pastel purple | `~194,170,232` (`#C2AAE8`) | categorical role B (tertiary zones, a distinct affordance class) |
|
||||
| `accent/hot` | brighter primary tint | `~200,236,178` (`#C8ECB2`) | hover / live / drag feedback (a lighter pastel-lime) |
|
||||
|
||||
**Locked values, where they diverge from the starting table (2026-07-31).** `accent/secondary`
|
||||
darkened to `#38A8A0` (same hue ~176°, same saturation, lightness 0.68 → 0.44), which put its
|
||||
binding limiter on the AA 4.5:1 text-on-fill pair (4.91:1 on `bg/base`) rather than the 3:1
|
||||
indicator floor (3.94:1 on `bg/cell`). Two roles were added past the original three, since
|
||||
"exactly three accents" is no longer a constraint: `overlay/trace` `#816AA6` (a muted violet for
|
||||
marks drawn *over* an accent fill — see §the two-neighbour problem below), and the spectral
|
||||
ramp's mid stop, which keeps the original pastel teal `#84D6D0` as its own constant rather than
|
||||
aliasing `accent/secondary`.
|
||||
|
||||
**The two-neighbour problem.** A mark drawn *over* an accent fill has to read against the fill
|
||||
*and* the surface behind it. For the envelope trace those are the lime waveform (relative
|
||||
luminance 0.648) and `bg/base` (0.024) — 9.41:1 apart, so `sqrt(9.41)` ≈ **3.07:1 is the most
|
||||
any single value can clear against both**. `overlay/trace` sits at that optimum; it has no
|
||||
headroom in either direction, and no value at all can clear 3:1 against the lime *and* on
|
||||
`bg/cell`. Such a role is confined to the band it was picked for.
|
||||
|
||||
These are light-ish, low-saturation tints. On the **REAPER-grey surfaces** (`bg/cell` ≈
|
||||
`#3a3a3a`) they still clear **AA-large (3:1)** and the **state-indicator** floor — but the
|
||||
margin is **much smaller than it was on near-black** (roughly ~6:1–7:1 on grey vs. ~15:1
|
||||
on `#12121x`). This is a real DS-2-revision tension: **the greyer background pulls the
|
||||
margin is **much smaller than it was on near-black** (~7.6:1 primary / ~5.5:1 tertiary /
|
||||
~3.9:1 the darkened secondary on grey, vs. ~15:1 on `#12121x`). This is a real DS-2-revision tension: **the greyer background pulls the
|
||||
pastels toward the floor** at the same time the "keep it soft" rule pulls them away from
|
||||
saturation. Approach from the **soft side** (§2.1) — keep them as pastel as possible while
|
||||
still clearing the floor on **grey, not near-black** — but **if any pastel used as a state
|
||||
@@ -470,9 +486,12 @@ the rest of the UI. Concretely: the ramp runs **pastel-lime (primary, low) → p
|
||||
(secondary, mid) → pastel-purple (tertiary, high)** as a three-stop gradient through the
|
||||
accent constants (or a slightly wider pastel arc that passes *through* those three anchor
|
||||
points), keeping every stop in the pastel band. This ties the spectrum to the palette:
|
||||
the same three hues that mean "live / category A / category B" elsewhere are the endpoints
|
||||
and midpoint of the spectrum here, so the strip reads as an extension of the accent system,
|
||||
not a separate neon flourish. The **active** zone still lifts to `accent/primary` +
|
||||
the same hues that mean "live / category A / category B" elsewhere anchor the spectrum here,
|
||||
so the strip reads as an extension of the accent system, not a separate neon flourish.
|
||||
**The mid stop is its own constant, not an alias of `accent/secondary`** (decoupled
|
||||
2026-07-31): the ramp is a luminance progression while the accents are categorical roles, and
|
||||
darkening secondary for a categorical reason inverted lo→mid→hi. A monotonicity test now guards
|
||||
the ordering. The **active** zone still lifts to `accent/primary` +
|
||||
its bloom, so "which zone is live" stays unambiguous over the categorical spectral bands.
|
||||
|
||||
- **Feel:** premium, almost visualizer-grade — but soft and cohesive, a pastel spectrum
|
||||
|
||||
+24
-13
@@ -24,23 +24,34 @@ L7 sub-pass, 2026-07-27):
|
||||
- **DS-2 — palette is role-based, not hardcoded hue**, in one constants block
|
||||
(`theme`): `bg/base`, `bg/panel`, `bg/cell`, `line/hairline`, `text/primary`,
|
||||
`text/dim`, `accent/primary`, `accent/secondary`, `accent/tertiary`,
|
||||
`accent/hot`, `warn`. Neutral ladder sits in REAPER's mid-grey theme family
|
||||
(`bg/base` ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈ `#3a3a3a`,
|
||||
`line/hairline` ≈ `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈
|
||||
`accent/hot`, `overlay/trace`, `warn`. Neutral ladder sits in REAPER's mid-grey
|
||||
theme family (`bg/base` ≈ `#2b2b2b`, `bg/panel` ≈ `#333333`, `bg/cell` ≈
|
||||
`#3a3a3a`, `line/hairline` ≈ `#4a4a4a`, `text/primary` ≈ `#dcdcdc`, `text/dim` ≈
|
||||
`~#a0a0a0`+), elevation-ladder discipline (base < panel < cell by a few %,
|
||||
micro-gradient + inner highlight/shadow carry elevation, not hard borders).
|
||||
Three pastel accents carry categorical meaning: **primary (pastel lime) =
|
||||
live/active/selected**, secondary (pastel teal) + tertiary (pastel purple) =
|
||||
micro-gradient + inner highlight/shadow carry elevation, not hard borders). The
|
||||
accents carry categorical meaning: **primary (pastel lime) =
|
||||
live/active/selected**, secondary (deep teal) + tertiary (pastel purple) =
|
||||
supporting categorical distinctions (kinds, not intensity). `warn` (red/amber)
|
||||
is reserved **only** for byte-deleting or clip states (prune, delete).
|
||||
**The role count is not fixed at three** — a shade that works better in one spot
|
||||
earns its own named role (Daniel, 2026-07-31). What does not relax: draw sites
|
||||
ask by role. A literal color outside `theme`'s constants block is the violation,
|
||||
not the role count.
|
||||
- **WCAG-floor discipline (tight pairs to re-verify on any palette change):**
|
||||
`text/dim` on `bg/panel`/`bg/cell` is the classic mid-grey-on-mid-grey floor
|
||||
failure — must clear AA 4.5:1 body text. The three pastels as state
|
||||
indicators/active fills on `bg/cell` have a shrunken contrast cushion
|
||||
(~6:1–7:1, still clears 3:1 but re-check on any hue nudge). Body text on a
|
||||
pastel fill is a tight AA 4.5:1 pair. Take the most pastel value that still
|
||||
clears the floor, approached from the soft side, never re-saturated toward
|
||||
neon "to be safe."
|
||||
failure — must clear AA 4.5:1 body text. The accents as state indicators/active
|
||||
fills on `bg/cell` have a shrunken contrast cushion (primary ~7.6:1, tertiary
|
||||
~5.5:1, secondary only ~3.9:1 since it was darkened — all still clear 3:1, but
|
||||
re-check every one on any hue nudge). Body text on an accent fill is a tight AA
|
||||
4.5:1 pair, and for `accent/secondary` it is the *binding* limiter (~4.9:1), not
|
||||
the indicator floor. Take the most pastel value that still clears the floor,
|
||||
approached from the soft side, never re-saturated toward neon "to be safe."
|
||||
- **A mark drawn OVER an accent fill answers to two neighbours at once.**
|
||||
`overlay/trace` (the envelope trace on the waveform) must clear 3:1 against both
|
||||
`accent/primary` and `bg/base`, which sit 9.41:1 apart — so `sqrt(9.41)` ≈ 3.07:1
|
||||
is the ceiling for *any* single value, and the role sits at that optimum with no
|
||||
headroom either way. It is therefore confined to the waveform band: no value can
|
||||
clear 3:1 on `bg/cell` and against the lime simultaneously.
|
||||
- **"Speed is the selling point" — a design constraint on this geometry, not
|
||||
just the draw layer.** Sub-frame hover/press/drag feedback must repaint
|
||||
immediately on the input message. **No decorative animation** — no
|
||||
@@ -68,7 +79,7 @@ L7 sub-pass, 2026-07-27):
|
||||
- `tab_strip` — REAPER-free scrollable tab-strip layout + hit-test for the named-banks strip.
|
||||
- `prune_button` — pure layout/hit-test for the `bank_panel` footer Prune button.
|
||||
- `drag_out` — pure OS drag-out module: gesture-boundary decision and path-list assembly. The `InstrumentDrop` gesture signals that the shell should execute an instrument-drop rather than a file-copy drag.
|
||||
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + three-accent pastel system, WCAG contrast-floor helpers.
|
||||
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + the pastel accent system, the keyboard strip's spectral ramp (its own three stops, deliberately not aliases of the accents), WCAG contrast-floor helpers.
|
||||
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
|
||||
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
|
||||
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement.
|
||||
|
||||
+23
-15
@@ -34,24 +34,31 @@ constexpr KitColor kDirHairline {74, 74, 74, 255}; // #4a4a4a — subtle ste
|
||||
constexpr KitColor kDirTextPrimary{220, 220, 220, 255}; // #dcdcdc
|
||||
constexpr KitColor kDirTextDim {168, 168, 168, 255}; // #a8a8a8 (lifted from #a0a0a0)
|
||||
|
||||
// Three-accent pastel system: primary = pastel lime (the live/active/selected signal);
|
||||
// secondary = pastel teal, tertiary = pastel purple (CATEGORICAL distinctions — a KIND, never
|
||||
// intensity). accent/hot is a brighter tint OF the primary for hover/live/drag. On bg/cell the
|
||||
// pastels clear the 3:1 indicator floor comfortably at these values (primary ~7.6, secondary
|
||||
// ~6.8, tertiary ~5.5), so no per-hue nudge was needed. warn is reserved for byte-deleting
|
||||
// states only.
|
||||
// Accent system: primary = pastel lime (the live/active/selected signal); secondary = deep
|
||||
// teal, tertiary = pastel purple (CATEGORICAL distinctions — a KIND, never intensity).
|
||||
// accent/hot is a brighter tint OF the primary for hover/live/drag. Secondary is the one
|
||||
// accent off the pastel side: its binding limiter is the AA 4.5:1 text-on-fill pair (a bg/base
|
||||
// label on a secondary fill), NOT the 3:1 indicator floor, so it cannot go darker than this.
|
||||
// warn is reserved for byte-deleting states only.
|
||||
constexpr KitColor kDirAccentPrimary {176, 224, 152, 255}; // #B0E098 — pastel lime
|
||||
constexpr KitColor kDirAccentSecondary{132, 214, 208, 255}; // #84D6D0 — pastel teal
|
||||
constexpr KitColor kDirAccentSecondary{56, 168, 160, 255}; // #38A8A0 — deep teal
|
||||
constexpr KitColor kDirAccentTertiary {194, 170, 232, 255}; // #C2AAE8 — pastel purple
|
||||
constexpr KitColor kDirAccentHot {200, 236, 178, 255}; // #C8ECB2 — lighter pastel lime
|
||||
constexpr KitColor kDirWarn {235, 120, 90, 255}; // #eb785a — destructive only
|
||||
|
||||
// Spectral ramp: pastel lime (low) -> pastel teal (mid) -> pastel purple (high). Endpoints and
|
||||
// midpoint ARE the three accent constants (single source), so the keyboard strip reads as an
|
||||
// extension of the accent system.
|
||||
constexpr KitColor kDirSpectralLo = kDirAccentPrimary;
|
||||
constexpr KitColor kDirSpectralMid = kDirAccentSecondary;
|
||||
constexpr KitColor kDirSpectralHi = kDirAccentTertiary;
|
||||
// The envelope trace over the waveform. Two neighbours pin it at once — the lime accent it
|
||||
// crosses and bg/base behind it — which bound ANY single value to 3.07:1 against both; this is
|
||||
// that optimum, with no room in either direction. No categorical accent can carry it: every
|
||||
// one of them is light enough to fall under 2:1 against the lime.
|
||||
constexpr KitColor kDirOverlayTrace{129, 106, 166, 255}; // #816AA6 — muted violet
|
||||
|
||||
// Spectral ramp: pastel lime (low) -> pastel teal (mid) -> pastel purple (high). The mid stop
|
||||
// is its OWN value rather than the secondary accent it once aliased — the ramp is a luminance
|
||||
// progression while the accents are categorical roles, and darkening secondary for a
|
||||
// categorical reason inverted lo->mid->hi. Do not re-alias it.
|
||||
constexpr KitColor kDirSpectralLo = kDirAccentPrimary;
|
||||
constexpr KitColor kDirSpectralMid{132, 214, 208, 255}; // #84D6D0 — pastel teal
|
||||
constexpr KitColor kDirSpectralHi = kDirAccentTertiary;
|
||||
|
||||
// --- state transform helpers -------------------------------------------------
|
||||
|
||||
@@ -108,6 +115,7 @@ KitColor roleColor(Role role) {
|
||||
case Role::AccentSecondary: return kDirAccentSecondary;
|
||||
case Role::AccentTertiary: return kDirAccentTertiary;
|
||||
case Role::AccentHot: return kDirAccentHot;
|
||||
case Role::OverlayTrace: return kDirOverlayTrace;
|
||||
case Role::Warn: return kDirWarn;
|
||||
}
|
||||
return kDirBgBase; // unreachable; keeps non-void control flow total
|
||||
@@ -144,8 +152,8 @@ KitColor roleColorState(Role role, InteractionState state) {
|
||||
KitColor spectralColor(double t) {
|
||||
if (t < 0.0) t = 0.0;
|
||||
if (t > 1.0) t = 1.0;
|
||||
// Interpolate each half separately so the midpoint IS the secondary accent (a single
|
||||
// Lo->Hi lerp would skip it and drift the ramp off the accent family).
|
||||
// Interpolate each half separately so the mid stop is actually hit — a single Lo->Hi lerp
|
||||
// would skip it and drift the ramp off the pastel arc.
|
||||
if (t <= 0.5) {
|
||||
return mix(kDirSpectralLo, kDirSpectralMid, t / 0.5);
|
||||
}
|
||||
|
||||
+10
-8
@@ -1,14 +1,14 @@
|
||||
#pragma once
|
||||
// theme — the palette + type-scale core of the shared drawing kit: a ROLE-based color model
|
||||
// (bg/base, bg/panel, bg/cell, line/hairline, text/primary, text/dim, accent/primary,
|
||||
// accent/secondary, accent/tertiary, accent/hot, warn), an interaction-state model
|
||||
// (rest/hover/active/pressed/dragging/focus/disabled), and the WCAG contrast math that lets a
|
||||
// unit test prove every text-on-surface pair clears its floor.
|
||||
// accent/secondary, accent/tertiary, accent/hot, overlay/trace, warn), an interaction-state
|
||||
// model (rest/hover/active/pressed/dragging/focus/disabled), and the WCAG contrast math that
|
||||
// lets a unit test prove every text-on-surface pair clears its floor.
|
||||
//
|
||||
// Every role color is produced by roleColor() from ONE direction constants block (theme.cpp) —
|
||||
// the single point of change; no shell hardcodes a color, it asks by role. The spectral hue ramp
|
||||
// (spectralColor) lives here too so the keyboard strip derives its per-note hue from the same
|
||||
// source, anchored on the three accents (primary -> secondary -> tertiary).
|
||||
// source.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -36,9 +36,11 @@ enum class Role {
|
||||
TextPrimary, // labels, values
|
||||
TextDim, // secondary / units
|
||||
AccentPrimary, // live / active / selected — where the punch lives (pastel lime)
|
||||
AccentSecondary,// categorical role A (pastel teal) — a distinct KIND, never intensity
|
||||
AccentSecondary,// categorical role A (deep teal) — a distinct KIND, never intensity
|
||||
AccentTertiary,// categorical role B (pastel purple) — a distinct KIND, never intensity
|
||||
AccentHot, // hover / live / drag feedback (a brighter tint OF the primary accent)
|
||||
OverlayTrace, // a line/handle traced OVER an accent fill — reads against the fill AND the
|
||||
// surface behind it, which no accent role can do (they are all too light)
|
||||
Warn, // clip / destructive (prune, delete) — reserved for byte-deleting states
|
||||
};
|
||||
|
||||
@@ -70,9 +72,9 @@ KitColor roleColor(Role role);
|
||||
KitColor roleColorState(Role role, InteractionState state);
|
||||
|
||||
// Spectral hue ramp for the keyboard strip: maps normalized position t in [0, 1] (low note ->
|
||||
// high note) through accent/primary (low) -> accent/secondary (mid) -> accent/tertiary (high),
|
||||
// so the strip reads as an extension of the accent system rather than a separate flourish.
|
||||
// t is clamped to [0, 1].
|
||||
// high note) through a pastel lime -> teal -> purple arc, so the strip reads as part of the
|
||||
// palette rather than a separate flourish. The arc is its own three stops — a categorical
|
||||
// accent move must not be able to reorder it. t is clamped to [0, 1].
|
||||
KitColor spectralColor(double t);
|
||||
|
||||
// --- WCAG contrast (the "punch" rule, made testable) --------------------------
|
||||
|
||||
@@ -25,11 +25,9 @@ using audio::computeEnvelope;
|
||||
|
||||
namespace {
|
||||
// Marker roles — semantic, drawn through the kit's palette: start AND loop start/end both
|
||||
// = teal (secondary). Loop markers moved off tertiary/purple so the loop span + bars no
|
||||
// longer share a role with the envelope overlay trace, which owns tertiary exclusively in
|
||||
// this region (measured RGB was identical: #C2AAE8 for both). Secondary-vs-primary contrast
|
||||
// here measures 1.12:1, below tertiary's 1.37:1 — accepted deliberately: markers are bars
|
||||
// and a span fill, not the 1px trace that pairing was moved off of.
|
||||
// = teal (secondary). Markers are 2px bars and a 20%-alpha span fill, not the 1px trace, so
|
||||
// they live with 1.92:1 against the waveform; the trace, which cannot, has its own role.
|
||||
// Do not collapse the two back onto one role — they overlap in this rect.
|
||||
constexpr Role kRoleStartMarker = Role::AccentSecondary;
|
||||
constexpr Role kRoleLoopMarker = Role::AccentSecondary;
|
||||
} // namespace
|
||||
@@ -114,10 +112,8 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea
|
||||
const StageEnvelope env = packEnvelope(overlayEnv_, params_.play, frames, startFrame);
|
||||
const std::vector<EnvVertex> poly = buildEnvelopePolyline(env, waveArea, totalSeconds);
|
||||
|
||||
// Trace the polyline in the categorical TERTIARY accent (purple): the waveform behind it is
|
||||
// drawn in the primary lime, and the secondary teal this used to use sits too close to that
|
||||
// hue to separate from it. Clip x to the wave rect. Knots are handles, not line vertices.
|
||||
const LICE_pixel line = toLice(roleColor(Role::AccentTertiary));
|
||||
// Clip x to the wave rect. Knots are handles, not line vertices.
|
||||
const LICE_pixel line = toLice(roleColor(Role::OverlayTrace));
|
||||
const EnvVertex* prev = nullptr;
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.knot) continue;
|
||||
@@ -132,7 +128,7 @@ void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const OverlayArea
|
||||
// accent-hot when this node is the grabbed one. Every vertex is guaranteed in-bounds; the
|
||||
// handle is additionally clamped inside the band so one on an edge node never overhangs
|
||||
// into the neighbouring bands.
|
||||
const LICE_pixel handle = toLice(roleColor(Role::AccentTertiary));
|
||||
const LICE_pixel handle = toLice(roleColor(Role::OverlayTrace));
|
||||
const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot));
|
||||
for (const EnvVertex& v : poly) {
|
||||
if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseEnd) continue;
|
||||
|
||||
+66
-37
@@ -121,40 +121,55 @@ static void testSecondaryTertiaryAreDistinguishable() {
|
||||
CHECK(delta >= 60);
|
||||
}
|
||||
|
||||
// The instrument's envelope overlay is traced OVER the waveform, which draws in the primary
|
||||
// accent — an accent-on-accent pair no floor covers, since neither is a surface. It moved from
|
||||
// the secondary to the tertiary for exactly this reason, so the pair is pinned two ways: the
|
||||
// tertiary must separate from the primary MORE than the secondary did (the measurable half of
|
||||
// the move), and the separation is a hue one, since two pastels sit close in luminance by
|
||||
// construction. Whether the result reads clearly is a perceptual call, not this test's.
|
||||
static void testOverlayAccentSeparatesFromTheWaveformAccent() {
|
||||
const KitColor wave = roleColor(Role::AccentPrimary);
|
||||
const KitColor overlay = roleColor(Role::AccentTertiary);
|
||||
const KitColor prior = roleColor(Role::AccentSecondary);
|
||||
CHECK(contrastRatio(overlay, wave) > contrastRatio(prior, wave));
|
||||
// Pin the MEASURED value, not just the relational improvement: today's pair sits at
|
||||
// ~1.37:1 — BELOW the 3:1 indicator floor every other assertion in this file uses. That
|
||||
// is a known, held deviation (Daniel's palette call, tracked separately) rather than a
|
||||
// silently accepted one; a regression toward the old ~1.12:1 secondary pairing must fail.
|
||||
CHECK(contrastRatio(overlay, wave) >= 1.35);
|
||||
// Hue divergence against the waveform: the waveform's green dominates its red, the
|
||||
// overlay's red dominates its green — opposite balances, not two shades of one.
|
||||
CHECK(wave.g > wave.r);
|
||||
CHECK(overlay.r > overlay.g);
|
||||
const int delta = std::abs(int(wave.r) - int(overlay.r)) +
|
||||
std::abs(int(wave.g) - int(overlay.g)) +
|
||||
std::abs(int(wave.b) - int(overlay.b));
|
||||
CHECK(delta >= 60);
|
||||
// The envelope overlay is traced OVER the waveform, which draws in the primary accent — an
|
||||
// accent-on-accent pair no surface floor covers, since neither is a surface. overlay/trace
|
||||
// exists so that pair can clear the real 3:1 indicator floor; every categorical accent is light
|
||||
// enough to sit under 2:1 against the lime, so pointing the trace back at one fails here.
|
||||
static void testOverlayTraceClearsIndicatorFloorOnTheWaveform() {
|
||||
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::AccentPrimary))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// The trace's own surface floor. bg/base is the only surface it draws on (the waveform band
|
||||
// fills with it), and that confinement is forced, not convenient: clearing 3:1 against the lime
|
||||
// caps the trace's luminance at 0.183, while clearing 3:1 on bg/cell would demand 0.227. The two
|
||||
// are mutually exclusive, so this role must not be reused outside that band.
|
||||
static void testOverlayTraceClearsFloorOnItsDrawSurface() {
|
||||
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::BgBase))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// editor_paint_waveform.cpp draws the loop span/markers and the envelope overlay trace into
|
||||
// the SAME overlay rect. Loop markers are secondary (this track's reassignment, off tertiary,
|
||||
// which the envelope overlay now owns exclusively there); a future palette edit that collapses
|
||||
// the two back to one role must fail here rather than silently recreating the finding.
|
||||
static void testLoopMarkerAndEnvelopeOverlayRolesAreDistinct() {
|
||||
// the SAME overlay rect, so they must read as two things. Luminance cannot carry that — each is
|
||||
// pinned near its own floor — so the separation is a hue one: teal leans green over red, the
|
||||
// trace violet leans red over green. A palette edit collapsing them onto one role fails here.
|
||||
static void testLoopMarkerAndOverlayTraceAreDistinct() {
|
||||
const KitColor loopMarker = roleColor(Role::AccentSecondary);
|
||||
const KitColor envelopeOverlay = roleColor(Role::AccentTertiary);
|
||||
CHECK(!(loopMarker == envelopeOverlay));
|
||||
const KitColor trace = roleColor(Role::OverlayTrace);
|
||||
CHECK(!(loopMarker == trace));
|
||||
CHECK(loopMarker.g > loopMarker.r);
|
||||
CHECK(trace.r > trace.g);
|
||||
const int delta = std::abs(int(loopMarker.r) - int(trace.r)) +
|
||||
std::abs(int(loopMarker.g) - int(trace.g)) +
|
||||
std::abs(int(loopMarker.b) - int(trace.b));
|
||||
CHECK(delta >= 60);
|
||||
}
|
||||
|
||||
// AccentSecondary was darkened from a pastel teal (#84D6D0) to a deep teal (#38A8A0, same
|
||||
// hue/saturation, lower lightness). Locks the re-measured numbers so a future palette nudge
|
||||
// that erodes past either the 3:1 indicator floor or the tighter AA 4.5:1 text-on-fill floor
|
||||
// fails the build. The text-on-fill pair (bg/base label on a secondary fill) is the binding
|
||||
// limiter — it clears with far less headroom (~4.9:1) than the indicator floor on bg/cell
|
||||
// (~3.9:1), which is why the value could not go darker than this.
|
||||
static void testAccentSecondaryDarkerTealClearsFloors() {
|
||||
const KitColor sec = roleColor(Role::AccentSecondary);
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgBase)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgPanel)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= textFloor(TextClass::Large));
|
||||
// Tighter pins at the measured values: a regression that darkens secondary further would
|
||||
// clear these before it clears the nominal 3:1/4.5:1 floors above, catching the drift early.
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= 3.9);
|
||||
CHECK(contrastRatio(roleColor(Role::BgBase), sec) >= 4.9);
|
||||
}
|
||||
|
||||
static void testWarnClearsStateFloorOnBackground() {
|
||||
@@ -244,15 +259,15 @@ static void testDisabledDropsAlphaAndDesaturates() {
|
||||
// --- Direction C spectral ramp ------------------------------------------------
|
||||
|
||||
static void testSpectralIsPastelRampAnchoredOnAccents() {
|
||||
// DS-2 revised Direction C: the spectral ramp is a PASTEL sweep anchored on the three
|
||||
// accents — lime (low) -> teal (mid) -> purple (high) — NOT the old neon cool->hot.
|
||||
// DS-2 revised Direction C: the spectral ramp is a PASTEL sweep — lime (low) -> teal (mid)
|
||||
// -> purple (high) — NOT the old neon cool->hot.
|
||||
const KitColor lo = spectralColor(0.0);
|
||||
const KitColor mid = spectralColor(0.5);
|
||||
const KitColor hi = spectralColor(1.0);
|
||||
// The three stops ARE the three accent constants (single source — the strip belongs to
|
||||
// the accent system). This is the load-bearing identity of the pastel ramp.
|
||||
// The endpoints still ARE the primary/tertiary accent constants. The mid stop is NOT the
|
||||
// secondary accent and must not be re-aliased to it — see the monotonicity test below.
|
||||
CHECK(lo == roleColor(Role::AccentPrimary)); // low = pastel lime
|
||||
CHECK(mid == roleColor(Role::AccentSecondary)); // mid = pastel teal
|
||||
CHECK(!(mid == roleColor(Role::AccentSecondary)));
|
||||
CHECK(hi == roleColor(Role::AccentTertiary)); // high = pastel purple
|
||||
// Low is lime (green-dominant); high is purple (red+blue over green) — distinct hues.
|
||||
CHECK(lo.g > lo.r && lo.g > lo.b);
|
||||
@@ -265,6 +280,17 @@ static void testSpectralIsPastelRampAnchoredOnAccents() {
|
||||
CHECK(spectralColor(2.0) == hi);
|
||||
}
|
||||
|
||||
// The strip's stops must fall in ONE direction, or position along it stops meaning anything.
|
||||
// Nothing caught this when the mid stop aliased a categorical accent that was later darkened
|
||||
// below the high stop, inverting lo->mid->hi; that is the break this guards.
|
||||
static void testSpectralRampLuminanceIsMonotonic() {
|
||||
const double lo = relativeLuminance(spectralColor(0.0));
|
||||
const double mid = relativeLuminance(spectralColor(0.5));
|
||||
const double hi = relativeLuminance(spectralColor(1.0));
|
||||
CHECK(lo > mid);
|
||||
CHECK(mid > hi);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testContrastKnownAnchors();
|
||||
testTextPrimaryClearsBodyFloorOnSurfaces();
|
||||
@@ -273,8 +299,10 @@ int main() {
|
||||
testTextOnPastelFillClearsBodyFloor();
|
||||
testTextOnHoverSurfaceClearsFloor();
|
||||
testSecondaryTertiaryAreDistinguishable();
|
||||
testOverlayAccentSeparatesFromTheWaveformAccent();
|
||||
testLoopMarkerAndEnvelopeOverlayRolesAreDistinct();
|
||||
testOverlayTraceClearsIndicatorFloorOnTheWaveform();
|
||||
testOverlayTraceClearsFloorOnItsDrawSurface();
|
||||
testLoopMarkerAndOverlayTraceAreDistinct();
|
||||
testAccentSecondaryDarkerTealClearsFloors();
|
||||
testWarnClearsStateFloorOnBackground();
|
||||
testLabelOnActiveSurfaceClearsFloor();
|
||||
testRolesAreDistinctAndElevationMonotonic();
|
||||
@@ -283,6 +311,7 @@ int main() {
|
||||
testActiveIsPrimaryAccent();
|
||||
testDisabledDropsAlphaAndDesaturates();
|
||||
testSpectralIsPastelRampAnchoredOnAccents();
|
||||
testSpectralRampLuminanceIsMonotonic();
|
||||
|
||||
if (g_fail == 0) std::printf("theme: all tests passed\n");
|
||||
else std::printf("theme: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user