Guide

Everything you need to use processing.waves in Processing 4. The API mirrors p5.waves v3.5.0, with Java fluent builders in place of JS object literals.

Install

processing.waves is a single jar with no runtime dependencies. Two routes today; a third (Contribution Manager) will be added in a future release.

Route A: Manual copy

  1. Find your sketchbook in Processing 4: File > Preferences > Sketchbook location.

  2. Download waves.zip from the latest GitHub release.

  3. Unzip into <sketchbook>/libraries/. You should end up with:

    <sketchbook>/libraries/waves/
      library.properties
      library/waves.jar
      src/waves/*.java
      examples/*/.pde
  4. Restart Processing. Verify with Sketch > Import Library > waves.

Route B: Build from source (Windows)

With JDK 17+ on PATH and Processing 4 installed at the default location:

# Clone, build, and install in one shot
git clone https://github.com/seb-prjcts-be/processing.waves
cd processing.waves
./build.ps1 -Install

build.ps1 reads sketchbook.path.four from Processing's preferences file and copies the library where Processing expects it.

Java version: requires JDK 17+ at build time. The compiled jar uses --release 17 which is compatible with Processing 4's bundled JVM.

Platform support: tested on Windows. The compiled jar is pure Java with no native dependencies, so it should run on macOS and Linux without changes, but neither has been tested yet. If you hit a platform-specific issue, please open an issue.

Your first sketch

Open Processing 4, paste, run.

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

Three ways to call it

1. Lazy: Waves.wave(y)

One argument. You get back a deterministic wave value from seed 0 with default amplitude 100. Same input, same output, every run.

float v = Waves.wave(x);   // amplitude 100, wave picked by seed 0

2. Quick: by name or seed

float a = Waves.wave(x, "mountain peaks");  // by name
float b = Waves.wave(x, 42);                // by seed (deterministic pick)

3. Full control: WaveOpts

The fluent builder. Every option is chainable.

WaveOpts o = new WaveOpts()
  .wave("bumpy sine")
  .amplitude(80)
  .frequency(1.0f)
  .t(millis() / 1000.0f)
  .phase(0)
  .seed(7);

float y = Waves.wave(x, o);

Options

Every field on WaveOpts, with its type, default, and what it does.

Start here

MethodTypeDefaultDescription
.wave(name)StringnullOne of the 35 names. null → pick by seed.
.wave(idx)intPick by index (0…34).
.amplitude(v)float100Output is scaled to [−amp, +amp].
.frequency(v)float1Multiplier on x. On sine-based waves that reads as faster cycles — but not on all of them, see below.
.t(v)float0Time. Anything that produces motion uses this.
.seed(v)int0Picks the wave when .wave() is null, and seeds the PRNG for harsh waves.

Frequency is a zoom, not a speed dial. It multiplies x and nothing else, so what you get depends on where x sits in the formula. classic sine is sin(x*.1), which becomes sin(2x*.1) — genuinely faster. But shake out is a mirrored log chirp, and log(2x) = log(x) + log(2): a logarithm turns multiplying into adding, so frequency slides the shake sideways (and tightens the period) rather than speeding the whole wave up evenly. And half sine (sin(x*.05)*(x*.1%.5)) carries x twice, so it cycles faster and grows a taller envelope. One dial, three outcomes.

Level up

MethodTypeDefaultDescription
.phase(v)float0Added to x before evaluation. Shifts the wave horizontally.
.range(lo, hi)float,floatnullMap output to [lo, hi] instead of amplitude scaling. Useful for opacity 0–255, etc.
.wave(a, b)String,StringSet two waves for morphing.
.mix(v)float 0–10.5Morph position when two waves are set.
.group(g)Stringnull"gentle", "harsh", "closing", "ghost", "all", or String[] of names. Restricts which waves seed/shift can pick.

Go deeper

MethodTypeDefaultDescription
.shift(true)booleanfalseAuto-cycle through waves in the group, smoothly morphing between them.
.shiftInterval(s)float (sec)3How long each wave is held.
.shiftDuration(s)float (sec)1Morph time between consecutive waves.
.mode("wild")String"stable"Enables wild mode: noise modulation on top of the wave.
.unpredictability(v)float 0–10How strong the wild modulation is. 0 = stable, 1 = chaotic.

createSampler(): cache for tight loops

If you call Waves.wave(x, opts) thousands of times per frame, the cost of resolving the wave + reading group + computing stats adds up. createSampler() resolves all of that once and gives you a small object with a fast sample() method.

Waves.WaveSampler s = Waves.createSampler(new WaveOpts()
  .shift(true)
  .group("gentle")
  .amplitude(120));

void draw() {
  float t = millis() / 1000.0f;
  for (int x = 0; x < width; x += 3) {
    float y = s.sample(x, t);
    point(x, height / 2 + y);
  }
}

Use sample(x, t) when shift or t-dependent motion is on; sample(x) when t doesn't matter.

Wave Shift

The killer feature: set .shift(true) and processing.waves picks a random wave, holds it for shiftInterval seconds, then smoothly morphs into the next one over shiftDuration seconds. Forever.

Waves.WaveSampler s = Waves.createSampler(new WaveOpts()
  .shift(true)
  .shiftInterval(2.0f)   // hold each wave 2s
  .shiftDuration(1.5f)   // morph between waves over 1.5s
  .group("gentle")       // restrict the pool
  .amplitude(80));

The sampler exposes getters so you can show (or react to) what's playing:

text(s.waveName(),   10, 20);   // "mountain peaks"
text(s.targetName(), 10, 40);   // "batman" (what's coming next)
text(s.shifting(),   10, 60);   // true while morphing
text(s.mix(),        10, 80);   // 0.0 -> 1.0 during the morph

The shift sequence is non-deterministic across runs (uses Math.random() for per-session entropy), same as JS p5.waves.

Closing curved shapes

When you sample a wave around a circle, the last vertex rarely lines up with the first one — endShape(CLOSE) then draws an ugly seam back to the start. The fix is to sweep over an integer multiple of the wave's period: the x-distance over which wave(x) repeats.

Every periodic wave in the library has a measured period, listed on the Waves page. For "classic sine" it's 62.83. Multiply by however many lobes you want around the ring.

Why 62.83 and not 360? x is a generic coordinate here, like in noise(x) — not an angle. Ten of the 35 waves are not sine-based at all (square, triangle, ramp, noise...), so radians would be meaningless for them.

import waves.*;

WaveOpts o;

void setup() {
  size(600, 600);
  noFill();
  stroke(0);
  o = new WaveOpts().wave("classic sine").amplitude(30);
}

void draw() {
  background(245);
  translate(width / 2f, height / 2f);

  float period = 62.83f;        // classic sine
  int   lobes  = 8;             // bumps around the ring
  int   steps  = 240;           // resolution
  float sweep  = lobes * period;

  o.t(millis() / 1000.0f);
  beginShape();
  for (int i = 0; i < steps; i++) {
    float a = (i / (float)steps) * TWO_PI;
    float r = 180 + Waves.wave(i / (float)steps * sweep, o);
    vertex(r * cos(a), r * sin(a));
  }
  endShape(CLOSE);
}

Change lobes to 3, 5, 13 — any positive integer closes cleanly. Change steps for finer or coarser resolution; it never affects whether the shape closes.

Some waves have no clean period — "noise", "meta sine", and the rest of the no-close group on the Waves page. For those, use two rings (inner + outer) and connect them with radial lines, or fade the stroke to zero at the endpoints instead of closing.

Stay closed while shifting — .group("closing")

Shift and closed shapes fight each other by default. A ring sampled with .shift(true) tears its seam open the moment it lands on a new wave, because every formula has its own period and your fixed sweep stops lining up. The "closing" pool ends that fight: all 19 waves in it share one base period (62.8319, that's 2π/0.1), so a sweep of sampler.period() × lobes closes seamlessly through every transition — the shape keeps morphing into new waves and never shows a seam.

import waves.*;

Waves.WaveSampler ring;

void setup() {
  size(600, 600);
  noFill();
  stroke(0);
  ring = Waves.createSampler(new WaveOpts()
    .shift(true)
    .group("closing")    // 19 waves that all close on the same sweep
    .amplitude(30));
}

void draw() {
  background(245);
  translate(width / 2f, height / 2f);

  float sweep = ring.period() * 8;   // 8 lobes, holds across every shift
  float t = millis() / 1000.0f;
  beginShape();
  for (int i = 0; i < 240; i++) {
    float a = (i / 240.0f) * TWO_PI;
    float r = 180 + ring.sample(i / 240.0f * sweep, t);
    vertex(r * cos(a), r * sin(a));
  }
  endShape(CLOSE);
}

sampler.period() and sampler.targetPeriod() report the current and next wave's measured period as a Float — both return the stable base period for a closing pool, null for non-periodic waves (and targetPeriod() also before the first sample() call or on non-shift samplers). So you read the sweep length straight off the sampler instead of hard-coding a number per wave.

Experimental (since p5.waves 3.3.0). Period values may drift by ~0.001 in minor versions. Perfect for visuals; not for plotters, CNC, or anything that gets angry about sub-unit drift.

Morph between two waves

Pass two names, control the blend with mix from 0 to 1.

WaveOpts o = new WaveOpts()
  .wave("classic sine", "mountain peaks")
  .mix(0.5f);            // 0 = first wave, 1 = second

float y = Waves.wave(x, o);

Sweep mix over time and you get a smooth crossfade between two specific shapes. Useful when shift is too wild for the look you want.

Wild mode

One dial, .unpredictability(0..1), adds noise modulation on top of any wave. At 0 you get the clean wave; at 1 you get chaos that still respects the wave's overall envelope.

WaveOpts wild = new WaveOpts()
  .wave("mountain peaks")
  .mode("wild")
  .unpredictability(0.7f);

float y = Waves.wave(x, wild);

Time is just a number

You pass t in seconds — processing.waves never calls a clock itself. That means you can scrub time: map mouseX to t, run faster than real-time, run backwards, freeze at a moment.

float t = map(mouseX, 0, width, 0, 10);   // 10 seconds across the canvas
float y = sampler.sample(x, t);

Seed, group, and the wave pool

When you don't specify .wave(), processing.waves picks one from a pool. These control which:

Map directly to a range

Skip the amplitude scaling and map the wave's output to your own range.

// Use as opacity (0-255):
float alpha = Waves.wave(t, new WaveOpts().wave("classic sine").range(0, 255));
fill(255, alpha);

// Use as hue (0-360):
colorMode(HSB, 360, 100, 100);
float hue = Waves.wave(x, new WaveOpts().wave("noise").range(0, 360));

All 35 waves

Each name is also accessible by its index. See the visual gallery →

00classic sine
01sine
02sharp peaks
03square
04pulse
05stepped sine
06mountain peaks
07valleys
08zig-zag sine
09batman
10offset sine
11steps down
12steps
13squared sine
14bumpy sine
15wobble sine
16up down noise
17meta sine
18triangle
19ramp
20saw down
21saw up
22shake out
23grow random
24noise
25fuzzy pulse
26up down pulse
27bald patch
28fuzzy peak sine
29ramp up sine
30triangle sine
31round linked sine
32half sine
33smooth solid sine
34spike sine

Port notes: Java vs JS

The library is a byte-for-byte numerical port of p5.waves v3.5.0. Where the languages differ, we kept the semantics. Differences worth knowing:

Copy-paste starters

One wave, no time

import waves.*;
void setup() { size(800, 400); noStroke(); fill(255); }
void draw() {
  background(20);
  for (int x = 0; x < width; x += 6) {
    float y = Waves.wave(x, "classic sine");
    rect(x, height/2 + y, 4, 4);
  }
}

Animated wave (uses t)

import waves.*;
WaveOpts o;
void setup() {
  size(800, 400); stroke(255); noFill();
  o = new WaveOpts().wave("mountain peaks").amplitude(120);
}
void draw() {
  background(20);
  o.t(millis() / 1000.0f);
  beginShape();
  for (int x = 0; x < width; x += 2) vertex(x, height/2 + Waves.wave(x, o));
  endShape();
}

Auto-shifting ribbons

import waves.*;
Waves.WaveSampler s;
void setup() {
  size(800, 400); noStroke();
  s = Waves.createSampler(new WaveOpts()
    .shift(true).shiftInterval(2.5f).shiftDuration(1.5f)
    .group("gentle").amplitude(140));
}
void draw() {
  background(20);
  float t = millis() / 1000.0f;
  for (int row = 0; row < 12; row++) {
    fill(255, 30 + row*15);
    beginShape();
    for (int x = 0; x < width; x += 2) {
      float y = s.sample(x + row * 12, t);
      vertex(x, height/2 + y + row * 6);
    }
    endShape();
  }
}

Morph between two waves

import waves.*;
void setup() { size(800, 400); stroke(255); noFill(); }
void draw() {
  background(20);
  float mix = (sin(millis() * 0.0005f) * 0.5f) + 0.5f;
  WaveOpts o = new WaveOpts()
    .wave("classic sine", "triangle")
    .mix(mix).amplitude(120);
  beginShape();
  for (int x = 0; x < width; x += 2) vertex(x, height/2 + Waves.wave(x, o));
  endShape();
}

Use a wave as colour

import waves.*;
void setup() { size(800, 400); colorMode(HSB, 360, 100, 100); noStroke(); }
void draw() {
  for (int x = 0; x < width; x += 4) {
    float h = Waves.wave(x + frameCount, new WaveOpts()
      .wave("bumpy sine").range(0, 360));
    fill(h, 80, 95);
    rect(x, 0, 4, height);
  }
}

Wild mode (chaos dial)

import waves.*;
void setup() { size(800, 400); stroke(255); noFill(); }
void draw() {
  background(20);
  float u = map(mouseX, 0, width, 0, 1);   // 0 = stable, 1 = chaos
  WaveOpts o = new WaveOpts()
    .wave("mountain peaks")
    .mode("wild").unpredictability(u)
    .amplitude(120).t(millis()/1000.0f);
  beginShape();
  for (int x = 0; x < width; x += 2) vertex(x, height/2 + Waves.wave(x, o));
  endShape();
}