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
Find your sketchbook in Processing 4:
File > Preferences > Sketchbook location.Download
waves.zipfrom the latest GitHub release.Unzip into
<sketchbook>/libraries/. You should end up with:<sketchbook>/libraries/waves/ library.properties library/waves.jar src/waves/*.java examples/*/
.pde 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
| Method | Type | Default | Description |
|---|---|---|---|
| .wave(name) | String | null | One of the 34 names. null → pick by seed. |
| .wave(idx) | int | Pick by index (0…33). | |
| .amplitude(v) | float | 100 | Output is scaled to [−amp/2, +amp/2]. |
| .frequency(v) | float | 1 | Multiplier on x. Higher = faster cycles. |
| .t(v) | float | 0 | Time. Anything that produces motion uses this. |
| .seed(v) | int | 0 | Picks the wave when .wave() is null, and seeds the PRNG for harsh waves. |
Level up
| Method | Type | Default | Description |
|---|---|---|---|
| .phase(v) | float | 0 | Added to x before evaluation. Shifts the wave horizontally. |
| .range(lo, hi) | float,float | null | Map output to [lo, hi] instead of amplitude scaling. Useful for opacity 0–255, etc. |
| .wave(a, b) | String,String | Set two waves for morphing. | |
| .mix(v) | float 0–1 | 0.5 | Morph position when two waves are set. |
| .group(g) | String | null | "gentle", "harsh", "closing", "all", or String[] of names. Restricts which waves seed/shift can pick. |
Go deeper
| Method | Type | Default | Description |
|---|---|---|---|
| .shift(true) | boolean | false | Auto-cycle through waves in the group, smoothly morphing between them. |
| .shiftInterval(s) | float (sec) | 3 | How long each wave is held. |
| .shiftDuration(s) | float (sec) | 1 | Morph time between consecutive waves. |
| .mode("wild") | String | "stable" | Enables wild mode: noise modulation on top of the wave. |
| .unpredictability(v) | float 0–1 | 0 | How 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:
.seed(n): hashes the seed and picksfloor(rng(seed) * pool.size). Same seed = same wave..group(name): restricts the pool."gentle"(no PRNG / pure math),"harsh"(uses noise/random),"closing"(period divides 2π·n, safe for closing shapes), or"all".Custom pool: pass a
String[]of names:.group(new String[]{"classic sine","mountain peaks","triangle"}).
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 →
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:
No runtime string evaluator. JS uses
new Function(algoString)to evaluate wave formulas at runtime. Java replaces this with hand-translated lambdas, one per wave. The string-eval feature was never exposed via the public API, so no functional difference.Internal math runs in
double(matching JS Numbers) but the public API returnsfloat(Processing convention). Result: visually identical to JS, with sub-pixel match where deterministic.FNV-1a seed + mulberry32 PRNG ported 1:1.
seedFrom()returns the unsigned uint32 as alongso subsequent float math matches JS's unsigned-Number semantics.Shift entropy.
shift(true)usesMath.random()for per-session entropy, outputs are non-deterministic across runs, same as JS.Validated. A 35-case validator in
/testsruns the JS reference via Node and compares against the Java output. Tolerance:1e-3for stable mode,0.5for wild mode (small float vs double differences amplify through noise modulation).
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();
}