buildGlassCandidate function

ThemeSpec buildGlassCandidate(
  1. int seed,
  2. FamilyDefinition def,
  3. int engineVersion, {
  4. int attempt = 0,
  5. bool accessible = false,
})

Builds one unvalidated Liquid Glass candidate (the generator repairs and validates it, retries with palette/retry/n and finally falls back to a Liquid Glass hero).

Implementation

ThemeSpec buildGlassCandidate(
  int seed,
  FamilyDefinition def,
  int engineVersion, {
  int attempt = 0,
  bool accessible = false,
}) {
  final paletteTag = attempt == 0
      ? RngTag.palette
      : RngTag.sub(RngTag.palette, 'retry', attempt);
  final p = Rng.stream(seed, paletteTag);
  final dark = p.pickWeighted(def.brightnessWeights) == 0;
  final brightness = dark ? ThemeBrightness.dark : ThemeBrightness.light;
  final ranges = def.ranges(brightness);
  final scheme = pickWeightedValue(p, def.schemes);
  final lim = accessible
      ? GuardrailLimits.accessible
      : GuardrailLimits.standard;
  final bile = def.bile;
  final h0 = p.nextHueTenths();
  final offsets = glassBlobOffsets(scheme);
  final blobHues = [
    for (final o in offsets) _wrap(h0 + o + p.nextJitterTenths(100)),
  ];

  // Mesh: base + three vivid blobs.
  final baseL = ranges.background.lMilli.sample(p) / 1000;
  final baseC =
      p.nextIntIn(ranges.background.chroma.lo, ranges.background.chroma.hi) /
      1000;
  final baseHue = blobHues.first;
  final base = oklchToArgb(baseL, baseC, baseHue / 10);
  final blobL = dark ? glassDarkBlobLMilli : glassLightBlobLMilli;
  final blobRel = dark ? glassDarkBlobChroma : glassLightBlobChroma;
  final blobCap = dark ? glassBlobChromaCapMilli : glassLightBlobChromaCapMilli;
  final blobs = <int>[];
  final blobLch = <Lch>[];
  for (var i = 0; i < 3; i++) {
    var lMilli = blobL.sample(p);
    if (accessible && dark && lMilli > 480) lMilli -= 80;
    final l = lMilli / 1000;
    final rel = blobRel.sample(p);
    var hue = blobHues[i];
    final c0 = _relChroma(rel, blobCap, l, hue / 10);
    hue = bile.apply(hue, l, c0);
    final c = _relChroma(rel, blobCap, l, hue / 10);
    blobs.add(oklchToArgb(l, c, hue / 10));
    blobLch.add((l: l, c: c, h: hue / 10));
  }
  final b = Rng.stream(seed, RngTag.background);
  final drift = glassDriftPeriodMs.sampleStep(b, 5000);
  final background = BackgroundSpec(
    kind: BackgroundKind.meshGradient,
    colors: List.unmodifiable([base, ...blobs]),
    angleTenths: b.nextInt(3600),
    driftPeriodMs: drift,
  );
  final bgs = backgroundSamples(background);

  // Rims: decorative light catchers, opaque and ≥ 3:1 when accessible.
  final rimAlpha = dark ? p.nextIntIn(110, 160) : p.nextIntIn(200, 240);
  int rim(double hDeg) {
    final decorative = withAlpha(oklchToArgb(0.99, 0.01, hDeg), rimAlpha);
    if (!accessible) return decorative;
    final start = dark
        ? oklchToArgb(0.95, 0.02, hDeg)
        : oklchToArgb(0.32, 0.04, hDeg);
    return solveOnAll(start, bgs, ratioLarge) ?? blackOrWhite(base);
  }

  final policyTarget = lim.labelRatio;
  final labelC = p.nextIntIn(4, ranges.labelMaxChromaMilli) / 1000;
  Lch labelFor(double tintL, double hDeg) => tintL < 0.62
      ? (l: 0.98, c: labelC, h: hDeg)
      : (l: 0.18, c: labelC, h: hDeg);

  KeyColors? key(Lch tint, int alpha, {Lch? label}) => solveGlassKey(
    tint: tint,
    alpha: alpha,
    label: label ?? labelFor(tint.l, tint.h),
    bgs: bgs,
    target: policyTarget,
    edge: rim(tint.h),
  );

  // Digits: smoky (dark) or milky (light) glass.
  final h0Deg = baseHue / 10;
  final digitTint = (
    l: ranges.digit.lMilli.sample(p) / 1000,
    c: p.nextIntIn(ranges.digit.chroma.lo, ranges.digit.chroma.hi) / 1000,
    h: h0Deg,
  );
  final digitAlpha = (dark ? glassDarkDigitAlpha : glassLightDigitAlpha).sample(
    p,
  );
  // Function keys: a second, denser tone of the base hue.
  final fnTint = (
    l: ranges.function.lMilli.sample(p) / 1000,
    c: p.nextIntIn(ranges.function.chroma.lo, ranges.function.chroma.hi) / 1000,
    h: h0Deg,
  );
  final fnAlpha = (dark ? glassDarkFunctionAlpha : glassLightFunctionAlpha)
      .sample(p);
  // Operators: vivid glass in blob 2's hue; `=`: denser, blob 3's hue.
  Lch accentTint(RoleRange r, double hDeg) {
    var lMilli = r.lMilli.sample(p);
    if (accessible) {
      // 7:1 labels exist only on faces with Y ≤ 0.1 or ≥ 0.3: keep the
      // accent glass out of the mid-tones.
      lMilli = dark
          ? (lMilli < 760 ? 760 : lMilli)
          : (lMilli > 440 ? 440 : lMilli);
    }
    final l = lMilli / 1000;
    final rel = p.nextIntIn(r.chroma.lo, r.chroma.hi);
    var hue = (hDeg * 10).round() % 3600;
    hue = bile.apply(hue, l, _relChroma(rel, r.chroma.capMilli, l, hue / 10));
    return (
      l: l,
      c: _relChroma(rel, r.chroma.capMilli, l, hue / 10),
      h: hue / 10,
    );
  }

  final opTint = accentTint(ranges.operator, blobLch[1].h);
  final eqTint = accentTint(ranges.equals, blobLch[2].h);
  final opAlpha = (dark ? glassDarkOperatorAlpha : glassLightOperatorAlpha)
      .sample(p);
  final eqAlpha = (dark ? glassDarkEqualsAlpha : glassLightEqualsAlpha).sample(
    p,
  );

  final digitKey = key(digitTint, digitAlpha);
  final fnKey = key(fnTint, fnAlpha);
  final opKey = key(opTint, opAlpha);
  var eqKey = key(eqTint, eqAlpha);
  if (eqKey != null && opKey != null && digitKey != null) {
    eqKey = separateGlassEquals(eqKey, opKey, digitKey, lim, bgs);
  }

  // Display backing: dense surface glass; texts solved on it composited
  // over every sample.
  final surfaceL =
      (dark ? glassDarkSurfaceLMilli : glassLightSurfaceLMilli).sample(p) /
      1000;
  final surfaceC =
      p.nextIntIn(ranges.surfaceChroma.lo, ranges.surfaceChroma.hi) / 1000;
  var surfaceAlpha = (dark ? glassDarkSurfaceAlpha : glassLightSurfaceAlpha)
      .sample(p);
  final previewTarget = lim.previewRatio < ratioText
      ? ratioText
      : lim.previewRatio;
  final textTarget =
      (lim.displayRatio > previewTarget ? lim.displayRatio : previewTarget) +
      0.15;
  final textStart = oklchToArgb(dark ? 0.98 : 0.16, 0.012, h0Deg);
  int surface;
  List<int> panels;
  while (true) {
    surface = withAlpha(oklchToArgb(surfaceL, surfaceC, h0Deg), surfaceAlpha);
    panels = faceSamples(surface, bgs);
    final probe = solveOnAll(textStart, panels, textTarget);
    if (surfaceAlpha >= 250 ||
        (probe != null && _keepsPolarity(probe, panels, light: dark))) {
      break;
    }
    surfaceAlpha = surfaceAlpha + 10 > 250 ? 250 : surfaceAlpha + 10;
  }
  int onPanel(double l, double c, double hDeg, double target) =>
      solveOnAll(oklchToArgb(l, c, hDeg), panels, target) ??
      blackOrWhite(panels.first);
  final displayText = onPanel(
    dark ? 0.98 : 0.16,
    0.012,
    h0Deg,
    lim.displayRatio,
  );
  final previewText = onPanel(dark ? 0.80 : 0.42, 0.03, h0Deg, previewTarget);
  final error = onPanel(dark ? 0.74 : 0.50, 0.16, 25, lim.displayRatio);
  final accentL = dark ? 0.84 : 0.46;
  final accentStart = oklchToArgb(
    accentL,
    _relChroma(
      p.nextIntIn(ranges.accentChroma.lo, ranges.accentChroma.hi),
      ranges.accentChroma.capMilli,
      accentL,
      opTint.h,
    ),
    opTint.h,
  );
  final caret =
      solveOnAll(accentStart, panels, lim.caretRatio) ??
      blackOrWhite(panels.first);
  final focusRing =
      solveOnAll(accentStart, bgs, lim.focusRingRatio) ?? blackOrWhite(base);

  final glow = withAlpha(oklchToArgb(1, 0, 0), glassGlowAlpha.sample(p));
  final shadow = dark
      ? withAlpha(oklchToArgb(0.08, 0.02, h0Deg), 96)
      : withAlpha(oklchToArgb(0.30, 0.06, h0Deg), 46);

  final digit = digitKey ?? _fallbackKey(base);
  final function = fnKey ?? _fallbackKey(base);
  final operator = opKey ?? _fallbackKey(base);
  final equals = eqKey ?? _fallbackKey(base);
  var palette = Palette(
    background: base,
    surface: surface,
    displayText: displayText,
    previewText: previewText,
    error: error,
    caret: caret,
    focusRing: focusRing,
    accent: caret,
    digit: digit,
    operator: operator,
    equals: equals,
    function: function,
    shadow: shadow,
    highlight: glow,
    seedColor: operator.face | 0xFF000000,
  );
  palette = palette.copyWith(highlight: fitGlassGlow(palette, glow, bgs));

  final motion = def.motionRanges.vary(
    def.motion,
    Rng.stream(seed, RngTag.motion),
  );
  final s = Rng.stream(seed, RngTag.shape);
  final keyShape = KeyShapeSpec(
    kind: pickWeightedValue(s, def.shapes),
    cornerPermille: def.cornerPermille.sampleStep(s, 10),
  );
  final st = Rng.stream(seed, RngTag.style);
  final keyStyle = def.keyStyle.copyWith(
    kind: pickWeightedValue(st, def.styles),
    outlineWidthCdp: accessible
        ? 150 + 25 * st.nextInt(3)
        : 100 + 25 * st.nextInt(3),
    shadowOffsetCdp: 200 + 50 * st.nextInt(3),
    shadowBlurCdp: 600 + 100 * st.nextInt(5),
    opacityPermille: ((digit.face >>> 24) & 0xFF) * 1000 ~/ 255,
    frostPermille: (dark ? glassDarkFrost : glassLightFrost).sampleStep(st, 50),
    highlightPermille: glassHighlight.sampleStep(st, 50),
    refractionPermille: glassRefraction.sampleStep(st, 50),
  );
  final t = Rng.stream(seed, RngTag.type);
  final keyWeight = def.keyWeight.sampleStep(t, 50);
  final opWeight = keyWeight + def.operatorWeightBonus.sampleStep(t, 50);
  final type = TypeSpec(
    displayFont: pickWeightedValue(t, def.displayFonts),
    keyFont: pickWeightedValue(t, def.keyFonts),
    displayWeight: def.displayWeight.sampleStep(t, 50),
    keyWeight: keyWeight,
    operatorWeight: opWeight > 900 ? 900 : opWeight,
  );
  final name = generateThemeName(
    rng: Rng.stream(seed, RngTag.name),
    accentArgb: operator.face | 0xFF000000,
    family: ThemeFamily.liquidGlass,
    dark: dark,
  );

  return ThemeSpec(
    seed: seed,
    family: ThemeFamily.liquidGlass,
    engineVersion: engineVersion,
    brightness: brightness,
    accessible: accessible,
    palette: palette,
    keyShape: keyShape,
    keyStyle: keyStyle,
    type: type,
    background: background,
    motion: motion,
    sound: def.sound,
    name: name,
    nonColourOperatorCue: def.nonColourOperatorCue,
  );
}