TypeScript+p5.jsの環境でイージングアニメーションを実装する

## はじめに

Javascriptのイージングアニメーションをさせるライブラリはあるが、p5.js上でどのように適用できるかいまいち分かりませんでした。

シンプルに実装できるみたいだったので、実装してみました。

以下のページにあるJavascriptプログラムを参考にしています。

easing.js
https://gist.github.com/gre/1650294

この記事では、以下のようなことを行っています。

- 参考プログラムをTypeScriptで実装する
- 実際に `位置` と `大きさ` を変えるアニメーションを実装する

__easeInOutElasticでのアニメーション例__
![anim_num.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2278/66f34b17-1f56-292e-6529-8c5d9c1f9dac.gif)

p5.jsをTypeScriptで書く環境を構築する方法については、以下の記事をご参照ください。

TypeScript+webpackでProcessing(p5.js)の環境を構築する
https://qiita.com/uchiko/items/744d7559d37973a959ea


今回のサンプルプログラムは以下にあります。
https://github.com/memememomo/ts-easing-anim-sample


## サンプルプログラムの概要

今回作成したプログラムは、関係性を表すとクラス図に表すと以下のようになっています。

![class.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2278/a27d171a-6bd6-51c8-2a4a-f0bf684bc59f.png)

| 名前 | 概要 |
|-----|-----|
| Tween | イージングアニメーションを制御するクラス。<br>イージング関数を使用してアニメーションを実行する。 |
| Easing | イージング関数の型。<br>イージング関数を実装する場合は、<br>この型に合わせるようにする。 |
| Point | 座標とサイズを持った型。<br>アニメーションの対象になるオブジェクトは、<br>この型に合わせるようにする。 |


## イージング関数の実装

まず、イージング関数の型を以下のように実装しています。

```ts:easing.ts
type Easing = (t: number) => number;
```

この型に合わせてそれぞれのイージング関数を実装します。

```ts:easing.ts
export const easeIn = p => t => Math.pow(t, p);
export const easeOut = p => t => 1 - Math.abs(Math.pow(t - 1, p));
export const easeInOut = p => t =>
    t < 0.5 ? easeIn(p)(t * 2) / 2 : easeOut(p)(t * 2 - 1) / 2 + 0.5;

export const linear = easeInOut(1);
export const easeInQuad = easeIn(2);
export const easeOutQuad = easeOut(2);
export const easeInOutQuad = easeInOut(2);
export const easeInCubic = easeIn(3);
export const easeOutCubic = easeOut(3);
export const easeInOutCubic = easeInOut(3);
export const easeInQuart = easeIn(4);
export const easeOutQuart = easeOut(4);
export const easeInOutQuart = easeInOut(4);
export const easeInQuint = easeIn(5);
export const easeOutQuint = easeOut(5);
export const easeInOutQuint = easeInOut(5);
export const easeInElastic = t => (0.04 - 0.04 / t) * Math.sin(25 * t) + 1;
export const easeOutElastic = t => ((0.04 * t) / --t) * Math.sin(25 * t);
export const easeInOutElastic = t =>
    (t -= 0.5) < 0
        ? (0.02 + 0.01 / t) * Math.sin(50 * t)
        : (0.02 - 0.01 / t) * Math.sin(50 * t) + 1;
```

イージング関数を使用する流れは以下のようなイメージになります。

```ts:sample.ts
// アニメーション開始時刻から現在まで、どのくらいの時間が経過しているか計算
const diff = Date.now() - startTime;

// 全体のアニメーション再生時間から何割の時間が経過しているか計算
const t = diff / duration;

// イージング関数で計算
const rate = easingFunc(t);

// 線形補間で現在の値を算出。位置を変化させる場合は現在位置を算出することになる
const currentPos = startPos + (destPos - startPos) * rate;
```


## アニメーション対象の実装について

サンプルプログラムでは、以下のような型を実装しています。

```ts:easing.ts
type Point = {
    x?: number;
    y?: number;
    sizeX?: number;
    sizeY?: number;
};
```

イージングアニメーション処理を扱うTweenクラスでは、この型に準じたオブジェクトに関して、アニメーション処理を行います。

サンプルプログラムでは、Ellipseクラスを定義しています。

```ts:ellipse.ts
import * as p5 from "p5";

export class Ellipse {
  constructor(
    private p: p5,
    public x: number,
    public y: number,
    public sizeX: number,
    public sizeY: number
  ) {}

  draw() {
    this.p.push();
    this.p.ellipse(this.x, this.y, this.sizeX, this.sizeY);
    this.p.pop();
  }
}
```

Ellipseクラスは、 `x` `y` `sizeX` `sizeY` のメンバ変数を持っているため、Point型のオブジェクトとして扱えます。


## Tweenクラスの実装

以下のような流れで使用できるTweenクラスを実装しています。

```main.ts
// preload()
ellipsePos = new Ellipse(p, 100, 100, 100, 100);

posEasing = new Tween(ellipsePos);
posEasing.to({ x: 600, y: 600 }, 2000);
posEasing.easing(easeInOutElastic);
posEasing.start();


// draw()
posEasing.update();
ellipsePos.draw();
```

Tweenクラスの実装は以下のようになっています。

```ts:easing.ts
export class Tween {
    startPoint: Point;
    destPoint: Point;
    duration: number;
    easingFunc: Easing;
    startTime: number;
    isMove: boolean;
    delayTime: number;

    constructor(private point: Point) {
        this.startPoint = {
            x: point.x,
            y: point.y,
            sizeX: point.sizeX,
            sizeY: point.sizeY
        };
        this.easingFunc = linear;
        this.isMove = false;
        this.delayTime = 0;
    }

    to(to: Point, duration: number) {
        this.destPoint = to;
        this.duration = duration;
    }

    easing(e: Easing) {
        this.easingFunc = e;
    }

    delay(d: number) {
        this.delayTime = d;
    }

    start() {
        this.startTime = Date.now();
        this.isMove = true;
    }

    update() {
        if (!this.isMove) {
            return;
        }

        const diffTime = Date.now() - this.startTime - this.delayTime;

        if (diffTime < 0) {
            return;
        }

        const rate = this.easingFunc(diffTime / this.duration);

        if (this.destPoint.x != null) {
            this.point.x = lerp(this.startPoint.x, this.destPoint.x, rate);
        }

        if (this.destPoint.y != null) {
            this.point.y = lerp(this.startPoint.y, this.destPoint.y, rate);
        }

        if (this.destPoint.sizeX != null) {
            this.point.sizeX = lerp(
                this.startPoint.sizeX,
                this.destPoint.sizeX,
                rate
            );
        }

        if (this.destPoint.sizeY != null) {
            this.point.sizeY = lerp(
                this.startPoint.sizeY,
                this.destPoint.sizeY,
                rate
            );
        }

        if (diffTime >= this.duration) {
            this.isMove = false;
        }
    }
}
```



## まとめ

シンプルなイージングアニメーションを実装しました。いまのところ必要になる処理はこれで十分まかなえています。

他のライブラリと比べると、まだまだ実装が足りない部分があると思います。もしかしたら、p5js用のライブラリとして作り込むかもしれません。