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

 更新时间:2024年04月28日 08:54:31   作者:小小花生  
今天在写代码的时候遇到一个场景,在一个封闭图形顶点已知的情况下判断点击时是否点击在图形内部,本文给大家介绍了如何利用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判断点与封闭图形关系的资料请关注脚本之家其它相关文章!

相关文章

  • 利用AJAX实现WordPress中的文章列表及评论的分页功能

    利用AJAX实现WordPress中的文章列表及评论的分页功能

    在文中列表页方面利用AJAX制作滚动到底触发翻页的效果比较常见,而在评论加载时AJAX显示正在加载也很常用,下面就来看一下如何利用AJAX实现WordPress中的文章列表及评论的分页功能
    2016-05-05
  • 微信小程序 动态绑定数据及动态事件处理

    微信小程序 动态绑定数据及动态事件处理

    这篇文章主要介绍了微信小程序 动态绑定数据及动态事件处理的相关资料,需要的朋友可以参考下
    2017-03-03
  • 微信小程序实现红包雨功能

    微信小程序实现红包雨功能

    这篇文章主要为大家详细介绍了微信小程序实现红包雨功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-07-07
  • D3.js实现文本的换行详解

    D3.js实现文本的换行详解

    相信大家都知道在SVG中添加文本是使用text元素。但这个元素不能够自动换行,超出的部分就显示不出来了,怎么办呢?下面通过这篇文章来给大家详细介绍下实现的过程。
    2016-10-10
  • JavaScript计算出两个数的差值

    JavaScript计算出两个数的差值

    这篇文章主要为大家详细介绍了JavaScript计算出两个数的差值,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • 深入理解JavaScript中的for循环

    深入理解JavaScript中的for循环

    这篇文章主要给大家深入的介绍了JavaScript中的for循环,其中包括ES5中的三种for循环,分别是简单for循环、for-in以及forEach,另外还详细介绍了ES6新增的一种循环:for-of ,有需要的朋友可以参考借鉴,下面来一起看看吧。
    2017-02-02
  • JavaScript基于setTimeout实现计数的方法

    JavaScript基于setTimeout实现计数的方法

    这篇文章主要介绍了JavaScript基于setTimeout实现计数的方法,涉及javascript中setTimeout方法的使用技巧,需要的朋友可以参考下
    2015-05-05
  • JavaScript制作颜色反转小游戏

    JavaScript制作颜色反转小游戏

    本文给大家分享的是一个JavaScript实现的颜色反转的小游戏的源码,非常的简单好玩,有需要的小伙伴可以参考下
    2016-09-09
  • 微信小程序 生成携带参数的二维码

    微信小程序 生成携带参数的二维码

    这篇文章主要介绍了微信小程序 生成携带参数的二维码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值
    2019-10-10
  • js经验分享 JavaScript反调试技巧

    js经验分享 JavaScript反调试技巧

    在这篇文章中,我打算跟大家总结一下关于JavaScript反调试技巧方面的内容。值得一提的是,其中有些方法已经被网络犯罪分子广泛应用到恶意软件之中了,需要的朋友可以参考下
    2018-03-03

最新评论