// import { threadId } from "worker_threads";
window.cancelAnimationFrame =
window.cancelAnimationFrame || window.mozCancelAnimationFrame;
window.requestAnimationFrame =
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.mzRequestAnimationFrame ||
window.oRequestAnimationFrame;
export default class draw {
constructor(options = {}) {
// 1px solid #DCDFE6
this.options = options.data || []
this.defaut_circleStrokeStyle = ["#d2d2d2"]
this.defaut_circleFillStyle = ["#dcdcdc"]
this.circleStrokeStyle = options.circleStrokeStyle || ["#DCDFE6"]
this.circleFillStyle = options.circleFillStyle || ["#DCDFE6"]
this.DomCanvas = options.DomCanvas || "";
this.clock_radius = options.clock_radius || 34;
this.fontSize = options.fontSize || 12
this.clockLineWidth = 2; // 线的宽度
this.clearLineColor = "#f6f6f6"
this.canvas = document.getElementById(this.DomCanvas);
this.context = this.canvas.getContext("2d");
this.circle_option = []; // 画圆参数
this.line_option = []; // 画直线参数
this.context.lineWidth = this.clockLineWidth; // 线的宽度
this.context.strokeStyle = "#f80120"; // 线的颜色
this.context.shadowStyle = "#f80120"; // 阴影颜色
this.context.shadowOffsetX = 2; // 阴影水平距离
this.context.shadowOffsetY = 2; // 阴影垂直距离
this.context.shadowBlur = 4; // 设置或返回用于阴影的模糊级别
this.context.stroke(); // 绘制已定义路径
this.get_all_option = this.get_all_option.bind(this)
this.recursion_draw_circle = this.recursion_draw_circle.bind(this)
this.crossover_point = this.crossover_point.bind(this)
this.getAngle = this.getAngle.bind(this)
this.draw_line = this.draw_line.bind(this)
this.draw_triangle = this.draw_triangle.bind(this)
this.draw_circle = this.draw_circle.bind(this)
this.draw_text = this.draw_text.bind(this)
this.hypotenuse = this.hypotenuse.bind(this)
this.create_line = this.create_line.bind(this)
this.draw_translate_text = this.draw_translate_text.bind(this)
this.init = this.init.bind(this)
this.stop = this.stop.bind(this)
this.requestAnimationFrame = ''
this.dis = -1;
// this.init()
}
isArrayFn(o) {
// Arguments, Array, Boolean, Date, Error, Function, JSON, Math, Number, Object, RegExp, String.
return Object.prototype.toString.call(o) === '[object Array]';
}
init(data) {
this.options = data || this.options
// 生成参数
this.circle_option = []
this.get_all_option(this.options)
let parentuuid = [], uuid, delete_arr_subscript = [];
this.circle_option.map((children, p_index) => {
let key = []
this.circle_option.map((child, m_index) => {
if (children.text == child.text) {
if (!uuid) uuid = child.uuid;
if (this.isArrayFn(child.parentuuid)) {
parentuuid = [...child.parentuuid]
} else {
parentuuid.indexOf(child.parentuuid) == -1 && parentuuid.push(child.parentuuid);
}
// 记录需要删除数据的下标
key.indexOf(m_index) == -1 && key.push(m_index)
this.circle_option[p_index].parentuuid = parentuuid
this.circle_option[p_index].uuid = uuid
}
})
uuid = ''
parentuuid = []
key.pop()
key.map(item => (delete_arr_subscript.indexOf(item) == -1 && delete_arr_subscript.push(item)))
delete_arr_subscript = [...delete_arr_subscript, ...key]
})
// 删除重复数据
delete_arr_subscript.map(item => { this.circle_option[item] = null; })
this.circle_option = this.circle_option.filter((item) => { return item !== null })
// 画圆 并生成直线的参数 // 画线条
this.recursion_draw_circle()
this.canvas.onmousedown = (ev) => {
let e = ev || event;
let dx = e.clientX, dy = e.clientY; // 鼠标按下位置的坐标
this.isDown = true;
this.canvas.onmousemove = (ev) => {
if (this.isDown) {
let e = ev || event;
let mx = e.clientX;
let my = e.clientY;
let p = {
x: mx - this.canvas.getBoundingClientRect().left,
y: my - this.canvas.getBoundingClientRect().top
};
//求点到圆心的距离,用到了勾股定理
this.dis == -1 && this.circle_option.map((item, index) => { if (Math.sqrt((p.x - item.circleX) * (p.x - item.circleX) + (p.y - item.circleY) * (p.y - item.circleY)) <= this.clock_radius) this.dis = index; })
if (this.dis >= 0) {
this.circle_option[this.dis].mousemovecircleX = this.circle_option[this.dis].circleX + mx - dx; // 偏移量x
this.circle_option[this.dis].mousemovecircleY = this.circle_option[this.dis].circleY + my - dy; // 偏移量y
}
}
};
//鼠标移开事件
this.canvas.onmouseup = (ev) => {
this.isDown = false;
let e = ev || event;
if (this.dis != -1) {
this.circle_option[this.dis].circleY = this.circle_option[this.dis].mousemovecircleY || this.circle_option[this.dis].circleY
this.circle_option[this.dis].circleX = this.circle_option[this.dis].mousemovecircleX || this.circle_option[this.dis].circleX
this.circle_option[this.dis].startPoint = this.circle_option[this.dis].circleY
this.circle_option[this.dis].mousemovecircleX = 0; // 偏移量x
this.circle_option[this.dis].mousemovecircleY = 0; // 偏移量y
}
this.dis = -1;
// 重置
this.canvas.onmousemove = null;
this.canvas.onmouseup = null;
};
};
}
// 递归 画圆 和 文字 并生成线条的参数
recursion_draw_circle() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
let option = this.circle_option;
// 先画线 再画圆
// 线条坐标
this.line_option = []
for (let i = 0; i < option.length; i++) {
for (let j = 0; j < option.length; j++) {
// 父节点 多个
option[j].parentuuid.map(item => {
if (option[i].uuid == item) {
this.line_option.push({
startX: option[i].mousemovecircleX || option[i].circleX,
startY: option[i].mousemovecircleY || option[i].startPoint,
start_clock_radius: this.clock_radius + this.clockLineWidth + 1,
endX: option[j].mousemovecircleX || option[j].circleX,
endY: option[j].mousemovecircleY || option[j].startPoint,
end_clock_radius: this.clock_radius + this.clockLineWidth + 1,
draw: true
})
}
})
}
}
this.create_line(this.line_option)
let gravity = 0.06;//定义重力加速度;
let bounce = -0.8;//定义反弹系数
let startPoint = this.canvas.height + (this.clock_radius + this.clockLineWidth) * 2;
let padding = 5;
// 活动范围
let max_width = this.canvas.width - padding - (this.clock_radius + this.clockLineWidth) * 2;
let max_height = this.canvas.height - padding - (this.clock_radius + this.clockLineWidth) * 2;
// x轴坐标范围
let minx = this.clock_radius + padding / 2;
let maxx = minx + max_width;
// y轴坐标范围
let miny = this.clock_radius + this.clockLineWidth + padding / 2;
let maxy = miny + max_height;
let step = max_height / (option.length - 1); //每个球的间距
option.map((item, i) => {
item.mousemovecircleX = item.mousemovecircleX || 0
item.mousemovecircleY = item.mousemovecircleY || 0
item.circleX = item.circleX || (Math.random() * maxx)
item.circleY = item.circleY || (Math.random() * maxy)
item.flutter_circleX = item.flutter_circleX || item.circleX
item.flutter_circleY = item.flutter_circleY || item.circleY
item.flutter_circleX = Math.abs(item.flutter_circleX - (item.mousemovecircleX || item.circleX)) > 1 ? item.flutter_circleX : ((item.mousemovecircleX || item.circleX) + this.randm_stepX())
item.flutter_circleY = Math.abs(item.flutter_circleY - (item.mousemovecircleY || item.circleY)) > 1 ? item.flutter_circleY : ((item.mousemovecircleY || item.circleY) + this.randm_stepY())
// 飘动系数
// bounce = 0.04
bounce = 0
// 漂移距离
item.circleX = item.circleX > item.flutter_circleX ? (item.circleX - Math.random() * bounce) : (item.circleX + Math.random() * bounce)
item.circleY = item.circleY > item.flutter_circleY ? (item.circleY - Math.random() * bounce) : (item.circleY + Math.random() * bounce)
if (item.circleX < minx || item.circleX > maxx) item.flutter_circleX = item.circleX + this.randm_stepX()
if (item.circleY < miny || item.circleY > maxy) item.flutter_circleY = item.circleY + this.randm_stepY()
// 最大边界和最小边界
item.circleX = item.circleX < minx ? minx : item.circleX
item.circleX = item.circleX > maxx ? maxx : item.circleX
item.circleY = item.circleY < miny ? miny : item.circleY
item.circleY = item.circleY > maxy ? maxy : item.circleY
item.circleStrokeStyle = this.circleStrokeStyle[i] || this.defaut_circleStrokeStyle[0]
item.circleFillStyle = this.circleFillStyle[i] || this.defaut_circleFillStyle[0]
// 鼠标移动
item.startPoint = item.circleY;
// 如果再移动中则使用移动的坐标
this.draw_circle(item.mousemovecircleX || item.circleX, item.mousemovecircleY || item.startPoint, this.clock_radius, item.circleStrokeStyle, item.circleFillStyle)
this.draw_text(item.text, item.mousemovecircleX || item.circleX, item.mousemovecircleY || item.startPoint, this.fontSize, this.clock_radius * 2)
})
this.requestAnimationFrame = requestAnimationFrame(this.recursion_draw_circle)
}
randm_stepX() {
return (10 - (Math.random() * 20))
}
randm_stepY() {
return (30 - (Math.random() * 60))
}
stop() {
this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
cancelAnimationFrame(this.requestAnimationFrame)
}
// 获取参数 格式化
get_all_option(option, parentuuid = 0) {
option.map(item => {
// 圆中文字
this.circle_option.push({ text: item.text, uuid: item.uuid, parentuuid: parentuuid })
if (item.children.length) this.get_all_option(item.children, item.uuid)
})
}
/**
* 求交点坐标
* @param {*} x0 圆点坐标:
* @param {*} y0 圆点坐标:
* @param {*} r 半径:r
* @param {*} ao 角度:a0
*/
crossover_point(x0, y0, r, ao) {
return {
x: x0 + r * Math.cos(ao * Math.PI / 180),
y: y0 - r * Math.sin(ao * Math.PI / 180)
}
}
create_line(line_option) {
let degree = 24; // 角度
let bevel = 10 // 斜边长
// console.log(line_option)
// 画线条 生成画三角的参数
line_option.map(item => {
// 公式
// 1. getAngle 通过起始点和目标点获得角度
// 2. crossover_point 通过圆心坐标 圆半径 角度 得到 线条和圆的交点
// 角度
let start_ale = this.getAngle(item.startX, item.startY, item.endX, item.endY);
let end_ale = this.getAngle(item.endX, item.endY, item.startX, item.startY);
// 对应坐标
let start_intersection = this.crossover_point(item.startX, item.startY, item.start_clock_radius, start_ale)
let end_intersection = this.crossover_point(item.endX, item.endY, item.end_clock_radius, end_ale)
this.draw_line(start_intersection, end_intersection)
// return
let translate_text_option = {}
// 画关系线上的文字
// 文字坐标设定
if (item.startX > item.endX) {
// 算法 计算 三角形的和直线相交的交点 默认三角形斜边长 10
if (item.startY > item.endY)
translate_text_option = {
x: Math.abs(item.endX - item.startX - Math.cos(2 * Math.PI / 360 * end_ale) * bevel),
y: Math.abs(item.endY - item.startY + Math.sin(2 * Math.PI / 360 * end_ale) * bevel)
}
else
translate_text_option = {
x: Math.abs(item.endX - item.startX - Math.cos(2 * Math.PI / 360 * end_ale) * bevel),
y: Math.abs(item.endY - item.startY - Math.sin(2 * Math.PI / 360 * end_ale) * bevel)
}
} else {
if (item.startY > item.endY)
translate_text_option = {
x: Math.abs(item.endX - item.startX) + Math.cos(2 * Math.PI / 360 * end_ale) * bevel,
y: Math.abs(item.endY - item.startY) - Math.sin(2 * Math.PI / 360 * end_ale) * bevel
}
else
translate_text_option = {
x: Math.abs(item.endX - item.startX) + Math.cos(2 * Math.PI / 360 * end_ale) * bevel,
y: Math.abs(item.endY - item.startY) - Math.sin(2 * Math.PI / 360 * end_ale) * bevel
}
}
let a = item.startX > item.endX ? (item.startX - item.endX) : (item.endX - item.startX)
let b = item.endY > item.startY ? (item.endY - item.startY) : (item.startY - item.endY)
// 线的长度 大于 圆直径 画三角形
if (this.clock_radius * 2 < Math.sqrt(a * a + b * b)) {
// 邻边和对边
let hpe1 = this.hypotenuse(bevel, end_ale - degree);
let hpe2 = this.hypotenuse(bevel, end_ale + degree);
// 画三角形
this.draw_triangle({ x: end_intersection.x, y: end_intersection.y }, { x: end_intersection.x + hpe1.x, y: end_intersection.y - hpe1.y }, { x: end_intersection.x + hpe2.x, y: end_intersection.y - hpe2.y })
}
let font_witch = this.context.measureText("relation").width;
let fa = start_intersection.x > end_intersection.x ? (start_intersection.x - end_intersection.x) : (end_intersection.x - start_intersection.x)
let fb = start_intersection.y > end_intersection.y ? (start_intersection.y - end_intersection.y) : (end_intersection.y - start_intersection.y)
// 文字长度大于线的长度就不画文字
if (font_witch + bevel >= Math.sqrt(fa * fa + fb * fb)) return
let endX, endY, rotate;
if (item.startX > item.endX) {
endX = item.endX + translate_text_option.x / 2;
rotate = 180 - start_ale;
} else {
endX = item.startX + translate_text_option.x / 2;
rotate = 180 - end_ale;
}
if (item.startY > item.endY) {
endY = item.endY + translate_text_option.y / 2
} else {
endY = item.startY + translate_text_option.y / 2
}
// 清除之前线条颜色
let clearLineOption = this.hypotenuse(font_witch, rotate);
// 清除之前线条颜色
let clear_start = {};
let clear_end = {};
clear_start.x = endX - clearLineOption.x / 2
clear_start.y = endY - clearLineOption.y / 2
clear_end.x = endX + clearLineOption.x / 2
clear_end.y = endY + clearLineOption.y / 2
this.draw_line(clear_start, clear_end, 3, this.clearLineColor)
this.draw_translate_text(endX, endY, rotate);
})
}
/**
*
* @param {*} endX 中心点
* @param {*} endY 中心点
* @param {*} text 文本
* @param {*} rotate 角度
* @param {*} fillStyle 颜色
*/
draw_translate_text(endX, endY, rotate, text = "relation", fillStyle = "#000000") {
this.context.beginPath();
this.context.fillStyle = fillStyle;
this.context.font = `normal ${this.fontSize}px 微软雅黑`;//字体
this.context.textAlign = "center";
// this.context.fillText(end, endX, endY);
this.context.save();
this.context.translate(endX, endY);
this.context.rotate(rotate * Math.PI / 180);
// this.draw_line()
this.context.fillText(text, 0, 0);
this.context.restore();
}
//已知角度和斜边,求直角边
hypotenuse(long, angle) {
//获得弧度
var radian = 2 * Math.PI / 360 * (90 - angle);
return {
x: Math.sin(radian) * long,//邻边
y: Math.cos(radian) * long//对边
};
}
/**
* 两点获得角度
* @param {*} px 起点
* @param {*} py 起点
* @param {*} mx 终点
* @param {*} my 终点
*/
getAngle(px, py, mx, my) {//获得人物中心和鼠标坐标连线,与y轴正半轴之间的夹角
var x = Math.abs(px - mx);
var y = Math.abs(py - my);
var z = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
var cos = y / z;
var radina = Math.acos(cos);//用反三角函数求弧度
var angle = 180 / (Math.PI / radina);//将弧度转换成角度
if (mx > px && my > py) {//鼠标在第四象限
angle = 270 + angle;
}
if (mx == px && my > py) {//鼠标在y轴负方向上
angle = 270;
}
if (mx == px && my < py) {//鼠标在y轴负方向下
angle = 90;
}
if (mx > px && my == py) {//鼠标在x轴正方向上
angle = 180;
}
if (mx < px && my > py) {//鼠标在第三象限
angle = 270 - angle;
}
if (mx > px && my < py) {
angle = 90 - angle;
}
if (mx < px && my == py) {//鼠标在x轴负方向
angle = 360;
}
if (mx < px && my < py) {//鼠标在第二象限
angle = 90 + angle;
}
return angle;
}
// 画三角
draw_triangle(line1, line2, line3, color = "#cccccc") {
this.context.beginPath();
this.context.moveTo(line1.x, line1.y);
this.context.lineTo(line2.x, line2.y);
this.context.lineTo(line3.x, line3.y);
this.context.strokeStyle = color; // 圆线条颜色
this.context.fillStyle = color; // 填充颜色
this.context.closePath();
this.context.fill(); // 开始填充 填充当前绘图(路径)
this.context.stroke(); // 绘制已定义的路径
}
/**
* 画直线
* @param {*} x 开始
* @param {*} y 开始
* @param {*} tx 去
* @param {*} ty 去
* @param {*} width 线宽度
* @param {*} strokeStyle 线颜色
*/
draw_line(start, end, width = 2, strokeStyle = "#cccccc") {
this.context.beginPath();
this.context.strokeStyle = strokeStyle;
this.context.lineWidth = width;
this.context.moveTo(start.x, start.y); // 线开始位置
this.context.lineTo(end.x, end.y); // 线 结束位置
this.context.stroke();
}
/**
* 画圆
* @param {*} x
* @param {*} y
* @param {*} 半径
* @param {*} 圆线颜色
* @param {*} 填充颜色
*/
draw_circle(x, y, clock_radius, strokeStyle, fillStyle) {
this.context.beginPath(); // 起始一条路径,或重置当前路径
// 画圆
this.context.arc(
x, // x 坐标
y, // y 坐标
clock_radius,
0,
Math.PI * 2,
false
);
this.context.save();
this.context.strokeStyle = strokeStyle; // 圆线条颜色
this.context.fillStyle = fillStyle; // 填充颜色
this.context.fill(); // 开始填充 填充当前绘图(路径)
this.context.stroke(); // 绘制已定义的路径
this.context.restore(); // 返回之前保存过的路径状态和属性
}
/**
* 绘制文字
* @param {*} text 文字
* @param {*} x 坐标
* @param {*} y 坐标
* @param {*} fontSize 文字大小
* @param {*} padding padding
* @param {*} maxWidth 文字最大宽度 超过换行 对大两行
*/
draw_text(text, x, y, fontSize, maxWidth) {
this.context.fillStyle = "#ffffff";//颜色
this.context.font = `normal ${fontSize}px 微软雅黑`;//字体
this.context.textBaseline = "middle";//竖直对齐
this.context.textAlign = "center";//水平对齐
let totalFontWidth = this.context.measureText(text).width + 15;
if (totalFontWidth > maxWidth) {
let padding = 15
// 需要换行
let fontWidth = this.context.measureText(text).width / text.length; // 每个字的宽度
let w = maxWidth - padding * 2;
let nl = 1
for (let i = 1; i <= text.length;) {
if (fontWidth * i < w) nl = i++;
else break;
}
// 第一行
let ny = y - fontSize / 2 - 1;
this.context.fillText(text.substr(0, nl), x, ny);//绘制文字 x 坐标 y 坐标
// 第二行
ny = y + fontSize / 2 + 1;
if ((text.length - nl) > nl) // 有第三行
this.context.fillText(text.substr(nl, 3) + "...", x, ny);//绘制文字 x 坐标 y 坐标
else
this.context.fillText(text.substr(nl, text.length), x, ny);//绘制文字 x 坐标 y 坐标
} else {
// 不需要
this.context.fillText(text, x, y);//绘制文字shiy x 坐标 y 坐标
}
}
}
如何使用
import robotball from "./robotball";
// 数据结构如下
// this.hierarchyTree = {
// 太平人寿保险有限公司: [
// "太平福禄嘉倍终身重大疾病保险",
// "太平成长无忧终身重大疾病保险",
// "太平福禄嘉倍终身重大疾病保险2",
// "太平福禄嘉倍终身重大疾病保险3",
// "太平福禄嘉倍终身重大疾病保险4",
// "太平福禄嘉倍终身重大疾病保险5",
// "太平福禄嘉倍终身重大疾病保险6",
// "太平福禄嘉倍终身重大疾病保险w",
// "太平成长无忧终身重大疾病保险e",
// "太平成长无忧终身重大疾病保险1"
// ],
// 太平福禄嘉倍终身重大疾病保险: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险2: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险3: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险4: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险5: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险6: ["托叫"],
// 太平福禄嘉倍终身重大疾病保险w: ["托叫"],
// 太平成长无忧终身重大疾病保险e: ["托叫"],
// 太平成长无忧终身重大疾病保险1: ["托叫"],
// 托叫: ["指一次性支付保险费", "指一次性支付保险费"]
// };
//实例化
this.robotball = new robotball({
DomCanvas: "canvas1",
circleStrokeStyle: ["#ec5252"],
circleFillStyle: ["#ff7373"],
//clock_radius: 64, // 圆大小
//fontSize: 20 // 字体大小
});
// 格式化canvas参数
initCanvasData() {
let key = Object.keys(this.hierarchyTree)[0];
return [
{
text: key,
children: this.get_subset(this.hierarchyTree[key]),
uuid: generateUUID()
}
];
},
// 画图
this.robotball.init(this.initCanvasData());