thumbnail

インクのようなマウスストーカーの作り方

インクや液体、滴のような動きをするマウスストーカーの作り方を紹介します。
完成形のコードのコピペするだけでも使えますが、コードの意味を理解することによって自分好みのマウスストーカーにカスタマイズできるようになるので表現の幅が広がるはずです。
基礎的な仕組みの説明をした後、カスタマイズした例も記載します。

今回紹介するマウスストーカーのアルゴリズムは、CodePenに公開されているRicardo Mendieta様(@mendieta)の「ink Cursor」のアルゴリズムを借用させていただいております。CodePenのソースコードはMITライセンスになります。そのため、初めに掲示するソースコード全体の上部にMITライセンスを掲載しております

完成形・作りたいもの

「液体」や「インク」と文字にしてもいまいち伝わりにくいと思うので、まずは完成形をご覧ください。

繰り返しになりますが、CodePenに公開されているRicardo Mendieta様(@mendieta)の「ink Cursor」のアルゴリズムの一部分の解説が主となります。次節からプログラムの断片を掲載しますが、ここで掲載したソースコードの一部を切り取ったものの再表示となるため、MITライセンスの掲載は割愛しています。

液体の動きの解説

液体の2つの状態

デモを動かしてみると気づくと思いますが、このマウスストーカー は2種類の状態から成ります。

2つの状態

  1. カーソルが止まっている状態: ストーカーがその場でウネウネ動く
  2. カーソルが動いている状態:カーソルの移動方向に液体が流れるような動き

JavaScriptでマウスの状態を検知してこの2つの状態を切り替えています。

1.カーソルが止まっている状態

2.カーソルが動いている状態

液体の動きの正体

液体の動きは大きく分けて2つステップで構成されています。

2つのステップ

  1. 様々な大きさの円を描画して周期的に動かす(JavaScript)
  2. SVGでフィルタをかけて円をつなげる

1については、SVGフィルタを外すとその挙動が分かります。

1の円をぼかした後、コントラストに対するフィルタをかけることによって、液体のように見せているわけです。

では、次節以降でコードの解説をします。

液体の動きのコード

円の生成

円はDotクラスで管理し、大きい円から小さい円を順に生成して配列に格納します。

const width = 30 //球の大きさ
const amount = 26 //生成する球の数
let dots: Dot[] = [] //生成した球を格納する配列

//円のクラス
class Dot {
  /* 後述 */
}

//円の生成と格納
const buildDots = () => {
  for (let i = 0; i < amount; i++) {
    //iが大きくなるにつれ小さい円になる(後述)
    let dot = new Dot(i)
    dots.push(dot)
  }
}

Dotクラス

続いてDotクラスのプロパティについて詳しくみていきます。

class Dot {
 x = 0 // 現在のx座標
 y = 0 // 現在のy座標
 private anglespeed = 0.05 //円が動くスピード(可変)
 private element = document.createElement('span')
 private lockX = this.x // カーソルが止まった時のx座標
 private lockY = this.y // カーソルが止まった時のy座標
 private scale: number // 円の大きさ
 private range: number // 円が動く範囲
 private angleX = 0  // 0 ~ 2π の乱数:(後述)
 private angleY = 0 // 0 ~ 2π の乱数:(後述)

  constructor(
    private index: number, // 円の番号(0 ~ amount)
  ){
    // 球の大きさ(可変)
    // indexに反比例 = 後に生成された円ほど小さくなる
    this.scale = 1 - 0.04 * this.index

    // 円が動く範囲(可変)
    // scaleに反比例 = indexに反比例 = 小さい円ほど広範囲を動く
    this.range = width / 2 - width / 2 * this.scale + 2
    
    this.element.classList.add(String(this.index))
    gsap.set(this.element, { scale: this.scale });
    cursor?.appendChild(this.element)
  }

  //カーソルが止まった時に呼び出されるメソッド
  public lock() {
    /* 後述 */
  }

  //止まった円の描画
  public draw() {
    /* 後述 */
  }
}


可変と書いてある変数を調整するとマウスストーカーの動きに影響するので、色々と試してみてください。

Dotクラスには、カーソルが静止した時の処理も含まれています。まずはlock()です。

  //カーソルが止まった時に1度だけ呼び出される
  public lock() {
    this.lockX = this.x;//とまったときのx座標の位置
    this.lockY = this.y;//とまったときのy座標の位置
    this.angleX = Math.PI * 2 * Math.random();// (0 ~ 2πの乱数)。カーソルが止まった時に乱数固定
    this.angleY = Math.PI * 2 * Math.random();
  }

カーソルが止まるたびに呼び出され、その都度止まったマウス位置を取得し、0 ~ 2πの乱数を2つ生成します

この2つの乱数は次の描画処理で使うことになります。

カーソルが止まっている時の描画

実際に描画を行っているのがdraw()メソッドです

  /**
   * 止まった円の描画
   * indexがsineDots番目以降の円だけ動かす
   * idle: boolean // マウスが止まっている時true(グローバル変数)
   * sineDots: 動かす円の数(グローバル変数)
   */
  public draw() {
    if (!idle || this.index <= sineDots) {
      gsap.set(this.element, {x: this.x, y: this.y})
    } else { // カーソルが止まっている時の円の描画
      this.angleX += this.anglespeed;//毎フレーム加算される
      this.angleY += this.anglespeed;//同上
      // this.lockX = 0
      this.y = this.lockY + Math.sin(this.angleY) * this.range; //y方向の動き
      this.x = this.lockX + Math.cos(this.angleX) * this.range; //x方向の動き
      gsap.set(this.element, { x: this.x, y: this.y });
    }
  }

コードの通り、全ての円を動かしているわけではありません。sineDotesで定義した数字より大きい番号の円のみ動かしています。

カーソルが止まっている時はelseブロックが実行されます。その中で周期的な動きを実現しているのがsin()とcos()で定義されている三角関数の部分です。

※三角関数については別途記事にする予定です。

ここでの動作を簡単に説明すると、カーソル位置(lockX, lockY)から半径rangeの範囲内を早さanglespeedで周期的に動く球を描画しています。
angleXとangleYは静止するたびに発生する0 ~ 2πの乱数でした。よって、静止するたびにx座標とy座標の三角関数の初期位相が変わるため毎回異なる動きを実現しています。

カーソルが動いている時の描画

// 描画更新
const positionCursor = () => {
  let x = mousePosition.x
  let y = mousePosition.y
  dots.forEach((dot, index, dots) => {
    let nextDot = dots[index + 1] || dots[0]
    dot.x = x
    dot.y = y
    dot.draw()
    //カーソルが動いている時
    // 0.4は可変
    if (!idle || index <= sineDots) {
      const dx = (nextDot.x - dot.x) * 0.4;
      const dy = (nextDot.y - dot.y) * 0.4;
      x += dx;
      y += dy;
  }
  })
}

カーソルが動いている時の処理はif文の中で行なっています。
小さい円になればなるほどdxが蓄積されて動きが遅れる単純な仕組みです。0.4などの係数をかけることで遅延する距離を調整できます。

カーソルを早く動かしすぎると円と円の間が離れすぎるため、マウスストーカー が千切れてしまいます

下に載せたカスタマイズの例で千切れないようにする簡単な例を載せてます。

以上が円のアニメーションに関する説明です。次にHTML側でSVGフィルタをかける処理について簡単に説明します。

円を液体のようにつなげるSVGフィルタ

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="800">
  <defs>
    <filter id="goo">
      <!-- ぼかし -->
      <feGaussianBlur in="SourceGraphic" stdDeviation="6" result="blur" />
      <!-- 透過度に対してコントラストをつける -->
      <feColorMatrix in="blur" mode="matrix" 
        values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 35 -15" result="goo" />
      <feComposite in="SourceGraphic" in2="goo" operator="atop" />
    </filter>
  </defs>
</svg>
  • <feGauusianBlur>で円全体にガウシアンフィルターをかけてぼかす
  • <feColorMatrix>で透過度に対してコントラストをつけることで、全体をつなげて液体のように見せる

す。SVGフィルタやガウシアンフィルタについては別で記事にする予定です。

カスタマイズ

解説してきた各パラメータの意味を理解すれば、カスタマイズは簡単にできます。

応用例

  1. ホバー時にマウスストーカーを拡大したり液体の動く速度を早める
  2. 早く動かしても千切れない
  3. 時間によってアニメーション

時間経過で色が変わる、千切れないインクカーソルのデモです。

まとめ

以上がインクのようなマウスストーカーの解説になります。
コードの内容を理解することによって、様々な応用例が思い浮んだかと思います。
是非参考にしてみてください。