TECH DETAILS

Aurora Borealis - Day 20

Advanced Level

Table of Contents

01 / Aurora Curtain Simulation

The Northern Lights appear as glowing curtains of light that wave and undulate across the sky. We simulate this effect using multiple overlapping "curtains", each with its own movement properties.

// Initialize multiple curtain layers const CURTAIN_COUNT = 5; let curtains = []; function initCurtains() { for (let i = 0; i < CURTAIN_COUNT; i++) { curtains.push({ // Horizontal offset for layering offset: i * 150, // Phase offset for wave desynchronization phase: Math.random() * Math.PI * 2, // Wave properties amplitude: Math.random() * 30 + 20, frequency: Math.random() * 0.003 + 0.002, speed: Math.random() * 0.5 + 0.3, // Vertical undulation verticalWave: Math.random() * 0.002 + 0.001, // Color shifting phase colorPhase: Math.random() * Math.PI * 2 }); } }

Layered Depth Effect

Each curtain has slightly different movement parameters. When overlapped with transparency, this creates the illusion of depth and complexity seen in real auroras. The random phase offsets ensure curtains don't move in sync.

02 / Wave Mathematics

Aurora curtains undulate in complex patterns. We combine multiple sine waves to create natural-looking wave motion in both horizontal and vertical directions.

// For each vertical band in the curtain for (let i = 0; i < bandCount; i++) { const x = i * bandWidth; const curtainTime = time * settings.windSpeed * curtain.speed; // Horizontal wave - makes the curtain sway side to side const horizontalWave = Math.sin( i * curtain.frequency * 100 + // Spatial frequency curtainTime + // Time-based motion curtain.phase // Phase offset ) * curtain.amplitude * settings.waveHeight; // Vertical wave - makes top/bottom edges undulate const verticalWave = Math.sin( i * curtain.verticalWave * 100 + curtainTime * 0.7 // Slower vertical motion ) * auroraHeight * 0.3 * settings.waveHeight; // Calculate band position const bandTopY = baseY + horizontalWave + verticalWave; const bandBottomY = baseY + auroraHeight + horizontalWave; }
Wave Composition

Combining waves at different frequencies (spatial and temporal) creates organic-looking motion. The key is slight frequency variations between curtain layers.

03 / Color Interpolation

Auroras shift through colors as you move across the curtain. We smoothly interpolate between palette colors based on position and time.

// Color palettes for different aurora types const palettes = { classic: [ { r: 34, g: 197, b: 94 }, // Green { r: 16, g: 185, b: 129 }, // Teal { r: 52, g: 211, b: 153 } // Emerald ], purple: [ { r: 168, g: 85, b: 247 }, // Purple { r: 139, g: 92, b: 246 }, // Violet { r: 192, g: 132, b: 252 } // Light Purple ] }; // Calculate color index (0-1 range, wrapping) const colorIndex = ( (i / bandCount) + // Position across curtain curtainTime * 0.1 + // Slow color drift over time index * 0.2 // Offset per curtain layer ) % 1; // Find two adjacent colors to blend const color1Index = Math.floor(colorIndex * colors.length) % colors.length; const color2Index = (color1Index + 1) % colors.length; const colorMix = (colorIndex * colors.length) % 1; // Linear interpolation between colors const color = { r: colors[color1Index].r + (colors[color2Index].r - colors[color1Index].r) * colorMix, g: colors[color1Index].g + (colors[color2Index].g - colors[color1Index].g) * colorMix, b: colors[color1Index].b + (colors[color2Index].b - colors[color1Index].b) * colorMix };

Why Linear Interpolation Works

Linear interpolation (lerp) between RGB values creates smooth color transitions. The modulo operator (%) ensures colors wrap around seamlessly, creating a continuous gradient across the aurora.

04 / Vertical Gradient Bands

Each vertical slice of the aurora uses a gradient that fades from transparent at top and bottom to bright in the middle, mimicking how real auroras glow brightest along the curtain's center.

// Create vertical gradient for each band const gradient = ctx.createLinearGradient(x, bandTopY, x, bandBottomY); // Intensity variation - pulsing brightness const localIntensity = ( Math.sin(i * 0.1 + curtainTime * 0.3) * 0.5 + 0.5 ) * settings.intensity; // Aurora fade effect - bright in middle, fades at edges gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0)`); gradient.addColorStop(0.2, `rgba(${r}, ${g}, ${b}, ${localIntensity * 0.3})`); gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, ${localIntensity * 0.6})`); gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${localIntensity * 0.4})`); gradient.addColorStop(0.8, `rgba(${r}, ${g}, ${b}, ${localIntensity * 0.15})`); gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`); // Draw the band ctx.fillStyle = gradient; ctx.fillRect(x - bandWidth/2, bandTopY, bandWidth + 1, bandBottomY - bandTopY);
Band Width Overlap

Adding 1px to band width (bandWidth + 1) prevents visible gaps between adjacent bands caused by floating-point rounding.

05 / Twinkling Starfield

A starry sky backdrop adds to the atmosphere. Each star has its own twinkle cycle using a sine wave with random phase offset.

// Initialize stars with random properties function initStars() { for (let i = 0; i < STAR_COUNT; i++) { stars.push({ x: Math.random() * width, y: Math.random() * height * 0.7, // Upper 70% only size: Math.random() * 2 + 0.5, brightness: Math.random(), twinkleSpeed: Math.random() * 0.02 + 0.01, twinkleOffset: Math.random() * Math.PI * 2 }); } } // Draw twinkling stars function drawStars() { stars.forEach(star => { // Calculate twinkle using sine wave const twinkle = Math.sin( time * star.twinkleSpeed + star.twinkleOffset ) * 0.5 + 0.5; // Map -1..1 to 0..1 const alpha = star.brightness * twinkle * 0.8 + 0.2; // Draw star ctx.beginPath(); ctx.arc(star.x, star.y, star.size, 0, Math.PI * 2); ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`; ctx.fill(); // Add glow for brighter stars if (star.size > 1.5) { const glowGradient = ctx.createRadialGradient( star.x, star.y, 0, star.x, star.y, star.size * 3 ); glowGradient.addColorStop(0, `rgba(255,255,255,${alpha*0.3})`); glowGradient.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = glowGradient; ctx.fill(); } }); }

06 / Performance Optimization

Rendering hundreds of gradient bands every frame can be costly. Here are key optimizations used in the aurora simulation.

// 1. Reduce band count for performance const bandCount = 60; // Balance between smoothness and speed // 2. Use devicePixelRatio for sharp rendering function resizeCanvas() { canvas.width = container.offsetWidth * window.devicePixelRatio; canvas.height = container.offsetHeight * window.devicePixelRatio; ctx.scale(window.devicePixelRatio, window.devicePixelRatio); } // 3. Pre-calculate expensive values outside loops const baseY = height * 0.15; const auroraHeight = height * 0.5; const bandWidth = width / bandCount + 5; // 4. Limit star count and only render in visible area y: Math.random() * height * 0.7 // Stars only where visible // 5. Use requestAnimationFrame for smooth animation function animate() { // Clear and redraw ctx.clearRect(0, 0, width, height); drawBackground(); drawStars(); drawAurora(); drawHorizon(); time += 1; requestAnimationFrame(animate); }
Performance Trade-offs

60 bands provides a good balance - fewer bands look blocky, more bands slow down mobile devices. Test on your target devices and adjust as needed.

Layer Order Matters

Drawing order (background -> stars -> aurora -> horizon) ensures proper visual layering. The aurora is drawn with transparency, so stars show through faintly, adding depth.