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 · scaleY

One 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 · closing

A 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 · character

A 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 · grid

A 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 · grid

A 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 · shift

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