← all components
#07

Gravity well

A field of particles orbits a calm center. Move your cursor in and it becomes a gravity source, a black hole that bends their paths, pulls them into a spiral, and stretches them into light-streaks before they fall past the event horizon and respawn on the outer ring. Real inverse-square gravity math on a canvas, 60fps.

ReactCanvasJS
// LIVE DEMO

Become the gravity

Move your cursor. Everything orbits, then falls in.

share this component →FacebookX / TwitterLinkedIn
// THE CODE
React
"use client";
import { useEffect, useRef } from "react";

export default function GravityWell() {
  const ref = useRef(null);
  useEffect(() => {
    const cv = ref.current, ctx = cv.getContext("2d");
    if (matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let w, h, raf, cx, cy;
    const DPR = Math.min(devicePixelRatio || 1, 2);
    const mouse = { x: 0, y: 0, active: false };
    let pts = [];

    function resize() {
      const r = cv.getBoundingClientRect(); w = r.width; h = r.height;
      cv.width = w*DPR; cv.height = h*DPR; ctx.setTransform(DPR,0,0,DPR,0,0);
      cx = w/2; cy = h/2;
      const n = Math.min(150, Math.floor(w*h/5500));
      pts = Array.from({ length: n }, () => {
        const ang = Math.random()*Math.PI*2, rad = 40 + Math.random()*(Math.min(w,h)*0.42);
        const x = cx + Math.cos(ang)*rad, y = cy + Math.sin(ang)*rad, sp = 0.9 + Math.random()*0.5;
        return { x, y, px:x, py:y, vx:-Math.sin(ang)*sp, vy:Math.cos(ang)*sp, hue:190 + Math.random()*70 };
      });
    }
    function frame() {
      ctx.fillStyle = "rgba(5,8,16,.18)"; ctx.fillRect(0,0,w,h);
      ctx.globalCompositeOperation = "lighter";
      const gx = mouse.active ? mouse.x : cx, gy = mouse.active ? mouse.y : cy, G = mouse.active ? 2600 : 900;
      for (const p of pts) {
        p.px = p.x; p.py = p.y;
        const dx = gx-p.x, dy = gy-p.y, d2 = Math.max(dx*dx+dy*dy, 140), d = Math.sqrt(d2), a = G/d2;
        p.vx += (dx/d)*a; p.vy += (dy/d)*a; p.vx *= .995; p.vy *= .995;
        p.x += p.vx; p.y += p.vy;
        if (mouse.active && d < 16) {
          const ang = Math.random()*Math.PI*2, rad = Math.min(w,h)*0.45;
          p.x = cx+Math.cos(ang)*rad; p.y = cy+Math.sin(ang)*rad; p.px = p.x; p.py = p.y;
          p.vx = -Math.sin(ang); p.vy = Math.cos(ang);
        }
        const sp = Math.min(Math.hypot(p.x-p.px, p.y-p.py), 22), al = 0.25 + Math.min(sp/22,1)*0.6;
        ctx.strokeStyle = `hsla(${p.hue},95%,65%,${al})`; ctx.lineWidth = 1.6;
        ctx.beginPath(); ctx.moveTo(p.px,p.py); ctx.lineTo(p.x,p.y); ctx.stroke();
        ctx.fillStyle = `hsla(${p.hue},95%,72%,.9)`;
        ctx.beginPath(); ctx.arc(p.x,p.y,1.4,0,7); ctx.fill();
      }
      const g = ctx.createRadialGradient(gx,gy,0,gx,gy, mouse.active?60:30);
      g.addColorStop(0, mouse.active ? "rgba(129,140,248,.5)" : "rgba(56,189,248,.25)");
      g.addColorStop(1, "rgba(0,0,0,0)");
      ctx.fillStyle = g; ctx.beginPath(); ctx.arc(gx,gy, mouse.active?60:30, 0, 7); ctx.fill();
      if (mouse.active) { ctx.globalCompositeOperation = "source-over"; ctx.fillStyle = "#05070d";
        ctx.beginPath(); ctx.arc(gx,gy,9,0,7); ctx.fill(); }
      ctx.globalCompositeOperation = "source-over";
      raf = requestAnimationFrame(frame);
    }
    const onMove = e => { const r = cv.getBoundingClientRect(); mouse.x = e.clientX-r.left; mouse.y = e.clientY-r.top; mouse.active = true; };
    const onLeave = () => mouse.active = false;
    resize(); frame();
    addEventListener("resize", resize); cv.addEventListener("pointermove", onMove); cv.addEventListener("pointerleave", onLeave);
    return () => { cancelAnimationFrame(raf); removeEventListener("resize", resize);
      cv.removeEventListener("pointermove", onMove); cv.removeEventListener("pointerleave", onLeave); };
  }, []);
  return <canvas ref={ref} className="w-full h-[380px] rounded-2xl bg-[#05080f]" />;
}
// HOW IT WORKS
  • 01Each particle is given a tangential velocity so it naturally orbits the center, like a stable solar system.
  • 02Gravity is computed as an inverse-square pull toward the active source (the cursor, or the center when idle), so paths curve realistically.
  • 03Particles are drawn as streaks from their previous to current position, so fast motion near the well stretches into light-trails; ones that cross the horizon respawn on the outer ring.
Want a custom component for your product?
Let's build it →