03 Canvas 基础入门,原生绘制一个流程图

944 阅读11分钟

引言

在这一章中,我们将带大家探讨 Canvas 在流程图库中的应用场景、相关技术实现以及它在实际项目中的优势。

Canvas 的概念大家可能都听过,HTML5 Canvas 是一种用于绘制图形和创建动态内容的强大技术,它允许通过 JavaScript 动态绘制和渲染图形。但因为大多数人在工作过程中可能不会直接使用到,所以这一节我们从基础概念到应用,给大家介绍一下这项技术。

相比于 SVG 这种基于 XML 的矢量图形,Canvas 使用的是给予像素的绘制方式,非常适合需要频繁更新、动态渲染大量元素的场景,比如游戏、数据可视化、动画等。那么具体什么是 Canvas 呢?

什么是 Canvas ?

Canvas 是一种 HTML 元素,它为开发者提供了一个绘图区域,可以通过 JavaScript 在其上绘制任意图形。开发者通过 JavaScript 操控它,绘制诸如矩形、圆形、线条和自定义形状,还可以进行更高级的图像处理、动画制作和用户交互。

一个基本的 Canvas 元素在 HTML 中的定义像下面这样:

<canvas id="myCanvas" width="400" height="300"></canvas>

说明:

  • id:为 Canvas 元素赋予唯一的标识位,便于在 JavaScript 中访问。
  • width 和 height:定义了 Canvas 画布的宽度和高度(默认值为 300x150)

Canvas 自身值提供一个绘图区域,所有的绘图逻辑都需要通过 JavaScript 来实现。接线来,我们将通过代码了解如何在 Canvas 中绘制简单的流程图形状和连线。

Canvas 基本操作

在 Canvas 中绘图的基本步骤如下:

  1. 获取 Canvas 元素的上下文(context):

    • context 是 Canvas 的绘图环境,它提供了所有绘图相关的 API
    • 通过 getContext('2d') 方法可以获取 2D 绘图上下文
  2. 绘制图形:

    • 使用上下文对象提供的绘图方法来绘制矩形、圆形、线条等图形
    • 通过制定颜色、线条宽度等属性,可以控制绘制的样式 

示例:绘制矩形

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 设置矩形填充颜色
ctx.fillStyle = 'lightgreen';

// 绘制矩形 (x, y, width, height)
ctx.fillRect(50, 50, 100, 50);

在上面的代码中,我们首先获取了 myCanvas 元素的 2D 上下文 ctx,然后用 fillRect() 方法绘制了一个 100x50 的绿色矩形。效果如下所示:

image.png

其它常用绘图方法

  • fillRect(x, y, width, height):绘制填充矩形
  • strokeRect(x, y, width, height):绘制矩形边框
  • arc(x, y, radius, startAngle, endAngle):绘制圆形或弧形
  • moveTo(x, y) 和 lineTo(x, y):绘制线条
  • fillText(text, x, y):绘制文本

Canvas 绘制简易流程图

为了更熟悉上面的 API,我们接下来用 Canvas 也来绘制一个简易的流程图。跟 SVG 类似,也会包含以下元素:

  • 开始节点(圆形)
  • 处理步骤(矩形)
  • 判断条件(菱形)
  • 连接这些形状的线条
  1. 添加 Canvas 画布

首先,在 HTML 文件中创建一个 400x300 的 Canvas 画布:

<canvas id="flowchartCanvas" width="400" height="300"></canvas>

在 JavaScript 中获取 Canvas 元素并设置上下文:

const canvas = document.getElementById('flowchartCanvas');
const ctx = canvas.getContext('2d');

2. ### 绘制开始节点(圆形)

我们使用 arc() 方法绘制一个圆形,表示流程图的开始节点。

// 设置圆的样式
ctx.fillStyle = 'lightblue';
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;

// 绘制圆形 (x, y, radius, startAngle, endAngle)
ctx.beginPath();
ctx.arc(100, 50, 30, 0, Math.PI * 2);  // 圆心(100, 50),半径30
ctx.fill();  // 填充
ctx.stroke();  // 描边

// 添加“开始”文本
ctx.fillStyle = 'black';
ctx.font = '16px Arial';
ctx.fillText('开始', 85, 55);

说明:

  • arc(100, 50, 0, Math.PI * 2) 绘制了一个半径为 30 的圆形,圆心在 (100, 50)
  • 使用 fillText() 方法在圆形中间添加了 "开始" 文本
  1. 绘制处理步骤(矩形)

我们使用 fillRect() 和 strokeRect() 来绘制处理步骤的矩形框。

// 设置矩形的样式
ctx.fillStyle = 'lightgreen';
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;

// 绘制矩形 (x, y, width, height)
ctx.fillRect(180, 35, 100, 30);
ctx.strokeRect(180, 35, 100, 30);

// 添加“处理”文本
ctx.fillStyle = 'black';
ctx.fillText('处理', 210, 55);

说明:

  • fillRect(200, 35, 100, 30) 绘制了一个宽 100、高 30 的矩形,表示流程的处理步骤
  • 文本 ”处理“ 添加在矩形的中心位置
  1. 绘制判断条件(菱形)

菱形可以通过四条线连接四个顶点来绘制。使用 moveTo() 和 lineTo() 来绘制菱形。

// 设置菱形的样式
ctx.fillStyle = 'lightyellow';
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;

// 绘制菱形
ctx.beginPath();
ctx.moveTo(350, 30);  // 顶点
ctx.lineTo(380, 50);  // 右侧顶点
ctx.lineTo(350, 70);  // 底部顶点
ctx.lineTo(320, 50);  // 左侧顶点
ctx.closePath();
ctx.fill();
ctx.stroke();

// 添加“判断”文本
ctx.fillStyle = 'black';
ctx.fillText('判断', 335, 55);

说明:

  • moveTo() 和 lineTo() 方法通过连接点的方式绘制了菱形
  • 在菱形内添加”判断“文本
  1. 绘制连线

我们同样使用 moveTo() 和 lineTo() 方法来绘制连线,并为终点添加箭头效果。

// 设置线条样式
ctx.strokeStyle = 'black';
ctx.lineWidth = 2;

// 绘制从开始到处理的连线
ctx.beginPath();
ctx.moveTo(130, 50);  // 从圆的右侧开始
ctx.lineTo(180, 50);  // 到矩形的左侧
ctx.stroke();

// 绘制从处理到判断的连线
ctx.beginPath();
ctx.moveTo(280, 50);  // 从矩形的右侧开始
ctx.lineTo(320, 50);  // 到菱形的左侧
ctx.stroke();

然后通过绘制三角形的方法,为线条末端添加箭头:

    // 绘制箭头(右侧箭头)
    ctx.beginPath();
    ctx.moveTo(320, 50);  // 起点
    ctx.lineTo(315, 45);  // 左侧箭头
    ctx.lineTo(315, 55);  // 右侧箭头
    ctx.closePath();
    ctx.fill();
  1. 完整的代码及效果

<canvas id="flowchartCanvas" width="400" height="300"></canvas>

<script>
    const canvas = document.getElementById('flowchartCanvas');
    const ctx = canvas.getContext('2d');

    // 开始节点
    ctx.fillStyle = 'lightblue';
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(100, 50, 30, 0, Math.PI * 2);
    ctx.fill();
    ctx.stroke();
    ctx.fillStyle = 'black';
    ctx.font = '16px Arial';
    ctx.fillText('开始', 85, 55);

    // 处理步骤
    ctx.fillStyle = 'lightgreen';
    ctx.fillRect(180, 35, 100, 30);
    ctx.strokeRect(180, 35, 100, 30);
    ctx.fillStyle = 'black';
    ctx.fillText('处理', 210, 55);

    // 设置菱形的样式
    ctx.fillStyle = 'lightyellow';
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;

    // 绘制菱形
    ctx.beginPath();
    ctx.moveTo(350, 30);  // 顶点
    ctx.lineTo(380, 50);  // 右侧顶点
    ctx.lineTo(350, 70);  // 底部顶点
    ctx.lineTo(320, 50);  // 左侧顶点
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    // 添加“判断”文本
    ctx.fillStyle = 'black';
    ctx.fillText('判断', 335, 55);

    // 设置线条样式
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 2;

    // 绘制从开始到处理的连线
    ctx.beginPath();
    ctx.moveTo(130, 50);  // 从圆的右侧开始
    ctx.lineTo(180, 50);  // 到矩形的左侧
    ctx.stroke();

    // 绘制箭头 1
    ctx.beginPath();
    ctx.moveTo(180, 50);  // 起点
    ctx.lineTo(175, 45);  // 左侧箭头
    ctx.lineTo(175, 55);  // 右侧箭头
    ctx.closePath();
    ctx.fill();

    // 绘制从处理到判断的连线
    ctx.beginPath();
    ctx.moveTo(280, 50);  // 从矩形的右侧开始
    ctx.lineTo(320, 50);  // 到菱形的左侧
    ctx.stroke();

    // 绘制箭头 2
    ctx.beginPath();
    ctx.moveTo(320, 50);  // 起点
    ctx.lineTo(315, 45);  // 左侧箭头
    ctx.lineTo(315, 55);  // 右侧箭头
    ctx.closePath();
    ctx.fill();
 </script>

效果如下:

image.png  

其它一些常用的知识点

在使用 Canvas 构建流程图或其他复杂图形应用时,除了基本的绘制和交互功能,还有很多重要的知识点和技巧,可以提升开发效率和用户体验。以下是一些常用的知识点,他们在流程设计和 Canvas 图形应用中非常有用:

绘制复杂图形与路径(Path)

除了简单的矩形、圆形等基础形状外,Canvas 提供了路径(path)绘制功能,可以用来绘制复杂的多边形、曲线或不规则的图形。路径是通过一系列的线段和曲线构成的,它允许开发者灵活绘制各种形状。

    // 绘制多边形(如菱形)
    ctx.beginPath();
    ctx.moveTo(150, 50);   // 移动到初始点
    ctx.lineTo(200, 100);  // 画第一条边
    ctx.lineTo(150, 150);  // 画第二条边
    ctx.lineTo(100, 100);  // 画第三条边
    ctx.closePath();       // 闭合路径
    ctx.fillStyle = 'lightyellow';
    ctx.fill();
    ctx.stroke();

说明:

  • beginPath():开始绘制新路径
  • moveTo(x, y) 和 lineTo(x, y):定义路径的起点和终点。
  • closePath():闭合路径。
  • fill() 和 stroke():填充和描边路径。

Canvas 变换(Transform)

Canvas 提供了对图形进行缩放、旋转、平移和倾斜的能力。通过 transform 操作,可以轻松改变图形的现实效果。这在流程图中移动节点、旋转节点、箭头等方向等场景中非常有用。

    // 旋转一个矩形
    ctx.save();  // 保存当前状态
    ctx.translate(150, 100);  // 将绘图原点移动到(150, 100)
    ctx.rotate(Math.PI / 4);  // 旋转45度(PI/4弧度)
    ctx.fillStyle = 'lightblue';
    ctx.fillRect(-50, -25, 100, 50);  // 绘制旋转后的矩形
    ctx.restore();  // 恢复之前的状态

说明:

  • translate(x, y):平移画布的原点
  • rotate(angle):绕画布的原点旋转指定角度
  • scale(x, y):对图形进行缩放
  • sava() 和 restore():保存和恢复画布状态,避免操作影响后续绘制

文本绘制与样式(Text)

Canvas 提供了强大的文本绘制功能,可以通过 fillText 和 strokeText 方法绘制文本,并使用 font、textAlign 等属性设置字体和样式。在流程图中,通常需要为每个节点或流程步骤添加描述性文字。

    ctx.font = '20px Arial';
    ctx.fillStyle = 'black';
    ctx.textAlign = 'center';
    ctx.fillText('开始', 200, 50);  // 在(200, 50)位置绘制居中的文本

说明:

  • font:设置文本的字体和大小
  • fillText(text, x, y):填充绘制文本
  • strokeText(text, x, y):描边绘制文本
  • textAlign:设置文本对齐方式(如:left、center、right)

阴影效果(Shadow)

为了增加视觉效果,Canvas 还允许开发者为图形添加阴影。这些在流程图中可以增强节点的立体感或突出部分内容。

    ctx.shadowColor = 'rgba(0, 0, 0, 0.5)';
    ctx.shadowBlur = 10;
    ctx.shadowOffsetX = 5;
    ctx.shadowOffsetY = 5;

    ctx.fillStyle = 'lightgreen';
    ctx.fillRect(100, 100, 100, 50);  // 带阴影的矩形

说明:

  • shadowColor:阴影的颜色
  • shadowBlur:阴影模糊的程度
  • shadowOffsetX 和 shadowOffsetY:阴影的偏移量

渐变和图案填充

Canvas 支持线性渐变和放射渐变,适用于流程图中节点的颜色渐变处理。另外,Canvas 也可以使用图片作为填充图案,这在设置流程图背景或网格时会非常有用。

线性渐变:

    const gradient = ctx.createLinearGradient(0, 0, 200, 0);
    gradient.addColorStop(0, 'red');
    gradient.addColorStop(1, 'blue');

    ctx.fillStyle = gradient;
    ctx.fillRect(100, 100, 200, 100);  // 渐变填充的矩形

图案填充:

    const img = new Image();
    img.src = 'pattern.png';
    img.onload = function() {
      const pattern = ctx.createPattern(img, 'repeat');
      ctx.fillStyle = pattern;
      ctx.fillRect(0, 0, 400, 300);  // 使用图案填充整个画布
    };

说明:

  • createLinearGradient(x0, y0, x1, y1):创建线性渐变
  • addColorStop(position, color):为渐变添加颜色
  • createPattern(image, repetition):创建基于图片的重复团

图形剪辑(Clip)

通过 clip() 方法,Canvas 可以限制绘图区域,确保绘制的内容仅在指定区域内。这对于流程图中局部显示或高亮某些区域非常有用。

    // 创建一个圆形剪辑区域
    ctx.beginPath();
    ctx.arc(150, 150, 100, 0, Math.PI * 2);
    ctx.clip();

    // 在剪辑区域内绘制
    ctx.fillStyle = 'lightblue';
    ctx.fillRect(0, 0, 300, 300);  // 只在圆形区域内显示

说明:

  • clip():将当前路径设为剪辑区域,之后的绘图操作只会影响该区域

Canvas 状态管理

Canvas 的 save() 和 restore() 方法允许你保存和恢复画布的状态,这在处理复杂的流程图时非常重要。每次调用 save(),会将当前画布的属性(如颜色、变化、剪辑区域等)保存到一个栈中,之后可以通过 restore() 恢复该状态。

    ctx.save();  // 保存当前状态
    ctx.fillStyle = 'red';
    ctx.fillRect(10, 10, 100, 100);  // 绘制红色矩形


    ctx.restore();  // 恢复之前的状态
    ctx.fillRect(150, 10, 100, 100);  // 使用原来的状态绘制

说明:

  • save() 和 restore():管理复杂绘图场景中不同状态的切换,避免全局属性影响局部绘制。

高效绘图和性能优化

由于 Canvas 是基于像素的绘图,每次更新或重新绘制都可能涉及大量计算。对于复杂的流程图,保持高性能尤为重要。这里介绍一些常见的优化策略:

  • 双缓冲技术:

通过创建一个离屏 Canvas,现在离屏 Canvas 上绘制图形,再将结果渲染到主屏幕上,可以避免频繁的重回造成的性能瓶颈

    const offscreenCanvas = document.createElement('canvas');
    const offscreenCtx = offscreenCanvas.getContext('2d');
    offscreenCanvas.width = 400;
    offscreenCanvas.height = 300;


    // 在离屏 Canvas 上绘制
    offscreenCtx.fillStyle = 'blue';
    offscreenCtx.fillRect(0, 0, 100, 100);


    // 将离屏 Canvas 的内容复制到主 Canvas
    ctx.drawImage(offscreenCanvas, 0, 0);
  • 减少重绘

在不需要的时候,尽量避免不必要的 clearRect() 和重绘操作

  • 合并绘制操作

将多次绘制操作合并到一个 beginPath() 中,这样可以减少上下文切换的消耗

处理图片和导出功能

Canvas 可以通过 drawImage() 方法加载和操作图片,也可以将 Canvas 的内容导出为图像(如 PNG、JPEG)。这在流程图导出功能中非常有用。

加载图片:

    const img = new Image();
    img.src = 'example.png';
    img.onload = function() {
      ctx.drawImage(img, 50, 50, 100, 100);  // 将图片绘制到画布
    };

导出 Canvas 内容为图片

    const image = canvas.toDataURL('image/png');
    const link = document.createElement('a');
    link.href = image;
    link.download = 'flowchart.png';
    link.click();

说明:

  • drawImage(image, x, y):将图片绘制到指定为止
  • toDataURL(type):将 Canvas 内容导出为指定类型的图像数据

总结

在了解完这些基础知识以及一些常用的技巧之后,我们将在下一章具体介绍 SVG 和 Canvas 在一些关键模块实现的技术差异。