Copied!
Blog

Building the special effects: shaders, scroll-linked motion, and the other 10%

A grain shader, a magnetic button, a liquid SVG morph — the specific effects that make award sites feel expensive, with the code for each.

← Blog
Abstract glowing neon streaks against a dark background
Photo by Logan Voss on Unsplash

September 21, 2026

Structure, content and layout are most of any site — call it 90% of the work, and it's the part every competent build gets right. The other 10% is the handful of specific effects a visitor actually remembers, and it's small enough to break down effect by effect. Here are three we see constantly in captured sites, with working code for each.

Try it on any site. The free WebGL and Three.js checker shows you whether any page runs WebGL or Three.js, and which version. Paste a URL, no sign-up for your first lookup.

Film-grain / noise overlay

A cheap, full-screen noise shader blended over the page reads as "cinematic" almost instantly — it's one of the highest ratio-of-effort- to-effect tricks available. A single fragment shader, rendered to a fullscreen quad on top of everything else with a screen or overlay blend mode:

uniform float uTime;

float rand(vec2 co) {
  return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453);
}

void main() {
  vec2 uv = gl_FragCoord.xy / resolution.xy;
  float grain = rand(uv + fract(uTime)) * 0.06;
  gl_FragColor = vec4(vec3(grain), grain);
}

Keep the opacity low — 0.04–0.08 is usually enough to read as texture without visibly dithering the page underneath it.

Magnetic buttons

A button that leans toward the cursor as it approaches, then snaps back on mouseleave. No physics library required — just distance-based transform with a spring-ish ease on the way out:

btn.addEventListener("mousemove", (e) => {
  const r = btn.getBoundingClientRect();
  const x = e.clientX - (r.left + r.width / 2);
  const y = e.clientY - (r.top + r.height / 2);
  gsap.to(btn, { x: x * 0.3, y: y * 0.3, duration: 0.3, ease: "power2.out" });
});
btn.addEventListener("mouseleave", () => {
  gsap.to(btn, { x: 0, y: 0, duration: 0.5, ease: "elastic.out(1, 0.4)" });
});

The elastic.out on the return, not the approach, is what sells it — the button following the cursor should feel damped and precise, the snap-back should feel like it's got a little momentum to shed.

Liquid / SVG morph transitions

GSAP's MorphSVGPlugin interpolating between two path shapes is the mechanism behind most "liquid" logo reveals and page transitions — it's quite literally what our own preloader mark does on this site, morphing between six shapes on load. The trick isn't the plugin call, it's authoring path data with a matching point count and winding order between shapes, or the morph interpolates through garbage geometry along the way.

gsap.to(".mark-path", {
  morphSVG: "#shape-2",
  duration: 0.9,
  ease: "power3.inOut",
});
Performance guardrail: GPU-only, and pause off-screen. Every effect above should animate transform/opacity or write to a shader uniform — never layout-triggering properties. And anything running a per-frame loop (a shader, a physics-ish magnetic effect) should pause via IntersectionObserver the moment it scrolls out of view. A noise shader nobody can see is still costing a frame budget if you let it keep rendering.

None of these effects are hard to build once you know exactly what a site is doing — the hard part is usually figuring out which one it is. That's what reverse_engineer_animations and reverse_engineer_webgl exist to answer directly, instead of guessing from a screen recording.