两点之间,怎么连一条「好看」的曲线?(贝塞尔与控制点)
给平面上两点 A、B,直线最短,但流程图、思维导图、节点编辑器里的连线全是弧线 / S 形。难点其实不在"画曲线"——SVG <path> 的 Q/C 命令、Canvas 的 quadraticCurveTo/bezierCurveTo
一行就画出来了——而在控制点放哪。这一页把贝塞尔曲线拆开:控制点到底控制什么,以及只给两个端点时,怎么自动算出让曲线落得自然的控制点。
拖动画布里的实心点 A、B 改两端;手动控制点模式里所有空心控制点 C 都能拖,还能 + / − 控制点把曲线从二次一路升到 n 次。拖构造参数 t 滑块(或点「播放」),看 de Casteljau 怎么靠一层层线性插值把曲线一点点描出来——那条会动的橙色切线,就是控制点"控制"的东西。换模式看两种自动定控制点的策略。
1 · 控制点到底「控制」什么?(de Casteljau)
二次贝塞尔只有一个控制点 C。给定参数 ,曲线上的点是:
。直接套这个多项式当然能算,但 de Casteljau 算法把它拆成纯粹的线性插值,几何意义一目了然:其一,在
上取 t 处的点 Q0,在
上取 t 处的点 Q1;其二,再在
上取 t 处的点,就是曲线点 P(t)。关键在 Q0→Q1 这条线段:它始终是曲线在 P 处的切线。所以在两端 t=0 / t=1,切线分别是
和
——这就是为什么常说「控制点和端点的连线,就是曲线在该端点的切线方向」。控制点不在曲线上,它通过定住两端的出发方向来塑形。三次贝塞尔同理,只是多一层插值、多一个控制点(各管一端)。
2 · 从二次到 n 次:同一套插值,层数随阶数增长
控制点不止能有一两个。给定端点 A、B 与 m 个控制点,得到的是一条 (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 基:,其中 C(n,i) 是二项式系数——二次的 1,2,1、三次的 1,3,3,1 正是杨辉三角的行。系数对每个控制点是一组随 t 变化的权重,且恒为正、加起来恒等于 1:所以曲线始终落在控制点的凸包内,永远不会"甩"到控制多边形之外。
阶数不是越高越好。次数一高,单个控制点会牵动整条曲线(没有局部性),还容易出现多余的摆动;数值上高次幂也更敏感。所以实务里几乎只用二次 / 三次,要表达复杂形状时把多段三次首尾拼接(样条 spline),而不是一味升阶——这也是字体、SVG、绘图软件的统一做法。
3 · 只给两个端点,控制点怎么「自动」算?
实际场景(连两个节点、连两个 DOM 元素)里,你手上只有 A、B 两点,得程序化地定出控制点。两种常用策略:
其一,对称弧——放在中垂线上。 取中点 M=(A+B)/2,沿垂直于 AB 的单位法向 n 偏移:。k 是弯曲系数:符号决定朝哪边鼓,绝对值决定多弯,k=0 退化成直线。偏移量乘上 |AB| 是关键——两点拉远时曲线等比例变弯,弧度始终自然。张鑫旭文里那句「除数越大、曲率越小」说的就是这个尺度系数(他把
k 写成 1/除数 的形式)。切到「对称弧」模式拖 A、B,弧形会跟着等比缩放。
其二,S 形连接器——让曲线从端点"切向引出"。 用三次贝塞尔,两个控制点分别贴着 A、B 沿水平方向拉出去:,,其中
。由第一节的切线性质,曲线就会水平地离开 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 — 第一节那套"逐层线性插值"的算法本体,以及它为何数值稳定、为何能顺手做曲线细分。