validateBauhaus function

List<GuardrailViolation> validateBauhaus(
  1. ThemeSpec spec,
  2. GuardrailLimits lim,
  3. List<int> bgs
)

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;
}