validateClaymorphism function

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

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