A seed is a person

The same number always yields the same face. That sounds like a convenience and is in truth the hardest promise this project makes – it forces a rule on every later change.

One generator for everything

All the randomness in this project is fourteen lines. No Math.random() anywhere in the drawing path, no library: mulberry32 is small, fast and – this is the point – deterministic.

Neighbouring seeds

Symptom: Nine heads side by side look more alike than they should – similar head widths, similar features, visible stripes in the grid. Cause: A grid place gets blattSeed + index. Neighbouring seeds are therefore the normal case here, not the exception – and the first values of an LCG-like source correlate visibly for neighbouring start values. Fix: Throw away eight draws. macheZufall calls the generator eight times before handing it out; after that seed 4711 and seed 4712 are two strangers.

Everything hangs off one raw source

range, int, pick, chance and weighted are conveniences, and each draws its number from the same function r. That is not thrift but the precondition for the next paragraph: only a stream that springs from one place can be recorded and played back.

A range with outliers

range(a, b) distributes evenly. Whoever wants more character and widens the range makes every head a bit more extreme – and a sheet full of extremes is monotonous again, just differently. mitAusreissern(rng, a, b, aussenA, aussenB) leaves the middle as it was: eight draws in ten land exactly where range(a, b) would have put them. Only the outermost tenth on each side is stretched and reaches aussenA or aussenB. An outlier is rare, and the truly extreme one rarer still, because it is the end of the stretch.

It is ONE draw from the stream, like range: the streams behind it stay put. That is why head shape and layout genes could get outliers in August 2026 without a single seed's features or colours shifting – and a measure whose draw lay in the middle has stayed the same to the stroke. Nine measures with a tenth of margin on each side does mean, though: most heads have a slightly stretched value somewhere, about every second one has a clearly extreme measure, and only those hitting two margins at once get truly odd – a cucumber with eyes almost at the ears.

Record and play back

This is needed in exactly one place: the nose is drawn a second time on top of the beard. It has to be the same line, not a similar one – the pen draws its warp from the stream, and a second, fresh stream would give a nose that misses by a hair's breadth. Two noses that almost coincide look like a printing error.

The other solution would have been a separate stream for the shape. It was rejected: a separate stream would not shift the main stream – but it would have to come from somewhere, and every derivation that draws from the main stream shifts everything after it.

The tape runs out

Symptom: On the second pass the stream returns undefined, and the drawing crashes in the middle of the face. Cause: The second pass draws more values than the first – for instance because a shape needs a different number depending on the branch. Fix: abspielen repeats the tape from the start instead of failing. The repetition is wrong by then anyway; a crash would still be the worse answer.

Forks: separate streams without side effects

An FNV hash over the label, combined with the seed. The same seed plus the same label always yields the same stream, and two different labels yield two independent ones. That lets every drawing step roll dice without shifting anybody.

Every side stream has a name. strom(seed, label) is the handle for it: an ablegerSeed and a macheZufall in one.

StreamWhereFor what
'layout'core/dna.tsthe four layout genes
'handschrift'styles/klar/index.tsthe style's head data: eye jitter, side, beat offset, beard position, dot size, star shape, heart, cheek arc, neck width, star size, number of eye-ring arcs, second star shape, star colour, size of the star eyes, collar (seat, depth, width, band height, ribs, dropped stitch, point, opening, skew), lens (tint colour and strength), mouth width, laughing mouth (tongue or teeth, tongue width)
'zacken'schale.tsthe teeth of the hairline
'schlaefenfall'schale.tshow far the hair falls at the temple
'afroBeulen'schale.tsthe three bumps that turn the sphere into hair
'afroSorte'afro.tswhich of the six archetypes is drawn
'afroRauung'afro.tsthe fine roughening of the edge
'afroFransen'afro.tsthe fringes
'afroKringel'afro.tsthe curls inside the mass
'straehnen'masse.tswhich strand is erased and which gets its dark twin
'locken'haar.tsthe coils at the edge of the curl cloud
'dutt'haar.tsthe knot of the bun
'fuelle'haar.tsthe fullness of the hairdo
'hutform'hut.tsthe kind of hat and its measurements
'feder'app/blick.tsstiffness, breath, blink, chatter
'name'app/namen.tsthe syllables under the head
'marke'app/logo.tsthe typesetting errors above the sheet
'kopfform#3'core/wurf.tsthe workshop's third roll on the skull shape; likewise 'proportionen#n', 'haltung#n', 'farben#n'
'hand:haar:igel'core/wurf.tsthe handwriting of a rolled feature – the stream hangs off the VALUE and, from the second throw on, off the EDITION ('hand:haar:igel#3'): the same hedgehog from the same throw, a different one from the next. The colours have one too ('hand:farben:2'): the tone of the star eyes sits in no colour gene

Until recently these places said seed ^ 0x64757474. The numbers were readable words in hex – 0x64757474 is "dutt", the bun – and that was not a joke but bookkeeping: two forks with the same constant would have been the same stream, and a duplicated number is noticed faster when looking it up than a duplicated random value. Only it was the same bookkeeping that ablegerSeed already did – written by hand, in a notation you first have to translate. Whoever needed a new stream had to look for a free number. Now they write the word down.

Draw order is part of the contract

New fields always go at the end. A field drawn in the middle of the stream shifts everything after it – and with it every existing face. A seed is the address of a person; whoever changes the order moves everyone house.

That is exactly what happened: kragen, zeichen, pupille, zierrat and most recently the colour gene kleidT came later. By subject matter they belong among the other features; in the code they stand behind the palette – with a comment saying why. kleidT even sits INSIDE the palette object and is still drawn last: the object is an order for the reader, the stream one for the seeds.

The same rule holds one level down. In kopfdaten the clear style draws its own scatter, and there too new things go at the back: the star of the star eye and the heart on the mouth stand at the end, because otherwise a head that carries neither would have changed merely from the fact that they exist. When an existing feature later gains a new draw – the star size came after the star shape – that draw is appended too, not placed next to the old one: that is why wuerfleSternForm and wuerfleSternGroesse are separate, and wuerfleStern calls both only for the demos.

Why the layout comes from its own stream

The feature choice needs the layout: which eye shape may be chosen depends on how far apart the eyes are. If the layout came from the main stream it would have to be drawn before the features – and every later change to the layout genes would shift all the features of all the seeds. The fork makes the two streams independent: the layout can be worked on without anyone getting a new face.

The catalogue is data, not a switch

Which variants exist, how likely they are and where they do not fit – that lives in core/katalog.ts as a list. A switch over categories would be the same information, spread across as many places as there are readers.

nurBei is hard: if the position does not fit, the variant drops out. lieber is soft: if it fits, the weight triples. And if everything drops out, the unfiltered list wins – a category may never be empty, because weighted on an empty table has no answer.

Compatibility comes after the draw

This is why headwear is rarer in the diagram above than its weight says: it is struck afterwards. A cap over a mass of hair gives a bulge at the hairline – only the hat may go over the shell, because its brim covers the hairline, and even it not over braids or a bun, which stick out on top. Eye rings next to rosy cheeks give a rash.

Striking happens after the draw, not before. Filtering the candidates beforehand changes the number of values drawn – and thus shifts every seed as soon as a rule changes. A silent throw that is discarded costs nothing.

The names

A face with a name gets looked at, a face without a name gets skimmed. The syllables are chosen so that they mean nothing in any language anybody here reads – and now and then, in a good one case in five, a third one is added. Names of equal length in a row look like a form.

The names too hang off their own stream (mulberry32(seed · 2246822519), four draws to warm up). They are labels, not identity – but they are just as deterministic as the face, or the same head would be called something different at every redraw.

A name becomes a seed

It works the other way round as well: type in your name and you get your head – the same one, today and a year from now, on every device. The name becomes a number for that, and from there on the number is a seed like any other. The hash is the one the project already has (FNV-1a, see forks), only starting from zero: the name alone should produce the number.

The name must not mean the head. No initial letter picking the skull shape, no ending deciding the skin tone. Names carry origin and gender, and a feature hanging off that would no longer be a die but a prejudice with a formula. The hash destroys exactly that structure – that is its job, not a side effect. Measured against two thousand German given names and surnames: no collision, no clumps in the features, and two names differing in a single letter drift as far apart as two strangers.

Before that the name is brought into one spelling, and whatever coincides there gets the same head: upper and lower case are the same name, doubled spaces count once, and a “ü” made of one character is the same as one made of u + diaeresis. Diacritics do remain, though – “Müller” and “Muller” are two names and should be two faces.

The name labels, the seed carries. The address holds both: ?name=Ada%20Lovelace&kopf=2551461758. If a kopf is there, it wins – the name is never recomputed. Only on the first visit, when a name stands there alone, does the number fall, and the workshop writes it into the address right away. After that the head hangs off what every head hangs off: the seed and the catalogue. And not, on top of that, off the hash and the spelling rule never being touched again.

Two identical names are not a bug but the normal case: two people are called Max Müller and get the same head. Anyone who dislikes that keeps rolling – after the first roll the head is a roll anyway, not a number. Real hash collisions are rare beside that: across a hundred thousand different names, one pair is expected to land on the same seed.