buildNeumorphicCandidate function

ThemeSpec buildNeumorphicCandidate(
  1. int seed,
  2. FamilyDefinition def,
  3. int engineVersion, {
  4. required String paletteTag,
  5. required bool accessible,
})

One unvalidated Neumorphism candidate for seed (the generator adds repair, retries and the hero fallback). paletteTag is the palette stream of this attempt (palette, then palette/retry/n).

Implementation

ThemeSpec buildNeumorphicCandidate(
  int seed,
  FamilyDefinition def,
  int engineVersion, {
  required String paletteTag,
  required bool accessible,
}) {
  final p = Rng.stream(seed, paletteTag);
  final dark = p.pickWeighted(def.brightnessWeights) == 0;
  final brightness = dark ? ThemeBrightness.dark : ThemeBrightness.light;
  final r = def.ranges(brightness);
  final lim = accessible
      ? GuardrailLimits.accessible
      : GuardrailLimits.standard;
  final policy = lim.labelPolicy;
  final bile = def.bile;

  // Hues.
  final scheme = pickWeightedValue(p, def.schemes);
  final h0 = p.nextHueTenths();
  int at(int offset) => _wrap(h0 + offset + p.nextJitterTenths(100));
  final accentHue = switch (scheme) {
    HarmonyScheme.monochrome => h0,
    HarmonyScheme.analogous => at(p.nextChance(1, 2) ? 300 : -300),
    HarmonyScheme.complementary || HarmonyScheme.duotone => at(1800),
    HarmonyScheme.splitComplementary => at(p.nextChance(1, 2) ? 1500 : 2100),
    HarmonyScheme.triadic => at(p.nextChance(1, 2) ? 1200 : 2400),
  };
  final baseH = h0 / 10;

  // Surface, display well, shadows, rim.
  final bgL = r.background.lMilli.sample(p) / 1000;
  final bgC = _drawChroma(p, r.background.chroma, bgL, baseH);
  final bg = oklchToArgb(bgL, bgC, baseH);
  final surfL = clamp01(bgL + r.surfaceOffsetMilli.sample(p) / 1000);
  final surface = oklchToArgb(
    surfL,
    _drawChroma(p, r.surfaceChroma, surfL, baseH),
    baseH,
  );
  final shadows = dark ? softDarkShadows : softLightShadows;
  final hiL = clamp01(bgL + shadows.highlightMilli.sample(p) / 1000);
  final loL = clamp01(
    bgL -
        (accessible ? shadows.accessibleShadowMilli : shadows.shadowMilli)
                .sample(p) /
            1000,
  );
  var highlight = oklchToArgb(hiL, bgC * 0.5, baseH);
  if (luminanceArgb(highlight) <= luminanceArgb(bg)) highlight = 0xFFFFFFFF;
  var shadow = oklchToArgb(loL, bgC + 0.01, baseH);
  if (luminanceArgb(shadow) >= luminanceArgb(bg)) shadow = 0xFF000000;
  final rim = softRimFor(bg);

  // Accent: label colour of accent-label operators, pressed label, caret
  // and focus ring; ≥ the label floor on the surface and the display well.
  final accentL = dark ? 0.80 : 0.48;
  final accentC = _drawChroma(p, r.accentChroma, accentL, accentHue / 10);
  final accent = _solveAccent(
    accentL,
    accentC,
    accentHue,
    [bg, surface],
    lim.labelRatio > lim.caretRatio ? lim.labelRatio : lim.caretRatio,
    bile,
  );

  // Labels.
  final labelC = p.nextIntIn(5, r.labelMaxChromaMilli) / 1000;
  int solveOn(int face, double l, double c, double hDeg) =>
      solveLabel(face, (l: l, c: c, h: hDeg), policy: policy) ??
      blackOrWhite(face);
  final ink = solveOn(bg, r.labelStartLMilli / 1000, labelC, baseH);
  final fnInk = solveOn(bg, dark ? 0.86 : 0.36, 0.05, accentHue / 10);
  final digit = KeyColors(face: bg, label: ink, edge: rim);
  final function = KeyColors(face: bg, label: fnInk, edge: rim);

  // Operators.
  final opStyle = p.nextChance(1, 2)
      ? SoftOperatorStyle.accentFace
      : SoftOperatorStyle.accentLabel;
  final opLDraw = r.operator.lMilli.sample(p);
  final opCDraw = p.nextIntIn(r.operator.chroma.lo, r.operator.chroma.hi);
  final KeyColors operator;
  if (opStyle == SoftOperatorStyle.accentLabel) {
    operator = KeyColors(face: bg, label: accent, edge: rim);
  } else {
    final bgMilli = (bgL * 1000).round();
    final opMilli = dark
        ? (opLDraw < bgMilli + 100 ? bgMilli + 100 : opLDraw)
        : (opLDraw > bgMilli - 100 ? bgMilli - 100 : opLDraw);
    operator = _accentKey(
      opMilli / 1000,
      _chromaAt(r.operator.chroma, opCDraw, opMilli / 1000, accentHue / 10),
      accentHue,
      labelStartL: dark ? 0.97 : 0.20,
      labelC: labelC,
      rim: rim,
      policy: policy,
      bile: bile,
    );
  }

  // `=`: a full accent face.
  var eqMilli = r.equals.lMilli.sample(p);
  if (accessible) {
    if (dark && eqMilli < 720) eqMilli = 720;
    if (!dark && eqMilli > 440) eqMilli = 440;
  }
  final eqCDraw = p.nextIntIn(r.equals.chroma.lo, r.equals.chroma.hi);
  final equals = _accentKey(
    eqMilli / 1000,
    _chromaAt(r.equals.chroma, eqCDraw, eqMilli / 1000, accentHue / 10),
    accentHue,
    labelStartL: dark ? 0.18 : 0.98,
    labelC: labelC,
    rim: rim,
    policy: policy,
    bile: bile,
  );

  // Display texts on the display well and the surface.
  final backdrops = [surface, bg];
  int text(double l, double c, double hDeg, double target) =>
      solveOnAll(oklchToArgb(l, c, hDeg), backdrops, target) ??
      blackOrWhite(surface);
  final displayText = text(dark ? 0.96 : 0.20, 0.012, baseH, lim.displayRatio);
  final previewText = text(
    dark ? 0.80 : 0.40,
    0.02,
    baseH,
    lim.previewRatio < ratioText ? ratioText : lim.previewRatio,
  );
  final error = text(dark ? 0.74 : 0.48, 0.15, 25, lim.displayRatio);

  final palette = Palette(
    background: bg,
    surface: surface,
    displayText: displayText,
    previewText: previewText,
    error: error,
    caret: accent,
    focusRing: accent,
    accent: accent,
    digit: digit,
    operator: operator,
    equals: equals,
    function: function,
    shadow: shadow,
    highlight: highlight,
    seedColor: equals.face,
  );

  // Shape, style, type, motion, name.
  final s = Rng.stream(seed, RngTag.shape);
  final shapeKind = pickWeightedValue(s, def.shapes);
  final corner = def.cornerPermille.sampleStep(s, 10);
  final st = Rng.stream(seed, RngTag.style);
  final rimDraw = st.pick(softRimWidthsCdp);
  final keyStyle = def.keyStyle.copyWith(
    outlineWidthCdp: accessible ? softRimWidthsCdp.last : rimDraw,
  );
  final t = Rng.stream(seed, RngTag.type);
  final displayFont = pickWeightedValue(t, def.displayFonts);
  final keyFont = pickWeightedValue(t, def.keyFonts);
  final displayWeight = def.displayWeight.sampleStep(t, 50);
  final keyWeight = def.keyWeight.sampleStep(t, 50);
  final opWeight = keyWeight + def.operatorWeightBonus.sampleStep(t, 50);
  final motion = def.motionRanges.vary(
    def.motion,
    Rng.stream(seed, RngTag.motion),
  );
  final name = generateThemeName(
    rng: Rng.stream(seed, RngTag.name),
    accentArgb: equals.face,
    family: def.family,
    dark: dark,
  );

  return ThemeSpec(
    seed: seed,
    family: def.family,
    engineVersion: engineVersion,
    brightness: brightness,
    accessible: accessible,
    palette: palette,
    keyShape: KeyShapeSpec(
      kind: shapeKind,
      cornerPermille: shapeKind == KeyShapeKind.circle ? 500 : corner,
    ),
    keyStyle: keyStyle,
    type: TypeSpec(
      displayFont: displayFont,
      keyFont: keyFont,
      displayWeight: displayWeight,
      keyWeight: keyWeight,
      operatorWeight: opWeight > 900 ? 900 : opWeight,
    ),
    background: BackgroundSpec(
      kind: BackgroundKind.solid,
      colors: List.unmodifiable([bg]),
    ),
    motion: motion,
    sound: def.sound,
    name: name,
    nonColourOperatorCue: def.nonColourOperatorCue,
  );
}