canvas拾取方案汇总

399 阅读3分钟

canvas拾取方案汇总

使用canvas绘制图形的时候,也是经常会有交互的。 再canvas中是没有js 操作 dom的那么方便的事件系统。也有一些拾取方案...

简介
当前流行的图表库的底层渲染层都是使用 Canvas 或者 SVG 。从功能上来说 SVG 的功能更全面一些,提供的接口更丰富,使用更简单,但是所有的渲染和拾取(点击获取图形)都是浏览器内置的行为,其性能跟浏览器相关;而 Canvas 是使用一种直接绘制的方式,其渲染性能和拾取性能跟用户的实现的方式密切相关,性能的优化空间非常大。本文关注 Canvas 的拾取的方案以及各种方案的优缺点,提供给用户在合适的场景下选择恰当的拾取方式。

拾取方案
由于 Canvas 不会保存绘制图形的信息,一旦绘制完成用户在浏览器中得到的是一张图片,用户在图片上点击时时不能获取对应的图形信息,所以需要缓存图形的信息,根据用户点击的位置进行判断击中了那些图形。常见的拾取方案有以下几种:

  • 使用缓存离屏渲染 Canvas 通过颜色拾取图形
  • 使用 Canvas 内置的 API 拾取图形
  • 使用几何运算拾取图形
  • 混杂上面的几种方式来拾取图形

第一种(几何坐标计算)

利用canvas的event事件中的x、y来判断, 基础的图形(正方形、圆形,直线)自然好判断,如果是多边形、不规则图形就有一些计算的成本。 效果图:

w1w.gif

案例1(圆形):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>

<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const radius = 50;
const circleX = 50;
const circleY = 220;

ctx.beginPath();
ctx.arc(circleX, circleY, radius, 0, Math.PI * 2 , true);
ctx.closePath();
ctx.fill();
ctx.stroke();


canvas.onclick = function(event) {
  const { x, y } = event;
  //勾股定理计算 c2 = a2 + b2
  const py = Math.sqrt(Math.pow(x - circleX, 2) + Math.pow(y - circleY, 2))

  if(py <= radius) {
      console.log('点击到了圆形')
  }
}
</script>

案例2(矩形)
效果图:

w2w.gif

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
  <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>
</html>

<script>
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const rectX = 50;
const rectY = 50;
const rectW = 50;
const rectH = 50;

ctx.beginPath();
ctx.fillRect(rectX, rectY, rectW, rectH)
ctx.stroke();


canvas.onclick = function(event) {
  const { x, y } = event;
  const x1 = rectX;
  const y1 = rectY;
  const x2 = rectX + rectW;
  const y2 = rectY + rectH;
  
  if(x >= x1 && x <= x2 && y >= y1 && y <= y2) {
      console.log('点击到了矩形');
  }

  console.log(x, y, 'canvas')
}


</script>

阅读d3-lasso源码 他实现套索框选是用了多边形的库, 需要必须涉及计算的话可以参考:(github.com/mikolalysen…]

第二种(自带Api isPointInPath)

isPointInPath 如果指定的点位于当前路径中,则返回 true,否则返回 false。 ⚠不支持strokeRect、fillRect。

示例一(基础用法):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas的事件监听</title>
</head>

<body>
    <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    //isPointInPath  如果指定的点位于当前路径中,则返回 true,否则返回 false
    //不支持 strokeRect
    //不支持 fillRect
    const canvas = document.querySelector('canvas')

    const ctx = canvas.getContext('2d')

   
    ctx.beginPath()
    ctx.rect(10, 5, 50, 50)
    ctx.fill()
    ctx.closePath()

    ctx.beginPath()
    ctx.rect(10, 60, 50, 50)
    ctx.stroke()
    ctx.closePath()
    



    canvas.onclick = function (e) {
        const canvasInfo = canvas.getBoundingClientRect()
        const { x, y, clientX, clientY } = e
        const some = ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
        console.log(some,'some')
    }

</script>

总结: 正常编写canvas程序的这个写法,再用api isPointInPath判断是否点击的时候,就只能判断最后一个绘制的图形。

示例二(给基本用法封装下):

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas的事件监听</title>
</head>

<body>
    <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    //isPointInPath  
    //不支持 strokeRect
    //不支持 fillRect
    const canvas = document.querySelector('canvas')

    const ctx = canvas.getContext('2d')

   function draw1() {
       ctx.beginPath()
       ctx.rect(10, 5, 50, 50)
       ctx.fill()
       ctx.closePath()
   }

   function draw2() {
        ctx.beginPath()
        ctx.rect(10, 60, 50, 50)
        ctx.stroke()
        ctx.closePath()
   }


   const drawArray = [draw1,draw2]
   drawArray.forEach(fn => fn())


    canvas.onclick = function (e) {
        ctx.clearRect(0,0,600,600)
        const canvasInfo = canvas.getBoundingClientRect()
        const { x, y, clientX, clientY } = e

        const someArray = drawArray.map(item => {
            item()
           const some = ctx.isPointInPath(e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
           return some
        })
        console.log(someArray,'someArray')
    }
</script>

总结: 这个样确实能确定点击到了那个元素,但是每次都要重新绘制,还是不太友好的。

示例3(new Path2D结合isPointInPath)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Canvas的事件监听</title>
</head>

<body>
    <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    //isPointInPath  
    //不支持 strokeRect
    //不支持 fillRect
    const canvas = document.querySelector('canvas')

    const ctx = canvas.getContext('2d')

    const path1 = new Path2D()
    path1.rect(10, 5, 50, 50)
    ctx.fill(path1)


    const path2 = new Path2D()
    path2.rect(10, 60, 50, 50)
    ctx.stroke(path2)

    const paths = [path1, path2];
    



    canvas.onclick = function (e) {
        const canvasInfo = canvas.getBoundingClientRect()
        const { x, y, clientX, clientY } = e
        const pathSomeArray = paths.map((item) => {
           return ctx.isPointInPath(item,e.clientX - canvasInfo.left, e.clientY - canvasInfo.top)
        })
        console.log(pathSomeArray,'path1123')
    }
</script>

总结: path2D 和 isPointInPath使用起来是比前面两种示例写法友好点。

离屏渲染(通过唯一颜色拾取图形)

OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象。它在窗口环境和web worker环境均有效。OffscreenCanvas构造函数。创建一个新的OffscreenCanvas对象。

效果图:

w23w.gif 用法示例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>

<body>
    <canvas width="600" height="600" style="border: 1px solid;"></canvas>
</body>

</html>
<script>

    const canvas = document.querySelector('canvas');
    const ctx = canvas.getContext('2d');

     //MDN: OffscreenCanvas提供了一个可以脱离屏幕渲染的canvas对象
    const osCanvas = new OffscreenCanvas(canvas.width, canvas.height);
    const osCtx = osCanvas.getContext('2d');

   
    ctx.beginPath()
    ctx.rect(10, 5, 50, 50)
    ctx.fill()
    ctx.closePath()

    ctx.save();
    ctx.beginPath();
    ctx.rect(10, 60, 50, 50);
    ctx.stroke();
    ctx.closePath();
    ctx.restore();

    osCtx.save();
    osCtx.beginPath();
    osCtx.fillStyle = `rgba(${1}, ${1}, ${1}, ${255})`;
    osCtx.strokeStyle = `rgba(${1}, ${1}, ${1}, ${255})`;
    osCtx.rect(10, 5, 50, 50)
    osCtx.fill()
    osCtx.closePath()
    osCtx.restore();

    osCtx.save();
    osCtx.beginPath()
    osCtx.fillStyle = `rgba(${2}, ${2}, ${2}, ${255})`;
    osCtx.strokeStyle = `rgba(${2}, ${2}, ${2}, ${255})`;
    osCtx.rect(10, 60, 50, 50)
    osCtx.stroke()
    osCtx.fill()
    osCtx.closePath()
    osCtx.restore();
    



    canvas.onclick = function (e) {
        const x = event.offsetX;
        const y = event.offsetY;
        const rgba = osCtx.getImageData((x), (y), 1, 1).data;
        console.log(rgba, 'rgba>>>>>>>>>................')
    }

</script>

总结: 如果不考虑性能来说的话,离屏渲染应该是最好的拾取方案。 性能来说的话他毕竟重复绘制了两次。

提供一个离屏渲染方便操作rgba的对象:

const helperCanvasId = {
        idPool: {},

        createOnceId() {
            return Array(3)
            .fill(0)
            .map(() => Math.ceil(Math.random() * 255))
            .concat(255)
            .join("-");
        },
        
        idToRgba(id) {
           return id.split("-");
        },

        rgbaToId(rgba) {
         return rgba.join("-");
        },

        createId() {
          let id = this.createOnceId();
          while (this.idPool[id]) {
            id = this.createOnceId(e);
          }
         return id;
        }
    };

调用helperCanvasId.createId()即可生成一个不会重复的rgba值可用来绑定离屏渲染的ctx

结束语

以上就是canvas中的拾取方案,您更倾向于那个那,可以留言一块学习交流。

  • 大家好 我是三原,多谢您的观看,我会更加努力(๑•̀ㅂ•́)و✧多多总结。
  • 每个方法都是敲出来验证过通过的,有需要可以放心复制。
  • 如果您给帮点个赞👍就更好了 谢谢您~~~~~
  • 期待您的关注