平面几何:求向量 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

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

相关推荐
yinuo22 分钟前
Git Submodule 与 Subtree 全方位对比:使用方式与场景选择
前端
yinuo32 分钟前
深入理解与实战 Git Subtree
前端
向上的车轮1 小时前
Actix Web 不是 Nginx:解析 Rust 应用服务器与传统 Web 服务器的本质区别
前端·nginx·rust·tomcat·appche
Liudef061 小时前
基于LLM的智能数据查询与分析系统:实现思路与完整方案
前端·javascript·人工智能·easyui
潘小安1 小时前
跟着 AI 学(三)- spec-kit +claude code 从入门到出门
前端·ai编程·claude
金梦人生2 小时前
让 CLI 更友好:在 npm 包里同时支持“命令行传参”与“交互式对话传参”
前端·npm
Mintopia2 小时前
🐋 用 Docker 驯服 Next.js —— 一场前端与底层的浪漫邂逅
前端·javascript·全栈
Mintopia2 小时前
物联网数据驱动 AIGC:Web 端设备状态预测的技术实现
前端·javascript·aigc
一个W牛2 小时前
报文比对工具(xml和sop)
xml·前端·javascript
鸡吃丸子3 小时前
浏览器是如何运作的?深入解析从输入URL到页面渲染的完整过程
前端