このサイトでは、マウスカーソルが光る星になって、動かすと尾を引きます。装飾的な演出ですが、実装には地味な落とし穴がいくつかありました。

追従は「少し遅らせる」と流れ星になる

星本体はポインタの座標をそのまま使わず、毎フレーム現在位置から目標へ少しずつ近づけます。

hx += (mx - hx) * 0.28;
hy += (my - hy) * 0.28;

この補間係数が、演出の性格を決めます。1に近いとただのカーソル、小さすぎると鈍重。0.3前後で「軌跡を描いて飛んでいる」感じになりました。

尾はフレームではなく、移動距離から生やす

最初は描画ループの中でパーティクルを生成していましたが、これには罠があります。非アクティブなタブや低速な環境ではフレームが間引かれ、尾が途切れ途切れになるのです。

そこで、ポインタの移動イベント側で「前回の生成地点からの距離」を測り、一定距離ごとに補間しながら粒を置く方式に変えました。フレームレートに関係なく、速く動かせば長い尾、ゆっくりなら短い尾になります。

「ホバーで拡大するとズレる」の正体

リンクの上でカーソルが大きくなる演出を入れたところ、拡大した瞬間に星の中心がポインタからズレる問題が出ました。

原因は、中央寄せの translate(-50%, -50%) を transform で、拡大を別プロパティの scale で当てていたことです。別プロパティの scale は transform のに適用されるため、「中央寄せしてから拡大」ではなく「全体を拡大」になり、中心が流れてしまいます。

修正は、適用順を自分で制御できるよう、ひとつの transform に統合することでした。

head.style.transform =
  `translate(${hx}px,${hy}px) translate(-50%,-50%) scale(${s})`;

移動 → 中央寄せ → 拡大の順で書けば、どんな倍率でも星の中心はポインタに一致します。transform は「書いた順に右から掛かる」性質を体で覚えた一件でした。

触れない環境では出さない

この手の演出はタッチ端末では意味がないどころか邪魔になります。メディアクエリで「ホバーできて、精密なポインタがある」環境だけに限定し、それ以外は通常のカーソルのままにしています。

matchMedia("(hover: hover) and (pointer: fine)").matches

演出は「全員に見せる」より「効く環境にだけ出す」方が、結果として品が良くなる気がします。