tmlib.js で気持ち良い変形アニメーションするボタンを作ってみた

コリスさんの 『[[CSS]これから増えてきそうなMaterial Designにぴったり！気持ちいいアニメーションで表示させるモーダルコンテンツ](http://coliss.com/articles/build-websites/operation/css/morphing-modal-window-by-codyhouse.html)』
ってやつを見て良いなと思ったので tmlib.js でも作ってみました!!

画面遷移とかにつかうと便利ですね♪

## Demo

ボタンを押すとサークルに変形して画面いっぱいに広がります.
もう一度クリックすると元に戻ります.

[runstant](http://goo.gl/Hb2wR3)

## Code

```js
/*
 * # tutorial - tmlib.js
 * tmlib.js のチュートリアルです.
 * http://phi-jp.github.io/tmlib.js/tutorial.html
 */

var SCREEN_WIDTH    = 640;              // スクリーン幅
var SCREEN_HEIGHT   = 960;              // スクリーン高さ
var SCREEN_CENTER_X = SCREEN_WIDTH/2;   // スクリーン幅の半分
var SCREEN_CENTER_Y = SCREEN_HEIGHT/2;  // スクリーン高さの半分
var ASSETS = {
    "player": "http://jsrun.it/assets/s/A/3/j/sA3jL.png",
    "bg": "http://jsrun.it/assets/a/G/5/Y/aG5YD.png",
};

// main
tm.main(function() {
    // キャンバスアプリケーションを生成
    var app = tm.display.CanvasApp("#world");
    // リサイズ
    app.resize(SCREEN_WIDTH, SCREEN_HEIGHT);
    // ウィンドウにフィットさせる
    app.fitWindow();
    
    // ローダーで画像を読み込む
    var loading = tm.ui.LoadingScene({
        assets: ASSETS,
        width: SCREEN_WIDTH,
        height: SCREEN_HEIGHT,
    });
    
    // 読み込み完了後に呼ばれるメソッドを登録
    loading.onload = function() {
        // メインシーンに入れ替える
        var scene = MainScene();
        app.replaceScene(scene);
    };
    // ローディングシーンに入れ替える
    app.replaceScene(loading);

    // 実行
    app.run();
});

// シーンを定義
tm.define("MainScene", {
    superClass: "tm.app.Scene",
    
    init: function() {
        this.superInit();
        
        // モーフィングボタンを生成
        var button = MorphingButton({
            fillStyle: "hsl({0}, 60%, 50%)".format(Math.rand(0, 360)),
            cornerRadius: 16,
            fontSize: 48,
            text: 'Button',
        }).addChildTo(this);
        
        button.setPosition(SCREEN_CENTER_X, SCREEN_CENTER_Y);
        // 円が広がりきった際のイベント
        button.oncircled = function() {
            this.app.pushScene(TestScene());
            // シーンが戻ってきたらボタンも戻す
            this.one('resume', function() {
                button.morphDefault();
            });
        }.bind(this);

        button.onpointingover = function() {
            this.app.element.style.cursor = 'pointer';
        }.bind(this);
        button.onpointingout = function() {
            this.app.element.style.cursor = 'auto';
        }.bind(this);
        button.onpush = function() {
            this.app.element.style.cursor = 'auto';
        }.bind(this);
    },
    
    update: function(app) {
    }
});

tm.define("TestScene", {
    superClass: "tm.app.Scene",
    
    init: function() {
        this.superInit();
        
        var label = tm.display.Label('tmlib.js でモーフィングボタンを\n作ってみたよ♪').addChildTo(this);
        label.setPosition(SCREEN_CENTER_X, SCREEN_CENTER_Y);
        label.alpha = 0;
        label.tweener
            .fadeIn(200);
    },
    
    onpointingstart: function() {
        this.app.popScene();
    }
});


tm.define("MorphingButton", {
    superClass: "tm.ui.FlatButton",
    
    init: function(param) {
        this.superInit(param);
        
        this.initialWidth = this.width;
        this.initialHeight = this.height;
        this.initialCornerRadius = this.cornerRadius;
        
        this.on('push', function() {
            this.morphCircle();
        });
    },
    
    // 円形に変形
    morphCircle: function(radius) {
        var circleSize = radius || 1200;
        
        this.setInteractive(false);
        this.label.tweener
            .clear()
            .fadeOut(200)
            ;
        this.tweener
            .clear()
            .wait(300)
            .to({
                width: this.height,
                cornerRadius: this.height/2
            }, 300, 'easeOutQuint')
            .to({
                width: circleSize,
                height: circleSize,
                cornerRadius: circleSize/2
            }, 500, 'easeOutQuint')
            .call(function() {
                this.flare('circled');
                this.setInteractive(true);
            }, this)
            ;
    },
    
    // 元の形に変形
    morphDefault: function() {
        this.setInteractive(false);
        this.label.tweener
            .clear()
            .wait(800)
            .fadeIn(200)
            ;
        
        this.tweener
            .clear()
            .to({
                width: this.initialHeight,
                height: this.initialHeight,
                cornerRadius: this.initialHeight/2
            }, 500, 'easeOutQuint')
            .to({
                width: this.initialWidth,
                cornerRadius: this.initialCornerRadius,
            }, 300, 'easeOutQuint')
            .call(function() {
                this.flare('defaulted');
                this.setInteractive(true);
            }, this)
            ;
    },
});

```



## Tips

### 押したときにイベント登録

今回は `MorphingButton` というクラスを定義して使っています.
`MorphingButton` は tm.ui.FlatButton を継承しているので押すと push イベントが発火します.

init 時に push イベントを登録し, ここで押されたら円形に広がるという処理を登録しています.

```js
        this.on('push', function() {
            this.morphCircle();
        });
```

### 良い感じのタイミングでシーン遷移

ボタンを生成している側で, 広がりきった際に発火するイベント `circled` に
関数を登録しています.

ここで, TestScene を `pushScene()` しています.
そして pushScene したシーンが破棄されたら, ボタンが置いてあるシーン TestScene 側で
`resume` イベントが発火するのでボタンを元に戻す関数 `morphDefault()` を呼んでいます.

これでキレイにアニメーションしながら画面遷移することができます.

```js
        // 円が広がりきった際のイベント
        button.oncircled = function() {
            this.app.pushScene(TestScene());
            // シーンが戻ってきたらボタンも戻す
            this.one('resume', function() {
                button.morphDefault();
            });
        }.bind(this);
```

以上, 備忘録でした!