大家好,我是前端西瓜哥。
今天我们来学习如何求向量 a 到向量 b扫过的弧度,或者也可以说是角度,转换一下就好了。
求两向量的夹角
求两向量的夹角很简单,用点积公式。
变换一下:
代码实现:
css
const getAngle = (a, b) => {
// 点积
const dot = a.x * b.x + a.y * b.y;
const d = Math.sqrt(a.x * a.x + a.y * a.y) * Math.sqrt(b.x * b.x + b.y * b.y);
const cosTheta = dot / d;
return Math.acos(cosTheta);
};
需要注意的是,这个夹角是没有方向的,为大于等于 0 小于 180 度,我们不知道其中一个向量在另一个向量的哪一次。
边缘场景
上面的代码有两个 corner case 需要处理。
(1)有至少一个向量为零向量
零向量没有方向,和其他向量没法构成夹角。参与运算时也会导致除数为零,最后会返回 NaN。
这个怎么处理?自行决定。
比如可以返回角度 0;或者返回 NaN;或者直接报错,要求使用者在使用该方法前先自己判断是否为零向量,否则不能传进来。
css
const d = Math.sqrt(a.x * a.x + a.y * a.y) * Math.sqrt(b.x * b.x + b.y * b.y);
if (d === 0) {
return null
}
(2)余弦值误差
然后一个比较难发现的,就是浮点数误差,导致角度余弦值 cosTheta 略微超出 [-1, 1]
的范围,比如 1.00000001,这个用 Math.acos
进行反余弦运算,得到的是。。。NaN。
为什么我会知道?
因为我写一个复杂算法的时候,发现在某个极限场景下拿到了 NaN,一步步 debugger 发现是这个误差问题,真的没想到还有这个坑。
修正回 [-1, 1]
范围即可:
ini
// 修正精度问题导致的 cosTheta 超出 [-1, 1] 的范围
// 导致 Math.acos(cosTheta) 的结果为 NaN
if (cosTheta > 1) {
cosTheta = 1;
} else if (cosTheta < -1) {
cosTheta = -1;
}
向量 a 到向量 b 扫过的夹角
但很多的情况下,角度是有方向的:逆时针或顺时针。
我们往往想知道的是 向量 A 沿着特定方向旋转,要旋转多少角度才能到达向量 B 的位置。
我们要求的角度在 -180 到 180 范围,负数表示沿反方向旋转多少多少度。(也可以不用负数,只能沿正方向扫过去,用 0 到 360 表示)
为了判断方向,我们需要使用叉积。叉积在图形学中经常用来判断左右或内外。
三维中两个向量 a、b 的叉积运算,会使用 a x b
表示,其结果也是一个向量 c。向量 c 会同时垂直于向量 a、b,或者可以理解为垂直于它们形成的平面)。
叉积运算出来的结果向量的方向,在右手坐标系(二维坐标中,我们习惯的 x 向右,y 向上,z 朝脸上)中,满足 右手定则,见下图:
这个二维向量也能用,叉积是一个标量,即一个数字,对应三维空间中,第三个维度 z 的值。
对于叉积 a x b
,如果结果为正值,则 b 在 a 的左边;如果结果为负值,则 b 在 a 的左边;如果结果为 0,表示他们向量相同,属于 corner case,左右随便选一个。
但是 Canvas、SVG 这些,都是左手坐标系(x 轴向右,y 轴向下,z 朝脸上),在用它们时用的是左手定则,a x b
和前面说的刚好反过来。
注意叉积不满足交换律,交换后就反向了,
回到算法。
这里假设角度的正方向为顺时针方向,则如果 a x b 为正值,则 b 在 a 的右边,不需要修正;如果 b 在 a 的左边,就要取负值,进行修正:
css
// 通过叉积判断方向,如果 b 在 a 的左边,则取负值
if (a.x * b.y - a.y * b.x < 0) {
theta = -theta;
}
完整代码
ini
/**
* 求向量 a 到向量 b 扫过的夹角
* 这里假设顺时针方向为正
*/
const getSweepAngle = (a, b) => {
// 点乘求夹角
const dot = a.x * b.x + a.y * b.y;
const d = Math.sqrt(a.x * a.x + a.y * a.y) * Math.sqrt(b.x * b.x + b.y * b.y);
// 零向量特殊处理
if (d === 0) {
return undefined;
}
let cosTheta = dot / d;
// 修正精度问题导致的 cosTheta 超出 [-1, 1] 的范围
// 导致 Math.acos(cosTheta) 的结果为 NaN
if (cosTheta > 1) {
cosTheta = 1;
} else if (cosTheta < -1) {
cosTheta = -1;
}
let theta = Math.acos(cosTheta);
// 叉积判断方向,如果 b 在 a 的左边,取反
if (a.x * b.y - a.y * b.x < 0) {
theta = -theta;
}
return theta;
};
可视化交互
demo 地址:
结尾
我是前端西瓜哥,关注我,学习更多平面几何知识。
相关阅读,
在容器内显示图片的五种方案:contain、cover、fill、none、scale-down
本文使用 文章同步助手 同步