All components

Fractal Glass Parallax Effect

WebGL animation component · Published 2026-07-21 · by vanguardia.dev

Open live demo ↗ Raw prompt (.md)

What it does

Full-viewport Three.js hero where a custom fragment shader refracts the image through vertical fractal-glass ribbons (mod-based stripe displacement averaged over 11 samples, smoothstep-faded at the edges). Moving the mouse feeds a lerped uMouse uniform that drives a horizontal parallax amplified inside the distorted stripes.

How it's built

Category3d-webgl
Techthree
Complexitysection
Performance costheavy
Mobile-safeyes

three webgl shader fractal-glass refraction parallax mousemove hero

Rebuild it with AI

To reproduce this animation in your own project, copy the prompt below into Claude Code, Cursor or any AI coding agent. The prompt is validated — it describes the exact structure, timing and easing, so the agent rebuilds the effect faithfully and you can then adapt colors, copy and layout to your design.

The full prompt

Fractal Glass Parallax Hero — build prompt

Goal

Build a full-viewport hero for a fictional design studio called "Glassform". A single editorial portrait fills the screen, but it is not shown directly: it is rendered on a WebGL plane and refracted through a custom fragment shader that bends the image into dozens of vertical fractal-glass ribbons (like looking through fluted / reeded glass). Moving the mouse feeds a smoothed pointer position into the shader that produces a subtle horizontal parallax, amplified inside the distorted stripes so the ribbons appear to slide over each other. The star effect is the shader itself, driven by a lerped mouse uniform on a requestAnimationFrame loop.

Tech

Vanilla HTML/CSS/JS with ES module imports. Use three (npm) only — there is no GSAP and no scroll library in this component. All motion comes from the Three.js render loop + a GLSL fragment shader. Assume a fresh Vite project; three is installed via npm and imported as import * as THREE from "three".

Split the code into three files:

  • index.html
  • styles.css
  • script.js — the Three.js app (<script type="module" src="./script.js">)
  • Put the GLSL strings in a small shaders.js module that exports vertexShader and fragmentShader, imported by script.js. (Inlining them in script.js is also fine.)

Layout / HTML

nav
  .logo > a            → "Ω Glassform"   (use the Ω ohm sign, HTML entity &#8486;, before the word)
  .nav-links > a ×3    → "Experiments", "Objects", "Exhibits"
section.hero
  img#glassTexture     → src to the hero image, alt=""   (this <img> is hidden via CSS; it is only a source for the WebGL texture)
  .hero-content
    h1                 → "Designed for the space between silence and noise."
    p                  → "Developed by Glassform"

The Three.js renderer's <canvas> is appended to .hero by JS at runtime (it becomes a child of .hero).

Styling

  • Font: Manrope (Google Fonts, variable weight 200..800). body { font-family: "Manrope", sans-serif; }.
  • Colors: pure black background #000, pure white text #fff. No other colors in CSS — all color comes from the refracted image.
  • Global reset: * { margin:0; padding:0; box-sizing:border-box; }.
  • h1: font-size: 4rem; font-weight: 500; letter-spacing: -0.1rem; line-height: 1;
  • a, p: color:#fff; text-decoration:none; font-size:0.85rem; font-weight:550; line-height:1; display:inline-block;
  • nav: position:fixed; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-start; z-index:2;
  • nav .nav-links: display:flex; gap:0.75rem;
  • .hero: position:relative; width:100%; height:100svh; overflow:hidden;
  • .hero img#glassTexture: display:none; (critical — the raw <img> must never be visible; it only feeds the texture).
  • .hero-content: position:absolute; left:0; bottom:0; width:100%; padding:2rem; display:flex; justify-content:space-between; align-items:flex-end; (it paints above the static WebGL canvas because it is positioned).
  • .hero-content h1: width:60%;
  • Responsive @media (max-width:1000px): .hero-content { align-items:flex-start; flex-direction:column-reverse; gap:1rem; } and .hero-content h1 { width:100%; }.

The effect (be exact — this is the whole component)

Three.js scene setup
  • scene = new THREE.Scene().
  • camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1) — left/right/top/bottom/near/far. This maps a [-1,1] quad to the full viewport.
  • renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); append renderer.domElement to the .hero container.
  • Geometry: new THREE.PlaneGeometry(2, 2) (fills the whole ortho space). mesh = new THREE.Mesh(geometry, material); scene.add(mesh).
  • Material: THREE.ShaderMaterial with the uniforms below plus the vertex + fragment shaders.
Config constants (exact values — they define the look)
lerpFactor:            0.035   // pointer smoothing per frame
parallaxStrength:      0.1     // base parallax amount (uParallaxStrength)
distortionMultiplier:  10      // how much the ribbons amplify parallax (uDistortionMultiplier)
glassStrength:         2.0     // displacement gain per stripe (uGlassStrength)
glassSmoothness:       0.0001  // sample spacing for the 11-tap blur (uglassSmoothness)
stripesFrequency:      35      // number of vertical glass ribbons (ustripesFrequency)
edgePadding:           0.1     // fade width at left/right edges (uEdgePadding)
Uniforms (initial values)
  • uTexture: null initially, set once the image loads.
  • uResolution: vec2(window.innerWidth, window.innerHeight).
  • uTextureSize: vec2(1, 1) initially, set to the image's natural pixel size on load.
  • uMouse: vec2(0.5, 0.5) (centered).
  • uParallaxStrength = 0.1, uDistortionMultiplier = 10, uGlassStrength = 2.0, ustripesFrequency = 35, uglassSmoothness = 0.0001, uEdgePadding = 0.1.
Texture loading

The hidden #glassTexture <img> is the source. When it is complete (or on its onload), build new THREE.Texture(imgElement), read naturalWidth/naturalHeight into uTextureSize, set texture.needsUpdate = true, and assign it to uTexture. Handle both the already-cached case and the async onload case.

Pointer smoothing (the only "animation" driver)
  • Keep two objects: mouse = {x:0.5, y:0.5} (current, smoothed) and targetMouse = {x:0.5, y:0.5} (raw target).
  • window mousemove: targetMouse.x = e.clientX / innerWidth; targetMouse.y = 1.0 - e.clientY / innerHeight (note the Y flip — WebGL origin is bottom-left).
  • lerp(a, b, f) = a + (b - a) * f.
  • In the render loop every frame: mouse.x = lerp(mouse.x, targetMouse.x, 0.035), same for y, then uMouse.value.set(mouse.x, mouse.y). This gives a slow, floaty follow (~0.035 easing factor).
Render loop

function animate(){ requestAnimationFrame(animate); /* lerp mouse + update uMouse */ renderer.render(scene, camera); } — call animate() once. Runs continuously; no start/stop trigger.

Resize

On window resize: renderer.setSize(innerWidth, innerHeight) and update uResolution to the new size. (Do not touch uTextureSize.)

Vertex shader

Pass-through: a varying vec2 vUv; vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);.

Fragment shader (the fractal-glass refraction — reproduce this math exactly)

Helpers:

  • getCoverUV(uv, textureSize) — emulates CSS background-size: cover. If either textureSize component < 1.0, return uv unchanged. Else: s = uResolution / textureSize; scale = max(s.x, s.y); scaledSize = textureSize * scale; offset = (uResolution - scaledSize) * 0.5; return (uv * uResolution - offset) / scaledSize;
  • displacement(x, num_stripes, strength)modulus = 1.0 / num_stripes; return mod(x, modulus) * strength; (a per-stripe sawtooth ramp).
  • fractalGlass(x) — an 11-tap box blur of the sawtooth: float d = 0.0; for (int i = -5; i <= 5; i++) { d += displacement(x + float(i) * uglassSmoothness, ustripesFrequency, uGlassStrength); } d /= 11.0; return x + d; (i.e. offset X by the averaged displacement — this is what "bends" the image into ribbons).
  • smoothEdge(x, padding) — returns 1.0 in the middle and fades the effect to 0 at the left/right edges: edge = padding; if (x < edge) return smoothstep(0.0, edge, x); else if (x > 1.0 - edge) return smoothstep(1.0, 1.0 - edge, x); return 1.0; (note the descending smoothstep(1.0, 1.0-edge, x) on the right side).

main() order:

  1. vec2 uv = vUv; float originalX = uv.x;
  2. float edgeFactor = smoothEdge(originalX, uEdgePadding);
  3. float distortedX = fractalGlass(originalX);
  4. uv.x = mix(originalX, distortedX, edgeFactor); — apply the ribbon distortion, faded near edges.
  5. float distortionFactor = uv.x - originalX; — how much this pixel got bent.
  6. Parallax:
  7. float parallaxDirection = -sign(0.5 - uMouse.x); (mouse right of center ⇒ +1, left ⇒ −1).
  8. vec2 parallaxOffset = vec2( parallaxDirection * abs(uMouse.x - 0.5) * uParallaxStrength * (1.0 + abs(distortionFactor) * uDistortionMultiplier), 0.0 ); — horizontal only, scaled by pointer distance from center and amplified where the glass is most distorted.
  9. parallaxOffset *= edgeFactor; (also fade parallax at edges).
  10. uv += parallaxOffset;
  11. vec2 coverUV = getCoverUV(uv, uTextureSize);
  12. If any component of coverUV is < 0.0 or > 1.0, coverUV = clamp(coverUV, 0.0, 1.0); (clamp to avoid sampling outside the image).
  13. gl_FragColor = texture2D(uTexture, coverUV);

Net visual result: 35 crisp vertical glass ribbons refracting the portrait, softly faded to the true image at the far-left/far-right ~10% of the width, with the whole field sliding horizontally as the (smoothed) mouse moves off-center — the slide being strongest along the ribbon seams.

Assets / images

  • 1 image sampled at a time, chosen from 4 interchangeable full-bleed source photos. Each is landscape, roughly 16:9 (about 1440×810–1080), and only ever one is wired into #glassTexture — the others are swappable alternates that all read well through the ribbons because each has strong tonal or color contrast. No brand logos or text in any of them.
  • hero image (primary) — a tightly cropped editorial fashion shot: a figure's torso and bent arm in a colorful abstract brushstroke-print garment against a saturated orange backdrop; warm skin tone, multicolor cloth (teal, mustard, deep red, navy, cream).
  • alternate 1 — a minimal still life: a single smooth rounded pebble/pod form nestled in a soft dimpled surface; near-monochrome cool grey-white with gentle shadow gradients.
  • alternate 2 — a cinematic space scene: a lone dark human silhouette standing before a huge glowing orange sun in a deep midnight-blue starfield; very high contrast (bright orange core, dark foreground).
  • alternate 3 — an abstract texture: concentric spiral grooves carved into a white plaster/clay surface; near-monochrome white and pale grey with fine dark line detail.
  • Set the chosen image as the src of #glassTexture. It is the only asset the shader ever samples; the <img> stays display:none. High-contrast subjects (the orange fashion crop and the sun silhouette especially) make the vertical glass ribbons read most clearly.

Behavior notes

  • Desktop, pointer-driven. There is no click, scroll, or keyboard interaction; touch devices simply see the centered (un-parallaxed) refraction.
  • The loop runs forever at rAF cadence; there is no reduced-motion branch in the original (the motion is subtle and pointer-gated — at rest the image sits still and centered).
  • pixelRatio is capped at 2 for performance. The canvas always matches the window size.
  • Cap the effect to WebGL-capable browsers; if uTexture is still null (image not yet decoded) the plane simply renders empty/black until the texture loads.