前端基础几何计算 – basic geometric tan sin cos

前言

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

image

这类设计,如果不考虑 responsive,其实非常简单,直接放一张 SVG 就可以了;但如果要兼顾 responsive,就困难多了。

通常需要计算坐标,然后动态生成 SVG 才能解决。

我在很多年前写过一篇 《crop image 需要的基础知识》,当时遇到的设计需求是做一个 crop image 功能,里面就涉及到了一些基础的几何计算,比如 tan sin cos 等等。

没想到时隔多年又给我遇上了。

可这么多年,那些知识早就忘光光了...😔

本篇作为一个复习,重新整理一下前端需要掌握的基础几何计算。

 

直角三角形 right triangle の 几何计算

image

左边是 triangle(三角形),右边是 right triangle(直角三角形)。

right triangle 最大特色是:它一定有一个 90° 角。

image

也正因为这个特性,它有一些固定的规律。只要我们掌握其中一部分信息,结合这些规律,就可以推算出完整的信息。

求 hypotenuse 斜边 – hypot 函数

斜边 hypotenuse(简称 hypot)指的是直角 90° 对面那条线

image

已知 right triangle 的长(A → B 简称 ab)是 4px,高(A → C 简称 ac)是 3px。

求斜边 bc 的长度。

它的几何公式是

hypot = √(a² + b²)

术语叫勾股定理 (Pythagorean Theorem)

我们一个一个看:

  1. √ 是 square root,平方根。

  2. a 指的是图中 ab 或 ac 任意一条线的长度,可以放 3px 也可以放 4px。

  3. ² 是 squared,平方。

  4. b 指的是图中 ab 或 ac 任意一条线的长度,如果之前 a 选了 ab 4px,那 b 就一定是剩下那一条 ac 3px。

Tips:√ 是 alt + 251 或微软输入法 dui,² 是 alt + 0178 或微软输入法 pingfang

把参数放入到公式里,长这样:hypot = √(3² + 4²) 计算结果是 5

用 JavaScript 写法是这样:

  1. √ 是 Math.sqrt(25)

  2. ² 是 3 ** 2 或者 Math.pow(3, 2)

  3. 完整公式长这样 const hypot = Math.sqrt(3 ** 2 + 4 ** 2);

  4. 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

除了求线条的长度之外,求各个角度也是日常所需。

image

∠acb(左上角)是多少度?

∠abc(右下角)是多少度?

right triangle 的规则是:三个角度相加一定是 180°,直角一定是占 90°,那剩下的 90° 就分给另外两个角。

如果 right triangle 的长度和高度一致,那就平分 45°,若是像上图那样不一致(一边 3px、一边 4px)则双边角度不同,需要用公式计算。

它的几何公式是:

tan(θ) = opposite /  adjacent

tan 全名是 Tangent,它是一个数学函数,我们不用懂原理,背起来用就可以了。

它接受一个参数 theta θ,单位是度 °。(Tips:θ 是微软输入法 theta

比如 tan(30°) = 0.5773...,用计算机就能按出来了

image

opposite(对边)、adjacent(邻边)、hypotenuse(斜边)指的是 right triangle 的三边线。

hypot 我们上面讲解过了,直角 90° 的对面就是 hypot。

opposite 和 adjacent 没有固定指向哪一边,它是依据我们要计算哪个角度而决定的。

看例子理解:

image

我们要计算出 ∠abc 的度。

此时,opposite(对边)指的是 ∠abc 的对面,也就是 ac 3px。

adjacent(邻边)则是 ab 4px。(hypot 斜边则是固定的 bc 5px)

依据公式 tan(θ) = opposite /  adjacent

我们要求的是里面那个 θ,因此需要把公式改成 θ = atan(opposite / adjacent)

atan 全名是 Arctangent,也是数学函数,照用就是了。

计算机里这么按

gif

tan⁻¹ 就是 atan。

算式:∠abc = atan(3 / 4) → atan(0.75) = 36.869897645844021296855612559093...°

小心小数坑:

36.8698... 是无限小数(术语叫无理数)就跟圆周率 π 一样。

上面这个结果是用 windows calculator 算的,如果用 JavaScript 算,只能得到 36.86989764584402,因为 JS 算得快,但不精准。

这种无限小数很容易造成小数丢失问题,因为电脑的储存是有限的,它存到一个量就停了,后续的也就丢失了。

一旦丢失了就无法再还原,计算就可能出现微差,这时就要特别注意了(下面会有例子)。

好,我们继续算另一边 ∠acb 的度。

image

最简单的方法是:直接拿 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...°

image

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 函数

image

已知,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))

计算结果

image

有了 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 */

总结

关键公式:

  1. 算斜边 hypotenuse

    hypot = √(a² + b²)
  2. 算角度
    tan(θ) = opposite /  adjacent
  3. 算对边 opposite 和 邻边 adjacent

    cos(θ) = adjacent / hypot

    sin(θ) = opposite / hypot

 

几何图 の x, y, θ 计算

上一 part 我们聚焦的是 right triangle,计算的是各个边长,和各个角度。

虽然我把 right triangle 画在几何图中,但却没有纳入 x, y 坐标的概念。

而这一 part 我们聚焦在几何图中 x, y 的常见计算。(注:这和 right triangle 没有直接关系(只有间接),别把它们混在一下理解)

几何图基础 – 坐标 & 角度

先讲一下几何图的基本规则:

image

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 轴那一条线算起。

上面是顺时针,下面则是逆时针。

image

上面负责 1 到 179°

下面负责 -1 到 -179°

它没有超过 180° 的。当超过 180° 就改用 negative 来表示。

用 x, y 计算出角度 by atan2

好,看回这张图

image

已知 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...°

image

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

image

已知,角度是 53.13010235415598°,radius(半径)的长度是 5(从坐标 0, 0 到 A point)。

求,A 的坐标 x, y。

把它想象成一个 right triangle

image

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

image

已知,角度是 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 放进去就可以了)

总结

关键公式:

  1. 算角度

    θ = atan2(y, x)
  2. 算 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)

image

我们上面有讲解过点了,这里不再赘述。

它也没有什么特别的,就是一个坐标 x, y 而已。

A point 的坐标是 x: 1, y: 1

这是另一个点

image

B point 坐标是 3, 1

线

把两个点连起来,就形成了一条线。

image

一条线,我们会在意:

  1. 它是横线(—)吗?

  2. 它是竖线(|)吗?

  3. 它是斜线吗?

    是正斜(/)往上,还是负斜(\)往下
  4. 它有多斜呢?

横线、竖线、正斜线、负斜线、斜度的判断手法

我们可以透过一些手法来判断它们:

  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 完全一致,那它不是一条线,它是两个重叠的点。

  2. 判断是横线

    const isHorizontalLine = aPoint.x === bPoint.x;

    如果两个点的 x 坐标相同,那就是横线。

  3. 判断是竖线

    const isVerticalLine = aPoint.y === bPoint.y;

    如果两个点的 y 坐标相同,那就是竖线。

  4. 判断是正斜线(/)或负斜线(\)

    不是横线、不是竖线、那就是斜线。

    斜线还分正斜(/)和负斜(\)。

    如果 x, y 的发展方向是一致的,那就是正斜线(/)。

    意思是:x 变大,y 也变大;或者 x 变小,y 也变小。

    反之,如果 x, y 的发展方向不一致,那就是负斜线(\)。

    意思是:x 变大,要却变小;或者 x 变小,y 却变大。

    image
  5. 计算斜度

    一条线有多斜,得看它的角度

    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) = 1

    y = 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 不是角度,而且无法换算成角度,但它和角度有一点点关系。

image

我们把线条看作是一个 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 要如何判断出:横线、竖线、正斜线、负斜线呢?

  1. 判断它是不是一条线

    如果 dx 和 dy 都是 0,代表它不是线,而是两个重叠的点。

  2. 判断是横线

    如果 dy 是 0,代表它是一条横线。

  3. 判断是竖线

    如果 dx 是 0,代表它是一条竖线。

  4. 判断是正斜线(/)或负斜线(\)

    我们上面有提到,正斜线的特色是线的发展是一致的: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 发展方向不一致。

  5. 计算斜度

    斜度的算法是 θ = 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 坐标相减。

如果是一条斜线呢?

image

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

image

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

直线的参数方程

这是一条线

image

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 = 2dy = 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

image

小心坑:永不触及的 y

一条斜线,无线延申,无论给定的 y 是多少,它都能算出 x。

但,如果它是一条横线就不同了。

image

假设这条线一直延申,当 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)指的是两条线交叉的那一个点坐标。

image

注:当然,并不是所有的线都会有交点,比如两条平行线就不可能有交点。但不管,我们先不思考这些情况。

要算出交点的坐标 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 = cxay = cy 时,atct 是多少?

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.75ct = 0.75

这里有一个知识点要注意:

atct 有可能会 > 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

image

判断无交点

两条平行线是不会有交点的,如果我们依然用公式计算,很可能会算到除于零,然后 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)

image

注:这个公式和常见教科书的公式顺序加减有一点出路,但不要紧,算出来结果是一样的。

看到吗,它最后有一个除法,如果除于 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)

image

圈圈圆圆圈圈

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

image

圆圈的中心点坐标是(3, 3),radius 是 2。

圆圈的其中一个特性是:在圈线上任意打上一个点,这个点和中心点的线长一定是 radius。

题:倘若有一个点,坐落在圈线上,已知 y 坐标是 4,求它的 x 坐标。

image

这题其实可以用 right triangle 的勾股定理去解。

想象它是一个 right triangle

image

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...

image

打完,收工。

等一下!

image

其实右边还有一个点,坐标 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...

image

打完,收工。

圆的标准方程

上一 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)

计算线和圈的交点

有一个圈和一条线,想知道它们的交点。

image

和双线求交点手法类似,用直线的参数方程 + 圆的标准方程来解。

P(t) = P0 + t * v

(x - h)² + (y - k)² = r²

目标是解出 t(time / 进度 / ratio),它会是两个数,因为这题有两个交点。

有了 t 就可以推算出具体坐标了。

一元二次方程的标准形式、判别式、求根公式

在开始解方程之前,我们需要补一些小知识(等会儿解方程需要用到):

  1. 一元二次方程的标准形式

    ax² + bx + c = 0

    要解出的是 x,abc 是参数。

    x 有三种可能性:

    • 无解(参数不对,解不出来)

    • 一解(可以解出一个数)

    • 二解(可以解出两个数,因为它有平方,换成平方根就会出现 ± 正负数)

  2. 判别式

    b² - 4ac

    判别式专门用来判别一元二次方程的 x 解(是无解、一解、还是二解?)

    b² - 4ac < 0 代表 x 无解

    b² - 4ac = 0 代表 x 是一解

    b² - 4ac > 0 代表 x 是二解

  3. 求根公式

    解一元二次方程需要用到一些聪明的小技巧,不够聪明的人最好死背求根公式:

    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)

image

总结

关键公式:

  1. 斜率公式,算线条的斜度(但不是很准,日常很少会用它)

    m = (y1 - y0) / (x1 - x0)
  2. vector delta x,y

    dx = x1 - x0

    dy = y1 - y0

  3. 直线参数方程

    P(t) = P0 + t * v
  4. 圆的标准方程
    (x - h)² + (y - k)² = r²
  5. 一元二次方程、判别式、求根公式

    ax² + bx + c = 0

    b² - 4ac

    x = -b ±√(b² - 4ac) / 2a

 

总结

本篇介绍了一些前端可能会用到的几何基础计算。

之后应该还会写几篇实战,记入一下我这次遇到的设计需求。

 

posted @ 2025-12-21 08:45  兴杰  阅读(1)  评论(0)    收藏  举报