目的: 绘制组织架构图
大概效果如下图:
需求拆分
- 默认相对画布居中
- 一级只有一个节点,二级水平分布,二级以下,垂直分布
- 矩形框框、每一层级的宽度、高度固定
- 父子节点,通过线条链接
- 父子节点、兄弟节点存在一定的间距
- 支持点击小圆圈折叠展开
- 每个矩形元素,会显示文字,文字居中对齐,过长则自动换行
准备工作
先学习一下canvas,了解canvas绘图的基本套路
根据以上的需求拆分,先用canvas的API画出各个单独的元素
1. 首先画矩形框
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Rect</title>
</head>
<body>
<canvas id="canvas" width="1024" height="2768" ></canvas>
</body>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
const centerX = width / 2
// 绘制的方法
const drawDept = (ctx, config) => {
ctx.fillStyle = config.fillStyle;
ctx.rect(config.x, config.y, config.width, config.height);
ctx.fill();
ctx.lineWidth = 1;
ctx.strokeStyle = '#222';
ctx.rect(config.x, config.y, config.width, config.height);
ctx.stroke()
}
const firstLevelItem = {
width: 150,
height: 75,
x: centerX - 150/2,
y: 10
}
const firstLevelConfig = {
width: firstLevelItem.width,
height: firstLevelItem.height,
x: firstLevelItem.x,
y: firstLevelItem.y,
fillStyle: "transparent",
}
drawDept(ctx,firstLevelConfig)
</script>
</html>
2. 画圆圈
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Circle</title>
</head>
<body>
<canvas id="canvas" width="1024" height="2768" ></canvas>
</body>
<script>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const width = canvas.width;
// 绘制的方法
const drawCollapseButton = (ctx,config)=>{
ctx.fillStyle = '#fff'
ctx.beginPath();
ctx.arc(config.x,config.y,config.r,0,2*Math.PI);
ctx.stroke();
ctx.fill();
drawText(ctx,{
x:config.x,
y:config.y + 8,
text: '+',
fontSize: 20,
lineHeight: 20,
containerHeight: config.r * 2,
color: '#222',
maxWidth: config.r * 2
})
}
drawCollapseButton(ctx,{
x: 20,
y: 20,
r: 10
})
</script>
</html>
3. 画连线
只要有水平和垂直两种布局,于是连线也有两种。 两种连线的共同点是,有起始点和结束点,0-n个中间点
代码有点多,只给出连线的函数
const drawLine = (ctx, config) => {
ctx.strokeStyle = config.borderColor;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(config.startPoint.x, config.startPoint.y);
config.middlePoint.forEach(item => {
ctx.lineTo(item.x, item.y)
})
ctx.lineTo(config.endPoint.x, config.endPoint.y)
ctx.stroke();
}
4. 写文字
canvas有提供些文字的API,但是不支持换行,于是,自己加了换行的代码 思路就是,
- 根据换行符
\n
切成数组 - 遍历数据,将每个元素,切成单个字符
- 使用canvas的api
measureText
测量字符的宽度 - 达到一行的宽度则作为一个字符串,存入数组
- 遍历数据画出字符
const workBreak = (ctx, text, maxWidth) => {
const objText = ctx.measureText(text);
let arrFillText = []
if (objText.width > maxWidth) {
const arrText = text.split('')
let newText = '';
arrText.forEach((world, index) => {
const currentText = `${newText}${world}`
const {width} = ctx.measureText(currentText);
if (width >= maxWidth) {
arrFillText.push(newText)
newText = world
} else {
newText = currentText
if (index === arrText.length - 1) {
arrFillText.push(newText)
}
}
})
} else {
arrFillText = [text]
}
return arrFillText
}
const drawText = (ctx, config) => {
ctx.font = `${config.fontSize}px serif`
ctx.fillStyle = config.color
ctx.textAlign = 'center'
const arrText = config.text.split('\n');
const arrDrawText = []
arrText.forEach((item) => {
const arr = workBreak(ctx, item, config.maxWidth)
arr.forEach((text) => {
arrDrawText.push(text)
})
})
const h = arrDrawText.length * config.lineHeight;
const gap = config.containerHeight - h
arrDrawText.forEach((text, index) => {
ctx.fillText(text, config.x, config.y + index * (config.lineHeight) + gap / 2, config.maxWidth)
})
}
5. 判断鼠标在当前区域内
isPointInPath
用于判断在当前路径中是否包含检测点的方法
ctx.isPointInPath(mousePoint.x,mousePoint.y)
6. 处理数据
因为架构图,展示出来,就是一颗树的形状,所以处理的过程中,递归会使用的比较频繁。
与html的常用元素,如div等有各种定位,canvas画的每个元素,都需要自己计算坐标位置。所以,必须在处理数据的同时,也把坐标确定下来。
接下来一节,数据处理的一些问题和思路
处理数据
先画个图,元素之间的各种关系会更清晰
宽度计算
- 因为第一层只有一个节点,所以,第一层的最大宽度
maxWidth=[所有二层]maxWidth之和+(gapV * [子元素数量-1])
与第二层相同即可 - 第二层某个元素的最大宽度
maxWidth=[maxWidth最大的第三层子元素的]maxWidth
- 从第三层开始,当前元素的
maxWidth=[子]maxWidth+(gapV * [当前级别-2])
高度计算
- 水平间距gapH
- 第一层的高度
maxHeight=[第二层中,最高的一组]maxHeight+gapH+[第一层的]height
- 第二层的高度及以下
maxHeight=[所有子元素]height*gapH*[子元素-1]数量+[自身的]height+gapH
元素起始点坐标
有了最大宽度和高度,就可以确定各个元素的坐标
连接点确定
- 水平布局的连接点在元素的中间
与父元素的连接点parentLinkPoint
起始点=父节点的childLinkPoint.x - (父节点的maxWidth)/2
index 在兄弟中的排位,0开始
x = 起始点 + index * gapV + 前面几个兄弟的maxWidth 之和
y = 父节点的childLinkPoint.y + gapH
与子元素的连接点 childLinkPoint
x = x + 当前节点的实际宽度[上图灰色块]的一半
y = y
- 垂直布局的连接点
与父元素的连接点
parentLinkPoint
x 在于父childLinkPoint.x 往右偏移gapV一半的位置
y 在于父childLinkPoint.y 往下偏移gapH的位置
与子元素的连接点 childLinkPoint
x = x
y = y 当前节点实际高度的一半
问题
按照以上思路,图,画出来了。 但是也遇到了其他的问题。
1. 图形拾取
即是当前鼠标在哪个图形上面,虽然canvas的API提供了判断鼠标在哪个图形上,但是根据网上的说法,这个方法,在多图形的情况下,存在一定的性能问题。
以下是一些常用的方法:
1. 使用 Canvas 内置的 API 拾取图形
isPointInPath
isPointInStroke
2. 使用几何运算拾取图形
需要对每种图形提供判断是否在图形内部和图形边上的方法
3. 使用缓存 Canvas 通过颜色拾取图形
4. 混杂上面的几种方式来拾取图形
2. 事件监听和处理
当有多个叠在一起的图形时,如何进行事件监听的响应 如何实现像我们普通元素一般,时间的捕获、冒泡~
在我发愁如何实现这些时,我发现了个神器,可以解我燃眉之急
ZRender
ZRender 是二维绘图引擎,它提供 Canvas、SVG、VML 等多种渲染方式。ZRender 也是 ECharts 的渲染器。
先整理ZRender哪些功能可以在我的组织架构系统里用到吧
如果自己增加事件监听系统,在图形嵌套的情况下,事件冒泡,是个麻烦的事,于是乎,偷懒啦
- 最主要就是因为封装了事件监听系统啦
- ZRender的元素,颗粒化较细,能满足我的需求 基本就是矩形、圆形等