路径与形状绘制
Canvas 的路径(Path)系统是所有复杂图形绘制的基础。通过路径 API,你可以组合直线、曲线、圆弧等基本元素,构建出任意复杂的二维图形,再通过 fill() 或 stroke() 进行填充或描边。本节将详细介绍路径的构建方法与各种形状绘制技巧。
前置知识
阅读本节前,建议先了解:Canvas 2D 上下文
基础概念
路径是一系列点的集合,这些点通过线段或曲线连接起来。Canvas 中的路径绘制遵循以下流程:
- 开始路径:
beginPath()—— 清空子路径列表 - 定义路径:
moveTo()、lineTo()、arc()等方法 —— 添加子路径 - 渲染路径:
fill()或stroke()—— 将路径绘制到画布上
路径的累积性
如果不调用 beginPath(),后续的路径指令会追加到当前路径上。这会导致 stroke() 或 fill() 重复绘制之前的路径。
路径 API 详解
beginPath()
beginPath() 清空当前路径的子路径列表,开始一条新路径。
ctx.beginPath(); // 清空之前的所有路径closePath()
closePath() 从当前点到起始点画一条直线,形成闭合路径。
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(200, 200);
ctx.closePath(); // 自动从 (200,200) 连线到 (50,50)
ctx.stroke();moveTo() 与 lineTo()
moveTo(x, y)—— 移动画笔到指定坐标(不画线)lineTo(x, y)—— 从当前点画一条直线到指定坐标
// 绘制三角形
ctx.beginPath();
ctx.moveTo(100, 20); // 移动到顶部顶点
ctx.lineTo(180, 160); // 画线到右下角
ctx.lineTo(20, 160); // 画线到左下角
ctx.closePath(); // 闭合路径
ctx.stroke(); // 描边矩形绘制
rect()
rect(x, y, width, height) 向当前路径添加一个矩形子路径。
ctx.beginPath();
ctx.rect(50, 50, 200, 100);
ctx.stroke();fillRect() 与 strokeRect()
这两个是便捷方法,不需要使用路径系统:
// 填充矩形(不走路径系统)
ctx.fillStyle = '#4285f4';
ctx.fillRect(50, 50, 200, 100);
// 描边矩形
ctx.strokeStyle = '#ea4335';
ctx.lineWidth = 3;
ctx.strokeRect(270, 50, 200, 100);
// 清除矩形区域
ctx.clearRect(100, 75, 100, 50);clearRect()
clearRect(x, y, width, height) 清除指定矩形区域内的像素,使其变为透明。
圆弧绘制
arc()
arc(x, y, radius, startAngle, endAngle, counterclockwise) 绘制圆弧。
| 参数 | 说明 |
|---|---|
x, y | 圆心坐标 |
radius | 半径 |
startAngle | 起始角度(弧度) |
endAngle | 结束角度(弧度) |
counterclockwise | 是否逆时针(默认 false,顺时针) |
// 绘制圆(完整圆弧)
ctx.beginPath();
ctx.arc(100, 100, 50, 0, Math.PI * 2); // 0 到 2π
ctx.fill();
// 绘制半圆
ctx.beginPath();
ctx.arc(250, 100, 50, 0, Math.PI); // 0 到 π
ctx.stroke();
// 绘制四分之一圆
ctx.beginPath();
ctx.arc(400, 100, 50, 0, Math.PI / 2); // 0 到 π/2
ctx.stroke();arcTo()
arcTo(x1, y1, x2, y2, radius) 根据两条切线和半径绘制圆弧。常用于绘制圆角。
/**
* arcTo 的工作原理:
* - 当前点到 (x1, y1) 的连线是第一条切线
* - (x1, y1) 到 (x2, y2) 的连线是第二条切线
* - radius 是圆弧的半径
* - 圆弧与两条切线相切
*/
// 绘制圆角矩形
function roundRect(ctx, x, y, width, height, radius) {
ctx.beginPath();
ctx.moveTo(x + radius, y);
ctx.arcTo(x + width, y, x + width, y + height, radius); // 右上角
ctx.arcTo(x + width, y + height, x, y + height, radius); // 右下角
ctx.arcTo(x, y + height, x, y, radius); // 左下角
ctx.arcTo(x, y, x + width, y, radius); // 左上角
ctx.closePath();
}曲线绘制
二次贝塞尔曲线:quadraticCurveTo()
quadraticCurveTo(cpx, cpy, x, y) —— 一个控制点的贝塞尔曲线。
ctx.beginPath();
ctx.moveTo(50, 200); // 起点
ctx.quadraticCurveTo(200, 50, 350, 200); // 控制点 (200,50),终点 (350,200)
ctx.stroke();
// 绘制控制点和辅助线
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.moveTo(50, 200);
ctx.lineTo(200, 50);
ctx.lineTo(350, 200);
ctx.strokeStyle = '#999';
ctx.stroke();
// 控制点
ctx.setLineDash([]);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(200, 50, 4, 0, Math.PI * 2);
ctx.fill();三次贝塞尔曲线:bezierCurveTo()
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) —— 两个控制点的贝塞尔曲线。
ctx.beginPath();
ctx.moveTo(50, 250); // 起点
ctx.bezierCurveTo(
150, 50, // 控制点 1
250, 350, // 控制点 2
350, 150 // 终点
);
ctx.lineWidth = 3;
ctx.strokeStyle = '#4285f4';
ctx.stroke();
// 辅助线
ctx.beginPath();
ctx.setLineDash([3, 3]);
ctx.strokeStyle = '#ccc';
ctx.moveTo(50, 250);
ctx.lineTo(150, 50);
ctx.lineTo(250, 350);
ctx.lineTo(350, 150);
ctx.stroke();
// 控制点
ctx.setLineDash([]);
ctx.fillStyle = 'red';
ctx.beginPath();
ctx.arc(150, 50, 4, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(250, 350, 4, 0, Math.PI * 2);
ctx.fill();二次与三次贝塞尔曲线对比
| 特性 | quadraticCurveTo | bezierCurveTo |
|---|---|---|
| 控制点数量 | 1 个 | 2 个 |
| 参数 | (cpx, cpy, x, y) | (cp1x, cp1y, cp2x, cp2y, x, y) |
| 灵活度 | 中等 | 高 |
| 适用场景 | 简单曲线、圆弧近似 | 复杂曲线、S 形曲线 |
椭圆绘制
ellipse()
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) 绘制椭圆。
// 水平椭圆
ctx.beginPath();
ctx.ellipse(150, 100, 120, 60, 0, 0, Math.PI * 2);
ctx.stroke();
// 旋转 30 度的椭圆
ctx.beginPath();
ctx.ellipse(400, 100, 120, 60, Math.PI / 6, 0, Math.PI * 2);
ctx.stroke();
// 椭圆弧
ctx.beginPath();
ctx.ellipse(150, 250, 100, 50, 0, 0, Math.PI);
ctx.fillStyle = '#34a853';
ctx.fill();实战示例
示例 1:绘制五角星
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>五角星</title>
<style>
canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
</style>
</head>
<body>
<h1>Canvas 五角星</h1>
<canvas id="starCanvas" width="400" height="400"></canvas>
<script>
const canvas = document.getElementById('starCanvas');
const ctx = canvas.getContext('2d');
/**
* 绘制五角星
* @param {CanvasRenderingContext2D} ctx
* @param {number} cx - 中心 X
* @param {number} cy - 中心 Y
* @param {number} outerR - 外圆半径
* @param {number} innerR - 内圆半径
* @param {number} points - 角数
*/
function drawStar(ctx, cx, cy, outerR, innerR, points) {
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
// 偶数索引为外圆顶点,奇数索引为内圆顶点
const radius = i % 2 === 0 ? outerR : innerR;
const angle = (Math.PI / points) * i - Math.PI / 2;
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();
}
// 绘制蓝色五角星
drawStar(ctx, 200, 200, 150, 60, 5);
// 渐变填充
const gradient = ctx.createRadialGradient(200, 200, 30, 200, 200, 150);
gradient.addColorStop(0, '#fbbc05');
gradient.addColorStop(1, '#ea4335');
ctx.fillStyle = gradient;
ctx.fill();
ctx.strokeStyle = '#c5221f';
ctx.lineWidth = 2;
ctx.stroke();
</script>
</body>
</html>示例 2:绘制心形
/**
* 使用贝塞尔曲线绘制心形
*/
function drawHeart(ctx, cx, cy, size) {
ctx.beginPath();
ctx.moveTo(cx, cy + size / 4);
// 左半部分
ctx.bezierCurveTo(
cx, cy,
cx - size / 2, cy,
cx - size / 2, cy + size / 4
);
ctx.bezierCurveTo(
cx - size / 2, cy + size / 2,
cx, cy + size * 0.6,
cx, cy + size * 0.75
);
// 右半部分
ctx.bezierCurveTo(
cx, cy + size * 0.6,
cx + size / 2, cy + size / 2,
cx + size / 2, cy + size / 4
);
ctx.bezierCurveTo(
cx + size / 2, cy,
cx, cy,
cx, cy + size / 4
);
ctx.closePath();
}
// 使用
drawHeart(ctx, 200, 100, 200);
ctx.fillStyle = '#ea4335';
ctx.fill();示例 3:绘制箭头
/**
* 绘制带箭头的线段
*/
function drawArrow(ctx, fromX, fromY, toX, toY, headLength) {
const angle = Math.atan2(toY - fromY, toX - fromX);
ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.stroke();
// 绘制箭头
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(
toX - headLength * Math.cos(angle - Math.PI / 6),
toY - headLength * Math.sin(angle - Math.PI / 6)
);
ctx.lineTo(
toX - headLength * Math.cos(angle + Math.PI / 6),
toY - headLength * Math.sin(angle + Math.PI / 6)
);
ctx.closePath();
ctx.fill();
}
// 使用
ctx.strokeStyle = '#4285f4';
ctx.fillStyle = '#4285f4';
ctx.lineWidth = 2;
drawArrow(ctx, 50, 200, 350, 150, 20);示例 4:多种形状综合展示
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>形状绘制综合</title>
<style>
canvas { border: 1px solid #ccc; display: block; margin: 10px 0; }
</style>
</head>
<body>
<h1>Canvas 形状绘制综合</h1>
<canvas id="shapesCanvas" width="600" height="500"></canvas>
<script>
const canvas = document.getElementById('shapesCanvas');
const ctx = canvas.getContext('2d');
// === 矩形 ===
ctx.fillStyle = '#4285f4';
ctx.fillRect(20, 20, 120, 80);
ctx.strokeStyle = '#ea4335';
ctx.lineWidth = 3;
ctx.strokeRect(20, 120, 120, 80);
// === 圆形 ===
ctx.beginPath();
ctx.arc(250, 60, 50, 0, Math.PI * 2);
ctx.fillStyle = '#34a853';
ctx.fill();
ctx.beginPath();
ctx.arc(250, 180, 50, 0, Math.PI * 2);
ctx.strokeStyle = '#fbbc05';
ctx.lineWidth = 3;
ctx.stroke();
// === 椭圆 ===
ctx.beginPath();
ctx.ellipse(450, 60, 80, 40, 0, 0, Math.PI * 2);
ctx.fillStyle = '#9c27b0';
ctx.fill();
// === 三角形 ===
ctx.beginPath();
ctx.moveTo(400, 120);
ctx.lineTo(500, 220);
ctx.lineTo(350, 220);
ctx.closePath();
ctx.fillStyle = '#ff5722';
ctx.fill();
// === 贝塞尔曲线花朵 ===
ctx.save();
ctx.translate(300, 380);
for (let i = 0; i < 6; i++) {
ctx.rotate(Math.PI / 3);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.quadraticCurveTo(60, -40, 0, -90);
ctx.quadraticCurveTo(-60, -40, 0, 0);
ctx.fillStyle = 'rgba(234, 67, 53, 0.3)';
ctx.fill();
ctx.strokeStyle = '#ea4335';
ctx.lineWidth = 1;
ctx.stroke();
}
// 花心
ctx.beginPath();
ctx.arc(0, 0, 15, 0, Math.PI * 2);
ctx.fillStyle = '#fbbc05';
ctx.fill();
ctx.restore();
// === 标签 ===
ctx.fillStyle = '#333';
ctx.font = '12px sans-serif';
ctx.fillText('矩形', 55, 220);
ctx.fillText('圆形', 230, 260);
ctx.fillText('椭圆', 435, 110);
ctx.fillText('三角形', 395, 240);
ctx.fillText('贝塞尔花朵', 255, 490);
</script>
</body>
</html>注意事项
1. beginPath() 的时机
// 常见错误:忘记 beginPath()
ctx.fillStyle = 'red';
ctx.fillRect(10, 10, 50, 50);
// 忘记 beginPath() —— 会重复之前的路径
ctx.beginPath(); // 必须调用!
ctx.arc(200, 100, 30, 0, Math.PI * 2);
ctx.fillStyle = 'blue';
ctx.fill();2. fill() 会自动闭合路径
fill() 在填充前会自动闭合路径(相当于自动调用 closePath()),但 stroke() 不会。
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.lineTo(200, 50);
ctx.lineTo(125, 150);
// fill() 会自动闭合,stroke() 不会
ctx.fill(); // 三角形被填充
ctx.stroke(); // 只描了两条线,缺少第三条3. 弧度与角度的转换
Canvas 中的角度使用弧度制:
// 角度转弧度
function degToRad(degrees) {
return degrees * (Math.PI / 180);
}
// 弧度转角度
function radToDeg(radians) {
return radians * (180 / Math.PI);
}
// 常用弧度值
const FULL_CIRCLE = Math.PI * 2; // 360 度
const HALF_CIRCLE = Math.PI; // 180 度
const RIGHT_ANGLE = Math.PI / 2; // 90 度最佳实践
1. 封装形状绘制工具
const Shapes = {
// 圆角矩形
roundRect(ctx, x, y, w, h, r) {
if (typeof r === 'number') r = [r, r, r, r];
const [tl, tr, br, bl] = r;
ctx.beginPath();
ctx.moveTo(x + tl, y);
ctx.arcTo(x + w, y, x + w, y + h, tr);
ctx.arcTo(x + w, y + h, x, y + h, br);
ctx.arcTo(x, y + h, x, y, bl);
ctx.arcTo(x, y, x + w, y, tl);
ctx.closePath();
},
// 正多边形
polygon(ctx, cx, cy, radius, sides, rotation) {
ctx.beginPath();
for (let i = 0; i < sides; i++) {
const angle = (Math.PI * 2 / sides) * i + (rotation || 0);
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();
},
// 箭头
arrow(ctx, fx, fy, tx, ty, size) {
const angle = Math.atan2(ty - fy, tx - fx);
ctx.beginPath();
ctx.moveTo(fx, fy);
ctx.lineTo(tx, ty);
ctx.moveTo(tx, ty);
ctx.lineTo(
tx - size * Math.cos(angle - Math.PI / 6),
ty - size * Math.sin(angle - Math.PI / 6)
);
ctx.moveTo(tx, ty);
ctx.lineTo(
tx - size * Math.cos(angle + Math.PI / 6),
ty - size * Math.sin(angle + Math.PI / 6)
);
}
};2. 路径复用模式
// 定义一次路径,多次使用
function createPath(ctx) {
ctx.beginPath();
ctx.moveTo(50, 50);
ctx.bezierCurveTo(100, 10, 150, 90, 200, 50);
}
// 先描边
createPath(ctx);
ctx.strokeStyle = '#4285f4';
ctx.lineWidth = 3;
ctx.stroke();
// 再填充
createPath(ctx);
ctx.fillStyle = 'rgba(66, 133, 244, 0.3)';
ctx.fill();3. 使用 isPointInPath 检测点击
// 检测点是否在路径内
ctx.beginPath();
ctx.arc(200, 200, 50, 0, Math.PI * 2);
ctx.fill();
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (ctx.isPointInPath(x, y)) {
console.log('点击在圆形内');
}
});下一节
继续学习:颜色、渐变与阴影