ZetCode

JavaScript Canvas isPointInPath 教程

最后修改于 2025 年 4 月 3 日

本教程将探讨 JavaScript 中 Canvas 的 isPointInPath 方法。此方法用于检测一个点是否在当前路径内,从而实现命中检测。它对于游戏和图表等交互式 Canvas 应用至关重要。

基本定义

isPointInPath 检查指定的坐标是否在当前路径内。如果点在路径内,则返回 true,否则返回 false。这对于检测 Canvas 元素的点击或悬停非常有用。

该方法有两种形式:isPointInPath(x, y)isPointInPath(path, x, y, fillRule)。第二种形式用于 Path2D 对象,并可选择填充规则(nonzero 或 evenodd)。

isPointInPath 基本用法

此示例展示了如何检测矩形路径内的点击。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Basic isPointInPath</title>
</head>
<body>

<canvas id="myCanvas" width="300" height="200"></canvas>
<p id="output">Click on the rectangle</p>

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const output = document.getElementById('output');
    
    // Draw rectangle
    ctx.beginPath();
    ctx.rect(50, 50, 200, 100);
    ctx.fillStyle = 'lightblue';
    ctx.fill();
    ctx.stroke();
    
    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)) {
            output.textContent = 'Clicked inside the rectangle!';
        } else {
            output.textContent = 'Clicked outside the rectangle';
        }
    });
</script>

</body>
</html>

此代码在 Canvas 上创建一个蓝色矩形。单击时,它会使用 isPointInPath 检查点击坐标是否在矩形路径内。

点击坐标相对于 Canvas 位置进行了调整。结果将显示在 Canvas 下方的段落元素中。

多形状检测

此示例演示了对多个形状的点击检测。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Multiple Shapes Detection</title>
</head>
<body>

<canvas id="myCanvas" width="400" height="300"></canvas>
<p id="output">Click on a shape</p>

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const output = document.getElementById('output');
    
    // Draw shapes
    ctx.beginPath();
    ctx.rect(50, 50, 100, 100);
    ctx.fillStyle = 'lightgreen';
    ctx.fill();
    ctx.stroke();
    
    ctx.beginPath();
    ctx.arc(250, 100, 50, 0, Math.PI * 2);
    ctx.fillStyle = 'lightcoral';
    ctx.fill();
    ctx.stroke();
    
    ctx.beginPath();
    ctx.moveTo(300, 200);
    ctx.lineTo(350, 250);
    ctx.lineTo(250, 250);
    ctx.closePath();
    ctx.fillStyle = 'lightblue';
    ctx.fill();
    ctx.stroke();
    
    canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        // Check rectangle
        ctx.beginPath();
        ctx.rect(50, 50, 100, 100);
        if (ctx.isPointInPath(x, y)) {
            output.textContent = 'Clicked on the square';
            return;
        }
        
        // Check circle
        ctx.beginPath();
        ctx.arc(250, 100, 50, 0, Math.PI * 2);
        if (ctx.isPointInPath(x, y)) {
            output.textContent = 'Clicked on the circle';
            return;
        }
        
        // Check triangle
        ctx.beginPath();
        ctx.moveTo(300, 200);
        ctx.lineTo(350, 250);
        ctx.lineTo(250, 250);
        ctx.closePath();
        if (ctx.isPointInPath(x, y)) {
            output.textContent = 'Clicked on the triangle';
            return;
        }
        
        output.textContent = 'Clicked outside all shapes';
    });
</script>

</body>
</html>

此示例绘制了三个形状:一个正方形、一个圆形和一个三角形。单击时,它会检查每个形状的路径以确定单击的是哪个。

对于每个形状,我们在检查 isPointInPath 之前会重新创建其路径。为了优化性能,一旦检测到命中,该方法就会立即返回。

使用 Path2D 对象

此示例展示了如何将 Path2D 对象与 isPointInPath 一起使用。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Path2D with isPointInPath</title>
</head>
<body>

<canvas id="myCanvas" width="400" height="300"></canvas>
<p id="output">Click on a shape</p>

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const output = document.getElementById('output');
    
    // Create Path2D objects
    const starPath = new Path2D();
    starPath.moveTo(100, 25);
    starPath.lineTo(120, 75);
    starPath.lineTo(175, 75);
    starPath.lineTo(135, 100);
    starPath.lineTo(150, 150);
    starPath.lineTo(100, 125);
    starPath.lineTo(50, 150);
    starPath.lineTo(65, 100);
    starPath.lineTo(25, 75);
    starPath.lineTo(80, 75);
    starPath.closePath();
    
    const heartPath = new Path2D();
    heartPath.moveTo(250, 75);
    heartPath.bezierCurveTo(250, 37, 300, 25, 300, 75);
    heartPath.bezierCurveTo(300, 125, 250, 150, 250, 175);
    heartPath.bezierCurveTo(250, 150, 200, 125, 200, 75);
    heartPath.bezierCurveTo(200, 25, 250, 37, 250, 75);
    heartPath.closePath();
    
    // Draw paths
    ctx.fillStyle = 'pink';
    ctx.fill(starPath);
    ctx.stroke(starPath);
    
    ctx.fillStyle = 'red';
    ctx.fill(heartPath);
    ctx.stroke(heartPath);
    
    canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        if (ctx.isPointInPath(starPath, x, y)) {
            output.textContent = 'Clicked on the star';
        } else if (ctx.isPointInPath(heartPath, x, y)) {
            output.textContent = 'Clicked on the heart';
        } else {
            output.textContent = 'Clicked outside shapes';
        }
    });
</script>

</body>
</html>

此示例使用 Path2D 对象创建复杂形状(星形和心形)。Path2D 允许在不重新创建的情况下重用路径,用于命中检测。

isPointInPath 方法接受 Path2D 作为第一个参数。这使得代码比重新创建路径更简洁、更有效。

填充规则演示

此示例演示了不同填充规则对点检测的影响。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Fill Rule with isPointInPath</title>
</head>
<body>

<canvas id="myCanvas" width="400" height="300"></canvas>
<p id="output">Click inside the concentric circles</p>

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const output = document.getElementById('output');
    
    // Draw concentric circles
    ctx.beginPath();
    ctx.arc(150, 150, 100, 0, Math.PI * 2);
    ctx.arc(150, 150, 50, 0, Math.PI * 2);
    ctx.fillStyle = 'lightgray';
    ctx.fill();
    ctx.stroke();
    
    canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        // Recreate path for hit detection
        ctx.beginPath();
        ctx.arc(150, 150, 100, 0, Math.PI * 2);
        ctx.arc(150, 150, 50, 0, Math.PI * 2);
        
        // Check with different fill rules
        const nonzero = ctx.isPointInPath(x, y, 'nonzero');
        const evenodd = ctx.isPointInPath(x, y, 'evenodd');
        
        output.textContent = `Nonzero: ${nonzero}, Evenodd: ${evenodd}`;
    });
</script>

</body>
</html>

此示例展示了填充规则如何影响复杂路径中的点检测。我们绘制了两个同心圆,并使用两种规则检查点包含情况。

“nonzero”规则(默认)将中心视为在内部,而“evenodd”规则将其视为在外部。这表明填充规则如何改变自相交路径的命中检测行为。

带有命中检测的交互式绘图

此示例创建了一个带有形状选择功能的交互式绘图应用程序。

index.html
<!DOCTYPE html>
<html>
<head>
    <title>Interactive Drawing with Hit Detection</title>
</head>
<body>

<canvas id="myCanvas" width="500" height="400"></canvas>
<div>
    <button id="addRect">Add Rectangle</button>
    <button id="addCircle">Add Circle</button>
    <p id="output">Click on shapes to select them</p>
</div>

<script>
    const canvas = document.getElementById('myCanvas');
    const ctx = canvas.getContext('2d');
    const output = document.getElementById('output');
    const addRect = document.getElementById('addRect');
    const addCircle = document.getElementById('addCircle');
    
    let shapes = [];
    let selectedShape = null;
    
    class Shape {
        constructor(path, type, color) {
            this.path = path;
            this.type = type;
            this.color = color;
            this.selected = false;
        }
    }
    
    function drawShapes() {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        
        shapes.forEach(shape => {
            ctx.fillStyle = shape.selected ? 'yellow' : shape.color;
            ctx.fill(shape.path);
            ctx.stroke(shape.path);
            
            // Draw selection indicator
            if (shape.selected) {
                ctx.strokeStyle = 'red';
                ctx.lineWidth = 3;
                ctx.stroke(shape.path);
                ctx.strokeStyle = 'black';
                ctx.lineWidth = 1;
            }
        });
    }
    
    // Add rectangle button
    addRect.addEventListener('click', () => {
        const x = Math.random() * 350 + 50;
        const y = Math.random() * 250 + 50;
        const width = Math.random() * 100 + 50;
        const height = Math.random() * 100 + 50;
        
        const path = new Path2D();
        path.rect(x, y, width, height);
        
        const colors = ['lightblue', 'lightgreen', 'pink', 'lavender'];
        const color = colors[Math.floor(Math.random() * colors.length)];
        
        shapes.push(new Shape(path, 'rectangle', color));
        drawShapes();
    });
    
    // Add circle button
    addCircle.addEventListener('click', () => {
        const x = Math.random() * 350 + 50;
        const y = Math.random() * 250 + 50;
        const radius = Math.random() * 50 + 25;
        
        const path = new Path2D();
        path.arc(x, y, radius, 0, Math.PI * 2);
        
        const colors = ['lightcoral', 'lightseagreen', 'plum', 'wheat'];
        const color = colors[Math.floor(Math.random() * colors.length)];
        
        shapes.push(new Shape(path, 'circle', color));
        drawShapes();
    });
    
    // Canvas click handler
    canvas.addEventListener('click', (e) => {
        const rect = canvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        // Deselect all first
        shapes.forEach(shape => shape.selected = false);
        selectedShape = null;
        
        // Check shapes in reverse order (top to bottom)
        for (let i = shapes.length - 1; i >= 0; i--) {
            if (ctx.isPointInPath(shapes[i].path, x, y)) {
                shapes[i].selected = true;
                selectedShape