wave_shift
shift · one sampler · rangeFilled ribbons that auto-shift between random wave formulas. Colour flows from red to blue across the strips. The output of one shift-sampler drives the ribbon height per column.
// Wave Shift
// Filled ribbons that auto-shift between random wave formulas.
// Color flows from red to blue across the strips.
import waves.*;
final int STRIPS = 20;
Waves.WaveSampler sampler;
void setup() {
size(460, 460);
noStroke();
textFont(createFont("Consolas", 11));
sampler = Waves.createSampler(new WaveOpts()
.shift(true)
.amplitude(1)
.frequency(0.5f));
}
void draw() {
background(245);
float t = millis() / 1000.0f;
float sw = (float)width / STRIPS;
float cy = height / 2f;
float rowH = height * 0.42f;
for (int i = 0; i < STRIPS; i++) {
float frac = i / (float)(STRIPS - 1);
float r = 255 * (1 - frac);
float b = 255 * frac;
fill(r, 0, b);
float v = sampler.sample(i * 0.4f, t + i * 0.1f);
rect(i * sw, cy - v * rowH, sw - 1, v * rowH * 2);
}
fill(0);
textSize(11);
textAlign(LEFT);
text(sampler.waveName(), 8, 16);
}
morph_wave
morph · two waves · sweepA field of horizontal lines where each row blends two wave formulas. Top rows = pure wobble sine, bottom rows = pure meta sine. The blend point sweeps up and down over time, so you see the shape transform mid-field.
// Morph Wave
// A field of horizontal lines where each row blends two wave formulas.
// Top rows = pure waveA. Bottom rows = pure waveB. Middle = the morph.
// The blend sweeps up and down over time, you see the shape transform.
import waves.*;
final String WAVE_A = "wobble sine";
final String WAVE_B = "meta sine";
final int ROW_COUNT = 50;
float t = 0;
void setup() {
size(460, 460);
}
void draw() {
background(250);
t += 0.0375f;
float centre = (sin(t * 0.3f) + 1) * 0.5f;
float rowH = (float)height / ROW_COUNT;
for (int row = 0; row < ROW_COUNT; row++) {
float rowFrac = row / (float)(ROW_COUNT - 1);
float gap = abs(rowFrac - centre);
float morphMix = constrain(1 - gap * 3, 0, 1);
float yBase = row * rowH + rowH * 0.5f;
float r = lerp(0, 255, morphMix);
float b = lerp(255, 0, morphMix);
noFill();
stroke(r, 0, b);
strokeWeight(1.2f + morphMix * 1.8f);
beginShape();
WaveOpts o = new WaveOpts()
.wave(WAVE_A, WAVE_B)
.mix(morphMix)
.t(t + row * 0.06f)
.frequency(0.08f)
.amplitude(rowH * 2.5f);
for (int x = 0; x < width; x += 3) {
float waveY = Waves.wave(x, o);
vertex(x, yBase + constrain(waveY, -rowH * 0.7f, rowH * 0.7f));
}
endShape();
}
noStroke();
fill(0, 0, 255);
textSize(10);
textFont(createFont("Consolas", 10));
textAlign(LEFT);
text(WAVE_A, 8, 16);
fill(255, 0, 0);
textAlign(RIGHT);
text(WAVE_B, width - 8, height - 8);
}
seamless_closing
shift · group: "closing" · periodA ring sampled with shift normally tears its seam open the moment it lands on a new wave — every formula has its own period. The "closing" pool fixes that: all 19 waves in it share one base period, so a sweep of ring.period() * LOBES closes through every morph. The loop keeps changing shape and never shows a seam.
// Seamless Closing
// A ring sampled with shift normally tears its seam open the moment it
// lands on a new wave — every formula has its own period. The "closing"
// pool fixes that: all 19 waves in it share one base period, so a sweep
// of ring.period() * LOBES closes through every morph. The loop keeps
// changing shape and never shows a seam.
import waves.*;
final int LOBES = 6; // base-period repeats around the ring
final int SEGS = 3000; // dense enough for the shortest closing wave
Waves.WaveSampler ring;
float t = 0;
void setup() {
size(600, 600);
strokeJoin(ROUND);
textFont(createFont("Consolas", 12));
ring = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(3)
.shiftDuration(1.4f)
.group("closing")
.amplitude(1));
}
void draw() {
background(12);
translate(width / 2f, height / 2f);
t += 0.02;
// ring.period() comes straight from the library: 62.8319 for the
// closing pool — stable through every shift. You never type it.
float sweep = ring.period() * LOBES;
float radius = width * 0.28f;
float wobble = width * 0.10f;
// Cyan at rest, warming to pink mid-morph.
color cold = color(60, 200, 255);
color warm = color(255, 80, 120);
stroke(lerpColor(cold, warm, ring.mix()));
strokeWeight(1.6f);
noFill();
beginShape();
for (int i = 0; i < SEGS; i++) {
float frac = i / (float)SEGS;
float angle = frac * TWO_PI;
float r = radius + ring.sample(frac * sweep, t) * wobble;
vertex(cos(angle) * r, sin(angle) * r);
}
endShape(CLOSE);
// HUD
noStroke();
fill(220);
textSize(12);
text(ring.waveName() + (ring.shifting() ? " to " + ring.targetName() : ""),
-width / 2f + 18, -height / 2f + 24);
}
ghost_delay
shift · group: "ghost" · periodOne 1D wave, read against a delayed copy of itself. Plot sample(u) against sample(u + tau) and a single scalar wave closes into a loop ring — its phase portrait. The "ghost" pool is six closing waves hand-picked to stay clean under that pairing, and shift keeps morphing between them, so the loop family never settles.
// Ghost Delay
// One 1D wave, read against a delayed copy of itself.
// x = sample(u), y = sample(u + tau): one scalar wave closes into a loop
// ring — its phase portrait. shift morphs the wave, so the loop family
// keeps changing. The strip along the bottom is that one wave; the dots
// are the two read points.
import waves.*;
final int N = 800;
Waves.WaveSampler sampler;
void setup() {
size(720, 720);
colorMode(HSB, 360, 100, 100, 100);
strokeJoin(ROUND);
textFont(createFont("Consolas", 12));
// "ghost" is a built-in pool of closing waves that stay clean under this
// delay. range(-1, 1) gives unit output (the default would be amplitude 100).
sampler = Waves.createSampler(new WaveOpts()
.group("ghost")
.shift(true)
.range(-1, 1));
}
void draw() {
background(230, 25, 8);
noFill();
float t = millis() / 1000.0f;
float hue = (t * 12) % 360;
float period = sampler.period(); // ghost waves share one period
float tau = period * (0.5f + 0.35f * sin(t * 0.35f)); // the ghost delay, breathing
// The ring: the wave against its own delayed self.
float rad = min(width, height) * 0.36f;
stroke(hue, 55, 100, 90);
strokeWeight(1.4f);
pushMatrix();
translate(width / 2f, height / 2f - 30);
beginShape();
for (int i = 0; i <= N; i++) {
float u = (i / (float)N) * period; // one period -> the ring closes
vertex(sampler.sample(u, t) * rad, sampler.sample(u + tau, t) * rad);
}
endShape(CLOSE);
popMatrix();
drawWaveStrip(t, tau, hue, period);
// shift is cycling the ghost pool; name the current wave.
noStroke();
fill(0, 0, 70);
textSize(12);
text(sampler.waveName(), 18, 26);
}
// The raw 1D wave as a height line, with the two read points (u and u + tau)
// marked. The whole trick is reading this one wave twice.
void drawWaveStrip(float t, float tau, float hue, float period) {
float baseY = height - 60;
float w = width - 120;
float left = 60;
float amp = 24;
noFill();
stroke(hue, 45, 100, 70);
strokeWeight(1.2f);
beginShape();
for (int i = 0; i <= N; i++) {
float u = (i / (float)N) * period;
vertex(left + (i / (float)N) * w, baseY - sampler.sample(u, t) * amp);
}
endShape();
noStroke();
fill(0, 0, 100);
circle(left, baseY - sampler.sample(0, t) * amp, 7);
fill(hue, 70, 100);
circle(left + (tau % period) / period * w, baseY - sampler.sample(tau, t) * amp, 7);
}
flow_fields
ASCII · one sampler · directionA grid of ASCII characters forms a flow field. Each cell's direction comes from a single shift-sampler, like noise, but with structure. The wave shifts every few seconds, so the whole field's character keeps changing.
// Flow Fields
// A grid of ASCII characters forms a flow field.
// Each cell's direction comes from waves. Like noise, but with structure.
// The wave formula shifts automatically every few seconds.
import waves.*;
final int COLS = 30;
final int ROWS = 30;
final String[] DIRS = { "-", "/", "|", "\\" };
Waves.WaveSampler sampler;
void setup() {
size(460, 460);
textFont(createFont("Consolas", 14));
textAlign(CENTER, CENTER);
noStroke();
fill(0);
sampler = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(4)
.shiftDuration(2)
.frequency(2)
.range(-1, 1));
}
void draw() {
background(255);
float t = millis() / 1000.0f;
float sz = (float)width / COLS;
textSize(sz * 0.9f);
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
float val = sampler.sample(col * 0.5f, t + row * 0.4f);
int idx = constrain((int)map(val, -1, 1.001f, 0, 4), 0, 3);
text(DIRS[idx], col * sz + sz / 2, row * sz + sz / 2);
}
}
}
binary_field
two samplers · sum · thresholdTwo independent shift-samplers, summed per cell, thresholded into a 2D pattern. Both samplers shift on their own schedule, so the field's character keeps evolving: sometimes interference, sometimes stripes, sometimes checker.
// Binary Field
// Two samplers, summed per cell, thresholded into a 2D pattern.
// Both samplers shift independently, so the field's character keeps
// evolving. Sometimes interference, sometimes stripes, sometimes checker.
import waves.*;
final int COLS = 30;
final int ROWS = 20;
Waves.WaveSampler rowS, colS;
void setup() {
size(460, 460);
noStroke();
textFont(createFont("Consolas", 11));
rowS = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(4)
.shiftDuration(1)
.range(-1, 1)
.seed(1));
colS = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(4.5f)
.shiftDuration(1.2f)
.range(-1, 1)
.seed(2));
}
void draw() {
background(245);
float t = millis() / 1000.0f;
float labelH = 28;
float cw = (float)width / COLS;
float ch = (height - labelH) / ROWS;
float offset = t * 0.4f;
for (int row = 0; row < ROWS; row++) {
float rv = rowS.sample(row * 5 + offset, t);
for (int col = 0; col < COLS; col++) {
float cv = colS.sample(col * 2.5f - offset, t);
boolean on = (rv + cv) > 0;
fill(on ? 15 : 230);
rect(col * cw, labelH + row * ch, cw - 0.5f, ch - 0.5f);
}
}
fill(40);
textSize(11);
textAlign(LEFT, CENTER);
text(rowS.waveName() + " x " + colS.waveName(), 8, labelH / 2);
}
random_walker
wave as velocity · trails · PGraphicsNo angles, no steps. Wave output IS the velocity. Five trails ride two shift-samplers as raw displacement. When the formulas shift, the movement character transforms entirely. Trails draw onto a PGraphics with low-alpha fade.
// Not So Random Walker
// No angles, no steps. Wave output IS the velocity.
// Five trails ride two shift-samplers as raw displacement.
// When formulas shift, movement character transforms entirely.
import waves.*;
final int WALKERS = 5;
Waves.WaveSampler xWave, yWave;
float[] wx = new float[WALKERS], wy = new float[WALKERS];
float[] prevX = new float[WALKERS], prevY = new float[WALKERS];
float t = 0;
PGraphics trail;
// R, G, B, yellow, purple
final int[][] palette = {
{255, 60, 60},
{60, 220, 60},
{60, 100, 255},
{255, 220, 40},
{180, 60, 255}
};
void setup() {
size(460, 460);
trail = createGraphics(460, 460);
trail.beginDraw();
trail.background(15);
trail.endDraw();
xWave = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(4)
.shiftDuration(1.5f)
.amplitude(2.5f)
.frequency(0.7f)
.seed(0));
yWave = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(5)
.shiftDuration(1.2f)
.amplitude(2.5f)
.frequency(0.55f)
.seed(77));
for (int i = 0; i < WALKERS; i++) {
float a = TWO_PI * i / WALKERS;
wx[i] = width / 2f + cos(a) * 40;
wy[i] = height / 2f + sin(a) * 40;
prevX[i] = wx[i];
prevY[i] = wy[i];
}
}
void draw() {
trail.beginDraw();
trail.noStroke();
trail.fill(15, 15, 15, 8);
trail.rect(0, 0, trail.width, trail.height);
t += 0.025f;
for (int i = 0; i < WALKERS; i++) {
float phase = i * 6.7f;
float vx = xWave.sample(t * 1.8f + phase, t);
float vy = yWave.sample(t * 2.1f + phase * 1.3f, t);
prevX[i] = wx[i];
prevY[i] = wy[i];
wx[i] += vx;
wy[i] += vy;
if (wx[i] < 0) wx[i] += width;
if (wx[i] > width) wx[i] -= width;
if (wy[i] < 0) wy[i] += height;
if (wy[i] > height) wy[i] -= height;
if (abs(wx[i] - prevX[i]) > width / 2) continue;
if (abs(wy[i] - prevY[i]) > height / 2) continue;
int[] col = palette[i];
trail.stroke(col[0], col[1], col[2], 200);
trail.strokeWeight(2.5f);
trail.line(prevX[i], prevY[i], wx[i], wy[i]);
}
trail.endDraw();
image(trail, 0, 0);
noStroke();
fill(255, 255, 255, 120);
textSize(10);
textFont(createFont("Consolas", 10));
textAlign(LEFT);
text(xWave.waveName() + " x " + yWave.waveName(), 8, 16);
}
wave_params
interactive · frequency · amplitudeEight layered wave lines. Mouse X drives frequency, mouse Y drives amplitude, and the formula itself shifts automatically. Drag across the canvas to feel what each parameter does to the shape.
// Shape Parameters
// mouseX = frequency, mouseY = amplitude.
// Wave formula shifts automatically.
// Drag to feel what each parameter does.
import waves.*;
final int LINE_COUNT = 8;
Waves.WaveSampler shiftSampler;
WaveOpts o;
float t = 0;
void setup() {
size(460, 460);
textFont(createFont("Consolas", 10));
// Only used to get shifting wave names
shiftSampler = Waves.createSampler(new WaveOpts()
.shift(true)
.shiftInterval(4)
.shiftDuration(1.5f)
.seed(42));
o = new WaveOpts();
}
void draw() {
background(245);
t += 0.015f;
// Mouse drives parameters
float freq = constrain(map(mouseX, 0, width, 0.15f, 3.5f), 0.15f, 3.5f);
float amp = constrain(map(mouseY, 0, height, 120, 8), 8, 120);
// Ping the sampler to keep shift state updated
shiftSampler.sample(0, t);
String waveName = shiftSampler.waveName();
// Draw layered lines — each offset in phase for depth
for (int i = 0; i < LINE_COUNT; i++) {
float progress = i / (float)(LINE_COUNT - 1);
float phase = i * 0.7f;
float lineAmp = amp * (1 - progress * 0.5f);
float gray = lerp(0, 200, progress);
float weight = lerp(2.5f, 0.8f, progress);
stroke(gray);
strokeWeight(weight);
noFill();
o.wave(waveName)
.t(t)
.amplitude(lineAmp)
.frequency(freq * 0.01f)
.phase(phase);
beginShape();
for (int x = 0; x <= width; x += 3) {
float val = Waves.wave(x, o);
vertex(x, height / 2f + val);
}
endShape();
}
// Labels
noStroke();
fill(0);
textSize(10);
textAlign(LEFT, TOP);
text(waveName, 8, 8);
textAlign(LEFT, BOTTOM);
text("frequency: " + nf(freq, 1, 2), 8, height - 8);
textAlign(RIGHT, BOTTOM);
text("amplitude: " + round(amp), width - 8, height - 8);
// Crosshair
stroke(0, 20);
strokeWeight(0.5f);
line(mouseX, 0, mouseX, height);
line(0, mouseY, width, mouseY);
}
wild_mode
wild mode · unpredictability · interactiveA grid of circles split down the middle. Left half: the wave as written. Right half: the same wave in wild mode. Mouse X is the unpredictability dial — drag right and feel the chaos build.
// Wild Mode
// Grid of circles: left half stable, right half wild.
// mouseX controls unpredictability — drag to feel the chaos build.
import waves.*;
final int COLS = 20;
final int ROWS = 14;
float t = 0;
int wildWave;
WaveOpts stableOpts, wildOpts;
void setup() {
size(460, 460);
noStroke();
textFont(createFont("Consolas", 10));
textAlign(CENTER);
wildWave = (int)random(Waves.count());
float cw = (float)width / COLS;
float ch = (height - 24) / (float)ROWS;
float maxR = min(cw, ch) * 0.44f;
stableOpts = new WaveOpts().wave(wildWave).range(3, maxR);
wildOpts = new WaveOpts().wave(wildWave).range(3, maxR).mode("wild");
}
void draw() {
background(238);
t += 0.012f;
float cw = (float)width / COLS;
float ch = (height - 24) / (float)ROWS;
int half = COLS / 2;
float unpred = constrain(map(mouseX, 0, width, 0, 1), 0, 1);
stableOpts.t(t);
wildOpts.t(t).unpredictability(unpred);
fill(0);
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
float cx = (col + 0.5f) * cw;
float cy = (row + 0.5f) * ch;
float coord = col * 0.15f + row * 0.3f;
WaveOpts o = (col < half) ? stableOpts : wildOpts;
float sz = Waves.wave(coord, o);
circle(cx, cy, sz * 2);
}
}
stroke(0, 30);
strokeWeight(1);
line(width / 2f, 0, width / 2f, height - 24);
noStroke();
fill(0);
textSize(10);
text("stable", width / 4f, height - 6);
text("wild " + nf(unpred, 1, 2), 3 * width / 4f, height - 6);
}
time_strata
manual time · scrub · HSB ribbonsTime is a plain number you pass in — the library never calls a clock. Sixteen filled ribbons, each frozen at its own moment in a 24-second window. Mouse X scrubs through the whole window like reading geological strata.
// Time Strata
// Time is a plain number — full manual control.
// Mouse X scrubs a time window; each layer is frozen at its own t.
// Layers are filled ribbons with HSB color, creating geological strata.
import waves.*;
final int LAYERS = 16;
final float WAVE_WINDOW = 6;
WaveOpts top, bottom;
void setup() {
size(460, 460);
colorMode(HSB, 360, 100, 100, 255);
textFont(createFont("Consolas", 10));
top = new WaveOpts().wave("classic sine").amplitude(25);
bottom = new WaveOpts().wave("classic sine").amplitude(15);
}
void draw() {
background(0, 0, 96);
float timeBase = map(mouseX, 0, width, 0, 24);
noStroke();
for (int i = LAYERS - 1; i >= 0; i--) {
float layerT = timeBase + (i / (float)LAYERS) * WAVE_WINDOW;
float y0 = map(i, 0, LAYERS - 1, 50, height - 50);
float wHue = (i * 22) % 360;
float alphaVal = map(i, 0, LAYERS - 1, 200, 60);
float freq = 0.6f + i * 0.08f;
fill(wHue, 70, 85, alphaVal);
top.t(layerT).frequency(freq);
bottom.t(layerT + 0.3f).frequency(freq);
beginShape();
for (int x = 0; x <= width; x += 3) {
float dy = Waves.wave(x * 0.15f, top);
vertex(x, y0 + dy);
}
for (int x2 = width; x2 >= 0; x2 -= 3) {
float dy2 = Waves.wave(x2 * 0.15f, bottom);
vertex(x2, y0 + dy2 + 22);
}
endShape(CLOSE);
}
// time cursor label
fill(0);
noStroke();
textSize(10);
textAlign(LEFT);
text("t = " + nf(timeBase, 1, 2), 12, 20);
text("move mouse", 12, 34);
}
color_field
six samplers · groups · colourA wave-driven take on random(155) + 100. Three slow base samplers set a per-channel floor; three faster field samplers add 0–100 on top, per cell. Every channel drifts inside a moving 100-wide window — static, but organised.
// Static Field
// A wave-driven take on `random(155) + 100`.
// Three slow base samplers set a per-channel floor in [0, 155].
// Three faster field samplers add 0 to 100 on top, per cell.
// Each channel stays inside a moving 100-wide window that drifts and morphs.
import waves.*;
Waves.WaveSampler baseR, baseG, baseB;
Waves.WaveSampler fieldR, fieldG, fieldB;
void setup() {
size(460, 460);
noStroke();
textFont(createFont("Consolas", 11));
baseR = Waves.createSampler(new WaveOpts()
.shift(true).group("gentle")
.range(0, 155).frequency(0.20f)
.shiftInterval(7).shiftDuration(2));
baseG = Waves.createSampler(new WaveOpts()
.shift(true).group("gentle")
.range(0, 155).frequency(0.17f)
.shiftInterval(8).shiftDuration(2));
baseB = Waves.createSampler(new WaveOpts()
.shift(true).group("gentle")
.range(0, 155).frequency(0.13f)
.shiftInterval(9).shiftDuration(2));
fieldR = Waves.createSampler(new WaveOpts()
.shift(true)
.group(new String[]{ "classic sine", "triangle", "bumpy sine", "mountain peaks", "wobble sine" })
.range(0, 1).frequency(0.08f)
.shiftInterval(4).shiftDuration(1.5f));
fieldG = Waves.createSampler(new WaveOpts()
.shift(true)
.group(new String[]{ "sine", "triangle", "squared sine", "valleys", "round linked sine" })
.range(0, 1).frequency(0.06f)
.shiftInterval(5).shiftDuration(1.5f));
fieldB = Waves.createSampler(new WaveOpts()
.shift(true)
.group(new String[]{ "classic sine", "sharp peaks", "bumpy sine", "half sine", "smooth solid sine" })
.range(0, 1).frequency(0.07f)
.shiftInterval(6).shiftDuration(1.5f));
}
void draw() {
background(245);
int cell = 8;
int top = 30;
float t = millis() / 1000.0f;
int r0 = floor(constrain(baseR.sample(0, t), 0, 155));
int g0 = floor(constrain(baseG.sample(100, t), 0, 155));
int b0 = floor(constrain(baseB.sample(200, t), 0, 155));
for (int y = top; y < height; y += cell) {
for (int x = 0; x < width; x += cell) {
float rp = constrain(fieldR.sample(x + y * 0.35f, t), 0, 1);
float gp = constrain(fieldG.sample(x * 0.3f + y, t), 0, 1);
float bp = constrain(fieldB.sample(x * 0.7f - y * 0.25f, t), 0, 1);
rp = lerp(rp, random(1), 0.15f);
gp = lerp(gp, random(1), 0.15f);
bp = lerp(bp, random(1), 0.15f);
fill(r0 + rp * 100, g0 + gp * 100, b0 + bp * 100);
rect(x, y, cell, cell);
}
}
fill(20);
textSize(11);
textAlign(LEFT, CENTER);
text("R " + r0 + "-" + (r0 + 100) +
" G " + g0 + "-" + (g0 + 100) +
" B " + b0 + "-" + (b0 + 100), 8, 12);
}
spiky_lissajous
periodicity · closed path · interactiveA classic 3:5 Lissajous curve, but sin() is replaced by a spiky wave formula. The pen traces one period-exact cycle and lands back on the start marker — proof that wave(0) == wave(A × period), so even sawtooth paths close. Click the canvas to cycle waves.
// Spiky Lissajous
// A classic a:b Lissajous curve, but sin() is replaced by a spiky
// wave formula. The pen traces the curve over one frozen-phase cycle,
// then lands exactly back on the start marker — proof that even with
// spikes or sawtooth edges, wave(0) == wave(A * period) for integer A,
// so the path closes.
import waves.*;
final String[] SPIKY_NAMES = { "sharp peaks", "batman", "zig-zag sine", "up down pulse" };
final float[] SPIKY_PERIODS = { PI * 10, PI * 20, PI * 10, PI * 10 };
final int[][] SPIKY_COLORS = {
{ 70, 220, 130 },
{ 255, 70, 70 },
{ 60, 160, 255 },
{ 250, 220, 40 }
};
final int RATIO_A = 3;
final int RATIO_B = 5;
final int SEGMENTS = 1400;
final int CYCLE_MS = 4500;
int waveIdx = 0;
float[] xs = new float[SEGMENTS + 1];
float[] ys = new float[SEGMENTS + 1];
WaveOpts o = new WaveOpts().amplitude(1);
void setup() {
size(720, 720);
strokeJoin(ROUND);
noFill();
textFont(createFont("Consolas", 12));
}
void draw() {
background(12);
translate(width / 2f, height / 2f);
String name = SPIKY_NAMES[waveIdx];
float period = SPIKY_PERIODS[waveIdx];
int[] col = SPIKY_COLORS[waveIdx];
float radius = min(width, height) * 0.34f;
// Freeze phase for one cycle so the pen draws a genuine closed loop
// and literally returns to (xs[0], ys[0]) at prog = 1.
int cycleId = millis() / CYCLE_MS;
float frozenMs = cycleId * (float)CYCLE_MS;
float phaseX = frozenMs * 0.00015f;
float phaseY = frozenMs * 0.00021f;
float prog = (millis() % CYCLE_MS) / (float)CYCLE_MS;
int drawnTo = floor(prog * SEGMENTS);
o.wave(name);
for (int i = 0; i <= SEGMENTS; i++) {
float theta = (i / (float)SEGMENTS) * period;
xs[i] = radius * Waves.wave(RATIO_A * theta + phaseX * period, o);
ys[i] = radius * Waves.wave(RATIO_B * theta + phaseY * period + period * 0.25f, o);
}
// Dim ghost of the full closed loop — proves the target doesn't move.
noFill();
stroke(col[0] * 0.22f, col[1] * 0.22f, col[2] * 0.22f);
strokeWeight(1);
beginShape();
for (int i = 0; i <= SEGMENTS; i++) vertex(xs[i], ys[i]);
endShape(CLOSE);
// Bright pen trail up to the current progress.
stroke(col[0], col[1], col[2]);
strokeWeight(1.4f);
beginShape();
for (int i = 0; i <= drawnTo; i++) vertex(xs[i], ys[i]);
endShape();
// Start marker — the "home" the pen must return to.
float homeR = 12 + sin(prog * TWO_PI) * 1.5f;
noStroke();
fill(255); circle(xs[0], ys[0], homeR);
fill(18); circle(xs[0], ys[0], homeR - 6);
// Pen cursor — the moving tip.
fill(255);
circle(xs[drawnTo], ys[drawnTo], 8);
// HUD
noStroke();
fill(220);
textSize(12);
text(name + " / ratio " + RATIO_A + ":" + RATIO_B,
-width / 2f + 18, -height / 2f + 24);
fill(120);
textSize(10);
text("click to cycle wave / the pen returns to the white dot",
-width / 2f + 18, -height / 2f + 42);
}
void mousePressed() {
waveIdx = (waveIdx + 1) % SPIKY_NAMES.length;
}
wave_volume_3d
P3D · three samplers · volumeA 16×16×16 point volume in P3D. Three desynchronised shift-samplers — one per axis — make the glowing surface breathe, drift sideways, and pulse in depth. The volume turns on its own; drag to rotate by hand.
// 3D Wave Volume (P3D)
// 16x16 grid with 3 shift-samplers — one per axis.
// Every axis breathes independently, creating a living 3D volume.
// Drag to rotate by hand; the volume also turns on its own.
import waves.*;
final int N = 16;
final float SPACING = 22;
final float HALF = (N - 1) * SPACING / 2;
Waves.WaveSampler samplerY, samplerX, samplerZ;
float rotAngle = 0;
float tilt = -0.5f;
void setup() {
size(460, 460, P3D);
frameRate(30);
noFill();
strokeWeight(4);
// Three desynchronised shift-samplers — each axis has its own wave life
samplerY = Waves.createSampler(new WaveOpts()
.shift(true).shiftInterval(3).shiftDuration(1.5f)
.range(-6, 6).frequency(0.4f).seed(0));
samplerX = Waves.createSampler(new WaveOpts()
.shift(true).shiftInterval(4).shiftDuration(1.2f)
.range(-3, 3).frequency(0.3f).seed(42));
samplerZ = Waves.createSampler(new WaveOpts()
.shift(true).shiftInterval(5).shiftDuration(1)
.range(-3, 3).frequency(0.35f).seed(77));
}
void draw() {
background(12);
translate(width / 2f, height / 2f);
rotateX(tilt);
rotAngle += 0.005f;
rotateY(rotAngle);
float t = millis() / 1000.0f;
beginShape(POINTS);
for (int xi = 0; xi < N; xi++) {
for (int zi = 0; zi < N; zi++) {
// Y: main surface displacement — full range across the cube
float dy = samplerY.sample(xi * 0.9f + zi * 0.6f, t);
// X: horizontal breathing — points drift sideways
float dx = samplerX.sample(zi * 0.8f + xi * 0.3f, t * 0.85f);
// Z: depth warping — grid pulses in and out
float dz = samplerZ.sample(xi * 0.7f + zi * 0.5f, t * 0.7f);
// Map dy to a grid row and spread points around the surface
float surfaceRow = N / 2f + dy;
float thick = 2.5f;
for (int yi = 0; yi < N; yi++) {
float gap = abs(yi - surfaceRow);
if (gap < thick) {
float glow = 1 - gap / thick;
int bright = round(80 + (yi / (float)(N - 1)) * 175);
stroke(bright, bright, 255, 255 * glow);
vertex(
-HALF + xi * SPACING + dx * SPACING * 0.4f,
-HALF + yi * SPACING,
-HALF + zi * SPACING + dz * SPACING * 0.4f
);
}
}
}
}
endShape();
}
// Drag to rotate the volume by hand
void mouseDragged() {
rotAngle += (mouseX - pmouseX) * 0.01f;
tilt += (mouseY - pmouseY) * 0.01f;
}