Canvas是HTML5最具代表性的特性之一,它为Web页面带来了动态图形和图像处理的能力。无论是数据可视化、游戏开发、图像编辑还是创意编程,Canvas都扮演着核心角色。本文将带领您系统学习Canvas的基础知识,并深入探讨其高级用法,帮助您全面掌握这一强大的绘图技术。

一、Canvas 基础

1.1 创建 Canvas 元素

Canvas 是一个矩形区域的容器,通过 JavaScript 在区域内绘制图形。在 HTML 中定义 Canvas 非常简单:

<canvas id="myCanvas" width="800" height="600">
  您的浏览器不支持 Canvas,请升级或更换浏览器。
</canvas>
  • width 和 height 属性设置画布的像素尺寸(注意:CSS 设置的宽高会影响显示比例,但绘图区域仍以属性值为准)。

  • 标签内的文本是降级内容,仅在浏览器不支持 Canvas 时显示。

1.2 获取 2D 绘图上下文

要真正开始绘图,必须获取 Canvas 的绘图上下文(context)。2D 上下文提供了所有绘制方法。

const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
1.3 基本绘图操作
绘制矩形

Canvas 提供了三个绘制矩形的方法:fillRect、strokeRect 和 clearRect。

// 填充矩形
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 50);   // (x, y, width, height)

// 描边矩形
ctx.strokeStyle = 'blue';
ctx.lineWidth = 2;
ctx.strokeRect(120, 10, 100, 50);

// 清除矩形区域
ctx.clearRect(20, 20, 80, 30);   // 擦除部分区域
绘制路径

路径是绘制复杂图形的基础。通过 beginPath() 开始新路径,然后使用 moveTo、lineTo 等方法定义路径,最后调用 stroke() 或 fill() 进行绘制。

ctx.beginPath();
ctx.moveTo(50, 50);   // 起点
ctx.lineTo(150, 50);  // 画到 (150,50)
ctx.lineTo(100, 150); // 画到 (100,150)
ctx.closePath();  // 闭合路径(回到起点)
ctx.stroke();   // 描边
// 也可以 ctx.fill() 填充
绘制圆形和弧线

使用 arc() 方法绘制圆弧或圆。

ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);  // 圆心 (200,200),半径50,起始角0,结束角2π
ctx.fillStyle = 'green';
ctx.fill();
1.4 样式和颜色

颜色设置

fillStyle 和 strokeStyle 可以接受颜色字符串、渐变色或图案。

ctx.fillStyle = 'red';               // 颜色名称
ctx.fillStyle = '#ff0000';            // 十六进制
ctx.fillStyle = 'rgb(255,0,0)';       // RGB
ctx.fillStyle = 'rgba(255,0,0,0.5)';  // RGBA 带透明度
渐变

Canvas 支持线性渐变和径向渐变。

// 线性渐变
const gradient = ctx.createLinearGradient(0, 0, 200, 0);  // 从 (0,0) 到 (200,0)
gradient.addColorStop(0, 'red');
gradient.addColorStop(1, 'blue');
ctx.fillStyle = gradient;
ctx.fillRect(10, 10, 200, 100);

// 径向渐变
const radialGradient = ctx.createRadialGradient(100, 100, 0, 100, 100, 50);
radialGradient.addColorStop(0, 'white');
radialGradient.addColorStop(1, 'black');
ctx.fillStyle = radialGradient;
ctx.fillRect(250, 10, 200, 100);
1.5 文本绘制

使用 fillText 和 strokeText 绘制文本,并可通过 font、textAlign 等属性设置样式。

ctx.font = '30px Arial';
ctx.fillStyle = 'black';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Hello Canvas', canvas.width / 2, canvas.height / 2);
1.6 图像操作
绘制图像

通过 drawImage 方法可以将 Image 对象、Canvas 元素或视频帧绘制到画布上。

const img = new Image();
img.onload = function() {
  // 在 (0,0) 绘制原图
  ctx.drawImage(img, 0, 0);
  // 缩放绘制:在 (0,0) 绘制宽100、高100的图像
  ctx.drawImage(img, 0, 0, 100, 100);
};
img.src = 'image.png';
1.7 变换操作

Canvas 支持平移、旋转、缩放等变换,且可以通过 save 和 restore 管理状态栈。

ctx.save();   // 保存当前状态

ctx.translate(100, 100);  // 平移
ctx.rotate(Math.PI / 4);  // 旋转 45°
ctx.scale(2, 0.5);  // 缩放

// 绘制变换后的图形
ctx.fillRect(0, 0, 50, 50);

ctx.restore();  // 恢复到保存的状态
1.8 动画实现

动画的核心是反复清除画布并重新绘制。使用 requestAnimationFrame 。

let x = 0;
const speed = 2;

function animate() {
  // 1. 清除画布
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // 2. 更新位置
  x += speed;
  if (x > canvas.width) x = 0;

  // 3. 绘制
  ctx.fillRect(x, 50, 50, 50);

  // 4. 请求下一帧
  requestAnimationFrame(animate);
}

animate();
1.9 事件处理

Canvas 本身不记录绘制的图形,但可以通过坐标计算实现交互。

canvas.addEventListener('click', (event) => {
  const rect = canvas.getBoundingClientRect();
  const x = event.clientX - rect.left;
  const y = event.clientY - rect.top;

  // 检查点是否在路径内(需要提前构建路径)
  if (ctx.isPointInPath(x, y)) {
    console.log('点击了图形!');
  }
});
1.10 性能优化技巧
离屏 Canvas

预先在内存中的 Canvas 绘制复杂图形,然后快速复制到主画布上,减少重复计算。

const offCanvas = document.createElement('canvas');
const offCtx = offCanvas.getContext('2d');

// 在离屏画布上绘制复杂图形
offCtx.fillStyle = 'red';
offCtx.fillRect(0, 0, 200, 200);

// 在主画布上直接绘制离屏画布
ctx.drawImage(offCanvas, 0, 0);
批量操作

减少不必要的状态改变,尽量将相同样式的绘制集中处理。

ctx.save();
ctx.fillStyle = 'red';
ctx.strokeStyle = 'blue';
for (let i = 0; i < 100; i++) {
  ctx.fillRect(i * 10, 0, 8, 8);
}
ctx.restore();
1.11 实际应用示例:简单绘图板

实现一个基本的鼠标绘图功能。

let isDrawing = false;
let lastX = 0, lastY = 0;

canvas.addEventListener('mousedown', (e) => {
  isDrawing = true;
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mousemove', (e) => {
  if (!isDrawing) return;
  ctx.beginPath();
  ctx.moveTo(lastX, lastY);
  ctx.lineTo(e.offsetX, e.offsetY);
  ctx.stroke();
  [lastX, lastY] = [e.offsetX, e.offsetY];
});

canvas.addEventListener('mouseup', () => isDrawing = false);
canvas.addEventListener('mouseleave', () => isDrawing = false);
1.12 常见问题解决:高清屏适配

在 Retina 等高清屏幕上,Canvas 可能模糊。需要根据设备像素比调整画布尺寸。

const dpr = window.devicePixelRatio || 1;
const displayWidth = canvas.clientWidth;
const displayHeight = canvas.clientHeight;

canvas.width = displayWidth * dpr;
canvas.height = displayHeight * dpr;
canvas.style.width = displayWidth + 'px';
canvas.style.height = displayHeight + 'px';

ctx.scale(dpr, dpr);

二、Canvas 进阶

2.1 Canvas 绘图的通用流程

无论简单还是复杂的绘图,都遵循一个清晰的流程:

  1. 获取上下文(2D 或 WebGL)。

  2. 设置样式(颜色、线宽、渐变等)。

  3. 构建路径(或直接调用绘图方法)。

  4. 执行绘制(填充、描边等)。

  5. 动画循环(如需动效,重复以上步骤)。

这一流程体现了 Canvas 的“即时模式”绘图思想,每次绘制都是直接操作像素,不保留图形对象。

2.2 Canvas 必须依赖 JavaScript 吗?

是的,Canvas 完全依赖 JavaScript 实现绘图。

<canvas> 标签本身只是一个空容器,不提供任何绘图能力。真正的绘制工作必须通过 JavaScript 调用绘图 API 完成。如果禁用 JavaScript,Canvas 区域将是一片空白(除非有降级内容)。这种设计使得 Canvas 极其灵活,可以与用户交互、动态生成内容,并与各种 JavaScript 库无缝集成。

3. 高阶用法
3.1 WebGL – 3D 图形渲染

通过 getContext('webgl') 可以获取 WebGL 上下文,利用 GPU 加速渲染 3D 场景。以下是一个极简的三角形绘制示例:

const gl = canvas.getContext('webgl');
const vsSource = `
  attribute vec4 aPosition;
  void main() {
    gl_Position = aPosition;
  }
`;
const fsSource = `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`;

// 编译着色器、创建程序、绑定缓冲区等(详细代码略)
// 最后绘制
gl.drawArrays(gl.TRIANGLES, 0, 3);

WebGL 编程较为复杂,通常借助 Three.js 等库简化开发。

3.2 图像处理与滤镜

通过 getImageData 和 putImageData 可以逐像素操作图像,实现各种滤镜效果。

function applyGrayscale() {
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
  const data = imageData.data;
  for (let i = 0; i < data.length; i += 4) {
    const gray = 0.299 * data[i] + 0.587 * data[i+1] + 0.114 * data[i+2];
    data[i] = data[i+1] = data[i+2] = gray;
  }
  ctx.putImageData(imageData, 0, 0);
}
3.3 物理引擎集成

将 Canvas 与简单的物理模拟结合,可以创建逼真的运动效果。

class Ball {
  constructor(x, y, vx, vy) {
    this.x = x; this.y = y;
    this.vx = vx; this.vy = vy;
    this.radius = 10;
  }
  update() {
    this.vy += 0.5; // 重力
    this.x += this.vx;
    this.y += this.vy;
    // 边界反弹
    if (this.y + this.radius > canvas.height) {
      this.y = canvas.height - this.radius;
      this.vy *= -0.8;
    }
  }
  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
    ctx.fillStyle = 'blue';
    ctx.fill();
  }
}

// 在动画循环中更新并绘制所有小球
3.4 数据可视化

Canvas 是绘制图表、地图等可视化内容的利器。可以封装一个简单的条形图组件:

class BarChart {
  constructor(canvas, data) {
    this.ctx = canvas.getContext('2d');
    this.data = data;
    this.width = canvas.width;
    this.height = canvas.height;
  }
  draw() {
    const max = Math.max(...this.data);
    const barWidth = this.width / this.data.length;
    this.data.forEach((value, i) => {
      const barHeight = (value / max) * this.height * 0.8;
      const x = i * barWidth;
      const y = this.height - barHeight;
      this.ctx.fillStyle = `hsl(${i * 30}, 70%, 50%)`;
      this.ctx.fillRect(x, y, barWidth - 2, barHeight);
    });
  }
}
3.5 游戏开发框架

利用 Canvas 可以构建游戏循环和实体组件系统。以下是一个简单的游戏循环模板:

class Game {
  constructor(canvas) {
    this.ctx = canvas.getContext('2d');
    this.entities = [];
    this.lastTime = 0;
  }
  start() {
    this.gameLoop(performance.now());
  }
  gameLoop(now) {
    const delta = (now - this.lastTime) / 1000;
    this.lastTime = now;

    this.update(delta);
    this.render();

    requestAnimationFrame((t) => this.gameLoop(t));
  }
  update(delta) {
    this.entities.forEach(e => e.update(delta));
  }
  render() {
    this.ctx.clearRect(0, 0, canvas.width, canvas.height);
    this.entities.forEach(e => e.draw(this.ctx));
  }
}
3.6 高级性能优化

离屏渲染(OffscreenCanvas)可以在 Worker 线程中执行绘图,避免阻塞主线程。浏览器支持 OffscreenCanvas 接口。

// 主线程
const offscreen = canvas.transferControlToOffscreen();
const worker = new Worker('worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// worker.js
self.onmessage = (e) => {
  const canvas = e.data.canvas;
  const ctx = canvas.getContext('2d');
  // 在 Worker 中绘制
  ctx.fillStyle = 'red';
  ctx.fillRect(0, 0, 100, 100);
};

Web Workers 处理像素数据:将耗时的图像处理任务交给 Worker,避免界面卡顿。

// 主线程
const worker = new Worker('filter-worker.js');
worker.postMessage(imageData);
worker.onmessage = (e) => {
  ctx.putImageData(e.data, 0, 0);
};

// filter-worker.js
self.onmessage = (e) => {
  const imageData = e.data;
  // 处理 imageData ...
  self.postMessage(imageData);
};
3.7 现代 Canvas 框架和库
  • Fabric.js:提供对象模型和交互能力,适合图形编辑器。

  • Konva.js:高性能 2D 绘图库,支持事件绑定和动画。

  • Paper.js:基于矢量图形的脚本框架,使用场景图。

  • PixiJS:2D WebGL 渲染引擎,性能卓越,适合游戏和交互应用。

  • Three.js:最流行的 3D 库,封装了 WebGL 细节。

这些库大大简化了复杂场景的开发,开发者可以根据需求选择合适的工具。

总结

Canvas 是 Web 图形开发的基石,从简单的矩形绘制到复杂的 3D 渲染,它提供了丰富的 API 和无限的扩展可能。本文从基础概念出发,涵盖了元素创建、上下文获取、基本绘图、样式、变换、动画、交互和性能优化,随后深入探讨了 WebGL、图像处理、物理引擎、数据可视化、游戏框架以及现代库的应用。掌握这些知识后,您将能够自信地使用 Canvas 构建各种创意项目,无论是数据可视化、小游戏还是图像处理工具。未来,随着 WebGPU 等新技术的出现,Canvas 的能力还将进一步扩展,值得持续关注和学习。

彩蛋

文中配有完整示例,点击《Canvas 基础示例》即可查看。