Back to Blog

Building Animated Sine Waves for My Portfolio

creative-codingsvelteastrocanvas

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:

  1. Filled region - a very faint white fill from the wave curve down to the bottom of the canvas, creating a layered depth effect
  2. 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; lineTo interpolates 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: fixed at 100vw 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.