ycc.js
Version:
Mini and powerful canvas engine for creating App or Game.
2,591 lines (2,176 loc) • 223 kB
JavaScript
/**
* @file Ycc.class.js
* @author xiaohei
* @date 2017/9/30
* @description Ycc.class文件
*
*/
/**
* 应用启动入口类,每个实例都与一个canvas绑定。
* 该canvas元素会被添加至HTML结构中,作为应用的显示舞台。
* @param config {Object} 整个ycc的配置项
* @param config.debugDrawContainer {Boolean} 是否显示所有UI的容纳区域
* 若开启,所有UI都会创建一个属于自己的离屏canvas,大小与舞台一致
* @constructor
*/
var Ycc = function Ycc(config){
/**
* 绘图环境
* @type {CanvasRenderingContext2D}
*/
this.ctx = null;
/**
* 与ycc绑定的canvas元素
* @type {null}
*/
this.canvasDom = null;
/**
* Layer对象数组。包含所有的图层
* @type {Array}
*/
this.layerList = [];
/**
* 实例的快照管理模块
* @type {Ycc.PhotoManager}
*/
this.photoManager = null;
/**
* ycc的图层管理器
* @type {null}
*/
this.layerManager = null;
/**
* 系统心跳管理器
*/
this.ticker = null;
/**
* 调试模块
* @type {null}
*/
this.debugger = null;
/**
* 资源加载器
* @type {Ycc.Loader}
*/
this.loader = new Ycc.Loader();
/**
* 异步请求的封装
* @type {Ycc.Ajax}
*/
this.ajax = new Ycc.Ajax();
/**
* 基础绘图UI。这些绘图操作会直接作用于舞台。
* @type {Ycc.UI}
*/
this.baseUI = null;
/**
* 系统的手势模块
* @type {null}
*/
this.gesture = null;
/**
* 整个ycc的配置项
* @type {{debugDrawContainer:boolean}}
*/
this.config = Ycc.utils.extend({
debugDrawContainer:false
},config||{});
/**
* 是否移动端
* @type {boolean}
*/
this.isMobile = Ycc.utils.isMobile();
this.stageW = 0;
this.stageH = 0;
/**
* dpi
* @type {number}
*/
this.dpi = this.getSystemInfo().devicePixelRatio;
};
/**
* 获取舞台的宽
*/
Ycc.prototype.getStageWidth = function () {
return this.canvasDom.width;
};
/**
* 获取舞台的高
*/
Ycc.prototype.getStageHeight = function () {
return this.canvasDom.height;
};
/**
* 绑定canvas元素,一个canvas绑定一个ycc实例
* @param canvas
* @return {Ycc}
*/
Ycc.prototype.bindCanvas = function (canvas) {
this.canvasDom = canvas;
this.ctx = canvas.getContext('2d');
this.layerList = [];
this.photoManager = new Ycc.PhotoManager(this);
this.layerManager = new Ycc.LayerManager(this);
this.ticker = new Ycc.Ticker(this);
this.debugger = new Ycc.Debugger(this);
this.baseUI = new Ycc.UI(this);
this.init();
return this;
};
/**
* 类初始化
*/
Ycc.prototype.init = function () {
//初始化手势库
this._initStageGestureEvent();
// 心跳模块初始化 ticker默认启动,10帧每刷新
this.ticker.start(6);
};
/**
*
* 需求实现:初始化舞台的手势事件监听器
* 1、事件传递给所有图层
* 2、事件传递给最上层UI
* 3、pc端和移动端统一,pc端视为一个触摸点,而移动端可以存在多个触摸点
* 4、dragging、dragend事件,的最上层UI为dragstart时所指的UI
* 5、所有鼠标事件均由舞台转发,转发的坐标均为绝对坐标。
* `layer`和`ui`可以调用各自的`transformToLocal`方法,将绝对坐标转换为自己的相对坐标。
*
* @private
*/
Ycc.prototype._initStageGestureEvent = function () {
var self = this;
// 鼠标/触摸点开始拖拽时,所指向的UI对象,只用于单个触摸点的情况
//var dragstartUI = null;
// 鼠标/触摸点开始拖拽时,所指向的UI对象map,用于多个触摸点的情况
var dragstartUIMap = {};
// 拖拽时的上一个坐标,用于判断是否下发dragging事件
var draggingLastXY = '';
var gesture = new Ycc.Gesture({target:this.ctx.canvas});
this.gesture = gesture;
gesture.addListener('tap',gestureListener);
gesture.addListener('longtap',gestureListener);
gesture.addListener('doubletap',gestureListener);
gesture.addListener('swipe',gestureListener);
gesture.addListener('swipeleft',gestureListener);
gesture.addListener('swiperight',gestureListener);
gesture.addListener('swipeup',gestureListener);
gesture.addListener('swipedown',gestureListener);
gesture.addListener('dragstart',dragstartListener);
gesture.addListener('dragging',draggingListener);
gesture.addListener('dragend',dragendListener);
// PC端专属事件
gesture.addListener('mousemove',gestureListener);
function dragstartListener(e) {
// 在canvas中的绝对位置
var x = parseInt(e.clientX - self.ctx.canvas.getBoundingClientRect().left),
y = parseInt(e.clientY - self.ctx.canvas.getBoundingClientRect().top);
// 重置位置
draggingLastXY = '';
var dragstartUI = self.getUIFromPointer(new Ycc.Math.Dot(x,y));
if(dragstartUI){
dragstartUIMap[e.identifier]=dragstartUI;
dragstartUI.triggerUIEventBubbleUp(e.type,x,y);
}
// dragstartUI&&dragstartUI.belongTo.enableEventManager&&triggerUIEvent(e.type,x,y,dragstartUI);
triggerLayerEvent(e.type,x,y);
}
function draggingListener(e) {
// 在canvas中的绝对位置
var x = parseInt(e.clientX - self.ctx.canvas.getBoundingClientRect().left),
y = parseInt(e.clientY - self.ctx.canvas.getBoundingClientRect().top);
// 位置在一个像素内,不下发dragging事件
if(draggingLastXY===x+'_'+y) return;
draggingLastXY = x+'_'+y;
var dragstartUI = dragstartUIMap[e.identifier];
if(dragstartUI){
dragstartUIMap[e.identifier]=dragstartUI;
dragstartUI.triggerUIEventBubbleUp(e.type,x,y);
}
triggerLayerEvent(e.type,x,y);
}
function dragendListener(e) {
// 在canvas中的绝对位置
var x = parseInt(e.clientX - self.ctx.canvas.getBoundingClientRect().left),
y = parseInt(e.clientY - self.ctx.canvas.getBoundingClientRect().top);
var dragstartUI = dragstartUIMap[e.identifier];
if(dragstartUI){
dragstartUIMap[e.identifier]=dragstartUI;
dragstartUI.triggerUIEventBubbleUp(e.type,x,y);
dragstartUI = null;
dragstartUIMap[e.identifier]=null;
}
triggerLayerEvent(e.type,x,y);
}
// 通用监听
function gestureListener(e) {
// console.log('通用监听',e);
// 在canvas中的绝对位置
var x = parseInt(e.clientX - self.ctx.canvas.getBoundingClientRect().left),
y = parseInt(e.clientY - self.ctx.canvas.getBoundingClientRect().top);
var ui = self.getUIFromPointer(new Ycc.Math.Dot(x,y));
// triggerLayerEvent(e.type,x,y);
// ui&&ui.belongTo.enableEventManager&&triggerUIEvent(e.type,x,y,ui);
if(ui){
ui.triggerUIEventBubbleUp(e.type,x,y,e);
}
triggerLayerEvent(e.type,x,y);
}
function triggerLayerEvent(type,x,y){
for(var i=self.layerList.length-1;i>=0;i--){
var layer = self.layerList[i];
if(!layer.enableEventManager) continue;
layer.enableEventManager&&layer.triggerListener(type,new Ycc.Event({
type:type,
x:x,
y:y
}));
}
}
function triggerUIEvent(type,x,y,ui){
ui.triggerListener(type,new Ycc.Event({
x:x,
y:y,
type:type,
target:ui
}));
}
/**
* 冒泡触发UI的事件
* @param type
* @param x
* @param y
* @param ui
* @return {null}
*/
// function triggerUIEventBubbleUp(type,x,y,ui) {
// if(ui && ui.belongTo && ui.belongTo.enableEventManager){
// // 触发ui的事件
// ui.triggerListener(type,new Ycc.Event({x:x,y:y,type:type,target:ui}));
//
// // 如果ui阻止了事件冒泡,则不触发其父级的事件
// if(ui.stopEventBubbleUp) return;
//
// // 触发父级ui的事件
// ui.getParentList().reverse().forEach(function (fa) {
// fa.triggerListener(type,new Ycc.Event({x:x,y:y,type:type,target:fa}));
// });
// }
// }
};
/**
* 清除
*/
Ycc.prototype.clearStage = function () {
this.ctx.clearRect(0,0,this.getStageWidth(),this.getStageHeight());
};
/**
* 根据id查找图层
* @param id 图层id
* @return {Ycc.Layer}
*/
Ycc.prototype.findLayerById = function (id) {
for(var i =0;i<this.layerList.length;i++){
var layer = this.layerList[i];
if(layer.id===id)
return layer;
}
return null;
};
/**
* 根据id查找UI
* @param id UI的id
* @return {Ycc.UI}
*/
Ycc.prototype.findUiById = function (id) {
for(var i =0;i<this.layerList.length;i++){
var layer = this.layerList[i];
for(var j=0;j<layer.uiList.length;j++){
var ui = layer.uiList[j];
if(ui.id===id)
return ui;
}
}
return null;
};
/**
* 获取舞台中某个点所对应的最上层UI。
* @param dot {Ycc.Math.Dot} 点坐标,为舞台的绝对坐标
* @param uiIsShow {Boolean} 是否只获取显示在舞台上的UI,默认为true
* @return {UI}
*/
Ycc.prototype.getUIFromPointer = function (dot,uiIsShow) {
var self = this;
uiIsShow = Ycc.utils.isBoolean(uiIsShow)?uiIsShow:true;
// 从最末一个图层开始寻找
for(var j=self.layerList.length-1;j>=0;j--){
var layer = self.layerList[j];
// 幽灵图层,直接跳过
if(layer.ghost) continue;
if(uiIsShow&&!layer.show) continue;
var ui = layer.getUIFromPointer(dot,uiIsShow);
if(ui)
return ui;
}
return null;
};
/**
* 获取舞台中某个点所对应的最上层UI,不遍历不可见图层
* 默认取可见且不是幽灵的UI
* @param dot {Ycc.Math.Dot} 点坐标,为舞台的绝对坐标
* @param options {object} 点坐标,为舞台的绝对坐标
* @param options.uiIsShow {boolean} UI是否可见
* @param options.uiIsGhost {boolean} UI是否未幽灵
* @return {Ycc.UI[]} 返回找到的UI列表,图层越靠后图层内的UI越靠前
*/
Ycc.prototype.getUIListFromPointer = function (dot,options) {
var self = this;
options = options || {
uiIsShow:true,
uiIsGhost:false
};
var uiList = [];
// 从最末一个图层开始寻找
for(var j=self.layerList.length-1;j>=0;j--){
var layer = self.layerList[j];
//
if(!layer.show) continue;
var list = layer.getUIListFromPointer(dot);
uiList = uiList.concat(list);
}
uiList = uiList.filter(function(item){return (item.show===options.uiIsShow)&&(item.ghost===options.uiIsGhost);});
return uiList;
};
/**
* 创建canvas,只针对H5端。微信小游戏的canvas为全局变量,直接使用即可
* @example
* var ycc = new Ycc();
* var stage = canvas || ycc.createCanvas();
* ycc.bindCanvas(stage);
*
* @param options
* @param options.width
* @param options.height
* @param options.dpiAdaptation 是否根据dpi适配canvas大小
* @return {*} 已创建的canvas元素
*/
Ycc.prototype.createCanvas = function (options) {
options = options||{};
var option = Ycc.utils.mergeObject({
width:window.innerWidth,
height:window.innerHeight,
dpiAdaptation:false
},options);
var canvas = document.createElement("canvas");
var dpi = this.getSystemInfo().devicePixelRatio;
if(option.dpiAdaptation){
canvas.width = option.width*dpi;
canvas.height = option.height*dpi;
canvas.style.width=option.width+'px';
}else{
canvas.width = option.width;
canvas.height = option.height;
}
// 去除5px inline-block偏差
canvas.style.display='block';
return canvas;
};
/**
* 获取系统信息
* @return {{model: string, pixelRatio: number, windowWidth: number, windowHeight: number, system: string, language: string, version: string, screenWidth: number, screenHeight: number, SDKVersion: string, brand: string, fontSizeSetting: number, benchmarkLevel: number, batteryLevel: number, statusBarHeight: number, safeArea: {right: number, bottom: number, left: number, top: number, width: number, height: number}, platform: string, devicePixelRatio: number}}
*/
Ycc.prototype.getSystemInfo = function () {
return {
"model":"iPhone 5",
"pixelRatio":window.devicePixelRatio||1,
"windowWidth":window.innerWidth,
"windowHeight":window.innerHeight,
"screenWidth":320,
"screenHeight":568,
"devicePixelRatio":window.devicePixelRatio||1
};
};
/**
* 创建一个离屏的绘图空间,默认大小与舞台等同
* @param options
* @param options.width
* @param options.height
*/
Ycc.prototype.createCacheCtx = function (options) {
options = options || {
width:this.getStageWidth(),
height:this.getStageHeight()
};
var ctxCache = this.createCanvas({
width:options.width,
height:options.height
}).getContext('2d');
// debug
// console.log('create cache ctx',options);
document.body.appendChild(ctxCache.canvas);
return ctxCache;
};;/**
* @file Ycc.utils.js
* @author xiaohei
* @desc
* 整个程序公用的基础工具模块
*
* @requires Ycc.init
*/
(function(Ycc){
Ycc.utils = {};
/**
* 合并两个对象。只会保留targetObj中存在的字段。
* @param targetObj 目标对象
* @param obj2 待合入的对象
* @param isDeepClone 是否进行深拷贝
* @return {{}} targetObj对象
*/
Ycc.utils.extend = function(targetObj, obj2,isDeepClone) {
if(isDeepClone)
obj2 = Ycc.utils.deepClone(obj2);
for (var i in targetObj) {
if(!targetObj.hasOwnProperty(i)) continue;
if (obj2 && typeof obj2[i] !=="undefined") {
targetObj[i] = obj2[i];
}
}
return targetObj;
};
/**
* 合并对象
* 将src所有的字段全部合并至target,若存在公有字段,则src会覆盖target对象的字段。
* 这个操作是浅拷贝。prototype内的属性不会被覆盖。
* @param target {object} 待覆盖的目标对象
* @param src {object} 源对象
* @return 返回target对象
*/
Ycc.utils.mergeObject = function(target,src){
src = src || {};
for(var key in src){
if(!src.hasOwnProperty(key)) continue;
if(typeof src[key]!=="undefined"){
target[key] = src[key];
}
}
return target;
};
/**
* 判断字符串
* @param str
* @return {boolean}
*/
Ycc.utils.isString = function(str) {
return typeof(str) === "string";
};
/**
* 判断数字
* @param str
* @return {boolean}
*/
Ycc.utils.isNum = function(str) {
return typeof(str) === "number";
};
/**
* 判断boolean
* @param str
* @return {boolean}
*/
Ycc.utils.isBoolean = function(str) {
return typeof(str) === "boolean";
};
/**
* 判断对象
* @param str
* @return {boolean}
*/
Ycc.utils.isObj = function(str) {
return typeof(str) === "object";
};
/**
* 判断函数
* @param str
* @return {boolean}
*/
Ycc.utils.isFn = function(str) {
return typeof(str) === "function";
};
/**
* 判断数组
* @param str
* @return {boolean}
*/
Ycc.utils.isArray = function(str) {
return Object.prototype.toString.call(str) === '[object Array]';
};
/**
* 检测是否是移动端
* @return {boolean}
*/
Ycc.utils.isMobile = function () {
var userAgentInfo = navigator.userAgent;
var Agents = ["Android", "iPhone",
"SymbianOS", "Windows Phone",
"iPad", "iPod"];
var flag = false;
for (var v = 0; v < Agents.length; v++) {
if (userAgentInfo.indexOf(Agents[v]) > 0) {
flag = true;
break;
}
}
return flag;
};
/**
* 深拷贝某个对象或者数组
* @param arrOrObj
* @return {*}
*/
Ycc.utils.deepClone = function(arrOrObj){
return (Ycc.utils.isArray(arrOrObj))? deepCopy(arrOrObj):deepExtend(arrOrObj);
function deepExtend(obj){
var tempObj = {};
for(var i in obj){
if(!obj.hasOwnProperty(i)) continue;
tempObj[i] = obj[i];
if(Ycc.utils.isArray(obj[i])){
tempObj[i] = deepCopy(obj[i]);
}else if(Ycc.utils.isObj(obj[i])){
tempObj[i] = deepExtend(obj[i]);
}else{
tempObj[i] = obj[i];
}
}
return tempObj;
}
function deepCopy(arr){
var newArr = [];
var v = null;
for(var i=0;i<arr.length;i++){
v = arr[i];
if(Ycc.utils.isArray(v))
newArr.push(deepCopy(v));
else if(Ycc.utils.isObj(v))
newArr.push(deepExtend(v));
else{
newArr.push(v);
}
}
return newArr;
}
};
/**
* 迷你模板,替换__field__,其中的`field`为`renderObj`中的字段
* 返回替换后的模板文本
* @param tpl 模板字符串
* @param renderObj 渲染的对象
* @return {string}
*/
Ycc.utils.renderTpl=function (tpl,renderObj) {
return tpl.replace(/__.+?__/g,function (txt) {
console.log('匹配到的文本-->',txt);
var key = txt.slice(2).slice(0,-2).trim();
if(renderObj[key]!==undefined)
return renderObj[key];
else
return txt;
});
};
/**
* 释放obj内存 只清空字段
* @param obj
*/
Ycc.utils.releaseObject = function (obj) {
for(var key in obj){
if(!obj.hasOwnProperty(key)) continue;
delete obj[key];
}
};
/**
* 释放arr内存 只清空元素
* @param arr
*/
Ycc.utils.releaseArray = function (arr) {
arr.length = 0;
};
/**
* 检测是否微信环境
* @return {boolean}
*/
Ycc.utils.isWx = function () {
return "undefined"!== typeof wx;
}
})(Ycc);
;/**
* @file Ycc.Math.js
* @author xiaohei
* @date 2017/11/2
* @description Ycc.Math文件
*/
(function (Ycc) {
var extend = Ycc.utils.extend;
/**
* 数学表达式模块
* @constructor
*/
Ycc.Math = function () {};
/**
* 点
* @param x {number} x坐标
* @param y {number} y坐标
* @constructor
*//**
* 点
* @param [dot] {object}
* @param dot.x {number} x坐标
* @param dot.y {number} y坐标
* @constructor
*/
Ycc.Math.Dot = function (dot) {
/**
* x坐标
* @type {number}
*/
this.x = 0;
/**
* y坐标
* @type {number}
*/
this.y = 0;
var len = arguments.length;
if(len===1){
this.x = dot.x;
this.y = dot.y;
}else if(len===2){
this.x = arguments[0];
this.y = arguments[1];
}
};
/**
* 点是否在某个区域内
* @param rect {Ycc.Math.Rect} 区域
*/
Ycc.Math.Dot.prototype.isInRect = function (rect) {
return this.x>=rect.x&&this.x<=rect.x+rect.width && this.y>=rect.y && this.y<=rect.y+rect.height;
};
/**
* 判读两点位置是否相同
* @param dot
* @return {boolean}
*/
Ycc.Math.Dot.prototype.isEqual = function (dot) {
return this.x===dot.x && this.y===dot.y;
};
/**
* 点的加法/点的偏移量
* @param dot {Ycc.Math.Dot} 加的点
* @return {Ycc.Math.Dot} 返回一个新的点
*/
Ycc.Math.Dot.prototype.plus = function (dot) {
return new Ycc.Math.Dot(this.x+dot.x,this.y+dot.y);
};
/**
* 将当前点绕另外一个点旋转一定度数
* @param rotation 旋转角度
* @param anchorDot 锚点坐标
* @return 旋转后的点
*/
Ycc.Math.Dot.prototype.rotate = function (rotation,anchorDot) {
anchorDot=anchorDot||new Ycc.Math.Dot(0,0);
var dotX = this.x,dotY=this.y,anchorX=anchorDot.x,anchorY=anchorDot.y;
var dx = (dotX - anchorX)*Math.cos(rotation/180*Math.PI) - (dotY - anchorY)*Math.sin(rotation/180*Math.PI)+anchorX;
var dy = (dotY - anchorY)*Math.cos(rotation/180*Math.PI) + (dotX - anchorX)*Math.sin(rotation/180*Math.PI)+anchorY;
return new Ycc.Math.Dot(dx,dy);
};
/**
* 判断三点是否共线
* @param dot1
* @param dot2
* @param dot3
*/
Ycc.Math.Dot.threeDotIsOnLine = function (dot1,dot2,dot3) {
// 存在位置相同点肯定共线
if(dot1.isEqual(dot2) || dot1.isEqual(dot3) || dot2.isEqual(dot3))
return true;
// 三个点x一样
if(dot1.x===dot2.x&&dot2.x===dot3.x)
return true;
var k1 = Math.abs(dot1.y-dot2.y)/Math.abs(dot1.x-dot2.x);
var k2 = Math.abs(dot1.y-dot3.y)/Math.abs(dot1.x-dot3.x);
return k1===k2;
};
/**
* 区域
* @param startDot {Dot}
* @param width
* @param height
* @constructor
*//**
* 区域
* @param x
* @param y
* @param width
* @param height
* @constructor
*//**
* 区域
* @param rect
* @param rect.x
* @param rect.y
* @param rect.width
* @param rect.height
* @constructor
*/
Ycc.Math.Rect = function (rect) {
/**
* 构造器的引用
* @type {function}
*/
this.yccClass = Ycc.Math.Rect;
/**
* 左上角x坐标
* @type {number}
*/
this.x = 0;
/**
* 左上角y坐标
* @type {number}
*/
this.y = 0;
/**
* 区域宽
* @type {number}
*/
this.width = 0;
/**
* 区域高
* @type {number}
*/
this.height = 0;
var len = arguments.length;
if(len===1){
this.x = rect.x;
this.y = rect.y;
this.width = rect.width;
this.height = rect.height;
}else if(len===3){
this.x = arguments[0].x;
this.y = arguments[0].y;
this.width = arguments[1];
this.height = arguments[2];
}else if(len === 4){
this.x = arguments[0];
this.y = arguments[1];
this.width = arguments[2];
this.height = arguments[3];
}
this.toPositive();
};
/**
* 将矩形的长和宽转换为正数
*/
Ycc.Math.Rect.prototype.toPositive = function () {
var x0 = this.x,
y0 = this.y,
x1 = this.x + this.width,
y1 = this.y + this.height;
this.x = x0<x1?x0:x1;
this.y = y0<y1?y0:y1;
this.width = Math.abs(this.width);
this.height = Math.abs(this.height);
};
/**
* 获取区域的顶点列表
* @return {Ycc.Math.Dot[]}
*/
Ycc.Math.Rect.prototype.getVertices = function () {
return [
new Ycc.Math.Dot(this.x,this.y),
new Ycc.Math.Dot(this.x+this.width,this.y),
new Ycc.Math.Dot(this.x+this.width,this.y+this.height),
new Ycc.Math.Dot(this.x,this.y+this.height),
new Ycc.Math.Dot(this.x,this.y)
];
};
/**
* 根据顶点更新数值
* @param vertices
* @return {*}
*/
Ycc.Math.Rect.prototype.updateByVertices = function (vertices) {
if(!Ycc.utils.isArray(vertices))
return console.error('参数必须是数组!');
this.x = vertices[0].x;
this.y = vertices[0].y;
this.width = vertices[1].x-this.x;
this.height = vertices[2].y-this.y;
};
/**
* 向量构造函数
* @constructor
*/
Ycc.Math.Vector = function () {
this.x = 0;
this.y = 0;
this.z = 0;
if(arguments.length===3 || arguments.length===2){
this.x=arguments[0]||0;
this.y=arguments[1]||0;
this.z=arguments[2]||0;
}
if(arguments.length===1){
if(!Ycc.utils.isObj(arguments[0])) console.error('constructor need a objec as param!');
this.x=arguments[0].x||0;
this.y=arguments[0].y||0;
this.z=arguments[0].z||0;
}
};
/**
* 向量的点乘法
* @param v2 {Ycc.Math.Vector} 点乘向量
* @return {number}
*/
Ycc.Math.Vector.prototype.dot = function (v2) {
return this.x*v2.x+this.y*v2.y+this.z*v2.z;
};
/**
* 向量的叉乘法
* @param v2 {Ycc.Math.Vector} 叉乘向量
* @return {number}
*/
Ycc.Math.Vector.prototype.cross = function (v2) {
var res = new Ycc.Math.Vector();
res.x = this.y*v2.z-v2.y*this.z;
res.y = v2.x*this.z-this.x*v2.z;
res.z = this.x*v2.y-v2.x*this.y;
return res;
};
/**
* 获取向量的模长
* @return {number}
*/
Ycc.Math.Vector.prototype.getLength = function () {
return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z,2);
};
/**
* 矩阵的构造方法。
* @param data {array} 矩阵所有行拼接的数组
* @param m {number} 行数
* @param n {number} 列数
* @constructor
*/
Ycc.Math.Matrix = function (data,m,n) {
this.data = data;
this.m = m;
this.n = n;
};
/**
* 矩阵点乘法
* @param M {Ycc.Math.Matrix} 另一个矩阵
*/
Ycc.Math.Matrix.prototype.dot = function (M) {
if(M.m!==this.n || M.n!==this.m)
return console.error('两个矩阵的行数和列数不对应,不能相乘!');
var N = new Ycc.Math.Matrix([],this.m,this.m);
// 循环行
for(var i=1;i<=this.m;i++){
// 循环矩阵赋值
for(var k=1;k<=this.m;k++){
var temp =0;
// 循环列
for(var j=1;j<=this.n;j++){
temp += this.get(i,j)*M.get(j,k);
}
N.set(i,k,temp);
}
}
return N;
};
/**
* 获取矩阵i行j列的元素。
* 注:i,i下标从1开始
* @param i
* @param j
* @return {number}
*/
Ycc.Math.Matrix.prototype.get = function (i, j) {
return this.data[(i-1)*this.n+j-1];
};
/**
* 设置矩阵i行j列的元素为val
* 注:i,i下标从1开始
* @param i
* @param j
* @param val
*/
Ycc.Math.Matrix.prototype.set = function (i, j, val) {
this.data[(i-1)*this.n+j-1] = val;
};
})(Ycc);;/**
* @file Ycc.Tree.class.js
* @author xiaohei
* @date 2018/8/6
* @description Ycc.Tree.class文件
*/
(function (Ycc) {
// 节点的自增id
var nodeID = 1;
/**
* 存储所有树节点的引用,不允许重载
* 使用prototype好处:
* 1、所有Tree对象公用
* 2、JSON.stringify不会将此序列化,避免抛循环引用的错误
* key为$id val为Tree对象
* @type {{}}
*/
var nodeMap = {};
/**
* 树的构造函数
* 若参数为空,默认创建只有一个根节点的树
* @constructor
*/
Ycc.Tree = function() {
/**
* 节点的自增ID,不允许修改,且每个对象必须唯一
* @type {number}
*/
this.$id = nodeID++;
/**
* 节点的父节点ID,不允许修改
* @type {null|Ycc.Tree}
*/
this.$parentID = null;
/**
* 节点的子节点列表
* @type {Array}
*/
this.children = [];
/**
* 节点所携带的数据
* @type {any}
*/
this.data = null;
// 存入map中,方便通过id寻找
nodeMap[this.$id] = this;
};
/**
* 释放当前节点的内存,非递归
* @param treeNode
*/
Ycc.Tree.release = function (treeNode) {
// 删除父级children的引用
var pa = treeNode.getParent();
if(pa){
var children = pa.children;
var index = children.indexOf(treeNode);
if(index!==-1)
children[index]=null;
}
// 删除nodeMap引用
delete nodeMap[treeNode.$id];
/**
* 节点的子节点列表
* @type {Array}
*/
treeNode.children.length = 0;
/**
* 节点所携带的数据
* @type {any}
*/
treeNode.data = null;
};
/**
* 获取nodeMap表
* @return {{}}
*/
Ycc.Tree.getNodeMap = function () {
return nodeMap;
};
/**
* 获取nodeMap表
* @return {{}}
*/
Ycc.Tree.prototype.getNodeMap = function () {
return nodeMap;
};
/**
* 获取父级
* @return {Ycc.Tree}
*/
Ycc.Tree.prototype.getParent = function () {
if(!this.$parentID) return null;
return nodeMap[this.$parentID];
};
/**
* 添加一颗子树
* @param tree
*/
Ycc.Tree.prototype.addChildTree = function (tree) {
if(tree.$parentID) return console.error("sub tree's parent has exist! can't add!",tree);
tree.$parentID = this.$id;
this.children.push(tree);
return this;
};
/**
* 删除一颗子树,只能删除直接子节点
* @param tree
* @return {*}
*/
Ycc.Tree.prototype.removeChildTree = function (tree) {
var index = this.children.indexOf(tree);
if(index===-1) return this;
this.children.splice(index,1);
return this;
};
/**
* 获取树的深度
* @return {number}
*/
Ycc.Tree.prototype.getDepth = function () {
var self = this;
var dep = 1;
if(self.children.length>0){
for(var i=0;i<self.children.length;i++){
var subDep = self.children[i].getDepth();
dep = subDep+1>dep?subDep+1:dep;
}
}
return dep;
};
/**
* 树的迭代器,返回集中常用的迭代方法
* @param option 遍历时的配置项
* @param option.reverse 遍历子节点时,是否将子节点反向,默认false,目前仅支持depthDown遍历
* @return {{each: each, leftChildFirst: leftChildFirst, rightChildFirst: rightChildFirst, depthDown: depthDown}}
*/
Ycc.Tree.prototype.itor = function (option) {
var self = this;
option = option || {
reverse:false
};
/**
* 父代优先遍历
* 先遍历父代,再依次遍历子代
* 若cb返回true,则停止遍历
* @param cb
* @return {boolean}
*/
function each(cb) {
if(cb.call(self,self)) return true;
if(self.children.length>0){
for(var i=0;i<self.children.length;i++){
if(self.children[i].itor().each(cb)) return true;
}
}
return false;
}
/**
* 左树优先遍历
* 只要最左边的树不为空就继续遍历其子树,最后遍历根节点
* 若cb返回true,则停止遍历
*
* @param cb
* @return {boolean}
*/
function leftChildFirst(cb) {
if(self.children.length>0){
for(var i=0;i<self.children.length;i++){
if(self.children[i].itor().leftChildFirst(cb)) return true;
}
}
if(cb.call(self,self)) return true;
}
/**
* 右树优先遍历
* 只要最右边的树不为空就继续遍历其子树,最后遍历根节点
* 若cb返回true,则停止遍历
*
* @param cb
* @return {boolean}
*/
function rightChildFirst(cb) {
if(self.children.length>0){
for(var i=self.children.length-1;i>=0;i--){
if(self.children[i].itor().rightChildFirst(cb)) return true;
}
}
if(cb.call(self,self)) return true;
}
/**
* 根据当前节点,按层级依次向下遍历
* 这只是depthDownByNodes的特殊情况
* 若cb返回true,则停止遍历
*
* @param cb(node)
* @return {boolean}
*/
function depthDown(cb) {
depthDownByNodes([self],cb);
}
/**
* 根据所给的节点列表,按层级依次向下遍历
* 若cb返回true,则停止遍历
*
* @param nodes {Ycc.Tree[]}
* @param cb(node,layer)
* @param [layer] {number} 当前nodes列表所在的层级,可选参数
* @return {boolean|number}
*/
function depthDownByNodes(nodes,cb,layer){
if(nodes.length===0)
return true;
layer=layer||0;
layer++;
// 待遍历的节点判断反向
var itorNodes = option.reverse?reverse(nodes):nodes;
// 下一层需要遍历的所有节点
var nextNodes = [];
// 是否停止遍历下一层的标志位
var breakFlag = false;
for(var i=0;i<itorNodes.length;i++) {
var rvl = cb.call(self, itorNodes[i], layer);
// 如果返回为true,则表示停止遍历下一层
// 如果返回为-1,则表示当前节点的所有子孙节点不再遍历
if (rvl===true) {
breakFlag = true;
break;
}else if(rvl===-1)
continue;
nextNodes = nextNodes.concat(option.reverse?reverse(itorNodes[i].children):itorNodes[i].children);
}
if(breakFlag){
nextNodes = [];
}
// 是否反向
if(option.reverse) nextNodes.reverse();
return depthDownByNodes(nextNodes,cb,layer);
}
/**
* 将list反向
* @param list
* @return {Array}
*/
function reverse(list){
var ls = [];
for(var i=list.length-1;i>=0;i--){
ls.push(list[i]);
}
return ls;
}
return {
each:each,
leftChildFirst:leftChildFirst,
rightChildFirst:rightChildFirst,
depthDown:depthDown
};
};
/**
* 转化为节点列表
* @return {Ycc.Tree[]}
*/
Ycc.Tree.prototype.toNodeList = function () {
var list = [];
this.itor().depthDown(function (node) {
list.push(node);
});
return list;
};
/**
* 获取节点列表按照层级的分类
* key为层级,val为Ycc.Tree列表
* @return {{}}
*/
Ycc.Tree.prototype.getNodeListGroupByLayer = function () {
var list = {};
this.itor().depthDown(function (node,layer) {
if(!list[layer])
list[layer] = [];
list[layer].push(node);
});
return list;
};
/**
* 获取节点的所有父级,靠近节点的父级排序在后
* @return {Ycc.Tree[]}
*/
Ycc.Tree.prototype.getParentList = function () {
var node = this;
var list = [];
while(node.$parentID){
var parent = nodeMap[node.$parentID];
list.unshift(parent);
node = parent;
}
return list;
};
/**
* 获取节点的所有兄弟节点,包括自身
* @return {Ycc.Tree[]}
*/
Ycc.Tree.prototype.getBrotherList = function () {
var list = [];
if(!this.$parentID)
list = [this];
else
list = nodeMap[this.$parentID].children;
return list;
};
/////////////////////////// static Methods
/**
* 根据传入的json构造一棵树
*
* 若节点有数据必须包含data字段
* 若节点有子节点必须包含children字段,且为数组
* 只关注data和children字段,其他字段将忽略
*
* @param json {object} json对象,示例:{data,children}
* @return {Ycc.Tree}
*/
Ycc.Tree.createByJSON = function (json) {
var root = new Ycc.Tree();
root.data = json.data;
if(Ycc.utils.isArray(json.children) && json.children.length>0){
for(var i=0;i<json.children.length;i++){
root.addChildTree(Ycc.Tree.createByJSON(json.children[i]));
}
}
return root;
};
/**
* 根据传入的节点列表构造一棵树
*
* 只关注字段id,parentID
*
* 构造成功后将生成新的$id,$parentID,且所有字段都将放入data中,包括id和parentID
*
* @param nodes {Array[]} json对象数组
* @return {Ycc.Tree}
*/
Ycc.Tree.createByNodes = function (nodes) {
if(!Ycc.utils.isArray(nodes) || nodes.length===0)
return console.error('need an Array as param!');
var root = null;
var treeNodes = [];
nodes.forEach(function (node) {
var treeNode = new Ycc.Tree();
treeNode.data = node;
treeNodes.push(treeNode);
if(!Ycc.utils.isNum(node.parentID) && !node.parentID)
root = treeNode;
});
treeNodes.forEach(function (treeNode) {
treeNodes.forEach(function (subNode) {
if(subNode.data.parentID === treeNode.data.id){
treeNode.addChildTree(subNode);
}
});
});
return root;
};
})(Ycc);;/**
* @file Ycc.Graph.class.js
* @author xiaohei
* @date 2018/8/14
* @description Ycc.Graph.class文件
*
* 图的结构类,有向图、无向图等
*/
(function (Ycc) {
// 节点唯一id
var vid = 1;
// 边的唯一id
var eid = 1;
// 图的唯一id
var gid = 1;
// 节点map key为$id val为有向顶点/无向顶点
var vMap = {};
// 边map key为$id val为有向边/无向边
var eMap = {};
/**
* 图的结构类
* @constructor
*/
Ycc.Graph = function (type) {
/**
* 图的分类 1--有向图 2--无向图
* @type {number}
*/
this.type = type||1;
/**
* 图的id
* @type {number}
*/
this.$id = gid++;
/**
* 图包含的顶点
* @type {Ycc.Graph.DirectedV[] | Ycc.Graph.UnDirectedV[]}
*/
this.vList = [];
/**
* 图包含的边
* @type {Ycc.Graph.E[]}
*/
this.eList = [];
};
/**
* 获取顶点的map
* @return {{}}
*/
Ycc.Graph.prototype.getMapV = function () {
return vMap;
};
/**
* 获取边的map
* @return {{}}
*/
Ycc.Graph.prototype.getMapE = function () {
return eMap;
};
/**
* 广度优先搜索
* @param vArrId {array} 顶点$id数组,代表从哪些顶点开始遍历
* @param cb {function} 回调函数
* 若回调函数返回true,则遍历结束
* @param [vSearchedId] 已遍历的$id数组
*
* @return {boolean}
*/
Ycc.Graph.prototype.bfs = function (vArrId,cb,vSearchedId) {
vArrId = vArrId||[];
vSearchedId = vSearchedId || [];
// 递归结束条件
if(vSearchedId.length===this.vList.length)
return true;
// 若未结束,但长度为0,说明图中存在孤立部分,任取一个孤立部分的顶点,继续遍历
if(vArrId.length===0){
var tempID = null;
for(var j=0;j<this.vList.length;j++){
if(vSearchedId.indexOf(this.vList[j].$id)===-1){
tempID=this.vList[j].$id;
break;
}
}
return this.bfs([tempID],cb,vSearchedId);
}
// 修改已遍历的顶点
var v = null;
for(var i=0;i<vArrId.length;i++){
v = vMap[vArrId[i]];
if(cb.call(this,v)) return true;
vSearchedId.push(v.$id);
}
// 下一层需要遍历的节点
var next = [];
for(var k=0;k<vArrId.length;k++){
v = vMap[vArrId[k]];
var temp = v.getAccessibleIds().filter(function (id) {
return vSearchedId.indexOf(id)===-1;
});
next = next.concat(temp);
}
return this.bfs(next,cb,vSearchedId);
};
/**
* 图的深度优先遍历
* @param vStartID 从哪个顶点开始遍历
* @param cb
* @param vSearchedId 已遍历的$id数组
*/
Ycc.Graph.prototype.dfs = function (vStartID,cb,vSearchedId) {
vSearchedId = vSearchedId || [];
// 修改已遍历的顶点
var v = vMap[vStartID];
vSearchedId.push(v.$id);
if(cb.call(this,v)) return true;
// 遍历可达的节点
var accessibleIds = v.getAccessibleIds();
for(var k=0;k<accessibleIds.length;k++){
var next = accessibleIds[k];
if(vSearchedId.indexOf(next)===-1){
if(this.dfs(next,cb,vSearchedId))
return true;
}
}
// 递归结束条件
if(vSearchedId.length===this.vList.length)
return true;
// 若递归未结束,说明图中存在孤立部分,任取一个孤立部分的顶点,继续遍历
var tempID = null;
for(var j=0;j<this.vList.length;j++){
if(vSearchedId.indexOf(this.vList[j].$id)===-1){
tempID=this.vList[j].$id;
break;
}
}
return this.dfs(tempID,cb,vSearchedId);
};
/**
* 创建一个有向图
* @static
* @param vArr {Array} 顶点列表,示例:[{id,data,...}]
* @param eArr {Array} 边列表,示例:[{fromId,toId,data,...}]
*/
Ycc.Graph.createDirectedGraph = function (vArr,eArr) {
var graph = new Ycc.Graph(1);
var vList = graph.vList;
var eList = graph.eList;
vArr.forEach(function (v) {
vList.push(new Ycc.Graph.DirectedV(v));
});
eArr.forEach(function (e) {
var from = null,to=null;
var edge = new Ycc.Graph.DirectedE();
for(var i =0;i<vList.length;i++){
// 两个都找到了就跳出去
if(from && to) break;
var v = vList[i];
if(v.data.id === e.fromId){
from = v;
v.outIDs.push(edge.$id);
}
if(v.data.id === e.toId){
to = v;
v.inIDs.push(edge.$id);
}
}
edge.init(from.$id,to.$id,e);
eList.push(edge);
});
return graph;
};
/**
* 有向图中的顶点类
* @param data 节点的数据
* @constructor
*/
Ycc.Graph.DirectedV = function (data) {
/**
* 节点id
* @type {number}
*/
this.$id = vid++;
/**
* 节点所携带的数据
* @type {any}
*/
this.data = data;
/**
* 节点的入边$id列表
* @type {number[]}
*/
this.inIDs = [];
/**
* 节点的出边$id列表
* @type {number[]}
*/
this.outIDs = [];
// 放入map,方便查找
vMap[this.$id] = this;
};
/**
* 获取有向图中,某个节点指向的节点ID列表
* @return {Array}
*/
Ycc.Graph.DirectedV.prototype.getAccessibleIds = function () {
var ids = [];
this.outIDs.forEach(function (id) {
ids.push(eMap[id].toID);
});
return ids;
};
/**
* 有向图中的边类
* @constructor
*/
Ycc.Graph.DirectedE = function () {
/**
* 边的id
* @type {number}
*/
this.$id = eid++;
/**
* 边所携带的数据,比如权重
* @type {any}
*/
this.data = null;
/**
* 边的起点$id
* @type {number}
*/
this.fromID = null;
/**
* 边的终点$id
* @type {number}
*/
this.toID = null;
// 放入map,方便查找
eMap[this.$id] = this;
};
/**
* 有向图边的初始化
* @param fromID
* @param toID
* @param data
*/
Ycc.Graph.DirectedE.prototype.init = function (fromID, toID, data) {
this.fromID = fromID;
this.toID = toID;
this.data = data;
};
/**
* 创建一个无向图
* @static
* @param vArr {Array} 顶点列表,示例:[{id,data,...}]
* @param eArr {Array} 边列表,ids为边关联的两个顶点id列表,长度为2,示例:[{ids,data,...}]
*/
Ycc.Graph.createUnDirectedGraph = function (vArr,eArr) {
var graph = new Ycc.Graph(2);
var vList = graph.vList;
var eList = graph.eList;
vArr.forEach(function (v) {
vList.push(new Ycc.Graph.UnDirectedV(v));
});
eArr.forEach(function (e) {
// 边的节点id列表
var ids = [];
var edge = new Ycc.Graph.UnDirectedE();
for(var i =0;i<vList.length;i++){
// 遍历的顶点
var v = vList[i];
// 两个都找到了就跳出去
if(ids.length===2) break;
if(v.data.id === e.ids[0] || v.data.id === e.ids[1]){
ids.push(v.$id);
v.eIDs.push(edge.$id);
}
}
edge.init(ids,e);
eList.push(edge);
});
return graph;
};
/**
* 无向图中的顶点类
* @param data {any} 顶点携带的数据
* @constructor
*/
Ycc.Graph.UnDirectedV = function (data) {
/**
* 节点id
* @type {number}
*/
this.$id = vid++;
/**
* 节点所携带的数据
* @type {any}
*/
this.data = data;
/**
* 节点的边$id列表
* @type {number[]}
*/
this.eIDs = [];
// 放入map,方便查找
vMap[this.$id] = this;
};
/**
* 获取节点的可达节点id列表
* @return {number[]}
*/
Ycc.Graph.UnDirectedV.prototype.getAccessibleIds = function () {
var accessibleIds=[];
for(var i=0;i<this.eIDs.length;i++){
// 边
var edge = eMap[this.eIDs[i]];
if(edge.vIDs[0]===this.$id){
accessibleIds.push(edge.vIDs[1]);
}
if(edge.vIDs[1]===this.$id){
accessibleIds.push(edge.vIDs[0]);
}
}
return accessibleIds;
};
/**
* 无向图中的边类
* @constructor
*/
Ycc.Graph.UnDirectedE = function () {
/**
* 边的id
* @type {number}
*/
this.$id = eid++;
/**
* 边所携带的数据,比如权重
* @type {any}
*/
this.data = null;
/**
* 关联的两个顶点$id数组 长度为2
* @type {number[]}
*/
this.vIDs = [];
// 放入map,方便查找
eMap[this.$id] = this;
};
/**
* 无向图中的边类初始化
* @param ids
* @param data
*/
Ycc.Graph.UnDirectedE.prototype.init = function (ids, data) {
this.vIDs = ids;
this.data = data;
};
})(Ycc);;/**
* @file Ycc.Ticker.class.js
* @author xiaohei
* @date 2017/10/26
* @description Ycc.Ticker.class文件
*/
(function (Ycc) {
/**
* 系统心跳管理类。
* 管理系统的心跳;自定义帧事件的广播;帧更新图层的更新等。
*
* 注:
* 心跳间隔时间为1e3/60;
* 无论帧率为多少,心跳间隔时间不变;
* 总帧数<=总心跳次数;
* 只有当总帧数*每帧的理论时间小于总心跳时间,帧的监听函数才会触发,以此来控制帧率;
*
* @param yccInstance
* @constructor
*/
Ycc.Ticker = function (yccInstance) {
this.yccClass = Ycc.Ticker;
/**
* ycc实例的引用
* @type {Ycc}
*/
this.yccInstance = yccInstance;
/**
* 当前帧
* @type {Frame}
*/
this.currentFrame = null;
/**
* 启动时间戳
* @type {number}
*/
this.startTime = performance.now();
/**
* 上一帧刷新的时间戳
* @type {number}
*/
this.lastFrameTime = this.startTime;
/**
* 上一帧刷新时的心跳数
* @type {number}
*/
this.lastFrameTickerCount = 0;
/**
* 当前帧与上一帧的刷新的时间差
* @type {number}
*/
this.deltaTime = 0;
/**
* 当前帧与上一帧时间差的期望值(根据帧率计算而来的)
* @type {number}
*/
this.deltaTimeExpect = 0;
/**
* 实际帧间隔与期望帧间隔的时间比
* @type {number}
*/
this.deltaTimeRatio = 1;
/**
* 所有自定义的帧监听函数列表
* @type {function[]}
*/
this.frameListenerList = [];
/**
* 默认帧率
* @type {number}
*/
this.defaultFrameRate = 60;
/**
* 默认帧间隔
* @type {number}
*/
this.defaultDeltaTime = 1e3/this.defaultFrameRate;
/**
* 每帧之间间隔的心跳数
* @type {number}
*/
this.tickerSpace = 1;
/**
* 总帧数
* @type {number}
*/
this.frameAllCount = 0;
/**
* 总心跳次数
* @type {number}
*/
this.timerTickCount = 0;
/**
* 定时器ID。用于停止心跳。
* @type {number}
* @private
*/
this._timerId = 0;
/**
* 心跳是否已经启动
* @type {boolean}
* @private
*/
this._isRunning = false;
};
/**
* 定时器开始
* @param [frameRate] 心跳频率,即帧率
* 可取值有[60,30,20,15]
*/
Ycc.Ticker.prototype.start = function (frameRate) {
var timer = requestAnimationFrame || webkitRequestAnimationFrame || mozRequestAnimationFrame || oRequestAnimationFrame || msRequestAnimationFrame;
var self = this;
//重置状态
self.currentFrame = null;
self.timerTickCount = 0;
self.lastFrameTickerCount = 0;
// 正常设置的帧率
frameRate = frameRate?frameRate:self.defaultFrameRate;
// 每帧之间的心跳间隔,默认为1
self.tickerSpace = parseInt(60/frameRate)||1;
// 每帧理论的间隔时间
self.deltaTimeExpect = 1000/frameRate;
// 初始帧数量设为0
self.frameAllCount = 0;
// 启动时间
self.startTime = performance.now();
// 正在进行中 不再启动心跳
if(self._isRunning) return;
// timer兼容
timer || (timer = function(callback) {
return setTimeout(function () {
callback(Date.now());
}, 1e3 / 60);
}
);
// 启动心跳
// self._timerId = timer.call(window, cb);
self._timerId = timer(cb);
self._isRunning = true;
// 心跳回调函数。约60fps
function cb(curTime) {
// 总的心跳数加1
self.timerTickCount++;
if(self.timerTickCount - self.lastFrameTickerCount === self.tickerSpace){
// 设置 总帧数加1
self.frameAllCount++;
// 设置 两帧的时间差
self.deltaTime = curTime-self.lastFrameTime;
// 设置 帧间隔缩放比
self.deltaTimeRatio = self.deltaTime/self.deltaTimeExpect;
// 设置 上一帧刷新时间
self.lastFrameTime += self.deltaTime;
// 设置 上一帧刷新时的心跳数
self.lastFrameTickerCount = self.timerTickCount;
// 构造一帧
self.currentFrame = new Frame(self);
// 执行所有自定义的帧监听函数
self.broadcastFrameEvent(self.currentFrame);
// 执行所有图层的帧监听函数
self.broadcastToLayer(self.currentFrame);
}
// 递归调用心跳函数
// self._timerId = timer.call(window,cb);
self._timerId = timer(cb);
}
};
/**
* 停止心跳
*/
Ycc.Ticker.prototype.stop = function () {
var stop = cancelAnimationFrame || webkitCancelAnimationFrame || mozCancelAnimationFrame || oCancelAnimationFrame;
stop || (stop = function (id) {
return clearTimeout(id);
});
stop(this._timerId);
this._isRunning = false;
this.currentFrame = null;
};
/**
* 给每帧添加自定义的监听函数
* @param listener
*/
Ycc.Ticker.prototype.addFrameListener = function (listener) {
this.frameListenerList.push(listener);
};
/**
* 移除某个监听函数
* @param listener
*/
Ycc.Ticker.prototype.removeFrameListener = function (listener) {
var index = this.frameListenerList.indexOf(listener);
if(index!==-1)
this.frameListenerList.splice(index,1);
};
/**
* 执行所有自定义的帧监听函数
*/
Ycc.Ticker.prototype.broadcastFrameEvent = function (frame) {
for(var i =0;i<this.frameListenerList.length;i++){
var listener = this.frameListenerList[i];
Ycc.utils.isFn(listener) && listener(frame);
}
};
/**
* 执行所有图层的监听函数
*/
Ycc.Ticker.prototype.broadcastToLayer = function (frame) {
for(var i = 0;i<this.yccInstance.layerList.length;i++){
var layer = this.yccInstance.layerList[i];
layer.show && layer.enableFrameEvent && layer.onFrameComing(frame);
}
};
/**
* 帧 私有类
* @constructor
* @param ticker {Ycc.Ticker}
*/
function Frame(ticker){
/**
* 帧创建时间
* @type {number}
*/
this.createTime = Date.now();
/**
* 与上一帧的时间差
* @type {number}
*/
this.deltaTime = ticker.deltaTime;
/**
* 实时帧率
* @type {number}
*/
this.fps = parseInt(1000/this.deltaTime);
/**
* 帧下标,表示第几帧
* @type {number}
*/
this.frameCount = ticker.frameAllCount;
/**
* 当前帧是否已全部绘制,ticker回调函数可根据此字段判断
* 以此避免一帧内的重复绘制
* @type {boolean}
*/
this.isRendered = false;
}
})(Ycc);;/**
* @file Ycc.Debugger.class.js
* @author xiaohei
* @date 2018/10/24
* @description Ycc.Debugger.class文件
*/
(function (Ycc) {
/**
* ycc的调试模块
* @constructor
*/
Ycc.Debugger = function (yccInstance) {
this.yccClass = Ycc.Debugger;
/**
* ycc的实例
*/
this.yccInstance = yccInstance;
/**
* 信息面板显示的UI 帧间隔
* @type {Ycc.UI}
*/
this.deltaTime = null;
/**
* 信息面板显示的UI 帧间隔期望值
* @type {Ycc.UI}
*/
this.deltaTimeExpect = null;
/**
* 信息面板显示的UI 总帧数
* @type {Ycc.UI}
*/
this.frameAllCount = null;
/**
* 信息面板显示的UI 帧间隔平均值
* @type {Ycc.UI}
*/
this.deltaTimeAverage = null;
/**
* 当前帧渲染耗时
* @type {Ycc.UI}
*/
this.renderTime = null;
/**
* 当前帧渲染的所有UI个数
* @type {Ycc.UI}
*/
this.renderUiCount = null;
this.totalJSHeapSize = null;
this.usedJSHeapSize = null;
this.jsHeapSizeLimit = null;
/**
* 调试面板所显示的字段,示例:[{name,cb,ui}]
* @type {Array}
*/
this.fields = [];
/**
* 调试面板的容纳区
* @type {Ycc.UI.Rect}
*/
this.rect = null;
/**
* 调试面板的图层
*/
this.layer = null;
};
Ycc.Debugger.prototype.init = function () {
this.rect = new Ycc.UI.Rect({
name:'debuggerRect',
rect:new Ycc.Math.Rect(10,10,200,140),
color:'rgba(255,255,0,0.5)'
});
this.layer = yccInstance.layerManager.newLayer({
name:"debug图层"
});
var self = this;
this.yccInstance.ticker.addFrameListener(function () {
self.updateInfo();
});
};
/**
* 显示调试面板
*/
Ycc.Debugger.prototype.showDebugPanel = function () {
this.init();
var layer = this.layer;
if(layer.uiList.indexOf(this.rect)===-1)
layer.addUI(this.rect);
};
/**
* 更新面板的调试信息
*/
Ycc.Debugger.prototype.updateInfo = function () {
// 强制使debug面板置顶
var layerList = this.yccInstance.layerList;
var index = layerList.indexOf(this.layer);
// bug 调试layer必须存在
if(index===-1) return;
if(index+1!==layerList.length){
layerList.splice(index,1);
layerList.push(this.layer);
}
this.rect.rect.height = this.fields.length*20;
this.fields.forEach(function (field) {
field.ui.content = field.name+' '+field.cb();
});
};
/**
* 添加一个信息项
* @param name
* @param cb() {function}
* cb必须返回一个值,这个值将直接填入
*
*/
Ycc.Debugger.prototype.addField = function (name, cb) {
var index = this.fields.length;
var ui = new Ycc.UI.SingleLineText({
content:"usedJSHeapSize "+cb(),
fontSize:'12px',
rect:new Ycc.Math.Rect(0,20*index,100,20),
color:'green'
});
this.fields.push({name:name,cb:cb,ui:ui});
this.rect.addChild(ui);
};
/**
* 将调试面板添加至某个图层
* @param layer {Ycc.Layer}
*/
Ycc.Debugger.prototype.addToLayer = function (layer) {
if(layer.uiList.indexOf(this.rect)===-1)
layer.addUI(this.rect);
};
/**
* 更新某个调试字段的回调函数
* @param name
* @param cb
*/
Ycc.Debugger.prototype.updateField = function (name,cb) {
for(var i=0;i<this.fields.length;i++){
if(this.fields[i].name===name){
this.fields[i].cb=null;
this.fields[i].cb=cb;
return;
}
}
};
/**
* 调试日志信息类
* @param message
* @constructor
*/
Ycc.Debugger.Log = function (message) {
this.message = '[Ycc logger]=> '+message;
};
/**
* 调试错误信息类
* @param message
* @constructor
*/
Ycc.Debugger.Error = function (message) {
this.message = '[Ycc error]=> '+message;
};
})(Ycc);;/**
* @file Ycc.Loader.class.js
* @author xiaohei
* @date 2017/10/9
* @description Ycc.Loader.class文件
*/
(function (Ycc) {
/**
* ycc实例的资源加载类
* @constructor
*/
Ycc.Loader = function () {
this.yccClass = Ycc.Loader;
/**
* 异步模块
* @type {Ycc.Ajax}
*/
this.ajax = new Ycc.Ajax();
/**
* 基础地址,末尾必须有斜线'/'
* @type {string}
*/
this.basePath = '';
};
/**
* 并发加载资源
* @param resArr
* @param [resArr.name] 资源名称,方便查找
* @param resArr.url 资源的url
* @param [resArr.type] 资源类型 image,audio,默认为image
* @param [resArr.res] 资源加载完成后,附加给该字段
* @param [resArr.crossOrigin] 资源跨域配置项
* @param endCb 资源加载结束的回调
* @param [progressCb] 资源加载进度的回调
* @param [endResArr] 用于存储加载已结束的音频,一般不用传值
* @param [endResMap] 用于存储加载已结束的音频map,一般不用传值。注:map的key是根据name字段生成的
*/
Ycc.Loader.prototype.loadResParallel = function (resArr, endCb, progressCb,endResArr,endResMap) {
endResArr = endResArr || [];
endResMap = endResMap || {};
for(var i=0;i<resArr.length;i++){
var curRes = resArr[i];
var successEvent = "load";
var errorEvent = "error";
curRes.type = curRes.type || 'image';
if(curRes.type==='image'){
curRes.res = new Image();
curRes.res.src = curRes.url;
curRes.res.crossOrigin = curRes.crossOrigin||'';
}
if(curRes.type==='audio'){
successEvent = 'loadedmetadata';
curRes.res = new Audio();
curRes.res.src = curRes.url;
curRes.res.preload = "load";
curRes.res.crossOrigin = curRes.crossOrigin||'';
}
curRes.res.addEventListener(successEvent,listener(curRes,i,true));
curRes.res.addEventListener(errorEvent,listener(curRes,i,false));
function listener(curRes,index,error) {
return function () {
endResArr.push(curRes);
if(typeof curRes.name!=='undefined') endResMap[curRes.name] = curRes.res;
Ycc.utils.isFn(progressCb) && progressCb(curRes,error,index);
if(resArr.length===endResArr.length){
endCb(endResArr,endResMap);
}
};
}
}
};
/**
* 依次加载资源
* @param resArr
* @param [resArr.name] 资源名称,方便查找
* @param resArr.url 资源的url
* @param [resArr.type] 资源类型 image,audio
* @param [resArr.res] 资源加载完成后,附加给该字段
* @param endCb 资源加载结束的回调
* @param [progressCb] 资源加载进度的回调
* @param [endResArr] 用于存储加载已结束的音频,一般不用传值
* @param [endResMap] 用于存储加载已结束的音频map,一般不用传值。注:map的key是根据name字段生成的
*/
Ycc.Loader.prototype.loadResOneByOne = function (resArr, endCb, progressCb,endResArr,endResMap) {
endResArr = endResArr || [];
endResMap = endResMap || {};
if(resArr.length===endResArr.length){
endCb(endResArr,endResMap);
return;
}
var self = this;
// 当前加载的下标
var index = endResArr.length;
var curRes = resArr[index];
var successEvent = "load";
var errorEvent = "error";
curRes.type = curRes.type || 'image';
var timerId = 0;
polyfillWx(self.basePath + curRes.url,function (fullPath) {
if(curRes.type==='image'){
curRes.res = new Image();
curRes.res.src = fullPath;
curRes.res.addEventListener(successEvent,onSuccess);
curRes.res.addEventListener(errorEvent,onError);
// 超时提示只针对图片
timerId = setTimeout(function () {
curRes.res.removeEventListener(successEvent,onSuccess);
curRes.res.removeEventListener(errorEvent,onSuccess);
onError({message:"获取资源超时!"});
},curRes.timeout||10000);
}else if(curRes.type==='audio'){
// 兼容wx端
if("undefined"!==typeof wx){
curRes.res = new Audio();
curRes.res.src = fullPath;
successEvent = 'loadedmetadata';
errorEvent = 'error';
curRes.res.addEventListener(successEvent,onSuccess);
curRes.res.addEventListener(errorEvent,onError);
return;
}
curRes.res = new AudioPolyfill();
if(!curRes.res.context){
onError({message:"浏览器不支持AudioContext!"});
return;
}
console.log(fullPath);
self.ajax.get(fullPath,(function (curRes) {
return function (arrayBuffer) {
curRes.res.context.decodeAudioData(arrayBuffer, function(buf) {
curRes.res.buf=buf;
onSuccess();
}, onError);
}
})(curRes),onError,'arraybuffer');
}
});
function onSuccess() {
// console.log('loader:',curRes.name,'success');
clearTimeout(timerId);
if(curRes.type==='image' || ("undefined"!==typeof wx && curRes.type==='audio' )){
curRes.res.removeEventListener(successEvent,onSuccess);
curRes.res.removeEventListener(errorEvent,onErr