← 几何 · 平面里的那些「判定」 / 两点之间,怎么连一条「好看」的曲线?(贝塞尔与控制点) 待审核 4 / 8
concept · bézier curve

两点之间,怎么连一条「好看」的曲线?(贝塞尔与控制点)

给平面上两点 AB,直线最短,但流程图、思维导图、节点编辑器里的连线全是弧线 / S 形。难点其实不在"画曲线"——SVG <path>Q/C 命令、Canvas 的 quadraticCurveTo/bezierCurveTo 一行就画出来了——而在控制点放哪。这一页把贝塞尔曲线拆开:控制点到底控制什么,以及只给两个端点时,怎么自动算出让曲线落得自然的控制点。

拖动画布里的实心点 A、B 改两端;手动控制点模式里所有空心控制点 C 都能拖,还能 + / − 控制点把曲线从二次一路升到 n 次。拖构造参数 t 滑块(或点「播放」),看 de Casteljau 怎么靠一层层线性插值把曲线一点点描出来——那条会动的橙色切线,就是控制点"控制"的东西。换模式看两种自动定控制点的策略。

1 · 控制点到底「控制」什么?(de Casteljau)

二次贝塞尔只有一个控制点 C。给定参数 t[0,1]t \in [0,1],曲线上的点是:

B(t)=(1t)2A+2(1t)tC+t2BB(t) = (1-t)^2\cdot A + 2(1-t)t\cdot C + t^2\cdot B。直接套这个多项式当然能算,但 de Casteljau 算法把它拆成纯粹的线性插值,几何意义一目了然:其一,在 ACA\to C 上取 t 处的点 Q0,在 CBC\to B 上取 t 处的点 Q1;其二,再在 Q0Q1Q0\to Q1 上取 t 处的点,就是曲线点 P(t)。关键在 Q0→Q1 这条线段:它始终是曲线在 P 处的切线。所以在两端 t=0 / t=1,切线分别是 ACA\to CCBC\to B——这就是为什么常说「控制点和端点的连线,就是曲线在该端点的切线方向」。控制点不在曲线上,它通过定住两端的出发方向来塑形。三次贝塞尔同理,只是多一层插值、多一个控制点(各管一端)。

2 · 从二次到 n 次:同一套插值,层数随阶数增长

控制点不止能有一两个。给定端点 ABm 个控制点,得到的是一条 (m+1) 次贝塞尔曲线。切到 手动控制点 模式,用 + / − 控制点增减,看曲线、控制多边形与 de Casteljau 脚手架怎么随阶数一起长高。

de Casteljau 是天然递归的:不管多少个控制点,每一步都只做同一件事——把当前这层相邻两点按 t 线性插值,得到点数少一个的下一层;反复做,直到只剩一个点,那就是 P(t)n 个控制点 → n 层插值:二次 2 层、三次 3 层、五次 5 层……倒数第二层(剩两点的那条线段)永远是 P 处的切线,这条性质与阶数无关。把它写成几行代码就是:for (let pts = [...所有点]; pts.length > 1; ) pts = pts.slice(1).map((p, i) => lerp(pts[i], p, t));

展开成多项式,就是 Bernstein 基:B(t)=ΣC(n,i)(1t)(ni)tiPiB(t) = Σ C(n,i)\cdot (1-t)^(n-i)\cdot t^i\cdot P_i,其中 C(n,i) 是二项式系数——二次的 1,2,1、三次的 1,3,3,1 正是杨辉三角的行。系数对每个控制点是一组随 t 变化的权重,且恒为正、加起来恒等于 1:所以曲线始终落在控制点的凸包内,永远不会"甩"到控制多边形之外。

阶数不是越高越好。次数一高,单个控制点会牵动整条曲线(没有局部性),还容易出现多余的摆动;数值上高次幂也更敏感。所以实务里几乎只用二次 / 三次,要表达复杂形状时把多段三次首尾拼接(样条 spline),而不是一味升阶——这也是字体、SVG、绘图软件的统一做法。

3 · 只给两个端点,控制点怎么「自动」算?

实际场景(连两个节点、连两个 DOM 元素)里,你手上只有 A、B 两点,得程序化地定出控制点。两种常用策略:

其一,对称弧——放在中垂线上。 取中点 M=(A+B)/2,沿垂直于 AB 的单位法向 n 偏移:C=M+kABnC = M + k\cdot |AB|\cdot nk 是弯曲系数:符号决定朝哪边鼓,绝对值决定多弯,k=0 退化成直线。偏移量乘上 |AB|关键——两点拉远时曲线等比例变弯,弧度始终自然。张鑫旭文里那句「除数越大、曲率越小」说的就是这个尺度系数(他把 k 写成 1/除数 的形式)。切到「对称弧」模式拖 A、B,弧形会跟着等比缩放。

其二,S 形连接器——让曲线从端点"切向引出"。 用三次贝塞尔,两个控制点分别贴着 A、B 沿水平方向拉出去:C1=(A.x+fΔx,A.y)C1 = (A.x + f\cdot Δx, A.y),C2=(B.xfΔx,B.y)C2 = (B.x - f\cdot Δx, B.y),其中 Δx=B.xA.xΔx = B.x - A.x。由第一节的切线性质,曲线就会水平地离开 A、水平地抵达 B,像一根柔软的电缆——这正是节点编辑器 / 流程图里连线的经典观感。f 控制引出的长度(占水平跨度的比例,≈0.5 最顺)。把布局换成竖排,只要把引出方向改成垂直(偏移 A.y/B.y 而非 A.x/B.x)即可。

4 · 画出来只是一行,但坐标系要对齐

算好控制点后,渲染就一句话。SVG:<path d="M ax ay Q cx cy bx by" />(三次用 C c1x c1y c2x c2y bx by);Canvas:ctx.moveTo(ax,ay); ctx.quadraticCurveTo(cx,cy,bx,by)。真正容易出错的是坐标:连两个 DOM 元素时,各元素的中心点要用 getBoundingClientRect() 取到,再统一减去容器(那块定位的画布 / SVG)的偏移换算到同一坐标系里,否则曲线会整体偏移。元素一旦能拖动 / 滚动,这套取点 + 重算控制点 + 重绘 path 就得跟着动起来。

5 · 它用在哪里

节点编辑器 / 流程图(n8n、Figma、Blender shader、各类 no-code):节点间那根可拖拽的"线"几乎都是上面的 S 形三次贝塞尔。关系图谱 / 思维导图:实体之间的连线用对称弧避免直线重叠、看清方向。CSS offset-path / SMIL:让元素沿一条贝塞尔路径运动。手写 / 签名平滑:把采样到的离散触点用贝塞尔串起来,去掉折线感。字体与矢量图形:字形轮廓本身就是一段段二次 / 三次贝塞尔。核心永远是同一件事——先把控制点算对,画只是最后一行

6 · 🔗 相关链接

  • JS 实现两点之间的连接曲线 · 张鑫旭——本页的出处:从连接两个可拖拽 DOM 元素的实战需求出发,讲怎么用贝塞尔控制点把"线"画顺,含坐标换算与拖动重绘的工程细节。
  • A Primer on Bézier Curves · pomax — 贝塞尔曲线的交互式"大全":de Casteljau、求导/切线、等分、求交、拟合……几乎每个性质都配可拖的可视化。
  • SVG 路径:曲线命令 Q / C · MDN — 规范侧:<path> 的二次 Q / 三次 C 及其平滑版 T / S 怎么写,本页 path 字符串就照它生成。
  • De Casteljau's algorithm · Wikipedia — 第一节那套"逐层线性插值"的算法本体,以及它为何数值稳定、为何能顺手做曲线细分。