ヒット領域のDisplayObject.suppressCrossDomainErrors対応


画像のクロスドメインエラー

Animaiteで作成した成果物をCodePenで共有するときに、別ドメインからの画像の読み込みは問題なく表示できていたのですが、画像をよみこんだオブジェクトで衝突判定する場合にエラーが出ました。
"An error has occurred. This is most likely due to security restrictions on reading canvas pixel data with local or cross-domain images."

エラーの原因

エラーを起こしていたのは、オブジェクトにon()でクリックイベントを付与していた箇所。クリックしてもイベントが発生しません。原因を調べていたところ、createjs.Bitmap()で画像を読み込んで生成したボタンにHitイベントを発火させる時、CreateJSの仕様でドメインが違う場合、エラーが出るように設定してありました。


/**
* Suppresses errors generated when using features like hitTest, mouse events, and {{#crossLink "getObjectsUnderPoint"}}{{/crossLink}}
* with cross domain content.
* @property suppressCrossDomainErrors
* @static
* @type {Boolean}
* @default false
**/

DisplayObject.suppressCrossDomainErrors = false;

詳しくはFAQに記載されてます。
https://github.com/CreateJS/EaselJS/wiki/FAQ

エラーを起こしていた部分

アラートのテキストを表示させていたのは以下のCoreの部分でした。


/**
* @method _testHit   
* @protected     
* @param {CanvasRenderingContext2D} ctx  
* @return {Boolean}  
**/
p._testHit = function(ctx) {
try {
var hit = ctx.getImageData(0, 0, 1, 1).data[3] > 1;     
} catch (e) {   
if (!DisplayObject.suppressCrossDomainErrors) {
throw "An error has occurred. This is most likely due to security restrictions on reading canvas pixel data with local or cross-domain images.";
            }
        }
        return hit;
    };
getImageData();

なぜ、画像データを読み込んでる?

getImageData()の部分をさらにCoreFileを読みながら追ってみたところ、画像の色データを解析しているようです。イベントの衝突判定をする時に、ドメインに置かれた画像の色データを読み込もうとするとエラーが出ました。

以下の部分で、色のアルファ値をみているようでした。
透明な部分はヒット領域から外してくれるのかな☺️

    /**
     * @method _getObjectsUnderPoint
     * @param {Number} x
     * @param {Number} y
     * @param {Array} arr
     * @param {Boolean} mouse If true, it will respect mouse interaction properties like mouseEnabled, mouseChildren, and active listeners.
     * @param {Boolean} activeListener If true, there is an active mouse event listener on a parent object.
     * @param {Number} currentDepth Indicates the current depth of the search.
     * @return {DisplayObject}
     * @protected
     **/
    p._getObjectsUnderPoint = function(x, y, arr, mouse, activeListener, currentDepth) {
        currentDepth = currentDepth || 0;
        if (!currentDepth && !this._testMask(this, x, y)) { return null; }
        var mtx, ctx = createjs.DisplayObject._hitTestContext;
        activeListener = activeListener || (mouse&&this._hasMouseEventListener());

        // draw children one at a time, and check if we get a hit:
        var children = this.children, l = children.length;
        for (var i=l-1; i>=0; i--) {
            var child = children[i];
            var hitArea = child.hitArea;
            if (!child.visible || (!hitArea && !child.isVisible()) || (mouse && !child.mouseEnabled)) { continue; }
            if (!hitArea && !this._testMask(child, x, y)) { continue; }

            // if a child container has a hitArea then we only need to check its hitAre2a, so we can treat it as a normal DO:
            if (!hitArea && child instanceof Container) {
                var result = child._getObjectsUnderPoint(x, y, arr, mouse, activeListener, currentDepth+1);
                if (!arr && result) { return (mouse && !this.mouseChildren) ? this : result; }
            } else {
                if (mouse && !activeListener && !child._hasMouseEventListener()) { continue; }

                // TODO: can we pass displayProps forward, to avoid having to calculate this backwards every time? It's kind of a mixed bag. When we're only hunting for DOs with event listeners, it may not make sense.
                var props = child.getConcatenatedDisplayProps(child._props);
                mtx = props.matrix;

                if (hitArea) {
                    mtx.appendMatrix(hitArea.getMatrix(hitArea._props.matrix));
                    props.alpha = hitArea.alpha;
                }

                ctx.globalAlpha = props.alpha;
                ctx.setTransform(mtx.a,  mtx.b, mtx.c, mtx.d, mtx.tx-x, mtx.ty-y);
                (hitArea||child).draw(ctx);
                if (!this._testHit(ctx)) { continue; }
                ctx.setTransform(1, 0, 0, 1, 0, 0);
                ctx.clearRect(0, 0, 2, 2);
                if (arr) { arr.push(child); }
                else { return (mouse && !this.mouseChildren) ? this : child; }
            }
        }
        return null;
    };

参考文献

色のAPIに関しては、以下の文献を見直しました。
https://html.spec.whatwg.org/multipage/canvas.html#dom-context-2d-
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalAlpha

対策

FAQには変更しても良いような事が書いてありましたが、そもそも、メンテナンスも面倒になるので、スクリプトで生成できるシェイプで対応しました。以下のようにシェイプを追加して、ボタンの機能を移植しました。

card_01_hit = new createjs.Shape();
card_01.hitArea = card_01_hit;
stage.addChild(card_01_hit);
card_01_hit.graphics.beginFill("rgba(255,255,255,0.05)").drawCircle(card_01.x+75, card_01.y+75, 75);

card_01_hit.on("click", function(e) {.....}

調整後

See the Pen Animaite by tomo (@tomoka) on CodePen.