Building an Animated WebGL Landing Page with Procedural Shaders

How we created CodeFrog’s animated landing page with two custom WebGL fragment shaders, procedural terrain generation, emoji-based animal animations, and a JavaScript physics system that synchronizes DOM elements with GPU-rendered ground contours.

February 2026 WebGL / GLSL / JavaScript codefrog.app Procedural Generation
2
WebGL Shaders
11
Animated Animals
0
External Libraries
60fps
Target Frame Rate

The Challenge

CodeFrog’s landing page needed to feel alive — not just another static marketing site with stock images and gradient backgrounds. We wanted visitors to immediately sense the craft and attention to detail that goes into the product itself. The page needed to:

Architecture Overview

The animation system consists of four layers that composite together:

ShaderThe Aurora Borealis Shader

The aurora shader creates a slowly undulating curtain of light across the full viewport. It uses layered sine waves with varying frequencies and amplitudes to simulate the characteristic curtain-like motion of real aurora borealis.

Key Techniques

Performance

The aurora canvas renders at half resolution (scale = 0.5) with DPR capped at 1.5. Since the effect is a soft, blurred glow, the reduced resolution is imperceptible but cuts GPU fill rate by 75%. The shader also pauses rendering when the browser tab is hidden via the visibilitychange event.

ShaderThe Procedural Garden Shader

The garden scene is a single GLSL ES 1.0 fragment shader that procedurally generates an entire farm landscape: undulating terrain, textured soil, carrots with animated leaf tops, grass blades, and a semi-circular duck pond with water ripples — all in real time on the GPU.

Terrain Generation

The ground contour is defined by a base height (0.62 in UV space) plus two displacement functions:

// Ground wave: base + slow sine + noise for organic undulation float gBase = 0.62; float gw = gBase + 0.015 * sin(p.x * 6.0) + 0.008 * noise(vec2(p.x * 10.0, 0.5));

The sine wave creates gentle rolling hills, while the noise function adds smaller irregularities that prevent the terrain from looking artificial. Both operate in aspect-ratio-corrected coordinate space (p = vec2(uv.x * aspect, uv.y)) so the scene scales correctly at any viewport width.

Soil Rendering

Below the ground line, soil is rendered with a three-color depth gradient (light topsoil, medium earth, dark subsoil) modulated by two noise octaves at different scales. Random pebbles and root-like textures are added via high-frequency noise thresholds:

Carrot Rendering (Cell-Based)

Carrots are rendered using a cell-based approach for efficiency. The horizontal space is divided into cells of width 0.16, and each cell contains one carrot. The carrot body is a tapered shape that narrows from base to tip using a power curve:

// Carrot body: tapered width decreasing with height float w = cmw * (1.0 - pow(t, 0.7)); float c = smoothstep(w, w * 0.2, abs(p.x - cx)); // Orange color gradient from tip to base vec3 cc = mix(vec3(1.0, 0.6, 0.15), vec3(0.8, 0.35, 0.05), t); cc -= 0.03 * sin(t * 45.0); // horizontal ridges

Each carrot has three animated leaf fronds that spread outward and sway with the wind. Carrots only appear on the right side of the pond to leave room for the grass patch on the left.

Grass Blades

The left side of the scene features individually rendered grass blades, each with its own height, width, and sway animation derived from hash-based pseudo-random values. Two blades per cell create density, and dual sine waves at different frequencies simulate wind:

// Per-blade wind sway using two sine waves float sw = 0.006 * sin(iTime * 1.3 + bx * 12.0 + hash(sd) * 6.28) + 0.003 * sin(iTime * 2.1 + bx * 8.0);

The Semi-Circle Pond

The pond is defined as a semi-circle using an ellipse distance function centered horizontally in the scene:

float pdx = (p.x - pondCx) / pondR; float pdy = (gBase - uv.y) / pondR; float pd = pdx * pdx + pdy * pdy; float pondMask = smoothstep(1.0, 0.9, pd) * step(uv.y, gBase);

Water rendering includes depth-based color gradient (shallow turquoise to deep navy), noise-based ripples, a shimmer effect near the surface, and a muddy bank at the edges. The ground contour is also flattened near the pond using a smoothstep function to prevent the sine-wave terrain from pushing above the waterline.

Alpha Compositing

The garden canvas uses {alpha: true, premultipliedAlpha: false} in its WebGL context. The shader outputs alpha based on vertical position:

// Fade to transparent at top, letting aurora show through as sky float topA = smoothstep(0.98, 0.66, uv.y); gl_FragColor = vec4(color, topA);

This creates a seamless transition where the garden’s ground and vegetation are fully opaque at the bottom, gradually becoming transparent toward the top. The aurora borealis shader, rendered on a separate fixed canvas behind the page, shows through as the sky — no image compositing or extra draw calls required.

AnimationThe Duck Lineup System

The garden features 11 emoji animals (1 frog leading 10 ducks) that walk across the scene, line up at the pond’s edge, wait for the group to assemble, then cross together. Each animal follows the shader’s ground contour in real time.

Ground-Following Physics

The core challenge was synchronizing DOM-positioned emoji elements with a GPU-rendered ground contour. Since JavaScript can’t read back from the shader, we replicated the shader’s terrain math in JavaScript:

// JavaScript replica of GLSL hash function function gh(v) { var s = Math.sin(v) * 43758.5453; return s - Math.floor(s); } // JavaScript replica of GLSL 2D noise function n2(px, py) { var ix = Math.floor(px), iy = Math.floor(py); var fx = px - ix, fy = py - iy; fx = fx * fx * (3 - 2 * fx); fy = fy * fy * (3 - 2 * fy); var nn = ix + iy * 157; return (1-fy) * ((1-fx) * gh(nn) + fx * gh(nn+1)) + fy * ((1-fx) * gh(nn+157) + fx * gh(nn+158)); }

The groundAt(xPercent) function computes the same terrain height as the shader — including the sine wave, noise displacement, and pond flattening — for any horizontal position. Each animal’s bottom CSS property is updated every frame to match this computed ground height.

Three-Phase Behavior

The animation runs in a continuous loop with three distinct phases:

CSS Animations

Each animal has a duck-bob CSS animation with staggered delays for natural-looking movement. The frog and ducks use different font sizes (35–42px) to add visual variety. On mobile, sizes scale down proportionally.

AnimationFish in the Pond

Two fish emojis swim back and forth inside the pond using pure CSS animations. The fish-swim keyframes handle horizontal movement and direction flipping:

/* Fish swim left-to-right then flip and return */ @keyframes fish-swim { 0% { left: 38%; transform: scaleX(-1); } 46% { left: 56%; transform: scaleX(-1); } 50% { left: 56%; transform: scaleX(1); } 96% { left: 38%; transform: scaleX(1); } 100% { left: 38%; transform: scaleX(-1); } }

The second fish has a negative animation delay (-4s) so the two fish are offset in their cycle, and a smaller font size to create depth. A separate fish-bob animation adds subtle vertical movement.

DesignPerformance Optimization

Both shaders maintain 60fps on integrated GPUs through several optimizations:

DesignCompositing & Layout Integration

Aurora as Background

The aurora canvas is position: fixed behind all content. Page sections use semi-transparent dark backgrounds with backdrop blur, creating a frosted glass effect where the aurora subtly illuminates edges and gaps between content cards. The footer uses rgba(15,18,33,0.85) with backdrop-filter: blur(12px) to match other section panels.

Garden Blending

The garden canvas sits in normal document flow (not fixed), positioned after the footer. Its WebGL context is initialized with alpha: true and the shader outputs decreasing alpha toward the top. This creates a natural gradient from opaque terrain at the bottom to transparent sky at the top, with the aurora visible through the transparency. No CSS opacity or mix-blend-mode is needed — the compositing happens at the WebGL level.

CSP Compliance

All inline <script> and <style> tags use PHP-generated nonce attributes that match the Content-Security-Policy header. The shader source code is built as a JavaScript string array ([...].join('\n')) rather than loaded from an external file, keeping everything in a single request while remaining CSP-compliant.

Results

Lessons Learned

← Back to Case Studies