<canvas id="canvas"></canvas>
body {
  margin: 0;
}

#canvas {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
const SVG = `
<svg xmlns="http://www.w3.org/2000/svg" 
   viewBox="0 0 718 440" width="718" height="440">
<path d="M179.4,433.6h-0.1l167.9-215.5c-23.2,1.5-46.5,1.2-69.7-1.5c-0.3,0.1-0.5,0.1-0.9-0.1c-5.6-0.6-11.3-1.3-16.8-2.2
  c-4.6-0.6-9.4-1.4-14-2.3c-4.4-0.8-8.7-1.7-13-2.6c-4.4-0.9-8.7-1.9-13-3.1c-4.2-1-8.6-2.2-12.8-3.5c-2.8-0.8-5.5-1.7-8.3-2.6
  c-2.2-0.6-4.2-1.3-6.4-2.1c-3.8-1.3-7.8-2.6-11.7-4.1c-8-3-15.9-6-23.6-9.5c-4.1-1.8-8.2-3.7-12.2-5.6c-3.2-1.5-6.3-3.1-9.4-4.9
  c-11.6-5.9-22.6-12.2-32.9-19.1l-17.6-11.6l0.5-0.5C59,123.5,34.9,100.6,13.9,74.7l92.5-78.5c15.3,19.6,35.2,35.1,55.1,49.2l0.3-0.3
  l17.8,10.3c6.4,3.7,13.2,7.4,19.5,10.5c51.6,24.8,108.6,32.7,163.4,24.4c35.2-5.4,69.6-17.5,101-36.3c5.9-3.4,11.2-7,16.6-10.8
  c3-2.8,7.8-7.3,10.7-10.2l20.6-26.4l7.3-9.2l0.4,0.3l93.4,72.8c0,0-8.8,13.5-27.1,45.1c-1.4,3.1-2.8,6.3-3.9,9.6
  c-34.3,93.8-15.9,199,47.9,274.9"/>
</svg>`;

const imageURL = "data:image/svg+xml;base64," + btoa(SVG);

const canvas = document.querySelector("#canvas");
const ctx = canvas.getContext("2d");

canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const gridStep = 10;
const circleRadius = 4;
const customDistance = 120;

(async function () {
  const points = [];
  const maskImage = await loadImg(imageURL);
  if (maskImage.height === 0) throw new Error("svg mask height is 0");

  const scaleMask = Math.min(
    canvas.width / maskImage.width,
    canvas.height / maskImage.height
  );

  const maskCanvas = document.createElement("canvas");
  const maskCtx = maskCanvas.getContext("2d");
  maskCanvas.width = canvas.width;
  maskCanvas.height = canvas.height;

  maskCtx.save();
  maskCtx.translate(maskCanvas.width / 2, maskCanvas.height / 2);
  maskCtx.scale(scaleMask, scaleMask);
  maskCtx.translate(-maskImage.width / 2, -maskImage.height / 2);
  maskCtx.drawImage(maskImage, 0, 0, maskImage.width, maskImage.height);
  maskCtx.restore();

  const maskData = maskCtx.getImageData(
    0,
    0,
    maskCanvas.width,
    maskCanvas.height
  );

  for (let y = 0; y < maskData.height; y += gridStep) {
    for (let x = 0; x < maskData.width; x += gridStep) {
      const index = (y * maskData.width + x) * 4;
      const alpha = maskData.data[index + 3];

      if (alpha > 128) {
        points.push({ x, y, tx: 0, ty: 0 });
      }
    }
  }

  window.addEventListener("mousemove", (e) => {
    const mx = e.clientX;
    const my = e.clientY;

    points.forEach((p) => {
      const deltaX = Math.floor(p.x - mx) * -0.45;
      const deltaY = Math.floor(p.y - my) * -0.45;
      const distance = Math.hypot(deltaX, deltaY);

      if (distance < customDistance) {
        gsap.to(p, { ty: deltaY, tx: deltaX, duration: 0.5, });
      } else {
        gsap.to(p, { ty: 0, tx: 0, duration: 0.6, });
      }
    });
  });
  
  gsap.ticker.add(() => draw(ctx, points));
})();

function draw(ctx, points) {
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
  ctx.beginPath();
  points.forEach(p => {
    ctx.moveTo(p.x + p.tx, p.y + p.ty);
    ctx.arc(p.x + p.tx, p.y + p.ty, circleRadius, 0, 2 * Math.PI);
  });
  ctx.fill();
}

function loadImg(src) {
  return new Promise((resolve) => {
    const img = new Image();
    img.addEventListener("load", () => resolve(img));
    img.crossOrigin = "Anonymous";
    img.src = src;
  });
}

// 

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://unpkg.co/gsap@3/dist/gsap.min.js