Back to Hub
UI PERFORMANCE •

Pixels and Waveforms: Creative Coding in the Browser.

Two browser APIs turn a plain web page into a creative instrument: Canvas 2D for pixels and the Web Audio API for sound. Together they cover generative art, data visualization, music tools, games, and accessible audio feedback. This guide is hands-on — every snippet runs if you paste it into NitroIDE's preview.

Canvas 2D: the immediate-mode drawing board

Canvas is immediate-mode: you issue draw commands, pixels appear. No scene graph, no retained objects — which makes it blazing fast for custom rendering:

const ctx = canvas.getContext('2d');
function frame(t) {
  ctx.fillStyle = '#0a0a0a';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < 120; i++) {
    const x = canvas.width / 2 + Math.cos(t / 500 + i) * i * 2;
    const y = canvas.height / 2 + Math.sin(t / 500 + i) * i * 2;
    ctx.fillStyle = `hsl(${(t / 20 + i * 3) % 360}, 80%, 60%)`;
    ctx.fillRect(x, y, 4, 4);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Crisp on every screen: canvas looks blurry on high-DPI displays unless you scale it. Multiply canvas.width/height by devicePixelRatio and call ctx.scale(devicePixelRatio, devicePixelRatio) — one of the most common canvas bugs, fixed in two lines.

Crisp text on canvas

Canvas text rendering is notoriously rough compared to DOM text. Practical fixes: always set an explicit ctx.font (don't rely on the 10px default), enable ctx.textBaseline deliberately, and for editor-grade typography consider shaping text properly — libraries like HarfBuzz compiled to WASM exist precisely because complex scripts need real text shaping, which canvas won't do for you.

Web Audio: a modular synthesizer in JS

The Web Audio API is a graph of nodes: sources → effects → destination. Building a playable synth takes surprisingly little code:

// Browsers require a user gesture before audio starts
button.addEventListener('click', () => {
  const ctx = new AudioContext();
  const osc = ctx.createOscillator();
  const gain = ctx.createGain();
  const filter = ctx.createBiquadFilter();
  osc.type = 'sawtooth'; osc.frequency.value = 220;
  filter.type = 'lowpass'; filter.frequency.value = 1200;
  gain.gain.setValueAtTime(0, ctx.currentTime);
  gain.gain.linearRampToValueAtTime(0.4, ctx.currentTime + 0.05);
  gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.2);
  osc.connect(filter).connect(gain).connect(ctx.destination);
  osc.start(); osc.stop(ctx.currentTime + 1.3);
});

That envelope — quick attack, exponential decay — is what separates "beep" from "instrument." Everything is scheduled on the audio clock (ctx.currentTime), which is sample-accurate and independent of frame rate.

Sample-accurate timing with AudioWorklet

For custom DSP — a distortion effect, a granular sampler, a metronome that never drifts — you need code running on the audio thread itself. AudioWorklet replaced the old (now removed) ScriptProcessorNode:

// metronome-processor.js — runs on the real-time audio thread
class Metronome extends AudioWorkletProcessor {
  process(inputs, outputs) {
    const out = outputs[0][0];
    for (let i = 0; i < out.length; i++) {
      // emit a click every 0.5s, sample-accurate
      out[i] = (this.pos++ % 24000 < 200) ? 0.8 : 0;
    }
    return true; // keep alive
  }
}
registerProcessor('metronome', Metronome);
await ctx.audioWorklet.addModule('metronome-processor.js');
new AudioWorkletNode(ctx, 'metronome').connect(ctx.destination);

Because this runs on the audio rendering thread, timing is sample-accurate — no setInterval drift. Keep the process() method lean: no allocations, no async work, or you'll hear glitches.

UI sonification: sound as accessibility

Sound isn't just for music apps. Short, subtle audio cues — a soft tick on toggle, a rising pitch as a progress bar fills — give non-visual feedback that helps everyone, including screen-reader users navigating custom controls. The Web Audio snippets above are all you need; keep cues under 200 ms and always respect prefers-reduced-motion-style user preferences for sound where your app offers a mute.

The one rule of web audio: AudioContext starts suspended until a user gesture. Always create or resume it inside a click/keydown handler — otherwise you'll debug silence for an hour. This is a browser autoplay policy, not a bug in your code.

Try it live: edit and run this example right here, no signup needed.

Make Some Noise. Draw Some Pixels.

Creative coding needs instant feedback — get it in a zero-setup browser IDE.

Launch NitroIDE