How the poster machine works
This is the workshop manual: the seeded random number generator, the composition algorithm's weighting rules, and the trick that turns a live SVG into a file on your disk. Everything here runs in your browser in plain JavaScript — about 300 lines, no frameworks, no canvas, no server.
Das Konzept
HAUS is a fictional "digital workshop" honoring the 1919 Bauhaus. Visitors compose posters from the movement's vocabulary — circle, square, triangle, bar, arc, halftone dot field, diagonal type — while the site teaches the principles the school actually taught: form follows function, Kandinsky's triad, asymmetric balance.
The aesthetic school is the subject itself. Warm paper white (#f4f1ea), the sacred triad — red #be1e2d, blue #21409a, yellow #ffd500 — plus black. Hard geometry, thick rules, 45° type. No gradients anywhere on the site, and no shadow softer than a hard offset: every box-shadow is Npx Npx 0, a flat plate of ink, the way a letterpress would print it.
NR. 1919 · rendered live by this page
The poster at left is not an image. It is the same HAUS.render() call the main page uses, executed with seed 1919 as this manual loaded. Reload — it will be identical, atom for atom. That determinism is the whole machine: a poster is just five numbers and a word.
{ seed: 1919, density: 6, palette: 'triad',
comp: 'diagonal', title: 'AUSSTELLUNG' }
Die Saat — the seeded PRNG
Math.random() can never repeat itself, which makes it useless for a poster archive. HAUS uses mulberry32, a tiny 32-bit generator: give it the same seed and it produces the same infinite stream of "random" numbers, on every machine, forever.
function mulberry32(a) {
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
On top of that single stream sits a small toolkit — range(a,b), pick(arr), chance(p), shuffle(arr), weighted(pairs) — so the composition code reads like design decisions, not arithmetic. One rule keeps it deterministic: every random decision must come from this stream, in the same order, every render. That is why clicking a poster on the wall reconstructs it perfectly: the wall never stores pictures, only seeds.
Der Algorithmus
Pure randomness looks like spilled confetti. The machine feels "designed" because it enforces four editorial rules before any dice are rolled:
1 · One dominant element, anchored on a third
Every poster gets exactly one XL element — a giant circle (radius 118–168 units on a 420-wide canvas) or a full-bleed diagonal bar — anchored on a rule-of-thirds intersection, never the center. Everything else is a counterweight to it.
2 · A strict size hierarchy
Of the remaining 2–8 elements (the density slider), the first two or three are Medium (88–136 units) and the rest are Small (30–62). Three distinct size classes is what separates a composition from a scatter.
3 · Weighted vocabulary
Shapes are drawn from weighted decks, not uniform picks — circles lead, arcs and rings season:
| Shape | M weight | S weight |
|---|---|---|
| Circle | 3.0 | 2.4 |
| Square | 2.2 | 2.0 |
| Triangle | 2.2 | 2.0 |
| Bar | — | 1.8 |
| Arc | 1.6 | 1.2 |
| Ring | 1.6 | 1.2 |
A halftone dot field (4–6 × 5–7 dots) may replace one Small slot when density ≥ 5, at 40% probability, once per poster — the print-shop texture, rationed like a spice.
4 · A balanced color deck
Colors are dealt from whole shuffled palettes concatenated together, so a six-element triad poster can never come out all-red:
function colorDeck(rng, names, n) {
let deck = [];
while (deck.length < n) deck = deck.concat(rng.shuffle(names));
return deck.slice(0, n); // every color appears before any repeats
}
Placement then follows the chosen composition: Raster snaps to a 3×4 cell grid with ±9 units of jitter; Diagonale spaces elements along a corner-to-corner band with perpendicular scatter, everything rotated 45°; Radial rings them around the dominant element's focal point at stepped angles. Overlap is allowed on purpose — the draw order (XL first, type last) makes it read as overprinting, not collision.
The type, and the keyline trick
The title is set in Archivo Black, diagonal or vertical, sized to fit its own length: fontSize = min(cap, available / (chars × 0.84)). Because black type can land on a black square, every title carries an invisible bodyguard — a paper-colored stroke painted under the fill:
<text fill="#141414" stroke="#f4f1ea"
stroke-width="3" paint-order="stroke">BÜHNE</text>
paint-order="stroke" draws the outline first, so the letterform stays crisp. On paper the halo is invisible; over ink it becomes a knockout keyline — exactly how 1920s printers kept type legible over solids.
Der Druck — downloading the SVG
The poster already is SVG markup, so "download" is just wrapping the same string in a standalone document and handing it to the browser as a Blob:
const svg = '<?xml version="1.0" encoding="UTF-8"?>' +
'<svg xmlns="http://www.w3.org/2000/svg" ' +
'viewBox="0 0 420 594" width="840" height="1188">' +
render(state) + '</svg>';
const blob = new Blob([svg], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'haus-' + state.seed + '.svg'; // e.g. haus-1919.svg
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
No server, no canvas rasterization, no quality loss — the file is infinitely scalable vector, small enough to email, and named after its seed so it can always be regenerated.
Wie es gebaut wurde
This site was built by Claude (Fable 5) writing vanilla HTML, CSS, and JavaScript by hand — no frameworks, no build step, no libraries. The generator lives in one plain script (assets/haus.js); the page wiring is an IIFE at the bottom of the document. During construction, every wall poster was rendered headlessly and inspected as an image before its seed was allowed into the archive.
It is one room of a 25-site showcase demonstrating what Fable 5 can do in web design — 25 wildly different aesthetics, all hand-written. Visit the hub: fable-25-dhb.pages.dev.
Nimm das mit — steal this
- Seed everything
Swap
Math.random()for mulberry32 in any generative work. Reproducibility turns "random output" into "an archive with addresses" — shareable, debuggable, curatable. - Hierarchy before randomness
One XL, a few M, the rest S. Decide the size classes first and let chance fill in the details. This single rule fixes 80% of "generative art looks like noise."
- Deal colors, don't pick them
Concatenate shuffled copies of your palette and deal from the top. Perfect distribution, zero balancing code.
- paint-order: stroke is a free legibility layer
A background-colored stroke under any SVG text guarantees contrast over unpredictable backgrounds — generated art, photos, charts.
- Hard shadows are a style
box-shadow: 8px 8px 0 #141414plus a 3px border reads as print, not UI. Commit to it site-wide and the whole page snaps into one visual voice.