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;
}

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

参考链接

在这里插入图片描述

posted @ 2021-12-21 11:32  anyRTC  阅读(112)  评论(0编辑  收藏  举报