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:
2026-07-31 13:27:20 -04:00
parent 04e4f875af
commit 91f71f92bd
6 changed files with 153 additions and 88 deletions
+24 -13
View File
@@ -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:17: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
View File
@@ -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
View File
@@ -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) --------------------------
+6 -10
View File
@@ -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;