FlashCC でパブリッシュした CreateJS ファイルで callback 付きの gotoAndPlay を実現する

ご世間様は[アレ](https://github.com/PexJS/PexJS)に夢中な中で、CreateJSさんのニッチな話題です。

## 簡単な機能要件

### 必須

- TARGET_LABEL を再生したとき、TARGET_LABEL_end まで辿り着いた瞬間に callback が発火すること
- Flash 内のスクリプトの動作に影響を及ぼさないこと
- CreateJS 本体の改造は出来る限りしないこと。ただし、後から prototype を拡張したり弄ったりするのは OK とする（した）
- Flash 内スクリプトでも、外部のスクリプトからでも呼べるようにすること

### 努力目標

- 再生中、任意のタイミングで gotoAnd(Play|Stop) もしくは同メソッドが呼ばれた場合は、callback にエラーを渡して発火すること


## アプローチと失敗例

1〜2日うだうだやった結果の発表をさせていただきます。

ユースケースなど色々考えた結果、 createjs.MovieClip.prototype へ gotoAndPlayWithCallback というメソッドを生やすという方向性です。

※サンプルは、エラーとか例外処理とか色々省いてます

### 1. timeline の change イベントを使って終了を検知する(ほぼ失敗)

```javascript
createjs.MovieClip.prototype.gotoAndPlayWithCallback = function(key, callback) {
    var that = this;
    var timeline = this.timeline;
    var startLabelPos = timeline.resolve(key);
    var endLabelPos = timeline.resolve(key + '_end');

    if (endLabelPos === null) {
        console.error('Undefined end label.');
    }
    timeline.addEventListener('change', _callbackHandler);
    return this.gotoAndPlay(key);

    function _callbackHandler(e) {
        if (e.target.position >= endLabelPos) {
            timeline.removeEventListener('change', _callbackHandler);
            callback.call(that);
        }
    }
};

```

このコードで大枠は問題ない。再生は開始し、きちんと callback も呼ばれる。そう思っていた時期が僕にもありました。

しかし、以下の問題点を抱える。

#### 問題点

- Flash 内スクリプトが endLabelPos の位置に存在し、その中で gotoAnd(Play|Stop) されると、その瞬間に timeline.position が変化するので callback が永遠に発火しない 
- 努力目標は達成できない上に、上の条件でも起こるがどこからか gotoAndHoge を呼ばれるとイベントが走り続ける（頑張ればできるけど、前述のがあるので考えるのをやめた）


### 2. timeline に addTween する(ほぼ失敗)

```javascript
createjs.MovieClip.prototype.gotoAndPlayWithCallback = function(key, callback) {
    var that = this;
    var timeline = this.timeline;
    var endLabelPos = timeline.resolve(key + '_end');

    if (endLabelPos === null) {
        console.error('Undefined end label.');
    }
    var tween = createjs.Tween.get(this).wait(endLabelPos).call(_callbackHandler);

    timeline.addTween(tween);
    return this.gotoAndPlay(key);

    function _callbackHandler() {
        timeline.removeTween(tween);
        callback.call(that);
    }
};
```

このコードで大枠はｍ(ry

#### 問題点

- ほとんど 1 と一緒。先に gotoAnd(Play|Stop) が走ると、後から登録した tween のスクリプトは発火しない。理由は後述


### 番外編. MovieClip のスクリプトを"簡単に"書き換える

これ、完全に頭がイカれてたときのアイディアで公開したくもないくらいみっともないんだけど、同じような失敗をしたとが死んで日本の自殺率が高まらないために書いておく。

```javascript
// 　 　 　　　＿＿＿_
// 　 　　　／　　 　 　＼
// 　　　／　 _ノ 　ヽ､_　 ＼
// 　 ／ 　oﾟ⌒　　　⌒ﾟo　 ＼ 　どうやっても終了フレームに Flash 内スクリプトがあるとうまくいかないお
// 　 |　　　　 （__人__）　　　　|　　
// 　 ＼　　 　 ｀ ⌒´ 　 　 ／
// 
// 
// 
// 　 　 　　　＿＿＿_
// 　 　　　／⌒　　⌒＼
// 　　　／（ ●） 　（●）＼
// 　 ／::::::⌒（__人__）⌒:::::＼ 　　じゃあ、Flash 内スクリプトを書き換えてしまえばいいお
// 　 |　　　　　|r┬-|　　　　 |
// 　 ＼ 　　 　 `ー'´ 　 　 ／


createjs.MovieClip.prototype.gotoAndPlayWithCallback = function(key, callback) {
    var that = this;
    var timeline = this.timeline;
    var endLabelPos = timeline.resolve(key + '_end');

    if (endLabelPos === null) {
        console.error('Undefined end label.');
    }
    // Flash 内スクリプトは frame_{N} で登録されてるので
    var frameCallbackKey = 'frame_' + endLabelPos; 
    var frameCallback = this[frameCallbackKey];

    if (frameCallback) {
        // 無理矢理書き換えちゃう
        this[frameCallbackKey] = function() {
            // コールバックを呼ぶ
            callback.call(that);
            // 無理矢理戻して
            that[frameCallbackKey] = frameCallback;
            // 元の callback を呼ぶ
            frameCallback.call(that);
        };
    } else {
        timeline.addEventListener('change', _callbackHandler);
    }
    return this.gotoAndPlay(key);

    function _callbackHandler(e) {
        if (e.target.position >= endLabelPos) {
            timeline.removeEventListener('change', _callbackHandler);
            callback.call(that);
        }
    }
};
```

#### 問題点

- 動く訳ねーだろ（何で動かないかというと、this.frame_{N} は最初に addTween されているので、this.frame_{N} の実体を書き換えたところで既に timeline から参照されてないから）


-------------------------

## ここまでのハイライト

- timeline.onchange は、フレームが変わった瞬間に呼ばれる訳ではなく、フレームに登録されたスクリプト内で gotoAndHoge されると、 **その瞬間に timeline.position が変化してしまう** ので、それ以降のスクリプトで正しい position が取れない。(prevPos とか取れるけど信用できない)
- 上記とほぼ同様で、ticker.ontick でも、スクリプトで gotoAndHoge されると、その瞬間に position が(ry
- FlashCC で埋め込んだフレームのスクリプトは、最初に Tween オブジェクトが生成されて addTween されていて、this.frame_{N} を参照することはないので書き換えても無駄

ということ。

timeline.onchange に関しては、change イベントが走った position くらいは保証してもらいたいものだが、多分頑張ってる感じだと思うのでもうちょっと頑張って欲しいなとは思った。

this.frame_{N} は直接参照されないならそんなとこに置いとくなよとか思ったりはした。


## 結局どうするの

いずれの場合も、防がなければいけないのはただ1点

**終了ラベルのフレームで後から登録した登録したコールバックが呼ばれるまで、もしくは終了ラベルにたどり着くまでのフレームで gotoAndHHoge が呼ばれた時の処理**

であると気付くと思う。

なので、シンプルに言えば、gotoAndHoge が呼ばれたときだけにフォーカスすれば良いということだ。

CreateJS.MovieClip の gotoAndHoge 系の処理では、必ず MovieClip._goto という private なメソッドが呼ばれて、その中で timeline.position が変動するようになっている。

その特性を利用して、以下のように解決することができる。

### 正解

```javascript
;(function(cjs) {
var p = cjs.MovieClip.prototype;

p.gotoAndPlayWithCallback = function(label, callback) {
    var that = this;
    var timeline = this.timeline;

    this.gotoAndPlay(label);
    timeline.__cb = new CallbackHandler(this, label, function() {
        timeline.__cb = null;
        callback.call(that);
    });
    return this;
};

// オリジナルの _goto メソッドを一旦他の場所へ
p.__originalGoto = p._goto;
// _goto を上書きして、timeline に callback が登録されているかどうかを見てあげる
p._goto = function(positionOrLabel) {
    var timeline = this.timeline;

    // __cb が登録されているかをチェック
    // 存在すれば gotoAndPlayWithCallback の最中に呼ばれたことになる
    var __cb = timeline.__cb;

    if (__cb) {
        // 先に消しておく
        timeline.__cb = null;
        // isCancel = true にして finish を呼ぶ
        __cb.finish(true);
    }
    this.__originalGoto(positionOrLabel);
};

// gotoAndPlayWithCallback 用のコールバック管理用オブジェクト
function CallbackHandler(mc, label, callback) {
    var that = this;
    var timeline = this.timeline = mc.timeline;
    // 終了ラベルを取得
    var endLabelPos = this.endPosition = timeline.resolve(label + '_end');
    var cb = function() {
        that.finish(true);
    };

    this.mc = mc;
    this.finished = false;
    this.callback = callback;
    // endLabelPos 待って callback を発火する tween オブジェクトを生成する
    this.tween = cjs.Tween.get(mc).wait(endLabelPos).call(cb);
    timeline.addTween(this.tween);
}

CallbackHandler.prototype = {
    finish: function(isCancel) {
        // 念のため
        if (this.finished) {
            return;
        }
        var timeline = this.timeline;

        this.finished = true;
        // tween を消しておく
        timeline.removeTween(this.tween);
        // isCancel が立ってない、もしくは isCance だけど現在の timeline.position が endPosition だったら callback を呼ぶ
        if (!isCancel || (isCancel && timeline.position === this.endPosition)) {
            this.callback.call(this.mc, isCancel);
        }
    }
};

})(createjs);

///////////////////////////////////

// mc には play, play_end というラベルが存在するとして

mc.gotoAndPlayWithCallback('play', function(err) {
    if (err) {
        // 失敗
        return;
    } 
    // 成功
});

```

これにより、必須要件から努力目標まですべてカバーすることができる。

もう何も怖くない。

--------------

CreateJS の挙動だけならまだしも、FlashCC からはき出されるコードの特性を知っておかないと深い部分まで探るのが本当にしんどい。

まだ書き途中だがパブリッシュされたファイルのおおよそは [ブログ](http://dameleon.hatenablog.com/entry/2014/04/01/211755) のほうに書いてある。

現場からは以上です。