前端基础几何计算 – basic geometric tan sin cos
前言
做前端,有时候会遇到不懂 responsive 的设计师。她会用 Figma 设计出一些特别的方案,比如:用二次贝塞尔曲线 + 箭头,从一张图里的 object 指向外面的一个 div。

这类设计,如果不考虑 responsive,其实非常简单,直接放一张 SVG 就可以了;但如果要兼顾 responsive,就困难多了。
通常需要计算坐标,然后动态生成 SVG 才能解决。
我在很多年前写过一篇 《crop image 需要的基础知识》,当时遇到的设计需求是做一个 crop image 功能,里面就涉及到了一些基础的几何计算,比如 tan sin cos 等等。
没想到时隔多年又给我遇上了。
可这么多年,那些知识早就忘光光了...😔
本篇作为一个复习,重新整理一下前端需要掌握的基础几何计算。
直角三角形 right triangle の 几何计算

左边是 triangle(三角形),右边是 right triangle(直角三角形)。
right triangle 最大特色是:它一定有一个 90° 角。

也正因为这个特性,它有一些固定的规律。只要我们掌握其中一部分信息,结合这些规律,就可以推算出完整的信息。
求 hypotenuse 斜边 – hypot 函数
斜边 hypotenuse(简称 hypot)指的是直角 90° 对面那条线

已知 right triangle 的长(A → B 简称 ab)是 4px,高(A → C 简称 ac)是 3px。
求斜边 bc 的长度。
它的几何公式是
hypot = √(a² + b²)
术语叫勾股定理 (Pythagorean Theorem)
我们一个一个看:
-
√ 是 square root,平方根。
-
a 指的是图中 ab 或 ac 任意一条线的长度,可以放 3px 也可以放 4px。
-
² 是 squared,平方。
-
b 指的是图中 ab 或 ac 任意一条线的长度,如果之前 a 选了 ab 4px,那 b 就一定是剩下那一条 ac 3px。
Tips:√ 是 alt + 251 或微软输入法 dui,² 是 alt + 0178 或微软输入法 pingfang。
把参数放入到公式里,长这样:hypot = √(3² + 4²) 计算结果是 5。
用 JavaScript 写法是这样:
-
√ 是
Math.sqrt(25) -
² 是
3 ** 2或者Math.pow(3, 2) -
完整公式长这样
const hypot = Math.sqrt(3 ** 2 + 4 ** 2); -
Tips:JS 有 built-in 算 hypot 的函数可用:
const hypot = Math.hypot(3, 4);
用 CSS 写法是这样:--hypot: hypot(3px, 4px);
求长或高
反向,如果我们有 hypot 斜边和另外其中一边(长或高),我们也可以用相同公式做推算。
hypot = √(a² + b²) 算式:√(3² + 4²) → √(9 + 16) → √25 = 5
a = √(hypot² - b²) 算式:√(5² - 4²) → √(25 - 16) → √9 = 3
b = √(hypot² - a²) 算式:√(5² - 3²) → √(25 - 9) → √16 = 4
求角度 ∠° by atan 函数
Tips:∠ 是微软输入法 jiao,° 是 alt + 0176 或微软输入法 du。
除了求线条的长度之外,求各个角度也是日常所需。

∠acb(左上角)是多少度?
∠abc(右下角)是多少度?
right triangle 的规则是:三个角度相加一定是 180°,直角一定是占 90°,那剩下的 90° 就分给另外两个角。
如果 right triangle 的长度和高度一致,那就平分 45°,若是像上图那样不一致(一边 3px、一边 4px)则双边角度不同,需要用公式计算。
它的几何公式是:
tan(θ) = opposite / adjacent
tan 全名是 Tangent,它是一个数学函数,我们不用懂原理,背起来用就可以了。
它接受一个参数 theta θ,单位是度 °。(Tips:θ 是微软输入法 theta)
比如 tan(30°) = 0.5773...,用计算机就能按出来了

opposite(对边)、adjacent(邻边)、hypotenuse(斜边)指的是 right triangle 的三边线。
hypot 我们上面讲解过了,直角 90° 的对面就是 hypot。
opposite 和 adjacent 没有固定指向哪一边,它是依据我们要计算哪个角度而决定的。
看例子理解:

我们要计算出 ∠abc 的度。
此时,opposite(对边)指的是 ∠abc 的对面,也就是 ac 3px。
adjacent(邻边)则是 ab 4px。(hypot 斜边则是固定的 bc 5px)
依据公式 tan(θ) = opposite / adjacent
我们要求的是里面那个 θ,因此需要把公式改成 θ = atan(opposite / adjacent)。
atan 全名是 Arctangent,也是数学函数,照用就是了。
计算机里这么按

tan⁻¹ 就是 atan。
算式:∠abc = atan(3 / 4) → atan(0.75) = 36.869897645844021296855612559093...°
小心小数坑:
36.8698... 是无限小数(术语叫无理数)就跟圆周率 π 一样。
上面这个结果是用 windows calculator 算的,如果用 JavaScript 算,只能得到 36.86989764584402,因为 JS 算得快,但不精准。
这种无限小数很容易造成小数丢失问题,因为电脑的储存是有限的,它存到一个量就停了,后续的也就丢失了。
一旦丢失了就无法再还原,计算就可能出现微差,这时就要特别注意了(下面会有例子)。
好,我们继续算另一边 ∠acb 的度。

最简单的方法是:直接拿 90° - ∠abc(36.87...°)= 53.13...°。
因为 right triangle 的规则 ∠abc + ∠ acb 一定是 90°,我们已算出 ∠abc,拿它来扣就可以得出 ∠acb 了。
当然,想用公式算也可以 θ = atan(opposite / adjacent)。
此时,opposite 变成了 ab 4px,adjacent 变成了 ac 3px,因为角度换了。
算式:∠acb = atan(4 / 3) → atan(1.333...) = 53.13010235415598...°

JavaScript atan
atan 用 JavaScript 写是这样 const radian = Math.atan(opposite / adjacent);
注意,我的 variable 命名是 radian 而不是 degree。
因为,JavaScript atan 和一般 calculator atan 返回的单位不同。
calculator 返回的是 degree 角度,JS 返回的是 radian 弧度(注:radian 和 radius 不同哦,radius 是半径)。
degree 和 radian 是不同单位,但可以互换。
互换公式是
radian = degree * π / 180
degree = radian * 180 / π
π 是 Pi 圆周率 3.1416...(alt 227 或微软输入法 pai)
用 JavaScript 写长这样:
export function degreeToRadian(degree: number): number {
return degree * Math.PI / 180;
}
export function radianToDegree(radian: number): number {
return radian * 180 / Math.PI;
}
算式
const opposite = 4;
const adjacent = 3;
const acbDegree = radianToDegree(Math.atan(opposite / adjacent)); // 53.13010235415598
小心小数坑:
转换是有可能发生小数丢失问题的哦!比如
const radian1 = Math.atan(4 / 3); // 0.9272952180016122
const degree = radianToDegree(radian1); // 53.13010235415598
const radian2 = degreeToRadian(degree); // 0.9272952180016123
一去一回,radian 结尾从 22 变成了 23。
虽然绝大部分的场景,这些微差都可以忽略不计,但我们始终要保持警惕,避免浪费时间 debug。
CSS atan
CSS 用的是 degree,不是 radian,无需转换。
--opposite: 4;
--adjacent: 3;
--acb-degree: atan(var(--opposite) / var(--adjacent)); /* 53.1301deg */
求 opposite & adjacent by sin cos 函数

已知,hypot 的长度是 5。
∠acb 是 53.13...°
求,opposite 和 adjacent 的长度。
首先,我们先尝试用 hypot + tan 函数去解这一题。(注:这不是 right way,不要真的去用)
公式
hypot = √(a² + b²)
tan(θ) = opposite / adjacent
算式
√(opposite² + adjacent²) = 5
→ opposite² + adjacent² = 5²
→ opposite² = 5² - adjacent²
→ opposite = √(5² - adjacent²)
opposite 还无法解开,把它带到另一个算式
√(5² - adjacent²) / adjacent = tan(53.13°)
(5² - adjacent²) / adjacent² = tan(53.13°)²
5² - adjacent² = tan(53.13°)² * adjacent²
5² = (tan(53.13°)² * adjacent²) + adjacent²
5² = (tan(53.13°)² + 1) * adjacent²
adjacent² = 5² / (tan(53.13°)² + 1)
adjacent = √(5² / (tan(53.130102354155978703144387440907°)² + 1))
计算结果

有了 adjacent 再倒回去算 opposite
opposite = √(5² - 3²) = 4
虽然算式复杂了一些,但最后还是解出来了。
我们用 JavaScript 算一遍看看
const adjacent = Math.sqrt(5 ** 2 / (Math.pow(Math.tan(degreeToRadian(53.13010235415598)) , 2) + 1)); // 2.9999999999999996
答案不是 3,而是 2.99...,有微差🤔。
这个微差来自 degree to radian 的转换。
53.13010235415598° 来自 atan(4 / 3),而它原本的 radian 是 0.9272952180016122。
const radian = Math.atan(4 / 3); // 0.9272952180016122
我们不要转成 degree,试试直接用 radian
const adjacent = Math.sqrt(5 ** 2 / (Math.pow(Math.tan(0.9272952180016122) , 2) + 1)); // 3
这样就准了。(因此,一定要小心小数坑...)
为什么说上面这个算法不是 right way?
第一,它算法复杂,第二,它有小数问题。
有一套公式既简单,又没有小数问题:
cos(θ) = adjacent / hypot
sin(θ) = opposite / hypot
cos 全名是 Cosine,它和 tan 一样,也是数学函数,我们不需要懂原理,背起来用就是了。
sin 全名是 Sine,一样,背起来。
算式
adjacent = cos(53.13010235415598°) * 5 = 3
opposite = sin(53.13010235415598°) * 5 = 4
JS 写法
const adjacent = Math.cos(degreeToRadian(53.13010235415598)) * 5; // 3
const opposite = Math.sin(degreeToRadian(53.13010235415598)) * 5; // 4
CSS 写法
--degree: 53.13010235415598deg;
--radius: 5px;
--adjacent: calc(cos(var(--degree)) * var(--radius)); /* 3px */
--opposite: calc(sin(var(--degree)) * var(--radius)); /* 4px */
总结
关键公式:
-
算斜边 hypotenuse
hypot = √(a² + b²)
- 算角度
tan(θ) = opposite / adjacent
- 算对边 opposite 和 邻边 adjacent
cos(θ) = adjacent / hypot
sin(θ) = opposite / hypot
几何图 の x, y, θ 计算
上一 part 我们聚焦的是 right triangle,计算的是各个边长,和各个角度。
虽然我把 right triangle 画在几何图中,但却没有纳入 x, y 坐标的概念。
而这一 part 我们聚焦在几何图中 x, y 的常见计算。(注:这和 right triangle 没有直接关系(只有间接),别把它们混在一下理解)
几何图基础 – 坐标 & 角度
先讲一下几何图的基本规则:

Tips:画几何图的在线工具 – geometry
A、B、C、D 分别为 4 个点(point)。
每个点坐落在几何图上不同的位置,位置以坐标 x、y 来表示。
水平线(横向 horizontal)是 x 轴,负责 x 坐标。
垂直线(竖向 vertical)是 y 轴,负责 y 坐标。
我们把图切成 4 个部分来看(中间十字架切割):左上,右上,左下,右下。
左边 x 坐标是 negative(如 -B.x、-C.x);右边 x 坐标是 positve(如 A.x、D.x)。
上面 y 坐标是 positive(如 A.y、B.y);下面 y 坐标是 negative(如 -C.y、-D.y)。
注:y 轴的正负方向和 CSS position、translate 是相反得哦,别搞混了。
除了坐标 x, y,另一个重点是角度。
每一个点与中心点 0, 0 对接连成一条线,共四条,每一条都有各自的角度(A°、B°、C°、D°)。
角度的规则:
上面是 positive(如 A°、B°),下面是 negative(如 -C°、-D°)。
不管是上面还是下面,都是从右边 x 轴那一条线算起。
上面是顺时针,下面则是逆时针。

上面负责 1 到 179°
下面负责 -1 到 -179°
它没有超过 180° 的。当超过 180° 就改用 negative 来表示。
用 x, y 计算出角度 by atan2
好,看回这张图

已知 4 个点分别的坐标:A(3, 3)、B(-2, 2)、C(-3, -2)、D(5, -2)。
求,它们个别的角度:A°、B°、C°、D°。
采用几何公式:
θ = atan2(y, x)
注意!它和上一 part 我们讲解过的 atan 不同哦。
θ = atan(opposite / adjacent) 用在 right triangle 里求角度。
atan 接受一个参数,返回一个角度。
atan2 全名是 2-argument arctangent。
它用在几何图,接受两个参数,坐标 y 和 坐标 x,返回一个角度。
atan 和 atan2 或许底层原理有一些共通点,但在上层,最好把它们分开看待。
atan 用于 right triangle 内,它和 opposite、adjacent 打交道。
atan2 用于几何图,它和 x, y 打交道。
算式:
A° = atan2(3, 3) = 45°
B° = atan2(2, -2) = 135°
C° = atan2(-2, -3) = -146.30993247402023...°
D° = atan2(-2, 5) = -21.80140948635181...°

calculator 一般不会有 atan2 函数,我们得用 JavaScript:
const aPoint = { x: 3, y: 3 }
const aDegree = radianToDegree(Math.atan2(aPoint.y, aPoint.x)); // 45
CSS 的写法:
--a-x: 3;
--a-y: 3;
--a-degree: atan2(var(--a-y), var(--a-x)); /* 45deg */
用角度和 radius 计算出 x, y by sin, cos

已知,角度是 53.13010235415598°,radius(半径)的长度是 5(从坐标 0, 0 到 A point)。
求,A 的坐标 x, y。
把它想象成一个 right triangle

radius 就是 hypot 斜边
角度就是 ∠cba
A 的坐标 x 就是 adjacent 邻边长度
A 的坐标 y 就是 opposite 对边长度
因此,我们可以采用和 right triangle 相同的公式:
cos(θ) = adjacent / hypot
sin(θ) = opposite / hypot
命名稍作修改
cos(θ) = x / radius
sin(θ) = y / radius
我们要求的是 x, y 坐标,公式调整一下
x = cos(θ) * radius
y = sin(θ) * radius
算式:
x = cos(53.13010235415598°) * 5 = 3
y = sin(53.13010235415598°) * 5 = 4
JavaScript
const degree = 53.13010235415598;
const radius = 5;
const x = Math.cos(degreeToRadian(degree)) * radius; // 3
const y = Math.sin(degreeToRadian(degree)) * radius; // 4
CSS
--degree: 53.13010235415598deg;
--radius: 5px;
--x: calc(cos(var(--degree)) * var(--radius)); /* 3px */
--y: calc(sin(var(--degree)) * var(--radius)); /* 4px */
sin cos 处理 negative x, y 的疑虑
sin cos 用于 right triangle 我们很放心,因为所有参数和计算结果都只会是 positive。
可用于几何图呢?
坐标 x, y 有可能是 negative,角度也有可能是 negative,我们这样直接套用安全吗?
放心,是安全的,因为 sin cos 有兼顾 negative。
cos(45°) 和 cos(-45°) 计算结果是一样的,这确保了右边的 x 一定是 positive。
cos(>90°) 返回的是 negative,这确保了左边的 x 一定是 negative(因为左边会超过 90°)。
用角度和 x 算出 radius 和 y

已知,角度是 53.13010235415598°,x 坐标 3。
求,y 坐标和 radius。
其实和上一题是一样的,只是信息掉转了而已。
我们用相同的公式,放入不同的参数就可以算出结果了。
还是这套公式:
cos(θ) = x / radius
sin(θ) = y / radius
稍微调动一下
radius = x / cos(θ)
算式:
radius = 3 / cos(53.13010235415598°) = 5
y = sin(53.13010235415598°) * 5 = 4(拿算好的 radius 放进去就可以了)
总结
关键公式:
-
算角度
θ = atan2(y, x)
-
算 x, y, radius
cos(θ) = x / radius
sin(θ) = y / radius
有 x, y,可以算出角度 θ。
有角度 θ 和 radius,可以算出 x, y。
有角度 θ 和 x 或 y,可以算出 radius 和 x 或 y。
注意:有 radius 和 x 或 y,无法算出唯一角度 θ。
这个公式 cos(θ) = x / radius 表面上看,若我们有 x 和 radius,理应可以推算出 θ。
但其实不然,因为 cos(45°) === cos(-45°)。
公式算出的结果一定是 positive,但角度 θ 其实也可能是 negative。
几何计算 の 点、线、长度、双线交点,圆圈,线圈交点
这一 part,我们继续讲解几何中常被使用、与线条、圆圈相关的公式。
点
这是一个点(point)

我们上面有讲解过点了,这里不再赘述。
它也没有什么特别的,就是一个坐标 x, y 而已。
A point 的坐标是 x: 1, y: 1
这是另一个点

B point 坐标是 3, 1
线
把两个点连起来,就形成了一条线。

一条线,我们会在意:
-
它是横线(—)吗?
-
它是竖线(|)吗?
-
它是斜线吗?
是正斜(/)往上,还是负斜(\)往下 -
它有多斜呢?
横线、竖线、正斜线、负斜线、斜度的判断手法
我们可以透过一些手法来判断它们:
-
判断它是不是一条线
const aPoint = { x: 1, y: 1 }; const bPoint = { x: 3, y: 1 } const sameCoordinate = aPoint.x === bPoint.x && aPoint.y === bPoint.y; // false如果两个点的坐标 x, y 完全一致,那它不是一条线,它是两个重叠的点。
-
判断是横线
const isHorizontalLine = aPoint.x === bPoint.x;如果两个点的 x 坐标相同,那就是横线。
-
判断是竖线
const isVerticalLine = aPoint.y === bPoint.y;如果两个点的 y 坐标相同,那就是竖线。
-
判断是正斜线(/)或负斜线(\)
不是横线、不是竖线、那就是斜线。
斜线还分正斜(/)和负斜(\)。
如果 x, y 的发展方向是一致的,那就是正斜线(/)。意思是:x 变大,y 也变大;或者 x 变小,y 也变小。
反之,如果 x, y 的发展方向不一致,那就是负斜线(\)。
意思是:x 变大,要却变小;或者 x 变小,y 却变大。
![image]()
-
计算斜度
一条线有多斜,得看它的角度
![image]()
算角度的公式是
θ = atan2(y, x)
我们需要先把 A point 作为 B point 的中心点。
因为这个公式是以 0, 0 作为中心点,也就是图中我额外用红笔画上去的十字架。
B point 目前的坐标是 3, 3,这是以 0, 0 为中心,如果换成以 A point 为中心,它的坐标会变成多少?
换算公式是
fromPoint + (fromBasePoint - toBasePoint)套用在上图的例子
fromPoint 是 B(3, 3)
fromBasePoint 原本的中心点是(0, 0)
toBasePoint 是 A(2, 1)
x = 3 + (0 - 2) = 1y = 3 + (0 - 1) = 2![image]()
Tips: JavaSript changeBasePoint 函数
function changeBasePoint(fromPoint:Coordinate, fromBasePoint: Coordinate, toBasePoint: Coordinate): Coordinate; function changeBasePoint(fromPoint:number, fromBasePoint: number, toBasePoint: number): number; function changeBasePoint(fromPoint:Coordinate | number, fromBasePoint: Coordinate | number, toBasePoint: Coordinate | number): Coordinate | number { if (typeof fromPoint === 'number') { return internalChangeBasePoint(fromPoint, fromBasePoint as number, toBasePoint as number); } else { return { x: internalChangeBasePoint(fromPoint.x, (fromBasePoint as Coordinate).x, (toBasePoint as Coordinate).x), y: internalChangeBasePoint(fromPoint.y, (fromBasePoint as Coordinate).y, (toBasePoint as Coordinate).y) } } function internalChangeBasePoint(fromPoint:number, fromBasePoint: number, toBasePoint: number) : number { return fromPoint + (fromBasePoint - toBasePoint); } }好,这个时候就可以套用算角度公式了
θ = atan2(y, x) → atan2(2, 1) = 63.43494882292201...°这条线的斜度是 63.43...°
需要特别注意一点,这个公式计算出来的角度可能会超过 90°,也可能会是 negative。
如果我们在意的只是斜度,那可以做一个 math abs 去掉 negative,然后再除于 90 取余数,这样就不怕超过 90° 了
Tips:当角度是 1 到 89° 或 -91 到 -179° 代表它是正斜线(/)往上;当角度是 91 到 179° 或 -1 到 -89° 代表它是负斜线(\)往下。
斜率公式 (Slope Formula)
上面我们用了好几种不同的方式来判断线条,有点繁琐。
斜率公式可以替我们简化(虽然它无法覆盖所有场景),我们来试一试。
m = (y1 - y0) / (x1 - x0)
0 代表 A point,1 代表 B point。
算式
m = (b.y - a.y) / (b.x - a.x)
→ (3 - 1) / (3 - 2)
→ 2 / 1
= 2
2 代表什么呢?
m > 0 代表它是一条正斜线(/)
m < 0 代表它是一条负斜线(\)
m = 0 代表它是一条横线(—)
m = error 代表它是一条竖线(|)
为什么它会 error?因为它的公式中 m = (b.y - a.y) / (b.x - a.x) 有一个除法。
当竖线时,b.x - a.x 会得到 0,任何数除于 0 都会 error。
这也是程序员不喜欢用斜率公式的原因之一,需要提前过滤掉竖线情况,否则程序会报错。
m 是角度吗?
不,m 不是角度,而且无法换算成角度,但它和角度有一点点关系。

我们把线条看作是一个 right triangle。
求 ∠cab,公式是 θ = atan(opposite / adjacent)
opposite 是 cb 长度,可以用 b.y - a.y 获得。
adjacent 是 ca 长度,可以用 b.x - a.x 获得。
因此,算式变成 θ = atan((b.y - a.y) / (b.x - a.x))。
注意看,atan 里的 (b.y - a.y) / (b.x - a.x),这不就刚巧是 m 算式吗 m = (b.y - a.y) / (b.x - a.x)
因此,算式变成 θ = atan(m) → atan(2) = 63.43494882292201...°。
虽然 m → θ 转换结果是正确的,但这仅仅是因为这题没有涉及 negative,一旦 x, y 涉及 negative,计算出来的角度就不准了。
这也是为什么算角度一定得使用 atan2 而不能只靠 atan。
这也是程序员不喜欢用斜率公式的原因之二。
向量 vector 判断法
斜率公式有致命的两个缺陷,因此大家更喜欢用向量判断法。
它的公式是
dx = x1 - x0
dy = y1 - y0
dx 的 d stand for delta 或 different,简单说就是 before after 的变化差记入。
和斜率公式 m 最大的不同是,m 会拿 dy 除于 dx 留下一个数,而 vector 没有除法,它保留了两个数。
那 vertor 要如何判断出:横线、竖线、正斜线、负斜线呢?
-
判断它是不是一条线
如果 dx 和 dy 都是 0,代表它不是线,而是两个重叠的点。
-
判断是横线
如果 dy 是 0,代表它是一条横线。
-
判断是竖线
如果 dx 是 0,代表它是一条竖线。
-
判断是正斜线(/)或负斜线(\)
我们上面有提到,正斜线的特色是线的发展是一致的:x 变大,y 也变大;x 变小,y 也变小。
负斜线的特色是线的发展是不一致的:x 变大,y 就变小;x 变小,y 就变大。
如果 delta (dx or dy) 是 positive,那就是变大。
如果 delta (dx or dy) 是 negative,那就是变小。
正斜的要求是:dx dy 一起是 positive 或者一起是 negative。
负斜的要求是:dx 和 dy 一个是 positive 另一个必须是 negative。
利用负负得正原理,我们可以用这个算式做判断:
dx * dy > 0代表是正斜,因为 positive 乘于 positive 或 negative 乘于 negative 都会得到 positive,这代表 dx dy 发展方向一致。反之,
dx * dy < 0代表是负斜,因为 positive 乘于 negative 结果会是 negative,这代表 dx dy 发展方向不一致。 -
计算斜度
斜度的算法是θ = atan2(dy, dx)其原理和我们上一 part 自己推算的大同小异。
结论:dx dy 包含了许多信息,甚至连角度也可以推算出来,因此它非常好用。下面我们还会在许多公式中看见它的身影。
计算线的长度
如果是一条横线,就拿两个点的 x 坐标相减,然后用 math abs(absolute value)把结果强转成 positive。
因为我们要计算的是长度,所以不需要考虑是 a − b 还是反过来 b − a,也不需要受到 x 坐标可能是 negative 的影响。
总之,a、b 相减,再用 math abs 强转成 positive 就对了。
如果是一条竖线,手法一样,只是把 x 坐标换成 y 坐标相减。
如果是一条斜线呢?

首先,把它想象成一个 right triangle

C 和 A 在同一个水平线,因此 C.x = A.x 都是 4。
C 和 B 在同一个垂直线,因此 C.y = A.y 都是 1。
C 代表 right triangle 的直角,它的对面就是 hypot 斜边,也就是我们要算出的 A → B 线长度。
第一步:算出 ac 横线的长度,Math.abc(1 - 4) = 3。
第二步:算出 bc 竖线的长度,Math.abs(5 - 1) = 4。
第三步:用 hypot 公式 √(a² + b²) → √(3² + 4²) → √(9 + 16) → √25 = 5
计算结果:A → B 斜线的长度是 5
直线的参数方程
这是一条线

1. 求,ab 中心点的坐标。
2. 想象,这条线继续延申,一直到坐标 y 10,请问这时的 x 会是多少?
这两题,我们当然可以用上面学过的公式去算,但这里要教一个很厉害的直线公式:
P(t) = P0 + t * v
这个公式叫 -- 直线的参数方程
P stand for point
t stand for time
v stand for vector
P0 的 0 zero 意思是起点
从命名上,我们很难看出它究竟是啥,得透过例子理解:
有两个点:A(2,1)、B(4,5)如上图
P0 指的是起点 A。
v 指的是 delta x, y,也就是 dx = 4 - 2 = 2,dy = 5 - 1 = 4。
接着把公式分别 apply 给 x 和 y
x = a.x + t * dx → 2 + t * 2
y = a.y + t * dy → 1 + t * 4
x 和 t 是什么呢?
x 是我们想计算出来的新坐标。
t stand for time,但在这里,把它看作是一个 "进度 或 ratio" 会更容易理解。
t = 0 代表没有进度
x = 2 + 0 * 2 = 2
y = 1 + 0 * 4 = 1
坐标(2, 1)刚巧就是起点 A point,这就是没有 "进度" 的意思。
t = 1
x = 2 + 1 * 2 = 4
y = 1 + 1 * 4 = 5
坐标(4, 5)刚巧就是终点 B point,1 代表满进度。
解题1:算两点的中心
回到我们刚才的题1:求,ab 中心点的坐标。
中心点就是进度 0.5,所以 t = 0.5。
x = 2 + 0.5 * 2 = 3
y = 1 + 0.5 * 4 = 3
ab 中心点坐标是(3, 3)
这就是直线参数方程的玩法。
解题2:延申线,给定 y,求 x
继续解题2:想象,这条线继续延申,一直到坐标 y 10,请问这时的 x 会是多少?
相同的公式,但已知是 y,那就先求 t。
y = 1 + t * 4
→ 10 = 1 + t * 4
→ 10 - 1 = t * 4
→ 9 / 4 = t
→ t = 2.25
2.25 是什么意思?
0 是起点,1 是终点,2 代表线继续延申到多一倍的长度,2.25 就是多 2.25 倍的长度。
经过 2.25 倍之后,此时终点坐标 x 就是我们要的。
x = 2 + 2.25 * 2 = 6.5

小心坑:永不触及的 y
一条斜线,无线延申,无论给定的 y 是多少,它都能算出 x。
但,如果它是一条横线就不同了。

假设这条线一直延申,当 y = 2 时,它的 x 会是多少?
如果我们傻傻去跑公式
y = a.y + t * dy → 1 + t * 0
2 = 1 + t * 0
2 - 1 = t * 0
t = 1 / 0
除于零,直接炸了!
所以,当遇到横线或竖线时,我们需要提前做判断,确保这个 y 或 x 坐标是延申线可触及的。
计算双线的交点
交点(intersection point)指的是两条线交叉的那一个点坐标。

注:当然,并不是所有的线都会有交点,比如两条平行线就不可能有交点。但不管,我们先不思考这些情况。
要算出交点的坐标 x, y 我们同样可以用直线参数方程。
P(t) = P0 + t * v
ab 简称 a line,cd 简称 c line。
先算出它们的 vector -- delta x 和 y。
adx = b.x - a.x → 4 - 1 = 3
ady = b.y - a.y → 4 - 1 = 3
cdx = d.x - c.x → 3 - 4 = -1
cdy = d.y - d.y → 4 - 1 = 3
接着直线算式
ax = a.x + at * adx → 1 + at * 3
ay = a.y + at * ady → 1 + at * 3
cx = c.x + ct * cdx → 4 + ct * -1
cy = c.y + ct * cdy → 1 + ct * 3
交点代表什么?
代表两条线上的某一个点,有相同的坐标 x, y。
然后,我们要求的是,两条线个别的 "time / 进度 / ratio" 要多少能到达这个交点坐标。
换句话说,当 ax = cx 和 ay = cy 时,at 和 ct 是多少?
ax = cx
→ 1 + at * 3 = 4 + ct * -1
→ at * 3 = 4 - 1 - ct
→ at = (3 - ct) / 3
解不完,先把 at 带去另一个公式继续解
ay = cy
→ 1 + at * 3 = 1 + ct * 3
→ 1 - 1 + 3at = 3ct
→ 3at = 3ct
→ 3 * ((3 - ct) / 3) = 3ct
→ 3 - ct = 3ct
→ 3 = 3ct + ct
→ 3 = 4ct
→ ct = 3 / 4
→ ct = 0.75
把 ct 带回上一个公式继续解
→ at = (3 - 0.75) / 3
→ at = 0.75
计算结果 at = 0.75,ct = 0.75。
这里有一个知识点要注意:
at 和 ct 有可能会 > 1 或 < 0。
> 1 或 < 0 代表它是延申线才会有交点,线内并没有交点。
这时就要看业务需求了,如果不想包含延申线,那就判断当 > 1 或 < 0 时,等同于没有交点。
好,最后拿其中一个直线公式算出交点 x, y:
ax = 1 + at * 3
→ ax = 1 + 0.75 * 3
→ ax = 3.25
ay = 1 + at * 3
→ ay = 1 + 0.75 * 3
→ ay = 3.25

判断无交点
两条平行线是不会有交点的,如果我们依然用公式计算,很可能会算到除于零,然后 error。
正确的做法是在计算公式前,先做一个判断,如果发现是平行线,那就直接判定没有交点。
要判断两条线有没有交点,可以通过这个公式
adx * cdy - cdx * ady
cdx 是第二条线 C line 的 delta x
adx 是第一条线 A line 的 delta x
算式:3 * 3 - -1 * 3 = 12。
如果这个数是 0 那就表示没有交点。
测试一题看看:
a line = 1,1 → 2,1
c line = 1,2 → 2,2
adx = 2 - 1 = 1
ady = 1 - 1 = 0
cdx = 2 - 1 = 1
cdy = 2 - 2 = 0
cdx * ady - cdy * adx
→ 1 * 0 - 0 * 1
→ 0 - 0
→ 0
零代表没有交点。
判断无交点公式 の 揭秘
这个公式的原理是啥?
cdx * ady - cdy * adx
其实它就是直线参数方程解交点算式的一部分。
上一 part 我在解算式时,一边解,一边带入参数值(为了好理解),所以看不出来。
这里,我们用纯变量的方式再解一次,这样就能一目了然了。
相同题目
a line = 1,1 → 4,4(a point to b point)
c line = 4,1 → 3,4(c point to d point)
直线参数方程
P(t) = P0 + t * v
先算出各自的 vector -- delta x, y
adx = b.x - a.x
ady = b.y - a.y
cdx = d.x - c.x
cdy = d.y - c.y
套入直线参数方程
ax = a.x + at * adx
ay = a.y + at * ady
cx = c.x + ct * cdx
cy = c.y + ct * cdy
交点
ax = cx
→ a.x + at * adx = c.x + ct * cdx 下一步是解出 at
→ at * adx = c.x - a.x + ct * cdx
→ at = (c.x - a.x + ct * cdx) / adx
ay = cy
→ a.y + at * ady = c.y + ct * cdy 下一步是把 ct 挪去左边
→ a.y - c.y + at * ady = ct * cdy
→ ct = (a.y - c.y + at * ady) / cdy 下一步是带入 at
→ ct = (a.y - c.y + ((c.x - a.x + ct * cdx) / adx) * ady) / cdy 下一步是左右乘于 adx,为了抵消中间那个除于 adx
→ ct * adx = ((a.y - c.y) * adx + (c.x - a.x + ct * cdx) * ady) / cdy 下一步是把 ct 从括弧里解出,然后挪去左边
→ ct * adx = ((a.y - c.y) * adx + (c.x - a.x) * ady + ct * cdx * ady) / cdy
→ ct * adx * cdy = (a.y - c.y) * adx + (c.x - a.x) * ady + ct * cdx * ady
→ ct * adx * cdy - ct * cdx * ady = (a.y - c.y) * adx + (c.x - a.x) * ady 下一步是抽出来 ct
→ ct * (adx * cdy - cdx * ady) = (a.y - c.y) * adx + (c.x - a.x) * ady
→ ct = ((a.y - c.y) * adx + (c.x - a.x) * ady) / (adx * cdy - cdx * ady)

注:这个公式和常见教科书的公式顺序加减有一点出路,但不要紧,算出来结果是一样的。
看到吗,它最后有一个除法,如果除于 0 就会 error。
而这个分母,就是判断有没有交点的公式 adx * cdy - cdx * ady。
题外话,把计算好的 ct 带回 at 算式 at = (c.x - a.x + ct * cdx) / adx 也可能发生除于 0 的问题。
因为只要 adx 是 0 它就出问题了。
因此,最好不要把 ct 带回去解,而是反过来重新解一遍,但这一次是先解出 ct 然后带入到解 at。
最终公式会是 at = ((c.y - a.y) * cdx + (a.x - c.x) * cdy) / (adx * cdy - cdx * ady)

圈圈圆圆圈圈
一个坐标(x, y)加上一个 radius(半径)长度,就可以画出一个圆圈。

圆圈的中心点坐标是(3, 3),radius 是 2。
圆圈的其中一个特性是:在圈线上任意打上一个点,这个点和中心点的线长一定是 radius。
题:倘若有一个点,坐落在圈线上,已知 y 坐标是 4,求它的 x 坐标。

这题其实可以用 right triangle 的勾股定理去解。
想象它是一个 right triangle

right triangle 斜边的长度就是 radius 的长度 2。
right triangle 的高度是 cb 线的长度 b.y - a.y → 4 - 3 = 1。
有斜边和高,依据勾股定理
hypot = √(a² + b²)
我们就可以算出 right triangle 的长度,算式:
hypot = √(a² + b²)
→ 2 = √(1² + b²)
→ 2² = 1 + b²
→ 4 - 1 = b²
→ b = √3
→ b = 1.732...
right triangle 的长度(或 ac 线的长度)是 1.732... (很多小数)
有 ac 线长就可以算出 B 点的坐标了。
b.x = a.x - ac → 3 - 1.732... = 1.268...

打完,收工。
等一下!

其实右边还有一个点,坐标 y 同样是 4。
这也是圆圈在几何中的一个特色:一个 y 对应两个 x。
它怎么算呢?
刚的算式有一环是不完整的
→ b = √3
→ b = 1.732...
平方根应该要解出正负数才对。
→ b = √3
→ b = ±1.732...
因为 1.732 * 1.732 等于 3,同时 -1.732 * -1.732 负负得正也等于 3。
因此,还得解多一个数:b.x = a.x - (-ac) → 3 + 1.732... = 4.732...

打完,收工。
圆的标准方程
上一 part,我们是用 right triangle 的勾股定理来推算,原理没错,但不够方便。
更直接的方式是用 -- 圆的标准方程:
(x - h)² + (y - k)² = r²
它是从勾股定理演化而来,加入了几何坐标概念。
x, y 是我们要求的坐标
h, k 是圆圈的中心坐标
r 是圆圈的 radius
套回我们上一题:
圆圈中心坐标 3, 3
radius 2
已知点坐标 y: 4,求 x 坐标。
算式:
(x - h)² + (y - k)² = r²
→ (x - 3)² + (4 - 3)² = 2²
→ (x - 3)² + 1 = 4
→ (x - 3)² = 4 - 1
→ x - 3 = √3
→ x = ±1.732 + 3
→ x = 1.732 + 3 = 4.732...
→ x = -1.732 + 3 = 1.268...
两个坐标:(4.732, 4) 和 (1.268, 4)
计算线和圈的交点
有一个圈和一条线,想知道它们的交点。

和双线求交点手法类似,用直线的参数方程 + 圆的标准方程来解。
P(t) = P0 + t * v
(x - h)² + (y - k)² = r²
目标是解出 t(time / 进度 / ratio),它会是两个数,因为这题有两个交点。
有了 t 就可以推算出具体坐标了。
一元二次方程的标准形式、判别式、求根公式
在开始解方程之前,我们需要补一些小知识(等会儿解方程需要用到):
-
一元二次方程的标准形式
ax² + bx + c = 0
要解出的是 x,abc 是参数。
x 有三种可能性:
-
无解(参数不对,解不出来)
-
一解(可以解出一个数)
-
二解(可以解出两个数,因为它有平方,换成平方根就会出现 ± 正负数)
-
-
判别式
b² - 4ac
判别式专门用来判别一元二次方程的 x 解(是无解、一解、还是二解?)
b² - 4ac < 0代表 x 无解b² - 4ac = 0代表 x 是一解b² - 4ac > 0代表 x 是二解 -
求根公式
解一元二次方程需要用到一些聪明的小技巧,不够聪明的人最好死背求根公式:
x = (-b ±√(b² - 4ac)) / 2a
直接把参数 abc 移进去就能得出 x 了。
解方程 – 直线的参数方程 + 圆的标准方程
开始解方程吧 🚀
题:
圆圈 a 中心点坐标(3, 3)
圆圈 a radius 是 2
线 b 坐标(6, 3)→(2, 6)
公式:
P(t) = P0 + t * v
(x - h)² + (y - k)² = r²
先解线 b
bdx = c.x - b.x
bdy = c.y - b.y
bx = b.x + t * bdx
by = b.y + t * bdy
再解圈 a
(ax - a.x)² + (ay - a.y)² = r² 下一步是把线的 x, y 带入到圈里,因为所谓交点就是线圈坐标相同的点,既 ax = bx,ay = by。
→ ((b.x + t * bdx) - a.x)² + ((b.y + t * bdy) - a.y)² = r²
→ (t * bdx + b.x - a.x)² + (t * bdy + b.y - a.y)² = r² b.x - a.x 意思是 ab 线的 delta x,我们改用 abdx 代替吧,整齐一点。
→ (t * bdx + abdx)² + (t * bdy + abdy)² = r² 下一步是展开平方,经典方程解法:(m + n)² 展开变成 m² + 2mn + n²
→ t² * bdx² + 2 * t * bdx * abdx + abdx² + t² * bdy² + 2 * t * bdy * abdy + abdy² = r² 下一步是让它变成一元二次方程形态:ax² + bx + c = 0
分三段看
(bdx² + bdy²) * t² + (2 * bdx * abdx + 2 * bdy * abdy) * t + (abdx² + abdy² - r²) = 0
abc 个别为:
a = (bdx² + bdy²)
b = (2 * bdx * abdx + 2 * bdy * abdy)
c = (abdx² + abdy² - r²)
x² = t² x 就是 t 就是我们要的解
先用判别式 b² - 4ac 判断 x 有几个解。
无解代表线和圈没有交点,一解代表只有一个交点,二解代表有两个交点。
b² - 4ac
→ (2 * bdx * abdx + 2 * bdy * abdy)² - 4 * (bdx² + bdy²) * (abdx² + abdy² - r²)
→ (2 * (c.x - b.x) * (b.x - a.x) + 2 * (c.y - b.y) * (b.y - a.y))² - 4 * ((c.x - b.x)² + (c.y - b.y)²) * ((b.x - a.x)² + (b.y - a.y)² - 2²)
→ (2 * (2 - 6) * (6 - 3) + 2 * (6 - 3) * (3 - 3))² - 4 * ((2 - 6)² + (6 - 3)²) * ((6 - 3)² + (3 - 3)² - 2²)
→ (2 * -4 * 3 + 2 * 3 * 0)² - 4 * ((-4)² + 3²) * (3² + 0² - 2²)
→ (-24 + 0)² - 4 * (16 + 9) * (9 + 0 - 4)
→ 576 - 4 * 25 * 5
→ 76
76 > 0 代表有两个交点。
最后用求根公式 x = (-b ±√(b² - 4ac)) / 2a
t1 = (-(2 * bdx * abdx + 2 * bdy * abdy) + √(b² - 4ac)) / (2 * (bdx² + bdy²))
→ (-(2 * (c.x - b.x) * (b.x - a.x) + 2 * (c.y - b.y) * (b.y - a.y)) + √(b² - 4ac)) / (2 * ((c.x - b.x)² + (c.y - b.y)²))
→ (-(2 * (2 - 6) * (6 - 3) + 2 * (6 - 3) * (3 - 3)) + √76) / (2 * ((2 - 6)² + (6 - 3)²))
→ (-(2 * -4 * 3 + 2 * 3 * 0) + √76) / (2 * ((-4)² + 3²))
→ (-(-24 + 0) + √76) / (2 * (16 + 9))
→ (24 + √76) / (2 * 25)
→ 0.654...(很多小数)
t2 = (24 - √76) / (2 * 25)
→ (24 - √76) / (2 * 25)
→ 0.305...(很多小数)
最终,t1 = 0.654... t2 = 0.305...。
有了 t1, t2 我们就可以推算最终的坐标了,把 t 带入回直线方程。
bx = b.x + t1 * bdx
→ 6 + 0.654 * (c.x - b.x)
→ 6 + 0.654 * (2 - 6)
→ 6 + 0.654 * -4
→ 6 + -2.616
→ 3.384...
by = b.y + t1 * bdy
→ 3 + 0.654 * (c.y - b.y)
→ 3 + 0.654 * (6 - 3)
→ 3 + 0.654 * 3
→ 4.962...
第一个交点是(3.384, 4.962)。
再用 t2 算第二个交点
bx = b.x + t2 * bdx
→ 6 + 0.305 * (c.x - b.x)
→ 6 + 0.305 * (2 - 6)
→ 6 + 0.305 * -4
→ 6 + -1.22
→ 4.78...
by = b.y + t2 * bdy
→ 3 + 0.305 * (c.y - b.y)
→ 3 + 0.305 * (6 - 3)
→ 3 + 0.305 * 3
→ 3.915...
第二个交点的坐标是(4.78, 3.915)
总结
关键公式:
-
斜率公式,算线条的斜度(但不是很准,日常很少会用它)
m = (y1 - y0) / (x1 - x0)
- vector delta x,y
dx = x1 - x0
dy = y1 - y0
-
直线参数方程
P(t) = P0 + t * v
- 圆的标准方程
(x - h)² + (y - k)² = r²
- 一元二次方程、判别式、求根公式
ax² + bx + c = 0
b² - 4ac
x = -b ±√(b² - 4ac) / 2a
总结
本篇介绍了一些前端可能会用到的几何基础计算。
之后应该还会写几篇实战,记入一下我这次遇到的设计需求。





浙公网安备 33010602011771号