Source: layer/Layer.js

/**
 * A Layer contains many displayObjects.
 * In Canvas mode, one layer is one canvas element.
 * If you have been using just one canvas element, try to have a few layers. But not too many.
 * @class
 * @extends collie.Component
 * @param {Object} [htOption]
 * @param {Number} [htOption.width=320] (px) It only works when initialize this class. It's different from a set method in displayObject.
 * @param {Number} [htOption.height=480] (px)
 * @param {Number} [htOption.x=0] (px)
 * @param {Number} [htOption.y=0] (px)
 * @param {Boolean} [htOption.useEvent=true] Use event on this layer. If you want to improve performance of event, you can set this option as false.
 * @param {Boolean} [htOption.visible=true] Visibility
 * @param {Boolean} [htOption.freeze=false] A Layer will not updated more.
 * @param {Boolean} [htOption.renderingMode=inherit] You can use DOM mixed with Canvas to each layers. [inherit|dom|canvas]
 */
collie.Layer = collie.Class(/** @lends collie.Layer.prototype */{
    /**
     * Class name
     * @type {String}
     */
    type : "layer",
    
    $init : function (htOption) {
        this.option({
            x : 0,
            y : 0,
            width : 320,
            height : 480,
            useEvent : true,
            visible : true,
            freeze : false,
            renderingMode : "inherit"
        });
        
        // 정렬을 해야한다면 일단 0으로 만들어 놓고 load될 때 정렬함
        this._sAlignLeft = null;
        this._sAlignTop = null;
        
        if (htOption !== undefined) {
            if (("x" in htOption) && (htOption.x === "left" || htOption.x === "right" || htOption.x === "center")) {
                this._sAlignLeft = htOption.x;
                htOption.x = 0;
            }
             
            if (("y" in htOption) && (htOption.y === "top" || htOption.y === "bottom" || htOption.y === "center")) {
                this._sAlignTop = htOption.y;
                htOption.y = 0;
            }
            
            this.option(htOption);
        }
        
        this._renderingMode = this._htOption.renderingMode === "inherit" ? collie.Renderer.getRenderingMode() : this._htOption.renderingMode;
        
        if (this._renderingMode === "canvas" && !collie.util.getDeviceInfo().supportCanvas) {
            this._renderingMode = "dom";
        }
        
        this.drawCount = 0; // debugging 용 draw count
        this.optionSetter("visible", this._setVisible.bind(this)); // 처음 set은 Drawing이 생성된 후에 실행 된다
        this._elParent = null;
        this._bChanged = false;
        this._aDisplayObjects = [];
        this._bLoaded = false;
        this._oEvent = new collie.LayerEvent(this);
        this._makeDrawing();
        this._setVisible();
    },
    
    /**
     * 렌더링 방법을 선택해서 Drawing 객체를 생성 한다
     * @private
     */
    _makeDrawing : function () {
        this._oDrawing = this._renderingMode === "dom" ? new collie.LayerDOM(this) : new collie.LayerCanvas(this);
    },
    
    /**
     * Return a drawing instance.
     * @return {collie.LayerCanvas|collie.LayerDOM}
     */
    getDrawing : function () {
        return this._oDrawing;
    },
    
    /**
     * Return a rendering mode of a current layer
     * @return {String} [dom|canvas]
     */
    getRenderingMode : function () {
        return this._renderingMode;
    },
    
    /**
     * Return a event instance
     * @return {collie.LayerEvent}
     */
    getEvent : function () {
        return this._oEvent;
    },
    
    /**
     * Return a HTMLElement of a parent
     * @return {HTMLElement}
     */
    getParent : function () {
        return this._elParent || false;
    },
    
    /**
     * 컨테이너에 엘리먼트 추가. 렌더러에서 load할 때 실행 된다
     * - 로드할 때 가장 큰 레이어를 기준으로 컨테이너의 크기를 정함
     * 
     * @private
     * @param {HTMLElement} elParent
     * @param {Number} nZIndex
     */
    load : function (elParent, nZIndex) {
        this.unload();
        this._bLoaded = true;
        this._elParent = this._elParent || elParent;
        this._elParent.style.width = Math.max(parseInt(this._elParent.style.width || 0, 10), this.option("width")) + "px";
        this._elParent.style.height = Math.max(parseInt(this._elParent.style.height || 0, 10), this.option("height")) + "px";
        this.getElement().style.zIndex = nZIndex;
        
        // 생성자 옵션에 정렬이 포함돼 있으면 load, unload를 반복하더라도 정렬을 계속한다.
        // 하지만 사용자가 직접 offset을 사용하는 경우에는 reset되도록 세 번째 인자를 통해 조치한다.
        if (this._sAlignLeft !== null) {
            this.offset(this._sAlignLeft, null, true);
        }
        
        if (this._sAlignTop !== null) {
            this.offset(null, this._sAlignTop, true);
        }
        
        this._elParent.appendChild(this.getElement());
    },
    
    /**
     * @private
     */
    unload : function () {
        if (this.isLoaded()) {
            this._oEvent.detachEvent();
            this._elParent.removeChild(this.getElement());
            this._elParent = null;
            this._bLoaded = false;
        }
    },
    
    /**
     * Layer의 attach Event를 순서 조작을 위해 Layer가 하지 않고 Renderer가 한다
     * @private
     */
    attachEvent : function () {
        this._oEvent.attachEvent();
    },
    
    /**
     * Layer의 detach Event를 순서 조작을 위해 Layer가 하지 않고 Renderer가 한다
     * @private
     */
    detachEvent : function () {
        this._oEvent.detachEvent();
    },
    
    /**
     * CSS의 display 속성과 유사
     * @private
     */
    _setVisible : function () {
        // Drawing이 생성되기 전에 옵션이 설정될 수도 있음
        if (this.getElement()) {
            this.getElement().style.display = this.option("visible") ? "block" : "none";
        }
    },
    
    /**
     * @private
     * @return {Boolean} 로딩 되어있는지 여부
     */
    isLoaded : function () {
        return this._bLoaded;
    },
    
    /**
     * Add a displayObject on this layer
     * @param {collie.DisplayObject} oDisplayObject
     */
    addChild : function (oDisplayObject) {
        // 추가할 때마다 정렬하기
        collie.util.pushWithSort(this._aDisplayObjects, oDisplayObject);
        oDisplayObject.setLayer(this);
        this.setChanged();
    },
    
    /**
     * Add many displayObjects in one time
     * @param {Array} aList An Array to contain DisplayObjects
     */
    addChildren : function (aList) {
        for (var i = 0, len = aList.length; i < len; i++) {
            this.addChild(aList[i]);
        }
    },
    
    /**
     * Remove a displayObject from this layer
     * @param {collie.DisplayObject} oDisplayObject
     * @param {Number} nIdx If you know an index of the displayObject, you can improve performance a little.
     */
    removeChild : function (oDisplayObject, nIdx) {
        oDisplayObject.unsetLayer();
        
        if (typeof nIdx !== "undefined") {
            this._aDisplayObjects.splice(nIdx, 1);
        } else {
            for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
                if (this._aDisplayObjects[i] === oDisplayObject) {
                    this._aDisplayObjects.splice(i, 1);
                    break;
                }
            }
        }
        
        this.setChanged();
    },
    
    /**
     * Remove displayObjects from this layer
     * @param {Array} aList An Array to contain DisplayObjects
     */
    removeChildren : function (aList) {
        for (var i = aList.length - 1; i >= 0; i--) {
            if (aList[i]) {
                this.removeChild(aList[i], i);
            }
        }
    },
    
    /**
     * Add layer to a Renderer(like a addTo method in DisplayObject)
     * @param {collie.Renderer} [oRenderer] A Renderer that will add a this layer. Default value is a collie.Rencerer. 
     * @return {collie.Layer} For method chaining
     * @example
     * before
     * ```
     * var layer = new collie.Layer();
     * collie.Renderer.addLayer(layer);
     * ```
     * after
     * ```
     * var layer = new collie.Layer().addTo();
     * ```
     */
    addTo : function (oRenderer) {
        oRenderer = oRenderer || collie.Renderer;
        oRenderer.addLayer(this);
        return this;
    },
    
    /**
     * zIndex가 변경되었다면 이 메소드를 호출
     * @private
     * @param {collie.DisplayObject} oDisplayObject
     */
    changeDisplayObjectZIndex : function (oDisplayObject) {
        this.removeChild(oDisplayObject);
        this.addChild(oDisplayObject);
    },
    
    /**
     * Return children that added to this layer
     * @return {Array}
     */
    getChildren : function () {
        return this._aDisplayObjects;
    },
    
    /**
     * Return a boolean value that is whether this layer has a child or not
     * @return {Boolean}
     */
    hasChild : function () {
        return this._aDisplayObjects && this._aDisplayObjects.length > 0;
    },
    
    /**
     * 변경된 사항이 있을 경우 DisplayObject에서 Layer에 setChanged를 해서 알린다. setChange된 레이어만 그리기 대상
     * @private
     */
    setChanged : function () {
        this._bChanged = true;
    },
    
    /**
     * If this layer needs to update display, It'll return value as true
     * @return {Boolean} true면 변경된 점 있음
     */
    isChanged : function () {
        return this._bChanged;
    },
    
    /**
     * 변경되지 않은 상태로 되돌린다
     * @private
     */
    unsetChanged : function () {
        this._bChanged = false;
    },
    
    /**
     * Return a Context2D instance
     * @return {Boolean|Object}
     */
    getContext : function () {
        return ("getContext" in this._oDrawing) ? this._oDrawing.getContext() : false;
    },
    
    /**
     * Return a HTMLElement of this layer
     * @return {HTMLElement}
     */
    getElement : function () {
        return ("getElement" in this._oDrawing) ? this._oDrawing.getElement() : false;
    },
    
    /**
     * Update children that added to this layer
     * 
     * @private
     * @param {Number} nFrameDuration 진행된 프레임 시간
     */
    update : function (nFrameDuration) {
        this.drawCount = 0;
        
        // 바뀐게 없으면 지나감
        if (!this.isChanged() || this.option("freeze")) {
            return;
        }
        
        this.clear();
        this.unsetChanged();
        var nWidth = this.option("width");
        var nHeight = this.option("height");
        
        // 등록된 객체 업데이트
        for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
            this._aDisplayObjects[i].update(nFrameDuration, 0, 0, nWidth, nHeight);
        }
    },
    
    /**
     * Clear a display. It only works with Canvas mode. 
     * 화면을 지운다. Canvas일 때만 작동
     */
    clear : function () {
        this._oDrawing.clear();
    },
    
    /**
     * Resize this layer
     * If you want to resize all of layers, you can use the resize method in Renderer.
     * @param {Number} nWidth
     * @param {Number} nHeight
     * @param {Boolean} bExpand If you want to resize contents in the layer, you should set this value as true
     * @see collie.Renderer#resize
     */ 
    resize : function (nWidth, nHeight, bExpand) {
        if (!bExpand) {
            this.option("width", nWidth || this._htOption.width);
            this.option("height", nHeight || this._htOption.height);
        }
        
        if (this._oDrawing) {
            this._oDrawing.resize(nWidth, nHeight, bExpand);
        }
        
        if (this._elParent) {
            this._elParent.style.width = Math.max(parseInt(this._elParent.style.width || 0, 10), nWidth || this.option("width")) + "px";
            this._elParent.style.height = Math.max(parseInt(this._elParent.style.height || 0, 10), nHeight || this.option("height")) + "px";
        }
        
        /**
         * Occur when this layer resized 
         * @event
         * @name collie.Layer#resize
         */
        this.fireEvent("resize");
    },
    
    /**
     * Change a coordinate of this layer.
     * 레이어의 부모의 크기는 등록된 레이어 중 가장 큰 레이어의 크기에 맞게 변경된다.
     * 
     * @param {Number|String} [nX] x좌표(px), left, right, center를 입력하면 Renderer의 크기 기준으로 정렬된다. 렌더러의 크기가 변하더라도 자동으로 움직이지 않는다.
     * @param {Number|String} [nY] y좌표(px), top, bottom, center를 입력하면 Renderer의 크기 기준으로 정렬된다. 렌더러의 크기가 변하더라도 자동으로 움직이지 않는다.
     * @param {Boolean} [bSkipResetInitAlign] private용 변수, 직접 쓰지 않는다.
     */
    offset : function (nX, nY, bSkipResetInitAlign) {
        var el = this.getElement();
        
        if (typeof nX !== "undefined" && nX !== null) {
            switch (nX) {
                case "left" :
                    nX = 0;
                    break;
                    
                case "right" :
                    nX = parseInt(this._elParent.style.width, 10) - this._htOption.width;
                    break;
                    
                case "center" :
                    nX = parseInt(this._elParent.style.width, 10) / 2 - this._htOption.width / 2;
                    break;
            }
            
            this.option("x", nX);
            el.style.left = nX + "px";
            
            if (!bSkipResetInitAlign) {
                this._sAlignLeft = null;
            }
        }
        
        if (typeof nY !== "undefined" && nY !== null) {
            switch (nY) {
                case "top" :
                    nY = 0;
                    break;
                    
                case "bottom" :
                    nY = parseInt(this._elParent.style.height, 10) - this._htOption.height;
                    break;
                    
                case "center" :
                    nY = parseInt(this._elParent.style.height, 10) / 2 - this._htOption.height / 2;
                    break;
            }
            
            this.option("y", nY);
            el.style.top = nY + "px";
            
            if (!bSkipResetInitAlign) {
                this._sAlignTop = null;
            }
        }
    },
    
    /**
     * 고민 중... 부모를 렌더러가 아닌 다른 엘리먼트에 붙이는 행위
     * 
     * @param {HTMLElement|String} elParent
     */
    setParent : function (elParent) {
        // string이면 엘리먼트를 구해 줌
        if (typeof elParent === "string") {
            elParent = document.getElementById(elParent);
        }
        
        if (this._bLoaded) {
            this._oEvent.detachEvent();
            this._elParent.removeChild(this.getElement());
            this._elParent = elParent;
            this._elParent.appendChild(this.getElement());
            this._oEvent.attachEvent();
        } else {
            this._elParent = elParent;
        }
    },
    
    /**
     * @private
     * @return {Object}
     */
    getParentPosition : function () {
        if (this._elParent !== null) {
            return this._elParent === collie.Renderer.getElement() ? collie.Renderer.getPosition() : collie.util.getPosition(this._elParent);
        }
    },
    
    /**
     * Clone a layer
     * 
     * @param {Boolean} bRecursive Whether clone a layer with displayObjects that added this layer or not
     * @return {collie.Layer}
     */
    clone : function (bRecursive) {
        var oLayer = new this.constructor(this._htOption);
        
        if (bRecursive && this._aDisplayObjects.length) {
            for (var i = 0, l = this._aDisplayObjects.length; i < l; i++) {
                this._aDisplayObjects[i].clone(true).addTo(oLayer);
            }
        }
        
        return oLayer;
    }
}, collie.Component);
comments powered by Disqus