Source: display/DisplayObject.js

/**
 * - A DisplayObject is basic class for display object.
 * - A DisplayObject can have one or many displayObject like a DOM Element.
 * - A offset values changed by when you set a spriteX and spriteY options, but there is no change when you set offsetX and offsetY values.
 * - You can use the addTo method with the method chaining
 * - A DisplayObject should be set useCache option as true if it doesn't change frequently.
 * 
 * @class
 * @extends collie.Component
 * @param {Object} [htOption] Options
 * @param {String} [htOption.name] 객체 이름, 중복 가능
 * @param {Number|String} [htOption.width="auto"] 너비, auto 값일 경우 backgroundImage가 설정되면 해당 이미지 너비 만큼 자동으로 변경 된다
 * @param {Number|String} [htOption.height="auto"] 높이, auto 값일 경우 backgroundImage가 설정되면 해당 이미지 높이 만큼 자동으로 변경 된다
 * @param {Number|String} [htOption.x=0] x축 위치, left, right, center 값을 사용하면 부모를 기준으로 정렬
 * @param {Number|String} [htOption.y=0] y축 위치, top, bottom, center 값을 사용하면 부모를 기준으로 정렬
 * @param {Number} [htOption.zIndex=0] 표시 순서. 높을 수록 위에 있음. 같으면 추가한 순서대로
 * @param {Number} [htOption.opacity=1] 투명도(0~1)
 * @param {Number} [htOption.angle=0] 회전각(0~360 deg)
 * @param {Number} [htOption.scaleX=1] x축 비율
 * @param {Number} [htOption.scaleY=1] y축 비율
 * @param {Number} [htOption.originX=center] scale, angle 적용 가로 기준 [left, right, center, 숫자]
 * @param {Number} [htOption.originY=center] scale, angle 적용 세로 기준 [top, bottom, center, 숫자]
 * @param {Number} [htOption.offsetX=0] 배경 이미지 시작 x좌표
 * @param {Number} [htOption.offsetY=0] 배경 이미지 시작 y좌표
 * @param {Number} [htOption.spriteX=null] 배경 가로 스프라이트 index, 너비 * index 값으로 offsetX가 설정된다
 * @param {Number} [htOption.spriteY=null] 배경 세로 스프라이트 index, 높이 * index 값으로 offsetY가 설정된다
 * @param {Number} [htOption.spriteLength=0] 배경 스프라이트 frame수, 가로폭 제한 스프라이트일 경우에 전체 프레임 수를 지정한다. 높이가 height보다 크지 않은 경우 적용되지 않는다
 * @param {Number} [htOption.spriteSheet=null] 배경 스프라이트 시트 이름, ImageManager에서 addSprite로 정보를 넣었을 경우 사용할 수 있다
 * @param {Array} [htOption.rangeX=null] X좌표 가용 범위. 배열로 최소, 최대값을 설정 [min, max], 상대 좌표임(현재 객체의 x, y좌표와 동일)
 * @param {Array} [htOption.rangeY=null] Y좌표 가용 범위. 배열로 최소, 최대값을 설정 [min, max], 상대 좌표임(현재 객체의 x, y좌표와 동일)
 * @param {Boolean} [htOption.positionRepeat=false] x,y 좌표의 범위 설정(rangeX, rangeY)이 되어 있는 경우 범위를 벗어나면 원점으로 돌아오는지 여부를 설정. fasle면 경계값까지만 움직이고 멈춤
 * @param {String} [htOption.backgroundColor] 배경색
 * @param {String} [htOption.backgroundImage] 배경이미지, 이미지매니져 리소스 이름이나 엘리먼트
 * @param {String} [htOption.backgroundRepeat='no-repeat'] 배경 이미지 반복 방법 repeat, repeat-x, repeat-y, no-repeat, no-repeat이 아니라면 useCache가 자동으로 true로 변함
 * @param {Boolean} [htOption.fitImage=false] 이미지를 객체 크기에 맞추기 
 * @param {collie.DisplayObject|Array} [htOption.hitArea] 이벤트 영역으로 사용될 다른 객체나 Polyline Path를 배열로 설정한다. 이 때 좌표는 상대 좌표 [[x1, y1], [x2, y2], ...] 
 * @param {Boolean} [htOption.debugHitArea=false] 이벤트 영역으로 사용된 hitArea를 화면에서 직접 확인할 수 있다. 성능에 좋지 않기 때문에 디버깅할 때만 사용해야 한다. 
 * @param {Number} [htOption.velocityX=0] x축 속도(초당 px)
 * @param {Number} [htOption.velocityY=0] y축 속도(초당 px)
 * @param {Number} [htOption.velocityRotate=0] 회전 속도(초당 1도)
 * @param {Number} [htOption.forceX=0] x축 힘(초당 px)
 * @param {Number} [htOption.forceY=0] y축 힘(초당 px)
 * @param {Number} [htOption.forceRotate=0] 회전 힘(초당 1도)
 * @param {Number} [htOption.mass=1] 질량
 * @param {Number} [htOption.friction=0] 마찰력
 * @param {Boolean} [htOption.useRealTime=true] SkippedFrame을 적용해서 싸이클을 현재 시간과 일치 
 * @param {Boolean} [htOption.useCache=false] 타일 캐시 사용 여부. 자식 객체를 모두 자신의 Context에 그려 놓는다.
 * @param {String|Boolean} [htOption.useEvent="auto"] 이벤트 사용 여부, Layer옵션과 DisplayObject 옵션 중에 하나라도 false라면 동작하지 않는다. auto면 attach된 이벤트가 있을 경우에만 동작한다 
 * @param {Boolean} [htOption.visible=true] 화면 표시 여부. false면 자식 객체도 보이지 않는다. "hidden" 값으로 설정하면 자식 객체는 표시하고 자신만 보이지 않게 한다
 */
collie.DisplayObject = collie.Class(/** @lends collie.DisplayObject.prototype */{
	/**
	 * 클래스 타입
	 * @type {String}
	 */
	type : "displayobject",

	$init : function (htOption) {
		this._bInitOption = true;
		this._htOption = {
			name : "", // 객체 이름
			width : 'auto',
			height : 'auto',
			x : 0,
			y : 0,
			zIndex : 0, // 표시 순서
			opacity : 1, // 투명도
			angle : 0, // 로테이션(각도)
			scaleX : 1,
			scaleY : 1,
			originX : "center",
			originY : "center",
			offsetX : 0,
			offsetY : 0,
			spriteX : null,
			spriteY : null,
			spriteLength : 0,
			spriteSheet : null,
			rangeX : null, // X좌표 가용 범위를 설정, 배열로 min, max 설정함 [min, max]
			rangeY : null, // Y좌표 가용 범위를 설정, 배열로 min, max 설정함 [min, max]
			positionRepeat : false, // x,y 좌표의 범위 설정이 되어 있는 경우 범위를 벗어나면 원점으로 돌아오는지 여부를 설정. fasle면 경계값까지만 움직이고 멈춤
			backgroundColor : '', // 배경색
			backgroundImage : '', // 배경이미지, 이미지매니져 리소스 이름이나 엘리먼트
			backgroundRepeat : 'no-repeat', // 배경 이미지 반복 repeat, repeat-x, repeat-y, no-repeat
			hitArea : null,
			debugHitArea : false,
			useCache : false,
			useEvent : "auto",
			fitImage : false, // 이미지를 객체 크기에 맞게 늘리기
			velocityX : 0,
			velocityY : 0,
			velocityRotate : 0,
			forceX : 0,
			forceY : 0,
			forceRotate : 0,
			mass : 1, // 질량
			friction : 0, // 마찰
			useRealTime : true,
			visible : true // 화면 표시 여부
		};
		
		if (htOption) {
			this.option(htOption);
		}
		
		this._htDirty = {};
		this._htMatrix = {};
		this._sId = ++collie.DisplayObject._idx;
		this._elImage = null;
		this._aDisplayObjects = [];
		this._oLayer = null;
		this._oParent = null;
		this._oDrawing = null;
		this._bIsSetOption = false;
		this._bChanged = true;
		this._bChangedTransforms = true;
		this._bCustomSize = false;
		this._aChangedQueue = null;
		this._htGetImageData = null;
		this._htRelatedPosition = {};
		this._htParentRelatedPosition = {};
		this._htBoundary = {};
		this._sRenderingMode = null;
		this._bRetinaDisplay = collie.Renderer.isRetinaDisplay();
		this._oTimerMove = null;
		this._nPositionRight = null;
		this._nPositionBottom = null;
		this._nImageWidth = 0;
		this._nImageHeight = 0;
		this._htImageSize = null;
		this._htSpriteSheet = null;
		this._htOrigin = {
			x : 0,
			y : 0
		};
		
		this.set(this._htOption);
		this._bIsSetOption = true;
	},
	
	/**
	 * 설정 값을 변경한다
	 * @example
	 * oDisplayObject.set({
	 * 	visible : false,
	 *  opacity : 1
	 * });
	 * 
	 * oDisplayObject.set("visible", true);
	 * 
	 * @param {String|Object} vKey 설정 이름. 여러개의 값을 Object로 한번에 설정할 수 있다.
	 * @param {Variables} vValue 값
	 * @param {Boolean} [bSkipSetter] setter를 수행하지 않음. 일반적으로 사용하는 것은 권장하지 않는다
	 * @param {Boolean} [bSkipChanged] 상태 변경을 하지 않는다. 상태변경을 하지 않게 되면 다시 그리지 않는다
	 * @return {collie.DisplayObject} For Method Chaining
	 */
	set : function (vKey, vValue, bSkipSetter, bSkipChanged) {
		if (typeof vKey === "object") {
			// 나머지 실행
			for (var i in vKey) {
				this.set(i, vKey[i]);
			}
		} else {
			// 값이 변하지 않았다면 처리하지 않음
			if (this._bIsSetOption && this._htOption[vKey] === vValue) {
				return;
			}
			
			// 크기 자동 변경 값 적용
			if (vKey === "width" || vKey === "height") {
				if (vValue !== "auto") {
					this._bCustomSize = true;
				} else if (vValue === "auto" && this.getImage() !== null) {
					vValue = this.getImageSize()[vKey];
				} else {
					vValue = 100;
				}
			}
			
			this._htOption[vKey] = vValue;
			this.setDirty(vKey); // record value to find
			
			if (!bSkipSetter) {
				this._setter(vKey, vValue);
			}
			
			if (!bSkipChanged) {
				// check if DisplayObject changed only transform values
				this.setChanged((vKey === 'x' || vKey === 'y' || vKey === 'angle' || vKey === 'scaleX' || vKey === 'scaleY' || vKey === 'opacity') ? true : false);
			}
		}
		
		return this;
	},
	
	/**
	 * setter
	 * @private
	 * @param {String} vKey 설정 이름
	 * @param {Variables} vValue 값
	 */
	_setter : function (vKey, vValue) {
		// zIndex hash 갱신
		if (vKey === "zIndex") {
			if (this._oParent) {
				this._oParent.changeDisplayObjectZIndex(this);
			} else if (this.getLayer()) {
				this._oLayer.changeDisplayObjectZIndex(this);
			}
		}
		
		// 값 보정
		if (vKey === "x" || vKey === "y") {
			if (typeof vValue === "string") {
				this.align(vKey === "x" ? vValue : false, vKey === "y" ? vValue : false);
			}
			
			this._fixPosition();
			this.resetPositionCache();
		}
		
		// 이미지 설정
		if (vKey === "backgroundImage") {
			this.setImage(vValue);
		}
		
		// 스프라이트 속성 적용
		if (vKey === 'spriteX' || vKey === 'spriteY' || vKey === 'spriteSheet') {
			this._setSpritePosition(vKey, vValue);
		}
		
		if ((vKey === 'width' || vKey === 'height')) {
			if (this._htOption.spriteX !== null) {
				this._setSpritePosition("spriteX", this._htOption.spriteX);
			}
			
			if (this._htOption.spriteY !== null) {
				this._setSpritePosition("spriteY", this._htOption.spriteY);
			}
		}
		
		// hitArea 배열 캐싱
		if (vKey === 'hitArea' && vValue instanceof Array) {
			this._makeHitAreaBoundary();
		}
		
		// origin 변환
		if (vKey === 'width' || vKey === 'height' || vKey === 'originX' || vKey === 'originY') {
			this._setOrigin();
		}
		
		// 배경 반복 상태면 캐시 사용
		if ((vKey === 'backgroundRepeat' && vValue !== 'no-repeat')) {
			this.set("useCache", true);
		}
		
		// 캔버스 캐시 생성
		if (vKey === 'useCache' && this._oDrawing !== null && this._oDrawing.loadCache) {
			if (vValue) {
				this._oDrawing.loadCache();
			} else {
				this._oDrawing.unloadCache();
			}
		}
	},
	
	/**
	 * 설정 값을 가져온다
	 * @param {String} sKey 값이 없으면 전체 값을 반환
	 * @return {Variable|Object} 설정 값
	 * @example
	 * var htData = oDisplayObject.get();
	 * var bVisible = oDisplayObject.get("visible");
	 * @example
	 * <caption>성능을 올리기 위해서는 메서드 호출을 최소한으로 줄이는 것이 좋다
	 * If you want to improve performance to your service, you should use less method call.</caption>
	 * // before
	 * var x = oDisplayObject.get("x");
	 * var y = oDisplayObject.get("y");
	 * var width = oDisplayObject.get("width");
	 * var height = oDisplayObject.get("height");
	 * 
	 * // after
	 * var htInfo = oDisplayObject.get();
	 * htInfo.x;
	 * htInfo.y;
	 * htInfo.width;
	 * htInfo.height;
	 * 
	 * // or you can access a htOption object directly. It's not recommend but It's better than frequently method call.
	 * oDisplayObject._htOption.x;
	 * oDisplayObject._htOption.y;
	 * oDisplayObject._htOption.width;
	 * oDisplayObject._htOption.height;
	 */
	get : function (sKey) {
		if (!sKey) {
			return this._htOption;
		} else {
			return this._htOption[sKey];
		}
	},
	
	/**
	 * 값이 변경된 것으로 설정
	 * 
	 * @param {String} sKey 키 이름, 값이 없으면 모든 값을 다시 적용함
	 */
	setDirty : function (sKey) {
		if (this._htDirty === null) {
			this._htDirty = {};
		}
		
		if (typeof sKey === "undefined") {
			for (var i in this._htOption) {
				this._htDirty[i] = true; 				
			}
		} else {
			this._htDirty[sKey] = true; 				
		}
	},
	
	/**
	 * 값이 변경된 것을 알림
	 *
	 * @param {String} sKey 키 이름
	 * @return {Boolean} true면 값이 변경 됐음
	 */
	getDirty : function (sKey) {
		if (!sKey) {
			return this._htDirty;
		} else {
			return this._htDirty[sKey] ? true : false;
		}
	},
	
	/**
	 * Dirty 값을 초기화, 다 그리고 난 후에 실행 한다
	 * @private
	 */
	_resetDirty : function () {
		this._htDirty = null;
	},
	
	/**
	 * DisplayObject의 자식을 추가 한다
	 * - 자식으로 들어간 DisplayObject는 현재 DisplayObject의 zIndex 영향을 받게 된다
	 * 
	 * @param {collie.DisplayObject} oDisplayObject
	 */
	addChild : function (oDisplayObject) {
		collie.util.pushWithSort(this._aDisplayObjects, oDisplayObject);
		oDisplayObject.setParent(this);
		
		if (this._oLayer !== null) {
			oDisplayObject.setLayer(this._oLayer);
		}
		
		this.setChanged();
	},
	
	/**
	 * 자식을 제거 한다
	 * @param {collie.DisplayObject} oDisplayObject
	 * @param {Number} nIdx 인덱스 번호를 알고 있다면 인덱스 번호를 지정
	 */
	removeChild : function (oDisplayObject, nIdx) {
		if (typeof nIdx !== "undefined") {
			this._aDisplayObjects[nIdx].unsetLayer();
			this._aDisplayObjects[nIdx].unsetParent();			
			this._aDisplayObjects.splice(nIdx, 1);
		} else {
			for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
				if (this._aDisplayObjects[i] === oDisplayObject) {
					this._aDisplayObjects[i].unsetLayer();
					this._aDisplayObjects[i].unsetParent();
					this._aDisplayObjects.splice(i, 1);
					break;
				}
			}
		}
		
		this.setChanged();
	},
	
	/**
	 * zIndex가 변경되었다면 이 메소드를 호출
	 * 
	 * @private
	 * @param {collie.DisplayObject} oDisplayObject
	 */
	changeDisplayObjectZIndex : function (oDisplayObject) {
		this.removeChild(oDisplayObject);
		this.addChild(oDisplayObject);
	},
	
	/**
	 * 레이어나 DisplayObject 객체에 현재 객체를 추가 한다.
	 * 
	 * @param {collie.Layer|collie.DisplayObject} oTarget
	 * @return {collie.DisplayObject}
	 */
	addTo : function (oTarget) {
		// 이미 추가돼 있다면 빼고 다시 넣음
		if (this._oLayer || this._oParent) {
			// 같은데라면 동작 취소
			if (this._oLayer === oTarget || this._oParent === oTarget) {
				return this;
			} else {
				this.leave();
			}
		}
		
		oTarget.addChild(this);
		return this;
	},
	
	/**
	 * 자식이 있는지 반환
	 * 
	 * @return {Boolean} 자식이 있다면 true
	 */
	hasChild : function () {
		return this._aDisplayObjects.length > 0;
	},
	
	/**
	 * 자식을 반환
	 * 
	 * @return {Array}
	 */
	getChildren : function () {
		return this._aDisplayObjects;
	},
	
	/**
	 * 부모를 반환
	 * 
	 * @return {collie.DisplayObject}
	 */
	getParent : function () {
		return this._oParent || false;
	},
	
	/**
	 * 부모를 설정
	 * - 직접 호출하지 않는다
	 * @private
	 * @param {collie.DisplayObject} oDisplayObject
	 */
	setParent : function (oDisplayObject) {
		this._oParent = oDisplayObject;	
	},
	
	/**
	 * 부모를 해제
	 * @private
	 */
	unsetParent : function () {
		this._oParent = null;
	},
	
	/**
	 * 부모가 있을 경우 부모에서 자신을 뺀다
	 * @return {collie.DisplayObject} 자신을 반환
	 */
	leave : function () {
		var oParent = null;
		
		if (this._oParent !== null) {
			oParent = this._oParent;
		} else if (this._oLayer) {
			oParent = this.getLayer();
		}
		
		if (oParent) {
			oParent.removeChild(this);
		}
		
		return this;
	},
	
	/**
	 * 아이디를 반환 한다
	 * 
	 * @return {String}
	 */
	getId : function () {
		return this._sId;
	},
	
	/**
	 * 현재 객체의 배경 이미지를 가져온다
	 * 
	 * @return {HTMLElement}
	 */
	getImage : function () {
		return this._elImage || null;
	},
	
	/**
	 * 이미지 크기를 반환, 레티나일 경우 보정된 값을 반환 한다
	 * Return a size of the image set to backgroundImage property.
	 * If The User has a retina display, this method would return a half of size.
	 * ex) 640*940 -> 320*480
	 * 
	 * @return {Boolean|Object} htSize 이미지가 로드되지 않았으면 false를 반환. It would return as false when it has not loaded the image yet.
	 * @return {Number} htSize.width
	 * @return {Number} htSize.height
	 */
	getImageSize : function () {
		return this._htImageSize || false;
	},
	
	/**
	 * 이미지를 설정한다
	 * - TODO 비동기 주의해야 함
	 * - TODO setImage 바로 못하게 해야 함 backgroundImage로... 값이 어긋남
	 * @param {String|HTMLImageElement} vImage ImageManager의 리소스 이름이나 이미지 엘리먼트
	 * @private
	 */
	setImage : function (vImage) {
		if (typeof vImage === "string" || !vImage) {
			// 이미 걸어놓은 이미지가 있다면 취소
			if (this._htGetImageData !== null && this._htGetImageData.name !== vImage) {
				collie.ImageManager.cancelGetImage(this._htGetImageData.name, this._htGetImageData.callback);
				this._htGetImageData = null;
			}
			
			if (!vImage) {
				this._elImage = null;
				this.setChanged();
			} else {
				this._htGetImageData = {
					name : vImage,
					callback : (function (elImage) {
						this.setImage(elImage);
					}).bind(this)
				};
				
				collie.ImageManager.getImage(this._htGetImageData.name, this._htGetImageData.callback);
			}
			
			return;
		}
		
		// 같은 이미지면 적용하지 않음
		if (this._elImage && this._elImage === vImage) {
			return;
		}
		
		// reflow 예방을 위한 이미지 크기 캐시
		this._elImage = vImage;
		this._nImageWidth = vImage.width;
		this._nImageHeight = vImage.height;
		this._htImageSize = {
			width : this._bRetinaDisplay ? this._nImageWidth / 2 : this._nImageWidth,
			height : this._bRetinaDisplay ? this._nImageHeight / 2 : this._nImageHeight
		};
		this._htSpriteSheet = collie.ImageManager.getSprite(this._htOption.backgroundImage);
		
		// 사용자가 크기를 설정 안했으면 자동으로 이미지 크기로 설정 됨
		if (!this._bCustomSize) {
			this.set({
				width : this._htImageSize.width,
				height : this._htImageSize.height
			});
		}
		
		this._setSpritePosition("spriteSheet", this._htOption.spriteSheet);
		this._setSpritePosition("spriteX", this._htOption.spriteX);
		this._setSpritePosition("spriteY", this._htOption.spriteY);
		this.setDirty("backgroundImage");
		this.setChanged();
	},
	
	/**
	 * 드로잉 객체를 반환
	 * @return {collie.DisplayObjectCanvas|collie.DisplayObjectDOM}
	 */
	getDrawing : function () {
		return this._oDrawing;
	},
	
	/**
	 * 변경된 내용이 있을 경우 Layer에 알린다
	 * - 개발용
	 * TODO setChanged 실행 횟수가 많은데 중복 실행을 줄이면 성능이 향상되나?
	 * -> flag만 두고 실제 setChanged 전파는 draw하기 전에 하는 것임
	 * 
	 * @private
	 * @param {Boolean} bChangedTransforms transform 값이 변경되는지 여부
	 */
	setChanged : function (bChangedTransforms) {
		// 이미 변경된 것으로 돼 있다면 실행하지 않음
		if (this._bChanged || (bChangedTransforms && this._bChangedTransforms)) {
			return;
		}
		
		if (this._oLayer !== null) {
			this._oLayer.setChanged();
		}
		
		if (!bChangedTransforms) {
			this._bChanged = true;
		}
		
		this._bChangedTransforms = true;
		
		// 부모가 있다면 부모도 바뀐 상태로 변경, 반복적으로 부모에게 전달됨
		if (this._oParent) {
			this._oParent.setChanged(false); // transforms만 바꼈어도 부모에게는 전체가 바뀐것으로 통보
		}
	},
	
	/**
	 * 변경된 내용이 반영 되었을 때
	 * TODO changed라는 이름 변경할 필요성 있음
	 * @private
	 */
	unsetChanged : function () {
		this._bChanged = false;
		this._bChangedTransforms = false;
	},
	
	/**
	 * 현재 객체에 변경된 내용 여부를 반환
	 * DOM일 경우 변경된게 없으면 다시 안그림
	 * 
	 * @param {Boolean} bChangedOnlyTranforms
	 * @return {Boolean}
	 */
	isChanged : function (bChangedOnlyTranforms) {
		return !bChangedOnlyTranforms ? (this._bChanged || this._bChangedTransforms) : !this._bChanged && this._bChangedTransforms;
	},
	
	/**
	 * 레이어에 객체를 추가
	 * 
	 * - 직접 사용하지 않는다
	 * @private
	 * @param {collie.Layer} oLayer
	 */
	setLayer : function (oLayer) {
		// 중복된 값이 있으면 에러
		if (this._sId in collie.DisplayObject.htFactory) {
			throw new Error('Exists DisplayObject Id ' + this._sId);
		}
		
		collie.DisplayObject.htFactory[this._sId] = this;
		this._oLayer = oLayer;
		this._sRenderingMode = this._oLayer.getRenderingMode();
		this._makeDrawing();
		this._oDrawing.load();
		this.setChanged();
		
		// 정렬 적용
		if (typeof this._htOption.x === "string" || typeof this._htOption.y === "string") {
			this.align(typeof this._htOption.x === "string" ? this._htOption.x : false, typeof this._htOption.y === "string" ? this._htOption.y : false);
		}
		
		if (this._nPositionRight !== null) {
			this.right(this._nPositionRight);
			this._nPositionRight = null;
		}
		
		if (this._nPositionBottom !== null) {
			this.bottom(this._nPositionBottom);
			this._nPositionBottom = null;
		}
		
		// 자식도 setLayer 적용
		for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
			this._aDisplayObjects[i].setLayer(oLayer);
		}
	},

	/**
	 * 레이어에서 객체를 뺌
	 * @private
	 */	
	unsetLayer : function () {
		if (this.getLayer()) {
			for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
				this._aDisplayObjects[i].unsetLayer();
			}
		
			this._oDrawing.unload();
			this.setDirty();
			this.setChanged();
			this._sRenderingMode = null;
			this._oDrawing = null;
			this._oLayer = null;
			delete collie.DisplayObject.htFactory[this._sId];
		}
	},
	
	/**
	 * @private
	 */
	_makeDrawing : function () {
		if (this._oDrawing === null) {
			this._oDrawing = this._sRenderingMode === "dom" ? new collie.DisplayObjectDOM(this) : new collie.DisplayObjectCanvas(this);
		}
	},
	
	/**
	 * 레이어 반환
	 * 
	 * @return {collie.Layer|Boolean}
	 */
	getLayer : function () {
		return this._oLayer || false;
	},
	
	/**
	 * 다양한 속성을 변경하며 사용할 경우 addMatrix를 이용해 설정을 미리 만들고 changeMatrix로 변경해 사용할 수 있다.
	 * 
	 * @param {Array|Object} vMatrix 배열로 여러개를 한번에 넣을 수 있음
	 * @param {String} vMatrix.name Matrix 이름
	 * @param {Number} vMatrix.property 변경할 설정을 입력한다 
	 * @example
	 * oDisplayObject.addMatrix({
	 * 	name : "test"
	 * 	offsetX : 0,
	 * 	offsetY : 100
	 * });
	 * oDisplayObject.addMatrix([
	 * 	{ name : "test2", offsetX : 100, offsetY : 100, width : 50, height : 50 },
	 * 	{ name : "test3", offsetX : 200, offsetY : 100, width : 80, height : 80 }
	 * ]);
	 * 
	 * oDisplayObject.changeMatrix("test2");
	 * oDisplayObject.changeMatrix("test3");
	 */
	addMatrix : function (vMatrix) {
		if (vMatrix instanceof Array) {
			for (var i = 0, len = vMatrix.length; i < len; i++) {
				this.addMatrix(vMatrix[i]);
			}
			return;
		}
		
		this._htMatrix[vMatrix.name] = vMatrix;
		delete this._htMatrix[vMatrix.name].name;
	},
	
	/**
	 * 해당 Matrix로 변경한다
	 * 
	 * @param {String} sName 매트릭스 이름
	 */
	changeMatrix : function (sName) {
		if (sName in this._htMatrix) {
			this.set(this._htMatrix[sName]);
		}
	},
	
	/**
	 * DisplayObject를 갱신한다.
	 * 
	 * @param {Number} nFrameDuration 진행된 프레임 시간
	 * @param {Number} nX 부모로 부터 내려온 x좌표
	 * @param {Number} nY 부모로 부터 내려온 y좌표
	 * @param {Number} nLayerWidth 레이어 너비, update는 tick안에 있는 로직이기 때문에 성능 극대화를 위해 전달
	 * @param {Number} nLayerHeight 레이어 높이
	 * @param {Object} oContext 부모의 Canvas Context, useCache를 사용하면 넘어온다
	 * @return {Boolean} true를 반환하면 계속 바뀔게 있다는 뜻
	 * @private
	 */
	update : function (nFrameDuration, nX, nY, nLayerWidth, nLayerHeight, oContext) {
		this._updateMovableOption(nFrameDuration);
		
		// Canvas 방식이고, 보이지 않는 객체면 그린걸로 친다, 자식도 그리지 않아도 된다.
		if (this._sRenderingMode === "canvas" && !this._htOption.visible) {
			this.unsetChanged();
			return;
		}
		
		nX += this._htOption.x;
		nY += this._htOption.y;
		
		// Canvas에서 화면 밖으로 나가거나 DOM에서 바뀐게 있을 떄 그림
		if (
				(this._sRenderingMode === "dom" && this.isChanged()) || (
				this._sRenderingMode === "canvas" && (
					nX + this._htOption.width >= 0 ||
					nX <= nLayerWidth ||
					nY + this._htOption.height >= 0 ||
					nY <= nLayerHeight
				)
			)) {
			this._oDrawing.draw(nFrameDuration, nX, nY, nLayerWidth, nLayerHeight, oContext);
		}
		
		this.unsetChanged();
		this._resetDirty();
		
		// 움직임이 있으면 다시 바뀐 상태로 둠
		if (
			this._htOption.velocityX !== 0 ||
			this._htOption.velocityY !== 0 ||
			this._htOption.velocityRotate !== 0 ||
			this._htOption.forceX !== 0 ||
			this._htOption.forceY !== 0 ||
			this._htOption.forceRotate !== 0
		) {
			this.setChanged(true);
		}
		
		// Canvas 방식은 자식을 직접 그리고, DOM 방식이면 부모가 보이지 않는 상태면 자식도 그리지 않는다
		if (this._sRenderingMode === "canvas" || !this._htOption.visible) {
			return;
		}
		
		// update 자식에게 전파
		if (this.hasChild()) {
			for (var i = 0, len = this._aDisplayObjects.length; i < len; i++) {
				this._aDisplayObjects[i].update(nFrameDuration, nX, nY, nLayerWidth, nLayerHeight);
			}
		}
	},
	
	_updateMovableOption : function (nFrameDuration) {
		if (
			this._htOption.velocityX !== 0 ||
			this._htOption.velocityY !== 0 ||
			this._htOption.velocityRotate !== 0 ||
			this._htOption.forceX !== 0 ||
			this._htOption.forceY !== 0 ||
			this._htOption.forceRotate !== 0
		) {
			var nFrame = Math.max(17, nFrameDuration) / 1000;
			
			// skippedFrame 적용을 하지 않는다면 1frame 씩만 그림
			if (!this._htOption.useRealTime) {
				nFrame = 1;
			}
			
			var nVelocityX = this._htOption.velocityX;
			var nVelocityY = this._htOption.velocityY;
			var nX = this._htOption.x;
			var nY = this._htOption.y;
			
			// 힘 적용 a = F / m
			nVelocityX += (this._htOption.forceX / this._htOption.mass) * nFrame;
			nVelocityY += (this._htOption.forceY / this._htOption.mass) * nFrame;
			
			// 마찰력 적용
			var nForceFrictionX = this._htOption.friction * nVelocityX * this._htOption.mass * nFrame;
			var nForceFrictionY = this._htOption.friction * nVelocityY * this._htOption.mass * nFrame;
			
			if (nVelocityX !== 0) {
				nVelocityX = (Math.abs(nVelocityX) / nVelocityX !== Math.abs(nVelocityX - nForceFrictionX) / (nVelocityX - nForceFrictionX)) ? 0 : nVelocityX - nForceFrictionX;
			}
			
			if (nVelocityY !== 0) {
				nVelocityY = (Math.abs(nVelocityY) / nVelocityY !== Math.abs(nVelocityY - nForceFrictionY) / (nVelocityY - nForceFrictionY)) ? 0 : nVelocityY - nForceFrictionY;
			}
			
			nX += nVelocityX * nFrame;
			nY += nVelocityY * nFrame;
			nVelocityX = Math.floor(nVelocityX * 1000) / 1000;
			nVelocityY = Math.floor(nVelocityY * 1000) / 1000;
			
			if (this._htOption.friction && Math.abs(nVelocityX) < 0.05) {
				nVelocityX = 0;
			}
			
			if (this._htOption.friction && Math.abs(nVelocityY) < 0.05) {
				nVelocityY = 0;
			}
		
			// 변경이 있을 때만 설정
			if (
				nX !== this._htOption.x ||
				nY !== this._htOption.y ||
				nVelocityX !== this._htOption.velocityX ||
				nVelocityY !== this._htOption.velocityY
			) {
				this.set("x", nX);
				this.set("y", nY);
				this.set("velocityX", nVelocityX);
				this.set("velocityY", nVelocityY);
			}
			
			if (this._htOption.forceRotate !== 0) {
				this.set("velocityRotate", this._htOption.velocityRotate + this._htOption.forceRotate);
			}
			
			if (this._htOption.velocityRotate !== 0) {
				var nAngleRad = collie.util.fixAngle(collie.util.toRad(this._htOption.angle + this._htOption.velocityRotate * nFrame));
				this.set("angle", Math.round(collie.util.toDeg(nAngleRad) * 1000) / 1000);
			}
		}
	},
	
	/**
	 * 부모와 연관된 전체 좌표를 구한다(절대좌표)
	 * @todo 메소드 명이 직관적이지 못하다
	 * 
	 * @return {Object} htPos
	 * @return {Number} htPos.x
	 * @return {Number} htPos.y
	 */
	getRelatedPosition : function () {
		if (this._htRelatedPosition.x === null) {
			this._htRelatedPosition.x = this._htOption.x;
			this._htRelatedPosition.y = this._htOption.y;
			
			if (this._oParent) {
				var htPosition = this._oParent.getRelatedPosition();
				this._htRelatedPosition.x += htPosition.x;
				this._htRelatedPosition.y += htPosition.y;
			}
		}
		
		return this._htRelatedPosition;
	},
	
	/**
	 * 현재 표시 객체의 사각형 영역을 반환 한다
	 * - transform된 영역을 반환
	 * TODO Transform Matrix의 origin에 상대좌표를 적용해야 하기 때문에 캐시를 적용할 수 없음
	 * TODO Transform 안 된지도 부모를 타고 가봐야 알 수 있음!
	 * 
	 * @param {Boolean} bWithRelatedPosition 절대좌표로 변경해서 반환하는지 여부
	 * @param {Boolean} bWithPoints 좌표를 반환하는지 여부, Sensor의 box hittest에서 쓰임
	 * @return {Object} oBoundary
	 * @return {Number} oBoundary.left
	 * @return {Number} oBoundary.right
	 * @return {Number} oBoundary.top
	 * @return {Number} oBoundary.bottom
	 * @return {Number} oBoundary.isTransform 트랜스폼 사용 여부
	 * @return {Array} oBoundary.points bWithPoints를 true로 하면 좌표 배열이 넘어옴, [[x, y], [x, y], ...]
	 */
	getBoundary : function (bWithRelatedPosition, bWithPoints) {
		var htBoundary = collie.Transform.getBoundary(this, bWithPoints);
		this._htBoundary.left = htBoundary.left;
		this._htBoundary.right = htBoundary.right;
		this._htBoundary.top = htBoundary.top;
		this._htBoundary.bottom = htBoundary.bottom;
		this._htBoundary.isTransform = htBoundary.isTransform;
		this._htBoundary.points = htBoundary.points;
		
		// 절대 좌표로 변환해서 반환
		if (bWithRelatedPosition) {
			var htPos = this.getRelatedPosition();
			
			if (this._htBoundary.points) {
				for (var i = 0, l = this._htBoundary.points.length; i < l; i++) {
					this._htBoundary.points[i][0] += htPos.x;
					this._htBoundary.points[i][1] += htPos.y;
				}
			}
			
			this._htBoundary.left += htPos.x;
			this._htBoundary.right += htPos.x;
			this._htBoundary.top += htPos.y;
			this._htBoundary.bottom += htPos.y;
		}
		
		return this._htBoundary;
	},
	
	/**
	 * 위치가 변경되는 경우 캐시를 초기화 해 줌
	 * @private
	 */
	resetPositionCache : function () {
		this._htRelatedPosition.x = null;
		this._htRelatedPosition.y = null;
		
		// 자체적으로 전파
		// TODO 속도 차이 반드시 확인해 봐야 함!!
		if (this.hasChild()) {
			for (var i = 0, l = this._aDisplayObjects.length; i < l; i++) {
				this._aDisplayObjects[i].resetPositionCache();
			}
		}
	},

	/**
	 * 이벤트와 관련된 영역을 반환 한다
	 * - transform된 영역을 반환
	 * - 절대 좌표로 변환해서 반환한다
	 * 
	 * @return {Object} htReturn
	 * @return {Number} htReturn.left minX
	 * @return {Number} htReturn.right maxX
	 * @return {Number} htReturn.top minY
	 * @return {Number} htReturn.bottom maxY
	 */
	getHitAreaBoundary : function () {
		if (!this._htOption.hitArea) {
			return this.getBoundary(true);
		} else if (this._htOption.hitArea instanceof Array) {
			var aPoints = collie.Transform.points(this, collie.util.getBoundaryToPoints(this._htHitAreaBoundary));
			var htBoundary = collie.util.getBoundary(aPoints);
			var htPos = this.getRelatedPosition();
			
			return {
				left : htBoundary.left + htPos.x,
				right : htBoundary.right + htPos.x,
				top : htBoundary.top + htPos.y,
				bottom : htBoundary.bottom + htPos.y
			};
		} else { // displayObject일 경우
			return this._htOption.hitArea.getBoundary(true);
		}
	},
	
	/**
	 * Scale, Angle 변경의 중심점을 구한다
	 * 
	 * @private
	 * @return {Object} htResult
	 * @return {Number} htResult.x x축 Origin
	 * @return {Number} htResult.y y축 Origin
	 */
	getOrigin : function () {
		return this._htOrigin;
	},
	
	/**
	 * origin을 px로 설정한다
	 * @private
	 */
	_setOrigin : function () {
		switch (this._htOption.originX) {
			case "left" :
				this._htOrigin.x = 0;
				break;
				
			case "right" :
				this._htOrigin.x = this._htOption.width;
				break;
				
			case "center" :
				this._htOrigin.x = this._htOption.width / 2;
				break;
				
			default :
				this._htOrigin.x = parseInt(this._htOption.originX, 10);
		}
				
		switch (this._htOption.originY) {
			case "top" :
				this._htOrigin.y = 0;
				break;
				
			case "bottom" :
				this._htOrigin.y = this._htOption.height;
				break;
				
			case "center" :
				this._htOrigin.y = this._htOption.height / 2;
				break;
				
			default :
				this._htOrigin.y = parseInt(this._htOption.originY, 10);
		}
	},
	
	/**
	 * range를 사용하고 있는 경우 range에 맞게 포지션을 변경 한다
	 * 
	 * @private
	 */
	_fixPosition : function () {
		var nX = this._htOption.x;
		var nY = this._htOption.y;
		var nMinX;
		var nMaxX;
		var nMinY;
		var nMaxY;
		
		if (this._htOption.rangeX) {
			// 상대를 절대 값으로
			nMinX = this._htOption.rangeX[0];
			nMaxX = this._htOption.rangeX[1];
			
			if (this._htOption.positionRepeat) {
				if (nX < nMinX) { // 최소값 보다 작을 때
					do {
						nX += (nMaxX - nMinX);
					} while (nX < nMinX); 
				} else if (nX > nMaxX) { // 최대값 보다 클 때
					do {
						nX -= (nMaxX - nMinX);
					} while (nX > nMaxX);
				}
			} else {
				nX = Math.max(nMinX, nX);
				nX = Math.min(nMaxX, nX);
			}
			
			if (nX !== this._htOption.x) {
				// 절대를 상대 값으로
				this.set("x", nX, true);
			}
		}
		
		if (this._htOption.rangeY) {
			nMinY = this._htOption.rangeY[0];
			nMaxY = this._htOption.rangeY[1];
			
			if (this._htOption.positionRepeat) {
				if (nY < nMinY) { // 최소값 보다 작을 때
					do {
						nY += (nMaxY - nMinY);
					} while (nY < nMinY); 
				} else if (nY > nMaxY) { // 최대값 보다 클 때
					do {
						nY -= (nMaxY - nMinY);
					} while (nY > nMaxY);
				}
			} else {
				nY = Math.max(nMinY, nY);
				nY = Math.min(nMaxY, nY);
			}
			
			if (nY !== this._htOption.y) {
				this.set("y", nY, true);
			}
		}
	},
	
	/**
	 * hitArea 옵션이 배열로 들어올 경우 boundary를 구해서 저장해놓는다
	 * @private
	 */
	_makeHitAreaBoundary : function () {
		this._htHitAreaBoundary = collie.util.getBoundary(this._htOption.hitArea);
	},
	
	/**
	 * 객체의 위치를 정렬한다.
	 * 
	 * @param {String|Boolean} [sHorizontal=center] 수평 정렬 [left|right|center], false면 정렬하지 않음
	 * @param {String|Boolean} [sVertical=center] 수직 정렬 [top|bottom|center], false면 정렬하지 않음
	 * @param {collie.DisplayObject} [oBaseObject] 기준 객체, 값이 없을 경우 부모, 부모가 없을 경우 레이어를  기준으로 정렬 한다.
	 */
	align : function (sHorizontal, sVertical, oBaseObject) {
		if (!this.getLayer()) {
			return;
		}
		
		oBaseObject = oBaseObject || this.getParent();
		var nWidth = 0;
		var nHeight = 0;
		var nX = 0;
		var nY = 0;
		
		// 기준 크기 구함
		if (oBaseObject) {
			nWidth = oBaseObject._htOption.width;
			nHeight = oBaseObject._htOption.height;
		} else {
			nWidth = this._oLayer._htOption.width;
			nHeight = this._oLayer._htOption.height;
		}
		
		if (sHorizontal !== false) {
			nX = (sHorizontal === "right") ? nWidth - this._htOption.width : nWidth / 2 - this._htOption.width / 2;
			this.set("x", nX);
		}

		if (sVertical !== false) {
			nY = (sVertical === "bottom") ? nHeight - this._htOption.height : nHeight / 2 - this._htOption.height / 2;
			this.set("y", nY);
		}
	},
	
	/**
	 * 객체의 위치를 우측 기준으로 좌표만큼 이동한다
	 * 만일 Layer에 붙은 상태가 아니라면 붙은 후에 이동할 수 있도록 해 준다
	 * 
	 * @param {Number} nPosition 우측 기준 x좌표
	 * @return {collie.DisplayObject} 자기 자신을 반환
	 */
	right : function (nPosition) {
		var nWidth = 0;
		
		// 기준 크기 구함
		if (this._oParent) {
			nWidth = this._oParent._htOption.width;
		}
		
		if (!nWidth && this._oLayer) {
			nWidth = this._oLayer._htOption.width;
		}
		
		// 크기가 구해졌을 때만 정렬
		if (nWidth) {
			this.set("x", nWidth - (this._htOption.width + nPosition));
		} else {
			this._nPositionRight = nPosition;
		}
		
		return this;
	},
	
	/**
	 * 객체의 위치를 하단 기준으로 좌표만큼 이동한다
	 * 만일 Layer에 붙은 상태가 아니라면 붙은 후에 이동할 수 있도록 해 준다
	 * 
	 * @param {Number} nPosition 하단 기준 x좌표
	 * @return {collie.DisplayObject} 자기 자신을 반환
	 */
	bottom : function (nPosition) {
		var nHeight = 0;
		
		// 기준 크기 구함
		if (this._oParent) {
			nHeight = this._oParent.get("height");
		}
		
		if (!nHeight && this._oLayer) {
			nHeight = this._oLayer.option("height");
		}
		
		// 크기가 구해졌을 때만 정렬
		if (nHeight) {
			this.set("y", nHeight - (this._htOption.height + nPosition));
		} else {
			this._nPositionBottom = nPosition;
		}
		
		return this;
	},
	
	/**
	 * 지정한 비율에 맞게 크기를 변경 한다. 리샘플링과는 다르다
	 * 인자 둘 중 하나를 설정하면 설정한 부분의 비율에 맞춰서 크기를 변경 한다
	 * 
	 * @param {Number} [nWidth] 너비
	 * @param {Number} [nHeight] 높이
	 */
	resizeFixedRatio : function (nWidth, nHeight) {
		if (this.getImage()) {
			var nImageWidth = this.getImage().width;
			var nImageHeight = this.getImage().height;
			
			if (nWidth) {
				nHeight = nWidth * (nImageHeight / nImageWidth);
			} else if (nHeight) {
				nWidth = nHeight * (nImageWidth / nImageHeight);
			}
			
			this.set("width", Math.round(nWidth));
			this.set("height", Math.round(nHeight));
		}
	},
	
	/**
	 * Sprite 위치를 설정
	 * offsetX, offsetY로 값을 설정할 경우에 spriteX, spriteY는 정상적으로 동기화되지 못하는 문제가 있음 역추적 불가능
	 * @private
	 * @param {String} sKey 속성 이름
	 * @param {Number} nValue 값
	 */
	_setSpritePosition : function (sKey, nValue) {
		if (this._elImage && nValue !== null) {
			// spriteSheet 사용 시
			if (this._htOption.spriteSheet !== null) {
				var sheet = this._htSpriteSheet[this._htOption.spriteSheet];
				var nOffsetX; 
				var nOffsetY;
				
				if (sKey === "spriteSheet" && this._htSpriteSheet && this._htSpriteSheet[nValue]) {
					if (typeof sheet[0][0] !== "undefined") {
						if (this._htOption.spriteX !== null) { // 이미 spriteX가 있다면
							nOffsetX = sheet[this._htOption.spriteX][0];
							nOffsetY = sheet[this._htOption.spriteX][1];
						} else {
							nOffsetX = sheet[0][0];
							nOffsetY = sheet[0][1];
						}
					} else {
						nOffsetX = sheet[0];
						nOffsetY = sheet[1];
					}
					
					// 초기 위치 잡아줌
					this.set("offsetX", nOffsetX, true);
					this.set("offsetY", nOffsetY, true);
				} else if (sKey === "spriteX" && typeof sheet[nValue] !== "undefined") {
					this.set("offsetX", sheet[nValue][0], true);
					this.set("offsetY", sheet[nValue][1], true);
				}
			} else {
				var htImageSize = this.getImageSize();
				var nWidth = this._htOption.width;
				var nHeight = this._htOption.height;
				var nSpriteLength = this._htOption.spriteLength - 1; // 0부터 시작
				var nMaxSpriteX = (htImageSize.width / this._htOption.width) - 1;
				var nMaxSpriteY = (htImageSize.height / this._htOption.height) - 1;
				var nMaxOffsetX = htImageSize.width - 1;
				var nMaxOffsetY = htImageSize.height - 1;
				
				// spriteLength가 적용되어 있는 경우 최대 offset이 변경 됨
				if (nSpriteLength >= 0 && nHeight < htImageSize.height) {
					nMaxOffsetX = nMaxSpriteX * htImageSize.width;
					nMaxOffsetY = nMaxSpriteY * htImageSize.height;
				}
				
				switch (sKey) {
					case "spriteX" :
						var nOffsetX = 0;
						var nOffsetY = 0;
						
						// sprite길이를 지정했고 그게 최대 스프라이트 수보다 크다면 그것을 따라감
						if (nSpriteLength > nMaxSpriteX && nHeight < htImageSize.height) {
							nOffsetY = Math.floor(nValue / (nMaxSpriteX + 1)) * nHeight;
							nOffsetX = (nValue % (nMaxSpriteX + 1)) * nWidth;
						} else {
							nOffsetX = Math.min(nValue, nMaxSpriteX) * nWidth;
						}
						
						//TODO android 성능 문제, DisplayObject#set, timer, Animation#triggerCallback, spriteX 처리
						this.set("offsetX", nOffsetX, true);
						this.set("offsetY", nOffsetY, true);
						break;
						
					case "spriteY" :
						nValue = Math.min(nValue, nMaxSpriteY);
						this.set("offsetY", nValue * nHeight, true);
						break;
				}
			}
		}
	},
	
	/**
	 * attach된 이벤트 핸들러가 있는지 여부를 반환
	 *
	 * @return {Boolean}
	 */
	hasAttachedHandler : function () {
		if (
			this._htHandler && 
			(("click" in this._htHandler) && this._htHandler.click.length > 0) ||  
			(("mousedown" in this._htHandler) && this._htHandler.mousedown.length > 0) ||  
			(("mouseup" in this._htHandler) && this._htHandler.mouseup.length > 0)  
			) {
			return true;
		} else {
			return false;
		}
	},
	
	/**
	 * 특정 속도로 해당 지점까지 이동
	 * 
	 * @param {Number} nX 가고자 하는 곳의 x 좌표
	 * @param {Number} nY 가고자 하는 곳의 y 좌표
	 * @param {Number} nVelocity 초당 이동 거리(px), 속도가 0 이면 바로 이동한다.
	 * @param {Function} fCallback 이동이 끝난 후 실행될 콜백
	 * @param {collie.DisplayObject} fCallback.displayobject 현재 객체가 인자로 넘어감=
	 * @return {collie.AnimationTransition} 이동에 사용되는 타이머를 반환
	 */
	move : function (nX, nY, nVelocity, fCallback) {
		var nCurrentX = this._htOption.x;
		var nCurrentY = this._htOption.y;
		var nDistance = collie.util.getDistance(nCurrentX, nCurrentY, nX, nY);
		var nDuration = Math.round((nDistance / nVelocity) * 1000);
		
		if (this._oTimerMove !== null) {
			this._oTimerMove.stop();
			this._oTimerMove = null;
		}
		
		// duration이 없을 정도로 짧거나 속도가 0일 경우 Timer를 이용하지 않고 바로 이동
		if (!nVelocity || nDuration < collie.Renderer.getInfo().fps) {
			this.set({
				x : nX,
				y : nY
			});
			
			
			if (fCallback) {
				fCallback(this);
			}
		} else {
			var htOption = {
				from : [nCurrentX, nCurrentY],
				to : [nX, nY],
				set : ["x", "y"]
			};
			
			if (fCallback) {
				htOption.onComplete = function () {
					fCallback(this);
				};
			}
			
			this._oTimerMove = collie.Timer.transition(this, nDuration, htOption);
			return this._oTimerMove;
		}
	},
	
	/**
	 * 상대 경로로 이동
	 * 
	 * @param {Number} nX 가고자 하는 곳의 x 좌표
	 * @param {Number} nY 가고자 하는 곳의 y 좌표
	 * @param {Number} nVelocity 초당 이동 거리(px), 속도가 0 이면 바로 이동한다.
	 * @param {Function} fCallback 이동이 끝난 후 실행될 콜백
	 * @return {collie.AnimationTransition} 이동에 사용되는 타이머를 반환
	 */
	moveBy : function (nX, nY, nVelocity, fCallback) {
		var nCurrentX = this._htOption.x;
		var nCurrentY = this._htOption.y;
		return this.move(nCurrentX + nX, nCurrentY + nY, nVelocity, fCallback);
	},
	
	/**
	 * 문자열로 클래스 정보 반환
	 * 
	 * @return {String}
	 */
	toString : function () {
		return "DisplayObject" + (this._htOption.name ? " " + this._htOption.name : "")+ " #" + this.getId() + (this.getImage() ? "(image:" + this.getImage().src + ")" : "");
	},
	
	/**
	 * 객체 복사
	 * 이벤트는 복사되지 않는다.
	 * @param {Boolean} bRecursive 자식까지 모두 복사하는지 여부
	 * @return {collie.DisplayObject}
	 * @example
	 * var box = new collie.DisplayObject({
	 * 	width: 100,
	 * 	height: 100,
	 * 	backgroundColor: "blue"
	 * }).addTo(layer);
	 * 
	 * var box2 = box.clone().addTo(layer);
	 */
	clone : function (bRecursive) {
		var oDisplayObject = 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(oDisplayObject);
			}
		}
		
		return oDisplayObject;
	}
}, collie.Component);

/**
 * 표시 객체 아이디를 할당한다. 1씩 늘어남
 * 
 * @static
 * @private
 */
collie.DisplayObject._idx = 0;

/**
 * 생성된 표시 객체를 담는다. Layer에 추가하지 않아도 표시 객체를 아이디로만 가져올 수 있다
 * 
 * @static
 * @private
 */
collie.DisplayObject.htFactory = {};
comments powered by Disqus