这篇文章主要介绍“Canvas怎么判断点在形状内及内置API性能”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“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 Path4D(); 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 Path4D(); 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 | 自定义 | 内置 |
---|---|---|
1000 | 0.030 | 0.150 |
2000 | 0.038 | 0.243 |
3000 | 0.060 | 0.310 |
根据控制台打印,两种方法当前hover元素的判断一致,但执行时间上,90%的情况下,自定义实现的isPointInside()
优于内置APIisPointInPath()
。
所以,ChatGpt可以不负责任的讲结论,内置API也不一定是最优解,实践是唯一标准。
关于“Canvas怎么判断点在形状内及内置API性能”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注亿速云行业资讯频道,小编每天都会为大家更新不同的知识点。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。