Source: layer/LayerEvent.js

/**
 * 레이어 이벤트 처리
 * 
 * @class
 * @param {collie.Layer} oLayer
 */
collie.LayerEvent = collie.Class(/** @lends collie.LayerEvent.prototype */{
    /**
     * 클릭 탐지 값 (px)
     * TODO Androind에서는 반응이 느껴질 수 있으므로 수치를 크게 하는 것이 좋다. (약 12정도?)
     * @constant
     */
    THRESHOLD_CLICK : 7,

    $init : function (oLayer) {
        this._oLayer = oLayer;
        this._bHasTouchEvent = !!('ontouchstart' in window);
        this._fOnEvent = this._onEvent.bind(this);
        this._oMousedownObject = null;
        this._htEventRatio = {
            width : 1,
            height : 1
        };
        this._bAttached = false;
    },
    
    /**
     * @private
     */
    attachEvent : function () {
        var el = this._oLayer.getParent();
        
        if (this._bHasTouchEvent) {
            collie.util.addEventListener(el, "touchstart", this._fOnEvent);
            collie.util.addEventListener(el, "touchend", this._fOnEvent);
            collie.util.addEventListener(el, "touchmove", this._fOnEvent);
            collie.util.addEventListener(el, "touchcancel", this._fOnEvent);
        } else {
            collie.util.addEventListener(el, "mousedown", this._fOnEvent);
            collie.util.addEventListener(el, "mouseup", this._fOnEvent);
            collie.util.addEventListener(el, "mousemove", this._fOnEvent);
        }
        
        this._bAttached = true;
    },
    
    /**
     * @private
     */
    detachEvent : function () {
        var el = this._oLayer.getParent();
        
        if (this._bAttached) {
            if (this._bHasTouchEvent) {
                collie.util.removeEventListener(el, "touchstart", this._fOnEvent);
                collie.util.removeEventListener(el, "touchend", this._fOnEvent);
                collie.util.removeEventListener(el, "touchmove", this._fOnEvent);
                collie.util.removeEventListener(el, "touchcancel", this._fOnEvent);
            } else {
                collie.util.removeEventListener(el, "mousedown", this._fOnEvent);
                collie.util.removeEventListener(el, "mouseup", this._fOnEvent);
                collie.util.removeEventListener(el, "mousemove", this._fOnEvent);
            }
            
            this._bAttached = false;
        }
    },
    
    /**
     * 이벤트 핸들러
     * @private
     * @param {HTMLEvent} e
     */
    _onEvent : function (e) {
        // 이벤트를 사용하지 않으면 무시
        if (!this._oLayer._htOption.useEvent) {
            return;
        }
        
        e = e || window.event;
        var oEvent = this._bHasTouchEvent ? e.changedTouches[0] : e || window.event;
        var el =  this._bHasTouchEvent ? this._getEventTargetElement(e) : e.target || e.srcElement;
        var oDocument = el.ownerDocument || document;
        var oBody = oDocument.body || oDocument.documentElement;
        var nPageX = this._bHasTouchEvent ? oEvent.pageX : oEvent.pageX || oEvent.clientX + oBody.scrollLeft - oDocument.body.clientLeft;
        var nPageY = this._bHasTouchEvent ? oEvent.pageY : oEvent.pageY || oEvent.clientY + oBody.scrollTop - oDocument.body.clientTop;
        var sType = e.type;
        var oDisplayObject = null;
        
        // 이벤트가 일어난 곳의 상대 좌표를 계산
        var htPosition = this._oLayer.getParentPosition();
        var nRelatedX = nPageX - htPosition.x - this._oLayer._htOption.x;
        var nRelatedY = nPageY - htPosition.y - this._oLayer._htOption.y;
        nRelatedX = nRelatedX / this._htEventRatio.width;       
        nRelatedY = nRelatedY / this._htEventRatio.height;      
        
        if (sType === "touchcancel") {
            if (this._htEventStartPos !== null) {
                nRelatedX = this._htEventStartPos.x;
                nRelatedY = this._htEventStartPos.y;
            }
        }
        
        sType = this._convertEventType(sType);
        
        // 기본 액션을 멈춘다(isPreventDefault 상태일 때만)
        if (sType === "mousemove" || sType === "mousedown") {
            if (collie.Renderer.isPreventDefault()) {
                collie.util.stopEventDefault(e);
            }
        }
        
        // 좌표 기록
        //@TODO 객체 재 사용 해야 함
        if (sType === "mousedown") {
            this._htEventStartPos = {
                x : nRelatedX,
                y : nRelatedY
            };
        }

        // Layer 표현 방식대로 이벤트를 발생한다
        var bFiredEventOnTarget = this._fireEvent(e, sType, nRelatedX, nRelatedY);
        
        // 클릭 처리
        if (sType === "mouseup") {
            // 탐지 영역도 resize에 맞춰서 변경한다
            var nThresholdX = this.THRESHOLD_CLICK;
            var nThresholdY = this.THRESHOLD_CLICK;
            
            if (
                this._htEventStartPos &&
                this._htEventStartPos.x - nThresholdX <= nRelatedX &&
                nRelatedX <= this._htEventStartPos.x + nThresholdX &&
                this._htEventStartPos.y - nThresholdY <= nRelatedY &&
                nRelatedY <= this._htEventStartPos.y + nThresholdY
                ) {
                this._fireEvent(e, "click", nRelatedX, nRelatedY);
            }
            
            this._htEventStartPos = null;
        }
        
        // 이벤트 상태를 저장해서 다른 레이어에 표시객체 이벤트가 통과되지 않도록 방어한다
        collie.Renderer.setEventStatus(sType, bFiredEventOnTarget);
    },
    
    /**
     * 레이어에서 이벤트가 일어났을 때 표시 객체에 이벤트를 발생 시킨다
     * 
     * @param {Object} e 이벤트 원본
     * @param {String} sType 이벤트 타입, mouse 이벤트로 변형되서 들어온다
     * @param {Number} nX 이벤트가 일어난 상대좌표
     * @param {Number} nY 이벤트가 일어난 상대좌표
     * @return {Boolean} 표시 객체에 이벤트가 발생했는지 여부 
     * @private
     */
    _fireEvent : function (e, sType, nX, nY) {
        var oDisplayObject = null;
        var bIsNotStoppedBubbling = true;
        
        // 캔버스에서 이전 레이어에 객체에 이벤트가 일어났으면 다음 레이어의 객체에 전달되지 않는다
        if (sType !== "mousemove" && !collie.Renderer.isStopEvent(sType)) {
            var aDisplayObjects = this._oLayer.getChildren();
            oDisplayObject = this._getTargetOnHitEvent(aDisplayObjects, nX, nY);
            
            // mousedown일 경우 객체를 저장한다
            if (oDisplayObject) {
                bIsNotStoppedBubbling = this._bubbleEvent(oDisplayObject, sType, e, nX, nY);
                
                if (sType === "mousedown") {
                    this._setMousedownObject(oDisplayObject);
                }
                if (sType === "mouseup") {
                    this._unsetMousedownObject(oDisplayObject);
                }
            }
        }
        
        // mouseup 처리가 안된 경우 임의 발생
        if (sType === "mouseup" && this._getMousedownObject() !== null) {
            oDisplayObject = this._getMousedownObject();
            this._bubbleEvent(oDisplayObject, sType, e, nX, nY);
            this._unsetMousedownObject(oDisplayObject);
        }
        
        /**
         * click 이벤트, 모바일 환경일 때는 touchstart, touchend를 비교해서 좌표가 일정 이내로 움직였을 경우 click 이벤트를 발생한다d
         * @name collie.Layer#click
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 상대 x좌표
         * @param {Number} htEvent.y 상대 y좌표
         */
        /**
         * mousedown 이벤트, 모바일 환경일 때는 touchstart 이벤트도 해당 된다.
         * @name collie.Layer#mousedown
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 상대 x좌표
         * @param {Number} htEvent.y 상대 y좌표
         */
        /**
         * mouseup 이벤트, 모바일 환경일 때는 touchend 이벤트도 해당 된다.
         * @name collie.Layer#mouseup
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 상대 x좌표
         * @param {Number} htEvent.y 상대 y좌표
         */
        /**
         * mousemove 이벤트, 모바일 환경일 때는 touchmove 이벤트도 해당 된다.
         * @name collie.Layer#mouseup
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 상대 x좌표
         * @param {Number} htEvent.y 상대 y좌표
         */
        if (bIsNotStoppedBubbling) { // stop되면 Layer이벤트도 일어나지 않는다
            this._oLayer.fireEvent(sType, {
                event : e,
                displayObject : oDisplayObject,
                x : nX,
                y : nY
            });
        }
        
        return !!oDisplayObject;
    },
    
    /**
     * 이벤트 대상을 고른다
     * - 가장 위에 있는 대상이 선정되어야 한다
     * @private
     * @param {Array|collie.DisplayObject} vDisplayObject
     * @param {Number} nX 이벤트 상대 x 좌표
     * @param {Number} nY 이벤트 상대 y 좌표
     * @return {collie.DisplayObject|Boolean}
     */
    _getTargetOnHitEvent : function (vDisplayObject, nX, nY) {
        var oTargetObject = null;
        
        if (vDisplayObject instanceof Array) {
            for (var i = vDisplayObject.length - 1; i >= 0; i--) {
                // 자식부터
                if (vDisplayObject[i].hasChild()) {
                    oTargetObject = this._getTargetOnHitEvent(vDisplayObject[i].getChildren(), nX, nY);
                    
                    // 찾았으면 멈춤
                    if (oTargetObject) {
                        return oTargetObject;
                    }
                }

                // 본인도
                oTargetObject = this._getTargetOnHitEvent(vDisplayObject[i], nX, nY);
                
                // 찾았으면 멈춤
                if (oTargetObject) {
                    return oTargetObject;
                }
            }
        } else {
            return this._isPointInDisplayObjectBoundary(vDisplayObject, nX, nY) ? vDisplayObject : false;
        }
    },
    
    /**
     * 이벤트명 보정
     * 
     * @private
     * @param {String} sType 이벤트 타입
     * @return {String} 변환된 이벤트 타입
     */
    _convertEventType : function (sType) {
        var sConvertedType = sType;
        
        switch (sType) {
            case "touchstart" :
                sConvertedType = "mousedown";
                break;
                
            case "touchmove" :
                sConvertedType = "mousemove";
                break;
                
            case "touchend" :
            case "touchcancel" :
                sConvertedType = "mouseup";
                break;
                
            case "tap" :
                sConvertedType = "click";
                break;
        }
        
        return sConvertedType;
    },
    
    _getEventTargetElement : function (e) {
        var el = e.target;
        
        while (el.nodeType != 1) {
            el = el.parentNode;
        }
        
        return el;
    },
    
    /**
     * 이벤트 대상의 이벤트를 버블링으로 처리 한다
     * 
     * @private
     * @param {collie.DisplayObject} oDisplayObject 버블링 대상
     * @param {String} sType 이벤트명
     * @param {HTMLEvent} e
     * @param {Number} nX 이벤트 상대 x 좌표
     * @param {Number} nY 이벤트 상대 y 좌표
     * @param {collie.DisplayObject} oCurrentObject 이벤트 대상
     * @return {Boolean} 이벤트가 도중에 멈췄으면 false를 반환
     */
    _bubbleEvent : function (oDisplayObject, sType, e, nX, nY, oCurrentObject) {
        /**
         * click 이벤트, 모바일 환경일 때는 touchstart, touchend를 비교해서 좌표가 일정 이내로 움직였을 경우 click 이벤트를 발생한다d
         * @name collie.DisplayObject#click
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 이벤트 상대 x 좌표
         * @param {Number} htEvent.y 이벤트 상대 y 좌표
         */
        /**
         * mousedown 이벤트, 모바일 환경일 때는 touchstart 이벤트도 해당 된다.
         * @name collie.DisplayObject#mousedown
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 이벤트 상대 x 좌표
         * @param {Number} htEvent.y 이벤트 상대 y 좌표
         */
        /**
         * mouseup 이벤트, 모바일 환경일 때는 touchend 이벤트도 해당 된다.
         * @name collie.DisplayObject#mouseup
         * @event
         * @param {Object} htEvent
         * @param {collie.DisplayObject} htEvent.displayObject 대상 객체
         * @param {Event} htEvent.event 이벤트 객체
         * @param {HTMLEvent} htEvent.event 이벤트 객체
         * @param {Number} htEvent.x 이벤트 상대 x 좌표
         * @param {Number} htEvent.y 이벤트 상대 y 좌표
         */
        if (oDisplayObject.fireEvent(sType, { // stop() 하게 되면 버블링 멈춘다
            displayObject : oCurrentObject || oDisplayObject,
            event : e,
            x : nX,
            y : nY
        }) === false) {
            return false;
        }
        
        // 부모에 이벤트가 전달된다
        if (oDisplayObject.getParent() && !this._bubbleEvent(oDisplayObject.getParent(), sType, e, nX, nY, oDisplayObject)) {
            return false;
        }
        
        return true;
    },
    
    /**
     * DisplayObject 범위 안에 PointX, PointY가 들어가는지 확인
     * 
     * @private
     * @param {collie.DisplayObject} oDisplayObject
     * @param {Number} nPointX 확인할 포인트 X 좌표
     * @param {Number} nPointY 확인할 포인트 Y 좌표
     * @return {Boolean} 들어간다면 true
     */
    _isPointInDisplayObjectBoundary : function (oDisplayObject, nPointX, nPointY) {
        // 안보이는 상태거나 이벤트를 받지 않는다면 지나감
        if (
            !oDisplayObject._htOption.useEvent ||
            !oDisplayObject._htOption.visible ||
            !oDisplayObject._htOption.width ||
            !oDisplayObject._htOption.height ||
            (oDisplayObject._htOption.useEvent === "auto" && !oDisplayObject.hasAttachedHandler())
            ) {
            return false;
        }
        
        var htHitArea = oDisplayObject.getHitAreaBoundary();
        
        // 영역 안에 들어왔을 경우
        if (
            htHitArea.left <= nPointX && nPointX <= htHitArea.right &&
            htHitArea.top <= nPointY && nPointY <= htHitArea.bottom
        ) {
            // hitArea 설정이 없으면 사각 영역으로 체크
            if (!oDisplayObject._htOption.hitArea) {
                return true;
            } else {
                var htPos = oDisplayObject.getRelatedPosition();
                
                // 대상 Point를 상대 좌표로 변경
                nPointX -= htPos.x;
                nPointY -= htPos.y;
                
                // transform 적용
                var aHitArea = oDisplayObject._htOption.hitArea;
                aHitArea = collie.Transform.points(oDisplayObject, aHitArea);
                return this._isPointInPolygon(aHitArea, nPointX, nPointY);
            }
        }
        
        return false;
    },
    
    /**
     * 대상 Point가 폴리곤 안에 있는지 여부를 반환
     *
     * @private
     * @param {Array} 꼭지점 모음 [[x1, y1], [x2, y2], ... ]
     * @param {Number} nX 대상 점 x 좌표
     * @param {Number} nY 대상 점 y 좌표
     * @return {Boolean} true면 안에 있음
     */
    _isPointInPolygon : function (aVertices, nX, nY) {
        var bIntersects = false;
        
        for (var i = 0, j = aVertices.length - 1, len = aVertices.length; i < len; j = i++) {
            if (
                (aVertices[i][1] > nY) !== (aVertices[j][1] > nY) &&
                (nX < (aVertices[j][0] - aVertices[i][0]) * (nY - aVertices[i][1]) / (aVertices[j][1] - aVertices[i][1]) + aVertices[i][0])
            ) {
               bIntersects = !bIntersects;
            }
        }
        
        return bIntersects;
    },
    
    /**
     * mousedown 객체를 설정 한다
     * 이 객체를 설정하면 mouseup 이벤트가 나왔을 때 해당 객체에서 하지 않더라도 해당 객체에 mouseup을 일으켜준다
     * @param {collie.DisplayObject} oDisplayObject
     * @private
     */
    _setMousedownObject : function (oDisplayObject) {
        this._oMousedownObject = oDisplayObject;
    },
    
    /**
     * 지정된 mousedown 객체를 해제 한다. 같은 객체여야만 해제 된다
     * @private
     */
    _unsetMousedownObject : function (oDisplayObject) {
        if (this._oMousedownObject === oDisplayObject) {
            this._oMousedownObject = null;
        }
    },
    
    /**
     * mousedown 객체를 반환 한다
     * @private
     * @return {collie.DisplayObject}
     */
    _getMousedownObject : function () {
        return this._oMousedownObject;
    },
    
    /**
     * 이벤트 좌표 보정치를 설정 한다
     * 
     * @param {Number} nWidth
     * @param {Number} nHeight
     */
    setEventRatio : function (nWidth, nHeight) {
        this._htEventRatio.width = nWidth || this._htEventRatio.width;
        this._htEventRatio.height = nHeight || this._htEventRatio.height;
    },
    
    /**
     * 이벤트 좌표 보정치가 있다면 반환 한다
     * @private
     * @deprecated
     * @return {Object} htEventRatio
     * @return {Number} htEventRatio.width
     * @return {Number} htEventRatio.height
     */
    getEventRatio : function () {
        return this._htEventRatio;
    }
});
comments powered by Disqus