Source: ImageManager.js

/**
 * 이미지 리소스 관리
 * @example
 * // 한 개의 이미지를 로딩
 * collie.ImageManager.add("key", "sample.png");
 * new collie.DisplayObject({
 *  backgroundImage: "key"
 * });
 * @example
 * // 여러 이미지를 한 번에 로딩
 * collie.ImageManager.add({
 *  image1 : "image1.png",
 *  image2 : "image2.png"
 * }, function () {
 *  alert("complete");
 * });
 * @namespace
 */
collie.ImageManager = collie.ImageManager || new (collie.Class(/** @lends collie.ImageManager */{
    /**
     * 이미지 로딩 실패시 재시도 횟수
     * @type {Number}
     */
    RETRY_COUNT : 3,
    
    /**
     * 이미지 로딩 실패시 재시도 딜레이 ms
     * @type {Number}
     */
    RETRY_DELAY : 500,
    
    /**
     * DOM일 때 css3d 엘리먼트를 미리 만들어놓는지 여부
     * - 기능 불안정으로 기본 사용 false로 변경, top, left를 멀리 보내버리면 3d 렌더링에 부담이 될 수 있다.
     * @type {Boolean} 
     */
    USE_PRERENDERING_DOM : false,
    
    $init : function () {
        this._aImages = [];
        this._htImageNames = {};
        this._htImageRetryCount = {};
        this._htImageWhileLoading = {};
        this._nCount = 0;
        this._oSpriteSheet = new collie.SpriteSheet();
    },
    
    /**
     * 이미지를 추가
     * - 외부에서 직접 사용하면 count가 어긋나기 때문에 private 처리
     * 
     * @private
     * @param {String} sName 리소스 이름, 나중에 이 이름으로 리소스를 찾는다
     * @param {HTMLElement} elImage 저장할 엘리먼트
     */
    _addImage : function (elImage, sName) {
        var nLength = this._aImages.push({
            element : elImage,
            name : sName
        });
        
        var aCallback = this._htImageNames[sName];
        this._htImageNames[sName] = nLength - 1;
        delete this._htImageRetryCount[sName];
        
        // callback 실행
        if (aCallback && aCallback instanceof Array) {
            for (var i = 0, len = aCallback.length; i < len; i++) {
                aCallback[i](elImage, sName);
            }
            
            aCallback = null;
        }
        
        /**
         * 한개의 이미지가 로딩되었을 때 발생
         * @name collie.ImageManager#process
         * @event
         * @param {Object} oEvent
         * @param {String} oEvent.name 이미지 이름
         * @param {String} oEvent.url 이미지 URL
         * @param {Number} oEvent.count 현재 로딩된 갯수
         * @param {Number} oEvent.total 전체 이미지 갯수
         * @param {Number} oEvent.ratio 로딩된 이미지의 비율 (0~1)
         */
        this.fireEvent("process", {
            name : sName,
            url : elImage.src,
            count : nLength,
            total : this._nCount,
            ratio : Math.round((nLength / this._nCount) * 1000) / 1000
        });
        
        if (this._nCount === nLength) {
            /**
             * 등록된 이미지가 모두 로드 되었을 경우
             * @name collie.ImageManager#complete
             * @event
             * @param {Object} oEvent
             */
            this.fireEvent("complete");
        }
    },
    
    /**
     * 자리를 찜, 이미 자리가 있는 경우에는 아무것도 하지 않는다
     * 
     * @private
     * @param {String} sName 이미지 이름
     */
    _markImage : function (sName) {
        if (!this._htImageNames[sName]) {
            this._htImageNames[sName] = [];
        }
        
        if (!this._htImageRetryCount[sName]) {
            this._htImageRetryCount[sName] = 0;
        } 
    },
    
    /**
     * 해쉬를 다시 만듦
     * @private
     */
    _makeHash : function () {
        this._htImageNames = {};
        
        for (var i = 0, len = this._aImages.length; i < len; i++) {
            this._htImageNames[this._aImages[i].name] = i;
        }
    },
    
    /**
     * 이미지를 가져온다
     * 
     * @static
     * @param {String} sName 리소스 이름
     * @param {Function} fCallback 리소스가 로드되지 않았을 수도 있으므로 콜백으로 처리
     * @return {HTMLElement}
     */
    getImage : function (sName, fCallback) {
        if (!sName && sName !== 0) {
            return false;
        }
        
        // 마크되지 않은 이름이라면 마크함
        if (!(sName in this._htImageNames)) {
            this._markImage(sName);
        }
        
        // 마크가 된 상황이고 아직 로딩되지 않았다면
        if (this._htImageNames[sName] instanceof Array) {
            return (fCallback && this._addMarkCallback(sName, fCallback));
        } else {
            if (fCallback) {
                fCallback(this._aImages[this._htImageNames[sName]].element);
            } else {
                return this._aImages[this._htImageNames[sName]].element;
            }
        }
    },
    
    /**
     * 마크된 영역에 콜백을 등록, 로딩이 완료되면 콜백이 실행된다
     * @private
     * @param {String} sName
     * @param {Function} fCallback
     * @param {Function} fFail
     * @return {Boolean} callback이 등록될 경우 true를 반환
     */
    _addMarkCallback : function (sName, fCallback, fFail) {
        if ((sName in this._htImageNames) && this._htImageNames[sName] instanceof Array) {
            if (fFail) {
                var fError = function fError(oEvent) {
                    if (oEvent.name === sName) {
                        fFail();
                        this.detach("error", fError);
                    }
                };
                
                this.attach("error", fError);
            }
            
            if (fCallback) {
                this._htImageNames[sName].push(fCallback);
            }
            
            return true;
        } else {
            return false;
        }
    },
    
    /**
     * 이미지를 삭제한다
     * @param {String} sName 리소스 이름
     */
    removeImage : function (sName) {
        if (!(sName in this._htImageNames)) {
            return false;
        }
        
        var elImage = this._aImages.splice(this._htImageNames[sName], 1);
        this._makeHash();
        elImage.onload = null;
        elImage.onerror = null;
        elImage.src = null;
        elImage = null;
        this._oSpriteSheet.remove(sName);
    },
    
    /**
     * 이미지를 삭제한다
     * @see collie.ImageManager.removeImage
     */
    remove : function (sName) {
        this.removeImage(sName);
    },
    
    /**
     * 이미지 리소스를 추가한다
     * 
     * @example
     * // 1개의 이미지를 추가
     * collie.ImageManager.add("key", "sample.png", function () {
     *  // callback
     * });
     * @example
     * // 여러 개의 이미지를 추가
     * collie.ImageManager.add({
     *  key : "sample.png",
     *  key2 : "sample2.png"
     * }, function () {
     *  // callback
     * });
     * 
     * @see collie.ImageManager.addImage
     * @see collie.ImageManager.addImages
     */
    add : function () {
        if (typeof arguments[0] === "object") {
            this.addImages.apply(this, arguments);
        } else {
            this.addImage.apply(this, arguments);
        }
    },
    
    /**
     * 여러 개의 이미지 리소스를 한번에 추가 한다.
     * 
     * @param {Object} htList { sName : sURL , sName2 : sURL2 }
     * @param {Function} fCallback 선택한 파일이 모두 로드될 때 실행될 함수. 없으면 실행되지 않는다. 인자로 htList를 반환
     * @param {Function} fFail 선택한 파일 중에 한개라도 로드되지 않았을 때 실행될 함수. 실패한 이미지의 [el, sName, sURL] 배열 목록을 인자로 갖는다
     */
    addImages : function (htList, fCallback, fFail) {
        var fOnComplete = null;
        var fOnFail = null;
        var nTotalCount = 0;
        var nCurrentCount = 0;
        var aFailedImages = [];
        
        // 돌면서 갯수 먼저 파악
        for (var i in htList) {
            nTotalCount++;
        }
        
        // 콜백
        if (fCallback && fCallback !== null) {
            fOnComplete = (function () {
                nCurrentCount++;
                
                if (nCurrentCount >= nTotalCount) {
                    fCallback(htList);  
                }
            }).bind(this);
        }
        
        // 실패했을 경우
        if (fFail && fFail !== null) {
            fOnFail = (function (el, sName, sURL) {
                aFailedImages.push([el, sName, sURL]);
                
                if (aFailedImages.length + nCurrentCount >= nTotalCount) {
                    fFail(aFailedImages);
                }
            }).bind(this);
        }
        
        // 로드
        for (var i in htList) {
            this.addImage(i, htList[i], fOnComplete, fOnFail);
        }
    },
    
    /**
     * 비동기로 이미지를 로딩
     * 
     * @param {String} sName 이미지 이름, 이름이 없을 경우 Loader에 저장하지 않는다
     * @param {String} sURL 이미지 주소
     * @param {Function} fCallback 성공시 실행될 함수
     * @param {HTMLElement} fCallback.elImage 엘리먼트
     * @param {String} fCallback.sName 리소스 이름
     * @param {String} fCallback.sURL URL
     * @param {Function} fFail 실패시 실행될 함수
     */
    addImage : function (sName, sURL, fCallback, fFail) {
        // 이미 이미지가 있으면 바로 콜백 실행
        if (this.getImage(sName)) {
            if (fCallback && fCallback !== null) {
                fCallback(this.getImage(sName), sName, sURL);
            }
            return;
        }
        
        // 이미 로딩 중이고 마크가 된 상황이라면 콜백 등록하고 멈춤
        if ((sName in this._htImageWhileLoading) && this._addMarkCallback(sName, fCallback, fFail)) {
            return;
        }
        
        this._nCount++;
        this._markImage(sName);
        var el = new Image();
        
        // DOM모드면 미리 OpenGL 레이어로 변환해 놓는다
        if (this.USE_PRERENDERING_DOM && collie.Renderer.getRenderingMode() === "dom" && collie.util.getSupportCSS3d() && !collie.util.getDeviceInfo().android) {
            el.style.webkitTransform = "translateZ(0)";
            el.style.position = "absolute";
            el.style.visibility = "hidden";
            collie.Renderer.getElement().appendChild(el);
        }
        
        this._htImageWhileLoading[sName] = el;
        
        el.onload = (function (e) {
            this._addImage(el, sName);
            
            if (fCallback && fCallback !== null) {
                fCallback(el, sName, sURL);
            }
            
            el.onerror = el.onload = null;
            this._deleteWhileLoading(sName);
        }).bind(this);
        
        el.onerror = (function (e) {
            // 재시도
            if (this._htImageRetryCount[sName] < this.RETRY_COUNT) {
                this._htImageRetryCount[sName]++;
                
                /**
                 * 한 개의 이미지가 로딩 실패 했을 때 실행
                 * @name collie.ImageManager#retry
                 * @event
                 * @param {Object} oEvent
                 * @param {String} oEvent.name 실패된 이미지 이름
                 * @param {String} oEvent.url 실패된 이미지 URL
                 * @param {Number} oEvent.count 현재 로딩된 갯수
                 * @param {Number} oEvent.total 전체 이미지 갯수
                 */
                this.fireEvent("retry", {
                    count : this._aImages.length,
                    total : this._nCount,
                    name : sName,
                    url : sURL,
                    delay : this.RETRY_DELAY,
                    retryCount : this._htImageRetryCount[sName]
                });
                
                setTimeout(function () {
                    // workaround http://code.google.com/p/chromium/issues/detail?id=7731
                    el.src = "about:blank";
                    el.src = sURL;
                }, this.RETRY_DELAY);
                return;
            }
            
            if (fFail && fFail !== null) {
                fFail(el, sName, sURL);
            }
            
            /**
             * 한 개의 이미지가 로딩 실패 했을 때 실행
             * @name collie.ImageManager#error
             * @event
             * @param {Object} oEvent
             * @param {String} oEvent.name 실패된 이미지 이름
             * @param {String} oEvent.url 실패된 이미지 URL
             * @param {Number} oEvent.count 현재 로딩된 갯수
             * @param {Number} oEvent.total 전체 이미지 갯수
             */
            this.fireEvent("error", {
                count : this._aImages.length,
                total : this._nCount,
                name : sName,
                url : sURL
            });
            
            el.onerror = el.onload = null;
            this._deleteWhileLoading(sName);
        }).bind(this);
        
        // Webkit 버그로 인해서 CORS 주석 처리
        // el.crossOrigin = "";
        el.src = sURL;
    },
    
    /**
     * 로딩 중에 임시로 담아놓는 변수를 제거
     * @private
     * @param {String} sName
     */
    _deleteWhileLoading : function (sName) {
        delete this._htImageWhileLoading[sName];
    },
    
    /**
     * 로드되고 있는 파일을 모두 멈춤
     */
    abort : function () {
        for (var i in this._htImageWhileLoading) {
            this._htImageWhileLoading[i].onload = this._htImageWhileLoading[i].onerror = null; 
            this._htImageWhileLoading[i].src = null;
            this._htImageWhileLoading[i] = null;
        }
        
        this._htImageWhileLoading = {};
        this._htImageStartedLoading = {};
    },
    
    /**
     * 등록된 파일을 모두 제거
     */
    reset : function () {
        this.abort();
        this._aImages = [];
        this._htImageNames = {};
        this._htImageRetryCount = {};
        this._htImageWhileLoading = {};
        this._nCount = 0;
        this._oSpriteSheet.reset();
    },
    
    /**
     * 비동기로 등록된 이미지 콜백을 취소 한다
     * DisplayObject에서 setImage처리할 때 자동으로 호출 된다
     * @private
     * @arguments collie.ImageManager.getImage
     */
    cancelGetImage : function (sName, fCallback) {
        if (this._htImageNames[sName] instanceof Array) {
            for (var i = 0, len = this._htImageNames[sName].length; i < len; i++) {
                if (this._htImageNames[sName][i] === fCallback) {
                    this._htImageNames[sName].splice(i, 1);
                    return;
                }
            }
        }
    },
    
    /**
     * 이미지에 스프라이트 시트 정보를 추가한다
     * 
     * @param {String} sImageName collie.ImageManager에 등록된 이미지 이름
     * @param {String|Object} vSpriteName 객체로 한 번에 여러 개의 정보를 등록할 수 있음
     * @example
     * collie.ImageManager.add({
     *  "sample" : "sample.png"
     * });
     * 
     * // Add Sprites with key-value object
     * collie.ImageManager.addSprite("sample", {
     *  normal : [0, 0], // [offsetX, offsetY]
     *  action : [30, 0],
     *  jump : [60, 0, 30, 30, 8] // [startOffsetX, startOffsetY, a width per one frame, a height per one frame, spriteLength] 
     * });
     * 
     * // or Add Sprites with array
     * collie.ImageManager.addSprite("sample", [
     *  [0, 0], // key 0
     *  [30, 0], // key 1
     *  [60, 0, 30, 30, 8] // key 2 and [startOffsetX, startOffsetY, a width per one frame, a height per one frame, spriteLength]
     * ]);
     * 
     * var item = new collie.DisplayObject({
     *  spriteSheet : "normal", // or 0
     *  backgroundImage : "sample"
     * });
     * 
     * // with Timer
     * collie.Timer.cycle(item, 1000, {
     *  from: 0, 
     *  to: 1,
     *  set: "spriteSheet"
     * });
     * 
     * // If you use five parameters in the addSprite method, you can use spriteX option with spriteSheet 
     * item.set("spriteSheet", "jump");
     * collie.Timer.cycle(item, 1000, {
     *  from: 0,
     *  to: 7 // spriteLength 8
     * });
     */
    addSprite : function (sImageName, vSpriteName, nOffsetX, nOffsetY) {
        this._oSpriteSheet.add(sImageName, vSpriteName, nOffsetX, nOffsetY);
    },
    
    /**
     * 스프라이트 시트 정보를 반환한다
     * 
     * @private
     * @param {String} sImageName collie.ImageManager에 등록된 이미지 이름
     * @return {Object} 
     */
    getSprite : function (sImageName) {
        return this._oSpriteSheet.get(sImageName);
    },
    
    /**
     * 스프라이트 정보를 제거한다
     * 
     * @param {String} sImageName collie.ImageManager에 등록된 이미지 이름
     */
    removeSprite : function (sImageName) {
        this._oSpriteSheet.remove(sImageName);
    }
    
    /**
     * 이미지를 사용 가능한 상태로 미리 만들어 놓는다. mark된 이미지는 DisplayObject에서 사용할 수 있다
     *
     * @name collie.ImageManager.mark 
     * @param {Object|String} vName 점찍을 이미지 이름, load에서 쓰이는 sName을 뜻하며 addImages의 HashTable 형태로 넣을 경우 키 값이 이름으로 들어가게 된다
     * @deprecated 정의되지 않은 이름을 부를 때 자동으로 mark되도록 수정
     */
    /**
     * @name collie.ImageManager.load 
     * @deprecated add로 변경
     */
}, collie.Component))();
comments powered by Disqus