buildGlassCandidate function
ThemeSpec
buildGlassCandidate(
- int seed,
- FamilyDefinition def,
- int engineVersion, {
- int attempt = 0,
- 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,
);
}