Building Animated Sine Waves for My Portfolio
The Idea
I wanted a subtle, living background for my portfolio - not distracting, but enough to make it feel dynamic. Smooth sine waves flowing across the screen felt like the right balance between mathematical elegance and visual restraint.
How It Works
The animation is a Svelte component (FourierCanvas.svelte) rendered as a client-side island inside an Astro layout. It uses the HTML5 Canvas API to draw 6 horizontal sine waves at different vertical positions, each flowing at its own speed and direction.
Each wave is defined by a few simple parameters:
const waves = [
{ y: 0.12, amp: 22, freq: 0.0025, speed: 0.35, opacity: 0.045 },
{ y: 0.28, amp: 28, freq: 0.0020, speed: -0.25, opacity: 0.040 },
// ... 4 more waves
];
- y - vertical position as a fraction of viewport height
- amp - wave amplitude in pixels
- freq - spatial frequency (how tight the oscillation is)
- speed - how fast the wave flows (negative = leftward)
- opacity - transparency of the white stroke
Every frame, each wave gets a “breathing” multiplier - a slow sinusoidal modulation of its amplitude - so the waves gently pulse over time rather than looking static.
const breath = 1 + 0.12 * Math.sin(time * 0.4 + wave.y * 5);
Rendering
For each wave, two passes are drawn:
- Filled region - a very faint white fill from the wave curve down to the bottom of the canvas, creating a layered depth effect
- Stroke line - a crisp 1px white line tracing the sine curve itself
The waves are all white (rgba(255, 255, 255, ...)) at low opacity, which works cleanly on the dark #161717 background and doesn’t fight with the content on top.
Performance
The animation is intentionally lightweight:
- 6 waves only - enough for visual interest, not enough to tax the GPU
- 4-6px step size - we don’t need to plot every pixel;
lineTointerpolates smoothly requestAnimationFrame- the browser handles 60fps scheduling natively- Debounced resize - window resize recalculates dimensions on a 150ms timeout, not every frame
- DPR-aware - canvas resolution scales with
devicePixelRatio(capped at 2x) for crisp rendering on retina displays without wasting GPU on 3x+ screens - Fixed positioning - the canvas is
position: fixedat100vw x 100vh, covering the full viewport with zero layout cost
Tech Stack
- Astro for static site generation
- Svelte for the interactive canvas component (
client:only="svelte") - Vanilla CSS with CSS custom properties for theming
- Cloudflare Pages for deployment
The result? A clean, breathing background that adds life to the page without getting in the way.