Canvas 2D API

深入理解 Canvas 2D API 的完整绘图功能:路径、形状、文本、图像处理、像素操作,以及高性能动画实现。

Canvas 2D API(Canvas 2D API)

一、Canvas 基础

1.1 创建 Canvas

<canvas id="myCanvas" width="800" height="600">
  您的浏览器不支持 Canvas
</canvas>
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');

// 获取 2D 上下文
console.log('Context:', ctx);

1.2 Canvas 坐标系统

Canvas 使用以左上角为原点的笛卡尔坐标系:

(0,0) ──────────→ X
  │
  │     (x, y)
  │
  ↓
  Y

1.3 基本属性

// 填充和描边颜色
ctx.fillStyle = '#FF0000';  // 红色
ctx.strokeStyle = '#0000FF';  // 蓝色

// 线宽
ctx.lineWidth = 2;

// 透明度
ctx.globalAlpha = 0.5;

二、绘制形状

2.1 矩形

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

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

// 清除矩形(透明)
ctx.clearRect(0, 0, canvas.width, canvas.height);

2.2 路径绘制

// 开始路径
ctx.beginPath();

// 移动到起点
ctx.moveTo(0, 0);

// 画线
ctx.lineTo(100, 100);
ctx.lineTo(200, 50);

// 弧线
ctx.arc(100, 100, 50, 0, Math.PI * 2);  // 整圆

// 贝塞尔曲线
ctx.quadraticCurveTo(cp1x, cp1y, x, y);  // 二次
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);  // 三次

// 闭合路径
ctx.closePath();

// 填充路径
ctx.fill();

// 描边路径
ctx.stroke();

2.3 圆形和椭圆

// 圆形
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle, counterclockwise);
ctx.fill();

// 椭圆
ctx.beginPath();
ctx.ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle);
ctx.fill();

三、文本绘制

3.1 文本基础

ctx.font = '24px Arial';
ctx.fillText('Hello Canvas', 50, 50);
ctx.strokeText('Hello Canvas', 50, 100);

3.2 文本样式

// 字体
ctx.font = 'bold 24px/1.5 Arial, sans-serif';

// 文本对齐
ctx.textAlign = 'left';  // left, center, right, start, end
ctx.textBaseline = 'top';  // top, hanging, middle, alphabetic, ideographic, bottom

// 文本度量
const metrics = ctx.measureText('Hello');
console.log('Width:', metrics.width);

3.3 文本渐变

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

ctx.font = '48px Arial';
ctx.fillStyle = gradient;
ctx.fillText('Gradient Text', 50, 100);

四、图像处理

4.1 绘制图像

const img = new Image();
img.onload = () => {
  // 绘制图像
  ctx.drawImage(img, 0, 0);

  // 缩放绘制
  ctx.drawImage(img, 0, 0, 200, 150);

  // 部分绘制
  ctx.drawImage(img, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
};
img.src = 'image.jpg';

4.2 图像合成

// 全局合成操作
ctx.globalCompositeOperation = 'source-over';  // 默认

// 其他模式:
// source-atop, source-in, source-out
// destination-atop, destination-in, destination-out
// lighter, copy, xor
// multiply, screen, overlay, darken, lighten, color-dodge, color-burn

4.3 裁剪

ctx.save();

// 创建裁剪路径
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2);
ctx.clip();

// 在裁剪区域内绘制
ctx.drawImage(img, 0, 0);

ctx.restore();  // 恢复

五、样式和渐变

5.1 线性渐变

const gradient = ctx.createLinearGradient(x0, y0, x1, y1);
gradient.addColorStop(0, 'red');
gradient.addColorStop(0.5, 'green');
gradient.addColorStop(1, 'blue');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 300, 100);

5.2 径向渐变

const gradient = ctx.createRadialGradient(
  x0, y0, r0,    // 内圆
  x1, y1, r1     // 外圆
);
gradient.addColorStop(0, 'yellow');
gradient.addColorStop(1, 'orange');

ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 300, 300);

5.3 阴影

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

ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 100, 100);

六、变换

6.1 基本变换

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

// 平移
ctx.translate(50, 50);

// 旋转(弧度)
ctx.rotate(Math.PI / 4);

// 缩放
ctx.scale(2, 2);

// 绘制
ctx.fillRect(0, 0, 100, 100);

ctx.restore();  // 恢复状态

6.2 矩阵变换

ctx.save();

ctx.transform(a, b, c, d, e, f);
// a: 水平缩放
// b: 水平倾斜
// c: 垂直倾斜
// d: 垂直缩放
// e: 水平移动
// f: 垂直移动

ctx.restore();

6.3 变换顺序

ctx.translate(100, 100);
ctx.rotate(Math.PI / 4);
ctx.translate(50, 50);

// 结果:先平移到 (100,100),旋转 45 度,再平移 (50,50)
// 最终变换 = T * R * T(从右到左应用)

七、像素操作

7.1 ImageData

// 获取像素数据
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const data = imageData.data;  // Uint8ClampedArray

// 遍历像素
for (let i = 0; i < data.length; i += 4) {
  const r = data[i];      // 红色
  const g = data[i + 1];  // 绿色
  const b = data[i + 2];  // 蓝色
  const a = data[i + 3];  // 透明度
}

// 修改像素
for (let i = 0; i < data.length; i += 4) {
  data[i] = 255 - data[i];       // 反色红色
  data[i + 1] = 255 - data[i + 1]; // 反色绿色
  data[i + 2] = 255 - data[i + 2]; // 反色蓝色
}

// 放回 canvas
ctx.putImageData(imageData, 0, 0);

7.2 创建 ImageData

// 创建一个新的 ImageData
const newImageData = ctx.createImageData(100, 100);

// 或者从已有的 ImageData 复制
const copiedImageData = ctx.createImageData(existingImageData);

八、动画

8.1 动画循环

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

  // 更新状态
  x += speed;

  // 绘制
  ctx.fillRect(x, y, width, height);

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

requestAnimationFrame(animate);

8.2 动画示例:弹跳球

class Ball {
  constructor() {
    this.x = canvas.width / 2;
    this.y = canvas.height / 2;
    this.vx = 3;
    this.vy = 2;
    this.radius = 20;
  }

  update() {
    this.x += this.vx;
    this.y += this.vy;

    // 碰撞检测
    if (this.x - this.radius < 0 || this.x + this.radius > canvas.width) {
      this.vx = -this.vx;
    }
    if (this.y - this.radius < 0 || this.y + this.radius > canvas.height) {
      this.vy = -this.vy;
    }
  }

  draw() {
    ctx.beginPath();
    ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
    ctx.fillStyle = 'red';
    ctx.fill();
  }
}

const ball = new Ball();

function animate() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  ball.update();
  ball.draw();

  requestAnimationFrame(animate);
}

animate();

九、性能优化

9.1 分层 Canvas

<canvas id="bg" style="position: absolute;"></canvas>
<canvas id="fg" style="position: absolute;"></canvas>
const bgCanvas = document.getElementById('bg');
const fgCanvas = document.getElementById('fg');
const bgCtx = bgCanvas.getContext('2d');
const fgCtx = fgCanvas.getContext('2d');

// 静态背景只绘制一次
function drawBackground() {
  // ... 绘制背景
}

// 频繁更新的前景
function drawForeground() {
  // ... 绘制前景
}

9.2 离屏 Canvas

const offscreen = document.createElement('canvas');
offscreen.width = canvas.width;
offscreen.height = canvas.height;
const offCtx = offscreen.getContext('2d');

// 在离屏 canvas 绘制复杂图形
function prepareComplexShape() {
  offCtx.clearRect(0, 0, offscreen.width, offscreen.height);
  // ... 绘制复杂图形
}

// 在主 canvas 复制
ctx.drawImage(offscreen, 0, 0);

参考资料

延展阅读