Basic wave
Single wave with default gradient stroke.
Organized by what you're building, not by how it's drawn — find your use case, copy the demo.
Core interactions you can remix immediately.
Single wave with default gradient stroke.
Stack multiple waves for depth and parallax.
Pointer movement shapes amplitude and wavelength.
Programmatic add/remove of wave layers.
Low-memory settings with pixel-ratio caps.
Swap easing curves for cinematic motion.
Explicit control over the render loop.
What you'll reach for behind a hero, a card, or a panel — texture and motion that doesn't compete with your content.
Grid-based interference with tempo-driven color.
// Grid interference with tempo-driven color
for (let y = 0; y <= rows; y++) {
for (let x = 0; x <= columns; x++) {
const value = Math.sin(x * freq + t) * Math.cos(y * freq + t);
ctx.arc(x * spacingX, y * spacingY, radius * value, 0, TWO_PI);
}
}Two counterphase strands with diagonal drift.
// Counterphase strands with diagonal drift
for (let y = 0; y <= height; y += 6) {
const xOffset = Math.sin(y * freq + time) * amplitude;
ctx.moveTo(centerX + xOffset, y);
ctx.lineTo(centerX - xOffset, y);
}Vertical flow with volumetric thickness.
// Vertical flow with volumetric thickness
for (let y = 0; y <= height; y += 6) {
const offset = Math.sin(y * freq + time + col * 0.6) * amp;
ctx.lineTo(columnX + offset, y);
}Angled strokes with tempo-locked shimmer.
// Angled strokes with tempo-locked shimmer
for (let i = 0; i < count; i++) {
const drift = Math.sin(time + i * freq) * amplitude;
ctx.moveTo(baseX + drift, baseY);
ctx.lineTo(baseX + drift + 24, baseY + 38);
}Parametric loops synced to time signature.
// Parametric loops synced to time
for (let i = 0; i <= 240; i++) {
const t = (i / 240) * TWO_PI;
const x = cx + Math.sin(t * freq + time) * amp;
const y = cy + Math.sin(t * (freq + 1) + phase) * amp;
}Layered horizons with depth-shifted phases.
// Layered horizons with depth-shifted phases
for (let layer = 0; layer < 8; layer++) {
for (let x = 0; x <= width; x += 6) {
const y = baseY + Math.sin(x * freq + phase) * amp;
ctx.lineTo(x, y);
}
}Outward pulses that peak on the downbeat.
// Outward pulses on the downbeat
for (let r = 18; r < radiusMax; r += 12) {
const offset = Math.sin(r * freq + time) * pulse;
ctx.arc(cx, cy, r + offset, 0, TWO_PI);
}Area under the wave filled with a linear gradient.
// Area fill with linear gradient
const grad = ctx.createLinearGradient(0, top, 0, height);
for (let x = 0; x <= width; x += 4) {
ctx.lineTo(x, centerY + Math.sin(x * freq + t) * amp);
}
ctx.fillStyle = grad;
ctx.fill();Simulated volume-driven multi-wave display.
// Volume-driven multi-wave display
waveConfigs.forEach((cfg) => {
for (let x = 0; x <= width; x += 4) {
const y = centerY + Math.sin(x * cfg.freq + t * cfg.speed) * amp;
ctx.lineTo(x, y);
}
});Rotated orientation for liquid-rising effects.
// Rotated orientation for liquid-rising effects
for (let y = 0; y <= height; y += 4) {
const x = centerX + Math.sin(y * freq + time) * amp;
ctx.lineTo(x, y);
}Dotted and dashed wave lines via setLineDash.
// Dotted and dashed wave lines
ctx.setLineDash([12, 6]);
for (let x = 0; x <= width; x += 4) {
const y = centerY + Math.sin(x * freq + time) * amp;
ctx.lineTo(x, y);
}Pulsing line thickness synchronized to tempo.
// Pulsing line thickness per segment
for (let x = 0; x < width; x += 6) {
const thickness = 1 + Math.abs(Math.sin(x * 0.01 + t * 2)) * 4;
ctx.lineWidth = thickness;
ctx.lineTo(x, centerY + Math.sin(x * freq + t) * amp);
}Screen blending for neon glow effects.
// Screen blending for neon glow
ctx.globalCompositeOperation = "screen";
colors.forEach((cfg) => {
ctx.shadowBlur = 20;
ctx.lineTo(x, centerY + Math.sin(x * freq + t + cfg.offset) * amp);
});Ultra-slow motion for maximum smoothness.
// Ultra-slow motion sine
for (let x = 0; x <= width; x += 6) {
const y = centerY + Math.sin(x * freq + time * 0.15) * amp;
ctx.lineTo(x, y);
}Click, drag, or speak into these — motion that responds to you, not just to time.
Tap to pluck. Damped amplitude returns to rest.
Tip: click the line to excite it.
// Tap to pluck, damped amplitude returns to rest
canvas.addEventListener("pointerdown", () => { state.amplitude = 100; });
state.amplitude *= 0.95; // decay each frame
const y = centerY + Math.sin(x * 0.02 + phase) * state.amplitude * 0.02;64 wave forest mapped across bass to treble.
// 64-band frequency display
analyser.getByteFrequencyData(data);
for (let i = 0; i < 64; i++) {
const amp = data[i] / 255;
ctx.moveTo(x, base - amp * height * 0.3);
ctx.lineTo(x, base + amp * height * 0.3);
}Orbit knots with adjustable a:b ratios.
// Orbit knots with adjustable a:b ratios
for (let i = 0; i <= 320; i++) {
const t = (i / 320) * TWO_PI;
const x = cx + ampX * Math.sin(a * t + delta + time);
const y = cy + ampY * Math.sin(b * t + time);
}Slow modulator controls a fast carrier.
// Slow modulator controls a fast carrier
const mod = Math.sin(time * 0.8);
for (let x = 0; x <= width; x += 4) {
const carrier = Math.sin(x * carrierFreq + time * 1.6);
const y = centerY + carrier * baseAmp * mod;
}Biometric, voice, and looping waveforms — the shapes a heartbeat monitor, a voice message, a listening indicator, an equalizer, or a seamless loop actually draw.
A stylized EKG trace — P wave, QRS spike, T wave, repeat.
// Stylized EKG cycle: P wave, QRS spike, T wave
const beat = ekgWaveform((x / width + time / TWO_PI) % 1);
const y = centerY - beat * amplitude * 3;
ctx.lineTo(x, y);Three soft, overlapping layers for a calm background texture.
// Three overlapping layers, each with its own hue and offset
layers.forEach((layer, i) => {
const y = centerY + Math.sin(x * freq + time + layer.offset) * amplitude;
ctx.strokeStyle = `hsla(${layer.hue}deg 70% 65% / ${layer.alpha})`;
});A voice-message-style bar waveform, driven by a speech envelope.
// Bar height from a speech-like envelope, with occasional pauses
const envelope = Math.sin(t) * 0.5 + Math.sin(t * 2.3) * 0.3 + Math.sin(t * 0.6) * 0.4;
const gate = Math.max(0.08, (Math.sin(t * 0.2) + 1) * 0.5);
const barHeight = Math.abs(envelope) * amplitude * gate;Pulsing rings around a mic dot, live from AudioSync.
// Ring radius pulses with live mic energy
const audioSync = new AudioSync();
audioSync.connect(micStream);
const energy = audioSync.getMetrics().energy;
const radius = baseRadius + smoothedEnergy * 32;A multi-band equalizer-style spectrum visualizer.
// 24 independent bands, each with its own phase offset
for (let i = 0; i < bands; i++) {
const level = (Math.sin(t) * 0.5 + Math.sin(t * 1.7 + i) * 0.3 + 1) * 0.5;
ctx.fillRect(x, baseline - barHeight, barWidth * 0.7, barHeight);
}A closed, continuously wobbling ribbon — no start, no end.
// Closed loop with a sine-modulated radius
for (let i = 0; i <= points; i++) {
const angle = (i / points) * TWO_PI;
const radius = baseRadius + Math.sin(angle * lobes + time) * amplitude;
ctx.lineTo(cx + Math.cos(angle) * radius, cy + Math.sin(angle) * radius);
}
ctx.closePath();Record your voice, watch the live waveform, then play it back.
// Live waveform while recording, then play the captured clip back
const audioSync = new AudioSync();
audioSync.connect(micStream);
generator.syncToAudio(audioSync, { amplitude: { source: "energy", intensity: 1.5 } });
const recorder = new MediaRecorder(micStream);
recorder.ondataavailable = (e) => chunks.push(e.data);
recorder.onstop = () => { audioEl.src = URL.createObjectURL(new Blob(chunks)); };A DJ-style crossfade between two independent waves.
// Two waves, each scaled by its own volume and the crossfade blend
const ampA = 26 * volumeA * (1 - crossfade);
const ampB = 26 * volumeB * crossfade;
ctx.lineTo(x, centerY + Math.sin(percent * TWO_PI * 2 + time) * ampA);
ctx.lineTo(x, centerY + Math.sin(percent * TWO_PI * 3 + time * 1.3) * ampB);Two features that are on by default but easy to miss — see them actually change the animation, not just read about them.
Toggle to preview how visitors with prefers-reduced-motion see this animation.
// Honored automatically — this just previews the effect on demand
const generator = new SineWaveGenerator({ el: canvas });
// generator.prefersReducedMotion is auto-detected from the OS;
// toggling it here simulates that state for this preview.
generator.prefersReducedMotion = true;The built-in gradient repaints for light or dark — no rebuild.
// "auto" follows prefers-color-scheme live by default
const generator = new SineWaveGenerator({ el: canvas, colorScheme: "auto" });
// Force a palette instead of following the system preference:
generator.colorScheme = "dark";
generator.resize();Four ready-to-copy scenes — pick one, drop it behind your header, done.
Horizontal layering with slow tides.
// Horizontal layering with slow tides
// Uses drawWaveformTerrain with 8 depth layersVertical strands with parallax depth.
// Vertical strands with parallax depth
// Uses drawDNAHelix with counterphase strandsBPM-reactive ripples with color sync.
// BPM-reactive ripples with color sync
// Uses drawRadialBloom with downbeat emphasisLissajous curves with orbital drift.
// Lissajous curves with orbital drift
// Uses drawLissajousOrbit with phase shiftFor when you want something that feels alive, not just animated.
Two angled lattices creating moire shimmer.
// Two angled lattices creating shimmer
const drawGrid = (rotation) => {
ctx.rotate(rotation);
for (let x = -max; x <= width + max; x += spacing) {
ctx.moveTo(x, -max);
ctx.lineTo(x, height + max);
}
};Letterforms that breathe with the beat.
// Letterforms that breathe with the beat
"SINE WAVE".split("").forEach((char, i) => {
const wave = Math.sin(time + i * freq) * amplitude;
ctx.fillText(char, cx + i * 18, cy + wave);
});Organic motion with exponential decay.
// Exponential decay envelope
const envelope = Math.exp(-3.2 * progress);
for (let x = 0; x <= width; x += 4) {
const y = centerY + Math.sin(x * freq + time) * amp * envelope;
ctx.lineTo(x, y);
}Nested oscillators for organic drift.
// Nested oscillators for organic drift
for (let x = 0; x <= width; x += 4) {
const nested = Math.sin(time + x * baseFreq * 0.4) * 0.6 + 1;
const y = centerY + Math.sin(x * (freq + mod * nested) + time) * amp;
ctx.lineTo(x, y);
}