【前端基础-Canvas】canvas如何触发事件?

348 阅读5分钟

牛客面经上面看到关于canvas的问题,梳理一下

  1. echarts是如何在canvas中识别鼠标移到了哪个元素?
  2. canvas和svg的区别,以及使用场景?

1.Echarts是如何在canvas中识别鼠标移到了哪个元素?

Echarts的底层渲染时基于canvas实现,但是canvas只是一个画布,并不能为画出来的位图添加事件监听,这一点和svg矢量图不同。

因此echarts针对canvas实现了一套事件机制,基本的思想是将绘制的元素位置存储起来,但鼠标进入到绘制的图像区域时触发相应的事件。

因为问题关键在于,如何确定鼠标是否在指定元素区域,问题转化成了:判断一个点是否在一个复杂多边形的内部

1.1 如何判断一个点是否在多边形内部?

在GIS中,判断一个坐标是否在多边形内部是个经常要遇到的问题。乍听起来还挺复杂。根据W. Randolph Franklin 提出的PNPoly算法,只需区区几行代码就解决了这个问题。[1]

判断一个点是否在多边形内部,一般有以下四种方法

  1. 面积和判别法: 判断目标点与多边形的每条边组成的三角形面积和是否等于该多边形,相等则在多边形内部。
  2. 夹角和判别法:判断目标点与所有边的夹角和是否为360度,为360度则在多边形内部。
  3. 引射线法:从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果有奇数个交点,则说明在内部,如果有偶数个交点,则说明在外部。(采纳)
  4. 转角法:按照多边形顶点逆时针顺序,根据顶点和判断点连线的方向正负(设定角度逆时针为正)求和判断;

1.2 射线法

射线法 的核心时从起点发出一个射线,然后计算多边形的边是否与这条射线相交。

aHR0cHM6Ly91cGxvYWQtaW1hZ2VzLmppYW5zaHUuaW8vdXBsb2FkX2ltYWdlcy8yNDczNTQzLTVkMjhlNzczNTA3ODJjMDQucG5nP2ltYWdlTW9ncjIvYXV0by1vcmllbnQvc3RyaXB8aW1hZ2VWaWV3Mi8yL3cvNTY4L2Zvcm1hdC93ZWJw.png

如上图所示从起点发出一条水平射线,可以通过计算通过计算起点的纵坐标是否在边的起点和终点的纵坐标所在的区间,以此来判断从起点发出的直线是否能够穿过这条边。

if ((p1[0] < p[0] && p2[0] >= p[0]) || (p2[0] < p[0] && p1[0] >= p[0])) 

接下来判断由起点出发的摄像是否穿过这条边,问题关键在于判断直线与边的交点是在起点的左边还是右边。 这个一个十分简单的高中数学问题。

问: 已知直线l,直线上两点p1,p2,和直线外一点p,求过点p的水平直线与直线l的交点?

解题思路也很简单,y=kx+b,利用p1,p2,求出直线方程,带入纵坐标,求出横坐标,最后与点p比较大小即可。

image.png

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<style>
    #canvas {
        margin-top: 20px;
        background: #639fb9;
    }
</style>
<body>


<div>
    <canvas id="canvas" width="600px" height="500px">您的浏览器不支持canvas</canvas>
</div>
</body>

<script>
    const arr1 = [[649, 228], [733, 215], [825, 220], [974, 296], [1036, 416], [1015, 512], [968, 562], [898, 574], [874, 518], [794, 478], [717, 478], [713, 378], [680, 306]];
    const arr2 = [[393, 404], [401, 353], [507, 256], [650, 229], [713, 378], [715, 476], [576, 447], [449, 481]];
    const arr3 = [[424, 663], [546, 637], [679, 669], [647, 723], [575, 753], [472, 747], [427, 665]];
    const arr4 = [[392, 407], [338, 504], [347, 557], [335, 590], [387, 665], [423, 664], [546, 638], [543, 585], [484, 516], [449, 482]];
    const arr5 = [[450, 483], [544, 587], [547, 637], [679, 668], [811, 660], [888, 613], [898, 575], [873, 520], [794, 481], [716, 477], [577, 447]];
    const scale = 0.5; //缩放倍数
    arr1.map((item => {
        item[0] *= scale;
        item[1] *= scale;
    }))
    arr2.map((item => {
        item[0] *= scale;
        item[1] *= scale;
    }))
    arr3.map((item => {
        item[0] *= scale;
        item[1] *= scale;
    }))
    arr4.map((item => {
        item[0] *= scale;
        item[1] *= scale;
    }))
    arr5.map((item => {
        item[0] *= scale;
        item[1] *= scale;
    }))
    const c = document.getElementById("canvas");
    const ctx = c.getContext("2d");

    function draw() {
        fillArea(-1);
    }
    // 当前阶段为填充其他都不填充
    function fillArea(areaNumber) {
        const arr = [arr1, arr2, arr3, arr4, arr5];
        for (let i = 0; i < arr.length; i++) {
            ctx.beginPath();
            ctx.strokeStyle = 'rgb(248,248,248)'
            ctx.moveTo(arr[i][0], arr[i][1]);

            for (let point of arr[i]) {
                ctx.lineTo(point[0], point[1]);
            }
            ctx.font = "14px bold 黑体";
            ctx.fillStyle = 'rgb(0,2,1)';
            ctx.fillText(i, arr[i][0] + 20, arr[i][1] + 20);
            ctx.closePath();
            ctx.stroke();
            ctx.fillStyle = areaNumber === i + 1 ? 'rgb(14,122,49)' : 'rgb(225,216,216)'; // 红
            ctx.fill();

        }

    }

    draw()

    canvas.addEventListener('mousemove', MoveArea)

    function MoveArea(e) {
        let areaNumber = 0;
        // 判断在那歌区域里面
        let p = [e.offsetX, e.offsetY];

        if (calculate(arr1, p)) {
            areaNumber = 1;
        } else if (calculate(arr2, p)) {
            areaNumber = 2;
        } else if (calculate(arr3, p)) {
            areaNumber = 3;
        } else if (calculate(arr4, p)) {
            areaNumber = 4;
        } else if (calculate(arr5, p)) {
            areaNumber = 5;
        } else areaNumber = -1;
        // console.log(calculate(arr1, [434, 168]))
        fillArea(areaNumber);
        console.log(areaNumber)

    }

    console.log(arr1)

    // 判断鼠标是否在这个区域里面
    function getMoveArea(p, arr) {
        let result = false;
        let start = arr[arr.length - 1];
        for (let i = 0; i < arr.length; i++) {
            let end = arr[i];
            if ((start[0] < p[0] && end[0] >= p[0]) || (start[0] > p[0] && end[0] <= p[0])) {
                result = !result;
            }

            start = end;
        }
        return result;
    }

    function calculate(arr, p) {
        let count = arr.length;
        let result = false;
        for (let i = 0, j = count - 1; i < count; i++) {
            let p1 = arr[i];
            let p2 = arr[j];
            if ((p1[0] < p[0] && p2[0] >= p[0]) || (p2[0] < p[0] && p1[0] >= p[0])) {
                if ((p[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1] < p[1]) {
                    result = !result;
                }
                // if (((p[0] - p1[0]) / (p2[0] - p1[0]) )< ((p[1] - p1[1]) / (p2[1] - p1[1]))) {
                //     result = !result;
                // }
            }
            j = i;
        }
        return result;
    }
</script>
</html>

1.3. 参考文章

[1] www.cnblogs.com/anningwang/…

[2] blog.csdn.net/libaineu200…

2. canvas和svg的区别,以及使用场景?

2.1 SVG(Scalable Vector Graphics)

概念: SVG是一种描述二维的矢量图形。 是基于xml的标记语言,SVG能够优雅而简洁的渲染出不同大小的图形,本质上svg相对于图形就好比HTML相对于文本。

特性: SVG 通过创建标签元素来表示图形元素。 从写法上来看,SVG类似于HTML,写法友好,但SVG需要由浏览器渲染和管理,将元素节点维护在DOM树中,在复杂的动态场景中(频繁的增删改查),SVG与一般的DOM元素一样会带来DOM操作的开销,所以SVG的渲染性能较低(可以通过虚拟DOM的方案尽可能的减少重绘,从而达到优化SVG的渲染问题,但是也只能解决一部分问题,当节点树太多时,此方法也无能为力),

优点: svg的优点是不依赖于分辨率。 s在任何分辨率下都可以被高质量的打印,svg可以在图像质量不下降的情况下被放大,不会失真。

使用场景:

  • 最适合大型渲染区域的应用程序,复杂度高(频繁的dom操作)会减慢应用,其中有一些图像会被重绘,不适合游戏
  • 对点线面这样的图形很擅长,因为这个特性也会被用来绘制地图,百度地图就是用svg来做,但是不能实现太复杂的效果,标签不能太密集,一旦太密集,牵一发而动全身,效率急剧下降。

2.2 canvas标签

概念: Canvas标签,在javaScript创建画布,更偏向于渲染层,能够提供底层图形渲染API。

特性: 在实际业务场景中,Canvas的简单操作和高效的渲染能力是它的优势,但是它的缺点是不能方便的控制它内部的元素。另外一个缺点是依赖于分辨率。

使用场景:

  • 适合图像密集密集型的游戏,适合图形经常变动,其中很多对象会被频繁的重绘。
  • 可以擅长是动画,以为他会清空画布,可以做出不错的游戏
  • 绘制出的一个标量图。我们喜欢用canvas做一些统计图表,饼状图、柱状图、曲线图。

2.3 参考文章

[1] Canvas与svg的比较 - 走看看 (zoukankan.com)