DEV Community

prodiamadmin
prodiamadmin

Posted on

Scroll as a timeline: a 3-billion-year diamond story in WebGL (GSAP + Lenis)

I open-sourced a scroll-driven WebGL piece that scrolls you back three billion years, to the moment a diamond formed, then forward to a finished ring. As you scroll, the stone is cut in front of you, rough crystal to brilliant.

Live: https://www.prodiam.co.za/oldest/
Source (MIT): https://github.com/prodiamadmin/deep-time-diamond

My first post covered the diamond itself. This one is about the part that makes it feel like a film: scroll position IS the timeline.

One timeline, scrubbed by scroll

Everything (camera moves, the diamond rotating, era cross-fades, the copy, the cut) lives on a single GSAP timeline. ScrollTrigger scrubs it, and Lenis smooths the scroll that feeds it:

const lenis = new Lenis({ lerp: 0.08, smoothWheel: !reduced });
lenis.on('scroll', ScrollTrigger.update);
gsap.ticker.add((t) => lenis.raf(t * 1000));

const tl = gsap.timeline({
  scrollTrigger: { trigger: scrollEl, start: 'top top', end: 'bottom bottom', scrub: 1.2 }
});
Enter fullscreen mode Exit fullscreen mode

Because it is one scrubbed timeline, the whole story is reversible: scroll up and the diamond un-cuts itself. No scroll listeners firing callbacks, no state machine. Just a timeline you drag with the wheel.

Acts get weight, not pixels

Each act of the story needs a different amount of room. The deep-time montage (older than the pyramids, older than the first flower) has to breathe; the transitions can be quick. So each act gets a weight, and the page height is the sum:

const WEIGHTS = [1.1, 3.6, 1.4, 1.4, 1.3, 1.7, 1.5];
const total = WEIGHTS.reduce((a, b) => a + b, 0);
scrollEl.style.height = total * 100 + 'vh';
Enter fullscreen mode Exit fullscreen mode

The deep-time act gets about three times the scroll distance of any other beat, so it lands instead of flashing by. Tuning the pacing becomes editing one array.

The cut is one tween

The diamond is a single procedural mesh with a rough-crystal morph target. The entire rough-to-brilliant reveal is one tween that drops the morph influence to zero and turns the optics on at the same instant:

tl.to(diamond.morphTargetInfluences, { 0: 0, duration: w, ease: 'power2.inOut' }, cut)
  .to(material, {
    roughness: 0, transmission: 1, dispersion: 1.25,
    clearcoat: 1, thickness: 2, envMapIntensity: 2.2,
    duration: w, ease: 'power2.inOut'
  }, cut);
Enter fullscreen mode Exit fullscreen mode

Reduced motion

If the visitor prefers reduced motion, I drop the Lenis smoothing and the scrub, so it degrades to a plain, honest page scroll:

const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
// new Lenis({ smoothWheel: !reduced }) and scrub: reduced ? false : 1.2
Enter fullscreen mode Exit fullscreen mode

Why

It was built for ProDiam (https://www.prodiam.co.za), a natural-diamond cutting house in South Africa, to show the three billion years before the ring. The code is MIT, so the same scroll engine works for any deep-time or how-it-is-made story. Fork it and point it at your own story. Feedback welcome, especially on the scroll performance.

Top comments (0)