みかづきブログ・カスタム

基本的にはちょちょいのほいです。

Next.jsで使えるAnimationクラスをつくる 🎥

f:id:kimizuka:20210103011013j:plain

npmで探したら似たようなものがある気もしなくもないですが、すごくシンプルなものが欲しかったので自作しました。
process.browserを使っている点以外はNuxt.jsに依存しているところはないですが、Next.jsで使うことを前提に作ってます。


TypeScrtipt

export type Easing = 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';

export function easeIn (x: number) {
  return 1 - Math.cos(x * Math.PI / 2);
}

export function easeOut(x: number) {
  return Math.sin(Math.PI * x / 2);
}

export function easeInOut(x: number) {
  return (1 - Math.cos(Math.PI * x)) / 2;
}

class Animation {
  public isStop: boolean = false;
  private startValue: number;
  private targetValue: number;
  private duration: number;
  private easing: Easing = 'linear';
  private step: (val: number) => void = function(val: number) {};
  private complete: (val: number) => void = function(val: number) {};
  private promise: Promise<number>;
  private progress:number = 0;

  constructor (param: {
    startValue: number;
    targetValue: number;
    duration: number;
    easing?: Easing;
    step?: (val: number) => void;
    complete?: (val: number) => void;
  }) {
    this.isStop = false;
    this.startValue = param.startValue;
    this.targetValue = param.targetValue;
    this.duration = param.duration;
    this.easing = param.easing || this.easing;
    this.step = param.step || this.step;
    this.complete = param.complete || this.complete;
    this.promise = new Promise((resolve, reject) => {
      const diff = this.targetValue - this.startValue;
      let startTime = 0;

      tick.call(this, startTime);

      function tick(delta) {
        if (!startTime) {
          startTime = delta;
        }

        if (!process.browser) {
          resolve(1);
          return;
        }

        this.progress = Math.min((delta - startTime) / this.duration, 1);

        switch (param.easing) {
          case 'easeIn':
            this.progress = easeIn(this.progress);
            break;
          case 'easeOut':
            this.progress = easeOut(this.progress);
            break;
          case 'easeInOut':
            this.progress = easeInOut(this.progress);
            break;
        }

        this.step(this.progress);

        if (this.isStop) {
          resolve(this.progress);
          return;
        }

        if (this.progress < 1) {
          requestAnimationFrame((delta: number) => {
            tick.call(this, delta);
          });
        } else {
          resolve(1);
        }
      }
    });
    this.promise.then(this.complete);
  }

  public stop(): Animation {
    this.isStop = true;

    return this;
  }
};

export default Animation;



使い方

import { useEffect, useState } from 'react';
import Animation from '../js/Animation';

export default function AnimationPage() {
  const [ animation, setAnimation ] = useState(new Animation({
    startValue: 0,
    targetValue: 0,
    duration: 1
  }));

  useEffect(() => {
    return () => {
      animation.stop();
    }
  }, [animation]);

  function handleClickBtn() {
    setAnimation(new Animation({
      startValue: 0,
      targetValue: 1,
      duration: 1000,
      step: (progress) => {
        if (progress < 1) { // 完了時にも発火するのでprogressが1未満の場合に絞る
          console.log(progress);
        }
      },
      complete: (progress) => {
        if (progress === 1) { // stopを叩いた時にも発火するのでprogressが1の場合に絞る
          console.log('complete');
        }
      }
    }));
  }

  return (
    <div>
      <button onClick={ handleClickBtn }>start</button>
    </div>
  );
}

}

という感じで使えます。

ポイント

イージング

linear、easeIn、easeOut、easeInOutを搭載しています。
easeInはeaseInSin、easeOutはeaseOutSin、easeInOutはeaseInOutSinとすべてSin波からつくってます。

引数

startValue → アニメーション開始時の値(必須)
targetValue → アニメーション完了時の値(必須)
duration → アニメーションの動作期間(必須)
easing → イージング関数(任意)※ デフォルトはlinear

アニメーションの仕組み

requestAnimationFrameに依存しています。なので非対応ブラウザやサーバでは動きません。
毎フレームprogressを計算しstepを実行。1以上になったらcompleteで登録した関数を実行します。
1のときにstepで登録した関数を実装するか否か迷いましたが実行しました。
なので、stepとcompleteを両方登録した場合は二重に発火します。
またこれも迷ったのですが、stopを叩いた時もcompleteが発火するので、completeに渡す関数の引数が1か確認する必要があります。
以上のことから、completeはほぼほぼ出番がなくて、stepの中にif文を書いて使うのが便利かと思われます。

step、complete、stopの仕様は、絶賛迷っているので改変する可能性はありますが、一旦こんな感じで使っていこうと思います。