JavaScript 一些实用辅助类库
"use strict"; var __emptyPoint = null, __emptyContext = null; const ColorRefTable = [ ['aliceblue','#f0f8ff'], ['antiquewhite','#faebd7'], ['aqua','#00ffff'], ['aquamarine','#7fffd4'], ['azure','#f0ffff'], ['beige','#f5f5dc'], ['bisque','#ffe4c4'], ['black','#000000'], ['blanchedalmond','#ffebcd'], ['blue','#0000ff'], ['blueviolet','#8a2be2'], ['brown','#a52a2a'], ['burlywood','#deb887'], ['cadetblue','#5f9ea0'], ['chartreuse','#7fff00'], ['chocolate','#d2691e'], ['coral','#ff7f50'], ['cornflowerblue'], ['cornsilk','#fff8dc'], ['crimson','#dc143c'], ['cyan','#00ffff'], ['darkblue','#00008b'], ['darkcyan','#008b8b'], ['darkgoldenrod','#b8860b'], ['darkgray','#a9a9a9'], ['darkgreen','#006400'], ['darkgrey','#a9a9a9'], ['darkkhaki','#bdb76b'], ['darkmagenta','#8b008b'], ['firebrick','#b22222'], ['darkolivegreen','#556b2f'], ['darkorange','#ff8c00'], ['darkorchid','#9932cc'], ['darkred','#8b0000'], ['darksalmon','#e9967a'], ['darkseagreen','#8fbc8f'], ['darkslateblue','#483d8b'], ['darkslategray','#2f4f4f'], ['darkslategrey','#2f4f4f'], ['darkturquoise','#00ced1'], ['darkviolet','#9400d3'], ['deeppink','#ff1493'], ['deepskyblue','#00bfff'], ['dimgray','#696969'], ['dimgrey','#696969'], ['dodgerblue','#1e90ff'], ['floralwhite','#fffaf0'], ['forestgreen','#228b22'], ['fuchsia','#ff00ff'], ['gainsboro','#dcdcdc'], ['ghostwhite','#f8f8ff'], ['gold','#ffd700'], ['goldenrod','#daa520'], ['gray','#808080'], ['green','#008000'], ['greenyellow','#adff2f'], ['grey','#808080'], ['honeydew','#f0fff0'], ['hotpink','#ff69b4'], ['indianred','#cd5c5c'], ['indigo','#4b0082'], ['ivory','#fffff0'], ['khaki','#f0e68c'], ['lavender','#e6e6fa'], ['lavenderblush','#fff0f5'], ['lawngreen','#7cfc00'], ['lemonchiffon','#fffacd'], ['lightblue','#add8e6'], ['lightcoral','#f08080'], ['lightcyan','#e0ffff'], ['lightgoldenrodyellow','#fafad2'], ['lightgray','#d3d3d3'], ['lightgreen','#90ee90'], ['lightgrey','#d3d3d3'], ['lightpink','#ffb6c1'], ['lightsalmon','#ffa07a'], ['lightseagreen','#20b2aa'], ['lightskyblue','#87cefa'], ['lightslategray','#778899'], ['lightslategrey','#778899'], ['lightsteelblue','#b0c4de'], ['lightyellow','#ffffe0'], ['lime','#00ff00'], ['limegreen','#32cd32'], ['linen','#faf0e6'], ['magenta','#ff00ff'], ['maroon','#800000'], ['mediumaquamarine','#66cdaa'], ['mediumblue','#0000cd'], ['mediumorchid','#ba55d3'], ['mediumpurple','#9370db'], ['mediumseagreen','#3cb371'], ['mediumslateblue','#7b68ee'], ['mediumspringgreen','#00fa9a'], ['mediumturquoise','#48d1cc'], ['mediumvioletred','#c71585'], ['midnightblue','#191970'], ['mintcream','#f5fffa'], ['mistyrose','#ffe4e1'], ['moccasin','#ffe4b5'], ['navajowhite','#ffdead'], ['navy','#000080'], ['oldlace','#fdf5e6'], ['olive','#808000'], ['olivedrab','#6b8e23'], ['orange','#ffa500'], ['orangered','#ff4500'], ['orchid','#da70d6'], ['palegoldenrod','#eee8aa'], ['palegreen','#98fb98'], ['paleturquoise','#afeeee'], ['palevioletred','#db7093'], ['papayawhip','#ffefd5'], ['peachpuff','#ffdab9'], ['peru','#cd853f'], ['pink','#ffc0cb'], ['plum','#dda0dd'], ['powderblue','#b0e0e6'], ['purple','#800080'], ['red','#ff0000'], ['rosybrown','#bc8f8f'], ['royalblue','#4169e1'], ['saddlebrown','#8b4513'], ['salmon','#fa8072'], ['sandybrown','#f4a460'], ['seagreen','#2e8b57'], ['seashell','#fff5ee'], ['sienna','#a0522d'], ['silver','#c0c0c0'], ['skyblue','#87ceeb'], ['slateblue','#6a5acd'], ['slategray','#708090'], ['slategrey','#708090'], ['snow','#fffafa'], ['springgreen','#00ff7f'], ['steelblue','#4682b4'], ['tan','#d2b48c'], ['teal','#008080'], ['thistle','#d8bfd8'], ['tomato','#ff6347'], ['turquoise','#40e0d0'], ['violet','#ee82ee'], ['wheat','#f5deb3'], ['white','#ffffff'], ['whitesmoke','#f5f5f5'], ['yellow','#ffff00'], ['yellowgreen','#9acd32'] ], UTILS = { toAngle(v){ return v / 180 * Math.PI; }, emptyArray(arr){ return !Array.isArray(arr) || arr.length === 0; }, isObject(obj){ return obj !== null && typeof obj === "object" && Array.isArray(obj) === false; }, isNumber(num){ return typeof num === "number" && isNaN(num) === false; }, //获取最后一个点后面的字符 getFileType(string){ let type = "", str = string.split('').reverse().join(''); for(let k = 0, len = str.length; k < len; k++){ if(str[k] === ".") break; type += str[k]; } return type.split('').reverse().join(''); }, //删除 string 所有的空格 deleteSpaceAll(str){ const len = str.length; var result = ''; for(let i = 0; i < len; i++){ if(str[i] !== '') result += str[i] } return result }, //删除 string 两边空格 removeSpaceSides(string){ return string.replace(/(^\s*)|(\s*$)/g, ""); }, //返回 num 与 num1 之间的随机数 random(num, num1){ if(num < num1) return Math.random() * (num1 - num) + num; else if(num > num1) return Math.random() * (num - num1) + num1; else return num; }, //生成 UUID generateUUID: function (){ const _lut = []; for ( let i = 0; i < 256; i ++ ) { _lut[ i ] = ( i < 16 ? '0' : '' ) + ( i ).toString( 16 ); } return function (){ const d0 = Math.random() * 0xffffffff | 0; const d1 = Math.random() * 0xffffffff | 0; const d2 = Math.random() * 0xffffffff | 0; const d3 = Math.random() * 0xffffffff | 0; const uuid = _lut[ d0 & 0xff ] + _lut[ d0 >> 8 & 0xff ] + _lut[ d0 >> 16 & 0xff ] + _lut[ d0 >> 24 & 0xff ] + '-' + _lut[ d1 & 0xff ] + _lut[ d1 >> 8 & 0xff ] + '-' + _lut[ d1 >> 16 & 0x0f | 0x40 ] + _lut[ d1 >> 24 & 0xff ] + '-' + _lut[ d2 & 0x3f | 0x80 ] + _lut[ d2 >> 8 & 0xff ] + '-' + _lut[ d2 >> 16 & 0xff ] + _lut[ d2 >> 24 & 0xff ] + _lut[ d3 & 0xff ] + _lut[ d3 >> 8 & 0xff ] + _lut[ d3 >> 16 & 0xff ] + _lut[ d3 >> 24 & 0xff ]; return uuid.toLowerCase(); //toLowerCase() 这里展平连接的字符串以节省堆内存空间 } }(), //欧几里得距离(两点的直线距离) distance(x, y, x1, y1){ return Math.sqrt(Math.pow(x - x1, 2) + Math.pow(y - y1, 2)); }, downloadFile(blob, fileName){ const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = fileName; link.click(); }, loadFileJSON(callback){ const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = a => { if(a.target.files.length === 0) return; const fr = new FileReader(); fr.onloadend = b => callback(b.target.result); fr.readAsText(a.target.files[0]); //fr.readAsDataURL(a.target.files[0]); } input.click(); }, get emptyPoint(){ if(__emptyPoint === null) __emptyPoint = new Point(); return __emptyPoint; }, get emptyContext(){ if(__emptyContext === null) __emptyContext = document.createElement("canvas").getContext('2d') return __emptyContext; }, } /** Ajax parameter: option = { url: 可选, 默认 '' method: 可选, post 或 get请求, 默认 post asy: 可选, 是否异步执行, 默认 true success: 可选, 成功回调, 默认 null error: 可选, 超时或失败调用, 默认 null change: 可选, 请求状态改变时调用, 默认 null data: 可选, 如果定义则在初始化时自动执行.send(data)方法 } demo: const data = `email=${email}&password=${password}`, //默认 post 请求: ajax = new Ajax({ url: './login', data: data, success: mes => console.log(mes), }); //get 请求: ajax.method = "get"; ajax.send(data); */ class Ajax{ constructor(option = {}){ this.url = option.url || ""; this.method = option.method || "post"; this.asy = typeof option.asy === "boolean" ? option.asy : true; this.success = option.success || null; this.error = option.error || null; this.change = option.change || null; //init XML this.xhr = new XMLHttpRequest(); this.xhr.onerror = this.xhr.ontimeout = option.error || null; this.xhr.onreadystatechange = event => { if(event.target.readyState === 4 && event.target.status === 200){ if(this.success !== null) this.success(event.target.responseText, event); } else if(this.change !== null) this.change(event); } if(option.data) this.send(option.data); } send(data = ""){ if(this.method === "get"){ this.xhr.open(this.method, this.url+"?"+data, this.asy); this.xhr.send(); } else if(this.method === "post"){ this.xhr.open(this.method, this.url, this.asy); this.xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); this.xhr.send(data); } } } /* IndexedDB 本地数据库 parameter: name: String; //需要打开的数据库名称(如果不存在则会新建一个) 必须 done: Function(IndexedDB); //链接数据库成功时的回调 默认 null version: Number; //数据库版本(高版本数据库将覆盖低版本的数据库) 默认 1 attribute: database: IndexedDB; //链接完成的数据库对象 transaction: IDBTransaction; //事务管理(读和写) objectStore: IDBObjectStore; //当前的事务 method: set(data, key, callback) //添加或更新 get(key, callback) //获取 delete(key, callback) //删除 traverse(callback) //遍历 getAll(callback) //获取全部 clear(callback) //清理所以数据 close() //关闭数据库链接 readOnly: static: indexedDB: Object; demo: new IndexedDB('TEST', db => { conosle.log(db); }); */ class IndexedDB{ static indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; get objectStore(){ //每个事务只能使用一次, 所以每次都需要重新创建 return this.database.transaction(this.name, 'readwrite').objectStore(this.name); } constructor(name, done = null, version = 1){ if(IndexedDB.indexedDB === undefined) return console.error("IndexedDB: 不支持IndexedDB"); if(typeof name !== 'string') return console.warn('IndexedDB: 参数错误'); this.name = name; this.database = null; const request = IndexedDB.indexedDB.open(name, version); request.onupgradeneeded = (e)=>{ //数据库不存在 或 版本号不同时 触发 if(!this.database) this.database = e.target.result; if(this.database.objectStoreNames.contains(name) === false) this.database.createObjectStore(name); } request.onsuccess = (e)=>{ this.database = e.target.result; if(typeof done === 'function') done(this); } request.onerror = (e)=>{ console.error(e); } } close(){ return this.database.close(); } clear(callback){ this.objectStore.clear().onsuccess = callback; } traverse(callback){ this.objectStore.openCursor().onsuccess = callback; } set(data, key = 0, callback){ this.objectStore.put(data, key).onsuccess = callback; } get(key = 0, callback){ this.objectStore.get(key).onsuccess = callback; } del(key = 0, callback){ this.objectStore.delete(key).onsuccess = callback; } getAll(callback){ this.objectStore.getAll().onsuccess = callback; } } /* TreeStruct 树结构基类 attribute: parent: TreeStruct; children: Array[TreeStruct]; method: add(v: TreeStruct): v; //v添加到自己的子集 remove(v: TreeStruct): v; //删除v, 前提v必须是自己的子集 export(): Array[Object]; //TreeStruct 转为 可导出的结构, 包括其所有的后代 getPath(v: TreeStruct): Array[TreeStruct]; //获取自己到v的路径 traverse(callback: Function): undefined; //迭代自己的每一个后代, 包括自己 callback(value: TreeStruct); //如返回 "continue" 则不在迭代其后代(不是结束迭代, 而是只结束当前节点的后代); traverseUp(callback): undefined; //向上遍历每一个父, 包括自己 callback(value: TreeStruct); //如返回 "break" 立即停止遍历; static: import(arr: Array[Object]): TreeStruct; //.export() 返回的 arr 转为 TreeStruct */ class TreeStruct{ static import(arr){ //json = JSON.parse(json); const len = arr.length; for(let k = 0, v; k < len; k++){ v = Object.assign(new TreeStruct(), arr[k]); v.parent = arr[arr[k].parent] || null; if(v.parent !== null) v.parent.add(v); arr[k] = v; } return arr[0]; } constructor(){ this.parent = null; this.children = []; } getPath(v){ var path; const pathA = []; this.traverseUp(tar => { if(v === tar){ path = pathA; return "break"; } pathA.push(tar); }); if(path) return path; const pathB = []; v.traverseUp(tar => { if(this === tar){ path = pathB.reverse(); return "break"; } else{ let i = pathA.indexOf(tar); if(i !== -1){ pathA.splice(i); pathA.push(tar); path = pathA.concat(pathB.reverse()); return "break"; } } pathB.push(tar); }); return path; } add(v){ v.parent = this; if(this.children.includes(v) === false) this.children.push(v); return v; } remove(v){ const i = this.children.indexOf(v); if(i !== -1) this.children.splice(i, 1); v.parent = null; return v; } traverse(callback, key = 0){ if(callback(this, key) !== "continue"){ for(let k = 0, len = this.children.length; k < len; k++){ this.children[k].traverse(callback, k); } } } traverseUp(callback){ var par = this.parent; while(par !== null){ if(callback(par) === "break") return; par = par.parent; } } export(){ const result = [], arr = []; var obj = null; this.traverse(v => { obj = Object.assign({}, v); obj.parent = arr.indexOf(v.parent); delete obj.children; result.push(obj); arr.push(v); }); return result; //JSON.stringify(result); } } Object.defineProperties(TreeStruct.prototype, { isTreeStruct: { configurable: false, enumerable: false, writable: false, value: true, } }); /* Point parameter: x = 0, y = 0; attribute x, y: Number; method: set(x, y): this; angle(): Number; copy(point): this; clone(): Point; distance(point): Number; //获取欧几里得距离 distanceMHD(point): Number; //获取曼哈顿距离 distanceCompare(point): Number; //获取用于比较的距离(相对于.distance() 效率更高) equals(point): Bool; //是否恒等 reverse(): this; //取反值 rotate(origin: Object{x,y}, angle): this; //旋转点 normalize(): this; //归一 */ class Point{ constructor(x = 0, y = 0){ this.x = x; this.y = y; } set(x = 0, y = 0){ this.x = x; this.y = y; return this; } angle(){ return Math.atan2(this.y, this.x); } copy(point){ return Object.assign(this, point); } clone(){ return Object.assign(new this.constructor(), this); } distance(point){ return Math.sqrt(Math.pow(point.x - this.x, 2) + Math.pow(point.y - this.y, 2)); } distanceMHD(point){ return Math.abs(this.x - point.x) + Math.abs(this.y - point.y); } distanceCompare(point){ return Math.pow(this.x - point.x, 2) + Math.pow(this.y - point.y, 2); } equals(point){ return point.x === this.x && point.y === this.y; } reverse(){ this.x = -this.x; this.y = -this.y; return this; } rotate(origin, angle){ const c = Math.cos(angle), s = Math.sin(angle), x = this.x - origin.x, y = this.y - origin.y; this.x = x * c - y * s + origin.x; this.y = x * s + y * c + origin.y; return this; } normalize(){ const len = 1 / (Math.sqrt(this.x * this.x + this.y * this.y) || 1); this.x *= len; this.y *= len; return this; } /* add(point){ this.x += point.x; this.y += point.y; return this; } sub(point){ this.x -= point.x; this.y -= point.y; return this; } multiply(point){ this.x *= point.x; this.y *= point.y; return this; } divide(point){ this.x /= point.x; this.y /= point.y; return this; } */ } Object.defineProperties(Point.prototype, { isPoint: { configurable: false, enumerable: false, writable: false, value: true, } }); /* Line parameter: x, y, x1, y1: Number; attribute: x, y, x1, y1: Number; method: set(x, y, x1, y1): this; containsPoint(x, y): Bool; //点是否在线上 intersectPoint(line: Line, point: Point): Point; //如果不相交则返回null, 否则返回交点Point isIntersect(line): Bool; //this与line是否相交 */ class Line{ constructor(x = 0, y = 0, x1 = 0, y1 = 0){ this.x = x; this.y = y; this.x1 = x1; this.y1 = y1; } set(x = 0, y = 0, x1 = 0, y1 = 0){ this.x = x; this.y = y; this.x1 = x1; this.y1 = y1; return this; } containsPoint(x, y){ return (x - this.x) * (x - this.x1) <= 0 && (y - this.y) * (y - this.y1) <= 0; } intersectPoint(line, point){ //解线性方程组, 求线段交点 //如果分母为0则平行或共线, 不相交 var denominator = (this.y1 - this.y) * (line.x1 - line.x) - (this.x - this.x1) * (line.y - line.y1); if(denominator === 0) return null; //线段所在直线的交点坐标 (x , y) const x = ((this.x1 - this.x) * (line.x1 - line.x) * (line.y - this.y) + (this.y1 - this.y) * (line.x1 - line.x) * this.x - (line.y1 - line.y) * (this.x1 - this.x) * line.x) / denominator; const y = -((this.y1 - this.y) * (line.y1 - line.y) * (line.x - this.x) + (this.x1 - this.x) * (line.y1 - line.y) * this.y - (line.x1 - line.x) * (this.y1 - this.y) * line.y) / denominator; //判断交点是否在两条线段上 if(this.containsPoint(x, y) && line.containsPoint(x, y)){ point.x = x; point.y = y; return point; } return null; } isIntersect(line){ //快速排斥: //两个线段为对角线组成的矩形,如果这两个矩形没有重叠的部分,那么两条线段是不可能出现重叠的 //这里的确如此,这一步是判定两矩形是否相交 //1.线段ab的低点低于cd的最高点(可能重合) //2.cd的最左端小于ab的最右端(可能重合) //3.cd的最低点低于ab的最高点(加上条件1,两线段在竖直方向上重合) //4.ab的最左端小于cd的最右端(加上条件2,两直线在水平方向上重合) //综上4个条件,两条线段组成的矩形是重合的 //特别要注意一个矩形含于另一个矩形之内的情况 if(!(Math.min(this.x,this.x1)<=Math.max(line.x,line.x1) && Math.min(line.y,line.y1)<=Math.max(this.y,this.y1) && Math.min(line.x,line.x1)<=Math.max(this.x,this.x1) && Math.min(this.y,this.y1)<=Math.max(line.y,line.y1))) return false; //跨立实验: //如果两条线段相交,那么必须跨立,就是以一条线段为标准,另一条线段的两端点一定在这条线段的两段 //也就是说a b两点在线段cd的两端,c d两点在线段ab的两端 var u=(line.x-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y-this.y), v = (line.x1-this.x)*(this.y1-this.y)-(this.x1-this.x)*(line.y1-this.y), w = (this.x-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y-line.y), z = (this.x1-line.x)*(line.y1-line.y)-(line.x1-line.x)*(this.y1-line.y); return u*v <= 0.00000001 && w*z <= 0.00000001; } } /* ShapeRect (一般2d矩形的原点在左上, 此矩形类的原点在中间) parameter: width = 0, height = 0 attribute: width, height: Number; //矩形的宽高 position: Point; //位置(是旋转和缩放的中心点) rotation: Number; //旋转(绕 Z 轴旋转的弧度) scale: Point; //缩放 method: setFromBox(box): this; //Box 转为 ShapeRect compute(): undefined; //计算出矩形 applyCanvas(context): undefined; //矩形应用到画布的上下文中 demo: const canvasRect = new ShapeRect(100, 150); console.log(canvasRect); const canvas = document.createElement("canvas"); canvas.width = WORLD.width; canvas.height = WORLD.height; canvas.style = ` position: absolute; z-index: 9999; background: rgb(127,127,127); `; document.body.appendChild(canvas); canvasRect.position.set(300, 300); canvasRect.rotation = UTILS.toAngle(45); canvasRect.scale.set(1.5, 1.5); canvasRect.compute(); const context = canvas.getContext("2d"); canvasRect.applyCanvas(context); context.strokeStyle = "red"; context.stroke(); */ class ShapeRect{ #dots = []; constructor(width = 0, height = 0){ this.width = width; this.height = height; this.position = new Point(); this.rotation = 0; this.scale = new Point(1, 1); } setFromBox(box){ this.width = box.w; this.height = box.h; this.position.set(box.cx, box.cy); return this; } compute(){ //scale const width = this.width * this.scale.x, height = this.height * this.scale.y, //position minX = this.position.x - width / 2, minY = this.position.y - height / 2, maxX = minX + width, maxY = minY + height, //rotation point = UTILS.emptyPoint; this.#dots.length = 0; point.set(minX, minY).rotate(this.position, this.rotation); this.#dots.push(point.x, point.y); point.set(maxX, minY).rotate(this.position, this.rotation); this.#dots.push(point.x, point.y); point.set(maxX, maxY).rotate(this.position, this.rotation); this.#dots.push(point.x, point.y); point.set(minX, maxY).rotate(this.position, this.rotation); this.#dots.push(point.x, point.y); } applyCanvas(context){ context.beginPath(); context.moveTo(this.#dots[0], this.#dots[1]); for(let k = 2, len = this.#dots.length; k < len; k += 2){ context.lineTo(this.#dots[k], this.#dots[k + 1]); } context.closePath(); } } /* Box 矩形 parameter: x = 0, y = 0, w = 0, h = 0; attribute: x,y: Number; 位置 w,h: Number; 大小 只读 mx, my: Number; // method: set(x, y, w, h): this; pos(x, y): this; //设置位置 size(w, h): this; //设置大小 setFromShapeRect(shapeRect): this; //ShapeRect 转为 Box (忽略 ShapeRect 的旋转和缩放) setFromCircle(circle, inner: Bool): this; // toArray(array: Array, index: Integer): this; copy(box): this; //复制 clone(): Box; //克隆 center(box): this; //设置位置在box居中 distance(x, y): Number; //左上角原点 与 x,y 的直线距离 isEmpty(): Boolean; //.w.h是否小于等于零 maxX(): Number; //返回 max x(this.x+this.w); maxY(): Number; //返回 max y(this.y+this.h); expand(box): undefined; //扩容; 把box合并到this equals(box): Boolean; //this与box是否恒等 intersectsBox(box): Boolean; //box与this是否相交(box在this内部也会返回true) containsPoint(x, y): Boolean; //x,y点是否在this内 containsBox(box): Boolean; //box是否在this内(只是相交返回fasle) computeOverflow(b: Box, r: Box): undefined; //this相对b超出的部分赋值到r; r的size小于或等于0的话说明完全超出 */ class Box{ get mx(){ return this.x + this.w; } get my(){ return this.y + this.h; } get cx(){ return this.w / 2 + this.x; } get cy(){ return this.h / 2 + this.y; } constructor(x = 0, y = 0, w = 0, h = 0){ //this.set(x, y, w, h); this.x = x; this.y = y; this.w = w; this.h = h; } set(x, y, w, h){ this.x = x; this.y = y; this.w = w; this.h = h; return this; } pos(x, y){ this.x = x; this.y = y; return this; } size(w, h){ this.w = w; this.h = h; return this; } setFromShapeRect(shapeRect){ this.width = shapeRect.width; this.height = shapeRect.height; this.x = shapeRect.position.x - this.width / 2; this.y = shapeRect.position.y - this.height / 2; return this; } setFromCircle(circle, inner = true){ if(inner === true){ this.x = Math.sin(-135 / 180 * Math.PI) * circle.r + circle.x; this.y = Math.cos(-135 / 180 * Math.PI) * circle.r + circle.y; this.w = this.h = Math.sin(135 / 180 * Math.PI) * circle.r + circle.x - this.x; } else{ this.x = circle.x - circle.r; this.y = circle.y - circle.r; this.w = this.h = circle.r * 2; } return this; } setFromPolygon(polygon, inner = true){ if(inner === true){ console.warn('Box: 暂不支持第二个参数为true'); } else{ const len = polygon.path.length; let x = Infinity, y = Infinity, mx = 0, my = 0; for(let k = 0, v; k < len; k+=2){ v = polygon.path[k]; if(v < x) x = v; else if(v > mx) mx = v; v = polygon.path[k+1]; if(v < y) y = v; else if(v > my) my = v; } this.set(x, y, mx - x, my - y); } return this; } toArray(array, index){ array[index] = this.x; array[index+1] = this.y; array[index+2] = this.w; array[index+3] = this.h; return this; } copy(box){ /* this.x = box.x; this.y = box.y; this.w = box.w; this.h = box.h; */ return Object.assign(this, box); //this.set(box.x, box.y, box.w, box.h); } clone(){ //return new this.constructor().copy(this); return Object.assign(new this.constructor(), this); } center(box){ this.x = (box.w - this.w) / 2 + box.x; this.y = (box.h - this.h) / 2 + box.y; return this; } distance(x, y){ return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); } isEmpty(){ return this.w <= 0 || this.h <= 0; } maxX(){ return this.x + this.w; } maxY(){ return this.y + this.h; } equals(box){ return this.x === box.x && this.w === box.w && this.y === box.y && this.h === box.h; } expand(box){ var v = Math.min(this.x, box.x); this.w = Math.max(this.x + this.w - v, box.x + box.w - v); this.x = v; v = Math.min(this.y, box.y); this.h = Math.max(this.y + this.h - v, box.y + box.h - v); this.y = v; } intersectsBox(box){ return box.x + box.w < this.x || box.x > this.x + this.w || box.y + box.h < this.y || box.y > this.y + this.h ? false : true; } containsPoint(x, y){ return x < this.x || x > this.x + this.w || y < this.y || y > this.y + this.h ? false : true; } containsBox(box){ return this.x <= box.x && box.x + box.w <= this.x + this.w && this.y <= box.y && box.y + box.h <= this.y + this.h; } computeOverflow(p, r){ r["copy"](this); if(this["x"] < p["x"]){ r["x"] = p["x"]; r["w"] -= p["x"] - this["x"]; } if(this["y"] < p["y"]){ r["y"] = p["y"]; r["h"] -= p["y"] - this["y"]; } var m = p["x"] + p["w"]; if(r["x"] + r["w"] > m) r["w"] = m - r["x"]; m = p["y"] + p["h"]; if(r["y"] + r["h"] > m) r["h"] = m - r["y"]; } } Object.defineProperties(Box.prototype, { isBox: { configurable: false, enumerable: false, writable: false, value: true, }, }); /* Circle 圆形 parameter: attribute: x,y: Number; 中心点 r: Number; 半径 //只读 r2: Number; //返回直径 r*2 method: set(x, y, r): this; pos(x, y): this; copy(circle: Circle): this; clone(): Circle; distance(x, y): Number; equals(circle: Circle): Bool; containsPoint(x, y): Bool; intersectsCircle(circle: Circle): Bool; intersectsBox(box: Box): Bool; setFromBox(box, inner = true): this; */ class Circle{ get r2(){ return this.r * 2; } constructor(x = 0, y = 0, r = -1){ //this.set(0, 0, -1); this.x = x; this.y = y; this.r = r; } set(x, y, r){ this.x = x; this.y = y; this.r = r; return this; } setFromBox(box, world = true, inner = true){ this.x = box.w / 2 + (world === true ? box.x : 0); this.y = box.h / 2 + (world === true ? box.y : 0); this.r = inner === true ? Math.min(box.w, box.h) / 2 : UTILS.distance(0, 0, box.w, box.h) / 2; return this; } toArray(array, index){ array[index] = this.x; array[index+1] = this.y; array[index+2] = this.r; return this; } pos(x, y){ this.x = x; this.y = y; return this; } copy(circle){ this.r = circle.r; this.x = circle.x; this.y = circle.y; return this; } clone(){ return new this.constructor().copy(this); } distance(x, y){ return Math.sqrt(Math.pow(this.x - x, 2) + Math.pow(this.y - y, 2)); } equals(circle){ return circle.x === this.x && circle.y === this.y && circle.r === this.r; } containsPoint(x, y){ return (Math.pow(x - this.x, 2) + Math.pow(y - this.y, 2) <= Math.pow(this.r, 2)); } intersectsCircle(circle){ return (Math.pow(circle.x - this.x, 2) + Math.pow(circle.y - this.y, 2) <= Math.pow(circle.r + this.r, 2)); } intersectsBox(box){ return (Math.pow(Math.max(box.x, Math.min(box.x + box.w, this.x)) - this.x, 2) + Math.pow(Math.max(box.y, Math.min(box.y + box.h, this.y)) - this.y, 2) <= Math.pow(this.r, 2)); } } Object.defineProperties(Circle.prototype, { isCircle: { configurable: false, enumerable: false, writable: false, value: true, } }); /* Polygon 多边形 parameter: path: Array[x, y]; attribute: //只读属性 path: Array[x, y]; method: add(x, y): this; //x,y添加至path; containsPoint(x, y): Bool; //x,y是否在多边形的内部(注意: 在路径上也返回 true) */ class Polygon{ #position = null; #path2D = null; get path(){ return this.#position; } constructor(path = []){ this.#position = path; this.#path2D = new Path2D(); var len = path.length; if(len >= 2){ if(len % 2 !== 0){ len -= 1; path.splice(len, 1); } const con = this.#path2D; con.moveTo(path[0], path[1]); for(let k = 2; k < len; k+=2) con.lineTo(path[k], path[k+1]); } } add(x, y){ this.#position.push(x, y); this.#path2D.lineTo(x, y); return this; } containsPoint(x, y){ return UTILS.emptyContext.isPointInPath(this.#path2D, x, y); } isInPolygon(checkPoint, polygonPoints) { var counter = 0; var i; var xinters; var p1, p2; var pointCount = polygonPoints.length; p1 = polygonPoints[0]; for (i = 1; i <= pointCount; i++) { p2 = polygonPoints[i % pointCount]; if ( checkPoint[0] > Math.min(p1[0], p2[0]) && checkPoint[0] <= Math.max(p1[0], p2[0]) ) { if (checkPoint[1] <= Math.max(p1[1], p2[1])) { if (p1[0] != p2[0]) { xinters = (checkPoint[0] - p1[0]) * (p2[1] - p1[1]) / (p2[0] - p1[0]) + p1[1]; if (p1[1] == p2[1] || checkPoint[1] <= xinters) { counter++; } } } } p1 = p2; } if (counter % 2 == 0) { return false; } else { return true; } } containsPolygon(polygon){ const path = polygon.path, len = path.length; for(let k = 0; k < len; k += 2){ if(this.containsPoint(path[k], path[k+1]) === false) return false; } return true; } toPoints(){ const path = this.path, len = path.length, result = []; for(let k = 0; k < len; k += 2) result.push(new Point(path[k], path[k+1])); return result; } toLines(){ const path = this.path, len = path.length, result = []; for(let k = 0, x = NaN, y; k < len; k += 2){ if(isNaN(x)){ x = path[k]; y = path[k+1]; continue; } const line = new Line(x, y, path[k], path[k+1]); x = line.x1; y = line.y1; result.push(line); } return result; } merge(polygon){ const linesA = this.toLines(), linesB = polygon.toLines(), nodes = [], newLines = [], pointA = new Point(), pointB = new Point(), forEachNodes = (pathA, pathB, funcA = null, funcB = null) => { for(let k = 0, lenA = pathA.length, lenB = pathB.length; k < lenA; k++){ if(funcA !== null) funcA(pathA[k]); for(let i = 0; i < lenB; i++){ if(funcB !== null) funcB(pathB[i], pathA[k]); } } } if(this.containsPolygon(polygon)){console.log('this -> polygon'); forEachNodes(linesA, linesB, lineA => newLines.push(lineA), (lineB, lineA) => { if(lineA.intersectPoint(lineB, pointA) === pointA) newLines.push(pointA.clone()); }); return newLines; } //收集所有的交点 (保存至 line) forEachNodes(linesA, linesB, lineA => lineA.nodes = [], (lineB, lineA) => { if(lineB.nodes === undefined) lineB.nodes = []; if(lineA.intersectPoint(lineB, pointA) === pointA){ const node = { lineA: lineA, lineB: lineB, point: pointA.clone(), disA: pointA.distanceCompare(pointB.set(lineA.x, lineA.y)), disB: pointA.distanceCompare(pointB.set(lineB.x, lineB.y)), } lineA.nodes.push(node); lineB.nodes.push(node); nodes.push(node); } }); //交点以原点为目标排序 for(let k = 0, sotr = function (a,b){return a.disA - b.disA;}, countA = linesA.length; k < countA; k++) linesA[k].nodes.sort(sotr); for(let k = 0, sotr = function (a,b){return a.disB - b.disB;}, countB = linesB.length; k < countB; k++) linesB[k].nodes.sort(sotr); var _loopTypeA, _loopTypeB; const result_loop = { lines: null, loopType: '', line: null, count: 0, indexed: 0, }, //遍历某条线 loop = (lines, index, loopType) => { const length = lines.length, indexed = lines.length, model = lines === linesA ? polygon : this; var line, i = 1; while(true){ if(loopType === 'next') index = index === length - 1 ? 0 : index + 1; else if(loopType === 'back') index = index === 0 ? length - 1 : index - 1; line = lines[index]; result_loop.count = line.nodes.length; if(result_loop.count !== 0){ result_loop.lines = lines; result_loop.loopType = loopType; result_loop.line = line; result_loop.indexed = index; if(loopType === 'next') addLine(line, model); return result_loop; } addLine(line, model); if(indexed === i++) break; } }, //更新或创建交点的索引 setNodeIndex = (lines, index, loopType) => { const line = lines[index], count = line.nodes.length; if(loopType === undefined) loopType = lines === linesA ? _loopTypeA : _loopTypeB; if(loopType === undefined) return; if(line.nodeIndex === undefined){ line.nodeIndex = loopType === 'next' ? 0 : count - 1; line.nodeState = count === 1 ? 'end' : 'start'; } else{ if(line.nodeState === 'end' || line.nodeState === ''){ line.nodeState = ''; return; } if(loopType === 'next'){ line.nodeIndex += 1; if(line.nodeIndex === count - 1) line.nodeState = 'end'; else line.nodeState = 'run'; } else if(loopType === 'back'){ line.nodeIndex -= 1; if(line.nodeIndex === 0) line.nodeState = 'end'; else line.nodeState = 'run'; } } }, //只有在跳线的时候才执行此方法, 如果跳线的话: 某条线上的交点必然在两端; getLoopType = (lines, index, nodePoint) => { const line = lines[index], lineNext = lines[index === lines.length - 1 ? 0 : index + 1], model = lines === linesA ? polygon : this, isLineBack = newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false, isLineNext = newLines.includes(lineNext) === false && model.containsPoint(lineNext.x, lineNext.y) === false; if(isLineBack && isLineNext){ const len = line.nodes.length; if(len >= 2){ if(line.nodes[len - 1].point.equals(nodePoint)) return 'next'; else if(line.nodes[0].point.equals(nodePoint)) return 'back'; } else console.warn('路径复杂', line); } else if(isLineNext){ return 'next'; } else if(isLineBack){ return 'back'; } return ''; }, //添加线至新的形状数组 addLine = (line, model) => { //if(newLines.includes(line) === false && model.containsPoint(line.x, line.y) === false) newLines.push(line); if(line.nodes.length === 0) newLines.push(line); else if(model.containsPoint(line.x, line.y) === false) newLines.push(line); }, //处理拥有交点的线 computeNodes = v => { if(v === undefined || v.count === 0) return; setNodeIndex(v.lines, v.indexed, v.loopType); //添加交点 const node = v.line.nodes[v.line.nodeIndex]; if(newLines.includes(node.point) === false) newLines.push(node.point); else return; var lines = v.lines === linesA ? linesB : linesA, line = lines === linesA ? node.lineA : node.lineB, index = lines.indexOf(line); setNodeIndex(lines, index); //选择交点状态 var nodeState = line.nodeState !== undefined ? line.nodeState : v.line.nodeState; if((v.count <= 2 && line.nodes.length > 2) || (v.count > 2 && line.nodes.length <= 2)){ if(line.nodeState === 'start'){ const backLine = v.loopType === 'next' ? v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1] : v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1]; if(newLines.includes(backLine) && backLine.nodes.length === 0){ nodeState = 'run'; } } else if(line.nodeState === 'end'){ const nextLine = v.loopType === 'next' ? v.lines[v.indexed === v.lines.length-1 ? 0 : v.indexed+1] : v.lines[v.indexed === 0 ? v.lines.length-1 : v.indexed-1]; const model = v.lines === linesA ? polygon : this; if(model.containsPoint(nextLine.x, nextLine.y) === false && nextLine.nodes.length === 0){ nodeState = 'run'; } } } switch(nodeState){ //不跳线 case 'run': if(v.loopType === 'back') addLine(v.line, v.lines === linesA ? polygon : this); return computeNodes(loop(v.lines, v.indexed, v.loopType)); //跳线 case 'start': case 'end': const loopType = getLoopType(lines, index, node.point); if(loopType !== ''){ if(lines === linesA) _loopTypeA = loopType; else _loopTypeB = loopType; if(loopType === 'back') addLine(line, v.lines === linesA ? this : polygon); return computeNodes(loop(lines, index, loopType)); } break; } } //获取介入点 var startLine = null; for(let k = 0, len = nodes.length, node; k < len; k++){ node = nodes[k]; if(node.lineA.nodes.length !== 0){ startLine = node.lineA; if(node.lineA.nodes.length === 1 && node.lineB.nodes.length > 1){ startLine = node.lineB.nodes[0].lineB; result_loop.lines = linesB; result_loop.loopType = _loopTypeB = 'next'; } else{ result_loop.lines = linesA; result_loop.loopType = _loopTypeA = 'next'; } result_loop.line = startLine; result_loop.count = startLine.nodes.length; result_loop.indexed = result_loop.lines.indexOf(startLine); break; } } if(startLine === null){ console.warn('Polygon: 找不到介入点, 终止了合并'); return newLines; } computeNodes(result_loop); return newLines; } } /* RGBColor parameter: r, g, b method: set(r, g, b: Number): this; //rgb: 0 - 255; 第一个参数可以为 css color setFormHex(hex: Number): this; // setFormHSV(h, s, v: Number): this; //h:0-360; s,v:0-100; 颜色, 明度, 暗度 setFormString(str: String): Number; //str: 英文|css color; 返回的是透明度 (如果为 rgba 则返回a; 否则总是返回1) copy(v: RGBColor): this; clone(): RGBColor; getHex(): Number; getHexString(): String; getHSV(result: Object{h, s, v}): result; //result: 默认是一个新的Object getRGBA(alpha: Number): String; //alpha: 0 - 1; 默认 1 getStyle() //.getRGBA()别名 stringToColor(str: String): String; //str 转为 css color; 如果str不是color格式则返回 "" */ class RGBColor{ constructor(r = 255, g = 255, b = 255){ this.r = r; this.g = g; this.b = b; } copy(v){ this.r = v.r; this.g = v.g; this.b = v.b; return this; } clone(){ return new this.constructor().copy(this); } set(r, g, b){ if(typeof r !== "string"){ this.r = r || 255; this.g = g || 255; this.b = b || 255; } else this.setFormString(r); return this; } setFormHex(hex){ hex = Math.floor( hex ); this.r = hex >> 16 & 255; this.g = hex >> 8 & 255; this.b = hex & 255; return this; } setFormHSV(h, s, v){ h = h >= 360 ? 0 : h; var s=s/100; var v=v/100; var h1=Math.floor(h/60) % 6; var f=h/60-h1; var p=v*(1-s); var q=v*(1-f*s); var t=v*(1-(1-f)*s); var r,g,b; switch(h1){ case 0: r=v; g=t; b=p; break; case 1: r=q; g=v; b=p; break; case 2: r=p; g=v; b=t; break; case 3: r=p; g=q; b=v; break; case 4: r=t; g=p; b=v; break; case 5: r=v; g=p; b=q; break; } this.r = Math.round(r*255); this.g = Math.round(g*255); this.b = Math.round(b*255); return this; } setFormString(color){ if(typeof color !== "string") return 1; var _color = this.stringToColor(color); if(_color[0] === "#"){ const len = _color.length; if(len === 4){ _color = _color.slice(1); this.setFormHex(parseInt("0x"+_color + "" + _color)); } else if(len === 7) this.setFormHex(parseInt("0x"+_color.slice(1))); } else if(_color[0] === "r" && _color[1] === "g" && _color[2] === "b"){ const arr = []; for(let k = 0, len = _color.length, v = "", is = false; k < len; k++){ if(is === true){ if(_color[k] === "," || _color[k] === ")"){ arr.push(parseFloat(v)); v = ""; } else v += _color[k]; } else if(_color[k] === "(") is = true; } this.set(arr[0], arr[1], arr[2]); return arr[3] === undefined ? 1 : arr[3]; } return 1; } getHex(){ return Math.max( 0, Math.min( 255, this.r ) ) << 16 ^ Math.max( 0, Math.min( 255, this.g ) ) << 8 ^ Math.max( 0, Math.min( 255, this.b ) ) << 0; } getHexString(){ return '#' + ( '000000' + this.getHex().toString( 16 ) ).slice( - 6 ); } getHSV(result){ result = result || {} var r=this.r/255; var g=this.g/255; var b=this.b/255; var h,s,v; var min=Math.min(r,g,b); var max=v=Math.max(r,g,b); var l=(min+max)/2; var difference = max-min; if(max==min){ h=0; }else{ switch(max){ case r: h=(g-b)/difference+(g < b ? 6 : 0);break; case g: h=2.0+(b-r)/difference;break; case b: h=4.0+(r-g)/difference;break; } h=Math.round(h*60); } if(max==0){ s=0; }else{ s=1-min/max; } s=Math.round(s*100); v=Math.round(v*100); result.h = h; result.s = s; result.v = v; return result; } getStyle(){ return this.getRGBA(1); } getRGBA(alpha){ alpha = typeof alpha === 'number' ? alpha : 1; return 'rgba('+this.r+','+this.g+','+this.b+','+alpha+')'; } stringToColor(str){ var _color = ""; for(let k = 0, len = str.length; k < len; k++){ if(str[k] === " ") continue; _color += str[k]; } if(_color[0] === "#" || (_color[0] === "r" && _color[1] === "g" && _color[2] === "b")) return _color; else{ for(let k = 0, len = ColorRefTable.length; k < len; k++){ str = ColorRefTable[k]; if(str[0] === _color) return str[1]; } } return ""; } } Object.defineProperties(RGBColor.prototype, { isRGBColor: { configurable: false, enumerable: false, writable: false, value: true, } }); /* Timer 定时器 parameter: func: Function; //定时器运行时的回调; 默认 null, 如果定义构造器将自动调用一次.restart()方法 speed: Number; //延迟多少毫秒执行一次 func; 默认 3000; step: Integer; //执行多少次: 延迟speed毫秒执行一次 func; 默认 Infinity; attribute: func, speed, step; //这些属性可以随时更改; //只读属性 readyState: String; //定时器状态; 可能值: '', 'start', 'running', 'done'; ''表示定时器从未启动 number: Number; //运行的次数 method: start(func, speed): this; //启动定时器 (如果定时器正在运行则什么都不会做) restart(): undefined; //重启定时器 stop(): undefined; //停止定时器 demo: //每 3000 毫秒 打印一次 timer.number, 10次后停止 new Timer(timer => { console.log(timer.number); if(timer.number === 10) timer.stop(); }, 3000); */ class Timer{ #restart = -1; #speed = 0; #isRun = false; #i = 0; #readyState = ''; //start|running get number(){ return this.#i; } get readyState(){ return this.#i >= this.step ? 'done' : this.#readyState; } get running(){ return this.#isRun; } constructor(func = null, speed = 3000, step = Infinity){ this.func = func; this.speed = speed; this.step = step; //this.onDone = null; if(typeof this.func === "function") this.restart(); } start(func, time){ if(typeof func === 'function') this.func = func; if(UTILS.isNumber(time) === true) this.speed = time; this.restart(); return this; } restart(){ if(this.#isRun === false){ setTimeout(this._loop, this.speed); this.#isRun = true; this.#restart = -1; this.#i = 0; this.#readyState = 'start'; } else{ this.#restart = Date.now(); this.#speed = this.speed; } } stop(){ if(this.#isRun === true){ this.#restart = -1; this.#i = this.step; } } _loop = () => { //重启计时器 if(this.#restart !== -1){ let gone = Date.now() - this.#restart; this.#restart = -1; if(gone >= this.#speed) gone = this.speed; else{ if(this.#speed === this.speed) gone = this.#speed - gone; else gone = (this.#speed - gone) / this.#speed * this.speed; } setTimeout(this._loop, gone); this.#i = 1; if(this.func !== null) this.func(this); } //正在运行 else if(this.#i < this.step){ setTimeout(this._loop, this.speed); this.#i++; if(this.#readyState !== 'running') this.#readyState = 'running'; if(this.func !== null) this.func(this); } //完成 else this.#isRun = false; } } /* SeekPath A*寻路 parameter: option: Object{ angle: Number, //8 || 16 timeout: Number, //单位为毫秒 size: Number, //每格的宽高 lenX, lenY: Number, //长度 disables: Array[0||1], heights: Array[Number], path: Array[], //存放寻路结果 默认创建一个空数组 } attribute: size: Number; //每个索引的大小 lenX: Number; //最大长度x (设置此属性时, 你需要重新.initMap(heights);) lenY: Number; //最大长度y (设置此属性时, 你需要重新.initMap(heights);) //此属性已废弃 range: Box; //本次的搜索范围, 默认: 0,0,lenX,lenY angle: Number; //8四方向 或 16八方向 默认 16 timeout: Number; //超时毫秒 默认 500 mapRange: Box; //地图box //此属性已废弃(run函数不在检测相邻的高) maxHeight: Number; //相邻可走的最大高 默认 6 //只读属性 success: Bool; //只读; 本次搜索是否成功找到终点; (如果为false说明.run()返回的是 距终点最近的路径; 超时也会被判定为false) path: Array[x, y, z]; //存放.run()返回的路径 map: Map; //地图的缓存数据 method: initMap(heights: Array[Number]): undefiend; //初始化类时自动调用一次; heights:如果你的场景存在高请定义此参数 run(x, y, x1, y1: Number): Array[x, y, z]; //参数索引坐标 getDots(x, y, a, r): Array[ix, iy]; //获取周围的点 x,y, a:8|16, r:存放结果数组 getLegalPoints(ix, iy, count): Array[x, y, z]; //获取 ix, iy 周围 合法的, 相邻的 count 个点 demo: const sp = new SeekPath({ angle: 16, timeout: 500, //maxHeight: 6, size: 10, lenX: 1000, lenY: 1000, }), path = sp.run(0, 0, 1000, 1000); console.log(sp); */ class SeekPath{ static _open = [] static _dots = [] //.run() .getLegalPoints() static dots4 = []; //._check() static _sort = function (a, b){return a["f"] - b["f"];} #map = null; #path = null; #success = true; #halfX = 50; #halfY = 50; #size = 10; #lenX = 10; #lenY = 10; constructor(option = {}){ this.angle = (option.angle === 8 || option.angle === 16) ? option.angle : 16; //8四方向 或 16八方向 this.timeout = option.timeout || 500; //超时毫秒 //this.maxHeight = option.maxHeight || 6; this.mapRange = new Box(); this.size = option.size || 10; this.lenX = option.lenX || 10; this.lenY = option.lenY || 10; this.#path = Array.isArray(option.path) ? option.path : []; this.initMap(option.disable, option.height); option = undefined } //this.#map[x][y] = Object{height: 此点的高,默认0, is: 此点是否可走,默认1(disable)}; get map(){ return this.#map; } //this.#path = Array[x,y,z] get path(){ return this.#path; } get success(){ return this.#success; } get size(){ return this.#size; } set size(v){ this.#size = v; v = v / 2; this.#halfX = v * this.#lenX; this.#halfY = v * this.#lenY; } get lenX(){ return this.#lenX; } set lenX(v){ this.#lenX = v; v = this.#size / 2; this.#halfX = v * this.#lenX; this.#halfY = v * this.#lenY; } get lenY(){ return this.#lenY; } set lenY(v){ this.#lenY = v; v = this.#size / 2; this.#halfX = v * this.#lenX; this.#halfY = v * this.#lenY; } toScene(n, v){ //n = "x|y" //n = n === "y" ? "lenY" : "lenX"; if(n === "y" || n === "z") return v * this.#size - this.#halfY; return v * this.#size - this.#halfX; } toIndex(n, v){ //n = n === "y" ? "lenY" : "lenX"; if(n === "y" || n === "z") return Math.round((this.#halfY + v) / this.#size); return Math.round((this.#halfX + v) / this.#size); } initMap(disable, height){ disable = Array.isArray(disable) === true ? disable : null; height = Array.isArray(height) === true ? height : null; const lenX = this.lenX, lenY = this.lenY; var getHeight = (ix, iy) => { if(height === null) return 0; ix = height[ix * lenY + iy]; if(ix === undefined) return 0; return ix; }, getDisable = (ix, iy) => { if(disable === null) return 1; ix = disable[ix * lenY + iy]; if(ix === undefined) return 0; return ix; }, map = []//new Map(); for(let x = 0, y, m; x < lenX; x++){ m = []//new Map(); for(y = 0; y < lenY; y++) m[y] = {x:x, y:y, height:getHeight(x, y), is:getDisable(x, y), g:0, h:0, f:0, p:null, id:""}//m.set(y, {x:x, y:y, height:getHeight(x, y), g:0, h:0, f:0, p:null, id:""}); map[x] = m;//map.set(x, m); } this.#map = map; this._id = -1; this._updateID(); this.mapRange.set(0, 0, this.#lenX-1, this.#lenY-1); map = disable = height = getHeight = undefined; } getLegalPoints(ix, iy, count, result = []){ const _dots = SeekPath._dots; result.length = 0; result[0] = this.#map[ix][iy]; count += 1; while(result.length < count){ for(let k = 0, i, n, d, len = result.length; k < len; k++){ n = result[k]; this.getDots(n.x, n.y, this.angle, _dots); for(i = 0; i < this.angle; i += 2){ d = this.#map[_dots[i]][_dots[i+1]]; if(d.is === 1 && this.mapRange.containsPoint(d.x, d.y) && !result.includes(d)){ if(Math.abs(n.x - d.x) + Math.abs(n.y - d.y) === 2 && this._check(n, d)){ result.push(d); } } } } } result.splice(0, 1); return result; } getLinePoints(now, next, count, result = []){ if(count === 1) return result[0] = next; if(count % 2 === 0) count += 1; const len = Math.floor(count / 2), nowPoint = UTILS.emptyPoint, angle90 = UTILS.toAngle(90); var i, ix, iy, n, nn = next; nowPoint.set(now.x, now.y).rotate(next, angle90); //now 以 next 为原点顺时针旋转 90 度 var disX = nowPoint.x - next.x, disY = nowPoint.y - next.y; for(i = 0; i < len; i++){ ix = disX + disX * i + next.x; iy = disY + disY * i + next.y; n = this.#map[ix][iy]; if(n.is === 1) nn = n; result[len-1-i] = nn; } result[len] = next; nn = next nowPoint.set(now.x, now.y).rotate(next, -angle90); disX = nowPoint.x - next.x; disY = nowPoint.y - next.y; for(i = 0; i < len; i++){ ix = disX + disX * i + next.x; iy = disY + disY * i + next.y; n = this.#map[ix][iy]; if(n.is === 1) nn = n; result[len+1+i] = nn; } return result; } getDots(x, y, a, r = []){ //获取周围的点 x,y, a:8|16, r:存放结果数组 r.length = 0; const x_1 = x-1, x1 = x+1, y_1 = y-1, y1 = y+1; if(a === 16) r.push(x_1, y_1, x, y_1, x1, y_1, x_1, y, x1, y, x_1, y1, x, y1, x1, y1); else r.push(x, y_1, x, y1, x_1, y, x1, y); } _updateID(){ //更新标记 this._id++; this._openID = "o_"+this._id; this._closeID = "c_"+this._id; } _check(dotA, dotB){ //检测 a 是否能到 b //获取 dotB 周围的4个点 并 遍历这4个点 this.getDots(dotB.x, dotB.y, 8, SeekPath.dots4); for(let k = 0, x, y; k < 8; k += 2){ x = SeekPath.dots4[k]; y = SeekPath.dots4[k+1]; if(this.mapRange.containsPoint(x, y) === false) continue; //找出 dotA 与 dotB 相交的两个点: if(Math.abs(dotA.x - x) + Math.abs(dotA.y - y) === 1){ //如果其中一个交点是不可走的则 dotA 到 dotB 不可走, 既返回 false if(this.#map[x][y].is === 0) return false; } } return true; } run(x, y, x1, y1, path = this.#path){ path.length = 0; if(this.#map === null || this.mapRange.containsPoint(x, y) === false) return path; var _n = this.#map[x][y]; if(_n.is === 0) return path; const _sort = SeekPath._sort, _open = SeekPath._open, _dots = SeekPath._dots, time = Date.now(); //var isDot = true, var suc = _n, k, mhd, g, h, f, _d; _n.g = 0; _n.h = _n.h = Math.abs(x1 - x) * 10 + Math.abs(y1 - y) * 10; _n.f = _n.h; _n.p = null; this._updateID(); _n.id = this._openID; _open.push(_n); while(_open.length !== 0){ if(Date.now() - time > this.timeout) break; _open.sort(_sort); _n = _open.shift(); if(_n.x === x1 && _n.y === y1){ suc = _n; break; } if(suc.h > _n.h) suc = _n; _n.id = this._closeID; this.getDots(_n.x, _n.y, this.angle, _dots); for(k = 0; k < this.angle; k += 2){ _d = this.#map[_dots[k]][_dots[k+1]]; if(_d.id === this._closeID || _d.is === 0 || this.mapRange.containsPoint(_d.x, _d.y) === false) continue; mhd = Math["abs"](_n["x"] - _d.x) + Math["abs"](_n["y"] - _d.y); g = _n["g"] + (mhd === 1 ? 10 : 14); h = Math["abs"](x1 - _d.x) * 10 + Math["abs"](y1 - _d.y) * 10; f = g + h; if(_d.id !== this._openID){ //如果是斜角和8方向: if(mhd !== 1 && this.angle === 16){ if(this._check(_n, _d)){ _d.g = g; _d.h = h; _d.f = f; _d.p = _n; _d.id = this._openID; _open.push(_d); } }else{ _d.g = g; _d.h = h; _d.f = f; _d.p = _n; _d.id = this._openID; _open.push(_d); } } else if(g < _d.g){ _d.g = g; _d.f = g + _d.h; _d.p = _n; } } } this.#success = suc === _n; while(suc !== null){ path.unshift(suc); //path.unshift(this.toScene("x", suc["x"]), suc["height"], this.toScene("y", suc["y"])); suc = suc["p"]; } _open.length = _dots.length = 0; return path; } } /* RunningList 如果触发过程中删除(回调函数中删除正在遍历的数组), 不仅 len 没有变(遍历前定义的len没有变, 真实的len随之减少), 而且还会漏掉一个key; */ class RunningList{ /* static getProxy(runName){ return new Proxy(new RunningList(runName), { get(tar, key){ }, set(tar, key, val){ } }); } */ constructor(runName = 'update'){ this._running = false; this._list = []; this._delList = []; this._runName = runName; } get length(){ return this._list.length; } add(v){ if(!this._list.includes(v)) this._list.push(v); } clear(){ this._list.length = 0; this._delList.length = 0; } push(...v){ v.forEach(_v => this._list.push(_v)); } splice(v){ if(this._running === true){ if(!this._delList.includes(v)) this._delList.push(v); } else{ const i = this._list.indexOf(v); if(i !== -1) this._list.splice(i, 1); } } update(){ var k, len = this._list.length; this._running = true; if(this._runName !== ''){ for(k = 0; k < len; k++) this._list[k][this._runName](); }else{ for(k = 0; k < len; k++) this._list[k](); } this._running = false; var i; len = this._delList.length; for(k = 0; k < len; k++){ //this.splice(this._delList[k]); i = this._list.indexOf(this._delList[k]); if(i !== -1) this._list.splice(i, 1); } this._delList.length = 0; } } /* TweenValue (从 原点 以规定的时间到达 终点) parameter: origin, end, time, onUpdate, onEnd; attribute: origin: Object; //原点(起点) end: Object; //终点 time: Number; //origin 到 end 花费的时间 onUpdate: Function; //更新回调; 一个回调参数 origin; 默认null; onEnd: Function; //结束回调; 没有回调参数; 默认null; (如果返回的是"restart"将不从队列删除, 你可以在onEnd中更新.end不间断的继续补间) method: reset(origin, end: Object): undefined; //更换 .origin, .end; 它会清除其它对象的缓存属性 reverse(): undefined; //this.end 复制 this.origin 的原始值 update(): undefined; //Tween 通过此方法统一更新 TweenValue demo: //init Tween: const tween = new Tween(), animate = function (){ requestAnimationFrame(animate); tween.update(); } //init TweenValue: const v1 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v)); animate(); tween.start(v1); */ class TweenValue{ constructor(origin = {}, end = {}, time = 500, onUpdate = null, onEnd = null, onStart = null){ this.origin = origin; this.end = end; this.time = time; this.onUpdate = onUpdate; this.onEnd = onEnd; this.onStart = onStart; //以下属性不能直接设置 this._r = null; this._t = 0; this._v = Object.create(null); } _start(){ var v = ""; for(v in this.origin) this._v[v] = this.origin[v]; this._t = Date.now(); //this.update(); } reset(origin, end){ this.origin = origin; this.end = end; this._v = Object.create(null); } reverse(){ var n = ""; for(n in this.origin) this.end[n] = this._v[n]; } update(){ if(this["_r"] !== null){ var ted = Date["now"]() - this["_t"]; if(ted >= this["time"]){ for(ted in this["origin"]) this["origin"][ted] = this["end"][ted]; if(this["onEnd"] !== null){ if(this["onEnd"](this) === "restart"){ if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); this["_start"](); } else this["_r"]["stop"](this); } else this["_r"]["stop"](this); } else{ ted = ted / this["time"]; let n = ""; for(n in this["origin"]) this["origin"][n] = ted * (this["end"][n] - this["_v"][n]) + this["_v"][n]; if(this["onUpdate"] !== null) this["onUpdate"](this["origin"]); } } } } Object.defineProperties(TweenValue.prototype, { isTweenValue: { configurable: false, enumerable: false, writable: false, value: true, } }); /* TweenAlone (相对于 TweenValue 此类可以独立补间, 不需要 Tween) demo: const v1 = new TweenAlone({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v)), animate = function (){ requestAnimationFrame(animate); v1.update(); } animate(); v1.start(); */ class TweenAlone extends TweenValue{ constructor(origin, end, time, onUpdate, onEnd, onStart){ super(origin, end, time, onUpdate, onEnd, onStart); } start(){ if(this.onStart !== null) this.onStart(); this._r = this; this._start(); } stop(){ this._r = null; } } /* Tween 动画补间 (TweenValue 的root, 可以管理多个TweenValue) parameter: attribute: method: start(value: TweenValue): undefined; stop(value: TweenValue): undefined; static: Value: TweenValue; demo: //init Tween: const tween = new Tween(), animate = function (){ requestAnimationFrame(animate); tween.update(); } //init TweenValue: const v2 = new Tween.Value({x:0, y:0}, {x:5, y:10}, 1000, v => console.log(v), v => { v2.reverse(); //v2.end 复制起始值 return "restart"; //返回"restart"表示不删除队列, 需要继续补间 }); animate(); tween.start(v2); */ class Tween extends RunningList{ static Value = TweenValue; constructor(){ super(); } start(value){ if(value.onStart !== null) value.onStart(); if(value._r === null) this.push(value); value._r = this; value._start(this); } stop(value){ if(value._r !== null) this.splice(value); value._r = null; } } /* TweenTarget 朝着轴插值(有效的跟踪动态目标, 注意此类需要配合 RunningList 类使用, 因为此类在任何情况下都没有阻止你调用.update()方法) parameter: v1 = {x: 0}, v2 = {x: 100}, distance = 1, //每次移动的距离 onUpdate = null, // onEnd = null attribute: v1: Object; //起点 v2: Object; //终点 onUpdate: Function; // onEnd: Function; // method: update(): undefined; //一般在动画循环里执行此方法 updateAxis(): undefined; //更新v1至v2的方向轴 (初始化时构造器自动调用一次) setDistance(distance: Number): undefined; //设置每次移动的距离 (初始化时构造器自动调用一次) demo: const ttc = new TweenTarget({x:0, y:0}, {x:100, y:100}, 10), //计时器模拟动画循环函数, 每秒执行一次.update() timer = new Timer(() => { ttc.update(); console.log('update: ', ttc.v1); }, 1000, Infinity); ttc.onEnd = v => { timer.stop(); console.log('end: ', v); } timer.start(); console.log(ttc); */ class TweenTarget{ #distance = 1; #distancePow2 = 1; #axis = {}; constructor(v1 = {x: 0}, v2 = {x: 100}, distance, onUpdate = null, onEnd = null){ this.v1 = v1; this.v2 = v2; this.onUpdate = onUpdate; this.onEnd = onEnd; this.setDistance(distance); this.updateAxis(); } setDistance(v = 1){ this.#distance = v; this.#distancePow2 = Math.pow(v, 2); } updateAxis(){ var n, v, len = 0; for(n in this.v1){ v = this.v2[n] - this.v1[n]; len += v * v; this.#axis[n] = v; } len = Math.sqrt(len); if(len !== 0){ for(n in this.v1) this.#axis[n] *= 1 / len; } } update(){ var n, len = 0; for(n in this.v1) len += Math.pow(this.v1[n] - this.v2[n], 2); if(len > this.#distancePow2){ for(n in this.v1) this.v1[n] += this.#axis[n] * this.#distance; if(this.onUpdate !== null) this.onUpdate(this.v1); } else{ for(n in this.v1) this.v1[n] = this.v2[n]; if(this.onEnd !== null) this.onEnd(this.v1); } } } /* EventDispatcher 自定义事件管理器 parameter: attribute: method: clearEvents(eventName): undefined; //清除eventName列表, 如果 eventName 未定义清除所有事件 customEvents(eventName, eventParam): this; //创建自定义事件 eventParam 可选 默认{} getParam(eventName): eventParam; trigger(eventName, callback): undefined; //触发 (callback: 可选) register(eventName, callback): undefined; // deleteEvent(eventName, callback): undefined; // demo: const eventDispatcher = new EventDispatcher(); eventDispatcher.customEvents("test", {name: "test"}); eventDispatcher.register("test", eventParam => { console.log(eventParam) //Object{name: "test"} }); eventDispatcher.trigger("test"); */ class EventDispatcher{ constructor(){ this._eventsList = {}; this.__eventsList = []; this.__trigger = ""; } clearEvents(eventName){ //if(this.__trigger === eventName) return console.warn("EventDispatcher: 清除事件失败"); if(this._eventsList[eventName] !== undefined) this._eventsList[eventName].func = [] else this._eventsList = {} } customEvents(eventName, eventParam = {}){ //if(typeof eventName !== "string") return console.warn("EventDispatcher: 注册自定义事件失败"); if(this._eventsList[eventName] !== undefined) return console.warn("EventDispatcher: "+eventName+" 已存在"); //eventParam = eventParam || {} //if(eventParam.type === undefined) eventParam.type = eventName; this._eventsList[eventName] = {func: [], param: eventParam} return this; } getParam(eventName){ return this._eventsList[eventName]["param"]; } trigger(eventName, callback){ //if(this._eventsList[eventName] === undefined) return; const obj = this._eventsList[eventName]; var k, len = obj.func.length; if(len !== 0){ if(typeof callback === "function") callback(obj["param"]); //更新参数(eventParam) //触发过程(如果触发过程中删除事件, 不仅 len 没有变, 而且还会漏掉一个key, 所以在触发过程中删除的事件要特殊处理) this.__trigger = eventName; for(k = 0; k < len; k++) obj["func"][k](obj["param"]); this.__trigger = ""; //触发过程结束 //删除在触发过程中要删除的事件 len = this.__eventsList.length; for(k = 0; k < len; k++) this.deleteEvent(eventName, this.__eventsList[k]); this.__eventsList.length = 0; } } register(eventName, callback){ if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); const obj = this._eventsList[eventName]; //if(obj.func.includes(callback) === false) obj.func.push(callback); //else console.warn("EventDispatcher: 回调函数重复"); obj.func.push(callback); } deleteEvent(eventName, callback){ if(this._eventsList[eventName] === undefined) return console.warn("EventDispatcher: "+eventName+" 不存在"); if(this.__trigger === eventName) this.__eventsList.push(callback); else{ const obj = this._eventsList[eventName], i = obj.func.indexOf(callback); if(i !== -1) obj.func.splice(i, 1); } } } export { UTILS, ColorRefTable, Ajax, IndexedDB, TreeStruct, Point, Line, Box, Circle, Polygon, RGBColor, Timer, SeekPath, RunningList, TweenValue, TweenAlone, Tween, TweenTarget, EventDispatcher, ShapeRect, }