buildPaperInkCandidate function
ThemeSpec
buildPaperInkCandidate(
- int seed,
- FamilyDefinition def,
- int engineVersion, {
- int attempt = 0,
- bool accessible = false,
Builds one unvalidated Paper & ink candidate (the generator repairs and
validates it, retries with palette/retry/n and finally falls back to
a Paper & ink hero).
Implementation
ThemeSpec buildPaperInkCandidate(
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 lim = accessible
? GuardrailLimits.accessible
: GuardrailLimits.standard;
final policy = lim.labelPolicy;
final bile = def.bile;
// P1–P3: brightness, sheet, tone.
final dark = p.pickWeighted(def.brightnessWeights) == 0;
final brightness = dark ? ThemeBrightness.dark : ThemeBrightness.light;
final sheetPick = p.pickWeighted(
dark ? paperInkDarkSheetWeights : paperInkLightSheetWeights,
);
final sheet = PaperSheet.values[dark ? 5 + sheetPick : sheetPick];
final tone = p.pickWeighted(paperInkToneWeights);
final kraft = sheet == PaperSheet.kraft;
// P4–P6: paper.
final paperRange = dark && tone == 1
? paperInkDarkTone1Papers[sheet.index - 5]
: paperInkPapers[sheet.index];
final paperL = paperRange.l.sample(p);
final paperC = paperRange.c.sample(p);
final paperH = paperRange.h.sample(p);
final paper = oklchToArgb(paperL / 1000, paperC / 1000, paperH / 10);
// P7–P10: ink.
final inkPick = p.pickWeighted(switch (sheet) {
PaperSheet.inkPaper => paperInkInkPaperInkWeights,
PaperSheet.blackboard => paperInkBlackboardInkWeights,
_ => paperInkLightInkWeights,
});
final inkType = PaperInkType.values[dark ? 4 + inkPick : inkPick];
final inkRange = paperInkInks[inkType.index];
var inkL = inkRange.l.sample(p);
final inkC = inkRange.c.sample(p);
final inkHueDraw = inkRange.h.sample(p);
final inkH = inkType == PaperInkType.carbon ? paperH : inkHueDraw;
if (accessible) {
inkL = dark
? (inkL < paperInkAccessibleDarkInkMinL
? paperInkAccessibleDarkInkMinL
: inkL)
: (inkL > paperInkAccessibleLightInkMaxL
? paperInkAccessibleLightInkMaxL
: inkL);
}
final inkLch = (l: inkL / 1000, c: inkC / 1000, h: inkH / 10);
final ink = oklchToArgb(inkLch.l, inkLch.c, inkLch.h);
// P11–P14: the stamp.
final stampInk = p.pickWeighted(
dark ? paperInkDarkStampWeights : paperInkLightStampWeights,
);
final stampH = paperInkStampHues[stampInk].sample(p);
var stampL = (dark ? paperInkDarkStampL : paperInkLightStampL[stampInk])
.sample(p);
final stampRel = paperInkStampRel.sample(p);
if (accessible) {
stampL = dark
? (stampL < paperInkAccessibleDarkStampMinL
? paperInkAccessibleDarkStampMinL
: stampL)
: (stampL > paperInkAccessibleLightStampMaxL
? paperInkAccessibleLightStampMaxL
: stampL);
}
final stampCap = dark
? paperInkDarkStampCapMilli
: paperInkLightStampCapMilli;
// P15–P18: the operator wash.
final washOffsetPick = p.pickWeighted(paperInkWashOffsetWeights);
final washJitter = p.nextJitterTenths(100);
final washCMilli = dark ? p.nextIntIn(42, 50) : p.nextIntIn(45, 75);
final washDL = dark ? p.nextIntIn(25, 45) : p.nextIntIn(45, 75);
// P19–P24: slips.
final digitDL = dark ? p.nextIntIn(35, 60) : p.nextIntIn(12, 28);
final cardL = p.nextIntIn(935, 960);
final cardC = p.nextIntIn(18, 30);
final cardH = p.nextIntIn(800, 900);
final fnDL = p.nextIntIn(35, 55);
final fnC = dark ? p.nextIntIn(15, 25) : p.nextIntIn(12, 22);
// P25–P27: rule ink.
final ruleRange = paperInkRules[sheet.index];
final ruleL = ruleRange.l.sample(p);
final ruleC = ruleRange.c.sample(p);
final ruleHueDraw = ruleRange.h.sample(p);
final ruleH = sheet == PaperSheet.inkPaper ? paperH : ruleHueDraw;
final rule = oklchToArgb(ruleL / 1000, ruleC / 1000, ruleH / 10);
// P28–P29: the display panel.
final panelA = p.nextIntIn(dark ? 30 : 975, dark ? 50 : 995);
final panelB = p.nextIntIn(0, 22);
final int panel;
if (dark) {
panel = oklchToArgb(
(paperL + panelA) / 1000,
paperC * 0.9 / 1000,
paperH / 10,
);
} else if (sheet == PaperSheet.graph) {
final l = paperL + 8 + (panelA - 975) * 7 ~/ 20;
panel = oklchToArgb((l > 995 ? 995 : l) / 1000, paperC / 2000, paperH / 10);
} else if (kraft) {
panel = oklchToArgb(
(950 + (panelA - 975)) / 1000,
(15 + panelB * 10 ~/ 22) / 1000,
85,
);
} else {
panel = oklchToArgb(
panelA / 1000,
(3 + panelB * 7 ~/ 22) / 1000,
paperH / 10,
);
}
// Faces (lightness × 1000).
final int digitL;
final Lch digitFace;
if (kraft) {
digitL = cardL;
digitFace = (l: cardL / 1000, c: cardC / 1000, h: cardH / 10);
} else if (dark) {
var l = paperL + digitDL;
if (accessible && l > 340) l = 340;
digitL = l;
digitFace = (l: l / 1000, c: paperC * 0.8 / 1000, h: paperH / 10);
} else {
final l = paperL + digitDL;
digitL = l > 992 ? 992 : l;
digitFace = (l: digitL / 1000, c: paperC * 0.6 / 1000, h: paperH / 10);
}
final Lch fnFace;
{
var l = dark ? digitL + fnDL : digitL - fnDL;
if (dark && accessible && l > 380) l = 380;
// Kraft: a darker cream card (the brown rule hue at low chroma reads
// pinkish grey); elsewhere the rule hue.
fnFace = (l: l / 1000, c: fnC / 1000, h: (kraft ? cardH : ruleH) / 10);
}
// The style stream (independent of the palette stream) fixes the hatch
// opacity the operator face must carry.
final st = Rng.stream(seed, RngTag.style);
final keyStyle = def.keyStyle.copyWith(
kind: pickWeightedValue(st, def.styles),
outlineWidthCdp: paperInkOutlineWidthCdp.sampleStep(st, 25),
edgeShadePermille: paperInkHatchPermille.sampleStep(st, 50),
shadowOffsetCdp: 0,
stateLayerPermille: 0,
);
final Lch opFace;
{
var offset = paperInkWashOffsets[washOffsetPick];
var washH = _wrap(stampH + offset + washJitter);
if (dark && paperInkDarkOliveHues.contains(washH)) {
offset = -offset;
washH = _wrap(stampH + offset + washJitter);
}
if (dark) washH = paperDarkWashHue(washH);
var l = dark ? digitL + washDL : digitL - washDL;
if (dark && accessible && l > 380) l = 380;
double chroma(int lMilli) =>
_capped(0.9 * maxChroma(lMilli / 1000, washH / 10), washCMilli);
// Light sheets: the hatch (the operator cue) must show on the wash —
// lighten the face until it does.
if (!dark) {
while (l < 970 &&
paperHatchContrast(
KeyColors(
face: oklchToArgb(l / 1000, chroma(l), washH / 10),
label: ink,
edge: ink,
),
keyStyle.edgeShadePermille,
) <
paperHatchRatio + 0.05) {
l += 10;
}
}
opFace = (l: l / 1000, c: chroma(l), h: washH / 10);
}
final stampChroma = _capped(
stampRel / 1000 * maxChroma(stampL / 1000, stampH / 10),
stampCap,
);
var eqFace = (l: stampL / 1000, c: stampChroma, h: stampH / 10);
// Keys: ink labels (repairPair keeps the ink when it passes); the stamp
// label knocked out to the slip (light) or the sheet (dark).
final paperLch = argbToOklch(paper);
final eqLabelStart = dark ? paperLch : digitFace;
KeyColors key(Lch face, Lch start) {
var pair = repairPair(face, start, policy: policy);
if (bile.recheckFinal) {
final f = argbToOklch(pair.face);
final hue = (f.h * 10).round() % 3600;
final rotated = bile.apply(hue, f.l, f.c);
if (rotated != hue) {
pair = repairPair(
(l: f.l, c: f.c, h: rotated / 10),
start,
policy: policy,
);
}
}
return KeyColors(face: pair.face, label: pair.label, edge: ink);
}
final digit = key(digitFace, inkLch);
final function = key(fnFace, inkLch);
final operator = key(opFace, inkLch);
var equals = key(eqFace, eqLabelStart);
if (minCvdDeltaE(equals.face, operator.face) < 0.065) {
// `=` must stand apart from the operators (CVD included): move the
// stamp 0.10 further from the slips, inside the lock clamps.
var l = eqFace.l + (dark ? 0.10 : -0.10);
if (accessible) {
l = dark
? (l < paperInkAccessibleDarkStampMinL / 1000
? paperInkAccessibleDarkStampMinL / 1000
: l)
: (l > paperInkAccessibleLightStampMaxL / 1000
? paperInkAccessibleLightStampMaxL / 1000
: l);
}
eqFace = (l: clamp01(l), c: eqFace.c, h: eqFace.h);
equals = key(eqFace, eqLabelStart);
}
// The sheet.
final background = _background(
Rng.stream(seed, RngTag.background),
sheet,
paper: paper,
rule: rule,
);
final bgs = paperInkSceneSamples(background)!;
// Outlines: the ink, re-solved to 3:1 on every sheet sample.
final edge = solveOnAll(ink, bgs, ratioLarge) ?? blackOrWhite(paper);
KeyColors edged(KeyColors k) => k.copyWith(edge: edge);
// Display texts on the panel and its grain / rule composites.
final backdrops = paperPanelSamples(
panel,
background,
accessible: accessible,
);
int onAll(int start, List<int> on, double target, int fallback) =>
solveOnAll(start, on, target) ?? fallback;
final previewTarget = lim.previewRatio < ratioText
? ratioText
: lim.previewRatio;
final displayText = onAll(
ink,
backdrops,
lim.displayRatio,
blackOrWhite(panel),
);
final previewText = onAll(
oklchToArgb(dark ? 0.78 : 0.42, inkLch.c, inkLch.h),
backdrops,
previewTarget,
blackOrWhite(panel),
);
// The red pen is solved 0.05 past its floor: it lands right on it, and
// the theme transition's OKLab round trip (at its rubber-band frames)
// could otherwise take it a hair under on a grain fleck.
final error = onAll(
oklchToArgb(dark ? 0.76 : 0.50, dark ? 0.12 : 0.17, 27),
backdrops,
lim.displayRatio + paperTextMargin,
blackOrWhite(panel),
);
final caretL = dark ? 0.80 : 0.46;
final caretC = 0.8 * maxChroma(caretL, stampH / 10);
final caret = onAll(
oklchToArgb(caretL, caretC < 0.18 ? caretC : 0.18, stampH / 10),
backdrops,
lim.caretRatio,
displayText,
);
final focus = onAll(edge, bgs, lim.focusRingRatio, blackOrWhite(paper));
var palette = Palette(
background: paper,
surface: panel,
displayText: displayText,
previewText: previewText,
error: error,
caret: caret,
focusRing: focus,
accent: equals.face,
digit: edged(digit),
operator: edged(operator),
equals: edged(equals),
function: edged(function),
shadow: edge | 0xFF000000,
highlight: withAlpha(edge, paperWashRimMaxAlpha),
seedColor: equals.face,
);
palette = palette.copyWith(
highlight: fitPaperWash(palette, palette.highlight, lim),
);
// Motion, shape, type, name.
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 type = _type(Rng.stream(seed, RngTag.type), def);
// The name accent is the stamp (the family's one chromatic ink), not the
// operator face `generateThemeName` documents: Paper's operator faces
// are faint washes, often too pale to name a colour.
final name = generateThemeName(
rng: Rng.stream(seed, RngTag.name),
accentArgb: equals.face,
family: ThemeFamily.paperInk,
dark: dark,
);
return ThemeSpec(
seed: seed,
family: ThemeFamily.paperInk,
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,
);
}