palette: give the spectral mid and the envelope trace their own roles
Decouples the keyboard strip's mid stop from accent/secondary, which the deep-teal darkening had inverted. Adds overlay/trace (#816AA6), the first value to clear 3:1 against the waveform. Replaces the frozen-premise test.
This commit is contained in:
+66
-37
@@ -121,40 +121,55 @@ static void testSecondaryTertiaryAreDistinguishable() {
|
||||
CHECK(delta >= 60);
|
||||
}
|
||||
|
||||
// The instrument's envelope overlay is traced OVER the waveform, which draws in the primary
|
||||
// accent — an accent-on-accent pair no floor covers, since neither is a surface. It moved from
|
||||
// the secondary to the tertiary for exactly this reason, so the pair is pinned two ways: the
|
||||
// tertiary must separate from the primary MORE than the secondary did (the measurable half of
|
||||
// the move), and the separation is a hue one, since two pastels sit close in luminance by
|
||||
// construction. Whether the result reads clearly is a perceptual call, not this test's.
|
||||
static void testOverlayAccentSeparatesFromTheWaveformAccent() {
|
||||
const KitColor wave = roleColor(Role::AccentPrimary);
|
||||
const KitColor overlay = roleColor(Role::AccentTertiary);
|
||||
const KitColor prior = roleColor(Role::AccentSecondary);
|
||||
CHECK(contrastRatio(overlay, wave) > contrastRatio(prior, wave));
|
||||
// Pin the MEASURED value, not just the relational improvement: today's pair sits at
|
||||
// ~1.37:1 — BELOW the 3:1 indicator floor every other assertion in this file uses. That
|
||||
// is a known, held deviation (Daniel's palette call, tracked separately) rather than a
|
||||
// silently accepted one; a regression toward the old ~1.12:1 secondary pairing must fail.
|
||||
CHECK(contrastRatio(overlay, wave) >= 1.35);
|
||||
// Hue divergence against the waveform: the waveform's green dominates its red, the
|
||||
// overlay's red dominates its green — opposite balances, not two shades of one.
|
||||
CHECK(wave.g > wave.r);
|
||||
CHECK(overlay.r > overlay.g);
|
||||
const int delta = std::abs(int(wave.r) - int(overlay.r)) +
|
||||
std::abs(int(wave.g) - int(overlay.g)) +
|
||||
std::abs(int(wave.b) - int(overlay.b));
|
||||
CHECK(delta >= 60);
|
||||
// The envelope overlay is traced OVER the waveform, which draws in the primary accent — an
|
||||
// accent-on-accent pair no surface floor covers, since neither is a surface. overlay/trace
|
||||
// exists so that pair can clear the real 3:1 indicator floor; every categorical accent is light
|
||||
// enough to sit under 2:1 against the lime, so pointing the trace back at one fails here.
|
||||
static void testOverlayTraceClearsIndicatorFloorOnTheWaveform() {
|
||||
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::AccentPrimary))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// The trace's own surface floor. bg/base is the only surface it draws on (the waveform band
|
||||
// fills with it), and that confinement is forced, not convenient: clearing 3:1 against the lime
|
||||
// caps the trace's luminance at 0.183, while clearing 3:1 on bg/cell would demand 0.227. The two
|
||||
// are mutually exclusive, so this role must not be reused outside that band.
|
||||
static void testOverlayTraceClearsFloorOnItsDrawSurface() {
|
||||
CHECK(contrastRatio(roleColor(Role::OverlayTrace), roleColor(Role::BgBase))
|
||||
>= textFloor(TextClass::Large));
|
||||
}
|
||||
|
||||
// editor_paint_waveform.cpp draws the loop span/markers and the envelope overlay trace into
|
||||
// the SAME overlay rect. Loop markers are secondary (this track's reassignment, off tertiary,
|
||||
// which the envelope overlay now owns exclusively there); a future palette edit that collapses
|
||||
// the two back to one role must fail here rather than silently recreating the finding.
|
||||
static void testLoopMarkerAndEnvelopeOverlayRolesAreDistinct() {
|
||||
// the SAME overlay rect, so they must read as two things. Luminance cannot carry that — each is
|
||||
// pinned near its own floor — so the separation is a hue one: teal leans green over red, the
|
||||
// trace violet leans red over green. A palette edit collapsing them onto one role fails here.
|
||||
static void testLoopMarkerAndOverlayTraceAreDistinct() {
|
||||
const KitColor loopMarker = roleColor(Role::AccentSecondary);
|
||||
const KitColor envelopeOverlay = roleColor(Role::AccentTertiary);
|
||||
CHECK(!(loopMarker == envelopeOverlay));
|
||||
const KitColor trace = roleColor(Role::OverlayTrace);
|
||||
CHECK(!(loopMarker == trace));
|
||||
CHECK(loopMarker.g > loopMarker.r);
|
||||
CHECK(trace.r > trace.g);
|
||||
const int delta = std::abs(int(loopMarker.r) - int(trace.r)) +
|
||||
std::abs(int(loopMarker.g) - int(trace.g)) +
|
||||
std::abs(int(loopMarker.b) - int(trace.b));
|
||||
CHECK(delta >= 60);
|
||||
}
|
||||
|
||||
// AccentSecondary was darkened from a pastel teal (#84D6D0) to a deep teal (#38A8A0, same
|
||||
// hue/saturation, lower lightness). Locks the re-measured numbers so a future palette nudge
|
||||
// that erodes past either the 3:1 indicator floor or the tighter AA 4.5:1 text-on-fill floor
|
||||
// fails the build. The text-on-fill pair (bg/base label on a secondary fill) is the binding
|
||||
// limiter — it clears with far less headroom (~4.9:1) than the indicator floor on bg/cell
|
||||
// (~3.9:1), which is why the value could not go darker than this.
|
||||
static void testAccentSecondaryDarkerTealClearsFloors() {
|
||||
const KitColor sec = roleColor(Role::AccentSecondary);
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgBase)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgPanel)) >= textFloor(TextClass::Large));
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= textFloor(TextClass::Large));
|
||||
// Tighter pins at the measured values: a regression that darkens secondary further would
|
||||
// clear these before it clears the nominal 3:1/4.5:1 floors above, catching the drift early.
|
||||
CHECK(contrastRatio(sec, roleColor(Role::BgCell)) >= 3.9);
|
||||
CHECK(contrastRatio(roleColor(Role::BgBase), sec) >= 4.9);
|
||||
}
|
||||
|
||||
static void testWarnClearsStateFloorOnBackground() {
|
||||
@@ -244,15 +259,15 @@ static void testDisabledDropsAlphaAndDesaturates() {
|
||||
// --- Direction C spectral ramp ------------------------------------------------
|
||||
|
||||
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.
|
||||
// DS-2 revised Direction C: the spectral ramp is a PASTEL sweep — lime (low) -> teal (mid)
|
||||
// -> purple (high) — NOT the old neon cool->hot.
|
||||
const KitColor lo = spectralColor(0.0);
|
||||
const KitColor mid = spectralColor(0.5);
|
||||
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.
|
||||
// The endpoints still ARE the primary/tertiary accent constants. The mid stop is NOT the
|
||||
// secondary accent and must not be re-aliased to it — see the monotonicity test below.
|
||||
CHECK(lo == roleColor(Role::AccentPrimary)); // low = pastel lime
|
||||
CHECK(mid == roleColor(Role::AccentSecondary)); // mid = pastel teal
|
||||
CHECK(!(mid == roleColor(Role::AccentSecondary)));
|
||||
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);
|
||||
@@ -265,6 +280,17 @@ static void testSpectralIsPastelRampAnchoredOnAccents() {
|
||||
CHECK(spectralColor(2.0) == hi);
|
||||
}
|
||||
|
||||
// The strip's stops must fall in ONE direction, or position along it stops meaning anything.
|
||||
// Nothing caught this when the mid stop aliased a categorical accent that was later darkened
|
||||
// below the high stop, inverting lo->mid->hi; that is the break this guards.
|
||||
static void testSpectralRampLuminanceIsMonotonic() {
|
||||
const double lo = relativeLuminance(spectralColor(0.0));
|
||||
const double mid = relativeLuminance(spectralColor(0.5));
|
||||
const double hi = relativeLuminance(spectralColor(1.0));
|
||||
CHECK(lo > mid);
|
||||
CHECK(mid > hi);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testContrastKnownAnchors();
|
||||
testTextPrimaryClearsBodyFloorOnSurfaces();
|
||||
@@ -273,8 +299,10 @@ int main() {
|
||||
testTextOnPastelFillClearsBodyFloor();
|
||||
testTextOnHoverSurfaceClearsFloor();
|
||||
testSecondaryTertiaryAreDistinguishable();
|
||||
testOverlayAccentSeparatesFromTheWaveformAccent();
|
||||
testLoopMarkerAndEnvelopeOverlayRolesAreDistinct();
|
||||
testOverlayTraceClearsIndicatorFloorOnTheWaveform();
|
||||
testOverlayTraceClearsFloorOnItsDrawSurface();
|
||||
testLoopMarkerAndOverlayTraceAreDistinct();
|
||||
testAccentSecondaryDarkerTealClearsFloors();
|
||||
testWarnClearsStateFloorOnBackground();
|
||||
testLabelOnActiveSurfaceClearsFloor();
|
||||
testRolesAreDistinctAndElevationMonotonic();
|
||||
@@ -283,6 +311,7 @@ int main() {
|
||||
testActiveIsPrimaryAccent();
|
||||
testDisabledDropsAlphaAndDesaturates();
|
||||
testSpectralIsPastelRampAnchoredOnAccents();
|
||||
testSpectralRampLuminanceIsMonotonic();
|
||||
|
||||
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