Each thumbnail is a live sketch. Click -> full example for the code and full-size canvas.
All 35 waves. Morph, shift, wild mode. Try everything.
One call returns one number. Use it for size, position, color - anything.
// One number - use it for anything
const v = Waves.wave(i * 0.3, {
wave: 'up down noise', t: t, range: [0, 1]
});
// Row 1: size
circle(x, y, v * 30);
// Row 2: position
circle(x, baseY + v * rowH * 0.8, 8);
// Row 3: color (pure red -> pure blue)
fill(255 - v * 255, 0, v * 255);
circle(x, y, 14);
The wave changes automatically every few seconds. Smooth morph between random formulas. Watch the ribbons transform.
const sampler = Waves.createSampler({
shift: true,
amplitude: 1,
frequency: 0.5
});
// Filled ribbons - color shifts with the wave
const t = millis() / 1000;
for (let i = 0; i < STRIPS; i++) {
const frac = i / (STRIPS - 1);
fill(255 - frac * 255, 0, frac * 255);
const v = sampler.sample(i * 0.4, t + i * 0.1);
rect(i * sw, cy - v * rowH, sw - 1, v * rowH * 2);
}
Move your mouse over the canvas. X = frequency, Y = amplitude. The formula shifts automatically - drag to feel the difference.
// mouseX = frequency, mouseY = amplitude
let freq = map(mouseX, 0, width, 0.15, 3.5);
let amp = map(mouseY, 0, height, 120, 8);
Waves.wave(x, {
wave: waveName, t: t,
amplitude: amp,
frequency: freq * 0.01
});
Left: order. Right: chaos. Same wave, one flag added. The difference speaks for itself.
// Left: stable circles. Right: wild circles.
// Stable - orderly pattern
const sz = Waves.wave(col + row * 0.5, {
wave: waveIdx, t: t, range: [4, maxR]
});
// Wild - same wave, chaos added
const szW = Waves.wave(col + row * 0.5, {
wave: waveIdx, t: t, range: [4, maxR],
mode: 'wild', unpredictability: 0.7
});
A grid of / - | \ characters. One sampler picks the direction per cell. Not unlike noise() and noiseSeed(), but with unexpected wave shifts every few seconds.
// ASCII flow field - sampler with shift
const DIRS = ['-', '/', '|', '\\'];
let sampler = createWaveSampler({
shift: true,
shiftInterval: 4,
shiftDuration: 2,
frequency: 2
});
// In draw():
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
let val = sampler.sample(col * 0.5, t + row * 0.4);
let idx = constrain(floor(map(val, -1, 1.001, 0, 4)), 0, 3);
text(DIRS[idx], col * sz + sz / 2, row * sz + sz / 2);
}
}
No built-in clock. Pass any number as t. Move the mouse to scrub through time. Freeze it, rewind it, speed it up.
// No clock - t is whatever you pass
// Move mouse to scrub through time
const t = map(mouseX, 0, width, 0, 24);
for (let i = 0; i < 12; i++) {
const layerT = t + (i / 12) * 6;
beginShape();
for (let x = 0; x <= width; x += 4) {
const dy = Waves.wave(x * 0.15, {
wave: strataWave, t: layerT,
amplitude: 15
});
vertex(x, y0 + dy);
}
endShape();
}
A wave-driven take on random(155) + 100. A slow base sampler sets a floor in [0, 155], a faster field sampler adds 0..100 per cell. Per channel.
// Slow base + fast field, per channel (R/G/B)
const baseR = Waves.createSampler({
shift: true, group: 'gentle',
range: [0, 155], frequency: 0.20,
shiftInterval: 7, shiftDuration: 2
});
const fieldR = Waves.createSampler({
shift: true,
group: ['classic sine', 'triangle', 'bumpy sine',
'mountain peaks', 'wobble sine'],
range: [0, 1], frequency: 0.08,
shiftInterval: 4, shiftDuration: 1.5
});
// ...baseG/fieldG, baseB/fieldB with own settings
// In draw: floor + field offset (0..100), per channel
const r0 = floor(baseR.sample(0, t));
fill(r0 + fieldR.sample(x + y*0.35, t) * 100,
g0 + fieldG.sample(x*0.3 + y, t) * 100,
b0 + fieldB.sample(x*0.7 - y*0.25, t) * 100);
Blend two wave formulas together. mix: 0 = pure A, mix: 1 = pure B. Animate it and the shape smoothly transforms.
// Blend two formulas with mix
const wA = floor(random(35));
const wB = floor(random(35));
// mix: 0 = pure A, mix: 1 = pure B
const morphMix = (sin(t * 0.3) + 1) * 0.5;
const wv = Waves.wave(x, {
wave: [wA, wB], // morph between two waves
mix: morphMix,
t: t + row * 0.06,
frequency: 0.08,
amplitude: rowH * 0.8
});
Classic Lissajous with sin swapped for a spiky wave. It still closes cleanly because θ sweeps one full period and the ratio is integer.
// Swap sin() for a spiky wave - curve still closes.
// A pen sweeps one cycle, then lands back on the start dot.
const A = 3, B = 5;
const prog = (millis() % CYCLE_MS) / CYCLE_MS;
const drawnTo = floor(prog * N);
for (let i = 0; i <= N; i++) {
const theta = (i / N) * wave.period;
xs[i] = r * Waves.wave(A * theta, { wave: wave.name, amplitude: 1 });
ys[i] = r * Waves.wave(B * theta + wave.period * 0.25, {
wave: wave.name, amplitude: 1
});
}
beginShape();
for (let i = 0; i <= drawnTo; i++) vertex(xs[i], ys[i]);
endShape();
// Home dot + pen tip. When drawnTo === N, they overlap exactly.
fill(255); circle(xs[0], ys[0], 10);
fill(18); circle(xs[0], ys[0], 5);
fill(255); circle(xs[drawnTo], ys[drawnTo], 6);
A height wave y = f(x) can never loop - one x would need many y. But plot a single wave against a delayed copy of itself, x = wave(u) and y = wave(u + τ), and the pair closes into a loop ring. Sweep the delay and it breathes from ellipse to petals.
// The delayed wave IS a p5.waves sampler - shift morphs it
// through the 'ghost' pool, so the loop family evolves.
const s = Waves.createSampler({
group: 'ghost', // built-in pool tuned for this
shift: true, range: [-1, 1] // range:[-1,1] -> unit output (default is 100)
});
// The sampler reports its own period - no 2*PI/0.1 to hardcode.
const period = s.period;
const delay = period * (0.5 + 0.35 * sin(t * 0.35)); // the ghost delay
for (let i = 0; i <= N; i++) {
const u = (i / N) * period; // one period -> ring closes
xs[i] = r * s.sample(u, t); // x = wave(u)
ys[i] = r * s.sample(u + delay, t); // y = same wave, delayed
}
beginShape();
for (let i = 0; i <= N; i++) vertex(xs[i], ys[i]);
endShape(CLOSE);
No angles, no steps. The wave output is the velocity. Five trails ride two shift-samplers as raw displacement, so when the formulas shift the whole movement character transforms.
// Wave output IS the velocity - no angle, no step.
const xWave = Waves.createSampler({
shift: true, shiftInterval: 4,
amplitude: 2.5, frequency: 0.7, seed: 0
});
const yWave = Waves.createSampler({
shift: true, shiftInterval: 5,
amplitude: 2.5, frequency: 0.55, seed: 77
});
// Per walker, per frame: one sample = one step of displacement
for (let i = 0; i < WALKERS; i++) {
const phase = i * 6.7;
const vx = xWave.sample(t * 1.8 + phase, t);
const vy = yWave.sample(t * 2.1 + phase * 1.3, t);
wx[i] += vx;
wy[i] += vy;
trail.line(prevX[i], prevY[i], wx[i], wy[i]);
}
Two samplers - one for rows, one for columns. Sum them per cell. Threshold the result. The whole 2D pattern from one nested loop.
// Two samplers, one for each axis
const rowS = Waves.createSampler({ wave: 'smooth solid sine', range: [-1, 1] });
const colS = Waves.createSampler({ wave: 'ramp up sine', range: [-1, 1] });
// Per frame: nested loop, sum, threshold
let t = millis() / 1000;
for (let row = 0; row < rows; row++) {
let rv = rowS.sample(row * 5 + t); // x-step ~ wave period
for (let col = 0; col < cols; col++) {
let cv = colS.sample(col * 2.5 - t);
fill((rv + cv) > 0 ? 15 : 225); // 1 = black, 0 = light
rect(col * cw, row * ch, cw, ch);
}
}
Three samplers - one per axis - each shifting on its own cycle. Points near the surface glow.
// Three shift-samplers - one per axis
const sY = Waves.createSampler({
shift: true, shiftInterval: 3,
range: [-5, 5], frequency: 0.4
});
const sX = Waves.createSampler({
shift: true, shiftInterval: 4,
range: [-2.5, 2.5], frequency: 0.3
});
const sZ = Waves.createSampler({
shift: true, shiftInterval: 5,
range: [-2.5, 2.5], frequency: 0.35
});
// Points near the surface glow
const dy = sY.sample(xi * 0.9 + zi * 0.6, t);