// theme — pure implementation. See theme.h. NO REAPER / SWELL / LICE / vendor. #include "theme.h" #include #include namespace reasampler { namespace { // =========================================================================== // THE ONE DIRECTION CONSTANTS BLOCK (DS-2: B "Neon Console" + C 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). // =========================================================================== // 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}; // 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}; // 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}; // 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 // --- state transform helpers ------------------------------------------------- std::uint8_t clamp8(int v) { return static_cast(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(std::lround(a.r + (b.r - a.r) * t))), clamp8(static_cast(std::lround(a.g + (b.g - a.g) * t))), clamp8(static_cast(std::lround(a.b + (b.b - a.b) * t))), a.a, }; } // Scale RGB by factor (brightness up/down), alpha untouched. KitColor scale(const KitColor& c, double factor) { return KitColor{ clamp8(static_cast(std::lround(c.r * factor))), clamp8(static_cast(std::lround(c.g * factor))), clamp8(static_cast(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) { // 8-bit gray from the perceptual weights (same weighting family as luminance). const int gray = clamp8(static_cast( std::lround(0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b))); const KitColor g{static_cast(gray), static_cast(gray), static_cast(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::Accent: return kDirAccent; case Role::AccentHot: return kDirAccentHot; 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 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); 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); 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); case InteractionState::Disabled: { // Desaturate and drop alpha to 40% (§3.3). KitColor d = desaturate(base, 0.6); d.a = static_cast(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; return mix(kDirSpectralLo, kDirSpectralHi, t); } 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