LoginSignup
12
6

More than 1 year has passed since last update.

react-springでとにかく楽してオシャンティなマウスストーカーを作る【Next+React+TypeScript】

Posted at

はじめに

(シリーズ記事を更新せずに別記事を投稿するのは)初投稿です。
趣味開発の副産物で色んなマウスストーカーを作ったので、今回はそれの紹介と解説的なことをしていこうと思います。

CSSはめんどい

わい「よし、ちゃちゃっとマウスストーカー実装しちゃりますか」

わい「ちょっと凝ったやつにしたいから、加減速できたり、色が変わるようにしたいねんな」

わい「実装方法考えるのめんどいし、ネットで見つけた他人のコードを †リスペクト† するか」

わい「ふーん。transformがあってtransitionがあって...はえー...」

わい「無理やわ」


人というのは悲しい生き物なので、面倒くさいことは極力避けようとします。
CSSもその一つです。
SCSSを使えば多少は楽になりますが、なんだかなぁ...って思うときがあります。
正直な話、CSSを書くのはめんどいです。

ということで、人類はreact-springを使いましょう。

react-spring is 何

react-springは、Reactの物理ベースなアニメーションライブラリです。
物理ベースと言うだけあって、要素に質量を持たせたりイージングを関数として定義したりばね定数を定義したりすることができます。
公式を見れば分かる通り、比較的簡単に本格的なアニメーションを実装することができます。
また、物理的な情報の設定は、ほとんどデフォルトの値が付いているので、最低限の設定でアニメーションを作成することができます。

実装

今回記事を執筆するにあたって、GitHubにリポジトリを作成しました。
またリポジトリはVercelとも連携してあるので、詰まったときは確認してみてください。

プロジェクトを作る

create-next-appで楽をしましょう。
下記のコマンドで、ちゃちゃっとプロジェクトを作成します。

npx create-next-app stylish-mouse-stalker --ts

そうしたら、react-springを依存関係に追加します。

yarn add react-spring

これで準備は完了です。

シンプルなマウスストーカー

まずはシンプルに、以下の機能を持つマウスストーカーを実装しましょう。

  • マウスに付いてくる
  • ピタッと止まらずに、グググッと止まる(語彙崩壊

これを実装すると、以下のようなマウスストーカーになります。

まずは、マウスを動かすためのカスタムフックを作りましょう。
hooks/useMouseStalker.tsxhooks/mouseEvent.tsxを作成してください。
そうしたら、以下の通りにファイルを変更しましょう。

hooks/useMouseStalker.tsx
import { useSpring } from 'react-spring';
import { Mouse, useMouseMove } from './mouseEvent';

export type SpringConfig = {
  frequency: number;
  damping: number;
};

const useMouseStalker = (initMouse: Mouse, mouseConfig: SpringConfig) => {
  const [springStyles, setSpringStyles] = useSpring(() => ({
    to: initMouse,
    config: mouseConfig,
  }));

  useMouseMove(initMouse, setSpringStyles);

  return springStyles;
};

export default useMouseStalker;
hooks/mouseEvent.tsx
import { useEffect } from 'react';
import { SpringRef } from 'react-spring';

export type Mouse = {
  width: number;
  height: number;
  borderRadius: number;
  opacity: number;
  top: number;
  left: number;
};

const useMouseMove = (initMouse: Mouse, setSpringStyles: SpringRef<Mouse>) => {
  useEffect(() => {
    const listener = (e: MouseEvent) => {
      setSpringStyles.start({
        opacity: 100,
        // initMouse.* / 2 は真ん中合わせ
        top: e.y - initMouse.height / 2,
        left: e.x - initMouse.width / 2,
      });
    };

    window.addEventListener('mousemove', listener);

    // EventListenerを追加したら、クリーンアップ関数で忘れずにremoveする(戒め
    return () => {
      window.removeEventListener('mousemove', listener);
    };
  }, [setSpringStyles, initMouse]);
};

export { useMouseMove };

次に、今作ったカスタムフックをもとに、マウスストーカーのコンポーネントを作成します。

components/MouseStalker.tsx
import { FC, CSSProperties } from 'react';
import { animated } from 'react-spring';
import useMouseStalker from '../hooks/useMouseStalker';

const initMouse = {
  width: 16,
  height: 16,
  borderRadius: 8,
  opacity: 0,
  top: 0,
  left: 0,
};

const springConfig = {
  // 変化の速さ. 大きくすると遅くなる.
  frequency: 0.2,
  // どのタイミングで減速するか. 大きくすると減速の開始が速くなる.
  damping: 2,
};

const mouseStyles: CSSProperties = {
  pointerEvents: 'none',
  position: 'fixed',
  zIndex: 100,
  backgroundColor: 'black',
};

const MouseStalker: FC = () => {
  const springStyles = useMouseStalker(initMouse, springConfig);

  return (
    <animated.div
      style={{
        ...mouseStyles,
        ...springStyles,
      }}
    />
  );
};

export default MouseStalker;

そうしたら、MouseStalkerコンポーネントをCustomAppの適当なところに追加すれば、実装完了です。

pages/_app.tsx
import type { AppProps } from 'next/app';
import { FC } from 'react';
import MouseStalker from '../components/MouseStalker';
import '../styles/globals.css';

const CustomApp: FC<AppProps> = ({ Component, pageProps }: AppProps) => (
  <div>
    <MouseStalker />
    <Component {...pageProps} />
  </div>
);

export default CustomApp;

ここまでできたら、yarn devで実行を確認してみてください。
create-next-appで生成されたページの上に、マウスに追従するようにちょこちょこ動く黒丸が出てきていたら、正常に実行できている証拠です。

さて、これだけでもまぁ十分といえば十分なんですが、できればもう一工夫ほしい所さん。
ということで、一工夫しましょう。

DOM要素のブレンド方法がオシャンティなマウスストーカー

これ以上コード書くのはめんどいけど、ちょっと今のままだと物足りないかなぁという兄貴も大丈夫です。
CSSもといReact.CSSPropertiesには、mixBlendModeなるお手軽オシャンティプロパティが存在します。
これがどんなプロパティかというと、DOM要素のレイヤーのブレンド方法を制御するプロパティです。
言葉だと分かりにくい部分もあると思うので、実際に見てみましょう。
こんなものです。

下にある要素の色によって、マウスストーカーの色が変化しているのが分かると思います。
これが、DOM要素のレイヤーのブレンド方法を制御するということです。

では、実際に実装方法を見ていきましょう。
まず、<body>に色を付与するために、pages/_document.tsxを作成します。
中身は以下の通りです。

pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
import { FC } from 'react';

const CustomDocument: FC = () => (
  <Html lang="en">
    <Head></Head>
    <body style={{ backgroundColor: 'white' }}>
      <Main />
      <NextScript />
    </body>
  </Html>
);

export default CustomDocument;

これをしないと、例えブラウザの背景が白でも、ブレンド方法を制御するCSSプロパティが上手く機能しないので気をつけましょう(大敗

そうしたら、MouseStalkerコンポーネントのスタイルを以下のように変更してください。

components/MouseStalker.tsx
import { animated } from 'react-spring';
import useMouseStalker from '../hooks/useMouseStalker';
const initMouse = {
  width: 16,
  height: 16,
  borderRadius: 8,
  opacity: 0,
  top: 0,
  left: 0,
};
const springConfig = {
  // 変化の速さ. 大きくすると遅くなる.
  frequency: 0.2,
  // どのタイミングで減速するか. 大きくすると減衰の開始が速くなる.
  damping: 2,
};
const mouseStyles: CSSProperties = {
  pointerEvents: 'none',
  position: 'fixed',
  zIndex: 100,
-  backgroundColor: 'black',
+  backgroundColor: 'white',
+  mixBlendMode: 'difference',
};

const MouseStalker: FC = () => {
  const springStyles = useMouseStalker(initMouse, springConfig);
  return (
    <animated.div
      style={{
        ...mouseStyles,
        ...springStyles,
      }}
    />
  );
};
export default MouseStalker;

これだけです。
先に述べた通り、mixBlendMode: 'difference'というのが、ブレンド方法をいい感じにしてくれるCSSプロパティです。
この場合は「除外」という方法DOM要素をブレンドしています。
その他のブレンド方法はこちらを参考にしてみてください。
マウスストーカーには向かないかもしれませんが、hueとかも使いようによってはいい感じになりそうだなぁと思いました。

リンクのホバー時に大きさが変化するマウスストーカー

さて、ここまでは割と簡単に実装できたと思います。
しかし、こんなんじゃ甘ちゃんだぜ...!という兄貴姉貴のために、もうひと頑張りしたマウスストーカーを用意しました。
それが以下です。

先程までの機能に加え、リンクのホバー時に大きさが変化するようになっています。

さて、ではこれをどう実装するのか、順に見ていきましょう。
まずは、リンクにマウスオーバーしたときの副作用フックと、リンクからマウスアウトしたときの副作用フックを作りましょう。

hooks/mouseEvent.tsx
+ import { useEffect } from 'react';
import { SpringRef } from 'react-spring';

export type Mouse = {
  width: number;
  height: number;
  borderRadius: number;
  opacity: number;
  top: number;
  left: number;
};

+ let isOver: boolean | undefined = undefined;
+ 
- const useMouseMove = (initMouse: Mouse, setSpringStyles: SpringRef<Mouse>) => {
+ const useMouseMove = (initMouse: Mouse, setSpringStyles: SpringRef<Mouse>, times: number) => {
  useEffect(() => {
    const listener = (e: MouseEvent) => {
+       if (isOver) {
+         setSpringStyles.start({
+           opacity: 100,
+           // 大きくした分だけ割る値も小さくする
+           top: e.y - initMouse.height / (2 / times),
+           left: e.x - initMouse.width / (2 / times),
+         });
+       } else {
        setSpringStyles.start({
          opacity: 100,
          // initMouse.* / 2 は真ん中合わせ
          top: e.y - initMouse.height / 2,
          left: e.x - initMouse.width / 2,
        });
+       }
    };

    window.addEventListener('mousemove', listener);

    return () => {
      window.removeEventListener('mousemove', listener);
    };
-   }, [setSpringStyles, initMouse]);
+   }, [setSpringStyles, initMouse, times]);
};

+ const useMouseOver = (initMouse: Mouse, setSpringStyles: SpringRef<Mouse>, times: number, tag: string) => {
+   useEffect(() => {
+     const elements = document.querySelectorAll(tag);
+     const listener = () => {
+       setSpringStyles.start({
+         width: initMouse.width * times,
+         height: initMouse.height * times,
+         borderRadius: initMouse.borderRadius * times,
+       });
+ 
+       isOver = true;
+     };
+ 
+     elements.forEach((element) => {
+       element.addEventListener('mouseover', listener);
+     });
+ 
+     return () => {
+       elements.forEach((element) => {
+         element.removeEventListener('mouseover', listener);
+       });
+     };
+   }, [initMouse, setSpringStyles, times, tag]);
+ };
+ 
+ const useMouseOut = (initMouse: Mouse, setSpringStyles: SpringRef<Mouse>, times: number, tag: string) => {
+   useEffect(() => {
+     const elements = document.querySelectorAll(tag);
+     const listener = () => {
+       setSpringStyles.start({
+         width: initMouse.width,
+         height: initMouse.height,
+         borderRadius: initMouse.borderRadius,
+       });
+ 
+       isOver = false;
+     };
+ 
+     elements.forEach((element) => {
+       element.addEventListener('mouseout', listener);
+     });
+ 
+     return () => {
+       elements.forEach((element) => {
+         element.removeEventListener('mouseout', listener);
+       });
+     };
+   }, [initMouse, setSpringStyles, times, tag]);
+ };
+ 
- export { useMouseMove };
+ export { useMouseMove, useMouseOver, useMouseOut };

そうしたら、マウスストーカーを動かすカスタムフックに、以下の変更を加えてください。

hooks/useMouseStalker.tsx
import { useSpring } from 'react-spring';
- import { Mouse, useMouseMove } from './mouseEvent';
+ import { Mouse, useMouseMove, useMouseOver, useMouseOut } from './mouseEvent';

export type SpringConfig = {
  frequency: number;
  damping: number;
};

- const useMouseStalker = (initMouse: Mouse, mouseConfig: SpringConfig) => {
+ const useMouseStalker = (initMouse: Mouse, mouseConfig: SpringConfig, times: number) => {
  const [springStyles, setSpringStyles] = useSpring(() => ({
    to: initMouse,
    config: mouseConfig,
  }));

-   useMouseMove(initMouse, setSpringStyles);
+   useMouseMove(initMouse, setSpringStyles, times);
+   useMouseOver(initMouse, setSpringStyles, times, 'a');
+   useMouseOut(initMouse, setSpringStyles, times, 'a');

  return springStyles;
};

export default useMouseStalker;

最後に、リンクホバー時にマウスを何倍まで拡大するかを引数に入れましょう。
だいたい3くらいがちょうどいいかなぁと思います。

components/MouseStalker.tsx
import { FC, CSSProperties } from 'react';
import { animated } from 'react-spring';
import useMouseStalker from '../hooks/useMouseStalker';

const initMouse = {
  width: 16,
  height: 16,
  borderRadius: 8,
  opacity: 0,
  top: 0,
  left: 0,
};

const springConfig = {
  // 変化の速さ. 大きくすると遅くなる.
  frequency: 0.2,
  // どのタイミングで減速するか. 大きくすると減衰の開始が速くなる.
  damping: 2,
};

const mouseStyles: CSSProperties = {
  pointerEvents: 'none',
  position: 'fixed',
  zIndex: 100,
  backgroundColor: 'white',
  mixBlendMode: 'difference',
};

const MouseStalker: FC = () => {
-   const springStyles = useMouseStalker(initMouse, springConfig);
+   const springStyles = useMouseStalker(initMouse, springConfig, 3);

  return (
    <animated.div
      style={{
        ...mouseStyles,
        ...springStyles,
      }}
    />
  );
};

export default MouseStalker;

お疲れさまです、これで実装は完了です。

おわりに

今回はCSSの代わりにreact-springを使ってマウスストーカーを作成してみました。
今後のWeb開発でCSSがもっと使いやすくなる...もしくはCSSの代替となる新しい技術が出ることを願っています。
またね。

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6