canvas 根据指定数据结构生成树形图

// 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());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值