Skip to content

Repository files navigation

vanilla.waves

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.


The math and DOM engine

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.


Install

CDN (jsDelivr)

<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.

Local

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.


Quick start

1. Sample a wave

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

2. The DOM engine

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

API

Math (identical to p5.waves)

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.

Engine (vanilla.waves only)

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.


Beyond the DOM

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 ring

waves-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.


Parity and drift

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).


Project layout

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.


License

MIT, Sebastien Vanblaere (seb@prjcts)

Releases

Packages

Contributors

Languages