Canvas如何判断点在形状内及内置API性能详解

 更新时间:2023年03月28日 08:57:49   作者:LewisFung  
这篇文章主要为大家介绍了Canvas如何判断点在形状内及内置API性能详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

背景

起因是有一个项目,需要在同一个canvas中渲染一批几何图形,当鼠标移动到其中某一个图形中,对这个形状高亮处理。基本实现方式是监听mousemove事件,回调中传入当前鼠标的位置,同时遍历所有图形,判断点是否在这个形状中,找到当前选中的元素并重新渲染canvas。

const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
canvas.addEventListener('mousemove', function(event) {
  const x = event.clientX - canvas.offsetLeft;
  const y = event.clientY - canvas.offsetTop;
  // Check each polygon to see if the mouse is inside
  for (let i = 0; i < polygons.length; i++) {
    const polygon = polygons[i];
    // Check if the mouse is inside the polygon
    if (isPointInside(polygon, x, y)) {
      console.log('Mouse is inside polygon ' + i);
      break;
    }
  }
});

当图形的量级持续上升,意味着JS逻辑执行时间同步增加,鼠标移动过快必然出现卡顿(低FPS)。

这个问题有很多优化的角度:

  • 降低鼠标事件执行的频率,即节流;
  • 分区判断,减少需要遍历的多边形数量;
  • 优化判断点是否在形状中的逻辑 isPointInside()

我初步实现的isPointInside()主要依赖几何坐标的计算,这里主要针对矩形、圆形、多边形实现:

/**
 * 判断点是否在形状内
 * @param shape
 * @param point
 * @param type
 * @returns
 */
export const isPointInside = (
  shape: IRect | ICircle | IPolygon,
  point: IPoint,
  type: EElementType,
): boolean => {
  if (!shape || !point) return false;
  switch (type) {
    case EElementType.Rect: {
      const rect = shape as IRect;
      return (
        rect.x <= point.x &&
        rect.x + rect.width >= point.x &&
        rect.y <= point.y &&
        rect.y + rect.height >= point.y
      );
    }
    case EElementType.Circle: {
      const circle = shape as ICircle;
      return (
        Math.sqrt(
          Math.pow(point.x - circle.x, 2) + Math.pow(point.y - circle.y, 2),
        ) <= circle.radius
      );
    }
    case EElementType.Polygon: {
      const polygon = shape as IPolygon;
      // 将多边形划分为若干个三角形
      for (let i = 1; i < polygon.length - 1; i++) {
        // 计算三角形的三个顶点和点 P 的叉积
        const cross1 = crossProduct(polygon[0], polygon[i], point);
        const cross2 = crossProduct(polygon[i], polygon[i + 1], point);
        const cross3 = crossProduct(polygon[i + 1], polygon[0], point);
        // 如果三个叉积的符号都相同,则说明点在三角形内部
        if (cross1 * cross2 > 0 && cross1 * cross3 > 0) {
          return true;
        }
      }
      return false;
    }
    default:
      return false;
  }
};

但当我把这个问题抛给ChatGpt时发现了另一种可能性:

const canvas = document.getElementById('my-canvas');
const ctx = canvas.getContext('2d');
canvas.addEventListener('mousemove', function(event) {
  const x = event.clientX - canvas.offsetLeft;
  const y = event.clientY - canvas.offsetTop;
  // Check each polygon to see if the mouse is inside
  for (let i = 0; i < polygons.length; i++) {
    const polygon = polygons[i];
    // Set the path and color of the polygon
    ctx.beginPath();
    ctx.moveTo(polygon.path[0].x, polygon.path[0].y);
    for (let j = 1; j < polygon.path.length; j++) {
      ctx.lineTo(polygon.path[j].x, polygon.path[j].y);
    }
    ctx.closePath();
    ctx.fillStyle = polygon.color;
    // Check if the mouse is inside the polygon
    if (ctx.isPointInPath(x, y)) {
      console.log('Mouse is inside polygon ' + i);
      break;
    }
  }
});

他推荐了Canvas内置的API Context.isPointInPath(path,x,y)判断一个点是否被一个路径包围。

MDN这样介绍:developer.mozilla.org/zh-CN/docs/…

这个API此前我并未留意,当我把自己的方法同时转达时,ChatGpt这样评价和对比两种方法:

The internal APIisPointInPath() is likely to be faster than the custom implementation of isPointInside()that you have provided, as it is a built-in method of the canvas API optimized for detecting whether a point is inside a path.

isPointInPath() is optimized for performance and is likely to be faster than your implementation, which calculates the shape of the annotation and checks if the point is inside. However, the exact performance difference depends on the complexity of the shapes and the number of annotations you have on the canvas.

Therefore, it's recommended to use isPointInPath() for performance reasons, especially if you have a large number of annotations. You can create a path for each annotation, and then check if the mouse pointer is inside any of the paths using isPointInPath()

出于性能考虑内置方法更好?为什么好?好到什么程度?

于是就有个接下来的实验。

测试案例

const canvas = document.createElement('canvas');
document.body.appendChild(canvas);
const count = 1000;
const width = 1500;
const height = 1500;
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
// Create random shapes
const shapes = [];
const createPathFromPoints = (points) => {
  const path = new Path3D();
  path.moveTo(points[0].x, points[0].y);
  for (let i = 1; i < points.length; i++) {
    path.lineTo(points[i].x, points[i].y);
  }
  path.closePath();
  return path;
};
const createCirclePathByPoint = (center, radius) => {
  const path = new Path3D();
  path.arc(center.x, center.y, radius, 0, 2 * Math.PI);
  path.closePath();
  return path;
};
for (let i = 0; i < count; i++) {
  const type = ['circle', 'rect', 'polygon'][Math.floor(Math.random() * 2)];
  let shape;
  let path;
  switch (type) {
    case 'rect': {
      shape = {
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        width: Math.random() * 30,
        height: Math.random() * 30,
      };
      const { x, y, width, height } = shape;
      path = createPathFromPoints([{x, y}, {x: x + width, y: y}, {x: x + width, y: y + height}, {x, y: y + height}]);
      break;
    }
    case 'circle':
      shape = {
        x: Math.random() * canvas.width,
        y: Math.random() * canvas.height,
        radius: Math.random() * 20,
      };
      path = createCirclePathByPoint({ x: shape.x, y: shape.y }, shape.radius);
      break;
    case 'polygon':
      shape = [
        { x: Math.random() * canvas.width, y: Math.random() * canvas.height }
      ];
      for(let i = 1; i < Math.floor(Math.random() * 10); i++) {
        shape.push({ x: shape[i-1].x + Math.random() * 20, y: shape[i-1].y + Math.random() * 20 });
      }
      path = createPathFromPoints(shape);
      break;
  }
  shapes.push({ shape, type, path });
}
function renderAllShapes(shapes, selectedIndex) {
  shapes.forEach(({ shape, type}, index) => {
    ctx.fillStyle = randomColor();
    switch (type) {
      case 'rect':
        ctx.fillRect(shape.x, shape.y, shape.width, shape.height);
        break;
      case 'circle':
        ctx.beginPath();
        ctx.arc(shape.x, shape.y, shape.radius, 0, 2 * Math.PI);
        ctx.fill();
        break;
      case 'polygon':
        ctx.beginPath();
        ctx.moveTo(shape[0].x, shape[0].y);
        for (let i = 1; i < shape.length; i++) {
          ctx.lineTo(shape[i].x, shape[i].y);
        }
        ctx.closePath();
        ctx.fill();
        break;
    }
  })
}
renderAllShapes(shapes);
let customWin = 0;
let builtinWin = 0;
canvas.addEventListener('mousemove', (e) => {
  const point = { x: e.clientX - canvas.offsetLeft, y: e.clientY - canvas.offsetTop };
  // Method 1
  const start1 = performance.now();
  const result1 = shapes.findIndex(({ shape, type }) => {
    return isPointInside(shape, point, type);
  });
  const end1 = performance.now();
  // Method 2
  const start2 = performance.now();
  const result2 = shapes.findIndex(({ path }) => {
    return ctx.isPointInPath(path, point.x, point.y);
  })
  const end2 = performance.now();
  if ((end1 - start1) < (end2 - start2)) {
    customWin++;
  } else if ((end1 - start1) > (end2 - start2)) {
    builtinWin++;
  }
  renderAllShapes(shapes);
  console.log(result1, result2);
  console.log(end1 - start1, end2 - start2);
  console.log(customWin, builtinWin);
});

上述代码canvas中随机创建了count个形状,分别使用两种方法判断鼠标hover形状,采用performance.now()毫秒级的记录执行时间。

同时执行两种方法,当count=1000时,FPS > 55正常使用,但是当count=10000时,FPS < 20,说明批量判断存在性能瓶颈。

Count自定义内置
10000.0300.150
20000.0380.243
30000.0600.310

根据控制台打印,两种方法当前hover元素的判断一致,但执行时间上,90%的情况下,自定义实现的isPointInside()优于内置APIisPointInPath()

所以,ChatGpt可以不负责任的讲结论,内置API也不一定是最优解,实践是唯一标准。

以上就是Canvas如何判断点在形状内及内置API性能详解的详细内容,更多关于Canvas内置API性能的资料请关注脚本之家其它相关文章!

相关文章

  • 学习笔记编写vue的第一个程序

    学习笔记编写vue的第一个程序

    这篇文章主要介绍了学习笔记vue的第一个程序,其中非常详细的为大家讲解了vue程序的创建步骤,有需要的朋友可以学习下,希望可以有所帮助
    2021-08-08
  • Three.js Interpolant实现动画插值

    Three.js Interpolant实现动画插值

    这篇文章主要为大家介绍了Three.js Interpolant实现动画插值示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • 停止编写 API函数原因示例分析

    停止编写 API函数原因示例分析

    这篇文章主要为大家介绍了停止编写API函数的原因及示例分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • JavaScript闭包closure详述

    JavaScript闭包closure详述

    这篇文章主要大家分享的是JavaScript闭包详述,在JavaScript部分,闭包是很重要的东西,所以我们今天就闭包的相关知识做一个总结,需要的朋友可以参考一下,希望对你有所帮助
    2021-12-12
  • TypeScript 内置高级类型编程示例

    TypeScript 内置高级类型编程示例

    这篇文章主要为大家介绍了TypeScript 内置高级类型编程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • LoadRunner调用JS加密后登录实现

    LoadRunner调用JS加密后登录实现

    这篇文章主要为大家介绍了LoadRunner调用JS加密后登录实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06
  • 微信小程序 Storage API实例详解

    微信小程序 Storage API实例详解

    这篇文章主要介绍了微信小程序 Storage API实例详解的相关资料,需要的朋友可以参考下
    2016-10-10
  • JavaScript编程通过Matlab质心算法定位学习

    JavaScript编程通过Matlab质心算法定位学习

    这篇文章主要为大家介绍了JavaScript编程中通过Matlab质心算法来定位的算法学习,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2021-10-10
  • js简单封装监听快捷键对象示例及使用

    js简单封装监听快捷键对象示例及使用

    这篇文章主要为大家介绍了js简单封装监听快捷键对象及使用示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • JavaScript CSS优雅实现网页多主题风格换肤功能详解

    JavaScript CSS优雅实现网页多主题风格换肤功能详解

    这篇文章主要为大家介绍了JavaScript CSS优雅的实现网页多主题风格换肤功能详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02

最新评论