核心知识点
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
, width
和 height
属性。
<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 还支持webgl
和webgl2
上下文用于 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)
路径是绘制复杂形状(直线、曲线、弧形等)的基础。绘制路径通常遵循以下步骤:
beginPath()
: 开始一个新的路径。这会清空当前的子路径列表。- 使用移动和绘制命令定义路径:
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
不同,它只定义路径,不直接绘制)。
closePath()
(可选): 从当前点绘制一条直线回到路径的起点,闭合形状。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 上。它有三种重载形式:
drawImage(image, dx, dy)
: 将图像image
在画布的(dx, dy)
位置绘制出来(原始尺寸)。drawImage(image, dx, dy, dWidth, dHeight)
: 将图像image
在画布的(dx, dy)
位置绘制出来,并缩放到dWidth
xdHeight
尺寸。drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
: 从源图像image
中裁剪出(sx, sy)
位置开始,sWidth
xsHeight
大小的区域,然后将其绘制到画布的(dx, dy)
位置,并缩放到dWidth
xdHeight
尺寸。
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 动画的核心是循环重绘。基本步骤如下:
- 清除画布: 使用
clearRect()
清除整个画布或需要更新的区域。 - 更新状态: 修改动画对象的位置、大小、颜色等属性。
- 绘制场景: 根据更新后的状态重新绘制所有内容。
- 请求下一帧: 使用
requestAnimationFrame(callback)
请求浏览器在下一次重绘之前调用指定的callback
函数 (通常是动画循环函数本身)。这比setTimeout
或setInterval
更高效、更平滑,因为它会与浏览器的刷新率同步,并在页面不可见时自动暂停。
// ... 获取 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 不维护图形对象,实现交互(如点击检测)需要自己编写逻辑。
-
监听事件: 在
<canvas>
元素上添加事件监听器 (如click
,mousemove
,mousedown
,mouseup
,touchstart
,touchmove
等)。 -
获取坐标: 在事件处理函数中,获取鼠标或触摸点相对于 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);
}); -
命中检测 (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
特性 | Canvas | SVG |
---|---|---|
类型 | 位图 (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 图形开发的重要技能。