p5.js Cursor

// p5 Custom Cursor — Instance Mode

const cursorSketch = function(p) {
  let trail = [];
  let isHover = false;
  let isPressed = false;

  p.setup = function() {
    p.createCanvas(p.windowWidth, p.windowHeight);
    p.noCursor();
  };

  p.draw = function() {
    p.clear();

    // detect hover over clickable elements
    let el = document.elementFromPoint(p.mouseX, p.mouseY);
    isHover = el && (el.tagName === 'A' || el.tagName === 'BUTTON'
      || el.closest('a') || el.closest('button'));

    trail.push({ x: p.mouseX, y: p.mouseY });
    if (trail.length > 12) trail.shift();

    let ballSize = isPressed ? 32 : (isHover ? 52 : 44);
    let r = isHover ? 50 : 200;
    let g = isHover ? 100 : 30;
    let b = isHover ? 220 : 70;

    for (let i = 0; i < trail.length; i++) {
      let alpha = p.map(i, 0, trail.length, 30, 180);
      let sz = p.map(i, 0, trail.length, 12, ballSize);
      p.fill(r, g, b, alpha);
      p.noStroke();
      p.circle(trail[i].x, trail[i].y, sz);
    }

    p.fill(r, g, b);
    p.circle(p.mouseX, p.mouseY, ballSize);
    p.fill(r * 0.6, g * 0.4, b * 0.6);
    let s = ballSize / 44;
    p.circle(p.mouseX - 6*s, p.mouseY - 7*s, 8*s);
    p.circle(p.mouseX + 6*s, p.mouseY - 7*s, 8*s);
    p.circle(p.mouseX, p.mouseY + 5*s, 8*s);

    if (isHover) {
      p.noFill();
      p.stroke(r, g, b, 100);
      p.strokeWeight(2);
      p.circle(p.mouseX, p.mouseY, ballSize + 16);
    }
  };

  p.mousePressed = function() { isPressed = true; };
  p.mouseReleased = function() { isPressed = false; };

  p.windowResized = function() {
    p.resizeCanvas(p.windowWidth, p.windowHeight);
  };
};

new p5(cursorSketch, document.getElementById('cursor-canvas'));

// CSS

#cursor-canvas {
  position: fixed;
  top: 0; left: 0;
  pointer-events: none;
  z-index: 9999;
}
* { cursor: none; }