写点什么

web 技术分享| 白板 SDK 的几种图形检测算法

作者:anyRTC开发者
  • 2021 年 12 月 21 日
  • 本文字数:3979 字

    阅读完需:约 13 分钟


要回那些还给老师的数学函数。


从初中数学开始,我们就开始接触各种算法公式:三角函数、勾股定理、正弦定理……,再到高等数字的微积分、线性代数。当我成为了一名程序员,我发现我几乎用不到,难道我写的是低等代码?那我岂不是低等程序员了?


幸运的是,最近要开发一个白板 SDK,从无足无措到查阅文档,重拾物理动画、三角函数、还有向量、矩阵变换等等,于是值得深思的是,作为程序员,如何让代码更加高效,应该是我们不断思考的问题,同样是实现一个功能,不同的实现方式,直接影响到运行速度,间接影响到用户体验。好在人类所知的数学公式在编程语言上都有 API 体现,根据业务的复杂和产品需求也衍生出了许许多多的算法,噪音算法、LRU 算法等等。


那么,今天我们一起来看看白板 SDK 有哪些检查算法可以为我们的白板 SDK 赋能呢?


在开始今天内容之前,我们需要知道,与数学坐标系不同的是,w3c 中的 Y 轴坐标系是向下的,也就是说,向下才是正方向。

🎯 图形命中检测

图形命中检测是一个十分常用的功能,在很多场景我们可以看到他们的“身影”:


  • Echarts 折线图中选中一条线

  • 画板应用中,橡皮擦工具擦除某一条轨迹

  • 画板应用中,选中某个元素


常用的检测方案有:


  • 利用 CanvasRenderingContext2D 上下文对象中的 isPointInStroke 方法,判断当前坐标是否在绘制的轨迹中。

  • 利用 CanvasRenderingContext2D 上下文对象中的 getImageData 方法,判断 ImageData 中当前坐标所在像素点的 alpha 值,如果不为 0,则表示当前像素点有绘制图像。

ctx.isPointInStroke

var canvas = document.getElementById("canvas");var ctx = canvas.getContext("2d");
ctx.rect(10, 10, 100, 100);ctx.rect(50, 50, 100, 100);ctx.stroke();console.log(ctx.isPointInStroke(10, 10)); // true
复制代码


但是这个方法也存在弊端,如果有多个图形同时存在,无法获取到真实结果。


举个🌰 :同样的代码,我们稍微改造一下,当前画板有两个矩形,我们需要判断坐标 (10, 10) 是否在绘制的轨迹上,我们无法获取到真实的结果,因为我们无法保证哪里使用了 beginPath


var canvas = document.getElementById("canvas");var ctx = canvas.getContext("2d");
ctx.rect(10, 10, 100, 100);ctx.stroke();console.log(ctx.isPointInStroke(10, 10)); // truectx.beginPath();ctx.rect(50, 50, 100, 100);ctx.stroke();console.log(ctx.isPointInStroke(10, 10)); // falseconsole.log(ctx.isPointInStroke(50, 50)); // true
复制代码


所以一般的做法是:使用离屏画板,画一次图形判断一次,因此这种做法有点消耗资源。


// 绘制第一个图形进行判断var offscreenCanvas = document.createElement('canvas');const offscreenCtx = offscreenCanvas.getContext('2d');ctx.rect(10, 10, 100, 100);return offscreenCtx.isPointInStroke(10, 10); // true
// 绘制第二个图形进行判断var offscreenCanvas = document.createElement('canvas');const offscreenCtx = offscreenCanvas.getContext('2d');ctx.rect(50, 50, 100, 100);return offscreenCtx.isPointInStroke(50, 50); // true
复制代码

ImageData 像素点检测

利用 CanvasRenderingContext2D.getImageData 获取某个像素点 ImageData


var canvas = document.getElementById("canvas");var ctx = canvas.getContext("2d");
ctx.rect(10, 10, 100, 100);ctx.stroke();
// 获取坐标 10, 10 所在像素点的 rgba 值,如果 alpha 的值不为 0,则表示该像素点上有绘制图形var pixels = ctx.getImageData(10, 10, 1, 1); // [red, green, blue, alpha]console.log(pixels[3] !== 0); // true
复制代码


相对于 ctx.isPointInStroke 检测方法,这种方法更值得推荐👍,仅需要获取当前像素点,并判断 alpha 值是否为 0。

💥 图形碰撞检测

使用场景:


  • 游戏碰撞:俄罗斯方块

  • 游戏碰撞:(飞机小游戏)子弹命中


外接矩形判定法

判断条件:两个矩形左上角的坐标及所处范围。



  • 当矩形 A 在矩形 B 之前时,矩形 A 左上角的 x 坐标 + 矩形 A 的宽 < 矩形 B 左上角 x 坐标,则表示矩形 A 与矩形 B 在 x 轴方向上不会发生碰撞;同理,矩形 A 左上角的 y 坐标 + 矩形 A 的高 < 矩形 B 左上角的 y 坐标,则表示矩形 A 与矩形 B 在 y 轴上不会发生碰撞。

  • 当矩形 B 在矩形 A 之前时, 矩形 B 左上角的 x 坐标 + 矩形 B 的宽 < 矩形 A 左上角 x 坐标,则表示矩形 A 与矩形 B 在 x 轴方向上不会发生碰撞;同理,矩形 B 左上角的 y 坐标 + 矩形 B 的高 < 矩形 A 左上角的 y 坐标,则表示矩形 A 与矩形 B 在 y 轴上不会发生碰撞。


// 当以下4个条件都不满足时,两个矩形才相交function checkRect(rectA, rectB) {  return !(    rectA.x + rectA.width < rectB.x ||    rectB.x + rectB.width < rectA.x ||    rectA.y + rectA.height < rectB.y ||    rectB.y + rectB.height < rectA.y  );}
// testvar rect1 = { x: 10, y: 10, width: 100, height: 100,};var rect2 = { x: 80, y: 80, width: 100, height: 100,};
console.log(checkRect(rect1, rect2)); // true
复制代码

外接矩形判定法 2

判断条件:最大的左上角坐标在最小右下角的坐标范围内。



无论矩形在哪个方位,只需要判断 x 轴和 y 轴是否相交这两个条件。


  • X 轴:找出两个矩形的左上角 X 坐标,取最大值 startX,可以判断该图形的前后顺序。找出两个矩形右下角的 X 坐标,取最小值 endX,这是矩形相交的临界点。如果 startX <= endX ,则表示矩形 A 与矩形 B 在 X 轴上发生碰撞。

  • Y 轴:找出两个矩形的左上角 Y 坐标,取最大值 startY,可以判断该图形的前后顺序。找出两个矩形右下角的 Y 坐标,取最小值 endY,这是矩形相交的临界点。如果 startY <= endY ,则表示矩形 A 与矩形 B 在 Y 轴上发生碰撞,则表示矩形 A 与矩形 B 在 Y 轴上发生碰撞。


// 当以下4个条件都不满足时,两个矩形才相交function checkRect(rectA, rectB) {  return Math.max(rectA.x, rectB.x) <= Math.min(rectA.x + rectA.width, rectB.x + rectB.width) &&  Math.max(rectA.y, rectB.y) <= Math.min(rectA.y + rectA.height, rectB.y + rectB.height);}
// testvar rect1 = { x: 10, y: 10, width: 100, height: 100,};var rect2 = { x: 80, y: 80, width: 100, height: 100,};var rect3 = { x: 111, y: 80, width: 100, height: 100,};
console.log(checkRect(rect1, rect2)); // trueconsole.log(checkRect(rect1, rect3)); // false
复制代码

外接圆判定法

判断条件:两个圆心之间的距离小于或等于两个圆的半径之和。



  • 如果两个圆心之间的距离小于两个圆半径之和,则两个圆没有相交;

  • 如果两个圆心之间的距离大于两个圆半径之和,则两个圆没有发生碰撞。


function checkCircle(circleA, circleB) {  var dx = circleB.x - circleA.x;  var dy = circleB.y - circleA.y;  var distance = Math.sqrt(dx * dx + dy * dy);    return distance < circleA.radius + circleB.radius;}
复制代码

图形包含检测

与碰撞检测不同的是,要判断出图形包含的关系。


  • in 图形

  • 图形 in 图形

in 图形

运用场景:鼠标是否点击了框选范围内(如果是则可以进行拖拽操作)


/*** 坐标是否在矩形中** 坐标点是否在矩形的范围中:* - 坐标点的 X 坐标大于或等于矩形左上角的 X 坐标* - 坐标点的 Y 坐标大于或等于矩形左上角的 Y 坐标* - 坐标点的 X 坐标小于或等于矩形右下角的 X 坐标* - 坐标点的 Y 坐标小于或等于矩形右下角的 Y 坐标** @param point - 一个坐标点: [x 坐标, y 坐标]* @param rect - 矩形的坐标数组:[左上角的 x 坐标, 左上角的 y 坐标, 右下角的 x 坐标, 右下角的 y 坐标]* @return {boolean} - true 包含 / false 不包含*/const pointInRect = (point: [number, number], rect: [number, number, number, number]): boolean => {
const x = point[0]; const y = point[1];
const x1 = rect[0]; const y1 = rect[1]; const x2 = rect[2]; const y2 = rect[3];
return x >= x1 && y >= y1 && x <= x2 && y <= y2;}
复制代码

图形 in 图形

运用场景:使用框选工具框选出包围的所有元素


/*** 判断矩形是否包含** 图像包含关系需判断:* - 外层矩形的左上角的 x、y 小于或等于内层矩形的 x、y,表示内层矩形左上角坐标在外层矩形的矢量方向内* - 内层矩形的右下角的 x、y 小于或等于外层矩形的 x、y,表示内层矩形右下角坐标在外层矩形的矢量方向内* 以上两个条件都满足则表示包含关系成立,否则不成立。** @param outerRect - 第一个矩形的坐标数组:[左上角的 x 坐标, 左上角的 y 坐标, 右下角的 x 坐标, 右下角的 y 坐标]* @param insideRect - 第二个矩形的坐标数组:[左上角的 x 坐标, 左上角的 y 坐标, 右下角的 x 坐标, 右下角的 y 坐标]* @return {boolean} - true 包含 / false 不包含*/
const rectContainsRect = (outerRect: [number, number, number, number], insideRect: [number, number, number, number]): boolean => {
const Xa1 = outerRect[0]; const Ya1 = outerRect[1]; const Xa2 = outerRect[2]; const Ya2 = outerRect[3];
const Xb1 = insideRect[0]; const Yb1 = insideRect[1]; const Xb2 = insideRect[2]; const Yb2 = insideRect[3];
return Xa1 <= Xb1 && Ya1 <= Yb1 && Xb2 <= Xa2 && Yb2 <= Ya2;}
复制代码


当框选工具在绘制框选范围时,应该计算是否包含白板上绘制的其他元素,然后根据业务需求进行特殊展示(显示元素被选中的外包围框等等)。

参考链接



发布于: 9 小时前阅读数: 8
用户头像

实时交互,万物互联! 2020.08.10 加入

实时交互,万物互联,全球实时互动云服务商领跑者!

评论

发布
暂无评论
web技术分享| 白板SDK的几种图形检测算法