跳到主要内容

核心知识点

1. 什么是 Canvas?

HTML <canvas> 元素提供了一个通过 JavaScript API 来绘制图形的位图(bitmap)画布。它本身只是一个 DOM 元素,并不直接绘制任何内容。真正的绘图能力来自于其关联的渲染上下文 (Rendering Context),最常用的是 2D 上下文 (CanvasRenderingContext2D)。

核心特性:

  • 基于像素 (Pixel-based): Canvas 绘制的是位图,放大后会失真(出现像素格子)。
  • 即时模式 (Immediate Mode): 绘图命令立即执行,直接在画布上绘制像素。一旦绘制完成,Canvas 不会“记住”绘制的对象(没有图形对象的概念)。修改图形需要重绘整个场景或相关区域。
  • 依赖脚本 (Script-driven): 所有绘图操作都通过 JavaScript 完成。
  • 动态性: 非常适合实时图形渲染、动画、游戏和图像处理。

与 SVG (矢量图形) 不同,Canvas 不维护 DOM 结构来表示绘制的图形,这使得它在处理大量对象或高频率重绘时通常性能更高,但也意味着无法直接通过 CSS 或 DOM 事件来操作单个图形元素。

2. 基本设置

要使用 Canvas,首先需要在 HTML 中添加 <canvas> 元素,并通常为其指定 id, widthheight 属性。

<canvas id="myCanvas" width="500" height="300" style="border: 1px solid black;">
您的浏览器不支持 Canvas。
<!-- Fallback content -->
</canvas>

<script>
const canvas = document.getElementById("myCanvas");

// 检查浏览器是否支持 Canvas
if (canvas.getContext) {
// 获取 2D 渲染上下文
const ctx = canvas.getContext("2d");
console.log("Canvas context acquired:", ctx);
// 在这里开始绘图...
} else {
console.error("Canvas not supported in this browser.");
}
</script>
  • width, height: 定义画布内部绘图表面的像素尺寸。重要: 不要仅用 CSS 设置宽高,这只会拉伸/压缩画布元素及其内容,而不会改变绘图表面的分辨率。
  • getContext('2d'): 获取 CanvasRenderingContext2D 对象,该对象包含了所有 2D 绘图 API。Canvas 还支持 webglwebgl2 上下文用于 3D 图形。

3. 坐标系

Canvas 使用标准的二维笛卡尔坐标系:

  • 原点 (0, 0) 位于画布的左上角
  • X 轴正方向向右
  • Y 轴正方向向下
  • 所有坐标和尺寸单位都是像素
// ... 获取 ctx 上下文 ...

// 在 (50, 30) 处绘制一个点 (通过绘制一个 1x1 的小矩形模拟)
ctx.fillStyle = "red";
ctx.fillRect(50, 30, 1, 1);

// 在 (100, 80) 处绘制另一个点
ctx.fillStyle = "blue";
ctx.fillRect(100, 80, 1, 1);

4. 绘制形状

4.1 矩形 (Rectangles)

Canvas 提供了直接绘制矩形的便捷方法:

  • fillRect(x, y, width, height): 绘制一个填充的矩形。
  • strokeRect(x, y, width, height): 绘制一个矩形边框。
  • clearRect(x, y, width, height): 清除指定矩形区域内的所有内容,使其变为完全透明。常用于动画中的擦除操作。
// ... 获取 ctx 上下文 ...

// 设置样式
ctx.fillStyle = "rgba(0, 0, 255, 0.5)"; // 半透明蓝色填充
ctx.strokeStyle = "black"; // 黑色边框
ctx.lineWidth = 2; // 边框宽度

// 绘制填充矩形
ctx.fillRect(10, 10, 100, 50);

// 绘制描边矩形
ctx.strokeRect(150, 10, 80, 80);

// 清除一小块区域
ctx.clearRect(30, 20, 40, 20);

4.2 路径 (Paths)

路径是绘制复杂形状(直线、曲线、弧形等)的基础。绘制路径通常遵循以下步骤:

  1. beginPath(): 开始一个新的路径。这会清空当前的子路径列表。
  2. 使用移动和绘制命令定义路径:
    • moveTo(x, y): 将“画笔”移动到指定点,不绘制。通常是路径的起点。
    • lineTo(x, y): 从当前点绘制一条直线到指定点。
    • arc(x, y, radius, startAngle, endAngle, anticlockwise?): 绘制圆弧或圆。角度以弧度为单位 (0 弧度在 3 点钟方向,顺时针增加)。anticlockwise (可选, 默认 false) 指定绘制方向。
    • arcTo(x1, y1, x2, y2, radius): 绘制与当前点和指定点相切的圆弧。
    • quadraticCurveTo(cp1x, cp1y, x, y): 绘制二次贝塞尔曲线。(cp1x, cp1y) 是控制点,(x, y) 是终点。
    • bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): 绘制三次贝塞尔曲线。(cp1x, cp1y)(cp2x, cp2y) 是控制点,(x, y) 是终点。
    • rect(x, y, width, height): 在当前路径中添加一个矩形子路径 (注意:这与 fillRect/strokeRect 不同,它只定义路径,不直接绘制)。
  3. closePath() (可选): 从当前点绘制一条直线回到路径的起点,闭合形状。
  4. fill()stroke():
    • fill(fillRule?): 使用当前的 fillStyle 填充路径包围的区域。fillRule (可选, 'nonzero' 或 'evenodd') 决定如何处理自交路径的填充。
    • stroke(): 使用当前的 strokeStyle, lineWidth 等样式描绘路径的轮廓。
// ... 获取 ctx 上下文 ...

// 绘制一个填充的三角形
ctx.beginPath();
ctx.moveTo(50, 150); // 起点
ctx.lineTo(100, 200); // 第二个顶点
ctx.lineTo(0, 200); // 第三个顶点
ctx.closePath(); // 闭合路径 (连接回起点 50, 150)
ctx.fillStyle = "green";
ctx.fill(); // 填充

// 绘制一个描边的圆弧 + 直线 + 贝塞尔曲线
ctx.beginPath();
ctx.strokeStyle = "purple";
ctx.lineWidth = 3;
ctx.moveTo(150, 150);
ctx.arc(150, 150, 40, 0, Math.PI * 1.5); // 绘制 3/4 圆弧 (0 到 270度)
ctx.lineTo(250, 150);
ctx.bezierCurveTo(280, 100, 320, 200, 350, 150); // 三次贝塞尔曲线
// 注意:没有 closePath(),路径是开放的
ctx.stroke(); // 描边

5. 样式 (Styling)

可以通过设置 CanvasRenderingContext2D 对象的属性来控制绘制内容的样式。

5.1 颜色、渐变和图案

  • fillStyle: 设置填充内容的颜色、渐变或图案。
  • strokeStyle: 设置描边(线条)的颜色、渐变或图案。

颜色值: 可以是 CSS 颜色字符串(如 'red', '#ff0000', 'rgb(255, 0, 0)', 'rgba(255, 0, 0, 0.5)')。

渐变:

  • createLinearGradient(x0, y0, x1, y1): 创建线性渐变对象。(x0, y0) 是起点,(x1, y1) 是终点。
  • createRadialGradient(x0, y0, r0, x1, y1, r1): 创建径向(放射状)渐变对象。(x0, y0, r0) 是起始圆,(x1, y1, r1) 是结束圆。
  • gradient.addColorStop(offset, color): 向渐变对象添加颜色断点。offset 是 0 到 1 之间的值,表示颜色位置。

图案:

  • createPattern(image, repetition): 使用图像 (如 <img>, <video>, 或另一个 <canvas>) 创建图案对象。repetition 指定如何重复图案 ('repeat', 'repeat-x', 'repeat-y', 'no-repeat')。
// ... 获取 ctx 上下文 ...

// 线性渐变填充
const linearGrad = ctx.createLinearGradient(20, 220, 120, 320);
linearGrad.addColorStop(0, "gold");
linearGrad.addColorStop(0.5, "red");
linearGrad.addColorStop(1, "maroon");
ctx.fillStyle = linearGrad;
ctx.fillRect(20, 220, 100, 100);

// 径向渐变描边 (假设已加载 id="myImage" 的图片)
const radialGrad = ctx.createRadialGradient(230, 270, 10, 230, 270, 50);
radialGrad.addColorStop(0, "lightblue");
radialGrad.addColorStop(1, "darkblue");
ctx.strokeStyle = radialGrad;
ctx.lineWidth = 10;
ctx.strokeRect(180, 220, 100, 100);

// 图案填充 (需要先加载图片)
const img = new Image();
img.onload = function () {
const pattern = ctx.createPattern(img, "repeat");
ctx.fillStyle = pattern;
ctx.fillRect(300, 220, 100, 100);
};
img.src = "path/to/your/image.png"; // 替换为实际图片路径

5.2 线条样式

  • lineWidth: 线条宽度 (像素)。
  • lineCap: 线条末端的样式 ('butt' (默认), 'round', 'square')。
  • lineJoin: 线条连接处的样式 ('miter' (默认), 'round', 'bevel')。
  • miterLimit: 当 lineJoin'miter' 时,限制斜接角的延伸长度。
  • setLineDash(segments): 设置虚线模式。segments 是一个数组,描述实线和空白的长度交替,例如 [5, 15] 表示 5px 实线,15px 空白。
  • lineDashOffset: 虚线模式的起始偏移量。
  • getLineDash(): 获取当前的虚线模式数组。
// ... 获取 ctx 上下文 ...

ctx.beginPath();
ctx.moveTo(400, 10);
ctx.lineTo(480, 60);
ctx.lineTo(400, 110);

ctx.lineWidth = 10;
ctx.strokeStyle = "orange";
ctx.lineCap = "round"; // 圆形末端
ctx.lineJoin = "bevel"; // 斜角连接
ctx.setLineDash([10, 5]); // 10px 实线, 5px 空白
ctx.lineDashOffset = 5; // 从 5px 偏移开始绘制虚线

ctx.stroke();

5.3 阴影

  • shadowColor: 阴影颜色。
  • shadowOffsetX: 阴影的水平偏移量。
  • shadowOffsetY: 阴影的垂直偏移量。
  • shadowBlur: 阴影的模糊程度。

阴影会应用于后续绘制的所有形状、文本和图像。

// ... 获取 ctx 上下文 ...

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

// 这个矩形将带有阴影
ctx.fillStyle = "lime";
ctx.fillRect(400, 150, 80, 40);

// 绘制完成后可以重置阴影,避免影响后续绘制
ctx.shadowColor = "transparent"; // 或 ctx.shadowOffsetX = 0; 等

6. 绘制文本

  • fillText(text, x, y, maxWidth?): 在指定位置绘制填充的文本。maxWidth (可选) 限制文本的最大宽度,超出则压缩。
  • strokeText(text, x, y, maxWidth?): 在指定位置绘制描边的文本。

文本样式通过以下属性控制:

  • font: CSS 字体样式字符串 (例如 'bold 24px Arial')。
  • textAlign: 水平对齐方式 ('start' (默认), 'end', 'left', 'right', 'center'),相对于 (x, y) 点。
  • textBaseline: 垂直对齐方式 ('top', 'hanging', 'middle', 'alphabetic' (默认), 'ideographic', 'bottom'),相对于 (x, y) 点。
  • direction: 文本方向 ('ltr', 'rtl', 'inherit' (默认))。
// ... 获取 ctx 上下文 ...

ctx.font = "bold 30px sans-serif";
ctx.fillStyle = "teal";
ctx.textAlign = "center"; // 水平居中对齐
ctx.textBaseline = "middle"; // 垂直居中对齐

ctx.fillText("Hello Canvas!", canvas.width / 2, 100); // 在画布中心绘制

ctx.font = "italic 20px serif";
ctx.strokeStyle = "red";
ctx.textAlign = "left"; // 左对齐 (默认)
ctx.textBaseline = "top"; // 顶部对齐
ctx.strokeText("Stroke Text", 10, 120);

7. 使用图像

drawImage() 方法用于将图像绘制到 Canvas 上。它有三种重载形式:

  1. drawImage(image, dx, dy): 将图像 image 在画布的 (dx, dy) 位置绘制出来(原始尺寸)。
  2. drawImage(image, dx, dy, dWidth, dHeight): 将图像 image 在画布的 (dx, dy) 位置绘制出来,并缩放到 dWidth x dHeight 尺寸。
  3. drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight): 从源图像 image 中裁剪出 (sx, sy) 位置开始,sWidth x sHeight 大小的区域,然后将其绘制到画布的 (dx, dy) 位置,并缩放到 dWidth x dHeight 尺寸。

image 参数可以是 <img> 元素、<video> 元素(会绘制当前帧)、或另一个 <canvas> 元素。

重要: 必须确保图像加载完成后再调用 drawImage()

// ... 获取 ctx 上下文 ...

const myImage = new Image();
myImage.onload = () => {
// 1. 绘制原始尺寸图片
ctx.drawImage(myImage, 10, 10);

// 2. 绘制缩放后的图片
ctx.drawImage(myImage, 150, 10, 100, 80); // 假设原图是其他尺寸

// 3. 绘制裁剪并缩放后的图片
// 从原图 (20, 20) 处裁剪 50x50 区域,绘制到画布 (300, 10) 处,尺寸为 80x80
ctx.drawImage(myImage, 20, 20, 50, 50, 300, 10, 80, 80);
};
myImage.onerror = () => {
console.error("Image failed to load.");
};
myImage.src = "path/to/another/image.jpg"; // 替换为实际图片路径

8. 变换 (Transformations)

变换允许移动、旋转或缩放 Canvas 的坐标系,从而影响后续的绘图操作。

  • translate(x, y): 将坐标系原点移动到 (x, y)
  • rotate(angle): 围绕当前原点旋转坐标系。angle 以弧度为单位。
  • scale(x, y): 缩放坐标系。x 是水平缩放因子,y 是垂直缩放因子。
  • transform(a, b, c, d, e, f): 使用变换矩阵直接修改变换。参数对应矩阵:
    [ a c e ]
    [ b d f ]
    [ 0 0 1 ]
  • setTransform(a, b, c, d, e, f): 重置当前变换矩阵,然后设置为指定的矩阵。
  • resetTransform(): 重置变换矩阵为单位矩阵(无变换)。

状态保存与恢复: 由于变换是累积的,并且会影响所有后续绘图,因此管理坐标系状态非常重要。

  • save(): 保存当前绘图状态(包括变换矩阵、样式、裁剪区域等)到一个栈中。
  • restore(): 从栈顶恢复之前保存的绘图状态。

save()restore() 通常成对使用,以隔离变换或样式更改,确保它们不影响其他部分的绘图。

// ... 获取 ctx 上下文 ...

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

// 移动原点到 (100, 100)
ctx.translate(100, 100);

// 绘制一个相对于新原点的矩形
ctx.fillStyle = "cyan";
ctx.fillRect(0, 0, 50, 30); // 实际绘制在 (100, 100) 到 (150, 130)

ctx.save(); // 保存平移后的状态

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

// 绘制一个旋转后的矩形 (相对于新原点和旋转)
ctx.fillStyle = "magenta";
ctx.fillRect(0, 0, 50, 30); // 绘制在 (100, 100) 并旋转 45 度

ctx.restore(); // 恢复到仅平移的状态 (旋转取消)

// 绘制一个仅平移未旋转的矩形
ctx.fillStyle = "yellow";
ctx.fillRect(60, 10, 50, 30); // 绘制在 (100+60, 100+10) = (160, 110)

ctx.restore(); // 恢复到初始状态 (平移取消)

// 绘制一个在原始坐标系的矩形
ctx.fillStyle = "grey";
ctx.fillRect(10, 10, 50, 30);

9. 像素操作

Canvas API 允许直接读取和写入画布的像素数据。

  • getImageData(sx, sy, sw, sh): 获取画布上指定矩形区域的像素数据,返回一个 ImageData 对象。
  • putImageData(imageData, dx, dy, dirtyX?, dirtyY?, dirtyWidth?, dirtyHeight?): 将 ImageData 对象中的像素数据绘制回画布的 (dx, dy) 位置。可选的 "dirty" 参数指定只绘制 imageData 中的一个子矩形区域,可以提高性能。
  • createImageData(width, height)createImageData(imagedata): 创建一个新的、空白的 ImageData 对象。

ImageData 对象包含三个属性:

  • width: 图像数据的宽度 (像素)。
  • height: 图像数据的高度 (像素)。
  • data: 一个 Uint8ClampedArray,包含按 RGBA 顺序排列的像素数据 (每个像素占 4 个字节:红、绿、蓝、Alpha)。数组长度为 width * height * 4

像素操作常用于图像滤镜、特效或复杂的分析。

// ... 获取 ctx 上下文 ...

// 先绘制一些内容
ctx.fillStyle = "rgb(255, 128, 64)";
ctx.fillRect(10, 10, 100, 80);

// 获取该区域的像素数据
const imageData = ctx.getImageData(10, 10, 100, 80);
const data = imageData.data; // Uint8ClampedArray

// 示例:将图像转为灰度
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = avg; // Red
data[i + 1] = avg; // Green
data[i + 2] = avg; // Blue
// data[i + 3] is Alpha, keep it as is
}

// 将修改后的数据写回 Canvas (覆盖原位置)
ctx.putImageData(imageData, 10, 10);

10. 动画

Canvas 动画的核心是循环重绘。基本步骤如下:

  1. 清除画布: 使用 clearRect() 清除整个画布或需要更新的区域。
  2. 更新状态: 修改动画对象的位置、大小、颜色等属性。
  3. 绘制场景: 根据更新后的状态重新绘制所有内容。
  4. 请求下一帧: 使用 requestAnimationFrame(callback) 请求浏览器在下一次重绘之前调用指定的 callback 函数 (通常是动画循环函数本身)。这比 setTimeoutsetInterval 更高效、更平滑,因为它会与浏览器的刷新率同步,并在页面不可见时自动暂停。
// ... 获取 ctx 上下文 ...

let x = 50; // 初始位置
let dx = 2; // 速度

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

// 2. 更新状态
x += dx;
if (x + 30 > canvas.width || x < 0) {
// 碰到边缘则反弹
dx = -dx;
}

// 3. 绘制场景
ctx.fillStyle = "blue";
ctx.fillRect(x, 50, 30, 30); // 绘制移动的方块

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

// 启动动画
animate();

11. 交互

由于 Canvas 不维护图形对象,实现交互(如点击检测)需要自己编写逻辑。

  1. 监听事件:<canvas> 元素上添加事件监听器 (如 click, mousemove, mousedown, mouseup, touchstart, touchmove 等)。

  2. 获取坐标: 在事件处理函数中,获取鼠标或触摸点相对于 Canvas 左上角的坐标。

    canvas.addEventListener("click", (event) => {
    const rect = canvas.getBoundingClientRect(); // 获取 canvas 在视口中的位置
    const scaleX = canvas.width / rect.width; // 处理 CSS 缩放
    const scaleY = canvas.height / rect.height; // 处理 CSS 缩放

    const mouseX = (event.clientX - rect.left) * scaleX;
    const mouseY = (event.clientY - rect.top) * scaleY;

    console.log(`Clicked at canvas coordinates: (${mouseX}, ${mouseY})`);

    // 在这里进行点击检测逻辑
    // isPointInShape(mouseX, mouseY, shapeData);
    });
  3. 命中检测 (Hit Detection): 判断点击坐标是否落在某个绘制的“对象”上。常见策略:

    • 边界框 (Bounding Box): 维护每个逻辑对象的矩形边界,检查点击点是否在边界内。最简单,但对非矩形对象不精确。
    • 几何计算: 对于圆形、多边形等,使用数学公式判断点是否在图形内部。
    • 路径检测 isPointInPath() / isPointInStroke(): 在定义了路径后(在 fill()stroke() 之前),可以使用 ctx.isPointInPath(x, y)ctx.isPointInStroke(x, y) 来检查点是否在路径内部或描边上。这需要重新定义路径(可能在内存中或离屏 Canvas 上进行检测)。
    • 颜色拾取 (Color Picking): 在一个隐藏的离屏 Canvas 上,用唯一颜色绘制每个可交互对象。当主 Canvas 被点击时,读取隐藏 Canvas 上对应像素的颜色,从而识别被点击的对象。

12. 性能优化

处理复杂场景或动画时,性能至关重要:

  • 减少 Canvas API 调用: 批量处理相似操作。
  • 最小化状态更改: 频繁更改 fillStyle, strokeStyle, font 等代价较高。按状态分组绘制(例如,先画所有红色物体,再画所有蓝色物体)。
  • 离屏渲染 (Offscreen Canvas): 将不经常变化的部分(如背景)预先绘制到一个隐藏的 Canvas 上,然后每帧只需将这个离屏 Canvas drawImage 到主 Canvas 上,而不是重绘所有细节。
  • 分层 Canvas: 使用多个绝对定位的 Canvas 叠加。静态背景放一层,动态元素放另一层,只需重绘动态层。
  • 局部重绘:clearRect() 并重绘发生变化的区域,而不是整个画布。
  • 使用 requestAnimationFrame: 这是动画的标准方法。
  • 简化绘图: 减少路径复杂度、避免不必要的阴影和复杂渐变。
  • Web Workers: 将计算密集型任务(如物理模拟、像素处理)移到后台线程,避免阻塞主线程。
  • 针对性优化: 使用浏览器开发者工具的 Performance 面板分析瓶颈。

13. 超越 2D: WebGL

虽然本报告主要关注 2D 上下文,但 <canvas> 也是 WebGL (Web Graphics Library) 的载体。通过 canvas.getContext('webgl')canvas.getContext('webgl2') 可以获取 WebGL 渲染上下文,用于在浏览器中创建硬件加速的 3D 和 2D 图形。WebGL 基于 OpenGL ES 标准,使用着色器语言 (GLSL) 进行编程,提供了更底层的图形控制能力。

14. 常见用例

Canvas 的特性使其非常适合以下场景:

  • 游戏开发: 渲染游戏画面、角色、特效。
  • 数据可视化: 绘制复杂的图表、图形,尤其需要高性能或像素级操作时。
  • 图像编辑器: 实现滤镜、裁剪、旋转、绘画工具等。
  • 实时图形: 视频处理、实时数据流的可视化。
  • 模拟器: 物理模拟、粒子系统等。
  • 艺术与创意应用: 在线画板、生成艺术。

15. Canvas vs. SVG

特性CanvasSVG
类型位图 (Raster)矢量 (Vector)
渲染方式即时模式 (Immediate Mode API)保留模式 (Retained Mode DOM)
可缩放性放大失真无损缩放
DOM 交互无内置对象模型,需手动实现交互/命中检测每个图形是 DOM 元素,易于 CSS/JS/事件交互
性能通常更适合大量对象或高频更新 (游戏/动画)更适合静态、复杂但对象数量不庞大的图形
文本文本渲染后成像素,不易选择/SEO文本是可访问的文本节点,利于选择/SEO
API基于 JavaScript 的绘图函数基于 XML 的声明式标记语言
适用场景游戏, 动画, 图像处理, 高性能可视化图标, Logo, 插画, 信息图, UI 元素

16. 总结

Canvas 提供了一个强大而灵活的接口,用于在网页上通过 JavaScript 创建和操作位图图形。它非常适合需要高性能渲染、像素级控制、实时更新和动画的应用场景。虽然其即时模式和基于像素的特性带来了与 SVG 不同的挑战(如交互实现和缩放问题),但也赋予了它在动态图形和复杂可视化方面的独特优势。熟练掌握 Canvas API,特别是路径、样式、变换、图像处理和动画循环,是现代 Web 图形开发的重要技能。