利用Canvas与射线法判断点是否在封闭多边形内

作者:白露

时间:2026-10-31

浏览:218

本文介绍在前端开发中,基于已知顶点坐标的封闭图形,如何利用Canvas绘制并结合射线法判断点击位置是否位于图形内部。通过监听点击事件获取坐标,计算射线与多边形边界的交点数量,若为奇数则判定为包含关系。

文章目录

背景

在开发过程中,遇到一个常见场景:已知封闭图形的顶点坐标,需要判断用户点击时是否落在图形内部。虽然算法领域有多种解决方案,但从前端交互实现的角度来看,直接获取结果的手段有限。因此,这里借鉴几何学中封闭图形边界线段与点的关系,利用射线与线段相交的交点数量来判断点击位置是否在图形内部(若在内部,则给予高亮反馈)。

图形绘制

由于项目场景涉及图片识别,前端获取的数据是二维数组形式的像素点,数据结构类似如下:

const vertexs = [
    [100, 200],
    [150, 300],
    [240, 300],
    // ...
]

数组中的数据表示在x轴和y轴上的像素坐标。首先利用Canvas将其绘制在页面上。由于项目技术架构选用React,此处先使用React的语法糖进行表述。

import React from 'react';
let canvas, ctx;

export default class Index extends React.Component() {
    componentDidMount():void {
        // 初始化画布信息
        canvas = document.getElementById('containerCanvas');
        ctx = canvas.getContext('2d');
        // 绘制封闭图形
        this.drawEnclosedGraph();
    }
    drawEnclosedGraph = () => {
        // 这里的vertexs是顶点数据的来源
        const { vertexs = [] } = this.props;
        // 封闭图形最少需要三个顶点坐标
        if(Array.isArray(vertexs) && vertexs.length > 2) {
            ctx.beginPath();
            ctx.moveTo(vertexs[0][0], vertexs[0][1]);
            for(let i = 1; i < vertexs.length; i++) {
                ctx.lineTo(vertexs[i][0], vertexs[i][1]);
            }
            ctx.closePath();
            ctx.lineWidth = 2;
            ctx.strokeStyle = 'orange';
            ctx.stroke();
        } else {
            console.error('err');
        }
    }
    render() {
        // 画布的宽高在我的实际场景下是涉及顶点信息识别地图的尺寸,需比例尺与底图的处理转化,与本文想叙述的关系不大,先设为1000/800
        return <canvas id="containerCanvas" width={1000} height={800}/>
    }
}

通过上述 drawEnclosedGraph 函数可以将顶点坐标 vertexs 下的数据渲染在画布中。

事件监听

通过监听点击事件,获取点击的坐标位置x和y值。由于图形操作较多,在实现上采用了类似事件委托的方式触发click事件。具体做法是在Canvas上层添加一个蒙层用于捕获点击事件,获取点击位置后,将x和y值传递给该Canvas组件,代码如下:

clickMask = (e) => {
    const getAbsLeft = (obj) => {
        let l = obj.offsetLeft;
        while(obj.offsetParent != null) {
            obj = obj.offsetParent;
            l += obj.offsetLeft;
        }
        return l;
    }
    const getAbsTop = (obj) => {
        let top = obj.offsetTop;
        while(obj.offsetParent != null) {
            obj = obj.offsetParent;
            top += obj.offsetTop;
        }
        return top;
    }
    const getAbsScrollD = (obj) => {
        let scrollToTop = obj.scrollTop;
        let scrollToLeft = obj.scrollLeft;
        while(obj.parentElement != null) {
            obj = obj.parentElement;
            scrollToTop += obj.scrollTop;
            scrollToLeft += obj.scrollLeft;
        }
        return {
            scrollToTop,
            scrollToLeft,
        };
    }
    const maskDiv = e.target;
    // 由于页面元素排序,或者滚动条滚动会对点击位置产生影响,这边通过上述函数作出补偿,保证点击位置不受滚动条影响
    const scrollD = getAbsScrollD(maskDiv);
    const divToTop = getAbsTop(maskDiv);
    const divToLeft = getAbsLeft(maskDiv);
    const divScrollTop = scrollD['scrollToTop'] || 0;
    const divScrollLeft = scrollD['scrollToLeft'] || 0;
    const mouseX = e.clientX;
    const mouseY = e.clientY;
    const clickInMapX = mouseX - divToLeft + divScrollLeft;
    // 这里的800是画布高度,如果场景中是变量,也可以通过clientHeight去获取container高度
    const clickInMapY = 800 - (mouseY - divToTop) - divScrollTop;
    this.isClickGraph({
        x: clickInMapX,
        y: clickInMapY
    });

}

点击结果判断

成功获取点击的XY值后,我们可以开始判断点击位置是否在图形内部。主要方法是将相邻顶点坐标转化为一元一次函数的表述方式,再通过判断点击位置向右延伸的射线与所有线段相交的数量是否为奇数,来判定点击位置是否在图形内部,代码如下:

const transVertexs2Lines = () => {
    const { vertexs = [] } = this.props;
    if(Array.isArray(vertexs) && vertexs.length > 2) {
        const lines = vertexs.map((curP, idx) => {
            const nextP = (idx === vertexs.length - 1) ? vertexs[0] : vertexs[idx + 1];
            const x1 = curP[0],
                y1 = curP[1],
                x2 = nextP[0],
                y2 = nextP[1];
            const lineRange = {
                yMax: Math.max(y1, y2),
                xMax: Math.max(x1, x2),
                yMin: Math.min(y1, y2),
                xMin: Math.min(x1, x2),
            };
            if(x1 === x2) {
                return {
                    k: 'empty',
                    b: 'empty',
                    ...lineRange
                }
            } else if(y1 === y2) {
                return {
                    k: 0,
                    b: y1,
                    ...lineRange,
                }
            } else {
                const k =  ((y1 - y2) / (x1 - x2)).toFixed(2);
                const b = y1 - (k * x1);
                return {
                    k,
                    b,
                    ...lineRange,
                }
            }
        })
        this.setState({
            linesInfo: lines
        })
    } else {
        console.error('err');
    }
}
const isClickGraph = (clickLocation = {x: 0, y: 0}) => {
    const { linesInfo = [] } = this.state;
    let interSectionNum = 0;
    Array.isArray(linesInfo) && linesInfo.forEach(eachLine => {
        const {
            k,b,
            xMin,
            xMax,
            yMin,
            yMax,
        } = eachLine;
        if(k === 0) {
            // 无交点
        } else if(k === 'empty') {
            if(x < xMin && y < yMax && y > yMin) {
                interSectionNum++;
            }
        } else {
            if(y > yMin && y < yMax) {
                const secX = ((y - b) / k).toFixed(2);
                if(secX > x) {
                    interSectionNum++;
                }
            }
        }
    })
    if(interSectionNum > 0 && interSectionNum % 2 === 1) {
        return true;
    } else {
        return false;
    }
}

通过上述代码中的 isClickGraph 函数可以判断出点击位置是否在图形内部。为了方便理解,下图简单解释了该算法的示意图。

背景

当相交点位为奇数个时,判定为在图形内部。

以上就是利用Canvas判断点与封闭图形包含关系的详细内容,更多关于Canvas判断点与封闭图形关系的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章