"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,
}