Processing 4 library

processing.waves

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

See all 34 waves ↓

See it in action

Six sketches ship with the library; five are previewed below. Each demonstrates a different facet: shift, morph, wild, samplers in tandem, samplers as velocity, range-mapped fields. 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.

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

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

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 →