163 lines
6.5 KiB
C++
163 lines
6.5 KiB
C++
// theme — pure implementation. See theme.h. NO REAPER / SWELL / LICE / vendor.
|
|
|
|
#include "theme.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
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<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,
|
|
};
|
|
}
|
|
|
|
// Scale RGB by factor (brightness up/down), alpha untouched.
|
|
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) {
|
|
// 8-bit gray from the perceptual weights (same weighting family as luminance).
|
|
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::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::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;
|
|
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
|