Posted at

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

More than 1 year has 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(){

}
}



感想

もうちょっと便利になりそうな余地が残りつつも

ド短期のプロジェクトの中で最低限の機能を持ったツールとしては

まぁまぁ使えたのでないかと思う。

実際の動きとか、タイミングの調整などをブラウザみながら

チーム内で検討ができたのはよかった。

バグっぽいことがあったとしても自分でつくってたので

なんとか吸収できるのと、ある程度使いづらい面も自分のせいなので黙認できた。