validateBauhaus function
The Bauhaus rules (called from validateTheme for Bauhaus specs only):
- GuardrailRule.bauhausOperatorCue: the recorded shape cue is real —
digit silhouette block or disc, operator silhouette the other one,
=a disc, fan or triangle that differs from the digits, blocks no rounder than 4 %. - GuardrailRule.bauhausPrimaries: operator and
=are two different primaries (bauhausPrimaryOf); digits are neutral; function keys neutral or a primary; the function accent opaque or off and distinct from the function face. - GuardrailRule.bauhausScene: the scene's encoding (kind, 6 colours,
no pattern, static, a quarter-turn orientation, colour 0 = the
background), a dark field that the digits stand out on, digit,
operator and
=faces visible on what they sit on, every scene shape colour distinct from the ground. - GuardrailRule.bauhausPanel: sheet, rule and accent opaque; the rule ≥ 3:1 on the sheet (WCAG 1.4.11); the accent distinct from the sheet (min ΔEOK over normal + CVD ≥ 0.07).
Implementation
List<GuardrailViolation> validateBauhaus(
ThemeSpec spec,
GuardrailLimits lim,
List<int> bgs,
) {
final out = <GuardrailViolation>[];
final p = spec.palette;
final shape = spec.keyShape;
// Operator cue.
void cue(String detail) => out.add(
GuardrailViolation(GuardrailRule.bauhausOperatorCue, 0, 1, detail),
);
const silhouettes = {KeyShapeKind.roundedRect, KeyShapeKind.circle};
const equalsFigures = {M3eShape.circle, M3eShape.fan, M3eShape.triangle};
if (spec.nonColourOperatorCue != OperatorCue.shape) cue('cue not recorded');
if (!silhouettes.contains(shape.kind)) cue('digit silhouette');
if (!silhouettes.contains(shape.operatorKind) ||
shape.operatorKind == shape.kind) {
cue('operator silhouette');
}
if (!equalsFigures.contains(shape.operatorM3eShape) ||
(shape.kind == KeyShapeKind.circle &&
shape.operatorM3eShape == M3eShape.circle)) {
cue('= figure');
}
if (shape.cornerPermille < 0 ||
shape.cornerPermille > bauhausMaxCornerPermille) {
cue('block corners');
}
// Primaries.
void primaries(double measured, double required, String detail) => out.add(
GuardrailViolation(
GuardrailRule.bauhausPrimaries,
measured,
required,
detail,
),
);
// A steered theme (engine v5) takes its accents from the intent: two
// strong accents of different hues stand in for two primaries.
final steered = spec.variant == ThemeVariant.steered;
final op = bauhausPrimaryOf(p.operator.face);
final eq = bauhausPrimaryOf(p.equals.face);
final opLch = argbToOklch(p.operator.face);
final eqLch = argbToOklch(p.equals.face);
if (steered) {
if (opLch.c < bauhausSteeredAccentMinChroma) {
primaries(opLch.c, bauhausSteeredAccentMinChroma, 'operator accent');
}
if (eqLch.c < bauhausSteeredAccentMinChroma) {
primaries(eqLch.c, bauhausSteeredAccentMinChroma, '= accent');
}
final d = (opLch.h - eqLch.h).abs() % 360;
final apart = d > 180 ? 360 - d : d;
if (apart < bauhausSteeredAccentApartDeg) {
primaries(
apart,
bauhausSteeredAccentApartDeg,
'operator and = share a hue',
);
}
} else {
if (op == null) {
primaries(opLch.c, 0.08, 'operator primary');
}
if (eq == null) {
primaries(eqLch.c, 0.08, '= primary');
}
if (op != null && op == eq) {
primaries(0, 1, 'operator and = share a hue');
}
}
final digitC = argbToOklch(p.digit.face).c;
if (digitC >= bauhausNeutralMaxChroma) {
primaries(digitC, bauhausNeutralMaxChroma, 'digit not neutral');
}
final fnC = argbToOklch(p.function.face).c;
if (fnC >= bauhausNeutralMaxChroma &&
bauhausPrimaryOf(p.function.face) == null &&
!(steered && fnC >= bauhausSteeredAccentMinChroma)) {
primaries(fnC, bauhausNeutralMaxChroma, 'function neither');
}
final accent = p.function.edge;
final accentAlpha = (accent >>> 24) & 0xFF;
if (accentAlpha != 0) {
final d = accentAlpha == 255 ? deltaEOKArgb(accent, p.function.face) : 0.0;
if (d < bauhausAccentDeltaE) {
primaries(d, bauhausAccentDeltaE, 'function accent');
}
}
// Scene.
void scene(double measured, double required, String detail) => out.add(
GuardrailViolation(GuardrailRule.bauhausScene, measured, required, detail),
);
final bg = spec.background;
final colors = bg.colors;
if (bg.kind != BackgroundKind.bauhausComposition) scene(0, 1, 'kind');
if (bg.pattern != BackgroundPattern.none ||
bg.patternOpacityPermille != 0 ||
bg.driftPeriodMs != 0) {
scene(0, 1, 'static, no pattern');
}
if (bg.angleTenths < 0 ||
bg.angleTenths > 2700 ||
bg.angleTenths % 900 != 0) {
scene(bg.angleTenths.toDouble(), 0, 'orientation');
}
if (colors.length != 6) {
scene(colors.length.toDouble(), 6, 'colours');
} else {
final ground = colors[0] | 0xFF000000;
final field = colors[1] | 0xFF000000;
if (colors[0] != p.background) scene(0, 1, 'ground = background');
final digit = p.digit.face | 0xFF000000;
if (field != ground) {
final l = argbToOklch(field).l;
final cr = contrastRatio(digit, field);
if (l > 0.22) scene(l, 0.22, 'field not dark');
if (cr < ratioLarge) scene(cr, ratioLarge, 'digits on the field');
}
final under = field != ground ? field : ground;
final visible = contrastRatio(digit, under);
if (visible < bauhausDigitVisibility) {
scene(visible, bauhausDigitVisibility, 'digits on the ground');
}
for (final (face, what) in [
(p.operator.face, 'operators'),
(p.equals.face, '='),
]) {
final cr = contrastRatio(face | 0xFF000000, under);
if (cr < bauhausKeyVisibility) {
scene(cr, bauhausKeyVisibility, '$what on the ground');
}
}
for (var i = 2; i < 6; i++) {
final d = deltaEOKArgb(colors[i] | 0xFF000000, ground);
if (d < bauhausSceneShapeDeltaE) {
scene(d, bauhausSceneShapeDeltaE, 'scene colour $i on the ground');
}
}
}
// Display.
void display(double measured, double required, String detail) => out.add(
GuardrailViolation(GuardrailRule.bauhausPanel, measured, required, detail),
);
bool opaque(int c) => ((c >>> 24) & 0xFF) == 0xFF;
if (!opaque(p.surface) || !opaque(p.shadow) || !opaque(p.highlight)) {
display(0, 1, 'opaque sheet, rule and accent');
}
final rule = contrastRatio(p.shadow, p.surface);
if (rule < ratioLarge) display(rule, ratioLarge, 'rule on the sheet');
final mark = minCvdDeltaE(p.highlight | 0xFF000000, p.surface | 0xFF000000);
if (mark < bauhausDisplayAccentDeltaE) {
display(mark, bauhausDisplayAccentDeltaE, 'accent on the sheet');
}
return out;
}