examples
Six renderers share the 30 fps DOM loop. Each registers a create function to build its elements and an update function to change their properties. The examples run on this page; their full source is in waves-demos.js.
equalizer
sampler · scaleYOne sampler read at a different position per bar. Height comes straight off the wave; because shift is on, the meter cycles through formulas and never settles.
VanillaWaves.register('eq', {
create(node, o, h) {
const n = h.num(o.count, 18), bars = [];
for (let i = 0; i < n; i++)
bars.push(node.appendChild(h.el('span')));
return { bars, n, s: h.makeSampler() };
},
update(st, t) {
for (let i = 0; i < st.n; i++) {
const v = 0.08 + st.s.sample(i*0.55, t) * 0.92;
st.bars[i].style.transform = 'scaleY(' + v + ')';
}
}
});
wave line
sampler · translateY · closingA single sampler traced across position and drawn as a row of dots. The row is the live waveform. Restrict the pool with data-group="closing" and it keeps its shape while it shifts.
VanillaWaves.register('line', {
create(node, o, h) {
const n = h.num(o.count, 80), dots = [];
for (let i = 0; i < n; i++)
dots.push(node.appendChild(h.el('span')));
return { dots, n, s: h.makeSampler({ amplitude: 1 }) };
},
update(st, t) {
for (let i = 0; i < st.n; i++) {
const v = st.s.sample(i*0.18, t);
st.dots[i].style.transform =
'translateY(' + (v*38).toFixed(1) + 'px)';
}
}
});
<div data-wv="line" data-group="closing"></div>
text loader
wave · characterA 4×4 grid where each cell picks a glyph from a wave value. Set data-text to choose the character ramp.
VanillaWaves.register('load', {
create(node, o, h) {
const cells = [];
for (let i = 0; i < 16; i++)
cells.push(node.appendChild(h.el('span')));
return { cells, s: h.makeSampler(),
text: o.text || 'LOADING',
prev: Array(16).fill('') };
},
update(st, t) {
const len = st.text.length; let i = 0;
for (let r = 0; r < 4; r++)
for (let c = 0; c < 4; c++, i++) {
const v = st.s.sample(c*0.5 + t, t + r*0.9);
let idx = Math.floor((v+1)/2 * len);
idx = Math.max(0, Math.min(len-1, idx));
const ch = st.text[idx];
if (ch !== st.prev[i]) {
st.prev[i] = ch; st.cells[i].textContent = ch;
}
}
}
});
ASCII flow field
shift · character ramp · gridA grid of characters, each glyph chosen from a shift-sampler - like noise, but with structure that keeps re-forming as the wave shifts. One <pre>, rebuilt from a string each frame.
VanillaWaves.register('ascii', {
create(node, o, h) {
const pre = h.el('pre', 'wv-ascii');
node.appendChild(pre);
return { pre,
cols: h.num(o.cols, 34), rows: h.num(o.rows, 12),
chars: o.chars || ' ·-=+*#%@',
s: h.makeSampler({ range: [-1, 1],
shiftInterval: 4, shiftDuration: 2 }) };
},
update(st, t) {
let out = '', len = st.chars.length;
for (let r = 0; r < st.rows; r++) {
for (let c = 0; c < st.cols; c++) {
const v = st.s.sample(c*0.5, t + r*0.4);
const i = Math.floor((v+1)/2 * len);
out += st.chars[Math.max(0, Math.min(len-1, i))];
}
out += '\n';
}
st.pre.textContent = out;
}
});
tone field
wave · opacity · gridA grid of ink cells whose opacity is a sampler value. A wave-driven take on a static field - every cell drifts inside a moving window, so it stays organised while it breathes.
VanillaWaves.register('field', {
create(node, o, h) {
const cols = h.num(o.cols, 24),
rows = h.num(o.rows, 10), cells = [];
node.style.gridTemplateColumns =
`repeat(${cols}, 1fr)`;
for (let i = 0; i < cols*rows; i++)
cells.push(node.appendChild(h.el('span')));
return { cells, cols, rows,
s: h.makeSampler({ range: [-1, 1] }) };
},
update(st, t) {
let i = 0;
for (let r = 0; r < st.rows; r++)
for (let c = 0; c < st.cols; c++, i++) {
const v = (st.s.sample(c*0.4, t + r*0.5) + 1) / 2;
st.cells[i].style.opacity = 0.06 + v * 0.94;
}
}
});
ribbons
SVG · stacked lines · shiftStacked wave lines from one shift-sampler, with each row offset in phase. The renderer draws them as SVG paths.
VanillaWaves.register('ribbons', {
create(node, o, h) {
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 120 60');
svg.setAttribute('preserveAspectRatio', 'none');
const rows = h.num(o.rows, 7), lines = [];
for (let r = 0; r < rows; r++) {
const p = document.createElementNS(NS, 'polyline');
p.setAttribute('class', 'wv-trace');
svg.appendChild(p); lines.push(p);
}
node.appendChild(svg);
return { lines, rows, n: h.num(o.count, 90),
s: h.makeSampler({ amplitude: 1 }) };
},
update(st, t) {
for (let r = 0; r < st.rows; r++) {
const base = 8 + r * (44 / (st.rows - 1));
let pts = '';
for (let i = 0; i <= st.n; i++) {
const x = (i/st.n)*46 + r*7;
const v = st.s.sample(x, t + r*0.35);
pts += (i/st.n*120) + ',' + (base + v*9) + ' ';
}
st.lines[r].setAttribute('points', pts);
}
}
});
Call VanillaWaves.destroy(el) when the animation is finished. Pass an element, selector or NodeList, or omit the argument to clean up all registered elements.