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.
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:
- Run at 60fps on any modern browser without jank, even on integrated GPUs
- Use zero external libraries — no Three.js, no GSAP, no dependencies to load
- Work with CSP — all inline scripts and styles must use nonce-based Content Security Policy headers
- Compose seamlessly — a full-screen aurora background behind all page content, with a garden scene at the footer that blends into the aurora through alpha transparency
- Be lightweight — the entire animation system (both shaders, all CSS, all JavaScript) lives in a single PHP file with zero additional asset loads
Architecture Overview
The animation system consists of four layers that composite together:
- Layer 1: Aurora canvas — A
position: fixedfull-screen<canvas>atz-index: 0running a WebGL aurora borealis shader at 45% opacity - Layer 2: Page content — Standard HTML sections with dark semi-transparent backgrounds (
rgba(15,18,33,0.85)) andbackdrop-filter: blur(12px)so the aurora subtly bleeds through - Layer 3: Garden canvas — An in-flow
<canvas>at the page footer running a procedural garden shader with alpha-based top fade, letting the aurora show through as the “sky” - Layer 4: Emoji overlay — An absolutely positioned
<div>over the garden canvas containing 11 animal emojis and 2 fish emojis, animated with JavaScriptrequestAnimationFrame
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
- Multi-octave displacement — Three sine waves at different frequencies (6.0, 10.0, 14.0) are combined to create organic, non-repeating motion
- Time-based animation — The
iTimeuniform drives wave phase offsets, creating continuous motion without any keyframe data - Color banding — Green and blue channels are mixed based on vertical position to create the characteristic green-to-purple gradient of northern lights
- Fixed positioning — The canvas is
position: fixedso it stays behind all content as the user scrolls, creating a parallax-like depth effect
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:
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:
- Pebbles —
noise(p * 50, uv * 35) > 0.78triggers a sandy color mix - Roots —
noise(p * 30 + iTime * 0.03, uv * 20) > 0.74adds slowly shifting dark streaks
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:
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:
The Semi-Circle Pond
The pond is defined as a semi-circle using an ellipse distance function centered horizontally in the scene:
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:
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:
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:
- Phase 0 — Approach: Animals walk rightward from off-screen at 7% per second, each stopping at a designated position near the pond edge (staggered by 2.8% gaps). The frog leads, arriving first.
- Phase 1 — Wait: Once all 11 animals have lined up, the group waits 1.2 seconds. The bob animation continues during the pause.
- Phase 2 — Cross: All animals resume walking together at the same speed, crossing the pond and exiting off the right side of the screen. When the last animal passes 110%, the cycle resets.
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:
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:
- Half-resolution rendering — Both canvases render at
scale = 0.5with DPR capped at 1.5, reducing fill rate by 75% with no visible quality loss for soft procedural effects - Visibility pausing — Both shaders stop their
requestAnimationFrameloops when the tab is hidden, saving battery on mobile devices - Cell-based rendering — Carrots and grass use a grid subdivision approach (checking only
di=-1..1neighboring cells) instead of iterating over every object, keeping the loop count constant regardless of viewport width - No readback — The ground-following system replicates the shader math in JavaScript rather than using
gl.readPixels(), which would stall the GPU pipeline - Single draw call per frame — Each shader is a single full-screen quad (6 vertices, 2 triangles). All scene complexity lives in the fragment shader with no geometry overhead
- No texture loads — Everything is procedurally generated. The page loads zero additional image assets for the animation system
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
- Two custom WebGL shaders creating a living, animated page background with zero external dependencies
- 11 emoji animals (1 frog + 10 ducks) with ground-following physics synchronized to GPU-rendered terrain
- 2 fish swimming in a procedurally generated pond with ripples and depth shading
- Consistent 60fps on modern browsers with half-resolution rendering and visibility pausing
- Full CSP compliance with nonce-based inline scripts and styles
- The entire animation system adds zero additional HTTP requests — no images, no libraries, no external shaders
- Responsive design: terrain, animals, and fish all scale naturally on mobile viewports
- Seamless compositing between aurora background, garden foreground, and page content through alpha blending and backdrop filters
Lessons Learned
- Replicate shader math, don’t read back — Using
gl.readPixels()to sample the ground height would stall the GPU pipeline. Instead, we ported the hash, noise, and smoothstep functions to JavaScript. The duplicate code is worth the 60fps frame budget. - Half resolution is free quality — Soft procedural effects like aurora glow, soil texture, and water ripples look identical at half resolution. The 75% fill rate savings is significant on integrated GPUs and laptop displays.
- Cell-based rendering scales — By dividing the scene into cells and only checking neighboring cells for carrot/grass rendering, the shader maintains constant performance regardless of viewport width. Without this, a 4K display would need to iterate over far more objects.
- Alpha compositing beats CSS tricks — Using WebGL alpha output for the garden-to-aurora transition produces smoother, more natural results than CSS
opacity,mask-image, ormix-blend-mode. The GPU handles the per-pixel alpha natively. - Emoji as sprites is surprisingly effective — Using native emoji for the ducks, frog, and fish means zero image assets, automatic high-DPI rendering, and cross-browser compatibility. The CSS
transform: scaleX(-1)trick flips them to face the direction of travel. - Aspect-ratio correction is essential — Operating in
vec2(uv.x * aspect, uv.y)space ensures circles stay circular and proportions are maintained across different viewport widths. Without this, the pond would stretch into an ellipse on wide monitors. - Visibility API saves battery — Pausing both
requestAnimationFrameloops when the tab is hidden is trivial to implement and eliminates 100% of GPU work while the user isn’t looking. - Keep the scene asymmetric — Having carrots on the right and grass on the left, with the pond slightly off-center, creates a more natural and interesting composition than a symmetric layout. Nature isn’t symmetric.