pin_drop当前位置:知识文库 ❯ 图文
多媒体:绘制路径 - 完整教程与代码示例
一、教程简介
路径是 Canvas 绘图的核心机制,几乎所有复杂图形(三角形、多边形、星形等)都需要通过路径来绘制。路径由一系列点、线段和曲线组成,通过 beginPath 开始新路径,使用各种绘图命令定义路径形状,最后通过 fill 或 stroke 进行渲染。本教程将深入讲解路径的创建、管理和渲染方法。
二、核心概念
什么是路径
路径是由一系列子路径(subpath)组成的图形轮廓。每个子路径由一个或多个线段或曲线段连接而成。
路径的生命周期:
beginPath()- 开始新路径,清除之前的路径列表moveTo()- 将画笔移动到起始点(不画线)- 各种绘图命令 -
lineTo、arc、quadraticCurveTo等 closePath()- 可选,闭合当前子路径fill()/stroke()- 渲染路径
路径与子路径
-
一个路径可以包含多个子路径
-
每次
moveTo()开始一个新的子路径 -
closePath()只闭合当前子路径,不会开始新路径 -
fill()会自动闭合所有未闭合的子路径
填充规则
Canvas 支持两种填充规则:
-
nonzero(默认):非零环绕规则,常用于普通填充
-
evenodd:奇偶规则,常用于创建镂空效果
三、语法与用法
beginPath - 开始新路径
代码示例
ctx.beginPath();清除当前路径中的所有子路径,开始一个全新的路径。每次绘制新图形前都应调用此方法,否则新路径会与旧路径合并。
closePath - 闭合路径
代码示例
ctx.closePath();从当前点绘制一条直线回到当前子路径的起始点,形成一个闭合图形。
fill - 填充路径
代码示例
ctx.fill();
ctx.fill(fillRule);stroke - 描边路径
代码示例
ctx.stroke();沿路径轮廓绘制线条。
rect - 向路径添加矩形
代码示例
ctx.rect(x, y, width, height);
将矩形添加到当前路径中(与 fillRect/strokeRect 不同,这个需要配合 fill()/stroke() 使用)。
isPointInPath - 检测点是否在路径内
代码示例
ctx.isPointInPath(x, y);
ctx.isPointInPath(x, y, fillRule);四、代码示例
示例1:路径基础 - 绘制各种多边形
(完整代码示例请参考原始 Markdown 文件)
示例2:填充规则与复合路径
(完整代码示例请参考原始 Markdown 文件)
五、浏览器兼容性
六、注意事项与最佳实践
1. 每次绘制前调用 beginPath
代码示例
// 错误:不调用 beginPath,路径会累积
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.stroke();
ctx.moveTo(10, 50);
ctx.lineTo(100, 50);
ctx.stroke();
// 第二次 stroke 会同时绘制两条线
// 正确:每次绘制前调用 beginPath
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(10, 50);
ctx.lineTo(100, 50);
ctx.stroke();2. fill 会自动闭合路径
代码示例
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.lineTo(55, 80);
// 没有 closePath,但 fill 会自动闭合
ctx.fill();3. closePath 不是 beginPath
代码示例
// closePath 只是画一条回到起点的线,不会开始新路径
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.lineTo(55, 80);
ctx.closePath(); // 画线回到 (10, 10)
// 此时仍在同一个路径中4. 使用 isPointInPath 进行碰撞检测
代码示例
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 重新构建路径(isPointInPath 检测当前路径)
ctx.beginPath();
ctx.arc(150, 150, 50, 0, Math.PI * 2);
if (ctx.isPointInPath(x, y)) {
console.log('点击了圆形区域');
}
});5. 复合路径减少绘制调用
代码示例
// 推荐:使用复合路径一次填充多个图形
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(50, 10);
ctx.lineTo(30, 50);
ctx.closePath();
ctx.moveTo(70, 10);
ctx.lineTo(110, 10);
ctx.lineTo(90, 50);
ctx.closePath();
ctx.fillStyle = 'red';
ctx.fill(); // 一次调用填充两个三角形
// 不推荐:分别绘制
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(50, 10);
ctx.lineTo(30, 50);
ctx.closePath();
ctx.fillStyle = 'red';
ctx.fill();
ctx.beginPath();
ctx.moveTo(70, 10);
ctx.lineTo(110, 10);
ctx.lineTo(90, 50);
ctx.closePath();
ctx.fillStyle = 'red';
ctx.fill();七、代码规范示例
代码示例
// 推荐:封装路径绘制函数
function drawTriangle(ctx, x1, y1, x2, y2, x3, y3, options) {
const { fill, stroke, lineWidth = 1, close = true } = options || {};
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.lineTo(x3, y3);
if (close) ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill();
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
// 推荐:正多边形绘制函数
function drawRegularPolygon(ctx, cx, cy, radius, sides, options) {
const {
fill = null,
stroke = null,
lineWidth = 1,
rotation = -Math.PI / 2,
fillRule = 'nonzero'
} = options || {};
ctx.beginPath();
for (let i = 0; i <= sides; i++) {
const angle = rotation + (i * 2 * Math.PI / sides);
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
if (fill) {
ctx.fillStyle = fill;
ctx.fill(fillRule);
}
if (stroke) {
ctx.strokeStyle = stroke;
ctx.lineWidth = lineWidth;
ctx.stroke();
}
}
// 使用示例
drawRegularPolygon(ctx, 200, 200, 60, 6, {
fill: 'rgba(88, 166, 255, 0.3)',
stroke: '#58a6ff',
lineWidth: 2
});八、常见问题与解决方案
常见问题
Q1:为什么绘制了多个图形,但之前的图形也被重新绘制了?
原因:没有调用 beginPath(),新路径与旧路径合并。
解决方案:
代码示例
// 每次绘制新图形前调用 beginPath
ctx.beginPath();
ctx.arc(100, 100, 30, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath(); // 必须调用
ctx.arc(200, 100, 30, 0, Math.PI * 2);
ctx.fill();Q2:fill 和 stroke 的顺序有什么影响?
原因:先填充再描边,描边会覆盖在填充之上;反之亦然。
解决方案:
代码示例
// 推荐:先填充再描边
ctx.beginPath();
ctx.arc(100, 100, 30, 0, Math.PI * 2);
ctx.fillStyle = '#58a6ff';
ctx.fill();
ctx.strokeStyle = '#1f6feb';
ctx.lineWidth = 2;
ctx.stroke();Q3:如何创建镂空效果?
解决方案:使用 evenodd 填充规则:
代码示例
ctx.beginPath();
ctx.rect(50, 50, 200, 200); // 外框
ctx.arc(150, 150, 50, 0, Math.PI * 2); // 内圆
ctx.fillStyle = '#58a6ff';
ctx.fill('evenodd'); // 内圆区域被镂空Q4:closePath 后还需要 moveTo 吗?
原因:closePath 只是画一条线回到起点,不会开始新子路径。
解决方案:
代码示例
// 闭合后开始新子路径需要 moveTo
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(100, 10);
ctx.lineTo(55, 80);
ctx.closePath(); // 回到 (10, 10)
// 开始新子路径
ctx.moveTo(200, 10); // 新的起点
ctx.lineTo(300, 10);
ctx.lineTo(255, 80);
ctx.closePath();Q5:如何检测点击是否在某个图形内?
解决方案:
代码示例
// 方案1:使用 isPointInPath(需要重建路径)
function isPointInShape(x, y) {
ctx.beginPath();
ctx.arc(150, 150, 50, 0, Math.PI * 2);
return ctx.isPointInPath(x, y);
}
// 方案2:使用 Path2D 对象(更高效)
const circlePath = new Path2D();
circlePath.arc(150, 150, 50, 0, Math.PI * 2);
canvas.addEventListener('click', function(e) {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (ctx.isPointInPath(circlePath, x, y)) {
console.log('点击了圆形');
}
});九、总结
路径是 Canvas 绘图的核心机制,关键要点:
-
1. beginPath:每次绘制新图形前必须调用,清除旧路径
-
2. closePath:从当前点画线回到起点,形成闭合图形
-
3. fill:填充路径内部,会自动闭合未闭合的子路径
-
4. stroke:沿路径轮廓描边
-
5. 填充规则:
nonzero(默认)和evenodd(镂空效果) -
6. 复合路径:多个子路径可以一次填充或描边,提高性能
-
7. Path2D:可复用的路径对象,便于碰撞检测和代码组织
下一教程将介绍如何使用路径绘制线条及线条样式。
本文涉及AI创作
内容由AI创作,请仔细甄别