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的路径系统和复杂图形绘制:

  1. 路径系统:深入理解路径的概念、状态管理和多子路径
  2. 高级路径方法:arcTo()、Path2D对象和填充规则
  3. 复杂图形:正多边形、星形、花瓣等图案的绘制
  4. 曲线路径:平滑曲线、波浪线、螺旋线的实现
  5. 路径交互:点击检测、碰撞检测和交互响应
  6. 路径动画:描边动画和路径变形效果
  7. 实用工具:路径简化、测量和操作工具

下一章我们将学习文本渲染与样式控制,包括字体设置、文本测量和高级文本效果。

9. 练习题

  1. 创建一个可以绘制任意正多边形的交互工具
  2. 实现一个路径编辑器,支持添加、删除和移动控制点
  3. 绘制一个复杂的曼陀罗图案
  4. 创建一个路径动画,模拟手写文字效果
  5. 实现一个简单的矢量图形编辑器