平面几何:求向量 a 到向量 b扫过的夹角

大家好,我是前端西瓜哥。

今天我们来学习如何求向量 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 地址:

codepen.io/F-star/pen/...

结尾

我是前端西瓜哥,关注我,学习更多平面几何知识。


相关阅读,

平面几何算法:求点到直线和圆的最近点

解析几何:计算两条线段的交点

几何算法:判断两条线段是否相交

图形编辑器开发:一些会用到的简单几何算法

几何算法:矩形碰撞和包含检测算法

在容器内显示图片的五种方案:contain、cover、fill、none、scale-down

本文使用 文章同步助手 同步

相关推荐
袋鼠云数栈UED团队2 分钟前
AI Coding 方法论分析:同一个需求 SpecKit 、 Superpowers、 MattpocockSkills 不同设计
前端·aigc·ai编程
掰头战士7 分钟前
Prompt Cache,如果agent全靠LLM方做隐式缓存实在不够用。
前端·llm·agent
LEE21 分钟前
前端转型全栈 01:数据建模,前端最大的盲区
前端·javascript·后端
货拉拉技术23 分钟前
个人提效,攒不成组织提效:货拉拉 AI Coding 落地实践
前端
雪芽蓝域zzs39 分钟前
第四十七节:驾驶舱大屏 ECharts 图表集成
前端·javascript·vue.js
Software攻城狮1 小时前
【React 学习方向(项目上手注意点)】
前端
林太白1 小时前
js-var和let以及const区别
前端·面试
看谷秀2 小时前
arkts-10 实战
前端·arkts
范小兵2 小时前
# DevEco CLI实战:鸿蒙App「至客」从0开发到正式上架
前端·harmonyos
半生过往2 小时前
前端工程师学习智能体开发(一)
前端·学习·状态模式