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:
+88
-31
@@ -12,6 +12,7 @@
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
|
||||
using namespace reasampler;
|
||||
@@ -39,7 +40,7 @@ static void testContrastKnownAnchors() {
|
||||
|
||||
// --- THE load-bearing test: every drawn text-on-surface pair clears its floor -
|
||||
|
||||
// The surfaces text lands on (near-black elevation stack).
|
||||
// The surfaces text lands on (DS-2 revised REAPER-grey elevation stack).
|
||||
static void testTextPrimaryClearsBodyFloorOnSurfaces() {
|
||||
const KitColor tp = roleColor(Role::TextPrimary);
|
||||
// Primary text is body text -> 4.5:1 on every surface it is drawn on.
|
||||
@@ -50,24 +51,68 @@ static void testTextPrimaryClearsBodyFloorOnSurfaces() {
|
||||
|
||||
static void testTextDimClearsItsFloorOnSurfaces() {
|
||||
const KitColor td = roleColor(Role::TextDim);
|
||||
// Dim/secondary text is used for units/counts (large-ish, low-emphasis) -> the
|
||||
// large/state floor 3:1 on the surfaces it appears on. Enforced from the vibrant side:
|
||||
// it must not be dimmed BELOW the floor.
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgBase)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgPanel)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgCell)) >= textFloor(TextClass::Large));
|
||||
// DS-2 revised (grey re-read): text/dim is used for keybinding sub-labels / units on
|
||||
// the mid-grey surfaces, and mid-grey-on-mid-grey is the classic AA failure. It is
|
||||
// LOCKED to the lightest grey that still reads dim while clearing AA 4.5:1 BODY on the
|
||||
// GREYEST surface it draws body text on (bg/cell). This is the load-bearing check: the
|
||||
// spec-start #a0a0a0 is UNDER floor on grey by design — the value was lifted until this
|
||||
// passes. Body floor on all three surfaces (bg/cell is the tight one).
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgBase)) >= textFloor(TextClass::Body));
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgPanel)) >= textFloor(TextClass::Body));
|
||||
CHECK(contrastRatio(td, roleColor(Role::BgCell)) >= textFloor(TextClass::Body));
|
||||
}
|
||||
|
||||
static void testAccentClearsStateFloorOnBackground() {
|
||||
// The accent is a UI-state indicator (selection border / active fill) -> 3:1 minimum
|
||||
// on the base background, "pushed to the floor from the vibrant side".
|
||||
CHECK(contrastRatio(roleColor(Role::Accent), roleColor(Role::BgBase))
|
||||
>= textFloor(TextClass::Large));
|
||||
// The hot (hover) accent is brighter still, so it also clears.
|
||||
CHECK(contrastRatio(roleColor(Role::AccentHot), roleColor(Role::BgBase))
|
||||
static void testAccentsClearIndicatorFloorOnCell() {
|
||||
// DS-2 revised (grey re-read): each of the three pastel accents is used as a state
|
||||
// indicator / active fill on bg/cell (selection border, active segment, active tab).
|
||||
// The greyer surface shrank the cushion from ~15:1 (near-black) to ~6-7:1 (grey), so
|
||||
// this is re-verified on bg/cell (NOT the old near-black) at the 3:1 large/indicator
|
||||
// floor. If any pastel dropped below, the fix is to nudge that hue deeper (hue stays
|
||||
// lime/teal/purple) — the value the test locks proves it did not need it here.
|
||||
for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary,
|
||||
Role::AccentHot}) {
|
||||
CHECK(contrastRatio(roleColor(a), roleColor(Role::BgCell))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
// The primary accent also leads the mode-switch/tab active fill drawn over bg/base.
|
||||
CHECK(contrastRatio(roleColor(Role::AccentPrimary), roleColor(Role::BgBase))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// Text drawn ON a pastel accent fill (retained tight pair, re-verified for the pastels):
|
||||
// active buttons/segments/tabs fill with the primary accent and draw their label in
|
||||
// bg/base (near-black-ish grey). That near-black-on-pastel pair must clear AA 4.5:1 body,
|
||||
// or active controls would be unreadable. Verified for EACH pastel that can be a text fill.
|
||||
static void testTextOnPastelFillClearsBodyFloor() {
|
||||
const KitColor label = roleColor(Role::BgBase); // what drawButton/drawListRow use
|
||||
for (Role a : {Role::AccentPrimary, Role::AccentSecondary, Role::AccentTertiary}) {
|
||||
CHECK(contrastRatio(label, roleColor(a)) >= textFloor(TextClass::Body));
|
||||
}
|
||||
}
|
||||
|
||||
// Secondary and tertiary are the CATEGORICAL pair (region titles, spectral bands). They
|
||||
// must read as DISTINCT categories, not two greys. Two pastels can be near-equal in
|
||||
// LUMINANCE yet clearly distinct in HUE (teal vs purple differ mainly by channel balance,
|
||||
// not brightness) — so distinguishability is a hue-separation check, not a luminance-
|
||||
// contrast one. Assert a meaningful per-channel difference: teal is green/blue-leaning,
|
||||
// purple is red/blue-leaning, so their channel BALANCE diverges substantially.
|
||||
static void testSecondaryTertiaryAreDistinguishable() {
|
||||
const KitColor sec = roleColor(Role::AccentSecondary); // teal: G,B high, R low
|
||||
const KitColor ter = roleColor(Role::AccentTertiary); // purple: R,B high, G lower
|
||||
CHECK(!(sec == ter));
|
||||
CHECK(!(roleColor(Role::AccentPrimary) == sec));
|
||||
// Hue divergence: teal's green dominates its red; purple's red dominates its green.
|
||||
// A muddying that collapsed them toward a common grey would break this ordering.
|
||||
CHECK(sec.g > sec.r); // teal leans green over red
|
||||
CHECK(ter.r > ter.g); // purple leans red over green
|
||||
// And the total channel-balance gap is large (sum of absolute channel deltas), so the
|
||||
// two are far apart in color space even though close in luminance.
|
||||
const int delta = std::abs(int(sec.r) - int(ter.r)) +
|
||||
std::abs(int(sec.g) - int(ter.g)) +
|
||||
std::abs(int(sec.b) - int(ter.b));
|
||||
CHECK(delta >= 60);
|
||||
}
|
||||
|
||||
static void testWarnClearsStateFloorOnBackground() {
|
||||
// warn (destructive) must be unmistakable -> clears the state floor on the base.
|
||||
CHECK(contrastRatio(roleColor(Role::Warn), roleColor(Role::BgBase))
|
||||
@@ -93,7 +138,7 @@ static void testRolesAreDistinctAndElevationMonotonic() {
|
||||
CHECK(!(roleColor(Role::BgBase) == roleColor(Role::BgPanel)));
|
||||
CHECK(!(roleColor(Role::BgPanel) == roleColor(Role::BgCell)));
|
||||
CHECK(!(roleColor(Role::TextPrimary) == roleColor(Role::TextDim)));
|
||||
CHECK(!(roleColor(Role::Accent) == roleColor(Role::Warn)));
|
||||
CHECK(!(roleColor(Role::AccentPrimary) == roleColor(Role::Warn)));
|
||||
// Elevation reads as increasing luminance base < panel < cell.
|
||||
CHECK(relativeLuminance(roleColor(Role::BgBase)) <
|
||||
relativeLuminance(roleColor(Role::BgPanel)));
|
||||
@@ -107,7 +152,7 @@ static void testRolesAreDistinctAndElevationMonotonic() {
|
||||
// --- Interaction-state transform ----------------------------------------------
|
||||
|
||||
static void testStateRestIsIdentity() {
|
||||
for (Role r : {Role::BgBase, Role::BgPanel, Role::BgCell, Role::Accent}) {
|
||||
for (Role r : {Role::BgBase, Role::BgPanel, Role::BgCell, Role::AccentPrimary}) {
|
||||
CHECK(roleColorState(r, InteractionState::Rest) == roleColor(r));
|
||||
}
|
||||
}
|
||||
@@ -121,13 +166,15 @@ static void testHoverLightensPressedDarkens() {
|
||||
CHECK(relativeLuminance(pressed) < relativeLuminance(rest));
|
||||
}
|
||||
|
||||
static void testActiveIsAccent() {
|
||||
CHECK(roleColorState(Role::BgCell, InteractionState::Active) == roleColor(Role::Accent));
|
||||
static void testActiveIsPrimaryAccent() {
|
||||
// DS-2 revised: the Active state is ALWAYS the primary accent ("what's live" leads).
|
||||
CHECK(roleColorState(Role::BgCell, InteractionState::Active)
|
||||
== roleColor(Role::AccentPrimary));
|
||||
}
|
||||
|
||||
static void testDisabledDropsAlphaAndDesaturates() {
|
||||
const KitColor rest = roleColor(Role::Accent);
|
||||
const KitColor dis = roleColorState(Role::Accent, InteractionState::Disabled);
|
||||
const KitColor rest = roleColor(Role::AccentPrimary);
|
||||
const KitColor dis = roleColorState(Role::AccentPrimary, InteractionState::Disabled);
|
||||
// Alpha drops to ~40%.
|
||||
CHECK(dis.a < rest.a);
|
||||
CHECK(dis.a >= 90 && dis.a <= 110); // 255 * 0.4 ~= 102
|
||||
@@ -135,16 +182,24 @@ static void testDisabledDropsAlphaAndDesaturates() {
|
||||
|
||||
// --- Direction C spectral ramp ------------------------------------------------
|
||||
|
||||
static void testSpectralInterpolatesEndpoints() {
|
||||
static void testSpectralIsPastelRampAnchoredOnAccents() {
|
||||
// DS-2 revised Direction C: the spectral ramp is a PASTEL sweep anchored on the three
|
||||
// accents — lime (low) -> teal (mid) -> purple (high) — NOT the old neon cool->hot.
|
||||
const KitColor lo = spectralColor(0.0);
|
||||
const KitColor hi = spectralColor(1.0);
|
||||
// Low is cool (blue-dominant), high is hot (red-dominant) — the identity of the ramp.
|
||||
CHECK(lo.b > lo.r);
|
||||
CHECK(hi.r > hi.b);
|
||||
// Midpoint sits between the endpoints on each channel.
|
||||
const KitColor mid = spectralColor(0.5);
|
||||
CHECK(mid.r > lo.r && mid.r < hi.r);
|
||||
// Clamps out of range.
|
||||
const KitColor hi = spectralColor(1.0);
|
||||
// The three stops ARE the three accent constants (single source — the strip belongs to
|
||||
// the accent system). This is the load-bearing identity of the pastel ramp.
|
||||
CHECK(lo == roleColor(Role::AccentPrimary)); // low = pastel lime
|
||||
CHECK(mid == roleColor(Role::AccentSecondary)); // mid = pastel teal
|
||||
CHECK(hi == roleColor(Role::AccentTertiary)); // high = pastel purple
|
||||
// Low is lime (green-dominant); high is purple (red+blue over green) — distinct hues.
|
||||
CHECK(lo.g > lo.r && lo.g > lo.b);
|
||||
CHECK(hi.b > hi.g && hi.r > hi.g);
|
||||
// A quarter-point interpolates within the lime->teal half (not off the family).
|
||||
const KitColor q = spectralColor(0.25);
|
||||
CHECK(q.b > lo.b && q.b < mid.b); // blue rises lime -> teal
|
||||
// Clamps out of range to the endpoints.
|
||||
CHECK(spectralColor(-1.0) == lo);
|
||||
CHECK(spectralColor(2.0) == hi);
|
||||
}
|
||||
@@ -153,15 +208,17 @@ int main() {
|
||||
testContrastKnownAnchors();
|
||||
testTextPrimaryClearsBodyFloorOnSurfaces();
|
||||
testTextDimClearsItsFloorOnSurfaces();
|
||||
testAccentClearsStateFloorOnBackground();
|
||||
testAccentsClearIndicatorFloorOnCell();
|
||||
testTextOnPastelFillClearsBodyFloor();
|
||||
testSecondaryTertiaryAreDistinguishable();
|
||||
testWarnClearsStateFloorOnBackground();
|
||||
testLabelOnActiveSurfaceClearsFloor();
|
||||
testRolesAreDistinctAndElevationMonotonic();
|
||||
testStateRestIsIdentity();
|
||||
testHoverLightensPressedDarkens();
|
||||
testActiveIsAccent();
|
||||
testActiveIsPrimaryAccent();
|
||||
testDisabledDropsAlphaAndDesaturates();
|
||||
testSpectralInterpolatesEndpoints();
|
||||
testSpectralIsPastelRampAnchoredOnAccents();
|
||||
|
||||
if (g_fail == 0) std::printf("theme: all tests passed\n");
|
||||
else std::printf("theme: %d CHECK(s) FAILED\n", g_fail);
|
||||
|
||||
Reference in New Issue
Block a user