canvas绘制组织架构图

1,891

目的: 绘制组织架构图

大概效果如下图:

需求拆分

  1. 默认相对画布居中
  2. 一级只有一个节点,二级水平分布,二级以下,垂直分布
  3. 矩形框框、每一层级的宽度、高度固定
  4. 父子节点,通过线条链接
  5. 父子节点、兄弟节点存在一定的间距
  6. 支持点击小圆圈折叠展开
  7. 每个矩形元素,会显示文字,文字居中对齐,过长则自动换行

准备工作

先学习一下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,但是不支持换行,于是,自己加了换行的代码 思路就是,

  1. 根据换行符 \n 切成数组
  2. 遍历数据,将每个元素,切成单个字符
  3. 使用canvas的apimeasureText测量字符的宽度
  4. 达到一行的宽度则作为一个字符串,存入数组
  5. 遍历数据画出字符
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画的每个元素,都需要自己计算坐标位置。所以,必须在处理数据的同时,也把坐标确定下来。
接下来一节,数据处理的一些问题和思路

处理数据

先画个图,元素之间的各种关系会更清晰

宽度计算

  1. 因为第一层只有一个节点,所以,第一层的最大宽度maxWidth=[所有二层]maxWidth之和+(gapV * [子元素数量-1])与第二层相同即可
  2. 第二层某个元素的最大宽度maxWidth=[maxWidth最大的第三层子元素的]maxWidth
  3. 从第三层开始,当前元素的maxWidth=[子]maxWidth+(gapV * [当前级别-2])

高度计算

  1. 水平间距gapH
  2. 第一层的高度maxHeight=[第二层中,最高的一组]maxHeight+gapH+[第一层的]height
  3. 第二层的高度及以下maxHeight=[所有子元素]height*gapH*[子元素-1]数量+[自身的]height+gapH

元素起始点坐标

有了最大宽度和高度,就可以确定各个元素的坐标

连接点确定

  1. 水平布局的连接点在元素的中间

与父元素的连接点parentLinkPoint

起始点=父节点的childLinkPoint.x - (父节点的maxWidth)/2
index 在兄弟中的排位,0开始
x = 起始点 + index * gapV + 前面几个兄弟的maxWidth 之和
y = 父节点的childLinkPoint.y + gapH

与子元素的连接点 childLinkPoint

x = x + 当前节点的实际宽度[上图灰色块]的一半
y = y
  1. 垂直布局的连接点 与父元素的连接点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. 混杂上面的几种方式来拾取图形

详见antV的具体说明

2. 事件监听和处理

当有多个叠在一起的图形时,如何进行事件监听的响应 如何实现像我们普通元素一般,时间的捕获、冒泡~

antV的一些文章

在我发愁如何实现这些时,我发现了个神器,可以解我燃眉之急

ZRender

ZRender 是二维绘图引擎,它提供 Canvas、SVG、VML 等多种渲染方式。ZRender 也是 ECharts 的渲染器。

先整理ZRender哪些功能可以在我的组织架构系统里用到吧

如果自己增加事件监听系统,在图形嵌套的情况下,事件冒泡,是个麻烦的事,于是乎,偷懒啦

  1. 最主要就是因为封装了事件监听系统啦
  2. ZRender的元素,颗粒化较细,能满足我的需求 基本就是矩形、圆形等