Code

Canvas

Animation Loop

IntermediaterequestAnimationFrame-driven animation — bouncing balls with gravity, friction, trails, and radial gradient shading. MDN Reference

// Always cancel on destroy to prevent leaks
private rafId = 0;

ngAfterViewInit() {
  const ctx = canvas.getContext('2d')!;
  const step = () => {
    // Semi-transparent clear = motion blur
    ctx.fillStyle = 'rgba(255,255,255,0.18)';
    ctx.fillRect(0, 0, W, H);

    for (const b of balls) {
      b.vy += gravity;    // gravity
      b.vx *= friction;   // air resistance
      b.vy *= friction;

      b.x += b.vx;
      b.y += b.vy;

      // Bounce
      if (b.x - b.r < 0) { b.x = b.r; b.vx = Math.abs(b.vx); }
      if (b.x + b.r > W) { b.x = W - b.r; b.vx = -Math.abs(b.vx); }
      if (b.y + b.r > H) {
        b.y = H - b.r;
        b.vy = -Math.abs(b.vy) * 0.85; // energy loss
      }

      // Radial gradient for 3D look
      const grad = ctx.createRadialGradient(
        b.x - b.r * 0.3, b.y - b.r * 0.3, 2,
        b.x, b.y, b.r
      );
      grad.addColorStop(0, '#fff');
      grad.addColorStop(1, b.color + '88');
      ctx.beginPath();
      ctx.arc(b.x, b.y, b.r, 0, Math.PI * 2);
      ctx.fillStyle = grad;
      ctx.fill();
    }

    this.rafId = requestAnimationFrame(step);
  };
  this.rafId = requestAnimationFrame(step);
}

ngOnDestroy() {
  cancelAnimationFrame(this.rafId); // essential!
}
Key Concepts
  • requestAnimationFrame(callback) — calls callback before the next repaint (~60 fps)
  • cancelAnimationFrame(id) — stops the loop; always call in ngOnDestroy
  • Semi-transparent clear (rgba(255,255,255,0.18)) leaves ghost trails (motion blur)
  • Multiply velocity by friction < 1 each frame for realistic deceleration
  • Add constant gravity to vy each frame to simulate downward acceleration
  • Bounce by negating velocity component when hitting a wall