TimelineMaxとdat.GUIで簡易タイムラインツールをつくって使ってみたので紹介と感想

読むのめんどくさい方は結論から読んで頂ければと。

# ハイライト

three.jsでwebglコンテンツを作りはじめてプロジェクトも中盤に差し掛かった頃...
その時の仕様が

* jsonにイベントの名前とフレームを書いておく
* tickerみたいなものが毎フレーム確認してjsonがあったらイベント飛ばす
* イベントを受け取ったらTweenMaxでオブジェクト動かしたりする

という状態になっていた。

jsonは以下のような感じだった気がする。

```json
{
  "0": "start",
  "40": "test"
}
```

元々、タイミングの変更とかを楽に検討したかったのでタイミングはjsonに持つことに
したが、TweenMaxのパラメーターとか変更できないし実用性には欠けていて
凝った演出をつくるのしんどい、UnityのAnimationみたいなツール欲しい...
となっていた。

# 検討

要件として

* GUIでパラメーターが調整できる
* 調整したパラメーターをコードに移植するのが簡単

の2つが上げられ、それ関連のライブラリを調べてみたところ

* https://github.com/vorg/timeline.js
* https://github.com/zz85/timeliner

などが見つかった。
Demoをしばらくみていたが、アニメーションカーブのバラエティが少ない、
イマイチ使い方がわからない、などの問題があって途方に暮れた。

しょうがないので(時間がなかったので1日くらいでできそうな)簡易的なものを
作ることにした。GUIはとりあえずみんな使ってるしdat.GUIでするとして
アニメーションどうしようか考えていった。

# 結論

gsapを使う準備は元々できていたのでTimelineMaxから試したらdat.GUI
とあわせてなんとなく最低限のものができたのでご紹介

オレオレツールすぎて誰かの役に立つことがあるのかはわからない。

要点は以下3点


## TimelineMaxで任意のタイミングで実行する


```js
const tl = new TimelineLite();
tl.to(this.hoge, 2, {x: 10});
tl.to(this.hoge, 2, {y: 10});
```

TimelineMax,基本的な使い方はこんな感じかと思うんですが、
最後の引数に秒数を入れると時間指定して実行できるらしい。

```js
const tl = new TimelineLite();
tl.to(this.hoge, 2, {x: 10});
tl.to(this.hoge, 2, {y: 10}, 2); //2秒後に実行
tl.to(this.hoge, 2, {z: 10}, 3); //3秒後に実行
```

みたいな感じで書けるみたいなのを見つけた。

## TimelineMaxでのシーケンス管理

progress()関数で0-1の値を投げればタイムラインを任意で
進めたり戻したりできる。

よって、

```js
this.time = 0;
this.obj = new THREE.Mesh(); //mesh等(仮)
const tl = new TimelineLite();
tl.pause(); //自動で進めたくないのでpause();
tl.to(this, 100, {time: 100, ease: Power0.None}); //100秒間のタイムラインをつくる
tl.to(this.obj.position, 2, {x: 10}, 2); //2秒時点から開始
tl.to(this.obj.position, 2, {y: 10}, 4); //4秒時点から開始

const fps = 1000/30;
let progress = 0;
setInterval(()=>{
	progress += fps;
	tl.progress(progress/100); //1-100sを0-1にmapして代入する
}, fps);
```

setIntervalとか使って簡単に書くと(動作確認していない)
こんな感じになるかと思う。
勿論、実際には自動で進める時と、pauseと、シーケンスを手動で動かす機能を
dat.GUIを絡めて実装をした。

## dat.GUIでjsonをsave/load

http://workshop.chromeexperiments.com/examples/gui/#5--Saving-Values
これで、save/loadができるのはわかっていたが、いまいち使い方わからなかったので
適当にググってこちらを参考に(というかほぼパクって)実装。

https://jsfiddle.net/ikatyang/182ztwao/

歯車のボタンを押すとjsonを吐いてくれるので、
ブラウザでいろいろいじって、jsonをコピーしてソースコードにペースト
するというワークフロー(これでも少し面倒だが...)

# Use Case

以下、実際に使っていたコードを
少し改変したもの色々省いてたり、全体のコードの一部だったりするので
これ単体では動かないがなんとなくの雰囲気だけ伝われば、、

```TestState.js
import * as THREE from 'three'
import Ticker from '../Ticker'
import TweenMax from 'gsap'
import State from './State'
import dat from 'three-extras/libs/dat.gui.min.js'

/*
とある簡単なステートパターンのステートクラス
begin()で入って、end()で抜ける。
update()が一定のfps(30fps)で呼ばれる
サンプルなのでthis.scene, this.cameraがこのクラス内の何処かで参照できると仮定する
*/

export default class TestState extends State {
  constructor(manager){
    super(manager);

    //イベント名と変更したいパラメーターを先に定義する
    this.param = {
      test1: {
        time: 2.33,
        duration: 24,
        rotation: Math.PI
      },
      test2: {
        time: 3,
        duration: 2.5
      },
      test3: {
        time: 4.66
      },
      progress: 0.1,
      update: this.tweenUpdate,
      autoUpdate: true
    }

    this.total = 27; //トータルタイム
    this.frame = 0; //最初のフレーム(もちろん0)

    const json = /*なんらかの方法でjsonを入れる*/
    this.gui = new dat.GUI({load: json, preset: "First"});

    //関係ないデータがjsonに入ってるのでそれ以外をdat.GUIに読ませる。(冗長になるので各値ごとにfolderをつくる)  
    Object.keys(this.param).forEach((key)=> {
      if(typeof this.param[key] !== 'object') return;
      const folder = this.gui.addFolder(key);
      this.gui.remember(this.param[key]);

      Object.keys(this.param[key]).forEach((key2)=> {
        folder.add(this.param[key], key2);
      });
    });

    //その他guiに加えたいボタンなどを定義
    //guiで変更した内容を反映するボタン
    this.gui.add(this.param, 'update');
    //自動で進めるかボタン(falseの時は30fpsでタイムラインが進む)
    this.gui.add(this.param, 'autoUpdate');
    //シーケンスバー
    this.gui.add(this.param, "progress", 0.0, this.total).onChange(()=>{
      //0 ~ totalタイム(今回は27s)までのバーをタイムラインシーケンスみたいにする。
      this.tl.progress(this.param.progress/this.total);
    }).listen();
  }

  begin(){
    //timelineつくる。自動で進んでもらっては困るのでpauseする。
    this.tl = new TimelineMax();
    this.tl.pause();
    this.setTween();
  }

  //タイムラインの設定
  setTween(){
    //最初に絶対書くもの(0秒~total秒をprogressの0-1とするため)
    //easeはリニアにすること。(そうしないと一定間隔でシーケンスが進まないため)
    this.tl.to(this, this.total, {"frame": this.total, ease: Power0.easeNone});
    this.param.progress = 0;

    

    //cam zoom out
    const test1 = this.param.test1;
    this.tl.fromTo(this.camera.rotation, test1.duration, {z: 0}, {z: test1.rotation, ease: Power3.easeIn}, test1.time);

    //この仕組みの辛いポイント①としては最初にnewして置かなければならないところ(onStartでsceneにaddする)
    this.obj = new Obj(); //THREE.Meshとか
    const test2 = this.param.test2;
    this.tl.to(this.obj, test2.duration, {opacity: 1, 
      onStart: ()=>{
        //onStartでシーンにaddする
        this.scene.add(this.obj);
      }
    }, test2.time);

    //任意のタイミングでイベントだけ欲しい場合はcall()を使う。
    const test3 = this.param.test3;
    this.tl.call(()=>{
      console.log("test3")
    }, false, false, test3.time);
  }

  //ブラウザで編集した数値を更新すると呼ばれる。一度tlをkillしてからまた設定する
  tweenUpdate(){
    super.tweenUpdate();
    this.reset();
    this.setTween();
  }

  //この仕組みの辛いポイント②としてシーケンスもどしても一度addしたものは消えたりしないのでreset関数は用意する
  reset(){
    this.scene.remove(this.obj);
    TweenMax.killAll();
  }


  //state end
  end(){
    TweenMax.killAll();
  }

  update(){
    if(this.param.autoUpdate == false) return;

    super.update();
    //guiのprogressをfps 1/30だけ進める(表示は0-27sにしたい)
    this.param.progress += 1/30;
    //progress(秒)0-1にmapしたのをtimelinemaxのprogressに入れる。
    this.tl.progress(this.param.progress/this.total);
  }

}
```


```State.js
//ステートクラスのもと  
import TweenMax from 'gsap'

export default class State{
  constructor(manager){
    this.manager = manager;

    this.tweenUpdate = this.tweenUpdate.bind(this);

  }

  begin(){
  }

  end(){
    
  }

  tweenUpdate(){
    if(this.tl != null){
      TweenMax.killAll();
      this.tl.kill();
      this.tl = null;
    }
    this.tl = new TimelineMax();
    this.tl.pause();
  }

  onEvent(e){

  }

  update(){

  }
}
```

#感想

もうちょっと便利になりそうな余地が残りつつも
ド短期のプロジェクトの中で最低限の機能を持ったツールとしては
まぁまぁ使えたのでないかと思う。

実際の動きとか、タイミングの調整などをブラウザみながら
チーム内で検討ができたのはよかった。

バグっぽいことがあったとしても自分でつくってたので
なんとか吸収できるのと、ある程度使いづらい面も自分のせいなので黙認できた。