Vue.jsを使ってシンプルでおしゃれなマウスストーカーを作ってみた 

#つくれるもの
Vue.js上でシンプルでおしゃれなマウスストーカー。
以下リンクから実物を見れます。
※レスポンシブ対応していません。PCからご覧ください。

https://taryotaryo.github.io/mouse_stalker_project/

#執筆の動機
Vue.jsを使用したマウスストーカーを実装した記事が見当たらなかったので、自分の備忘と誰かの参考になればと思い執筆しました。

#使用技術
* VueCLI version 3.9.3

#前提
* HTML、CSS(SCSS)、javaScript、Vue.jsの基礎知識
* Vue cliでプロジェクトフォルダを作成済みの状態からスタートします。
* デフォルトのApp.jsやHelloWorld.vueは初期化して、ブラウザの画面表示は真っ白な状態にします。

# 作成方法
###１．Componentsフォルダに必要なファイルを用意します。
* mouseStalker.vue(マウスストーカーのコンポーネントファイル)
* header.vue(マウスストーカーhover時の動作確認用に作成)

###２．作成したコンポーネントファイルをapp.vueにimportします。
```vuejs:app.vue
<template>
  <div id="app">
    <Mausestalker></Mausestalker>
    <Header></Header>
  </div>
</template>

<script>
import Header from './components/Header.vue'
import Mausestalker from './components/mouseStalker.vue'

export default {
  name: 'App',
  components: {
    Header,
    Mausestalker
  }
}
</script>
```

###３． マウスストーカーのスタイルを定義します。
```vuejs:mouseStalker.vue
<template>
  <div>
    <!--カーソル-->
    <p id="cursor"></p>
    <!--マウスストーカー-->
    <div id="stalker"></div>
  </div>
</template>

<style>
body {
  position: relative;
  height: 900px;
  background-color: #dcdcdc;
  cursor: none; /* bodyに対して設定することでページ全体でデフォルトカーソルを非表示にする*/
  text-align: center;
}

body a:hover{
  cursor: none; /*aタグホバー時のカーソルも非表示にする*/
}

#cursor{
  position: relative; /* 最初はページの左上に配置されるようにする */
  top: 0;
  left: 0;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px; /* カーソルの焦点を中央に合わせる */
  z-index: 3; /*一番手前に来るように*/
  background-color: #000;
  border-radius: 50%;
  opacity: 0; /*開いた瞬間非表示*/
  transition: transform 0.1s;
}

#stalker{
  position: relative;
  width: 40px;
  height: 40px;
  z-index: 2;/*一番手前に来るように*/
  background-color: #89c997;
  border-radius: 50%;
  opacity: 0; /*開いた瞬間非表示*/
  transition: transform 0.1s;
  pointer-events: none; /*マウス直下に常にstalker要素がくるのでホバー要素が働かなくなる。noneにすることでstalkerを無視する*/
}
</style>
```

### ４．header.vueを作成(コピペでOK)
マウスストーカーによるホバー時の動作を確認するために作成します。
HTMLやCSSをできる方には特にエッセンスはないので何も考えずコピペで大丈夫です。

```vuejs:header.vue
<template>
    <header>
      <div class="header-inner">
        <div class="header-home">Home</div>
        <div class="header-list">
            <ul class="header-lists">
                <a><li>About</li></a>
                <a><li>Works</li></a>
                <a><li>Posts</li></a>
                <a><li>Contact</li></a>
            </ul>
        </div>
      </div>
    </header>
</template>

<style lang="scss">
header {
  height: 900px;
  font-weight: bold;
  font-size: 26px;

  .header-home{
    line-height: 3;
    transition: 0.3s;
    &:hover {
      transform: scale(1.5);
    }
  }

  .header-inner{
    display: flex;
    justify-content: space-between;
    margin: 0 auto;
    width: 90%;

    .header-lists {
      list-style: none;
      display: flex;

      li {
        cursor: none;
        border-radius: 5px;
        padding: 10px;
        margin-right: 50px;
        transition: 0.3s;

          &:nth-of-type(4){
            margin-right: 0px;
          }

          &:hover {
            transform: scale(1.5);
          }
      }
    }
  }
}
</style>
```

ここまで設定した時点でページの表示は以下画像の状態になっていると思います。
シンプルなヘッダーだけがあり、CSSで「cursor:none」にしているのでカーソルも見えなくなっています。
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691609/879d286c-7a26-4fb2-0835-9c66cab6ac73.png)

### ５．マウスストーカー機能を実装する
①必要なdataを定義する
②マウスが動くたびにカーソルとマウスストーカーの座標を取得するメソッド(関数)を作成する
③cursorとstalkerに②の変更を適用する

```vuejs:mouseStalker.vue
<template>
  <div>
    <!--カーソル-->
    <p id="cursor" v-bind:style="{ opacity: opacityA, top: mouseY + 'px', left: mouseX + 'px' }"></p>
    <!--マウスストーカー-->
    <div id="stalker" v-bind:style="{ opacity: opacityB, transform: 'translate('+posX+'px,'+posY+'px)' }"></div>
  </div>
</template>

<script>
export default {
  data: function () {
    return {
      mouseX: 0,
      mouseY: 0,
      posX: 0,
      posY: 0,
      opacityA: 0,
      opacityB: 0
    }
  },
  methods: {
    getCursorCoordinate: function (event) {
      this.mouseX = event.pageX
      this.mouseY = event.pageY
      if (this.opacityA === 0) {
        this.opacityA = 1
      }
      setTimeout(function () {
        this.posX = `${this.mouseX - 20}`
        this.posY = `${this.mouseY - 24}`
        if (this.opacityB === 0) {
          this.opacityB = 1
        }
      }.bind(this), 100)
    }
  }
}
</script>
```

```vuejs:App.vue
<template>
  <div id="app" v-on:mousemove="callChildMethod()">
    <Mausestalker ref="child"></Mausestalker>
    <Header></Header>
  </div>
</template>

<script>
import Header from './components/Header.vue'
import Mausestalker from './components/mouseStalker.vue'

export default {
  name: 'App',
  components: {
    Header,
    Mausestalker
  },
  methods: {
    callChildMethod () {
      this.$refs.child.getCursorCoordinate(event)
    }
  }
}
</script>
```

#### ①必要なdataを定義する
* mouseX: カーソルのX座標
* mouseY: カーソルのY座標
* posX: マウスストーカーのX座標
* posY: マウスストーカーのY座標
* opacityA: カーソルの透明度
* opacityB: マウスストーカーの透明度

### ②マウスが動くたびにカーソルとマウスストーカーの座標を取得するメソッド(関数)を作成する
ページ全体を囲むappブロックでのmousemove(マウスの移動を検知)をトリガーにメソッド「getCursorCoordinate」を作動させる。

親コンポーネント「app.vue」からみた子コンポーネント「mouseStalker.vue」メソッドの利用は$refsで実現している。

このメソッドではmousemoveイベントが発生するたびにマウスのXY座標を取得し、mouseX、mouseY(カーソルの座標)に代入、そしてSettimeoutで0.1秒遅れて同じ値をposx、posy(マウスストーカーの座標)に代入している(またopacityが0の場合は1に書き換える。初回のみこの処理をおこなう)。これにより遅れてついてくるマウスストーカーの機能を実現。

### ③cursorとstalkerに②の変更を適用する
v-bind:styleでdataの値とcssプロパティをバインディングします。

cursorのtopプロパティの値をmouseX(mouseeventで取得したマウスのx座標)、leftプロパティをmouseY(mouseeventで取得したマウスのy座標)に紐づけます。

stalkerはcursorを追うように表現するので、transformプロパティの値をposX、posYを紐づけ。

ここまでできたらマウスストーカーの完成です。
黒い点についてくる緑色の玉のことです。
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/691609/ce7be509-a85f-3f89-ebea-89edabd12296.png)

# おまけ(追加実装)
ヘッダー要素にホバーした時にマウスストーカーを拡大する機能を実装します。
流れは以下の通りです。
①Header.vueとmouseStalker.vue間で通信を可能にするイベントバスを作成する
②Header.vueで発生したイベントを伝送する設定を入れる
③mouseStalker.vueで伝送されたイベントを受け取る設定を入れる
④Header.vueから渡ってきた通信をトリガーに発生するマウスストーカーの動作を定義する

### ①Header.vueとmouseStalker.vue間で通信を可能にするイベントバスを作成する

Header.vueでのマウスイベントを拾って、mouseStalker.vueに伝送するための機能を実装します。

まずはmain.jsに以下の一文を追加してください。イベントバス用に空のインスタンスを作成します。
**export const bus = new Vue() // eslint-disable-line**

そのままだとES-lintからmain.js内でbusを使えと怒られるので「// eslint-disable-line」と記載し、この行のES-lintによるチェックを無視します。

```Javascript:main.js
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

export const bus = new Vue() // eslint-disable-line

new Vue({
  render: h => h(App),
}).$mount('#app')
```

### ②Header.vueで発生したイベントを伝送する設定を入れる

main.jsで定義したbusをインポートします。

header.vue要素へのmouseoverイベント、mouseleaveイベントが発生する度にbusにイベントの発生を伝えるようにします。

```vuejs:header.vue
<template>
    <header>
      <div class="header-inner">
        <!-- homeのmouseover時にメソッドmouseEnter、mouseleave時にメソッドmouseLeaveを発火させる --!>
        <div class="header-home" v-on:mouseover="mouseEnter()" v-on:mouseleave="mouseLeave()">Home</div> -->
        <div class="header-list">
            <ul class="header-lists">
                <!-- ヘッダー項目のmouseover時にメソッドmouseEnter、mouseleave時にメソッドmouseLeaveを発火させる --!>
                <a><li v-on:mouseover="mouseEnter()" v-on:mouseleave="mouseLeave()">About</li></a>
                <a><li v-on:mouseover="mouseEnter()" v-on:mouseleave="mouseLeave()">Works</li></a>
                <a><li v-on:mouseover="mouseEnter()" v-on:mouseleave="mouseLeave()">Posts</li></a>
                <a><li v-on:mouseover="mouseEnter()" v-on:mouseleave="mouseLeave()">Contact</li></a> -->
            </ul>
        </div>
      </div>
    </header>
</template>

<script>
import { bus } from '../main.js' //main.jsで定義したbusをimportする
export default {
  methods: {
    //mouseoverの度にbusへイベントの発生を伝える
    mouseEnter: function () {
      bus.$emit('bus-event-onNav') 
    },
    //mouseleaveの度にbusへイベントの発生を伝える
    mouseLeave: function () {
      bus.$emit('bus-event-offNav')
    }
  }
}
</script>
</script>
```
### ③mouseStalker.vueで伝送されたイベントを受け取る設定を入れる
header.vueと同様にmain.jsで定義したbusをインポートします。
bus.$onでイベントを受け取り、第2引数のメソッドを発火させます。
第2引数にはホバー時、非ホバー時の動作を定義したメソッドを指定します。

```vuejs:mouseStalker.vue
<script>
import { bus } from '../main.js' // main.jsで定義したbusをimport
export default {
  data: function () {
    return {
      mouseX: 0,
      mouseY: 0,
      posX: 0,
      posY: 0,
      opacityA: 0,
      opacityB: 0,
      backgroundColor: '#000', // ホバー時にカーソルの色を変える
      onViewActive: false, // 要素にホバーしているかどうか
      zIndex: 2, // マウスストーカーの配置
    }
  },
  created: function () {
    /*Header.vueから伝送されたイベントを受け取り、関数を発火させる。
　　  header要素にホバー時はonNavメソッドを実行
　　　非ホバー時はoffNavメソッドを実行
    */
    bus.$on('bus-event-onNav', this.onNav) 
    bus.$on('bus-event-offNav', this.offNav)
  },
  methods: {
    getCursorCoordinate: function (event) {
      this.mouseX = event.pageX
      this.mouseY = event.pageY
      if (this.opacityA === 0) {
        this.opacityA = 1
      }
      setTimeout(function () {
        this.posX = `${this.mouseX - 20}`
        this.posY = `${this.mouseY - 24}`
        if (this.opacityB === 0) {
          this.opacityB = 1
        }
      }.bind(this), 100)
    },
    // ホバー時
    onNav: function () {
      this.onViewActive = true // ホバーしていることを示す
      this.zIndex = -1 // 要素の後ろに配置
      this.backgroundColor = 'transparent' // カーソルの黒点を透明に
    },
    // 非ホバー時(上記を元の状態に戻す)
    offNav: function () {
      this.onViewActive = false
      this.backgroundColor = '#000'
      this.zIndex = 2
    }
  }
}
</script>
```

### ④Header.vueから渡ってきたイベントをトリガーに発生するマウスストーカーの動作を定義する
v-ifを使い、onViewActiveの真偽値でマウスストーカーのCSSを切り替えられるように設定します。

```vuejs:mouseStalker.vue
<template>
  <div>
    <p id="cursor"
      v-bind:style="{
        opacity: opacityA,
        top: mouseY + 'px',
        left: mouseX + 'px',
        backgroundColor: backgroundColor <!-- ホバー時は透明に -->
      }">
    </p>
    <!-- 非ホバー時のマウスストーカー -->
    <div v-if="!onViewActive" id="stalker"
      v-bind:style="{
        opacity: opacityB,
        transform: 'translate('+posX+'px,'+posY+'px)'
        }">
    </div>
　　<!-- ホバー時のマウスストーカー --!>
    <div v-else-if="onViewActive" id="stalker"
      v-bind:style="{
        zIndex: zIndex, <!-- 要素の後ろに配置 -->
        opacity: opacityB,
        transform: 'translate('+posX+'px,'+posY+'px) scale(2)' <!-- 2倍に拡大 -->
      }">
    </div>
  </div>
</template>
```


これで完成です。

こうした方が良いよなどございましたらコメントくださると幸いです。
閲覧いただきありがとうございました。