目的: 绘制组织架构图
大概效果如下图:
需求拆分
- 默认相对画布居中
- 一级只有一个节点,二级水平分布,二级以下,垂直分布
- 矩形框框、每一层级的宽度、高度固定
- 父子节点,通过线条链接
- 父子节点、兄弟节点存在一定的间距
- 支持点击小圆圈折叠展开
- 每个矩形元素,会显示文字,文字居中对齐,过长则自动换行
准备工作
先学习一下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的apimeasureText测量字符的宽度
- 达到一行的宽度则作为一个字符串,存入数组
- 遍历数据画出字符
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的元素,颗粒化较细,能满足我的需求 基本就是矩形、圆形等