贝塞尔曲线的切线及其AABB问题
贝塞尔曲线的切线及其AABB问题
先聊点别的
2023 年抖音上居然还看到很多前端培训
各种直播前端教学(虽然是录播)但看起来还是有大批前往前端卷啊
说明了什么,很可能说明其它行业更难卷
这不是行业不景气业务下降了么..
互联网行业是肉眼可见的不景气
业务量也下降了,业务相关的工作也变的不再饱和
我这 80 后的工作积极性降低了,啊..开始摆烂了
我还好赶上了前端发展的草莽时期,否则估计也进不了这个行档
怎么形容我自己呢,对编程是又菜又爱
可以将时间花一部分在自己感兴趣的内容上了,想到哪里干到哪里
而最近在翻译文章时贝塞尔曲线又回顾了一下
这让我想起了 2020 年遇到过的一个技术问题:曲线的 AABB
(另外还想到了两个其它问题也需要攻克一下, 怎么莫名的想到了鲁讯先生的朝花夕拾.. 果然是年纪大了..)
AABB 即图形界中常说的 AABB (axis-aligned bounding box) 包围盒, 严格来说是未能实现 BB
规则的图形很容易通过顶点距离就可以计算出 BB, 但像贝塞尔曲线这样的曲线就不太好算
是时候解决一下了
要实现的效果



2020 年写微信小程序
在当年写了个简单绘图库
那是在 2020 年上一家公司,公司安排我负责微信小程序的开发
其中经常要用微信小程序生成海报,保存在图片用于在手机上的传播
单独手工去拼接生成海报还是比较麻烦的, Canvas 提供的 api 相对比较低级,
当时看到一些人开源出来的类似 json 配置形式生成海报图
这种配置类型的实现原理是大多是通过配置的坐标,大小,颜色,以及一些简单的 CSS 样式解析后在 canvas 上绘制
相对于纯手工去画,确实简单很多
但我更喜欢 Pixi.js、EaseJs 这类图形库的风格
当时就抽空写了一个简易的 Canvas 操作库 DuduCanvas
DuduCanvas 基本封装实现了图片,文本,形状等相关对象的绘制
调用的方式相比于配置要稍低级一点,拥有更大的自由度,例如添加一个圆形的头像图片:
const avatar = new Image({
image: loader.get('avatar'),
width: 100,
height: 100,
})
// 将头像变成圆形
avatar.borderRadius = '100%'
// 添加一个文本
const t1 = new Text()
t1.text = '你好世界Hello'
t1.color = 'red'
t1.x = 100
t1.y = 300
// 添加到舞台
stage.addChild(img, t1)
至少对于当时的项目来讲,DuduCanvas 运行的还不错,毕竟不是用它做动画或者游戏


还好,我代码存到了 github 上,在新公司临时做项目时还派上了用场用它画了个积分统计图

但它有几个缺点:
没有实现事件系统,当然它大部分时间只是用于生成海报,用不到事件交互
绘制曲线图形后的 BB 未能实现,需要自己手动指定
由于是 2020 年 当时微信小程序的 Canvas 2D 版本还牌测试版,所以使用的旧版 Canvas API
graphics 实现过于简单好多重复命令未去除
未能实现曲线的宽高计算(BB)
没过多久离职了,工作重心也从小程序转到其它前端项目
之后就没再管它
https://github.com/willian12345/DuduCanvas
2023 年我尝试着用微信开发者工具打开看了一下,还能运行
三阶贝塞尔曲线的 BB
之前在翻译 贝塞尔曲线文字路径 一文中提到过三阶贝塞尔曲线
它是用 C# 伪代码来讲解的
定义 4 个控制点:
(x1, y1), (x2, y2), (x3, y3), (x4,y4)
定义 A..H 系数
A = x3 - 3 * x2 + 3 * x1 - x0
B = 3 * x2 - 6 * x1 + 3 * x0
C = 3 * x1 - 3 * x0
D = x0
E = y3 - 3 * y2 + 3 * y1 - y0
F = 3 * y2 - 6 * y1 + 3 * y0
G = 3 * y1 - 3 * y0
H = y0
得到多项式:
x = At3 + Bt2 + Ct + D
y = Et3 + Ft2 + Gt + H
那么我们先用 Javascript 实现一下那篇文章中提到过的垂直于曲线的单位向量
假设我们要绘制的三阶贝塞尔曲线的四个控制点
[
{ x: 120, y: 320 },
{ x: 135, y: 440 },
{ x: 320, y: 280 },
{ x: 480, y: 340 },
];
下面是它三阶贝塞尔曲线采样点,t 取值 0-1 :
// 用 t 获取“样条曲线” 采样点
let sx = A * Math.pow(t, 3) + B * Math.pow(t, 2) + C * t + D
let sy = E * Math.pow(t, 3) + F * Math.pow(t, 2) + G * t + H
sx, sy 就是 t 从 0 - 1 时算出的曲线上的每个点
如果 t 取值足够小,那么在 canvas 上画出所有的点它就是一条贝塞尔曲线
t 间隔为 0.1 时:

t 间隔为 0.001 时:

画出垂直于曲线的向量关键, 在于对三阶贝塞尔曲线多项式的求导
如果你忘记了什么是求导(导函数), 没关系, 直接用公式就完了
我这个学渣都会用,你肯定也可以,
当然最好是回去复习一下高中后期的导函数部分,有助于理解曲线切线的几何意义
求导后得到向量:
// 求导前
x = At3 + Bt2 + Ct + D
y = Et3 + Ft2 + Gt + H
// 求导后
Vx = 3At2 + 2Bt + C
Vy = 3Et2 + 2Ft + G
用 Javascript 实现如下:
// (求导)用于计算曲线上采样点的切线向量
let tx = 3 * A * Math.pow(t, 2) + 2 * B * t + C
let ty = 3 * E * Math.pow(t, 2) + 2 * F * t + G
// 旋转 90 度或 270 度垂直于曲线采样点
let px = ty
let py = -tx
// 缩至单位向量
let magnitude = Math.sqrt(px * px + py * py)
px = px / magnitude
py = py / magnitude
// 为了向量可见,扩大 20 个单位
px *= 20;
py *= 20;
// 从采样点连接至切线向量偏移位置
console.log(sx + px, sy + py);

源码尽量平铺直叙:...
https://github.com/willian12345/blogpost/blob/main/curve/bezier/cubic-bezier-tangent-test.html
如果你对贝塞尔曲线感兴趣还可以看一下我翻译的《曲线编程艺术》的 贝塞尔曲线 这一章
把三阶贝塞尔曲线包起来
要实现三阶贝塞尔曲线的AABB(包围合)还是得从切线入手
比如像下面这个曲线
let points = [
{x: 120, y: 160 },
{x: 35, y: 200 },
{x: 220, y: 260 },
{x: 180, y: 40 },
];
四个点得出的结果:

先把它的四个点用直线连接画出来
ctx.beginPath();
ctx.lineWidth = 2;
ctx.setLineDash([1, 2]);
ctx.strokeStyle = '#076c75';
ctx.moveTo(points[0].x, points[0].y);
ctx.lineTo(points[1].x, points[1].y);
ctx.stroke()
ctx.beginPath();
ctx.lineWidth = 1;
ctx.moveTo(points[1].x, points[1].y);
ctx.strokeStyle = 'black';
ctx.lineTo(points[2].x, points[2].y);
ctx.stroke()
ctx.beginPath();
ctx.lineWidth = 2;
ctx.strokeStyle = '#076c75';
ctx.moveTo(points[2].x, points[2].y);
ctx.lineTo(points[3].x, points[3].y);
ctx.stroke();

蓝色的线就像是控制手柄
点 points[1] 和 points[2] 分别就是控制手柄
控制手柄就是 PS 内的钢笔工具用过吧?就是这个,长短与位置调节就控制了曲线的形状
BB 包围盒就是找到曲线所有转折点中最小和最大的转折点
找转折点,可理解为找到曲线上的斜率
还是从公式入手
在上一节中贝塞尔公式系数直接把 x, y 都用 A..H 表示出来了
这次先简化到一维比如 x , 系数用 A..D 表示
x 坐标方程即(y 轴坐标方程其实是一样的,只是算了两遍):
x = A (1-t)^3 +3 B t (1-t)^2 + 3 C t^2 (1-t) + D t^3
对其求导,关于 t 的微分,得到微分方程
dx/dt = 3 (B - A) (1-t)^2 + 6 (C - B) (1-t) t + 3 (D - C) t^2
= [3 (D - C) - 6 (C - B) + 3 (B - A)] t^2
+ [ -6 (B - A) - 6 (C - B)] t
+ 3 (B - A)
= (3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
合并整理后是一个二次函数:
dx/dt = (3 D - 9 C + 9 B - 3 A) t^2 + (6 A - 12 B + 6 C) t + 3 (B - A)
用其 a, b, c 简化系数代替后:
dx/dt = a t^2 + b t + c
我们要解决的是 dx/dt = 0
"斜率为 0 可能意味着曲线在该点处有一个极小值或极大值,或者曲线在该点处是一个水平切线"
反正我这个学渣是这么理解的
那么就是对二交方程求解
a t^2 + b t + c = 0
可用求根公式
- b +/- sqrt(b^2-4 a c)
-----------------------
2 a
解方程可得 两个解(根) t0, t1, 无解,或 1 个解
这就有了四个点的极值,起点,终点,和两个解
系数 a, b, c 就是根据公式代入, 比如 x 的坐标代入后:
let a = 3 * points[3].x - 9 * points[2].x + 9 * points[1].x - 3 * points[0].x;
let b = 6 * points[0].x - 12 * points[1].x + 6 * points[2].x;
let c = 3 * points[1].x - 3 * points[0].x;
还记得初中数学如何判断二次函数有几个根吧?
delta 即 b^2-4ac 判断 大于等于 0 即为有解
let delta = b * b - 4 * a * c;
判断有解后找到局部极限值 (local extreme)
代入求根公式:
t1 = (-b + Math.sqrt(delta)) / (2 * a);
t2 = (-b - Math.sqrt(delta)) / (2 * a);
我们只关心 0 <= t <= 1 的情况
将得到和 t1, t2 分别代入贝塞尔曲线公式
x = A (1-t)^3 +3 B t (1-t)^2 + 3 C t^2 (1-t) + D t^3
得到的就是真实的 x 坐标值,
所以需 x 要判断
if (x < xl) xl = x;
if (x > xh) xh = x;
记住是求出的二个根 t1, t2 分别代入判断
它有可能是最大值,也有可能是最小值 记作: xl, xh
对 y 同样进行一模一样的计算,t3, t4 也可以得到一最大值与最小值 记作:yl, yh
将它们从起点 左下,左上,右上,右下,左下终点 的顺序连接起来就是我们要的 BB 包围盒
ctx.moveTo(xl, yl); // 起点,左下
ctx.lineTo(xl, yh); // 左上
ctx.lineTo(xh, yh); // 右上
ctx.lineTo(xh, yl); // 右下
ctx.lineTo(xl, yl); // 终点,左下

如上图,包围盒围起来了,解决了计算贝塞尔曲线宽高计算的问题
画出切线验证
再把曲线的切线画出来,这回我们不画垂直向量,直接画切线
切线向量这道菜已经吃过了..
将 t 步长设为 0.1, 进行曲线采样, 画出绿色的切线
for( let t=0; t <=1; t += 0.1){
// 绘制起点移动到对应的曲线点上
const sx = calcBezierByT(pointXArray, t);
const sy = calcBezierByT(pointYArray, t);
ctx.moveTo(sx, sy)
// a t^2 + b t + c
// 切线向量
let vx = a1 * Math.pow(t,2) + b1 * t + c1
let vy = a2 * Math.pow(t,2) + b2 * t + c2
// 缩至单位向量
let magnitude = Math.sqrt(vx * vx + vy * vy)
// vx = -vx / magnitude;
// vy = -vy / magnitude;
vx = vx / magnitude;
vy = vy / magnitude;
// 向量长度变长 30 个单位
vx *= 30
vy *= 30
ctx.strokeStyle = 'green';
ctx.lineTo(sx + vx, sy + vy);
}
ctx.stroke();
}

(绿色颜色有点儿淡了感觉...)
代入上一节算出的 t1, t2, t3, t4 用红色画出局部极限值 (local extreme) 验证
注意 曲线不同,t1, t2, t3, t4 的值有可能有,有可能没有,且我们需要的是 t1 >= 0
需要这样处理
// 过滤
const tArray = [t1, t2, t3, t4].filter((t)=> t >= 0);
for( let i=0; i <= tArray.length; i++){
...与上面生成切线一样,只是 t 值是从 tArray 获取,而不是 0.1 步长
}

可以看到,红色标出的果然很 “极限”
代入不同的坐标值看看
const points = [
{ x: 20, y: 340 },
{ x: 50, y: 400 },
{ x: 320, y: 180 },
{ x: 480, y: 340 },
];

const points = [
{x: 13, y: 224 },
{x: 150, y: 100 },
{x: 251, y: 93 },
{x: 341, y: 224 },
];

(绿色颜色快看不出来了,PC上的微信截图工具会模糊截图...)
可以看到,有些曲线极限值就不一定有四个
https://github.com/willian12345/blogpost/blob/main/curve/bezier/aabb.html
后续
贝塞尔曲线虽然原理很简单,但深入后就会特别复杂,你们好好深入,反正以我的能力是深入不了的
作为一个打工人,就要有打工人的觉悟,主打一个随意,没必要在一个问题上死磕
东看看,西看看,说不定回头再来看问题,已具备足够的知识与资料后就解决了
创业公司麻,就是这么的不稳定,何况是在这样一个环境下
最近公司要让我重新再接触 unity ,这又绕回来了, c# 其实挺好的
参考资料:
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/bezierCurveTo
https://floris.briolas.nl/floris/2009/10/bounding-box-of-cubic-bezier/
https://stackoverflow.com/questions/24809978/calculating-the-bounding-box-of-cubic-bezier-curve
https://pomax.github.io/bezierinfo/#boundingbox
博客园: http://cnblogs.com/willian/
github: https://github.com/willian12345/
贝塞尔曲线的切线及其AABB问题的更多相关文章
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发"鼻涕"下拉粘连效果
前言 接着上一期Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件 的博客开始,同样,在开始前我们先来看一下目标效果: 下面上一下本章需要实现的效果图: 大家看到这个效果 ...
- 【Unity】贝塞尔曲线关于点、长度、切线计算在 Unity中的C#实现
原文:[Unity]贝塞尔曲线关于点.长度.切线计算在 Unity中的C#实现 写在前面 最近给项目做了个路径编辑,基本思路是满足几个基本需求: [额外说明]其实本篇和这个没关系,可以跳过" ...
- iOS开发 贝塞尔曲线
iOS开发 贝塞尔曲线UIBezierPath - 陌云 时间 2014-03-14 11:04:00 博客园-所有随笔区 原文 http://www.cnblogs.com/moyunmo/p/ ...
- iOS开发 贝塞尔曲线UIBezierPath
最近项目中需要用到用贝塞尔曲线去绘制路径 ,然后往路径里面填充图片,找到这篇文章挺好,记录下来 自己学习! 转至 http://blog.csdn.net/guo_hongjun1611/articl ...
- html5 canvas贝塞尔曲线篇(下)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- JS模拟CSS3动画-贝塞尔曲线
一.什么是贝塞尔曲线 1962年,法国工程师皮埃尔·贝塞尔(Pierre Bézier),贝塞尔曲线来为为解决汽车的主体的设计问题而发明了贝塞尔曲线.如今,贝赛尔曲线是计算机图形学中相当重要的一种曲线 ...
- AI: 字体设计中的贝塞尔曲线
http://www.xueui.cn/tutorials/illustrator-tutorials/designers-must-know-the-secret-of-the-bezier-cur ...
- 3阶(次)贝塞尔曲线的JavaScript(JS)实现
php中文网数学符号的显示太差了,推荐看这里 贝塞尔曲线简介:贝塞尔曲线,是贝塞尔老爷子在使用电子计算机设计汽车零件的时候 进行曲面设计而采用的一种参数化的样条曲线. 一般参数方程: B(t) = \ ...
- canvas贝塞尔曲线
贝塞尔曲线 Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线. 曲线定义:起始点.终止点.控制点.通过调整控制点,贝塞尔曲线的形状会发生变化. 1962年,法国数学家Pierr ...
- 贝塞尔曲线(UIBezierPath)属性、方法汇总
UIBezierPath主要用来绘制矢量图形,它是基于Core Graphics对CGPathRef数据类型和path绘图属性的一个封装,所以是需要图形上下文的(CGContextRef),所以一般U ...
随机推荐
- 【LeetCode滑动窗口专题#2】无重复字符的最长子串
#1传送门 无重复字符的最长子串 给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度. 示例 1: 输入: s = "abcabcbb" 输出: 3 解释: 因为 ...
- 2023-06-15:说一说Redis的Key和Value的数据结构组织?
2023-06-15:说一说Redis的Key和Value的数据结构组织? 答案2023-06-15: 全局哈希表 Redis使用哈希表作为保存键值对的数据结构,通过哈希函数将Key映射为哈希表中的一 ...
- 使用React和Redux进行前端应用程序:现代Web应用程序框架
目录 标题:<27. 使用 React 和 Redux 进行前端应用程序:现代 Web 应用程序框架> 背景介绍: 随着现代 Web 应用程序的发展,前端开发人员需要一种高效的.灵活的框架 ...
- 2 opencv-python核心库模块core
core模块定义了opencv中的基础数据结构和基础运算,是整个库的核心模块.而mat数据结构是opencv中最重要的数据结构,是opencv中图像最常用的存储格式. 1 基本数据结构 opencv的 ...
- 脱发秘籍:前端Chrome调试技巧汇总
Chrome浏览器调试工具的核心功能: 注:本文测试.截图均为Edge浏览器(内核是Chromium),浏览器内核可了解<有哪些浏览器/内核?> 00.基础操作汇总 操作类型 快捷键/说明 ...
- 【HDC.Cloud 2023】华为云区块链分论坛内容值得再读!
摘要:在Web3时代,基础设施不仅仅是传统意义上的服务器.网络等,还包括了区块链节点.智能合约等,这些基础设施的稳定性和可信度直接影响着Web3的发展. 本文分享自华为云社区<[HDC.Clou ...
- linux中利用crontab设置定时任务
linux中利用crontab设置定时任务: # 每隔1个小时运行一次命令,四个*分别代表分,时,日,月,周 # crontab -l查看:crontab -e编辑 */1 * * * * /root ...
- Centos7 安装部署 Kubernetes(k8s) 高可用集群
目录 一.系统环境 二.前言 三.Kubernetes(k8s)高可用简介 四.配置机器基本环境 五.部署haproxy负载均衡器 六.部署etcd集群 七.部署Kubernetes(k8s) mas ...
- 利用Abp过滤器实现业务数据“回收站”功能
@ 目录 原理 创建过滤器 使用过滤器 查询 删除 恢复 原理 回收站是当用户删除一条记录时,不是直接从数据库中删除,而是将其放入"回收站",以便用户可以在需要时恢复数据. 在Ab ...
- 运维自动化工具--Ansible
运维自动化工具Ansible 1. ansible安装 rocky安装 需要先安装 enel源 # yum install -y epel-release 然后再安装ansible # yum ins ...