JS-Canvas进阶指南:从零构建你的 Web 图形世界

187 阅读6分钟

前言

在数据可视化(ECharts)、在线文档、H5 游戏等领域,Canvas 都是不可或缺的核心技术。不同于 DOM 操作,Canvas 提供了立即模式的绘图能力。本文将带你系统盘点 Canvas 的核心 API 与实战技巧。

一、 快速上手:环境搭建

Canvas 就像一块画布,而 context 则是你的画笔,Canvas使用方法如下:

  • 创建canvas元素,并为其设置width和height
  • 通过id查找到该元素
  • 使用getContext('2d')获取绘制图形的上下文
<canvas id="drawing" width="200" height="200"></canvas>
const drawing = document.getElementById("drawing");

// 严谨起见,先检查浏览器是否支持 getContext
if (drawing.getContext) {
  const context = drawing.getContext("2d");
  console.log("Canvas 上下文获取成功");
}

二、 基础图形:矩形绘制

矩形是 Canvas 中唯一原生支持的形状,其他图形都需要通过路径组合。

方法描述
fillRect(x, y, w, h)绘制填充矩形(实心)
strokeRect(x, y, w, h)绘制描边矩形(空心)
clearRect(x, y, w, h)清除指定区域(橡皮擦效果)
  • 这三种方法都接受4个参数:

    • x代表绘制矩形的起点横坐标
    • y代表绘制矩形的起点纵坐标(坐标轴是向下的)
    • w代表绘制矩形的宽度(从x位置向右延升w距离)
    • h代表会在矩形的高度(从y位置向下延升h距离)
  • fillRect绘制出来的矩形,可使用fillStyle('颜色值')可以给矩形填充颜色

  • storkeRect绘制出来的矩形,可使用strokeStyle('颜色值')可以给矩形绘制轮廓

const context = drawing.getContext("2d");

// 1. 填充绿色矩形
context.fillStyle = "green";
context.fillRect(10, 10, 50, 50);

// 2. 红色边框矩形
context.strokeStyle = "red";
context.lineWidth = 2; // 设置线宽
context.strokeRect(70, 10, 50, 50);

// 3. 橡皮擦:擦除中间一部分
context.clearRect(15, 15, 20, 20); 

image.png


三、 路径的艺术:绘制任意形状

路径是 Canvas 的灵魂。记住核心流程:开始路径 -> 移动画笔 -> 绘制线条 -> 闭合/描边

1. 核心 API

  • beginPath() : 清空当前路径列表,开始新路径(防止之前的路径被重复绘制)。
  • moveTo(x, y) : 移动画笔,只把绘制起始点移动到(x, y)
  • lineTo(x, y) : 绘制一条从上个结束点到(x, y)的直线。
  • arc(x, y, radius, startAngle, endAngle, counterclockwise):以坐标(x, y)为圆心,以 radius 为半径绘制一条弧线,起始角度为 startAngle,结束角度为 endAnglecounterclockwise 表示是否逆时针计算起始角度和结束角度(默认为顺时针)
  • arcTo(x1, y1, x2, y2, radius) :绘制从起始点P0P1(x1,y1)的一条连线,截止绘制从P1(x1,y1)P2(x2,y2)的连线,接着将这两条线当做切线绘制一个半径为radius的圆弧。(当圆弧过大时,会取两条切线的延长线)使用较少

2. 实战:绘制切线圆弧 (arcTo)

 <canvas id="drawing" width="600" height="600" style="border: 1px solid aqua"></canvas>
   
  let drawing = document.getElementById('drawing')
  if (drawing.getContext) {
    let context = drawing.getContext('2d')
    const p0 = { x: 50, y: 50 } // 起点
    const p1 = { x: 150, y: 100 } // 控制点1
    const p2 = { x: 250, y: 50 } // 控制点2
    const radius =100 // 圆角半径

    context.beginPath()
    context.moveTo(p0.x, p0.y) // 定位起点
    context.arcTo(p1.x, p1.y, p2.x, p2.y, radius) // 绘制圆弧
    // context.lineTo(p2.x, p2.y) // 连接到终点
    context.stroke() // 描边
  }1.y, p2.x, p2.y, radius); 
context.stroke();

image.png

3. 实战:绘制时钟表盘

  let drawing = document.getElementById('drawing')
  if (drawing.getContext) {
    let context = drawing.getContext('2d')
    // 创建路径
    context.beginPath()
    // 绘制外圆
    context.arc(100, 100, 99, 0, 2 * Math.PI, false)
    // 绘制内圆
    context.moveTo(194, 100)
    context.arc(100, 100, 94, 0, 2 * Math.PI, false)
    // 绘制分针
    context.moveTo(100, 100)
    context.lineTo(100, 15)
    // 绘制时针
    context.moveTo(100, 100)
    context.lineTo(35, 100)
    // 描画路径
    context.stroke()
  }

image.png


四、 绘制文本

Canvas 文本也是图像的一部分,无法像 DOM 文本那样选中。

属性/方法描述
font设置字体,如 "bold 20px Arial"
textAlign水平对齐:start, end, left, right, center
textBaseline垂直对齐:top, middle, bottom
fillText(text, x, y, maxWidth)绘制实心文本,text为文本内容(字符串),xy为文本坐标,maxwidth为可选参数表示最大宽度
strokeText(text, x, y,maxWidth)绘制空心文本,参数同fillText,同fillText一起调用可实现填充+描边效果
context.font = '20px Arial';
context.textAlign = 'center';

// 实心字
context.fillStyle = 'blue';
context.fillText('Combined', 150, 150);

// 空心描边字(重叠效果)
context.strokeStyle = 'red';
context.strokeText('Combined', 150, 150);

五、 图像处理与像素操作

1. 绘制图片 (drawImage)

注意:必须等待图片加载完成后才能绘制,否则画布是空的。

const img = new Image();
img.src = 'test.jpg';
img.onload = function() {
    // 参数:图片对象, x, y, width, height
    // 还可以传入9个参数实现裁剪:drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh)
    context.drawImage(img, 0, 0, 200, 200);
}

2. 像素级操作 (ImageData)

这是 Canvas 最强大的功能,可用于滤镜、取色器等。

  • getImageData(x, y, w, h) :获取像素数据,返回值为ImageData 的实例,包含三个属性widthheightdata其data 属性是包含图像的原始像素信息的数组,每 4 个值代表一个像素 (R,G,B,A)(R, G, B, A)
  • putImageData(imageData, x, y) :将图像数据再绘制到画布上,ImageData:为ImageData实例,w,h表示绘制图像的宽高。

跨域警告:如果绘制的图片跨域且未开启 CORS,调用 getImageData 会报错(画布被污染)。


六、 图像合成 (Composite)

决定了当两个图形重叠时,谁显示、谁隐藏,或者颜色如何混合。

  • globalAlpha:全局透明度 (01)(0 \sim 1)
  • globalCompositeOperation:该属性定义新图形与画布已有内容的像素混合规则,其取值如下:
属性值 (Value)效果描述典型场景
source-over (默认)新图形覆盖在旧图形上方最普通的绘图模式
source-in仅显示新图形与旧图形重叠的部分(显示新图形内容)裁剪图形(如:将图片裁成圆形头像)
source-out仅显示新图形与旧图形不重叠的部分镂空效果、反向遮罩
destination-over新图形绘制在旧图形下方背景叠加(如:给已有文字加底色)
destination-out擦除旧图形中与新图形重叠的部分橡皮擦功能实现
lighter重叠区域颜色值相加(变亮效果)制作光效、粒子系统、火焰效果

经典案例:刮刮乐效果 (destination-out)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <style>
    /* 容器设置:保证文字和Canvas重叠 */
    .scratch-card {
      position: relative;
      width: 300px;
      height: 150px;
      margin: 20px auto;
      user-select: none; /* 禁止选中文字 */
    }
    
    /* 底层的中奖文字 */
    .prize-text {
      position: absolute;
      width: 100%;
      height: 100%;
      line-height: 150px;
      text-align: center;
      font-size: 40px;
      color: red;
      font-weight: bold;
      background-color: #f9f9f9;
      border: 1px solid #ccc;
      z-index: 1; /* 层级较低 */
    }

    /* 上层的 Canvas 遮罩 */
    canvas {
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2; /* 层级较高,盖住文字 */
      cursor: pointer;
    }
  </style>
</head>
<body>

  <div class="scratch-card">
    <div class="prize-text">🎉 中奖了!</div>
    <canvas id="mask" width="300" height="150"></canvas>
  </div>

  <script>
    const canvas = document.getElementById('mask');
    const ctx = canvas.getContext('2d');
    
    // 1. 初始化:填充灰色遮罩
    ctx.fillStyle = '#cccccc'; // 刮奖区的颜色
    ctx.fillRect(0, 0, 300, 150);
    
    // 状态标记:是否正在按下鼠标
    let isDrawing = false;

    // 2. 鼠标/触摸交互事件监听
    canvas.addEventListener('mousedown', () => isDrawing = true);
    canvas.addEventListener('mouseup', () => isDrawing = false);
    // 鼠标移出画布也停止刮奖
    canvas.addEventListener('mouseleave', () => isDrawing = false); 

    canvas.addEventListener('mousemove', (e) => {
      if (!isDrawing) return;

      // 获取鼠标在 Canvas 中的坐标
      // e.offsetX / e.offsetY 是相对于事件源元素的坐标
      const x = e.offsetX;
      const y = e.offsetY;

      // --- 核心代码开始 ---
      // 设置混合模式为“擦除”(即:让重叠部分变透明)
      ctx.globalCompositeOperation = 'destination-out';

      // 绘制圆形作为“笔触”(圆形比矩形手感更好)
      ctx.beginPath();
      ctx.arc(x, y, 15, 0, Math.PI * 2); // 15是半径,控制橡皮擦大小
      ctx.fill();
      // --- 核心代码结束 ---
    });
  </script>
</body>
</html>