Command Palette

Search for a command to run...

Hectal
Shots in this scene
SHOT 17.3Advanced ~24 min · shot 3 of 6

Noise and Generative Motion

In one line

Math.random() jumps from one value to the next, so it makes jitter, not drift. Value noise blends random values on a grid into a smooth, repeatable wander. This topic builds seeded random numbers and 1D noise by hand and fixes the seam that breaks a looping animation.

0/6 · 0%

Think of it like this

A book of random numbers. If you open it at page 42 and read, you always read the same numbers, so two people can draw the same picture. And a hilly road: its height is written down at every milestone, and between two milestones the road rises or falls smoothly. The book is a seeded random generator. The road is noise.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

seed
The starting number of a seeded generator. The same seed gives the same sequence on every machine.
noise
A smooth random function: nearby inputs give nearby outputs, unlike Math.random().
hash
A function that scrambles a number into another that looks random but is always the same for the same input.
smoothstep
The curve t × t × (3 − 2t): it starts and ends flat, so blending with it leaves no corners.
period
The distance after which a repeating function starts again. Wrapping noise with a period makes it loop.
generative
Made by rules and random numbers instead of drawn by hand. The same code and seed gives the same picture.

Step by step

01Read the seeds

"Same seed, same drawing" prints seed 42 twice with identical numbers and seed 43 once. Then it draws 40 dots from seed 42. Press New seed: the label says seed 43 and the dots change.

02Look at noise at the grid

"Noise by hand" first proves that noise(i) equals hash(i, 1) at 0, 1 and 2: the random values sit on whole numbers and everything between is blending.

03Check the smoothness

The third and fourth lines say the biggest change in a 0.01 step is 0.0108, and the slope is 0.000 on both sides of x = 1. Replace smooth by (t) => t and the slopes become −0.721 and 0.397.

04Find the seam

The last two lines compare a plain loop of 4 (0.7750 against 0.1741) with the periodic one (0.7750 against 0.7750). The wandering dot uses the periodic version, so it never jumps when its 8 seconds restart.

05Read the picture

The orange dot and its blue trail wander on a screen that is 300 by 100. Pause and step: each step moves the dot a little, never far.

Try it yourself

  1. 1

    Change the seed

    In "Same seed, same drawing" change the third generator to mulberry32(42.5). The third line prints the same numbers as seed 42 (0.6011 0.4483 0.8525 0.6697): the fraction is dropped.

  2. 2

    Make the corners visible

    In "Noise by hand" change the smoothstep line to const smooth = (t) => t;. The slope line prints −0.721 and 0.397, and the dot's path now turns sharply at the grid points, which happen every 2 seconds at this speed.

  3. 3

    Give x and y the same noise

    Change noise(u + 50, 2, 4) to noise(u, 1, 4). Now x and y are the same number, so the dot slides back and forth on one diagonal line instead of wandering.

Code & diagrams

Same seed, same drawing

A seeded generator makes the same list for the same seed, so the same seed draws the same 40 dots. The generator is a closure: each call moves it one step along.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

seed 42: 0.6011 0.4483 0.8525 0.6697
seed 42 again: 0.6011 0.4483 0.8525 0.6697
seed 43: 0.9998 0.2764 0.5294 0.0591
always in [0, 1): true
Noise by hand

hash, smoothstep and value noise in 15 lines. The numbers show the slope at a whole number and the seam of a loop, with and without a period. The dot wanders with periodic noise, so it does not jump every 8 s.

Press Run to see the page.
00:00.000
animations
0.00s0.10s0.20s0.30s0.40s0.50s0.60s0.70s0.80s0.90s

Nothing is moving yet. Hover, click or scroll the page: every animation it starts shows up here as a track.

frames

Console

Nothing logged yet.

Expected console output

noise at 0, 1, 2 equals the hash: true
noise at 0.5: 0.4142
biggest change in 0.01 of x: 0.0108
slope just before x = 1: 0.000, just after: 0.000
loop of 4, plain: noise(0) = 0.7750, noise(4) = 0.1741 (a jump of 0.6008)
loop of 4, periodic: noise(0) = 0.7750, noise(4) = 0.7750

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Loop without a period

In "Noise by hand" change noise(u, 1, 4) in the frame function to noise(u, 1).

terminal
$ Watch the dot for 8 seconds, or scrub across the loop point
── what you'll see ──
Once every 8 s the dot's x jumps by 0.6008 × 260, about 156 px, in one frame (plain noise(0) = 0.7750, noise(4) = 0.1741).

Break #2

Use Math.random() for every frame

Replace the x and y of the dot with 20 + Math.random() * 260 and 10 + Math.random() * 80.

terminal
$ Press Step a few times
── what you'll see ──
The dot jumps to an unrelated place on every frame: two random numbers are on average a third of the range apart (against at most 0.0108 per 0.01 step for noise). It is jitter, not drift, and the run cannot be repeated.

Myth vs fact

Myth

Seeded random numbers are less random, so they look worse.

Fact

To the eye, a good seeded generator looks the same as Math.random(). What you gain is repeatability: tests, shared pictures, and bug reports you can reproduce.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    Real generative art stacks several noise layers, each twice as fast and half as strong (called octaves), to get detail at many scales. It costs one noise() call per layer, so a canvas of thousands of dots reading four octaves each is a measurable cost: measure before you add layers.

  • ▸

    Gradient noise (Perlin) stores random directions on the grid instead of random values. It has no flat spots at the grid points, which value noise does have, so it looks less blocky. Value noise is the simpler thing to read and build by hand.

  • ▸

    A seed is also a good thing to log. When someone reports that a generated picture looks wrong, seed 91827 lets you draw exactly that picture.

Remember this

  1. 1

    The problem this solves: a dot that takes a new Math.random() position every frame jitters, because each value has nothing to do with the last. Real things drift: a firefly, clouds, a background that breathes. Also, Math.random() cannot be repeated, so you can neither reproduce a bug nor share a picture. Seeded random numbers fix the second problem and noise fixes the first.

  2. 2

    Seeded random numbers. mulberry32(seed) is a small function that returns a generator: call it, get a number from 0 up to (not including) 1, call it again, get the next. The same seed always gives the same list. Measured: seed 42 gives 0.6011, 0.4483, 0.8525, 0.6697; seed 43 gives 0.9998, 0.2764, 0.5294, 0.0591. In "Same seed, same drawing" the 40 dots are the same each time you run it, and New seed draws a different set. Two details: it takes a whole number, so seed 42.5 gave the very same list as seed 42 in our run, and it is for pictures, never for passwords or tokens.

  3. 3

    A stateless hash. For noise you must ask for the random value at any whole number, in any order, so you cannot use a generator that has a memory. hash(i, seed) mixes the integer and the seed with multiplications and shifts and returns a number from 0 to 1 that depends on nothing else. noise(0) and noise(1) are just hash(0) and hash(1).

  4. 4

    Value noise. Put a random value on every whole number. For a point between two of them, blend the two values by how far along you are, t. A straight blend is continuous but has corners at every whole number: the speed changes suddenly. Measured with a straight blend, the slope just before x = 1 was −0.721 and just after was 0.397. The S-curve t × t × (3 − 2t) (smoothstep) has zero slope at both ends: with it both were 0.000, so the motion has no corners. With smoothstep, the biggest change over a step of 0.01 was 0.0108; noise(0.5) was 0.4142.

  5. 5

    Using it. Read the noise at a position that moves slowly with time, noise(t × 0.5), and the value drifts. For a point that wanders in two dimensions use two different places (here an offset of 50 and a different seed), or the dot would slide along a straight diagonal, because x and y would be the same number. The speed of the wander is how fast you walk along the noise axis, and its size is what you multiply the 0-to-1 result by.

  6. 6

    The seam in a loop. Noise does not repeat. A background that must loop every 8 seconds jumps when it starts over: plain noise(0) was 0.7750 and noise(4) was 0.1741, a jump of 0.6008. Wrap the whole-number positions with % period, so the last grid value is the first one: with a period of 4, both were 0.7750. The period argument of noise() does that. If the user asks for reduced motion, replace the drift with one chosen pose (a fixed seed's first value) rather than a smaller drift.

Explain it without notes

01

What is the difference between Math.random() and noise, and when do you want each?

02

Why does value noise use smoothstep instead of a straight blend?

03

Why does a looping noise animation jump at the loop point, and how do you fix it?

Practice

01

Write noise2(x, y, seed) by blending four corner values.

02

Make "Same seed, same drawing" draw the same picture for a seed taken from the page address (#seed=7), and fall back to 42.

Done when you can

  • You can make repeatable random numbers with a seed and say where not to use them

  • You can build 1D value noise from a hash and smoothstep

  • You can explain why noise gives drift and Math.random() gives jitter

  • You can remove the seam from a looping noise animation with a period