library / interactive / gravity-points
live - 60fps 1920 x 1080
gravity-points.html - self-contained
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Gravity Points</title>
    <style>
      /*
        Gravity Points
        --------------
        A simplified version of the gravity points experiment.  Hundreds of
        particles drift randomly across the canvas until you click to create
        a gravity point.  Each gravity point attracts nearby particles with a
        force inversely proportional to distance squared.  Multiple points
        can be created, and they decay over time to avoid permanent chaos.
      */
      html, body {
        margin: 0;
        padding: 0;
        height: 100%;
        overflow: hidden;
        background: #222;
        color: #eee;
        font-family: sans-serif;
      }
      #info {
        position: absolute;
        top: 0;
        left: 0;
        padding: 5px 10px;
        font-size: 14px;
        background: rgba(0,0,0,0.4);
      }
      canvas {
        display: block;
      }
    </style>
  </head>
  <body>
    <canvas id="gravity"></canvas>
    <div id="info">Click to add gravity point</div>
    <script>
      (function() {
          const canvas = document.getElementById('gravity');
          const ctx    = canvas.getContext('2d');
          function resize() {
            canvas.width  = window.innerWidth;
            canvas.height = window.innerHeight;
          }
          window.addEventListener('resize', resize);
          resize();
          // Particle class
          class Particle {
            constructor() {
              this.x = Math.random() * canvas.width;
              this.y = Math.random() * canvas.height;
              this.vx = (Math.random() - 0.5) * 0.5;
              this.vy = (Math.random() - 0.5) * 0.5;
              this.radius = 2;
              this.colour = `hsl(${Math.random() * 360}, 60%, 60%)`;
            }
            update(gravityPoints) {
              // Apply gravitational forces
              for (const gp of gravityPoints) {
                const dx = gp.x - this.x;
                const dy = gp.y - this.y;
                const distSq = dx * dx + dy * dy + 0.1;
                const dist = Math.sqrt(distSq);
                const force = gp.strength / distSq;
                this.vx += (dx / dist) * force;
                this.vy += (dy / dist) * force;
              }
              // Dampen velocity slightly
              this.vx *= 0.99;
              this.vy *= 0.99;
              this.x += this.vx;
              this.y += this.vy;
              // Wrap around edges
              if (this.x < -this.radius) this.x = canvas.width + this.radius;
              if (this.x > canvas.width + this.radius) this.x = -this.radius;
              if (this.y < -this.radius) this.y = canvas.height + this.radius;
              if (this.y > canvas.height + this.radius) this.y = -this.radius;
            }
            draw(ctx) {
              ctx.beginPath();
              ctx.fillStyle = this.colour;
              ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
              ctx.fill();
            }
          }
          // Gravity point class
          class GravityPoint {
            constructor(x, y) {
              this.x = x;
              this.y = y;
              this.strength = 200; // gravitational strength
              this.life = 5000; // ms life
              this.birth = performance.now();
            }
            isAlive(now) {
              return now - this.birth < this.life;
            }
          }
          const particles = [];
          const count = 400;
          for (let i = 0; i < count; i++) particles.push(new Particle());
          const gravityPoints = [];
          canvas.addEventListener('click', (e) => {
            const rect = canvas.getBoundingClientRect();
            const x = e.clientX - rect.left;
            const y = e.clientY - rect.top;
            gravityPoints.push(new GravityPoint(x, y));
          });
          function animate() {
            ctx.fillStyle = 'rgba(34,34,34,0.3)';
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            const now = performance.now();
            // Remove expired gravity points
            for (let i = gravityPoints.length - 1; i >= 0; i--) {
              if (!gravityPoints[i].isAlive(now)) gravityPoints.splice(i, 1);
            }
            // Update particles
            for (const p of particles) p.update(gravityPoints);
            // Draw particles
            for (const p of particles) p.draw(ctx);
            // Draw gravity points for reference
            for (const gp of gravityPoints) {
              const alpha = 1 - (now - gp.birth) / gp.life;
              ctx.beginPath();
              ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
              ctx.lineWidth = 2;
              ctx.arc(gp.x, gp.y, 10, 0, Math.PI * 2);
              ctx.stroke();
            }
            requestAnimationFrame(animate);
          }
          animate();
      })();
    </script>
  </body>
</html>

About this animation

Particles attracted to mouse cursor, useful for interesting cursors.

Under the hood

N-Body Physics Attractors

A beautiful demonstration of gravitational physics. The user's mouse (or preset central points) act as massive gravity wells. Nearby particles map their trajectories to orbit or fling past these points based on basic Newtonian math calculations.

Source Code
Source copied
Partner hosting options for the copied effect:
Vercel Netlify Hostinger