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

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

相关推荐
Wyc7240938 分钟前
HTML:入门
前端·html
Sunny_lxm38 分钟前
自定义列甘特图,原生开发dhtmlxgantt根特图,根据数据生成只读根特图,页面展示html demo
前端·html·甘特图·dhtmlxgantt
熊猫钓鱼>_>2 小时前
建筑IT数字化突围:建筑设计企业的生存法则重塑
前端·javascript·easyui
GISer_Jing4 小时前
前端性能指标及优化策略——从加载、渲染和交互阶段分别解读详解并以Webpack+Vue项目为例进行解读
前端·javascript·vue
不知几秋4 小时前
数字取证-内存取证(volatility)
java·linux·前端
水银嘻嘻5 小时前
08 web 自动化之 PO 设计模式详解
前端·自动化
Zero1017137 小时前
【详解pnpm、npm、yarn区别】
前端·react.js·前端框架
&白帝&7 小时前
vue右键显示菜单
前端·javascript·vue.js
Wannaer7 小时前
从 Vue3 回望 Vue2:事件总线的前世今生
前端·javascript·vue.js
羽球知道8 小时前
在Spark搭建YARN
前端·javascript·ajax