Scroll Canvas

Lesson 8

A <canvas> is redrawn each scroll frame via onUpdate(self) { draw(self.progress) } in the ScrollTrigger config. self.progress (0→1) is passed to the draw function to render the correct visual state. No playback animation — pure scroll-driven canvas rendering.

Source Code script.js
gsap.registerPlugin(ScrollTrigger);

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const progressText = document.getElementById('progressText');

function resize() {
  canvas.width  = canvas.offsetWidth  * window.devicePixelRatio;
  canvas.height = canvas.offsetHeight * window.devicePixelRatio;
  ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
resize();
window.addEventListener('resize', resize);

// Draw function — progress goes 0 → 1
// Morphs a grid of dots: small → large, scattered → grid
function draw(progress) {
  const W = canvas.offsetWidth;
  const H = canvas.offsetHeight;

  ctx.clearRect(0, 0, W, H);

  const cols = 12;
  const rows = 8;
  const cellW = W / cols;
  const cellH = H / rows;

  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      const i = r * cols + c;
      const total = rows * cols;
      const phase = i / total; // each dot has a phase offset

      // dot reveals progressively
      const dotProgress = Math.max(0, Math.min(1, (progress - phase * 0.5) * 2));

      // base position
      const bx = cellW * c + cellW / 2;
      const by = cellH * r + cellH / 2;

      // scatter offset shrinks as progress grows
      const scatter = (1 - dotProgress) * 40;
      const seed = (c * 13 + r * 7) % 20 - 10;
      const x = bx + seed * scatter / 10;
      const y = by + ((r * 3 + c * 2) % 20 - 10) * scatter / 10;

      const maxR = Math.min(cellW, cellH) * 0.35;
      const radius = dotProgress * maxR;

      const alpha = dotProgress;

      ctx.beginPath();
      ctx.arc(x, y, Math.max(0, radius), 0, Math.PI * 2);
      ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
      ctx.fill();
    }
  }
}

draw(0);

ScrollTrigger.create({
  trigger: '.pin-wrap',
  start: 'top top',
  end: '+=2000',
  pin: true,
  scrub: true,
  onUpdate(self) {
    draw(self.progress);
    progressText.textContent = Math.round(self.progress * 100) + '%';
  },
});