1. 路径系统深入
1.1 路径的概念与原理
路径是Canvas中最强大的绘图工具,它允许我们创建复杂的图形。路径由一系列的子路径组成,每个子路径包含一系列的点和连接这些点的线段或曲线。
// 获取canvas和上下文
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 路径的基本流程
function pathWorkflow() {
// 1. 开始新路径
ctx.beginPath();
// 2. 定义路径形状
ctx.moveTo(50, 50); // 移动到起点
ctx.lineTo(150, 50); // 画线到指定点
ctx.lineTo(100, 150); // 继续画线
// 3. 可选:闭合路径
ctx.closePath();
// 4. 渲染路径
ctx.stroke(); // 描边
// 或者 ctx.fill(); // 填充
}
pathWorkflow();
1.2 路径状态管理
// 路径状态的保存与恢复
function pathStateManagement() {
// 保存当前绘图状态
ctx.save();
// 设置样式
ctx.strokeStyle = 'red';
ctx.lineWidth = 3;
// 绘制第一个图形
ctx.beginPath();
ctx.rect(200, 50, 100, 80);
ctx.stroke();
// 恢复之前的状态
ctx.restore();
// 绘制第二个图形(使用默认样式)
ctx.beginPath();
ctx.rect(350, 50, 100, 80);
ctx.stroke();
}
pathStateManagement();
1.3 多个子路径
// 在一个路径中包含多个子路径
function multipleSubPaths() {
ctx.beginPath();
// 第一个子路径:矩形
ctx.rect(50, 200, 80, 60);
// 第二个子路径:圆形
ctx.moveTo(200, 230); // 移动到新位置开始新子路径
ctx.arc(170, 230, 30, 0, Math.PI * 2);
// 第三个子路径:三角形
ctx.moveTo(250, 200);
ctx.lineTo(300, 200);
ctx.lineTo(275, 260);
ctx.closePath();
// 一次性填充所有子路径
ctx.fillStyle = 'lightblue';
ctx.fill();
ctx.strokeStyle = 'darkblue';
ctx.stroke();
}
multipleSubPaths();
2. 高级路径方法
2.1 arcTo() 方法
// arcTo() 绘制圆弧连接
function drawArcTo() {
// arcTo(x1, y1, x2, y2, radius)
// 从当前点到(x2,y2)绘制一条经过(x1,y1)的圆弧
ctx.beginPath();
ctx.moveTo(400, 200);
ctx.arcTo(500, 200, 500, 300, 50); // 圆角转弯
ctx.lineTo(500, 350);
ctx.strokeStyle = 'green';
ctx.lineWidth = 3;
ctx.stroke();
// 绘制辅助点
ctx.fillStyle = 'red';
ctx.fillRect(398, 198, 4, 4); // 起点
ctx.fillRect(498, 198, 4, 4); // 控制点1
ctx.fillRect(498, 298, 4, 4); // 控制点2
}
drawArcTo();
2.2 Path2D 对象
// 使用Path2D对象创建可重用的路径
function usePath2D() {
// 创建一个心形路径
const heartPath = new Path2D();
const x = 600, y = 250, size = 30;
heartPath.moveTo(x, y + size / 4);
heartPath.bezierCurveTo(x, y, x - size / 2, y, x - size / 2, y + size / 4);
heartPath.bezierCurveTo(x - size / 2, y + size / 2, x, y + size, x, y + size);
heartPath.bezierCurveTo(x, y + size, x + size / 2, y + size / 2, x + size / 2, y + size / 4);
heartPath.bezierCurveTo(x + size / 2, y, x, y, x, y + size / 4);
// 使用路径
ctx.fillStyle = 'red';
ctx.fill(heartPath);
// 可以重复使用同一个路径
ctx.translate(100, 0);
ctx.fillStyle = 'pink';
ctx.fill(heartPath);
ctx.translate(-100, 0); // 恢复变换
}
usePath2D();
2.3 路径方向与填充规则
// 路径方向和填充规则
function pathDirection() {
// 创建外圆(顺时针)
ctx.beginPath();
ctx.arc(150, 400, 50, 0, Math.PI * 2, false);
// 创建内圆(逆时针)
ctx.arc(150, 400, 25, 0, Math.PI * 2, true);
// 使用 "evenodd" 填充规则创建环形
ctx.fillStyle = 'orange';
ctx.fill('evenodd');
// 对比:使用默认 "nonzero" 填充规则
ctx.beginPath();
ctx.arc(300, 400, 50, 0, Math.PI * 2, false);
ctx.arc(300, 400, 25, 0, Math.PI * 2, false); // 同方向
ctx.fillStyle = 'lightgreen';
ctx.fill(); // 默认 "nonzero" 规则
}
pathDirection();
3. 复杂图形绘制
3.1 正多边形
// 绘制正多边形的通用函数
function drawRegularPolygon(x, y, radius, sides, rotation = 0) {
ctx.beginPath();
for (let i = 0; i < sides; i++) {
const angle = (i * 2 * Math.PI) / sides + rotation;
const px = x + Math.cos(angle) * radius;
const py = y + Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
}
// 绘制不同的正多边形
const polygons = [
{ sides: 3, color: 'red' }, // 三角形
{ sides: 5, color: 'green' }, // 五边形
{ sides: 6, color: 'blue' }, // 六边形
{ sides: 8, color: 'purple' } // 八边形
];
polygons.forEach((poly, index) => {
const x = 100 + index * 120;
const y = 500;
drawRegularPolygon(x, y, 40, poly.sides);
ctx.fillStyle = poly.color;
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
});
3.2 星形图案
// 绘制星形的通用函数
function drawStar(x, y, outerRadius, innerRadius, points, rotation = 0) {
ctx.beginPath();
for (let i = 0; i < points * 2; i++) {
const angle = (i * Math.PI) / points + rotation;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const px = x + Math.cos(angle) * radius;
const py = y + Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(px, py);
} else {
ctx.lineTo(px, py);
}
}
ctx.closePath();
}
// 绘制不同的星形
const stars = [
{ points: 5, outer: 30, inner: 15, color: 'gold' },
{ points: 6, outer: 35, inner: 18, color: 'silver' },
{ points: 8, outer: 32, inner: 16, color: 'bronze' }
];
stars.forEach((star, index) => {
const x = 600 + index * 100;
const y = 500;
drawStar(x, y, star.outer, star.inner, star.points);
ctx.fillStyle = star.color;
ctx.fill();
ctx.strokeStyle = 'black';
ctx.stroke();
});
3.3 花瓣图案
// 绘制花瓣图案
function drawFlower(x, y, petalCount, petalLength, petalWidth) {
ctx.save();
ctx.translate(x, y);
for (let i = 0; i < petalCount; i++) {
ctx.save();
ctx.rotate((i * 2 * Math.PI) / petalCount);
// 绘制单个花瓣
ctx.beginPath();
ctx.ellipse(0, -petalLength / 2, petalWidth, petalLength, 0, 0, Math.PI * 2);
ctx.fillStyle = `hsl(${(i * 360) / petalCount}, 70%, 60%)`;
ctx.fill();
ctx.strokeStyle = 'darkgreen';
ctx.stroke();
ctx.restore();
}
// 绘制花心
ctx.beginPath();
ctx.arc(0, 0, petalWidth, 0, Math.PI * 2);
ctx.fillStyle = 'yellow';
ctx.fill();
ctx.strokeStyle = 'orange';
ctx.stroke();
ctx.restore();
}
// 绘制不同的花朵
drawFlower(150, 650, 8, 40, 15);
drawFlower(350, 650, 12, 35, 12);
drawFlower(550, 650, 6, 50, 20);
4. 曲线与贝塞尔路径
4.1 平滑曲线连接
// 绘制平滑连接的曲线
function drawSmoothCurve(points) {
if (points.length < 2) return;
ctx.beginPath();
ctx.moveTo(points[0].x, points[0].y);
for (let i = 1; i < points.length - 1; i++) {
const cp1x = (points[i].x + points[i + 1].x) / 2;
const cp1y = (points[i].y + points[i + 1].y) / 2;
ctx.quadraticCurveTo(points[i].x, points[i].y, cp1x, cp1y);
}
// 连接到最后一个点
const lastPoint = points[points.length - 1];
ctx.quadraticCurveTo(
points[points.length - 2].x,
points[points.length - 2].y,
lastPoint.x,
lastPoint.y
);
ctx.stroke();
}
// 示例点集
const curvePoints = [
{ x: 50, y: 750 },
{ x: 150, y: 700 },
{ x: 250, y: 780 },
{ x: 350, y: 720 },
{ x: 450, y: 760 },
{ x: 550, y: 710 }
];
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
drawSmoothCurve(curvePoints);
// 绘制控制点
ctx.fillStyle = 'red';
curvePoints.forEach(point => {
ctx.fillRect(point.x - 2, point.y - 2, 4, 4);
});
4.2 波浪线
// 绘制波浪线
function drawWave(x, y, width, amplitude, frequency, phase = 0) {
ctx.beginPath();
for (let i = 0; i <= width; i++) {
const waveY = y + Math.sin((i * frequency + phase) * Math.PI / 180) * amplitude;
if (i === 0) {
ctx.moveTo(x + i, waveY);
} else {
ctx.lineTo(x + i, waveY);
}
}
ctx.stroke();
}
// 绘制不同的波浪
ctx.strokeStyle = 'purple';
ctx.lineWidth = 2;
drawWave(50, 850, 500, 20, 2); // 基础波浪
ctx.strokeStyle = 'orange';
drawWave(50, 900, 500, 15, 4, 90); // 高频波浪
ctx.strokeStyle = 'green';
drawWave(50, 950, 500, 25, 1, 45); // 低频波浪
4.3 螺旋线
// 绘制螺旋线
function drawSpiral(centerX, centerY, maxRadius, turns) {
ctx.beginPath();
const steps = turns * 100; // 每圈100个点
for (let i = 0; i <= steps; i++) {
const angle = (i / steps) * turns * 2 * Math.PI;
const radius = (i / steps) * maxRadius;
const x = centerX + Math.cos(angle) * radius;
const y = centerY + Math.sin(angle) * radius;
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
}
// 绘制不同的螺旋
ctx.strokeStyle = 'darkred';
ctx.lineWidth = 2;
drawSpiral(700, 750, 80, 3);
ctx.strokeStyle = 'darkblue';
ctx.lineWidth = 1;
drawSpiral(700, 900, 60, 5);
5. 路径检测与交互
5.1 点击检测
// 创建可点击的图形
class ClickableShape {
constructor(path, fillStyle, strokeStyle) {
this.path = path;
this.fillStyle = fillStyle;
this.strokeStyle = strokeStyle;
this.isHovered = false;
this.isClicked = false;
}
draw(ctx) {
ctx.fillStyle = this.isClicked ? 'red' :
this.isHovered ? 'lightblue' : this.fillStyle;
ctx.fill(this.path);
ctx.strokeStyle = this.strokeStyle;
ctx.stroke(this.path);
}
contains(x, y, ctx) {
return ctx.isPointInPath(this.path, x, y);
}
}
// 创建可交互的图形
const shapes = [];
// 创建圆形路径
const circlePath = new Path2D();
circlePath.arc(100, 1100, 40, 0, Math.PI * 2);
shapes.push(new ClickableShape(circlePath, 'lightgreen', 'darkgreen'));
// 创建矩形路径
const rectPath = new Path2D();
rectPath.rect(200, 1060, 80, 80);
shapes.push(new ClickableShape(rectPath, 'lightcoral', 'darkred'));
// 创建星形路径
const starPath = new Path2D();
starPath.moveTo(350, 1060);
for (let i = 0; i < 10; i++) {
const angle = (i * Math.PI) / 5;
const radius = i % 2 === 0 ? 40 : 20;
const x = 350 + Math.cos(angle) * radius;
const y = 1100 + Math.sin(angle) * radius;
starPath.lineTo(x, y);
}
starPath.closePath();
shapes.push(new ClickableShape(starPath, 'gold', 'orange'));
// 绘制所有图形
function drawShapes() {
shapes.forEach(shape => shape.draw(ctx));
}
// 鼠标事件处理
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let needsRedraw = false;
shapes.forEach(shape => {
const wasHovered = shape.isHovered;
shape.isHovered = shape.contains(x, y, ctx);
if (wasHovered !== shape.isHovered) {
needsRedraw = true;
}
});
if (needsRedraw) {
ctx.clearRect(50, 1020, 400, 160);
drawShapes();
}
});
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
shapes.forEach(shape => {
if (shape.contains(x, y, ctx)) {
shape.isClicked = !shape.isClicked;
}
});
ctx.clearRect(50, 1020, 400, 160);
drawShapes();
});
// 初始绘制
drawShapes();
5.2 路径碰撞检测
// 路径之间的碰撞检测
function pathCollision(path1, path2, ctx) {
// 简单的边界框检测
// 在实际应用中,可能需要更复杂的算法
// 这里使用采样点检测
const samplePoints = 20;
for (let i = 0; i < samplePoints; i++) {
for (let j = 0; j < samplePoints; j++) {
const x = i * (canvas.width / samplePoints);
const y = j * (canvas.height / samplePoints);
if (ctx.isPointInPath(path1, x, y) && ctx.isPointInPath(path2, x, y)) {
return true;
}
}
}
return false;
}
6. 路径动画
6.1 路径描边动画
// 路径描边动画
class PathAnimation {
constructor(path, duration = 2000) {
this.path = path;
this.duration = duration;
this.startTime = null;
this.isAnimating = false;
}
start() {
this.startTime = performance.now();
this.isAnimating = true;
this.animate();
}
animate() {
if (!this.isAnimating) return;
const currentTime = performance.now();
const elapsed = currentTime - this.startTime;
const progress = Math.min(elapsed / this.duration, 1);
// 清除之前的绘制
ctx.clearRect(500, 1020, 300, 160);
// 设置虚线动画
const dashLength = 200;
const dashOffset = dashLength * (1 - progress);
ctx.setLineDash([dashLength, dashLength]);
ctx.lineDashOffset = dashOffset;
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.stroke(this.path);
if (progress < 1) {
requestAnimationFrame(() => this.animate());
} else {
this.isAnimating = false;
ctx.setLineDash([]);
}
}
}
// 创建动画路径
const animPath = new Path2D();
animPath.moveTo(520, 1040);
animPath.bezierCurveTo(600, 1020, 700, 1080, 780, 1040);
animPath.bezierCurveTo(760, 1120, 640, 1160, 520, 1140);
animPath.closePath();
const pathAnim = new PathAnimation(animPath, 3000);
// 添加按钮来启动动画
const startButton = document.createElement('button');
startButton.textContent = '开始路径动画';
startButton.onclick = () => pathAnim.start();
document.body.appendChild(startButton);
7. 实用路径工具
7.1 路径简化
// 道格拉斯-普克算法简化路径
function simplifyPath(points, tolerance) {
if (points.length <= 2) return points;
// 找到距离线段最远的点
let maxDistance = 0;
let maxIndex = 0;
const start = points[0];
const end = points[points.length - 1];
for (let i = 1; i < points.length - 1; i++) {
const distance = pointToLineDistance(points[i], start, end);
if (distance > maxDistance) {
maxDistance = distance;
maxIndex = i;
}
}
// 如果最大距离大于容差,递归简化
if (maxDistance > tolerance) {
const left = simplifyPath(points.slice(0, maxIndex + 1), tolerance);
const right = simplifyPath(points.slice(maxIndex), tolerance);
return left.slice(0, -1).concat(right);
} else {
return [start, end];
}
}
// 计算点到线段的距离
function pointToLineDistance(point, lineStart, lineEnd) {
const A = point.x - lineStart.x;
const B = point.y - lineStart.y;
const C = lineEnd.x - lineStart.x;
const D = lineEnd.y - lineStart.y;
const dot = A * C + B * D;
const lenSq = C * C + D * D;
if (lenSq === 0) return Math.sqrt(A * A + B * B);
const param = dot / lenSq;
let xx, yy;
if (param < 0) {
xx = lineStart.x;
yy = lineStart.y;
} else if (param > 1) {
xx = lineEnd.x;
yy = lineEnd.y;
} else {
xx = lineStart.x + param * C;
yy = lineStart.y + param * D;
}
const dx = point.x - xx;
const dy = point.y - yy;
return Math.sqrt(dx * dx + dy * dy);
}
7.2 路径测量
// 测量路径长度
function measurePathLength(path, ctx) {
// 这是一个简化的实现
// 实际应用中可能需要更精确的算法
let totalLength = 0;
const precision = 100; // 采样精度
// 通过采样点计算近似长度
for (let i = 0; i < precision; i++) {
const t1 = i / precision;
const t2 = (i + 1) / precision;
// 这里需要根据具体路径类型实现
// 对于简单情况,可以使用线性插值
}
return totalLength;
}
// 获取路径上的点
function getPointOnPath(path, t, ctx) {
// t 是 0-1 之间的参数
// 返回路径上对应位置的点
// 这需要根据具体的路径实现
// 对于贝塞尔曲线,可以使用德卡斯特里奥算法
}
8. 小结
本章深入探讨了Canvas的路径系统和复杂图形绘制:
- 路径系统:深入理解路径的概念、状态管理和多子路径
- 高级路径方法:arcTo()、Path2D对象和填充规则
- 复杂图形:正多边形、星形、花瓣等图案的绘制
- 曲线路径:平滑曲线、波浪线、螺旋线的实现
- 路径交互:点击检测、碰撞检测和交互响应
- 路径动画:描边动画和路径变形效果
- 实用工具:路径简化、测量和操作工具
下一章我们将学习文本渲染与样式控制,包括字体设置、文本测量和高级文本效果。
9. 练习题
- 创建一个可以绘制任意正多边形的交互工具
- 实现一个路径编辑器,支持添加、删除和移动控制点
- 绘制一个复杂的曼陀罗图案
- 创建一个路径动画,模拟手写文字效果
- 实现一个简单的矢量图形编辑器