Each reusable element is broken into start, build, and end product. Every section includes a live animated preview and a full copyable block that you can paste into a p5 sketch with p5.waves loaded.
The curation engine works when the page stops treating motion as decoration. Pools, samplers, modules, and readouts become one authored system.
start
Stop thinking in single effects
The key shift is asking which family of wave behavior belongs to each layer, instead of asking how to animate one object.
build
Assign roles, not values
Pools curate tone, samplers curate responsibility, and modules reveal the choices in public through layout, type, and proof panels.
end product
One engine, many surfaces
The poster feels designed because motion, structure, and interface all come from the same logic instead of unrelated effects.
// the engine: pool -> role -> module
const sampler = Waves.createSampler({ group: ARRAY_GROUP, shift: true });
const grid = Waves.createGrid(24, 12, { group: 'harsh', threshold: 0.12 });
// each module pulls from one source:
// ribbons - one sampler per row, shift-driven
// panel - grid.sample(t) as a binary readout
// title - typeSampler offsets each letter
// ticker - sampler.waveName / .targetName as labels
// one t drives them all - the composition reads as one piece
This is the editorial backbone. Instead of letting every layer access every wave, you decide which behavior family belongs to which job.
start
Name the voices
gentle is readable, harsh ruptures, all stays broad, and the array pool is hand-picked for taste.
build
Bind color to character
Each group gets a visible identity: a label, a pool, and a color. The selection logic becomes visual direction.
end product
Rotation without chaos
The active pool can change over time, but the sketch still speaks in a controlled voice because each layer stays in its family.
// group narrows the pool that shift/seed picks from
const sampler = Waves.createSampler({
group: 'gentle', // or 'harsh', 'all'
shift: true,
range: [-1, 1]
});
// or pass your own curated list:
// group: ['classic sine', 'mountain peaks', 'meta sine']
// each pool has its own voice
const v = sampler.sample(i * 0.22, t);
Separate samplers are what make the composition feel layered. Motion, color, pulse, and type stop collapsing into one uniform behavior.
start
Define layer jobs
List what the sketch needs: drift, pulse, color selection, and typographic wobble. Those are different roles.
build
Tune each role separately
Readable motion uses gentle, pulse uses harsh, and typography uses the curated array. Each layer gets its own temperament.
end product
Layered behavior
The page gains nuance because not everything responds the same way. The system reads as composed instead of globally animated.
// one sampler per role - motion, color, pulse, type
const motion = Waves.createSampler({ group: 'gentle', shift: true });
const color = Waves.createSampler({ group: 'all', shift: true, range: [0, 1] });
const pulse = Waves.createSampler({ group: 'harsh', shift: true });
const type = Waves.createSampler({ group: ARRAY_GROUP, shift: true });
// each layer responds at its own tempo
const dx = motion.sample(0, t) * 40;
const fillIdx = floor(color.sample(0, t) * shades.length);
const power = pulse.sample(i * 0.18, t); // jagged
const wobbleY = type.sample(i * 0.25, t) * 8; // typographic drift
A line describes. A ribbon divides space. This is where the library starts behaving like layout instead of motion garnish.
start
Sample a line
Use Waves.wave() across the x-axis to get the upper contour of the future form.
build
Close it as a band
Run the lower edge back with a vertical offset. The moment the shape closes, it becomes printable material.
end product
Editorial strips
The final bands split the composition, carry tone, and push energy without abandoning the page structure.
// wave forms the upper edge - close it back as a ribbon
fill('#174cffaa');
beginShape();
for (let x = 0; x <= width; x += 14) {
vertex(x, y + Waves.wave(x * 0.015, {
group: 'gentle', shift: true, t, amplitude: 22
}));
}
for (let x = width; x >= 0; x -= 14) vertex(x, y + 26); // bottom edge
endShape(CLOSE);
The grid module is the proof panel. It gives the sketch a second language beside the hero layer and makes the internal logic visible.
start
Create a binary field
Use createGrid() with a chosen group so the panel already speaks in the same temperament as the rest of the sketch.
build
Frame it as a module
Render a bordered panel, draw the cells, and label the group. The output becomes a visible instrument instead of hidden logic.
end product
Proof beside the hero
The panel stabilizes the poster by pairing expressive bands with a rational readout surface.
// createGrid - a binary 2D field driven by two waves
const g = Waves.createGrid(24, 12, {
group: 'harsh',
threshold: 0.12 // cells where waveRow(row) + waveCol(col) > 0.12 = on
});
// per frame: read all cells in one call
const cells = g.sample(t);
for (let row = 0; row < g.rows; row++) {
for (let col = 0; col < g.cols; col++) {
const on = cells[row * g.cols + col] === 1;
fill(on ? '#ff3b2f' : '#ece5d7');
rect(col * cw, row * ch, cw, ch);
}
}
The type works because the movement is sampled and constrained. The word stays typographic first, live second.
start
Keep the word intact
Draw the title letter by letter while preserving the baseline and spacing logic of the word.
build
Sample each letter
Offset every glyph with the same sampler at a different sample position so the letters move as a family.
end product
Alive, not unstable
The final word drifts just enough to show life without breaking its own legibility.
// one sampler offsets each letter at a different sample position
const type = Waves.createSampler({ group: ARRAY_GROUP, shift: true });
textSize(110);
let cursor = x;
for (let i = 0; i < word.length; i++) {
const wobble = type.sample(i * 0.24, t) * 6;
text(word[i], cursor, y + wobble);
cursor += textWidth(word[i]) * 0.97;
}
// the word stays typographic first, alive second
The strip turns the sketch into an instrument. It translates sampler state into a graphic surface with live labels and progress.
start
Read the sampler
Sample first so waveName, targetName, mix, and shifting are current.
build
Translate state into UI
Bar fill becomes mix, labels expose the current and target wave, and the group color keeps the strip tied to the active pool.
end product
Readable instrument panel
The composition explains itself without becoming a dashboard. The logic is present, but still graphic.
// the sampler exposes its own state - render it as composition
sampler.sample(0, t); // pump state
const mixBar = sampler.shifting ? sampler.mix : 0;
fill('#080808'); // track
rect(44, 92, 560, 32);
fill(activeColor); // fill = morph progress
rect(44, 92, 560 * mixBar, 32);
text('GROUP / ' + sampler.waveName, 56, 113);
text('NEXT ' + sampler.targetName, 430, 113);
The background field stays useful because it is still attached to the same engine as the hero layer. It adds context without turning generic.
start
Choose a small vocabulary
Use words that belong to the library itself: wave names, behaviors, parameters, and descriptive tags.
build
Drive drift and visibility
Motion moves the words, pulse decides whether they appear, and type drift keeps them from freezing in place.
end product
Context without clutter
The words become ghost labels and marginalia behind the hero statement, not filler sprinkled on top.
// pulse gates visibility, motion drifts position, type wobbles
const a = pulse.sample(seed * 0.13, t);
if (a < 0.34) continue; // below threshold = invisible this frame
const dx = motion.sample(seed * 0.31, t) * 22;
const dy = type.sample(seed * 0.19, t + 1.5) * 16;
fill(0, map(a, 0.34, 1, 40, 150));
text(word, x + dx, y + dy);
The complete composition, live. Every module above runs together here.