/**
* Layer를 등록해서 그리는 렌더링 파이프라인
* @namespace
* @TODO frame을 기록하는 클래스의 경우 stop되고 다시 시작되면 0부터 시작하므로
자기가 기록한 frame이 현재 frame보다 클 때 보정 처리를 반드시 해줘야 한다.
이는 나중에 frame이 int 풀카운트가 되었을 때 처리가 있을지도 모르므로 필수
*/
collie.Renderer = collie.Renderer || new (collie.Class(/** @lends collie.Renderer */{
/**
* 기본 렌더링 FPS
* @type {String}
*/
DEFAULT_FPS : "60fps",
/**
* 레티나 디스플레이 여부 auto 값일 경우 자동 판단, true/false 값은 수동
* auto 일 때 자동 판단 됨
* @type {String|Boolean}
*/
RETINA_DISPLAY : false,
/**
* 이 값을 true로 변경하면 가상 딜레이를 발생할 수 있다.
* 가상 딜레이 발생 상태에서는 requestAnimationFrame이 동작하지 않으며
* 타이머 등이 스킵될 때 어떻게 동작하는지 확인할 수 있다.
*
* @type {Boolean}
* @example
* collie.Renderer.DEBUG_USE_DELAY = true;
* collie.Renderer.DEBUG_MAX_DELAY = 200;
* collie.Renderer.start();
*/
DEBUG_USE_DELAY : false,
/**
* 가상 딜레이 최대값(랜덤하게 발생, ms)
* @type {Number}
*/
DEBUG_MAX_DELAY : 200,
/**
* 렌더링 모드 [auto|canvas|dom]
* @type {String}
*/
DEBUG_RENDERING_MODE : "auto",
/**
* 자동 일시정지를 해제한다
* @type {Boolean}
*/
USE_AUTO_PAUSE : true,
/**
* If The Renderer can't render the screen in this time, It should be paused.
* (ms)
* @type {Number}
*/
DELAY_LIMIT : 3 * 1000,
$init : function () {
this._sVisibilityChange = this._getNamePageVisibility();
this._bPlaying = false;
this._bPause = false;
this._nFPS = 0;
this._nDuration = 0; // ms
this._nCurrentFrame = 0;
this._nSkippedFrame = 0;
this._nBeforeFrameTime = null; // ms
this._nBeforeRenderingTime = 0; // ms
this._aLayerList = [];
this._fRender = this._render.bind(this);
this._fCallback = null;
this._htCallback = {};
this._elContainer = document.createElement("div");
this._elContainer.className = "_collie_container";
this._elContainer.style.position = "relative";
this._elContainer.style.overflow = "hidden";
this._elParent = null;
this._nDebugDelayedTime = 0;
this._oRenderingTimer = null;
this._bLoaded = false;
this._sRenderingMode = null;
this._bUseRetinaDisplay = null;
this._htEventStatus = {};
this._htPosition = {};
this._bIsPreventDefault = true;
this._htDeviceInfo = collie.util.getDeviceInfo();
this._fOnChangeVisibility = this._onChangeVisibility.bind(this);
this._fOnPageShow = this._onPageShow.bind(this);
this._fOnPageHide = this._onPageHide.bind(this);
this._fRefresh = this.refresh.bind(this);
},
/**
* 페이지를 진입할 때 렌더러 처리
* @private
*/
_onPageShow : function () {
if (!this.USE_AUTO_PAUSE) {
return;
}
if (!this.isPlaying() && this._bPause) {
this.resume();
}
},
/**
* 페이지를 이탈할 때 렌더러 처리
* @private
*/
_onPageHide : function () {
if (!this.USE_AUTO_PAUSE) {
return;
}
if (this.isPlaying()) {
this.pause();
}
},
/**
* @private
*/
_onChangeVisibility : function () {
if (!this.USE_AUTO_PAUSE) {
return;
}
var state = document.visibilityState || document.webkitVisibilityState || document.mozVisibilityState;
if (state === "hidden") {
this.pause();
} else if (state === "visible") {
this.resume();
}
},
/**
* 렌더링 엘리먼트의 위치를 갱신한다
* 만일 렌더링 엘리먼트의 위치가 load 후에 변경될 경우 refresh 메소드를 실행시켜줘야 한다
*/
refresh : function () {
if (this._elParent !== null) {
this._htPosition = collie.util.getPosition(this._elParent);
}
},
/**
* 렌더러 엘리먼트의 현재 위치를 반환
* 렌더러가 load되지 않았다면 false를 반환
*
* @private
* @return {Object|Boolean} htResult
* @return {Number} htResult.x 페이지 처음부터의 x좌표
* @return {Number} htResult.y 페이지 처음부터의 y좌표
* @return {Number} htResult.width 너비
* @return {Number} htResult.height 높이
*/
getPosition : function () {
return this._bLoaded ? this._htPosition : false;
},
/**
* 렌더러에 적용할 레이어를 추가 한다
*
* @param {collie.Layer} oLayer
*/
addLayer : function (oLayer) {
if (!oLayer || !("type" in oLayer) || oLayer.type !== "layer") {
throw new Error('oLayer is not Layer instnace');
}
// 이미 추가된 레이어라면 무시
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
if (this._aLayerList[i] === oLayer) {
return;
}
}
this._aLayerList.push(oLayer);
// 로드된 상태에서는 자동으로 붙기
if (this._bLoaded) {
oLayer.load(this._elContainer, this._aLayerList.length);
this.resetLayerEvent();
}
},
/**
* 렌더러에 적용한 레이어를 제거 한다
*
* @param {collie.Layer} oLayer
*/
removeLayer : function (oLayer) {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
if (this._aLayerList[i] === oLayer) {
this._aLayerList[i].unload(); // 로딩되어 있으면 해제 시킴
this._aLayerList.splice(i, 1);
return;
}
}
},
/**
* 등록된 모든 레이어를 제거 한다
*/
removeAllLayer : function () {
for (var i = this._aLayerList.length - 1; i >= 0; i--) {
this._aLayerList[i].unload();
}
this._aLayerList = [];
},
/**
* 등록된 레이어를 모두 반환
*
* @return {Array}
*/
getLayers : function () {
return this._aLayerList;
},
/**
* 이벤트를 모두 해제하고 다시 건다
* @private
*/
resetLayerEvent : function () {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
this._aLayerList[i].detachEvent();
}
// 레이어 역순으로 이벤트가 동작해야 하기 때문에 이벤트는 역순으로 건다
for (var i = this._aLayerList.length - 1; i >= 0; i--) {
this._aLayerList[i].attachEvent();
}
},
/**
* 렌더러의 컨테이너 엘리먼트를 반환
* @return {HTMLElement}
*/
getElement : function () {
return this._elContainer;
},
/**
* 렌더러에 적용된 시간을 반환
*
* @return {Number} ms
*/
getDuration : function () {
return this._nDuration;
},
/**
* 렌더러 정보를 반환
*
* @return {Object} htInfo
* @return {Number} htInfo.frame 현재 프레임 수
* @return {Number} htInfo.skippedFrame 지나간 누적 프레임 수
* @return {Number} htInfo.fps
* @return {Number} htInfo.duration 지연시간(ms)
* @return {Number} htInfo.renderingTime 이전에 발생했던 렌더링 시간(ms)
* @return {Number} htInfo.beforeFrameTime 이전에 렌더러가 실행됐던 시간(timestamp)
*/
getInfo : function () {
// 객체 재활용
this._htCallback.frame = this._nCurrentFrame;
this._htCallback.skippedFrame = this._nSkippedFrame;
this._htCallback.fps = this._nFPS;
this._htCallback.duration = this._nDuration;
this._htCallback.renderingTime = this._nBeforeRenderingTime;
this._htCallback.beforeFrameTime = this._nBeforeFrameTime;
return this._htCallback;
},
/**
* 렌더링 모드를 반환
* - 두개의 방식을 섞어 쓰는 것은 속도가 느려서 1가지 방식을 사용하는 것이 낫다
* @return {String} [dom|canvas]
*/
getRenderingMode : function () {
if (this._sRenderingMode === null) {
var htDeviceInfo = collie.util.getDeviceInfo();
this._sRenderingMode = this.DEBUG_RENDERING_MODE;
if (!this._sRenderingMode || this._sRenderingMode === "auto") {
// 안드로이드 2.2 미만, 캔버스를 지원하지 않거나 ios 5 미만인 경우
// 안드로이드 4 버전대에서 DOM 불안정성이 발견됨
// 안드로이드 4.2.2 galaxy S4 부터 canvas가 더 빨라짐
if (
(
htDeviceInfo.android && !htDeviceInfo.chrome && (
(htDeviceInfo.android < 4.2 && htDeviceInfo.android >= 3) ||
htDeviceInfo.android < 2.2
)
) ||
!htDeviceInfo.supportCanvas ||
(htDeviceInfo.ios && htDeviceInfo.ios < 5)
) {
this._sRenderingMode = "dom";
} else {
this._sRenderingMode = "canvas";
}
}
// 캔버스를 지원하지 않으면 무조건 DOM 모드로
if (!htDeviceInfo.supportCanvas) {
this._sRenderingMode = "dom";
}
}
return this._sRenderingMode;
},
/**
* 렌더링 모드를 변경 한다
*
* @param {String} sMode [auto|dom|canvas]
* @example collie를 사용하기 전에 사용해야 한다.
* collie.Renderer.setRenderingMode("dom");
* collie.ImageManager.add({
* ...
* }, function () {
* ...
* });
*/
setRenderingMode : function (sMode) {
this.DEBUG_RENDERING_MODE = sMode.toString().toLowerCase();
this._sRenderingMode = null;
},
/**
* 레티나 디스플레이를 사용하고 있는지 여부
* IE9 미만에서는 무조건 false를 반환
*
* @return {Boolean}
*/
isRetinaDisplay : function () {
if (this._bUseRetinaDisplay === null) {
// android 4.0 이상도 retina 지원 추가
this._bUseRetinaDisplay = this.RETINA_DISPLAY !== "auto" ? this.RETINA_DISPLAY : window.devicePixelRatio >= 2 && (!collie.util.getDeviceInfo().android || collie.util.getDeviceInfo().android >= 4);
var htDeviceInfo = collie.util.getDeviceInfo();
// background-size를 지원하지 않는 상태에서 고해상도 디스플레이 모드 사용할 수 없음
if (htDeviceInfo.ie && htDeviceInfo.ie < 9) {
this._bUseRetinaDisplay = false;
}
}
return this._bUseRetinaDisplay;
},
/**
* 레티나 디스플레이 방식을 변경 한다
* @param {Boolean|String} vMode [false|true|"auto"]
* @example collie를 사용하기 전에 사용해야 한다.
* collie.Renderer.setRetinaDisplay(true);
* collie.ImageManager.add({
* ...
* }, function () {
* ...
* });
*/
setRetinaDisplay : function (vMode) {
this.RETINA_DISPLAY = vMode;
this._bUseRetinaDisplay = null;
},
/**
* requestAnimationFrame 사용 여부 반환
*
* @private
* @param {Boolean} bCancelName true면 CancelAnimationFrame 이름을 반환
* @return {bool|String} 사용 가능하면 함수명을 반환
*/
_getNameAnimationFrame : function (bCancelName) {
if (typeof window.requestAnimationFrame !== "undefined") {
return bCancelName ? "cancelAnimationFrame" : "requestAnimationFrame";
} else if (typeof window.webkitRequestAnimationFrame !== "undefined") {
return bCancelName ? "webkitCancelAnimationFrame" : "webkitRequestAnimationFrame";
} else if (typeof window.msRequestAnimationFrame !== "undefined") {
return bCancelName ? "msCancelAnimationFrame" : "msRequestAnimationFrame";
} else if (typeof window.mozRequestAnimationFrame !== "undefined") {
return bCancelName ? "mozCancelAnimationFrame" : "mozRequestAnimationFrame";
} else if (typeof window.oRequestAnimationFrame !== "undefined") {
return bCancelName ? "oCancelAnimationFrame" : "oRequestAnimationFrame";
} else {
return false;
}
},
/**
* Page Visibility Event 이름을 반환
* @private
* @return {String|Boolean}
*/
_getNamePageVisibility : function () {
if ("hidden" in document) {
return "visibilitychange";
} else if ("webkitHidden" in document) {
return "webkitvisibilitychange";
} else if ("mozHidden" in document) {
return "mozvisibilitychange";
} else {
return false;
}
},
/**
* 표현할 레이어를 elParent에 붙인다 시작전에 반드시 해야함
*
* @param {HTMLElement|String} elParent
*/
load : function (elParent) {
this.unload();
this._bLoaded = true;
// string이면 엘리먼트를 구해 줌
if (typeof elParent === "string") {
elParent = document.getElementById(elParent);
}
this._elParent = elParent;
this._elParent.appendChild(this._elContainer);
this.refresh();
if (this._aLayerList.length) {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
this._aLayerList[i].load(this._elContainer, i);
}
// 레이어 역순으로 이벤트가 동작해야 하기 때문에 이벤트는 역순으로 건다
for (var i = this._aLayerList.length - 1; i >= 0; i--) {
this._aLayerList[i].attachEvent();
}
}
// PageVisibility API를 사용할 수 있다면 사용
if (this._sVisibilityChange) {
collie.util.addEventListener(document, this._sVisibilityChange, this._fOnChangeVisibility);
// 모바일이라면 pageshow/pagehide를 사용
// In-App Browser일 때 pageshow/pagehide가 정상적으로 호출 안되는 문제점이 있음
} else if (!this._htDeviceInfo.desktop) {
collie.util.addEventListener(window, "pageshow", this._fOnPageShow);
collie.util.addEventListener(window, "pagehide", this._fOnPageHide);
}
// 렌더러 엘리먼트의 위치를 저장해 놓는다
collie.util.addEventListener(window, "resize", this._fRefresh);
},
/**
* 부모 엘리먼트에 붙인 레이어를 지움
*/
unload : function () {
if (this._bLoaded) {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
this._aLayerList[i].unload();
}
this._elParent.removeChild(this._elContainer);
this._elParent = null;
this._bLoaded = false;
if (this._sVisibilityChange) {
collie.util.removeEventListener(document, this._sVisibilityChange, this._fOnChangeVisibility);
} else if (!this._htDeviceInfo.desktop) {
collie.util.removeEventListener(window, "pageshow", this._fOnPageShow);
collie.util.removeEventListener(window, "pagehide", this._fOnPageHide);
}
collie.util.removeEventListener(window, "resize", this._fRefresh);
}
},
/**
* 렌더링 시작
* - callback 안에서 false를 반환하면 rendering을 멈춘다
*
* @param {Number|String} vDuration 렌더러의 시간 간격(ms), fps를 붙이면 fps 단위로 입력된다.
* @param {Function} fCallback 프레임마다 실행할 함수, 없어도 되고 process 이벤트를 받아서 처리해도 된다.
* @param {Number} fCallback.frame 현재 프레임
* @param {Number} fCallback.skippedFrame 시간이 밀려서 지나간 프레임 수
* @param {Number} fCallback.fps FPS
* @param {Number} fCallback.duration 지연 시간 (ms)
* @example
* fps를 붙이면 FPS단위로 입력할 수 있다.
* ```
* collie.Renderer.start("30fps");
* collie.Renderer.start(1000 / 30);
* ```
*/
start : function (vDuration, fCallback) {
if (!this._bPlaying) {
// this.stop();
vDuration = vDuration || this.DEFAULT_FPS;
this._nDuration = (/fps$/i.test(vDuration)) ? 1000 / parseInt(vDuration, 10) : Math.max(16, vDuration);
this._fCallback = fCallback || null;
this._bPlaying = true;
// FPS가 60일 때만 requestAnimationFrame을 사용한다
if (this._nDuration < 17) {
this._sRequestAnimationFrameName = this._getNameAnimationFrame();
this._sCancelAnimationFrameName = this._getNameAnimationFrame(true);
} else {
this._sRequestAnimationFrameName = false;
this._sCancelAnimationFrameName = false;
}
/**
* 렌더링 시작
* @name collie.Renderer#start
* @event
* @param {Object} oEvent
*/
this.fireEvent("start");
this._trigger(0);
}
},
_trigger : function (nDelay) {
if (!this._sVisibilityChange && this.USE_AUTO_PAUSE && window.screenTop < -30000) {
this.pause();
}
if (typeof nDelay === "undefined") {
nDelay = 0;
} else {
nDelay = parseInt(nDelay, 10);
}
// 가상 딜레이를 적용하려면 requestAnimationFrame을 제거
if (this._sRequestAnimationFrameName !== false && !this.DEBUG_USE_DELAY && this.USE_AUTO_PAUSE) {
this._oRenderingTimer = window[this._sRequestAnimationFrameName](this._fRender);
} else {
this._oRenderingTimer = setTimeout(this._fRender, nDelay);
}
},
/**
* 실제 화면을 렌더링
*
* @private
* @param {Number} nSkippedFrame collie.Renderer#draw 에서 넘어온 인자
* @param {Boolean} 실행중 여부와 관계 없이 그림
*/
_render : function (nSkippedFrame, bForcePlay) {
// stop 시점이 비동기라서 시점이 안맞을 수도 있음. 렌더링이 바로 중단되야 함
if (!this._bPlaying && !bForcePlay) {
return;
}
var nTime = this._getDate();
var nRealDuration = 0;
var nFrameStep = 1; // 진행할 프레임 단계
// 진행된 프레임이면 시간 계산
if (this._nBeforeFrameTime !== null) {
nRealDuration = nTime - this._nBeforeFrameTime; // 실제 걸린 시간
nFrameStep = nSkippedFrame || Math.max(1, Math.round(nRealDuration / this._nDuration)); // 60fps 미만으로는 버린다
if (nRealDuration > this.DELAY_LIMIT) {
this.pause();
return;
}
// requestAnimationFrame 인자가 들어옴
if (this._sRequestAnimationFrameName !== false && this.USE_AUTO_PAUSE) {
nSkippedFrame = 0;
nFrameStep = 1;
}
this._nSkippedFrame += Math.max(0, nFrameStep - 1);
this._nFPS = Math.round(1000 / (nTime - this._nBeforeFrameTime));
}
this._nCurrentFrame += nFrameStep;
var htInfo = this.getInfo();
// callback이 없거나 callback 실행 결과가 false가 아니거나 process 이벤트 stop이 발생 안한 경우에만 진행
/**
* 렌더링 진행
* @name collie.Renderer#process
* @event
* @param {Object} oEvent
* @param {Function} oEvent.stop stop 하면 렌더링이 멈춘다
*/
if ((this._fCallback === null || this._fCallback(htInfo) !== false) && this.fireEvent("process", htInfo) !== false) {
collie.Timer.run(this._nCurrentFrame, nRealDuration);
this._update(nRealDuration);
var nDebugDelayedTime = 0;
// 가상 딜레이 적용
if (this.DEBUG_USE_DELAY) {
nDebugDelayedTime = Math.round(Math.random() * this.DEBUG_MAX_DELAY);
this._nDebugDelayedTime += nDebugDelayedTime;
}
this._nBeforeRenderingTime = this._getDate() - nTime;
this._nBeforeFrameTime = nTime;
if (this._bPlaying) {
this._trigger(Math.max(0, this._nDuration - this._nBeforeRenderingTime + nDebugDelayedTime * 2));
}
} else {
this.stop();
}
},
/**
* 원하는 프레임으로 스킵해서 그린다
*
* @param {Number} nSkippedFrame 값이 없으면 스킵 없이, 값이 있으면 그 값만큼 프레임을 스킵해서 그린다
*/
draw : function (nSkippedFrame) {
this._fRender(nSkippedFrame, true);
},
/**
* 현재 시간을 가져 온다
* @private
* @return {Number} timestamp
*/
_getDate : function () {
return (+new Date()) + (this.DEBUG_USE_DELAY ? this._nDebugDelayedTime : 0);
},
/**
* 렌더링을 멈춘다
*/
stop : function () {
if (this._bPlaying) {
this._bPlaying = false;
this._resetTimer();
/**
* 렌더링 멈춤
* @name collie.Renderer#stop
* @event
* @param {Object} oEvent
*/
this.fireEvent("stop", this.getInfo());
this._sRenderingMode = null;
this._bUseRetinaDisplay = null;
this._fCallback = null;
this._nCurrentFrame = 0;
this._nBeforeRenderingTime = 0;
this._nSkippedFrame = 0;
this._nBeforeFrameTime = null;
}
},
_resetTimer : function () {
if (this._oRenderingTimer !== null) {
if (this._sCancelAnimationFrameName !== false) {
window[this._sCancelAnimationFrameName](this._oRenderingTimer);
} else {
clearTimeout(this._oRenderingTimer);
}
//TODO debug
window.tempTimer = window.tempTimer || [];
window.tempTimer.push(this._oRenderingTimer);
this._oRenderingTimer = null;
}
},
/**
* 잠시 멈춘다
*/
pause : function () {
if (this._bPlaying) {
this._bPlaying = false;
this._bPause = true;
/**
* 렌더러가 일시 정지 때 발생. getInfo 값이 이벤트 인자로 넘어간다
* @name collie.Renderer#pause
* @event
* @see collie.Renderer.getInfo
*/
this.fireEvent("pause", this.getInfo());
// 진행되고 있는 타이머를 해제
this._resetTimer();
}
},
/**
* 잠시 멈춘것을 다시 실행 한다
*/
resume : function () {
if (this._bPause) {
this._nBeforeFrameTime = this._getDate();
this._nBeforeRenderingTime = 0;
this._bPlaying = true;
this._bPause = false;
/**
* 렌더러가 일시 정지에서 해제될 때 발생. getInfo 값이 이벤트 인자로 넘어간다
* @name collie.Renderer#resume
* @event
* @see collie.Renderer.getInfo
*/
this.fireEvent("resume", this.getInfo());
this._trigger(0);
}
},
/**
* 현재 실행 중인지 여부를 반환
*
* @return {Boolean}
*/
isPlaying : function () {
return this._bPlaying;
},
/**
* 레이어 업데이트, 주로 다시 그리거나 동작 등을 업데이트 한다
*
* @param {Number} nFrameDuration 실제 진행된 시간
* @private
*/
_update : function (nFrameDuration) {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
this._aLayerList[i].update(nFrameDuration);
}
},
/**
* 이벤트의 레이어간 전달을 막기 위한 이벤트 상태를 설정 한다
*
* @private
* @param {String} sEventType 이벤트 타입
* @param {Boolean} bFiredOnTarget 이벤트가 대상에 발생했는지 여부
*/
setEventStatus : function (sEventType, bFiredOnTarget) {
this._htEventStatus = {
type : sEventType,
firedOnTarget : bFiredOnTarget
};
},
/**
* 객체 이벤트를 멈춰야 하는지 여부
* @private
* @param {String} sEventType 이벤트 타입
* @return {Boolean} 이벤트를 멈춰야 하는지 여부
*/
isStopEvent : function (sEventType) {
// click 이벤트는 임의로 발생시키기 때문에 mouseup 으로 간주
if (sEventType === "click") {
sEventType = "mouseup";
}
return sEventType === this._htEventStatus.type && this._htEventStatus.firedOnTarget;
},
/**
* 이벤트의 레이어간 전달을 막기 위한 이벤트 상태를 가져 온다
*
* @private
* @return {Object} htEventStatus
* @return {String} htEventStatus.type 이벤트 타입
* @return {Boolean} htEventStatus.firedOnTarget 이벤트가 대상에 발생했는지 여부
*/
getEventStatus : function () {
return this._htEventStatus;
},
/**
* 레이어 위에서 기본 이벤트(mousemove, mousedown) 동작을 막을지 여부를 설정 한다.
*
* @param {Boolean} bPreventDefault true면 기본 동작을 막는다.
*/
setPreventDefault : function (bPreventDefault) {
this._bIsPreventDefault = !!bPreventDefault;
},
/**
* 기본 동작을 막는지 여부를 반환
*
* @return {Boolean} true일 때 막는다, 기본값이 true
*/
isPreventDefault : function () {
return this._bIsPreventDefault;
},
/**
* 렌더러에 등록된 모든 레이어의 크기를 변경 한다
*
* @param {Number} nWidth
* @param {Number} nHeight
* @param {Boolean} bExpand 확장할지 크기만 변경할지 여부
*/
resize : function (nWidth, nHeight, bExpand) {
for (var i = 0, len = this._aLayerList.length; i < len; i++) {
this._aLayerList[i].resize(nWidth, nHeight, bExpand);
}
}
}, collie.Component))();