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.
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.
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.
| Stream | Where | For what |
|---|---|---|
'layout' | core/dna.ts | the four layout genes |
'handschrift' | styles/klar/index.ts | the 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.ts | the teeth of the hairline |
'schlaefenfall' | schale.ts | how far the hair falls at the temple |
'afroBeulen' | schale.ts | the three bumps that turn the sphere into hair |
'afroSorte' | afro.ts | which of the six archetypes is drawn |
'afroRauung' | afro.ts | the fine roughening of the edge |
'afroFransen' | afro.ts | the fringes |
'afroKringel' | afro.ts | the curls inside the mass |
'straehnen' | masse.ts | which strand is erased and which gets its dark twin |
'locken' | haar.ts | the coils at the edge of the curl cloud |
'dutt' | haar.ts | the knot of the bun |
'fuelle' | haar.ts | the fullness of the hairdo |
'hutform' | hut.ts | the kind of hat and its measurements |
'feder' | app/blick.ts | stiffness, breath, blink, chatter |
'name' | app/namen.ts | the syllables under the head |
'marke' | app/logo.ts | the typesetting errors above the sheet |
'kopfform#3' | core/wurf.ts | the workshop's third roll on the skull shape; likewise 'proportionen#n', 'haltung#n', 'farben#n' |
'hand:haar:igel' | core/wurf.ts | the 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.