Processing 4 library

processing.waves

35 wave shapes. One function call.
Pass a number in, get a number back. Java port of p5.waves.

See all 35 waves ↓

See it in action

Fourteen sketches ship with the library; thirteen are previewed below. Each demonstrates a different facet: shift, morph, wild, samplers in tandem, samplers as velocity, range-mapped fields, parameter play, manual time, colour fields, periodic closure, shift-proof closing rings, phase portraits, 3D volumes. Open them all from Processing 4 > File > Examples > Contributed Libraries > waves, or copy the source.

Live demos below are rendered in the browser by p5.js + p5.waves (JS). The Java code shown is what you paste into Processing to get the same result.

Prefer to compose your own? The p5.waves Lab lets you tune seven scenes live in the browser and export each one as a ready-to-run .pde.

wave_shift

shift · one sampler · range

Filled 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 · sweep

A 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" · period

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.

// 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" · period

One 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 · direction

A 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 · threshold

Two 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 · PGraphics

No 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 · amplitude

Eight 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 · interactive

A 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 ribbons

Time 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 · colour

A 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 · interactive

A 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 · volume

A 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;
}

Get Started

Drop the jar in your Processing sketchbook. Import. Call the function.

import waves.*; void setup() { size(800, 400); } void draw() { background(20); stroke(255); noFill(); WaveOpts o = new WaveOpts() .wave("mountain peaks") .t(millis() / 1000.0f) .amplitude(120); beginShape(); for (int x = 0; x < width; x += 3) { float y = Waves.wave(x, o); vertex(x, height / 2 + y); } endShape(); }
Full guide → Examples → Tune in the Lab → export .pde