Guide

Everything you need to use processing.waves in Processing 4. The API mirrors p5.waves v3.3.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 34 names. null → pick by seed.
.wave(idx)intPick by index (0…33).
.amplitude(v)float100Output is scaled to [−amp/2, +amp/2].
.frequency(v)float1Multiplier on x. Higher = faster cycles.
.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.

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", "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 shift sequence is non-deterministic across runs (uses Math.random() for per-session entropy), same as JS p5.waves.

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. Three things 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 34 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
22fade 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

Port notes: Java vs JS

The library is a byte-for-byte numerical port of p5.waves v3.3.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();
}