マウスストーカーを作ってみた。作り方もご紹介します。

マウスストーカーというものをご存知でしょうか?
マウスカーソルに追従してくるアイコンなどのこと指します。

昔のホームページではキラキラしたものやキャラクターが追従してきて結構鬱陶しいものが多くありました(笑)

しかし、現在ではお洒落なサイトでよく見かけるようになってきました。主に半透明の丸アイコンが多い印象です。

今回の記事ではマウスストーカーを調べながら作ってみましたのでご紹介します!

マウスストーカーを作ってみた。

今回作成したマウスストーカーの特徴は以下となります。

  • 大きさの違う2つの丸アイコンが時間差で追従
  • リンク(aタグ)にホバーしたときに追従アイコンの大きさと色を変更
  • クリックで追従アイコンを縮小
  • マウスカーソルが画面外に行くと追従アイコンを非表示

デモを用意しましたので御覧ください

PCでご確認ください。

マウスストーカーの作り方

ソースコードの紹介とともに作り方を紹介します。

TweenMaxをインストール

今回ご紹介する方法はアニメーション用ライブラリであるTweenMaxを使用します。

公式サイト

CDN(scriptタグ)の場合

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.1.3/TweenMax.min.js"></script>

NPMの場合

npm i gsap
import {TweenMax} from "gsap/TweenMax";

CSS(scss)を記述

CSSを下記のように記述します。HTMLはJavaScriptで生成するため不要です。

.js-cursor__main {
  pointer-events: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  background: #000;
  border-radius: 50%;
  z-index: 1001;
  opacity: 0;
  transition: opacity .2s;
  .is-moved & {
    opacity: 1;
  }
  .is-outside & {
    opacity: 0;
  }
}

.js-cursor__option {
  pointer-events: none;
  position: fixed;
  top: 0;
  left: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: #ddd;
  z-index: 1000;
  opacity: 0;
  transition: opacity .2s .2s, background .2s;
  .is-moved & {
    opacity: 0.5;
  }
  .is-outside & {
    opacity: 0;
  }
  .is-hover & {
    background: #00FFFF;
  }
}

.js-cursor__mainは小さい黒丸のアイコンです。
.js-cursor__optionは半透明のグレーのアイコンです。

JavaScriptを記述(ES6)

少々長いので後ほど解説します。

class Cursor {

  constructor() {

    const el = `
    <div class="js-cursor">
      <div class="js-cursor__main"></div>
      <div class="js-cursor__option"></div>
    </div>`
    document.body.insertAdjacentHTML('beforeend', el);

    this.wrap_el = document.querySelector('.js-cursor');
    this.main_el = document.querySelector('.js-cursor__main');
    this.option_el = document.querySelector('.js-cursor__option');

    this.position = {
      mouseX: 0,
      mouseY: 0,
      currentX: 0,
      currentY: 0
    }
    this.eventStatus = {
      click: false,
      hover: false
    }

  }

  init() {
    this.attachEvent();
    this.tween();
  }

  attachEvent() {
    //カーソルの位置を取得
    document.addEventListener('mousemove', (e) => {
      this.position.mouseX = e.clientX;
      this.position.mouseY = e.clientY;
      this.wrap_el.classList.add('is-moved');
    });

    //画面外判定
    document.body.addEventListener("mouseleave", () => {
      this.wrap_el.classList.add('is-outside');
    }, false);
    document.body.addEventListener("mouseenter", () => {
      this.wrap_el.classList.remove('is-outside');
    }, false);

    //クリック判定
    document.addEventListener('mousedown', (e) => {
      this.eventStatus.click = true;
    })
    document.addEventListener('mouseup', (e) => {
      this.eventStatus.click = false;
    })

    // aタグのホバー判定
    // 監視ターゲットの取得
    const body = document.body;
    // オブザーバーの作成
    const observer = new MutationObserver(records => {
      let link = document.querySelectorAll('a');
      for (const target of link) {
        target.addEventListener('mouseenter', (e) => {
          this.eventStatus.hover = true;
          this.wrap_el.classList.add('is-hover');
        })
        target.addEventListener('mouseleave', (e) => {
          this.eventStatus.hover = false;
          this.wrap_el.classList.remove('is-hover');
        })
      }
    })
    // 監視の開始
    observer.observe(body, {
      childList: true
    })

  }

  tween() {
    TweenMax.to({}, .001, {
      repeat: -1,
      onRepeat: () => {

        //減速処理
        this.position.currentX += (this.position.mouseX - this.position.currentX) * 0.5;
        this.position.currentY += (this.position.mouseY - this.position.currentY) * 0.5;

        TweenMax.set(this.main_el,
          {
            css: {
              x: this.position.currentX - 5,
              y: this.position.currentY - 5
            }
          });
        TweenMax.to(this.option_el, 0.3,
          {
            css: {
              x: this.position.currentX - 20,
              y: this.position.currentY - 20,
              scale: this.scale(this.eventStatus)
            }
          });
      }
    });
  }

  scale(v) {
    if (v.hover == true && v.click == false) {
      return 1.6
    } else if (v.hover == false && v.click == true) {
      return 0.6
    } else if (v.hover == true && v.click == true) {
      return 0.6
    } else {
      return 1
    }
  }

}

let cursor = new Cursor();
cursor.init();

ソースコードの解説

先程ご紹介したJavaScriptのソースコード解説をします。

HTMLを生成

マウスストーカー用のHTMLを</body>の直前に生成します。

const el = `
    <div class="js-cursor">
      <div class="js-cursor__main"></div>
      <div class="js-cursor__option"></div>
    </div>`
document.body.insertAdjacentHTML('beforeend', el);

マウスカーソルの操作に応じた状態を取得

  • カーソルのX座標、Y座標を取得
  • 画面外(ブラウザの外)に出たかを判定
  • クリック判定
  • aタグのホバー判定

上記4点の状態を取得します。
aタグのホバーに関してはAjaxなどで動的に追加された場合に対応するためMutationObserverを使用しbody内を監視しています。

attachEvent() {
  //カーソルの位置を取得
  document.addEventListener('mousemove', (e) => {
    this.position.mouseX = e.clientX;
    this.position.mouseY = e.clientY;
    this.wrap_el.classList.add('is-moved');
  });

  //画面外判定
  document.body.addEventListener("mouseleave", () => {
    this.wrap_el.classList.add('is-outside');
  }, false);
  document.body.addEventListener("mouseenter", () => {
    this.wrap_el.classList.remove('is-outside');
  }, false);

  //クリック判定
  document.addEventListener('mousedown', (e) => {
    this.eventStatus.click = true;
  })
  document.addEventListener('mouseup', (e) => {
    this.eventStatus.click = false;
  })

  // aタグのホバー判定
  // 監視ターゲットの取得
  const body = document.body;
  // オブザーバーの作成
  const observer = new MutationObserver(records => {
    let link = document.querySelectorAll('a');
    for (const target of link) {
      target.addEventListener('mouseenter', (e) => {
        this.eventStatus.hover = true;
        this.wrap_el.classList.add('is-hover');
      })
      target.addEventListener('mouseleave', (e) => {
        this.eventStatus.hover = false;
        this.wrap_el.classList.remove('is-hover');
      })
    }
  })
  // 監視の開始
  observer.observe(body, {
    childList: true
  })

}

マウスストーカーの状態を更新

TweenMaxを使用し0.001秒毎に状態を更新します。
repeatを-1にすると無限にループします。ループ時にonRepeatが実行されます。

減速処理

現在値 += (ターゲット値 - 現在値) * 減速率

this.position.currentX += (this.position.mouseX - this.position.currentX) * 0.5

と計算することでマウスストーカーがマウスカーソルに近づくにつれて減速します。
この計算式はパララックスや慣性スクロールなどにも使用されたりします。

減速率を小さくするほど遅くなります。

マウスストーカーをマウスカーソルの中心にする

x: this.position.currentX - 5,
y: this.position.currentY - 5

マウスストーカーのサイズは10px * 10pxなのでXY座標を-5することでマウスカーソルの中心にします。

tween() {
  TweenMax.to({}, .001, {
    repeat: -1,
    onRepeat: () => {

      //減速処理
      this.position.currentX += (this.position.mouseX - this.position.currentX) * 0.5;
      this.position.currentY += (this.position.mouseY - this.position.currentY) * 0.5;

      TweenMax.set(this.main_el,
        {
          css: {
            x: this.position.currentX - 5,
            y: this.position.currentY - 5
          }
        });
      TweenMax.to(this.option_el, 0.3,
        {
          css: {
            x: this.position.currentX - 20,
            y: this.position.currentY - 20,
            scale: this.scale(this.eventStatus)
          }
        });
    }
  });
}

ホバーやクリックでのマウスストーカーのサイズ変更

scale: this.scale(this.eventStatus)

ホバーでサイズを1.6倍、クリックで0.6倍に変更する処理です。

scale(v) {
    if (v.hover == true && v.click == false) {
      return 1.6
    } else if (v.hover == false && v.click == true) {
      return 0.6
    } else if (v.hover == true && v.click == true) {
      return 0.6
    } else {
      return 1
    }
  }

以上でソースコードの解説は終わりです。

最後に

マウスストーカーをうまく使用することでサイトをお洒落に見せることができます。この記事が少しでも参考になればと思います。

またソースコードについてですが僕自身書き方が正しいのか?もっとキレイな書き方があるのではいないか?といつも思っています。みなさんはどうなんでしょう?という余談でした。

参考にさせて頂いた記事・サイト

https://www.evoworx.co.jp/blog/mouse-stoker-gsap/
https://www.sonicjam.co.jp/