web技术分享| 白板SDK的几种图形检测算法
要回那些还给老师的数学函数。
从初中数学开始,我们就开始接触各种算法公式:三角函数、勾股定理、正弦定理……,再到高等数字的微积分、线性代数。当我成为了一名程序员,我发现我几乎用不到,难道我写的是低等代码?那我岂不是低等程序员
了?
幸运的是,最近要开发一个白板 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)); // true
ctx.beginPath();
ctx.rect(50, 50, 100, 100);
ctx.stroke();
console.log(ctx.isPointInStroke(10, 10)); // false
console.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
);
}
// test
var 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);
}
// test
var 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)); // true
console.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;
}
当框选工具在绘制框选范围时,应该计算是否包含白板上绘制的其他元素,然后根据业务需求进行特殊展示(显示元素被选中的外包围框等等)。
参考链接
- CanvasRenderingContext2D.getImageData()
- 《从 0 到 1 HTML5 Canvas 动画开发》 - 第16章碰撞检测
- 《JavaScript 权威指南》- 第21章 21.4.15 命中检测