Deterministic Randomness in Algorithmic Art
How seeded PRNGs are used for reproducible stochastic outputs in visual artworks.
You want a generative piece to exist for ages. Not “a version you once saw in a tab,” but a specific output you can name, return to, and regenerate later. You want the January render to still be January next year. You want a collector to type a string and see exactly what you saw. You want variation, but you also want repeatability.
Seeded random number generators are the tool for the job. The approach is widely used in games, where gameplay requires stochastic-feeling, yet repeatable effects & outcomes, such as smoke turbulence, recoil, etc. An RNG gives you numbers that look patternless. A seeded PRNG gives you the same patternless numbers again, on demand.
RNG, in one minute
An RNG is a source of numbers that appear unpredictable.
In code, you usually meet it as a function that returns a float in [0, 1]. Call it again, you get another. You can then map those numbers into the space that affect artworks: positions, sizes, angles, palette indices, timing offsets.
In most environments, the default Math.random() (or similar) is a PRNG hidden behind a stable interface. It has internal state you don’t control, and it starts from some seed you didn’t choose.
That is fine for “fresh every refresh.” It will require some sort of export of the generated parameters or rendering to a static version to be able to store a definitive “Piece #13” of some series.
Seeded PRNG: same input, same stream
A PRNG is deterministic. It is a small machine:
- State: a few integers.
- Step: a rule that updates the state.
- Output: a number derived from state.
A seed initializes the state. If the seed is the same, the state starts the same, the steps are the same, and the outputs arrive in the same order.
You can treat a seed as a stable name for a whole run of decisions.
Here is the mental model to keep in your head:
(seed string) -> [mix / hash] -> (PRNG state)
-> next() -> u0,u1,u2...
-> [mapping] -> parameters
-> renderer -> pixels / vectors
Nothing mystical happens between boxes. The “randomness” is the fact that small changes in state produce outputs that look uncorrelated to your eye, once you map them into your visual system.
A simple piece: two circles, twelve months
Imagine a square canvas with two filled circles:
- one red
- one black
Each circle has an (x, y) center and a radius. That’s six numbers.
Now decide that each month of a year gets its own seed string:
- “2026-01”
- “2026-02”
- …
- “2026-12”
For a given seed, you run the PRNG to get six uniform values in [0, 1):
- u0, u1, u2 for the red circle
- u3, u4, u5 for the black circle
Then you map them:
- x = lerp(margin, 1 - margin, u)
- y = lerp(margin, 1 - margin, u)
- r = lerp(minR, maxR, u)
Finally, scale (x, y, r) by the canvas size.
That’s the whole system. Inputs → transforms → outputs.
Input: “2026-01”
Transform: seed-mix → PRNG → six samples → remap ranges
Output: a red circle and a black circle drawn on a square
The seed string is not “metadata.” It is the piece’s address.
If you add a year picker, the interaction is clean: change the year, the seeds change, and you get a different set of twelve images. But within a year, January stays January forever. You can post “2026-09” and know anyone can reproduce the same arrangement.
Notice what you did not do:
- You did not store coordinates.
- You did not store an image.
- You stored a short string and a set of rules.
The piece is the pipeline.
What makes a seed “work” in practice
Many PRNGs accept a 32-bit integer seed. You often want to seed with text: dates, URLs, token IDs, human names.
So you add a small step: mixing.
Mixing takes an arbitrary string and produces one or more integers to initialize the PRNG state. This is usually a non-cryptographic hash or a “string-to-words” stir that spreads differences across bits.
The important property for art: similar seeds should not produce obviously similar outputs. “2026-01” should not look like a slight nudge of “2026-02” unless you want that.
If you do want continuity, don’t rely on “similar seed strings.” Make continuity a transform you control: blend outputs, interpolate parameters, or time-warp a single seeded run.
PRNGs as rule machines: state, period, and “good enough”
A PRNG has a period: how long the output stream can be before it repeats. For visual work, you rarely reach the period. What you care about more is whether early samples have visible structure.
A useful PRNG for art should:
- be fast enough that you don’t think about it
- have no obvious low-dimensional patterns in typical mappings
- be stable across platforms (same seed → same stream)
You can choose the appropriate PRNG for a project according to tradeoffs:
- alea: tiny, fast, easy to seed with strings; common in creative coding.
- xor128 / xorwow / xor4096 / xorshift7: xorshift-family generators; very fast; period varies;quality varies with output function.
- tychei: ChaCha-inspired; generally better mixing; slightly more cost.
- quick (ARC4-like): large period; more state; different performance profile.
For a concrete mental grip, consider a xorshift-style generator. The core idea is simple: a 32-bit or 64-bit integer is the state; each next() scrambles it with XORs and shifts; the result becomes the next state and also produces output.
Rules, not mystery:
- shifting moves bit influence across positions
- XOR mixes bits without carries
- the recurrence creates a long orbit through state space
That simplicity is why these generators are popular in realtime graphics. They also make it obvious where artifacts can come from: if your mapping only looks at a few bits, or you use correlated samples for correlated parameters, you can manufacture patterns.
Two practical constraints follow.
Constraint 1: Don’t waste entropy by accident.
If you do floor(u * 2) a lot, you are turning a rich stream into a coin flip stream. That can be fine, but it should be a choice.
Constraint 2: Decide whether you need independent streams.
If you use a single stream for everything—layout, palette, animation timing—you create hidden coupling. Sometimes that coherence is good. Sometimes it makes pieces feel “same-y.” A common pattern is to derive sub-seeds:
- seed “2026-01:layout”
- seed “2026-01:color”
- seed “2026-01:motion”
Same identity, separate decisions.
Building a seeded PRNG
We start by turning a seed string into 32-bit state.
A PRNG wants bits, not characters. So we build a small mixer: it walks the string, updates a 32-bit accumulator, and produces four different words by salting the same input.
The only requirement here is: small changes to the string should scramble many output bits.
// A small 32-bit string mixer (based on MurmurHash3 finalizers).
// Output is uint32 (0..2^32-1) and stable across JS engines.
function mix32(str, seed = 0) {
let h = (seed ^ str.length) >>> 0;
for (let i = 0; i < str.length; i++) {
h ^= str.charCodeAt(i);
h = Math.imul(h, 0x85ebca6b) >>> 0;
h ^= h >>> 13;
}
// final avalanche
h ^= h >>> 16;
h = Math.imul(h, 0xc2b2ae35) >>> 0;
h ^= h >>> 16;
return h >>> 0;
}
// Derive four 32-bit words from one seed string.
// Using different salts avoids "similar first words" across state lanes.
function seedToState4(seedStr) {
const s0 = mix32(seedStr, 0x243f6a88);
const s1 = mix32(seedStr, 0x85a308d3);
const s2 = mix32(seedStr, 0x13198a2e);
const s3 = mix32(seedStr, 0x03707344);
// xorshift128 requires the state not be all zeros.
// If it happens (rare), nudge one lane.
if ((s0 | s1 | s2 | s3) === 0) return [1, 0, 0, 0];
return [s0, s1, s2, s3];
}
Now the generator itself.
xorshift128 maintains a 128-bit state. Each call to nextUint32() does a small sequence of XOR and shift operations. Those operations are cheap, and they spread bit influence through the state.
The update rule is the “system.” Everything else is packaging.
// Marsaglia xorshift128 (32-bit lanes)
// period 2^128 - 1 (excluding all-zero state).
// Deterministic: same initial state => same output stream.
function makeXorShift128(seedStr) {
let [x, y, z, w] = seedToState4(seedStr);
function nextUint32() {
// Use uint32 arithmetic. >>> 0 forces unsigned 32-bit in JS.
const t = (x ^ (x << 11)) >>> 0;
x = y; y = z; z = w;
w = (w ^ (w >>> 19) ^ (t ^ (t >>> 8))) >>> 0;
return w;
}
// Map uint32 to [0, 1).
// 2^-32 = 1 / 4294967296
function nextFloat() {
return nextUint32() * 2.3283064365386963e-10;
}
// Convenience: integer in [0, n-1] without modulo bias for small n.
// (For n <= 2^24, bias is typically irrelevant in art; keep it simple.)
function int(n) {
return (nextFloat() * n) | 0;
}
// Convenience: float in [a, b)
function range(a, b) {
return a \+ (b - a) * nextFloat();
}
// Expose state optionally for debugging/repro checks.
function getState() { return [x, y, z, w]; }
return { nextUint32, nextFloat, int, range, getState };
}
A minimal usage example, showing “inputs → transforms → outputs”:
- input: a seed string
- transform: PRNG stream
- output: parameters
const rng = makeXorShift128("2026-01");
const red = {
x: rng.range(0.1, 0.9),
y: rng.range(0.1, 0.9),
r: rng.range(0.05, 0.35),
};
const black = {
x: rng.range(0.1, 0.9),
y: rng.range(0.1, 0.9),
r: rng.range(0.05, 0.35),
};
One practical rule: treat the PRNG as a stream you consume in a fixed order. If you add a new random draw earlier in the pipeline, everything after it changes. If you want to evolve a piece without breaking old outputs, split streams with derived seeds (e.g. “2026-01:layout”, “2026-01:color”).
Applying seeds to work
When I first encountered seeding, I used it as a vending machine for variation. I built a 6×3 grid of generative vector shapes. It was reproducible and abstract and empty.
Seeding does not supply meaning. It supplies controllable difference.
A better use is when the output needs a stable identity that still changes on a schedule.
Example: a personal website with seven color themes.
- Input: seed = local date string, e.g. “2026-01-22”
- Transform: seeded PRNG → integer in [0, 6]
- Output: choose a theme class for the
The piece is not “random theme.” The piece is “one outfit per day.” The seed becomes a calendar.
The same pattern scales up:
- daily generative hero image
- weekly typography choices
- per-article illustration variants keyed by URL slug
- token-based generative art keyed by token ID
The design work is deciding what should change and what should stay fixed, then placing those decisions in the pipeline.
Sampling intuition: clumps, coverage, and time-warp
Uniform random samples in 2D clump. Your eye will see clusters and voids and call them “too random.” That’s not a bug in the PRNG; it’s how independent sampling looks.
If you want even coverage, change the transform, not the seed.
Common options:
- Stratify: divide the domain into cells and sample within each. You keep variation but reducevoids.
- Low-discrepancy sequences: not random, but evenly filling. You can still seed by scramblingor choosing offsets.
- Blue-noise sampling: explicit separation constraints; looks “designed” while remainingirregular.
Time is a domain too. If you use PRNG values directly as per-frame noise, you get flicker. Instead, treat random outputs as control points and interpolate, or feed them into a smooth noise function. That is time-warping: turning a discrete stream into continuous motion.
A compact habit: separate event randomness (when something changes) from state randomness (what it becomes).
Libraries: choose your constraints
If you want a pragmatic default for the web:
- seedrandom: convenient string seeding, multiple algorithms; heavier.
- alea: small and fast; good enough for many visual tasks.
Pick based on what you need to keep stable:
- If you need exact reproducibility across machines and years, pin the algorithm and version.
- If you need tiny footprint, pick a small PRNG and own the implementation.
- If you need multiple streams and flexible seeding, a library pays for itself.
A final constraint: a seed is an identifier, not a secret. If a seed controls something sensitive, you need a different system.
Seeded PRNGs are not about “making things random.” They are about making choices replayable. Once you frame them as inputs → transforms → outputs, they become a clean, composable tool—one you can put on a timeline, put in a URL, or print under the frame.