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の仕様は、絶賛迷っているので改変する可能性はありますが、一旦こんな感じで使っていこうと思います。