JavaScript

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > Canvas内置API性能

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

作者: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()主要依赖几何坐标的计算,这里主要针对矩形、圆形、多边形实现:

/**
 * 判断点是否在形状内
 * @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性能的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文