a19d645a49
Corrects theme.h's large-text thresholds, names accent/secondary's real binding limiter, and adds compositeOver so the loop-span fill's 2.25:1 under-floor pair is asserted rather than assumed.
186 lines
8.3 KiB
C++
186 lines
8.3 KiB
C++
// theme — pure implementation. See theme.h.
|
|
|
|
#include "core/ui/theme.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace reasampler::ui {
|
|
|
|
namespace {
|
|
|
|
// ===========================================================================
|
|
// THE ONE DIRECTION CONSTANTS BLOCK. Every role color below is one of these constants;
|
|
// roleColor() is a pure switch over them — this is the single point of change for the
|
|
// visual direction. 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").
|
|
// ===========================================================================
|
|
|
|
// REAPER-theme mid-grey elevation stack, matching Daniel's REAPER theme so the dock reads as
|
|
// part of REAPER: base = window chrome grey, panel/cell one step lighter each. Elevation-ladder
|
|
// discipline: base < panel < cell by a few %, micro-gradient + inner highlight/shadow carry
|
|
// elevation, not hard borders.
|
|
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: REAPER body light-grey primary (#dcdcdc, clears ~8:1 on bg/cell) + a dimmer grey
|
|
// secondary. 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), the lightest
|
|
// grey that still reads dim while clearing the floor. Locked by test_theme.cpp.
|
|
constexpr KitColor kDirTextPrimary{220, 220, 220, 255}; // #dcdcdc
|
|
constexpr KitColor kDirTextDim {168, 168, 168, 255}; // #a8a8a8 (lifted from #a0a0a0)
|
|
|
|
// Accent system — roles and their categorical meaning are core/ui/CLAUDE.md's. Warning only:
|
|
// secondary CANNOT go darker. Its tightest pair is the velocity-curve trace on a HOVER-lightened
|
|
// bg/cell (3.03:1 against a 3:1 floor — the whole tree's thinnest margin), which no rest-surface
|
|
// check sees.
|
|
constexpr KitColor kDirAccentPrimary {176, 224, 152, 255}; // #B0E098 — pastel lime
|
|
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
|
|
|
|
// The envelope trace over the waveform. Warning only (the two-neighbour rule is
|
|
// core/ui/CLAUDE.md's): this value has NO room in either direction, and it is confined to the
|
|
// waveform band — inside the 20%-alpha loop-span fill it already sits under floor at 2.25:1,
|
|
// which no single value can fix.
|
|
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 -------------------------------------------------
|
|
|
|
std::uint8_t clamp8(int v) {
|
|
return static_cast<std::uint8_t>(v < 0 ? 0 : (v > 255 ? 255 : v));
|
|
}
|
|
|
|
// Linear blend from a toward b by t in [0, 1] (alpha carried from a — a state tint changes
|
|
// hue/brightness, not opacity; disabled handles alpha separately).
|
|
KitColor mix(const KitColor& a, const KitColor& b, double t) {
|
|
return KitColor{
|
|
clamp8(static_cast<int>(std::lround(a.r + (b.r - a.r) * t))),
|
|
clamp8(static_cast<int>(std::lround(a.g + (b.g - a.g) * t))),
|
|
clamp8(static_cast<int>(std::lround(a.b + (b.b - a.b) * t))),
|
|
a.a,
|
|
};
|
|
}
|
|
|
|
KitColor scale(const KitColor& c, double factor) {
|
|
return KitColor{
|
|
clamp8(static_cast<int>(std::lround(c.r * factor))),
|
|
clamp8(static_cast<int>(std::lround(c.g * factor))),
|
|
clamp8(static_cast<int>(std::lround(c.b * factor))),
|
|
c.a,
|
|
};
|
|
}
|
|
|
|
// Desaturate toward the color's own luminance-gray by amount in [0, 1].
|
|
KitColor desaturate(const KitColor& c, double amount) {
|
|
const int gray = clamp8(static_cast<int>(
|
|
std::lround(0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b)));
|
|
const KitColor g{static_cast<std::uint8_t>(gray),
|
|
static_cast<std::uint8_t>(gray),
|
|
static_cast<std::uint8_t>(gray), c.a};
|
|
return mix(c, g, amount);
|
|
}
|
|
|
|
double linearizeChannel(std::uint8_t v) {
|
|
const double s = v / 255.0;
|
|
return s <= 0.03928 ? s / 12.92 : std::pow((s + 0.055) / 1.055, 2.4);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
KitColor roleColor(Role role) {
|
|
switch (role) {
|
|
case Role::BgBase: return kDirBgBase;
|
|
case Role::BgPanel: return kDirBgPanel;
|
|
case Role::BgCell: return kDirBgCell;
|
|
case Role::LineHairline: return kDirHairline;
|
|
case Role::TextPrimary: return kDirTextPrimary;
|
|
case Role::TextDim: return kDirTextDim;
|
|
case Role::AccentPrimary: return kDirAccentPrimary;
|
|
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
|
|
}
|
|
|
|
KitColor roleColorState(Role role, InteractionState state) {
|
|
const KitColor base = roleColor(role);
|
|
switch (state) {
|
|
case InteractionState::Rest:
|
|
return base;
|
|
case InteractionState::Hover:
|
|
// Lighten toward the hot accent (~10%) — the "alive" cue.
|
|
return mix(base, roleColor(Role::AccentHot), 0.10);
|
|
case InteractionState::Active:
|
|
// "This is live" is always the primary hue — secondary/tertiary stay categorical.
|
|
return roleColor(Role::AccentPrimary);
|
|
case InteractionState::Pressed:
|
|
return scale(base, 0.82); // the surface "pushes in"
|
|
case InteractionState::Dragging:
|
|
return mix(roleColor(Role::AccentPrimary), roleColor(Role::AccentHot), 0.30);
|
|
case InteractionState::Focus:
|
|
// Focus keeps the surface; the shell draws a text/primary ring on top, and the
|
|
// fill nudges toward the primary accent so focus reads pre-ring.
|
|
return mix(base, roleColor(Role::AccentPrimary), 0.08);
|
|
case InteractionState::Disabled: {
|
|
KitColor d = desaturate(base, 0.6);
|
|
d.a = static_cast<std::uint8_t>(std::lround(base.a * 0.4));
|
|
return d;
|
|
}
|
|
}
|
|
return base;
|
|
}
|
|
|
|
KitColor spectralColor(double t) {
|
|
if (t < 0.0) t = 0.0;
|
|
if (t > 1.0) t = 1.0;
|
|
// 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);
|
|
}
|
|
return mix(kDirSpectralMid, kDirSpectralHi, (t - 0.5) / 0.5);
|
|
}
|
|
|
|
KitColor compositeOver(const KitColor& over, const KitColor& under, double alpha) {
|
|
if (alpha < 0.0) alpha = 0.0;
|
|
if (alpha > 1.0) alpha = 1.0;
|
|
return mix(under, over, alpha);
|
|
}
|
|
|
|
double relativeLuminance(const KitColor& c) {
|
|
return 0.2126 * linearizeChannel(c.r) +
|
|
0.7152 * linearizeChannel(c.g) +
|
|
0.0722 * linearizeChannel(c.b);
|
|
}
|
|
|
|
double contrastRatio(const KitColor& a, const KitColor& b) {
|
|
const double la = relativeLuminance(a);
|
|
const double lb = relativeLuminance(b);
|
|
const double lighter = std::max(la, lb);
|
|
const double darker = std::min(la, lb);
|
|
return (lighter + 0.05) / (darker + 0.05);
|
|
}
|
|
|
|
double textFloor(TextClass cls) {
|
|
return cls == TextClass::Body ? 4.5 : 3.0;
|
|
}
|
|
|
|
} // namespace reasampler::ui
|