Ein Seed ist eine Person
Dieselbe Zahl ergibt immer dasselbe Gesicht. Das klingt nach einer Bequemlichkeit und ist in Wahrheit die härteste Zusage dieses Projekts – sie zwingt jeder späteren Änderung eine Regel auf.
Ein Generator für alles
Der ganze Zufall dieses Projekts sind vierzehn Zeilen. Kein
Math.random() irgendwo im Zeichenpfad, keine Bibliothek:
mulberry32 ist klein, schnell und – das ist der Punkt –
deterministisch.
Symptom: Neun Köpfe nebeneinander sehen sich
ähnlicher, als sie sollten – ähnliche Kopfbreite, ähnliche
Merkmale, sichtbare Streifen im Raster.
Ursache: Ein Rasterplatz bekommt
blattSeed + index. Benachbarte Seeds sind hier also der
Normalfall, nicht die Ausnahme – und die ersten Werte einer
LCG-artigen Quelle korrelieren bei benachbarten Startwerten
sichtbar.
Lösung: Acht Züge wegwerfen. macheZufall
ruft den Generator achtmal, bevor er ihn herausgibt; danach sind
Seed 4711 und Seed 4712 zwei Fremde.
Alles hängt an einer Rohquelle
range, int, pick,
chance und weighted sind Bequemlichkeiten,
und jede zieht ihre Zahl aus derselben Funktion r. Das
ist keine Sparsamkeit, sondern die Voraussetzung für den nächsten
Absatz: nur ein Strom, der an einer Stelle entspringt, lässt sich
mitschreiben und noch einmal abspielen.
Mitschreiben und abspielen
Gebraucht wird das an genau einer Stelle: die Nase wird über dem Bart ein zweites Mal gezeichnet. Sie muss dabei dieselbe Linie sein, nicht eine ähnliche – der Stift zieht seinen Verzug aus dem Strom, und ein zweiter, frischer Strom ergäbe eine Nase, die um einen Haaresbreite danebenliegt. Zwei Nasen, die fast übereinanderliegen, sehen aus wie ein Druckfehler.
Die andere Lösung wäre ein eigener Strom für die Form gewesen. Sie wurde verworfen: ein eigener Strom verschöbe den Hauptstrom nicht – aber er müsste irgendwo herkommen, und jede Ableitung, die aus dem Hauptstrom zieht, verschiebt alles danach.
Symptom: Beim zweiten Durchgang liefert der Strom
undefined, und die Zeichnung stürzt mitten im Gesicht ab.
Ursache: Der zweite Durchgang zieht mehr Werte als
der erste – etwa weil eine Form je nach Zweig unterschiedlich viele
braucht.
Lösung: abspielen wiederholt das Band
von vorn, statt zu scheitern. Die Wiederholung stimmt dann ohnehin
nicht mehr; ein Absturz wäre trotzdem die schlechtere Antwort.
Forks: eigene Ströme ohne Nebenwirkung
Ein FNV-Hash über das Label, verrechnet mit dem Seed. Gleicher Seed plus gleiches Label ergibt immer denselben Strom, und zwei verschiedene Labels ergeben zwei unabhängige. Damit kann jeder Zeichenschritt würfeln, ohne irgendjemanden zu verschieben.
Jeder Nebenstrom hat einen Namen. strom(seed, label) ist
der Griff dafür: ein ablegerSeed und ein
macheZufall in einem.
| Strom | Wo | Wofür |
|---|---|---|
'layout' | core/dna.ts | die vier Layout-Gene |
'handschrift' | styles/klar/index.ts | die Kopfdaten des Stils: Augen-Jitter, Seite, Taktversatz, Bartlage, Punktmaß, Stern, Herz, Wangenbogen |
'zacken' | schale.ts | die Zacken des Ansatzes |
'schlaefenfall' | schale.ts | wie weit das Haar an der Schläfe fällt |
'afroBeulen' | schale.ts | die drei Beulen, die aus der Kugel Haar machen |
'afroSorte' | afro.ts | welcher der sechs Archetypen gezogen wird |
'afroRauung' | afro.ts | die feine Rauung des Rands |
'afroFransen' | afro.ts | die Fransen |
'afroKringel' | afro.ts | die Kringel in der Masse |
'straehnen' | masse.ts | welche Strähne radiert ist und welche ihren dunklen Zwilling bekommt |
'locken' | haar.ts | die Wickel am Rand der Lockenwolke |
'dutt' | haar.ts | der Knoten des Dutts |
'fuelle' | haar.ts | die Fülle der Frisur |
'hutform' | hut.ts | die Sorte Hut und ihre Maße |
'feder' | app/blick.ts | Steifigkeit, Atem, Lidschlag, Plappern |
'name' | app/namen.ts | die Silben unter dem Kopf |
'marke' | app/logo.ts | die Setzfehler über dem Blatt |
Bis vor kurzem stand an diesen Stellen seed ^ 0x64757474.
Die Zahlen waren lesbare Wörter in Hex – 0x64757474 ist
„dutt" –, und das war kein Scherz, sondern Buchführung: zwei Ableger
mit derselben Konstante wären derselbe Strom gewesen, und eine
doppelte Zahl fällt beim Nachschlagen schneller auf als ein doppelter
Zufallswert. Nur war es dieselbe Buchführung, die ablegerSeed
schon führte – von Hand geschrieben, in einer Schreibweise, die man
erst übersetzen muss. Wer einen neuen Strom brauchte, musste eine
freie Zahl suchen. Jetzt schreibt er das Wort hin.
Die Zieh-Reihenfolge ist Teil des Vertrags
Neue Felder immer ans Ende. Ein Feld, das mitten im Strom gezogen wird, verschiebt alles danach – und damit jedes bestehende Gesicht. Ein Seed ist die Adresse einer Person; wer die Reihenfolge ändert, zieht alle um.
Genau so ist es passiert: kragen, zeichen,
pupille und zierrat kamen später dazu.
Inhaltlich gehören sie zwischen die anderen Merkmale, im Code stehen
sie hinter der Palette – mit einem Kommentar, der sagt, warum.
Dieselbe Regel gilt eine Ebene tiefer. Der klare Stil zieht in
kopfdaten seine eigene Streuung, und auch dort kommt
Neues hinten dran: der Stern des Sternauges und das Herz auf dem Mund
stehen am Ende, weil ein Kopf, der sie nicht trägt, sich sonst allein
davon geändert hätte, dass es sie gibt.
Warum das Layout aus einem eigenen Strom kommt
Die Merkmalswahl braucht das Layout: welche Augenform gewählt werden darf, hängt davon ab, wie weit die Augen auseinanderstehen. Käme das Layout aus dem Hauptstrom, müsste es also vor den Merkmalen gezogen werden – und jede spätere Änderung an den Layout-Genen verschöbe sämtliche Merkmale sämtlicher Seeds. Der Fork macht die beiden Ströme unabhängig: am Layout darf gearbeitet werden, ohne dass jemand ein neues Gesicht bekommt.
Der Katalog ist Daten, kein switch
Welche Varianten es gibt, wie wahrscheinlich sie sind und wo sie
nicht hinpassen – das steht in core/katalog.ts als
Liste. Ein switch über Kategorien wäre dieselbe
Information, verteilt auf so viele Stellen, wie es Leser gibt.
nurBei ist hart: passt die Lage nicht, fällt die Variante
raus. lieber ist weich: passt sie, verdreifacht sich das
Gewicht. Und wenn alles herausfällt, gewinnt die ungefilterte Liste –
eine Kategorie darf nie leer sein, denn weighted auf
einer leeren Tabelle hat keine Antwort.
Verträglichkeit kommt nach der Ziehung
Das ist der Grund, warum Kopfbedeckungen im Diagramm oben seltener sind, als ihr Gewicht sagt: Sie werden nachträglich gestrichen. Eine Kappe über einer Haarmasse gibt einen Wulst am Ansatz – nur der Hut darf über die Schale, weil seine Krempe den Ansatz deckt, und auch er nicht über Zöpfen oder Dutt, die oben herausstehen. Augenringe neben rosigen Backen geben einen Ausschlag.
Gestrichen wird nach der Ziehung, nicht davor. Wer die Kandidaten vorher filtert, ändert die Zahl der gezogenen Werte – und verschiebt damit jeden Seed, sobald sich eine Regel ändert. Ein stiller Wurf, der weggeworfen wird, kostet nichts.
Die Namen
Ein Gesicht mit Namen wird angesehen, ein Gesicht ohne Namen wird überflogen. Die Silben sind so gewählt, dass sie in keiner Sprache, die hier jemand liest, etwas bedeuten – und gelegentlich, in gut jedem fünften Fall, kommt eine dritte dazu. Gleich lange Namen in einer Reihe sehen aus wie ein Formular.
Auch die Namen hängen an einem eigenen Strom
(mulberry32(seed · 2246822519), vier Züge zum Aufwärmen).
Sie sind Beschriftung, nicht Identität – aber sie sind genauso
determiniert wie das Gesicht, sonst hieße derselbe Kopf bei jedem
Neuzeichnen anders.