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

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

相关推荐
MarcoPage1 分钟前
第十九课 Vue组件中的方法
前端·javascript·vue.js
.net开发3 分钟前
WPF怎么通过RestSharp向后端发请求
前端·c#·.net·wpf
**之火24 分钟前
Web Components 是什么
前端·web components
顾菁寒25 分钟前
WEB第二次作业
前端·css·html
前端宝哥26 分钟前
10 个超赞的开发者工具,助你轻松提升效率
前端·程序员
你好龙卷风!!!27 分钟前
vue3 怎么判断数据列是否包某一列名
前端·javascript·vue.js
兔老大的胡萝卜1 小时前
threejs 数字孪生,制作3d炫酷网页
前端·3d
齐 飞2 小时前
MongoDB笔记02-MongoDB基本常用命令
前端·数据库·笔记·后端·mongodb
巧克力小猫猿2 小时前
基于ant组件库挑选框组件-封装滚动刷新的分页挑选框
前端·javascript·vue.js
FinGet3 小时前
那总结下来,react就是落后了
前端·react.js