利用canvas判断点与封闭图形的包含关系

背景

今天在写代码的时候遇到一个场景,在一个封闭图形顶点已知的情况下判断点击时是否点击在图形内部。可能在算法端有不少解决方案,但从一个前端的角度交互实现,第一反应没有很好的手段,于是借鉴封闭图形的围成线段与点之间的关系,通过射线与线段相交的点位数量来判断是否点击的位置是否在图形内。(如果在图形内部,给予高亮反馈)

图形绘制

由于项目场景和图片识别相关,前端获取的数据是二维数组下的像素点,类似以下结构:

js 复制代码
const vertexs = [
    [100, 200],
    [150, 300],
    [240, 300],
    // ...
]

其中数组中的数据表示在x,y轴像素坐标。首先利用canvas将其绘制在页面上, 由于技术架构当时选用的React,这边也先用React的语法糖表述

jsx 复制代码
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组件传递的方式,代码如下

js 复制代码
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值后,我们可以开始判断点击位置是否在图形内部,这边主要用的方式是将相邻顶点坐标转化成一元一次函数的表述方式,再通过判断点击的位置向右延伸的射线与所有线段相交的数量是否为奇数来判断点是否点击位置在图形内部,代码如下:

js 复制代码
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 函数可以判断出点击位置是否在图形内部,为了方便理解,下图简单解释了该算法的丑陋图片

当相交点位为奇数个时,为在图形内部。好了,希望这篇文章能对你在遇到该类场景时有思路上的启发,作者小菜轻喷。

相关推荐
GDAL3 分钟前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿3 分钟前
react防止页面崩溃
前端·react.js·前端框架
z千鑫30 分钟前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js
m0_748256141 小时前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小白学前端6662 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203982 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
outstanding木槿3 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08213 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
隐形喷火龙3 小时前
element ui--下拉根据拼音首字母过滤
前端·vue.js·ui
m0_748241124 小时前
Selenium之Web元素定位
前端·selenium·测试工具