buildNeumorphicCandidate function
ThemeSpec
buildNeumorphicCandidate(
- int seed,
- FamilyDefinition def,
- int engineVersion, {
- required String paletteTag,
- 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,
);
}