DEV Community

Cover image for How I made a scroll-driven "one day" animation with GSAP ScrollTrigger and Canvas (no images)
Andisa Siphosethu
Andisa Siphosethu

Posted on

How I made a scroll-driven "one day" animation with GSAP ScrollTrigger and Canvas (no images)

South Africans know the feeling: 18:00, Stage 4, and the whole street goes dark. I wanted a solar-installer website that shows what a battery backup does instead of just saying it. So I built Sunvault Solar, a demo site where scrolling plays one day of an install, from loadshedding at dusk to a house that stays lit at night.

Live demo: https://sunvault-solar-bice.vercel.app
Code: https://github.com/Andisa05/sunvault-solar

The idea: one function, one number

The whole scene is a single function: render(p), where p is scroll progress from 0 to 1. Given p, it draws the sky, stars, sun, houses, panels and battery for that exact moment. It has no internal state and no timeline of its own, so scrolling backwards just works.

const clamp = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
// progress of one stage inside the whole scroll, 0 → 1
const seg = (p, a, b) => clamp((p - a) / (b - a));
Enter fullscreen mode Exit fullscreen mode

seg() does most of the work. It turns "the part of the scroll between 42% and 60%" into its own 0 → 1 value, which I use for each stage:

const day = Math.min(seg(p, 0.42, 0.6), 1 - seg(p, 0.8, 0.9)); // sunrise, then sunset
const ours = seg(p, 0.85, 0.92);                                 // our house lights come on
Enter fullscreen mode Exit fullscreen mode

The sky colour is a blend between night and day colours based on day, the sun moves along an arc, and stars fade out as it gets lighter.

Wiring it to the scroll

GSAP ScrollTrigger pins the section and scrubs a plain object from 0 to 1. On every update I call render:

const state = { p: 0 };
gsap.to(state, {
  p: 1,
  ease: "none",
  scrollTrigger: { trigger: section, start: "top top", end: "+=400%", pin: true, scrub: 0.8 },
  onUpdate: () => scene.render(state.p),
});
Enter fullscreen mode Exit fullscreen mode

end: "+=400%" means the scene lasts four screen-heights of scrolling, and scrub: 0.8 adds a little smoothing so it feels weighty rather than jumpy. The five captions ("Site survey", "Install day" and so on) switch based on Math.floor(p * 5).

Smooth scrolling with Lenis

Lenis gives the page its smooth, slightly eased scroll. The key is to drive it from GSAP's ticker so both libraries agree on every frame:

const lenis = new Lenis({ lerp: 0.1 });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
Enter fullscreen mode Exit fullscreen mode

I skip Lenis entirely when the visitor has prefers-reduced-motion turned on.

Why canvas instead of video

  • Nothing to download. The scene is about 200 lines of drawing code, not megabytes of video frames.
  • Sharp on every screen. I size the canvas by devicePixelRatio (capped at 2) so it stays crisp on phones.
  • Easy to change. Want nine panels instead of eight, or a different roof colour? Change a number.

One small trick: the star field uses a seeded random generator, so stars stay in the same place on every frame instead of jumping around.

Stack

Next.js 16 (App Router), React 19, GSAP + ScrollTrigger, Lenis and Canvas 2D, deployed on Vercel.

If you're building something similar, or need a site like this for your business, I'm a Full Stack Developer in Gauteng, South Africa: https://andisa-portfolio.vercel.app

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.