惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hugging Face - Blog
Hugging Face - Blog
B
Blog
博客园_首页
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
G
GRAHAM CLULEY
Microsoft Azure Blog
Microsoft Azure Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
The GitHub Blog
The GitHub Blog
Security Latest
Security Latest
F
Full Disclosure
云风的 BLOG
云风的 BLOG
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
V
Visual Studio Blog
有赞技术团队
有赞技术团队
腾讯CDC
V
V2EX
Vercel News
Vercel News
C
Cisco Blogs
V2EX - 技术
V2EX - 技术
Scott Helme
Scott Helme
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
AWS News Blog
AWS News Blog
S
Schneier on Security
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
小众软件
小众软件
G
Google Developers Blog
C
Check Point Blog
C
CERT Recently Published Vulnerability Notes
博客园 - 叶小钗
S
SegmentFault 最新的问题
T
Tor Project blog
J
Java Code Geeks
L
Lohrmann on Cybersecurity
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
The Exploit Database - CXSecurity.com
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
博客园 - 司徒正美
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
Secure Thoughts
量子位
N
News and Events Feed by Topic
MyScale Blog
MyScale Blog
TaoSecurity Blog
TaoSecurity Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Engineering at Meta
Engineering at Meta

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog 【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog
Yesterday17 · 2020-07-01 · via Yesterday17's Blog

这篇文章总共探讨三个内容,正如标题所述:贝塞尔曲线、字体矢量化是假的与曲线运算。然而,实际上这三个问题是同一个问题,都是围绕着贝塞尔曲线展开的。而对贝塞尔曲线进行处理也可以得到很多有趣的有用的结果。

ToC

  • 贝塞尔曲线
    • 基本概念
    • 控制点与阶数
    • 高阶贝塞尔曲线
    • n 阶贝塞尔曲线的代数定义
  • 字体矢量化
  • 曲线运算
    • 化直为曲
    • 曲线分割
      • 结论
      • 证明
      • 图示
      • 简单代码实现
  • 结语

贝塞尔曲线

贝塞尔曲线是在 ASS 绘图代码、矢量作图、乃至字体设计中都广泛应用的一种曲线,比如 InkScapePath 绘制:

InkScape 中绘制贝塞尔曲线
InkScape 中绘制贝塞尔曲线

基本概念

我们将整条贝塞尔曲线用表达式 B(t)\bold{B}(t) 表示,其中 t∈[0,1]t\in[0,1]

从单纯的语义上来看,t 可以代表曲线描绘的进程。这种“进程感”在看曲线描绘的动图的时候可能更加明显一些,我们来看下面这张二阶贝塞尔曲线的绘画过程图:

https://upload.wikimedia.org/wikipedia/commons/3/3d/B%C3%A9zier_2_big.gif
https://upload.wikimedia.org/wikipedia/commons/3/3d/B%C3%A9zier_2_big.gif

注意下面的数字,在等号 = 后面其实还有一个小数点(.),因此数字是从 0 ~ 1 不断递增的。而随着数字的不断递增,曲线也在被不断描绘,最后当 t=1t=1 时,曲线描绘完成。

我们发现,图中出现了三个点:P0\bold{P}_0P1\bold{P}_1P2\bold{P}_2。我们将这三个点称为控制点,因为这三个点控制并决定了这条曲线的具体形状。

我们将曲线上的某一时刻的点用 B\bold{B} 表示,不难发现 B\bold{B} 的位置和 P0\bold{P}_0P1\bold{P}_1P2\bold{P}_2 三个点息息相关。那二者究竟是什么关系呢?

我们依然来看上面这张动图,仔细观察不断移动的各个圆点的位置和 tt 的关系,不难发现圆点的位置其实就相当于 P0\bold{P}_0P1\bold{P}_1 之间的 tt 处。比如当 t=0.1t=0.1 时,P0\bold{P}_0P1\bold{P}_1 之间的圆点就位于 P0\bold{P}_0 出发后十分之一处;P1\bold{P}_1P2\bold{P}_2 之间的圆点也是如此。在确定了这两个圆点之后,我们在这两个圆点连成的线上同样找到 t 的位置,对应的那个位置就是点 B 的所在了。

控制点与阶数

上面的介绍中我们不断地提到了“二阶贝塞尔曲线”这个概念,那阶数和控制点的数量又有着什么样的关联呢?其实我觉得这个问题大家心里已经有答案了(

对于贝塞尔曲线而言,最关键的就是控制点,而其阶数也和控制点的数量息息相关。当只有两个控制点时,贝塞尔曲线其实就相当于这两个点的连线,描绘这条曲线的过程也就相当于在两点之间线性插值的过程,因此被称为线性贝塞尔曲线;当存在三个控制点时,这样的贝塞尔曲线被称为二阶贝塞尔曲线;随着控制点数量的不断增多,我们能做到的对曲线的控制也就更加精细,能够绘制出的曲线形状也就更加自由。

于是在这里我们可以得到:贝塞尔曲线的阶数 = 控制点的数量 - 1

高阶贝塞尔曲线

下面我们来看个稍微复杂点的例子。这是一张四阶贝塞尔曲线的绘画过程图,t=0.25t=0.25,如下图所示:

https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/B%C3%A9zier_4_big.svg
https://upload.wikimedia.org/wikipedia/commons/thumb/b/bf/B%C3%A9zier_4_big.svg

可以看到,相对于二阶贝塞尔曲线,四阶就要复杂一些了,但依然是万变不离其宗。

首先依然是将各控制点 PP 按顺序依次连成线,可以看到各个 PP 点之间是用灰线连接的。

然后是确定各个 QQ 点的位置,并将 QQ 点也依次连接起来,图中使用的是绿色表示。

第三步是将各个 RR 点的位置确定,同样依次连接,见图中的蓝线。

最后,我们发现:蓝线只有两条。那这时将蓝线上的 SS 点位置确定,然后在 SS 点连线上根据 tt 就可以得到我们想要的 BB 点了。

可以看出,每经过一步,连线的数量就会减少 1。到最后只剩下一条线时,在这条线上确定的 tt 的位置就是 B(t)\bold{B}(t) 对应的点了。

n 阶贝塞尔曲线的代数定义

从代数上来看,贝塞尔曲线其实是由下面这个式子生成的(其中 n 代表曲线的阶数):

B(t)=∑i=0n(ni)Pi(1−t)n−iti​\bold{B}(t)=\sum\limits_{i=0}^{n}{\left(\begin{matrix}n \\ i\end{matrix}\right)\bold{P}_i(1-t)^{n-i}t^i}​

其中 nn 代表曲线的阶数,而 Pi\bold{P}_i 则代表第 ii 个控制点。P0\bold{P}_0 代表起点控制点,而 Pn−1\bold{P}_{n-1} 代表终点控制点。

字体矢量化

其实说字体矢量化是个伪命题,因为我们现在使用的字体大多本身就是矢量字体,矢量图形本身又何来矢量化一说呢(笑)?所以这个标题也只是为了引出这个事实罢了其实是我想不出别的标题了(逃。

以这个 为例,这里我们选用的字体是文泉驿正黑体,使用的软件是 FontForge。限于屏幕高度原因,这里无法将整个字截下,故上部会有所遮挡。

这里我们可以看到很多有着各种含义的标号。这里我建议各位真的有各位吗去简单阅读一下这篇关于 FontForge 使用的介绍文档[3]

我们需要了解的就是中间的 X 符号,这代表的就是贝塞尔曲线中间的控制点,而位于 X 左右的的圆点则代表贝塞尔曲线的起始控制点。可以看到,整个字中存在着大量的 X 号,而每一个 X 就代表着一条二阶贝塞尔曲线

为什么说是二阶贝塞尔曲线呢?因为 TrueType 字体在构建后就只剩下二阶贝塞尔曲线了,而我们使用的也正是发行的字体,因此图中出现的也就都是二阶贝塞尔曲线了。

在本章中,我们只讨论三阶路径,它在绘制字形时普遍使用。Spiro 路径将在下一章讨论,二阶曲线在绘制时很少用到,只会在 TrueType 字体中找到 — 他们更常出现在构建时.

FontForge 与字体设计 - 理解Bézier曲线 [3]

除去贝塞尔曲线之外,描绘字体的也就只剩下了直线(l),移动位置(m)和闭合曲线(通常可以用 m 直接代替)。而这些都能在 ASS 的绘图指令中找到对应。这也就是字体可以转化成绘图指令——也就是所谓的字体“矢量化”之名出现的原因了。

曲线运算

说是运算,其实也就只有两种:化直为曲和曲线分割。

化直为曲

我们知道,贝塞尔曲线是可以完美表示直线的,也就是上面提到的线性贝塞尔曲线。但当我们需要使用 n 阶贝塞尔曲线表现直线的时候又该怎么办呢?

其实也很简单,只要让中间的控制点位于起止点的连线上就可以了。这个原理也很好想明白,大家可以想想看(笑)

于是我这里用 JavaScript 简单实现了一下,看看就好(:

function l2b(start = [0, 0], end = [0, 0]) {

return [

start,

[start[0] + (end[0] - start[0]) / 3, start[1] + (end[1] - start[1]) / 3],

[

start[0] + ((end[0] - start[0]) / 3) * 2,

start[1] + ((end[1] - start[1]) / 3) * 2,

],

end,

];

}

曲线分割

结论

此处以三阶贝塞尔曲线为例。

如果我们想要在 t=kt=k 处截断 B(t)\bold{B}(t),那原曲线就相当于被拆成了左右两个部分。

我们知道:P0\bold{P_0}P1\bold{P_1}P2\bold{P_2}P3\bold{P_3} 是最开始的四个定位点,而接下来我们需要确定的是作图第一步确定的三个点 Q0\bold{Q_0}Q1\bold{Q_1}Q2\bold{Q_2}:

Q0=(1−k)P0+kP1Q1=(1−k)P1+kP2Q2=(1−k)P2+kP3\begin{aligned} \bold{Q_0}=(1-k)\bold{P_0}+k\bold{P_1} \\ \bold{Q_1}=(1-k)\bold{P_1}+k\bold{P_2} \\ \bold{Q_2}=(1-k)\bold{P_2}+k\bold{P_3} \\ \end{aligned}

然后,我们确定接下来的两个点 ​R0\bold{R_0} 和 ​R1\bold{R_1}​:

R0=(1−k)Q0+kQ1=(1−k)((1−k)P0+kP1)+k((1−k)P1+kP2)=(P0−2P1+P2)k2+(2P1−2P0)k+P0R1=(1−k)Q1+kQ2=(1−k)((1−k)P1+kP2)+k((1−k)P2+kP3)=(P1−2P2+P3)k2+(2P2−2P1)k+P1\begin{aligned} \bold{R_0}&=(1-k)\bold{Q_0}+k\bold{Q_1} \\ &=(1-k)((1-k)\bold{P_0}+k\bold{P_1})+k((1-k)\bold{P_1}+k\bold{P_2}) \\ &=(\bold{P_0}-2\bold{P_1}+\bold{P_2})k^2+(2\bold{P_1}-2\bold{P_0})k+\bold{P_0} \\ \bold{R_1}&=(1-k)\bold{Q_1}+k\bold{Q_2} \\ &=(1-k)((1-k)\bold{P_1}+k\bold{P_2})+k((1-k)\bold{P_2}+k\bold{P_3}) \\ &=(\bold{P_1}-2\bold{P_2}+\bold{P_3})k^2+(2\bold{P_2}-2\bold{P_1})k+\bold{P_1} \end{aligned}

最后,我们可以得到 ​​​​​B​​​​​\bold{B}​:​

B=(1−k)R0+kR1=(1−k)((P0−2P1+P2)k2+(2P1−2P0)k+P0)+k((P1−2P2+P3)k2+(2P2−2P1)k+P1)=(−P0+3P1−3P2+P3)k3+(3P0−6P1+3P2)k2+(−3P0+3P1)k+P0\begin{aligned} \bold{B}&=(1-k)\bold{R_0}+k\bold{R_1} \\ &=(1-k)((\bold{P_0}-2\bold{P_1}+\bold{P_2})k^2+(2\bold{P_1}-2\bold{P_0})k+\bold{P_0})+k((\bold{P_1}-2\bold{P_2}+\bold{P_3})k^2+(2\bold{P_2}-2\bold{P_1})k+\bold{P_1}) \\ &=(-\bold{P_0}+3\bold{P_1}-3\bold{P_2}+\bold{P_3})k^3+(3\bold{P_0}-6\bold{P_1}+3\bold{P_2})k^2+(-3\bold{P_0}+3\bold{P_1})k+\bold{P_0} \end{aligned}

我们的结论是:左半边的曲线可以用 ​P0−Q0−R0−B\bold{P_0}-\bold{Q_0}-\bold{R_0}-\bold{B} 这四个定位点表示,而右半边的曲线可以用 ​B−R1−Q2−P3\bold{B}-\bold{R_1}-\bold{Q_2}-\bold{P_3}​ 表示。

证明

下面我们来证明这个结论。我们先来看三阶贝塞尔曲线的多项式展开:(1)

B(t)=(1−t)3P0+3t(1−t)2P1+3t2(1−t)P2+t3P3=(1−3t+3t2−t3)P0+(3t−6t2+3t3)P1+(3t2−3t3)P2+t3P3=(−P0+3P1−3P2+P3)t3+(3P0−6P1+3P2)t2+(−3P0+3P1)t+P0\begin{aligned} \bold{B}(t)&=(1-t)^3\bold{P_0}+3t(1-t)^2\bold{P_1}+3t^2(1-t)\bold{P_2}+t^3\bold{P_3} \\ &= (1-3t+3t^2-t^3)\bold{P_0}+(3t-6t^2+3t^3)\bold{P_1}+(3t^2-3t^3)\bold{P_2}+t^3\bold{P_3} \\ &= (-\bold{P_0}+3\bold{P_1}-3\bold{P_2}+\bold{P_3})t^3+(3\bold{P_0}-6\bold{P_1}+3\bold{P_2})t^2+(-3\bold{P_0}+3\bold{P_1})t+\bold{P_0} \end{aligned}

将 ​tt 替换成 ​ktkt,多项式表示的就是 ​t=kt=k 截断处左半边的曲线:(2)

B(kt)=(−P0+3P1−3P2+P3)k3t3+(3P0−6P1+3P2)k2t2+(−3P0+3P1)kt+P0\begin{aligned} \bold{B}(kt)&= (-\bold{P_0}+3\bold{P_1}-3\bold{P_2}+\bold{P_3})k^3t^3+(3\bold{P_0}-6\bold{P_1}+3\bold{P_2})k^2t^2+(-3\bold{P_0}+3\bold{P_1})kt+\bold{P_0} \end{aligned}

此时,由于 t∈[0,1]t\in[0, 1]​,而 kk​ 是常数,因此 ​kt∈[0,k]kt\in[0, k],符合曲线左半边的定义域要求。

而而将 P0−Q0−R0−B\bold{P_0}-\bold{Q_0}-\bold{R_0}-\bold{B} 这四个定位点代入(1)式的 P0\bold{P_0}P1\bold{P_1}P2\bold{P_2}P3\bold{P_3} 中:(3)

B(t)=(1−3t+3t2−t3)P0+(3t−6t2+3t3)Q0+(3t2−3t3)R0+t3B=(1−3t+3t2−t3)P0+(3t−6t2+3t3)((1−k)P0+kP1)+(3t2−3t3)((P0−2P1+P2)k2+(2P1−2P0)k+P0)+t3((−P0+3P1−3P2+P3)k3+(3P0−6P1+3P2)k2+(−3P0+3P1)k+P0)=(−P0+3P1−3P2+P3)k3t3+(3P0−6P1+3P2)k2t2+(−3P0+3P1)kt+P0=B(kt)\begin{aligned} \bold{B}(t)&=(1-3t+3t^2-t^3)\bold{P_0}+(3t-6t^2+3t^3)\bold{Q_0}+(3t^2-3t^3)\bold{R_0}+t^3\bold{B} \\ &=(1-3t+3t^2-t^3)\bold{P_0} \\ &+(3t-6t^2+3t^3)((1-k)\bold{P_0}+k\bold{P_1}) \\ &+(3t^2-3t^3)((\bold{P_0}-2\bold{P_1}+\bold{P_2})k^2+(2\bold{P_1}-2\bold{P_0})k+\bold{P_0}) \\ &+t^3((-\bold{P_0}+3\bold{P_1}-3\bold{P_2}+\bold{P_3})k^3+(3\bold{P_0}-6\bold{P_1}+3\bold{P_2})k^2+(-3\bold{P_0}+3\bold{P_1})k+\bold{P_0}) \\ &=(-\bold{P_0}+3\bold{P_1}-3\bold{P_2}+\bold{P_3})k^3t^3+(3\bold{P_0}-6\bold{P_1}+3\bold{P_2})k^2t^2+(-3\bold{P_0}+3\bold{P_1})kt+\bold{P_0} = \bold{B}(kt) \end{aligned}

(1)、(3)式相等,故左半边的曲线可以用 P0−Q0−R0−B\bold{P_0}-\bold{Q_0}-\bold{R_0}-\bold{B} 这四个定位点表示。

同理,可证右半边的曲线可以用 B−R1−Q2−P3\bold{B}-\bold{R_1}-\bold{Q_2}-\bold{P_3} 表示。

你也可以使用下面这个脚本在 Octave 中验证这个结论:

pkg load symbolic

syms k t P0 P1 P2 P3

function retval = bezier(t, p0, p1, p2, p3)

expr = (1-t)^3*p0+3*t*(1-t)^2*p1+3*t^2*(1-t)*p2+t^3*p3

retval = expand(expr)

endfunction

function ret = next(k, p0, p1)

ret = expand((1-k)*p0+k*p1)

endfunction

# points

Q0=next(k, P0, P1)

Q1=next(k, P1, P2)

Q2=next(k, P2, P3)

R0=next(k, Q0, Q1)

R1=next(k, Q1, Q2)

B=next(k, R0, R1)

# left

left=bezier(k*t, P0, P1, P2, P3)

left_divided=bezier(t, P0, Q0, R0, B)

left == left_divided

# right

right=bezier(k+(1-k)*t, P0, P1, P2, P3)

right_divided=bezier(t, B, R1, Q2, P3)

right == right_divided

这种分割方式也适用于更高阶的贝塞尔曲线。

图示

既然上面是用的三阶贝塞尔曲线举例,那我就找一张三阶的图好了:

https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/B%C3%A9zier_3_big.svg/1280px-B%C3%A9zier_3_big.svg.png
https://upload.wikimedia.org/wikipedia/commons/thumb/8/89/B%C3%A9zier_3_big.svg/1280px-B%C3%A9zier_3_big.svg.png

可以看到,上面我们得出结论的 P0−Q0−R0−B\bold{P_0}-\bold{Q_0}-\bold{R_0}-\bold{B} 恰好就是 B\bold{B} 左边的三个点加上 B\bold{B};而 B−R1−Q2−P3\bold{B}-\bold{R_1}-\bold{Q_2}-\bold{P_3} 也恰好是 B\bold{B} 加上 B\bold{B} 右边三个点。

因此,当你一时想不起分割原理的时候,画一张这样的图就可以了(笑)

简单代码实现

这里简单用 JavaScript 实现了一个中点分割,还是看看就好(笑):

function splitB(start = [0, 0], p1 = [0, 0], p2 = [0, 0], p3 = [0, 0]) {

function average(a = [0, 0], b = [0, 0]) {

return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];

}

const m0 = average(start, p1);

const m1 = average(p1, p2);

const m2 = average(p2, p3);

const q0 = average(m0, m1);

const q1 = average(m1, m2);

const b = average(q0, q1);

return [

[start, m0, q0, b],

[b, q1, m2, p3],

];

}

结语

万万没想到的是写这样一篇文章用去了我整整一天的时间。问题的一切是从字体形变开始的,了解贝塞尔曲线只是试图实现这个效果中的第一步罢了。

贝塞尔曲线这方面的问题其实就是纯粹的数学问题,一句话概括就是多项式被离散数学支配的恐惧;而矢量字体这件事情则是被我完全忘记了,直到用 FontForge 看字形之后才反应过来。

分割贝塞尔曲线之后就可以做很多事情了,比如更精细地绘制图形,又或者是其他的各种需求,不过这都是后话了(笑)