#pragma once // 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/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 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 // shell (draw_kit) turns a KitColor into a LICE_pixel at the boundary; the theme never // names a LICE type. #include namespace reasampler { // A straight 8-bit-per-channel RGBA color, LICE-free. The draw shell converts this to a // LICE_pixel via LICE_RGBA at the boundary (draw_kit); nothing here depends on LICE's // packing. Deliberately NOT named "Color"/"RGBA" (both are common collision surfaces); // "KitColor" scopes it to the kit. struct KitColor { std::uint8_t r = 0; std::uint8_t g = 0; std::uint8_t b = 0; std::uint8_t a = 255; bool operator==(const KitColor& o) const { return r == o.r && g == o.g && b == o.b && a == o.a; } }; // 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 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 // role surface transformed by its current state; stateShift() below is that transform. enum class InteractionState { Rest, Hover, Active, // selected / active Pressed, Dragging, Focus, Disabled, }; // Text size classes for the WCAG floor. "Large" text (>= ~18.66px, or >= ~14px bold) and // UI-state indicators clear at 3:1; body text clears at 4.5:1 (WCAG 2.1 AA). The kit's // four cached fonts map onto these: title -> Large, label/value -> Body, micro -> Body. enum class TextClass { Body, // AA 4.5:1 Large, // AA-large 3:1 (also the floor for state indicators) }; // The concrete color for a role, produced from the ONE direction constants block. This is // the single choke point the "single point of change" guarantee rests on: the shell has // no other way to obtain a palette color, so re-picking the direction is editing the // kDirection* block this reads and nothing else. KitColor roleColor(Role role); // The color for a role under an interaction state — roleColor(role) transformed by the // state (hover lightens toward accent/hot, pressed darkens, disabled desaturates + drops // alpha, etc.). Surfaces use this so every component gets the whole state model for free. // Rest returns roleColor(role) unchanged. KitColor roleColorState(Role role, InteractionState state); // 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) -------------------------- // // The relative luminance of a color per WCAG 2.1 (sRGB linearization + the 0.2126/ // 0.7152/0.0722 weighting). Alpha is ignored — contrast is a question about the opaque // hues; a translucent overlay's effective color is the caller's to compose first. double relativeLuminance(const KitColor& c); // The WCAG contrast ratio between two colors, in [1, 21]. Symmetric; order-independent. double contrastRatio(const KitColor& a, const KitColor& b); // The contrast floor a text class must clear: 4.5 for Body, 3.0 for Large. The test that // proves the palette asserts contrastRatio(text, surface) >= textFloor(class) for every // pair the kit actually draws. double textFloor(TextClass cls); } // namespace reasampler