Site · Guide · Examples · p5.waves
Pass a number in, get a number back. You can use the 35 wave formulas in
vanilla.waves to change a bar's height, a colour or an element's position.
Register an element type and mark it with data-wv; the DOM engine calls your
update function in a shared 30 fps loop. Offscreen elements pause automatically.
The library uses the wave() and createSampler() math from
p5.waves, with a DOM engine for
plain HTML elements. Load it with one <script> tag. It needs no dependencies,
canvas or build step to run. With prefers-reduced-motion, the engine renders
one static frame instead of animating.
p5.waves the dialect for p5.js / canvas
processing.waves the dialect for Processing / Java
vanilla.waves the dialect for plain DOM/CSS, zero dependencies (you are here)
The comparison with p5.waves v3.6.0 recorded on 2026-07-16 passed 10,422 parity
checks (all 35 waves, morph, range, wild and samplers), with bit-identical
results. The current check-parity.mjs runs 8,620 checks comparing the generated
bundles with waves-core.js. See the recorded comparison.
wave(), createSampler() and the 35 wave formulas use the math from
p5.waves. The port removes its p5 prototype hook and exposes the functions
through VanillaWaves.
The DOM engine calls your update function for each visible registered element. You can use it for loaders, backgrounds, text fields or meters.
<script src="https://cdn.jsdelivr.net/gh/seb-prjcts-be/vanilla.waves@v0.3.1/vanilla.waves.min.js"></script>The bundle includes the math and DOM engine. The version is pinned to v0.3.1
so updates do not change your page unexpectedly. For the math alone, load
waves-core.js.
Copy vanilla.waves.min.js (or vanilla.waves.js for the readable build) next
to your HTML and point a <script> at it. No npm, no bundler.
VanillaWaves (alias vWaves, and Waves as a drop-in when p5.waves isn't
present) exposes the dialect:
// one value in, one value out: always a number
VanillaWaves.wave(x, { wave: 'mountain peaks', t: performance.now() / 1000, amplitude: 80 });
// a reusable sampler (cheaper for many calls per frame)
const s = VanillaWaves.createSampler({ shift: true, range: [-50, 50] });
s.sample(x, t); // position + time
s.waveName; // "mountain peaks" (live getter)Drive any DOM property with it: a bar height, a hue, a transform:
const s = VanillaWaves.createSampler({ seed: 13, range: [0, 100] });
function tick(now) {
bar.style.height = s.sample(0, now / 1000) + '%';
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);Register an element type once, then mark elements with data-wv="<type>".
The shared loop calls your update every frame; offscreen elements pause.
<div data-wv="bars" data-seed="13" data-count="20"></div>
<script src="vanilla.waves.min.js"></script>
<script>
VanillaWaves.register('bars', {
create(node, opts, h) {
const n = h.num(opts.count, 16);
const bars = [];
for (let i = 0; i < n; i++) bars.push(node.appendChild(h.el('span')));
return { bars, sampler: h.makeSampler() }; // state
},
update(state, t) { // every frame
state.bars.forEach((b, i) => {
// sampler value mapped to a scaleY
const v = 0.1 + state.sampler.sample(i * 0.5, t) * 0.9;
b.style.transform = 'scaleY(' + v.toFixed(3) + ')';
});
}
});
// auto-inits on DOMContentLoaded; call VanillaWaves.init() again after adding nodes
</script>When the work is done (e.g. content finished loading), stop and clean up:
VanillaWaves.destroy(myElement); // or a selector, NodeList, or nothing for all| Call | Returns |
|---|---|
VanillaWaves.wave(y) |
default wave, frozen |
VanillaWaves.wave(y, 3) |
seed 3 (hashed to a wave) |
VanillaWaves.wave(y, 'triangle') |
wave by name |
VanillaWaves.wave(y, { ... }) |
full options (below) |
VanillaWaves.createSampler({ ... }) |
a sampler object |
VanillaWaves.list() / .count / .data |
discover the 35 waves at runtime |
Options (all optional): wave (name / index / [a,b] to morph), t,
amplitude (default 100), range [min,max] (overrides amplitude),
frequency (1), phase (0), seed (0), mode 'stable'/'wild',
unpredictability (0..1, wild only), mix (0..1, for morph), shift (bool),
group 'gentle'/'harsh'/'closing'/'ghost'/'all'/array, shiftInterval (3),
shiftDuration (1).
Sampler: sample(y), sample(y, t), sample(y, t, mix). Live getters:
waveName, targetName, shifting, mix, period, targetPeriod.
Note: shift is not deterministic across page loads. A per-session
random offset is mixed in, so the same seed yields a different wave
sequence on every reload (identical within one session). This follows the
p5.waves behaviour.
| Call | Does |
|---|---|
VanillaWaves.register(name, { create, update?, destroy? }) |
define an element type |
VanillaWaves.init(target?) |
wire up [data-wv] (or a selector / element / NodeList) |
VanillaWaves.destroy(target?) |
stop and clean up |
create(node, opts, helpers) returns your state object; update(state, t)
mutates the DOM each frame; optional destroy(state, node) releases anything you
allocated (timers, listeners). Helpers: makeSampler(extra?) (a sampler with
element-friendly defaults), norm(v) (maps about [-1,1] to [0,1]),
num(v, default), el(tag, class?).
destroy removes only the nodes create added (author content stays), runs your
destroy disposer, and restores aria-hidden; so destroy() then init()
re-renders cleanly instead of duplicating children.
Conventions: marker attribute data-wv, ready class wv--ready, shared loop
at 30 fps, data-* attributes become opts, data-speed scales time. CSS
class and element names use a wv- prefix, never dots (p5.waves.loader is not
a valid selector).
Accessibility & seeds. Decorative elements get aria-hidden="true"
automatically; opt out with data-decorative="false" (or set your own
aria-hidden) and the engine leaves it alone. data-seed may be any string;
non-numeric seeds are hashed to a stable start offset, so they animate like
numeric ones.
Use sample(position, time) to supply values to another renderer. The waves
can drive three.js geometry, a Web Audio param curve, a CSS linear() easing
function, or a build-time bake in Node.
import { createSampler } from './waves-core.mjs';
// a ring that stays closed through every shift: sweep an integer number of periods
const ring = createSampler({ shift: true, group: 'closing', range: [-0.6, 0.6] });
const sweep = ring.period * 6; // 6 lobes
const r = 2 + ring.sample(u * sweep, t); // u = 0..1 around the ringwaves-core.mjs is the module entry on the math. It holds no formulas: it
imports waves-core.js for its side effect and re-exports it, keeping
one copy of the formulas. It works in browsers, Node and bundlers without a
build step. For the DOM engine, load vanilla.waves.js with a <script> tag.
There is no npm package yet, so import the module by path.
On the CDN (from v0.3.1). Import the module entry straight from jsDelivr:
import { createSampler, wave } from 'https://cdn.jsdelivr.net/gh/seb-prjcts-be/vanilla.waves@v0.3.1/waves-core.mjs'
Or copy it locally for a fully offline setup.
Four three.js examples (displaced geometry, a closing ring,
an instanced 2D field, and the wave baked into a GPU lookup texture), plus the
adapter rule that keeps the wave code renderer-agnostic and testable in Node:
docs/interop.html.
waves-core.js is the canonical p5.waves.js math with only the p5 prototype
hook removed. The drift-watch procedure compares the port with the p5.waves
baseline. It requires the Claude app to be open, as recorded in
docs/waves.feature.md.
Dialect baseline: p5.waves v3.6.0 (commit 804f91e, 35 waves).
waves-core.js the math (zero-dep port of p5.waves)
waves-core.mjs ES-module entry on the math (re-exports, no formulas of its own)
engine.js the DOM engine (shared loop, register/init/destroy)
vanilla.waves.js generated bundle (core + engine, readable)
vanilla.waves.min.js generated bundle, minified, the CDN artifact
index.html the Showcase (Pages landing, repo root)
docs/ the rest of the GitHub Pages site
examples.html the canvasless demo vocabulary, with source
engine.html the DOM engine, deep-dive
waves.html all 35 waves + periodicity
guide.html install, math, engine API, starters
interop.html driving other renderers (three.js, audio, CSS, Node)
about.html why it exists, parity, credits
style.css shared skin
waves-demos.js demo renderers (eq/line/load/wave/ribbons/ascii/field)
waves.feature.md port-coverage + drift-watch changedoc
The Pages site follows the same structure as p5.waves (Showcase at the repo
root, the rest under docs/) and a nav of its own: Showcase · Examples ·
Engine · Waves · Guide · Interop · About. The demos use the DOM engine.
Serve Pages from the repository root.
The element library (loaders, controls, draggables) lives in a separate package, vanilla.waves_elements, built on this engine.
MIT, Sebastien Vanblaere (seb@prjcts)