O hirunewani blog

Canvasを使ってカーソルにエフェクトを付与する

Created at

Canvas APIを利用してマウスに追従するエフェクトを付与する方法を紹介する。

Canvasを使ってカーソルにエフェクトを付与する

Canvasを全体の上に重ねて、マウスイベントに応じてエフェクトを生成させるシンプルな実装例。

<canvas id="cursor-effect"></canvas>

pointer-events: noneを忘れると、Canvasがマウスイベントを奪ってしまうため、注意が必要。

#cursor-effect {
  pointer-events: none;
  position: fixed;
  top: 0;
  left: 0;
}

マウスイベントから情報を受け取り、その情報をCanvasに反映させる。

const canvas = document.querySelector("#cursor-effect");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;

const items = [];

function createItem(x, y, length) {
  items.push({ x, y, length });
}

let x = 0;
let y = 0;

function onMouseMove(event) {
  x = event.clientX;
  y = event.clientY;

  const size = 10;
  createItem(x, y, size);
}

document.addEventListener("mousemove", onMouseMove);

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < items.length; i++) {
    const item = items[i];
    ctx.beginPath();
    ctx.rect(item.x, item.y, item.length, item.length);
    ctx.globalAlpha = (i / items.length) * 0.5;
    ctx.fillStyle = "#000";
    ctx.fill();

    item.length -= 0.1;
    if (item.length <= 0) {
      items.splice(i, 1);
      i--;
    }
  }
  requestAnimationFrame(draw);
}
draw();

画面のリサイズに対応する

これをやらないとCanvasが引き伸ばされてエフェクトが歪むか、一部のみエフェクトが表示されなくなってしまう。

function onResize(event) {
  canvas.width = window.innerWidth;
  canvas.height = window.innerHeight;
}
window.addEventListener("resize", onResize);
onResize();

カーソルの先端ではなく中心にエフェクトを生成する

マウスイベントの座標は、カーソルの先端であるため、そのまま生成するとカーソルの先端からオブジェクトが出ているようなエフェクトになってしまう。

機械的な印象を与えるので、それを避けたい場合は調整すると良い。今回は中心にエフェクトが生成されるように調整した。

function onMouseMove(event) {
  x = event.clientX;
  y = event.clientY;

  const size = 10;
  createItem(x - size / 2, y - size / 2, size);
}

ランダムなエフェクトにMath.random()を避ける

本当にランダムであれば、短期的には偏りが発生する場合もあるため、ランダム関数を利用することでランダムに見えなくなることがある。

また人はランダムでないものの方が心地よく感じる傾向にあるため、見栄えにおいてもランダム関数を安易に利用しない方がいい。

例えば、余りを利用して周期的にエフェクトを生成しても多くの人はランダムに感じる。

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  for (let i = 0; i < items.length; i++) {
    const item = items[i];

    // ...
    item.length -= 0.05 * ((i % 3) + 1);
    if (item.length <= 0) {
      items.splice(i, 1);
      i--;
    }
  }
  requestAnimationFrame(draw);
}

背景色に応じてエフェクトの色を変える

Canvasを上に重ねる仕組みの場合、mix-blend-modeなどを利用して背景色に応じてエフェクトの色を変えることができる。mix-blend-modeを利用している場合、重なっている画像や要素のbackground-colorに応じてエフェクトの色を変えることができる。

#cursor-effect {
  pointer-events: none;
  mix-blend-mode: difference;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 9999;
}

生成されるエフェクトの数を抑制する

throttle関数を使って生成されるエフェクトの数を制限するのでもいいが、適当な変数でカウントして制限した方がシンプルでありコストも低いため、おすすめしない。

let count = 0;

function onMouseMove(event) {
  x = event.clientX;
  y = event.clientY;

  if (count % 5 === 0) {
    const size = 20;
    createItem(x - size / 2, y - size / 2, size);
  }
  count++;
}

移動距離に応じてエフェクトの生成を抑制する

function getDistance(x1, y1, x2, y2) {
  return Math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2);
}

let x = 0;
let y = 0;

function onMouseMove(event) {
  if (getDistance(x, y, event.clientX, event.clientY) < 50) {
    return;
  }
  x = event.clientX;
  y = event.clientY;

  const size = 20;
  createItem(x - size / 2, y - size / 2, size);
}

クリック時に拡散するエフェクトを生成する

クリック地点から周囲のランダムな位置にエフェクトを生成するには、(x, y)を中心とした半径nの円周上の地点(x0, y0)を求めればいい。

function onClick(event) {
  Array.from({ length: 10 }).forEach((_, index) => {
    const x0 = x + index * 3 * Math.sin(Math.PI * 2 * Math.random());
    const y0 = y + index * 3 * Math.cos(Math.PI * 2 * Math.random());
    createItem(x0, y0, index);
  });
}

document.addEventListener("click", onClick);