validateClaymorphism function
The family guardrails (called from validateTheme for
ThemeFamily.claymorphism specs only): coloured, darker drop shadows
(GuardrailRule.clayShadowTint), keys standing off the body
(GuardrailRule.clayKeyBody), a visible highlight on every face
(GuardrailRule.clayHighlight), function labels legible where they
cross the shade ring (GuardrailRule.clayLabelRing), the focus ring
where it crosses a key's own drop shadow (GuardrailRule.focusRing,
"over key shadow"), an opaque, distinct display panel
(GuardrailRule.clayPanel) and the renderer contract
(GuardrailRule.clayStyle).
Implementation
List<GuardrailViolation> validateClaymorphism(
ThemeSpec spec,
GuardrailLimits lim,
List<int> bgs,
) {
final out = <GuardrailViolation>[];
final p = spec.palette;
final style = spec.keyStyle;
final bg = spec.background;
// Renderer contract first: the other rules read the scene colours.
void styleFail(String field, double measured, double required) => out.add(
GuardrailViolation(GuardrailRule.clayStyle, measured, required, field),
);
if (style.outlineWidthCdp != 0) {
styleFail('outlineWidthCdp', style.outlineWidthCdp.toDouble(), 0);
}
if (style.shadowOffsetCdp != spec.motion.depthCdp ||
!clayShadowCdp.contains(style.shadowOffsetCdp)) {
styleFail(
'shadowOffsetCdp = motion.depthCdp',
style.shadowOffsetCdp.toDouble(),
clayShadowCdp.lo.toDouble(),
);
}
if (!clayShadowBlurCdp.contains(style.shadowBlurCdp)) {
styleFail(
'shadowBlurCdp',
style.shadowBlurCdp.toDouble(),
clayShadowBlurCdp.lo.toDouble(),
);
}
if (!clayInnerShadePermille.contains(style.edgeShadePermille)) {
styleFail(
'edgeShadePermille',
style.edgeShadePermille.toDouble(),
clayInnerShadePermille.lo.toDouble(),
);
}
final hiAlpha = (p.highlight >>> 24) & 0xFF;
if ((p.highlight & 0x00FFFFFF) != 0x00FFFFFF ||
!clayHighlightAlpha.contains(hiAlpha)) {
styleFail(
'highlight (white, alpha 0xA0–0xC8)',
hiAlpha.toDouble(),
clayHighlightAlpha.lo.toDouble(),
);
}
final sceneOk =
bg.kind == BackgroundKind.claySoft &&
bg.colors.length == 9 &&
bg.colors.every((c) => (c >>> 24) & 0xFF == 0xFF);
if (!sceneOk) {
styleFail('background (claySoft, 9 opaque colours)', 0, 9);
return out;
}
final base = bg.colors[0];
final baseLch = argbToOklch(base);
final baseL = baseLch.l;
final baseY = luminanceArgb(base);
final bodyHue = _hueTenths(baseLch);
final dark = spec.isDark;
// Shadows: own hue, coloured, darker than the body.
for (final (name, g) in _groups) {
final k = _keyOf(p, g);
final alpha = (k.edge >>> 24) & 0xFF;
void tint(double measured, double required, String what) => out.add(
GuardrailViolation(
GuardrailRule.clayShadowTint,
measured,
required,
'$name $what',
),
);
if (alpha < clayShadowAlphaMin || alpha > clayShadowAlphaMax) {
tint(alpha.toDouble(), clayShadowAlphaMin.toDouble(), 'alpha');
continue;
}
final e = argbToOklch(k.edge | 0xFF000000);
final eHue = _hueTenths(e);
final int ref;
final double minC;
if (dark) {
ref = clayNightShadowHue(k.face, bodyHue, clayNightShadowL(baseL));
minC = 0.035;
} else {
final f = argbToOklch(k.face | 0xFF000000);
final raw = f.c >= 0.035 ? _hueTenths(f) : bodyHue;
ref = BileRule.v2.apply(raw, e.l, 0.1);
minC = _min(0.040, 0.75 * maxChroma(e.l, e.h));
}
if (e.c < minC) tint(e.c, minC, 'chroma');
final dh = _hueDistance(eHue, ref);
if (dh > 400) tint(dh / 10, 40, 'hue');
final darkened = luminanceArgb(compositeOver(k.edge, base));
if (darkened > 0.85 * baseY) tint(darkened, 0.85 * baseY, 'darkening');
}
// Keys stand off the body.
void body(String name, double measured, double required) => out.add(
GuardrailViolation(GuardrailRule.clayKeyBody, measured, required, name),
);
if (dark) {
final cr = contrastRatio(p.digit.face, base);
if (cr < 3) body('digit contrast', cr, 3);
for (final (name, g) in _groups) {
final d = _lightness(_keyOf(p, g).face) - baseL;
if (d < clayNightMinKeyAboveBodyMilli / 1000) {
body('$name lightness', d, clayNightMinKeyAboveBodyMilli / 1000);
}
}
} else {
final c = bg.colors;
for (final x in [c[0], c[3], c[6]]) {
final d = deltaEOKArgb(p.digit.face, x);
if (d < 0.045) body('digit', d, 0.045);
}
final wash = deltaEOKArgb(p.digit.face, c[1]);
if (wash < 0.025) body('digit vs wash', wash, 0.025);
final fnDe = deltaEOKArgb(p.function.face, base);
if (fnDe < 0.045) body('function', fnDe, 0.045);
final fnDl = baseL - _lightness(p.function.face);
if (fnDl < 0.03) body('function lightness', fnDl, 0.03);
final fnCvd = minCvdDeltaE(p.function.face, base);
if (fnCvd < clayFunctionMinCvdDeltaE) {
body('function (colour-blind)', fnCvd, clayFunctionMinCvdDeltaE);
}
}
// The highlight lifts every face and the pillow panel.
double lift(int face) =>
_lightness(clayHighlightComposite(face, p.highlight)) - _lightness(face);
for (final (name, g) in _groups) {
final d = lift(_keyOf(p, g).face);
if (d < clayMinHighlightLift) {
out.add(
GuardrailViolation(
GuardrailRule.clayHighlight,
d,
clayMinHighlightLift,
name,
),
);
}
}
{
final d = lift(p.surface);
if (d < clayMinHighlightLift) {
out.add(
GuardrailViolation(
GuardrailRule.clayHighlight,
d,
clayMinHighlightLift,
'panel',
),
);
}
}
// Function labels where they cross the ring.
final ring = clayLabelRingColor(
p.function,
style.edgeShadePermille,
p.highlight,
);
final ringCr = contrastRatio(p.function.label, ring);
if (ringCr < lim.labelRatio) {
out.add(
GuardrailViolation(GuardrailRule.clayLabelRing, ringCr, lim.labelRatio),
);
}
// The focus ring where it crosses a key's own drop shadow.
var ringWorst = double.infinity;
for (final x in clayFocusShadowSamples(p, bg.colors)) {
final cr = contrastRatio(p.focusRing, x);
if (cr < ringWorst) ringWorst = cr;
}
if (ringWorst < lim.focusRingRatio) {
out.add(
GuardrailViolation(
GuardrailRule.focusRing,
ringWorst,
lim.focusRingRatio,
'over key shadow',
),
);
}
// The display panel: an opaque pillow, lighter than and distinct from
// the body.
final surfaceAlpha = (p.surface >>> 24) & 0xFF;
final panelDe = deltaEOKArgb(p.surface, base);
if (surfaceAlpha != 255) {
out.add(
GuardrailViolation(
GuardrailRule.clayPanel,
surfaceAlpha.toDouble(),
255,
'opaque',
),
);
} else if (panelDe < 0.025) {
out.add(
GuardrailViolation(GuardrailRule.clayPanel, panelDe, 0.025, 'vs body'),
);
} else if (!clayPanelRaised(spec)) {
out.add(
GuardrailViolation(
GuardrailRule.clayPanel,
luminanceArgb(p.surface | 0xFF000000),
baseY,
'raised (lighter than the body)',
),
);
}
return out;
}