L-w3: complete three-accent pastel repalette + REAPER-grey neutrals

Finish roleColor/roleColorState for the three-accent enum, re-map every
Accent consumer (primary leads live; Pool/Banks titles categorical
secondary/tertiary), make spectralColor a lime->teal->purple three-stop
sweep, and re-lock the WCAG-floor tests on the grey ladder.
This commit is contained in:
2026-07-26 22:04:29 -04:00
parent fca311b4bd
commit 6abebba69e
5 changed files with 201 additions and 98 deletions
+14 -9
View File
@@ -435,7 +435,7 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
// Border: accent when selected, else hairline. A focus ring is a distinct text/primary
// double-line (the kit's focus convention) so focus reads even on a selected cell.
const KitColor border = selected ? roleColor(Role::Accent) : roleColor(Role::LineHairline);
const KitColor border = selected ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline);
LICE_DrawRect(bmp, rect.x, rect.y, rect.width, rect.height, toLice(border), 1.0f, 0);
if (focused) {
const LICE_pixel ring = toLice(roleColor(Role::TextPrimary));
@@ -446,7 +446,7 @@ void drawThumbnail(LICE_IBitmap* bmp, const CellRect& rect, const Envelope& env,
// selected cell (whose fill is already the accent) — there it draws in bg/base for contrast.
const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
const LICE_pixel waveCol =
toLice(selected ? roleColor(Role::BgBase) : roleColor(Role::Accent));
toLice(selected ? roleColor(Role::BgBase) : roleColor(Role::AccentPrimary));
if (env.empty()) {
const int midY = rect.y + rect.height / 2;
@@ -1023,21 +1023,26 @@ void drawRegionHeader(LICE_IBitmap* bmp, const RECT& region, const char* title,
LICE_Line(bmp, hdr.left, hdr.bottom - 1, hdr.right, hdr.bottom - 1,
toLice(roleColor(Role::LineHairline)), 1.0f, 0, false);
// Title, left (Font::Title — a region heading).
// Title, left (Font::Title — a region heading). The two regions are distinct KINDS of
// container, so the title carries a CATEGORICAL accent (DS-2 revised: secondary/tertiary
// mark kinds, never intensity) — Pool = secondary teal, Banks = tertiary purple. This is
// a category mark, NOT the "what's live" signal (that stays the primary-lime "Active:"
// readout beside it), keeping primary reserved for the live/active layer.
RECT titleRc = hdr;
titleRc.left += 8;
titleRc.right = titleRc.left + 120;
kitText(bmp, toKitBox(titleRc), title, Font::Title, Role::TextPrimary, Align::Left);
const Role titleRole = poolBtnIsPool ? Role::AccentSecondary : Role::AccentTertiary;
kitText(bmp, toKitBox(titleRc), title, Font::Title, titleRole, Align::Left);
// Active-bank readout — the UNMISTAKABLE indicator (settled B4 constraint), in the ACCENT
// role in BOTH region headers so the active/capture-target bank is legible even when it is
// not the shown tab and even when it is the pool. Accent = "where the punch lives" (DS-2).
// Active-bank readout — the UNMISTAKABLE indicator (settled B4 constraint), in the PRIMARY
// accent role in BOTH region headers so the active/capture-target bank is legible even when
// it is not the shown tab and even when it is the pool. Primary = "what's live" (DS-2).
const std::string readout = "Active: " + activeName;
RECT actRc = hdr;
actRc.left = titleRc.right + 6;
actRc.right = createBtnRect(region).left - 6;
if (actRc.right > actRc.left)
kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::Accent, Align::Left);
kitText(bmp, toKitBox(actRc), readout.c_str(), Font::Label, Role::AccentPrimary, Align::Left);
// Full-height toggle button: an arrow glyph. In split it means "maximize this region";
// when this region is already full it means "restore the split". Kit drawButton + hover.
@@ -1113,7 +1118,7 @@ void drawTabStrip(LICE_IBitmap* bmp, const RECT& region) {
// The active bank's tab gets a bright accent border (unmistakable), distinct from the
// shown tab's fill — active != shown, made visible (kit accent role).
const KitColor border = active ? roleColor(Role::Accent) : roleColor(Role::LineHairline);
const KitColor border = active ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline);
LICE_DrawRect(bmp, tr.x, tr.y, tr.width, tr.height, toLice(border), 1.0f, 0);
if (active)
LICE_DrawRect(bmp, tr.x + 1, tr.y + 1, tr.width - 2, tr.height - 2,
+3 -3
View File
@@ -208,7 +208,7 @@ void drawButton(LICE_IBitmap* bmp, const KitButtonBox& button, const char* label
const int radius = b.height >= 20 ? 5 : (b.height >= 12 ? 3 : 2);
const KitColor borderCol =
(state == InteractionState::Active || state == InteractionState::Focus)
? roleColor(Role::Accent)
? roleColor(Role::AccentPrimary)
: roleColor(Role::LineHairline);
LICE_RoundRect(bmp, static_cast<float>(b.x), static_cast<float>(b.y),
static_cast<float>(b.width - 1), static_cast<float>(b.height - 1),
@@ -238,7 +238,7 @@ void drawSlider(LICE_IBitmap* bmp, const SliderGeometry& geom, InteractionState
? InteractionState::Hover
: InteractionState::Active;
KitColor top, bottom;
gradientPair(roleColorState(Role::Accent, fillState), top, bottom);
gradientPair(roleColorState(Role::AccentPrimary, fillState), top, bottom);
fillGradient(bmp, geom.filled, top, bottom);
}
@@ -281,7 +281,7 @@ void drawWaveform(LICE_IBitmap* bmp, const KitBox& box, const Envelope& env) {
if (!bmp || box.empty()) return;
const LICE_pixel midCol = toLice(roleColor(Role::LineHairline));
const LICE_pixel waveCol = toLice(roleColor(Role::Accent));
const LICE_pixel waveCol = toLice(roleColor(Role::AccentPrimary));
if (env.empty()) {
const int midY = box.y + box.height / 2;
+63 -32
View File
@@ -10,39 +10,59 @@ namespace reasampler {
namespace {
// ===========================================================================
// THE ONE DIRECTION CONSTANTS BLOCK (DS-2: B "Neon Console" + C spectral).
// THE ONE DIRECTION CONSTANTS BLOCK (DS-2 revised: B "Neon Console" REAPER-grey
// neutrals + three-accent pastel system + C pastel spectral).
//
// This is the SINGLE POINT OF CHANGE. Every role color below is one of these
// constants; roleColor() is a pure switch over them. To re-pick the visual
// direction (§4: A Studio Rack / B Neon Console / C full spectral), edit THIS
// block — no shell, no other module, names a color. The values are chosen from
// the vibrant side of each WCAG floor (the "punch" rule): the accents are as
// saturated as they can be while text still clears 4.5:1 / 3:1 on the surfaces
// they land on (proven by test_theme.cpp).
// block — no shell, no other module, names a color. Values are locked against
// each WCAG floor (proven by test_theme.cpp): text/dim is lifted to the lightest
// grey that still clears AA 4.5:1 body on the greyest surface it draws on; each
// pastel accent is the softest tint that still clears the 3:1 indicator floor on
// bg/cell ("punch from the soft side" — DS-2 revised §2.1 grey re-read).
// ===========================================================================
// Near-black elevation stack (B: base ~18,18,22). Each step a few points lighter
// so elevation reads without a border.
constexpr KitColor kDirBgBase {18, 18, 22, 255};
constexpr KitColor kDirBgPanel {26, 26, 31, 255};
constexpr KitColor kDirBgCell {40, 40, 46, 255};
constexpr KitColor kDirHairline {60, 60, 66, 255};
// REAPER-theme mid-grey elevation stack (DS-2 revised — NOT near-black). Matches
// Daniel's REAPER theme so the dock reads as part of REAPER: base = window chrome
// grey, panel/cell one step lighter each. The elevation-ladder discipline is
// unchanged (base < panel < cell by a few %, micro-gradient + inner highlight/
// shadow carry elevation, not hard borders); only the VALUES moved up into grey.
constexpr KitColor kDirBgBase {43, 43, 43, 255}; // #2b2b2b — REAPER chrome grey
constexpr KitColor kDirBgPanel {51, 51, 51, 255}; // #333333 — one step lighter
constexpr KitColor kDirBgCell {58, 58, 58, 255}; // #3a3a3a — REAPER track bg
constexpr KitColor kDirHairline {74, 74, 74, 255}; // #4a4a4a — subtle step above cell
// Text: near-white primary + a dimmer secondary. Both must clear their floor on
// bg/panel AND bg/cell (the surfaces text lands on); the test enforces it.
constexpr KitColor kDirTextPrimary{224, 228, 234, 255};
constexpr KitColor kDirTextDim {150, 156, 166, 255};
// Text: REAPER body light-grey primary (#dcdcdc, clears ~8:1 on bg/cell) + a dimmer
// grey secondary. The greyer surfaces shrank the dim cushion (mid-grey-on-mid-grey
// is the classic AA failure): the spec-start #a0a0a0 lands ~4.35:1 on bg/cell, UNDER
// the AA 4.5 body floor — lifted to #a8a8a8 (~4.78:1 on bg/cell), the lightest grey
// that still reads dim while clearing AA 4.5 body on the greyest surface it draws
// body text on. Locked by test_theme.cpp.
constexpr KitColor kDirTextPrimary{220, 220, 220, 255}; // #dcdcdc
constexpr KitColor kDirTextDim {168, 168, 168, 255}; // #a8a8a8 (lifted from #a0a0a0)
// The single vivid accent — electric cyan leads (B). Bright enough to clear
// AA-large on near-black from the vibrant side; the hot tint is a lighter cyan
// for hover/live. warn is a reserved red/amber for byte-deleting states only.
constexpr KitColor kDirAccent {60, 200, 235, 255};
constexpr KitColor kDirAccentHot {130, 224, 245, 255};
constexpr KitColor kDirWarn {235, 120, 90, 255};
// The three-accent pastel system (DS-2 revised — replaces the single electric cyan).
// primary = pastel lime (the live/active/selected signal, the eye-magnet); 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 the
// greyer bg/cell the pastels clear the 3:1 indicator floor comfortably (primary ~7.6,
// secondary ~6.8, tertiary ~5.5) at the spec-start values, so no per-hue nudge was
// needed — the hues stay pastel lime/teal/purple. warn is a reserved red/amber 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 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
// Direction C spectral endpoints (cool-blue -> hot-magenta) for the keyboard strip.
constexpr KitColor kDirSpectralLo {70, 120, 235, 255}; // low notes: cool blue
constexpr KitColor kDirSpectralHi {235, 70, 170, 255}; // high notes: hot magenta
// Direction C pastel spectral ramp (DS-2 revised): a three-stop sweep through the
// accents — pastel lime (low) -> pastel teal (mid) -> pastel purple (high) — so the
// signature keyboard strip reads as an extension of the accent system, not a neon
// flourish. Endpoints/midpoint ARE the three accent constants (single source).
constexpr KitColor kDirSpectralLo = kDirAccentPrimary; // low notes: pastel lime
constexpr KitColor kDirSpectralMid = kDirAccentSecondary; // mid notes: pastel teal
constexpr KitColor kDirSpectralHi = kDirAccentTertiary; // high notes: pastel purple
// --- state transform helpers -------------------------------------------------
@@ -97,7 +117,9 @@ KitColor roleColor(Role role) {
case Role::LineHairline: return kDirHairline;
case Role::TextPrimary: return kDirTextPrimary;
case Role::TextDim: return kDirTextDim;
case Role::Accent: return kDirAccent;
case Role::AccentPrimary: return kDirAccentPrimary;
case Role::AccentSecondary: return kDirAccentSecondary;
case Role::AccentTertiary: return kDirAccentTertiary;
case Role::AccentHot: return kDirAccentHot;
case Role::Warn: return kDirWarn;
}
@@ -113,18 +135,20 @@ KitColor roleColorState(Role role, InteractionState state) {
// Lighten the surface toward the hot accent (~10%) — the "alive" cue.
return mix(base, roleColor(Role::AccentHot), 0.10);
case InteractionState::Active:
// The selected/active layer carries the accent itself.
return roleColor(Role::Accent);
// The selected/active layer carries the PRIMARY accent — "this is live"
// is always the primary hue (DS-2 revised: primary leads state; secondary/
// tertiary are categorical, never intensity).
return roleColor(Role::AccentPrimary);
case InteractionState::Pressed:
// The surface "pushes in": darken.
return scale(base, 0.82);
case InteractionState::Dragging:
// A live-drag element reads as active-but-lighter.
return mix(roleColor(Role::Accent), roleColor(Role::AccentHot), 0.30);
// A live-drag element reads as active-but-lighter (primary -> hot).
return mix(roleColor(Role::AccentPrimary), roleColor(Role::AccentHot), 0.30);
case InteractionState::Focus:
// Focus keeps the surface but is drawn with a text/primary ring by the
// shell; the fill nudges toward the accent so focus reads even pre-ring.
return mix(base, roleColor(Role::Accent), 0.08);
// shell; the fill nudges toward the primary accent so focus reads pre-ring.
return mix(base, roleColor(Role::AccentPrimary), 0.08);
case InteractionState::Disabled: {
// Desaturate and drop alpha to 40% (§3.3).
KitColor d = desaturate(base, 0.6);
@@ -138,7 +162,14 @@ KitColor roleColorState(Role role, InteractionState state) {
KitColor spectralColor(double t) {
if (t < 0.0) t = 0.0;
if (t > 1.0) t = 1.0;
return mix(kDirSpectralLo, kDirSpectralHi, t);
// Three-stop pastel sweep anchored on the accent trio (DS-2 revised Direction C):
// lime (low) -> teal (mid, t=0.5) -> purple (high). A single Lo->Hi lerp would skip
// the teal midpoint and drift the ramp off the accent family; interpolate each half
// so the midpoint IS the secondary accent and every stop stays in the pastel band.
if (t <= 0.5) {
return mix(kDirSpectralLo, kDirSpectralMid, t / 0.5);
}
return mix(kDirSpectralMid, kDirSpectralHi, (t - 0.5) / 0.5);
}
double relativeLuminance(const KitColor& c) {
+33 -23
View File
@@ -2,16 +2,20 @@
// theme — the REAPER-free, LICE-free palette + type-scale core of the shared drawing
// kit (Phase L, L1). This is the "one source of drawing" made testable at its root: a
// ROLE-based color model (bg/base, bg/panel, bg/cell, line/hairline, text/primary,
// text/dim, accent, 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 ("punch to the floor, not past it").
// 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 ("punch to the floor, not past it").
//
// THE SINGLE POINT OF CHANGE (DS-2): every role color is produced by roleColor() from
// ONE direction constants block (kDirection*, below) carrying the settled B (Neon
// Console) + C spectral values. Switching the visual direction is editing that block and
// nothing else — no shell hardcodes a color; the shell asks the theme by role. The
// spectral (Direction C) hue ramp lives here too (spectralColor) so the signature
// keyboard strip's L3 consumer derives its per-note hue from the same source.
// THE SINGLE POINT OF CHANGE (DS-2 revised): every role color is produced by roleColor()
// from ONE direction constants block (kDirection*, below) carrying the settled B (Neon
// Console) neutrals — now REAPER-theme mid-grey, not near-black — plus the three-accent
// pastel system (primary lime / secondary teal / tertiary purple) and the C pastel
// spectral ramp. Switching the visual direction is editing that block and nothing else —
// no shell hardcodes a color; the shell asks the theme by role. The spectral (Direction C)
// hue ramp lives here too (spectralColor) so the signature keyboard strip's L3 consumer
// derives its per-note hue from the same source (a pastel sweep anchored on the three
// accents: primary lime -> secondary teal -> tertiary purple).
//
// PURE MODULE: NO REAPER types, NO SWELL, NO LICE, NO vendor/ includes. Standard library
// only. Builds and unit-tests without REAPER. Mirror of mode_switch / bank_grid — the
@@ -40,15 +44,17 @@ struct KitColor {
// The structural palette roles (direction-independent — §2.1 of the design doc). The
// direction (B/C) sets the concrete hue behind each; the shell always asks by role.
enum class Role {
BgBase, // window canvas
BgPanel, // a raised region (list, waveform pane)
BgCell, // a control / row surface
LineHairline,// separators (used sparingly — elevation carries most separation)
TextPrimary, // labels, values
TextDim, // secondary / units
Accent, // selection / active / focus — where the punch lives
AccentHot, // hover / live / drag feedback (a brighter accent tint)
Warn, // clip / destructive (prune, delete) — reserved for byte-deleting states
BgBase, // window canvas
BgPanel, // a raised region (list, waveform pane)
BgCell, // a control / row surface
LineHairline, // separators (used sparingly — elevation carries most separation)
TextPrimary, // labels, values
TextDim, // secondary / units
AccentPrimary, // the live / active / selected signal — where the punch lives (pastel lime)
AccentSecondary,// categorical role A (pastel 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)
Warn, // clip / destructive (prune, delete) — reserved for byte-deleting states
};
// The interaction-state model every kit component honors (§3.3). A component draws its
@@ -83,11 +89,15 @@ KitColor roleColor(Role role);
// Rest returns roleColor(role) unchanged.
KitColor roleColorState(Role role, InteractionState state);
// Direction C's spectral hue ramp: maps a normalized position t in [0, 1] (low note ->
// high note across the keyboard strip) to a color, cool-blue at 0 -> hot-magenta at 1
// (§4 Direction C). The signature keyboard-strip surface (an L3 consumer) derives each
// note/zone's hue from this ONE function so the spectrum is defined in the same place as
// the rest of the palette. t is clamped to [0, 1].
// Direction C's spectral hue ramp (DS-2 revised — a PASTEL sweep anchored on the three
// accents, not the old neon cool-blue -> hot-magenta): maps a normalized position t in
// [0, 1] (low note -> high note across the keyboard strip) to a color that runs
// accent/primary (pastel lime, low) -> accent/secondary (pastel teal, mid) ->
// accent/tertiary (pastel purple, high). The same three hues that mean "live / category A
// / category B" elsewhere are the endpoints and midpoint here, so the strip reads as an
// extension of the accent system, not a separate flourish. The signature keyboard-strip
// surface (an L3 consumer) derives each note/zone's hue from this ONE function so the
// spectrum is defined in the same place as the rest of the palette. t is clamped to [0, 1].
KitColor spectralColor(double t);
// --- WCAG contrast (the "punch" rule, made testable) --------------------------