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

Published at
1256 words
6min read

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);