Help us understand the problem. What is going on with this article?

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

More than 3 years have passed since last update.

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

ハイライト

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

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

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

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

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

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

検討

要件として

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

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

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

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

結論

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

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

要点は以下3点

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

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

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

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の値を投げればタイムラインを任意で
進めたり戻したりできる。

よって、

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(){

  }
}

感想

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

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

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

sa-k0
たまになにかかきます
https://biwanoie.tokyo
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away