[TOC]
# 什么是贝塞尔曲线
贝塞尔曲线于 1962 年,由法国工程师皮埃尔·贝济埃(Pierre Bézier)所广泛发表,他运用贝塞尔曲线来为汽车的主体进行设计。
<br>
贝塞尔曲线主要用于二维图形应用程序中的数学曲线,曲线由起始点,终止点(也称锚点)和控制点组成,通过调整控制点,通过一定方式绘制的贝塞尔曲线形状会发生变化。后面会具体介绍绘制的方法。
<br>
在计算机图形学中贝赛尔曲线的运用很广泛,例如Photoshop中的钢笔效果,Flash5的贝塞尔曲线工具,在软件GUI开发中一般也会提供对应的方法来实现贝赛尔曲线,我们熟知的CSS动画过渡时间函数也是通过贝塞尔曲线(三阶贝塞尔曲线)获取的。
<br>
<br>
# 贝塞尔曲线分为哪些类型?
贝塞尔曲线根据**控制点**的数量分为:
* 一阶贝塞尔曲线(2 个控制点)
* 二阶贝塞尔曲线(3 个控制点)
* 三阶贝塞尔曲线(4 个控制点)
* n阶贝塞尔曲线(n+1 个控制点)
<br>
<br>
# 贝塞尔曲线是如何绘制出来的
下面以二街贝塞尔曲线为例。
在平面内任选 3 个不共线的点,依次用线段连接
![](https://box.kancloud.cn/089a8f8bf9d7aa7b53c316fc117a25d8_600x400.png)
<br>
在第一条线段上任选一个点 D。计算该点到线段起点的距离 AD,与该线段总长 AB 的比例。
![](https://box.kancloud.cn/cd107c07168b5fd14837f27c1b3bf55c_600x400.png)
<br>
根据上一步得到的比例,从第二条线段上找出对应的点 E,使得`AD:AB = BE:BC`。
![](https://box.kancloud.cn/0229cf90024260cdc0bd934d9188889c_600x400.png)
<br>
连接这两点 DE。
![](https://box.kancloud.cn/76cd205a6c5be54d8986ace4d9a79c46_600x400.png)
<br>
从新的线段 DE 上再次找出相同比例的点 F,使得`DF:DE = AD:AB = BE:BC`。
![](https://box.kancloud.cn/e14ef5104012aee14b09da3bf68040a4_600x400.png)
<br>
到这里,我们就确定了贝塞尔曲线上的一个点 F。接下来,请稍微回想一下中学所学的极限知识,让选取的点 D 在第一条线段上从起点 A 移动到终点 B,找出所有的贝塞尔曲线上的点 F。所有的点找出来之后,我们也得到了这条贝塞尔曲线。
![](https://box.kancloud.cn/9e0cb96e47a0978d16aa5e38403cc35f_600x400.png)
<br>
绘制过程如图
![](https://box.kancloud.cn/b37019efc7a089bd7681c36f4c518e45_360x150.gif)
<br>
当控制点个数为 4 时,步骤都是相同的,只不过我们每确定一个贝塞尔曲线上的点,要进行三轮取点操作。如图,`AE:AB = BF:BC = CG:CD = EH:EF = FI:FG = HJ:HI`,其中点 J 就是最终得到的贝塞尔曲线上的一个点。
![](https://box.kancloud.cn/9c924e0683dea75b1d489b916d17a0d3_600x400.png)
<br>
这样我们得到的是一条三次贝塞尔曲线。
![](https://box.kancloud.cn/a3b3abdd71def18b60bedc1d2a153034_600x400.png)
<br>
<br>
# 如何求贝塞尔曲线上的点坐标?
## 一阶贝塞尔曲线
![](https://box.kancloud.cn/99302ca3c0b218c37bb6c305f9e6c5c8_240x100.gif)
对于一阶贝塞尔曲线,我们可以通过几何知识,很容易根据 t 的值得出线段上那个点的坐标:
![](https://box.kancloud.cn/52954c8e5f037d4acd1de92a8e4d15ee_221x26.png)
<br>
然后可以得出:
![](https://box.kancloud.cn/d49162c4700ef6dfd4d71078f357e420_300x26.png)
<br>
## 二阶贝塞尔曲线
![](https://box.kancloud.cn/21c0261c945a079446bd129622bac9fb_240x100.gif)
对于二阶贝塞尔曲线,其实你可以理解为:在`P0P1`上利用一阶公式求出点`P0'`,然后在`P1P2`上利用一阶公式求出点`P1'`,最后在`P0'P1'`上再利用一阶公式就可以求出最终贝塞尔曲线上的点`P0''`。具体推导过程如下:
> 先求出线段上的控制点。
![](https://box.kancloud.cn/6a91439e03a025429e07035337e7aca6_190x30.png)
![](https://box.kancloud.cn/42096ea837fa42d044c6db97955604ad_190x30.png)
<br>
> 将上面的公式带入至下列公式中:
![](https://box.kancloud.cn/946062ad4e67cbc2636f1979ea283da5_217x30.png)
![](https://box.kancloud.cn/d500e35bfbf2bb7601da8ecba7e2551e_425x26.png)
![](https://box.kancloud.cn/785fb5e5835c21a9cc83514ee8845b9a_306x29.png)
<br>
> 得出以下公式:
![](https://box.kancloud.cn/a3d819414510e2f311554fdc3969e9ca_446x29.png)
<br>
## 三阶贝塞尔曲线
![](https://box.kancloud.cn/4ed064f5183fcfa327be33d9336232a1_240x100.gif)
将 `P0P1P2 `与 `P1P2P3` 看成是2个2阶段贝塞尔曲线,可根据上面分别求得坐标点公式:
![](https://box.kancloud.cn/795ec4915249f0f48f648d8639b190fa_768x69.png)
![](https://box.kancloud.cn/a16fb6fbea8e744855d654f8dfaf97c4_770x69.png)
<br>
将 p0''与p1''连接,根据之前的比例找到点P0''',根据一阶贝塞尔曲线,可得到公式:
![](https://box.kancloud.cn/cb24265e582f0f2659882dc4ed4df4e1_594x62.png)
<br>
分别将P0''和P1''代入以上公式,可得:
![](https://box.kancloud.cn/258b32aac8e710b662ef24ceb9693e7f_591x29.png)
<br>
## 多阶贝塞尔曲线
![](https://box.kancloud.cn/a141d309731f3a70b27315afec616ba0_334x62.png)
即:
![](https://box.kancloud.cn/7cd169ffffd49b53fe2ee9870d75d24e_259x62.png)
<br>
<br>
# 动态绘制贝塞尔曲线
详细见 [CodePen](https://codepen.io/surahe/pen/VNoOpd)
~~~
/**
* 遍历 points,画出对应的点并用线连起来
* @param {Array} points
*/
function drawCubicBezierCurzeHelper(points) {
ctx.save()
// 遍历 points 的坐标,画线
points.reduce((p, c) => {
ctx.strokeStyle = "gray"
drawLine(...p, ...c)
return c
})
// 遍历 points 的坐标,画红点
points.forEach((c) => {
ctx.beginPath()
ctx.fillStyle = 'red';
ctx.arc(...c, 20, 0, 2 * Math.PI);
ctx.fill()
})
ctx.restore()
}
/**
* 逐个画出贝塞尔曲线上的点
*/
function animation2() {
if (t >= 1) {
// 初始点动画执行完毕标志
isEnded = true;
return
}
// 获取当前 t 的点坐标
let initPoint2 = drawPoints(points)
ctx.strokeStyle = "blue"
// 以初始点和当前点画线
drawLine(...initPoint, ...initPoint2)
// 将当前点设置为初始点
initPoint = initPoint2
// 应该是使用缓存优化性能
cache = canvas.toDataURL("image/jpeg", 1);
let img = new Image();
img.src = cache
img.onload = function () {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 递归执行
window.requestAnimationFrame(() => {
t += 0.1
animation2()
})
}
}
~~~
<br>
<br>
# SVG 与 贝塞尔曲线
path的标签,可以绘制任意的路径,包括贝塞尔曲线。
<br>
三次贝塞尔曲线指令:`C x1 y1, x2 y2, x y`两个控制点`(x1,y1)`和(`x2,y2)`,`(x,y)`代表曲线的终点。字母`C`表示特定动作与行为,这里需要大写,表示标准三次方贝塞尔曲线。
<br>
下面一些描述贝塞尔曲线的代码(片段),其中字母M表示特定动作moveTo, 指将绘图的起点移动到此处。
~~~
<svg width="190px" height="160px">
<path d="M10 10 C 20 20, 40 20, 50 10" stroke="3" fill="none"/>
<path d="M70 10 C 70 20, 120 20, 120 10" stroke="3" fill="none"/>
<path d="M130 10 C 120 20, 180 20, 170 10" stroke="3" fill="none"/>
<path d="M10 60 C 20 80, 40 80, 50 60" stroke="3" fill="none"/>
<path d="M70 60 C 70 80, 110 80, 110 60" stroke="3" fill="none"/>
<path d="M130 60 C 120 80, 180 80, 170 60" stroke="3" fill="none"/>
<path d="M10 110 C 20 140, 40 140, 50 110" stroke="3" fill="none"/>
<path d="M70 110 C 70 140, 110 140, 110 110" stroke="3" fill="none"/>
<path d="M130 110 C 120 140, 180 140, 170 110" stroke="3" fill="none"/>
</svg>
~~~
<br>
曲线效果类似下面这张图:
![](https://box.kancloud.cn/2e6154086ca5c51409f5f446074cc15e_226x191.png)
可以看到`M`后面的起点,加`C`后面3个点,构成了贝赛尔曲线的`4`个点。
<br>
<br>
# Canvas 与贝塞尔曲线
## 二次方贝塞尔曲线
`CanvasRenderingContext2D.quadraticCurveTo()` 是 Canvas 2D API 新增二次贝塞尔曲线路径的方法。它需要2个点。 第一个点是控制点,第二个点是终点。 起始点是当前路径最新的点,当创建二次贝赛尔曲线之前,可以使用 `moveTo()` 方法进行改变。
<br>
语法
~~~
ctx.quadraticCurveTo(cpx, cpy, x, y);
~~~
参数
* `cpx`:控制点的 x 轴坐标。
* `cpy`:控制点的 y 轴坐标。
* `x`:终点的 x 轴坐标。
* `y`:终点的 y 轴坐标。
~~~js
// 二次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(50, 20);
ctx.quadraticCurveTo(230, 30, 50, 100);
ctx.stroke();
// 画起点和终点
...
// 画控制点
...
~~~
![](https://box.kancloud.cn/8e1098b10399f58030564b207d5f9709_301x202.png)
<br>
## 三次贝塞尔曲线
`CanvasRenderingContext2D.bezierCurveTo()` 是 Canvas 2D API 绘制三次贝赛尔曲线路径的方法。 该方法需要三个点。 第一、第二个点是控制点,第三个点是结束点。起始点是当前路径的最后一个点,绘制贝赛尔曲线前,可以通过调用 `moveTo()` 进行修改。
<br>
语法
~~~
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);
~~~
参数
* `cp1x`:第一个控制点的 x 轴坐标。
* `cp1y`:第一个控制点的 y 轴坐标。
* `cp2x`:第二个控制点的 x 轴坐标。
* `cp2y`:第二个控制点的 y 轴坐标。
* `x`:结束点的 x 轴坐标。
* `y`:结束点的 y 轴坐标。
~~~js
// 定义点 {x, y}
let start = { x: 50, y: 20 };
let cp1 = { x: 230, y: 30 };
let cp2 = { x: 150, y: 80 };
let end = { x: 250, y: 100 };
// 画三次贝塞尔曲线
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
ctx.stroke();
// 画起点和终点
...
// 画控制点
...
~~~
![](https://box.kancloud.cn/c8f388fc2617f920a0e50be3435ee0a8_369x205.png)
<br>
<br>
# CSS3动画与贝塞尔曲线
## timing-function
` timing-function` CSS 数据类型表示一个数学函数,它描述了在一个过渡或动画中一维数值的改变速度。这实质上让你可以自己定义一个加速度曲线,以便动画的速度在动画的过程中可以进行改变。这些函数通常被称为缓动函数。
<br>
这是一个表示时间输出比率的函数,表示为`<number>`,0, 0 代表初始状态,1, 1 代表终止状态。
<br>
输出比可以大于1.0(或者小于0.0)。这是因为在一种反弹效果中,动画是可以比最后的状态走的更远的,然后再回到最终状态。
![](https://box.kancloud.cn/89dba05979647dc9fe4d1fe3d776187c_249x332.png)
![](https://box.kancloud.cn/5df835e727688fe212194123ef6d1c30_249x332.png)
<br>
不过,如果输出值超过了它允许的范围,比如组成一个颜色的值大于了255或者小于了0,这个值会被修改为允许范围内的最接近的值(在颜色值这个例子中分别为255和0)。一些贝塞尔曲线展示了这些性质。
<br>
## cubic-bezier属性
cubic-bezier() 定义了一条 立方贝塞尔曲线(cubic Bézier curve)。这些曲线是连续的,一般用于动画的平滑变换,也被称为缓动函数(easing functions)。
<br>
一条立方贝塞尔曲线需要四个点来定义,P0 、P1 、P2 和 P3。P0 和 P3 是起点和终点,这两个点被作为比例固定在坐标系上,横轴为时间比例,纵轴为完成状态。P0 是 (0, 0),表示初始时间和初始状态。P3 是 (1, 1) ,表示终止时间和终止状态。
<br>
并非所有的三次贝塞尔曲线都适合作为计时函数,也并非所有的曲线都是数学函数,即给定横坐标为0或1的曲线。在CSS定义的P0和P3固定的情况下,三次贝塞尔曲线是一个函数,因此,**当且仅当P1和P2的横坐标都在[0,1]范围内**时,三次贝塞尔曲线是有效的。
<br>
在[0,1]范围之外的P1或P2纵坐标的立方贝塞尔曲线可能会产生弹跳效应。
<br>
当您指定无效的cubic-bezier曲线时,CSS会忽略整个属性。
### 语法
~~~
cubic-bezier(x1, y1, x2, y2)
~~~
***x1*,*y1*,*x2*,*y2***
`<number>`值表示横坐标,P1和P2点的纵坐标表示三次贝塞尔曲线。 x1和x2必须在[0,1]范围内,否则该值无效。
### 实例
~~~
cubic-bezier(0.1, 0.7, 1.0, 0.1) The canonical Bézier curve with four <number> in the [0,1] range.
cubic-bezier(0, 0, 1, 1) Using <integer> is valid as any <integer> is also a <number>.
cubic-bezier(-0.2, 0.6, -0.1, 0) Negative values for abscissas are valid, leading to bouncing effects.
cubic-bezier(1.1, 0, 4, 0) Values > 1.0 for abscissas are also valid.
~~~
<br>
## 抛物线运动
将抛物线运动分解为水平和垂直方向。
抛物线运动元素使用至少内外两层标签,例如,本demo抛物线运动物体是CSS世界这本书的缩略图,我们可以外面一层`<div>`,里面是`<img>`图片:
~~~
<div class="fly-item">
<img src="./book.jpg">
</div>
~~~
然后内外两次标签一个负责水平方向的translate移动,一个负责垂直方向的translate移动,然后使用不同的缓动函数,也就是使用不同的`timing-function`,在CSS3`animation`动画效果中是`animation-timing-function`属性,在CSS3`transition`过渡效果中是`transition-timing-function`属性。CSS代码如下:
~~~
.fly-item {
/* 水平移动,线性匀速 */
transition-timing-function: linear;
}
.fly-item > img {
/* 垂直移动,先慢后快 */
transition-timing-function: cubic-bezier(.55,0,.85,.36);
}
~~~
然后同时执行`translate`移动,抛物线效果就出现了。
<br>
<br>
# 工具
http://cubic-bezier.com/
![](https://box.kancloud.cn/cd57302fec2542049b7afda670d94c84_431x422.png)
x轴表示随着时间匀速前进,y轴表示随着时间推移运动距离的百分比,x y的最大值都为 1
<br>
<br>
# 参考资料
[贝塞尔曲线与CSS3动画、SVG和canvas的基情](https://www.zhangxinxu.com/wordpress/2013/08/%E8%B4%9D%E5%A1%9E%E5%B0%94%E6%9B%B2%E7%BA%BF-cubic-bezier-css3%E5%8A%A8%E7%94%BB-svg-canvas/)
[贝塞尔曲线扫盲](http://www.html-js.com/article/1628)
[【干货满满】贝塞尔曲线(Bézier curve)——什么神仙操作](https://juejin.im/post/5be99bf66fb9a049db72a956)
[这回试试使用CSS实现抛物线运动效果](https://www.zhangxinxu.com/wordpress/2018/08/css-css3-%E6%8A%9B%E7%89%A9%E7%BA%BF%E5%8A%A8%E7%94%BB/)
[深入理解贝塞尔曲线](https://juejin.im/post/5b854e1451882542fe28a53d)
[MDN - timing function](https://developer.mozilla.org/zh-CN/docs/Web/CSS/timing-function)
[MDN - quadraticCurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/quadraticCurveTo)
[MDN - bezierCurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/bezierCurveTo)
- 第一部分 HTML
- meta
- meta标签
- HTML5
- 2.1 语义
- 2.2 通信
- 2.3 离线&存储
- 2.4 多媒体
- 2.5 3D,图像&效果
- 2.6 性能&集成
- 2.7 设备访问
- SEO
- Canvas
- 压缩图片
- 制作圆角矩形
- 全局属性
- 第二部分 CSS
- CSS原理
- 层叠上下文(stacking context)
- 外边距合并
- 块状格式化上下文(BFC)
- 盒模型
- important
- 样式继承
- 层叠
- 属性值处理流程
- 分辨率
- 视口
- CSS API
- grid(未完成)
- flex
- 选择器
- 3D
- Matrix
- AT规则
- line-height 和 vertical-align
- CSS技术
- 居中
- 响应式布局
- 兼容性
- 移动端适配方案
- CSS应用
- CSS Modules(未完成)
- 分层
- 面向对象CSS(未完成)
- 布局
- 三列布局
- 单列等宽,其他多列自适应均匀
- 多列等高
- 圣杯布局
- 双飞翼布局
- 瀑布流
- 1px问题
- 适配iPhoneX
- 横屏适配
- 图片模糊问题
- stylelint
- 第三部分 JavaScript
- JavaScript原理
- 内存空间
- 作用域
- 执行上下文栈
- 变量对象
- 作用域链
- this
- 类型转换
- 闭包(未完成)
- 原型、面向对象
- class和extend
- 继承
- new
- DOM
- Event Loop
- 垃圾回收机制
- 内存泄漏
- 数值存储
- 连等赋值
- 基本类型
- 堆栈溢出
- JavaScriptAPI
- document.referrer
- Promise(未完成)
- Object.create
- 遍历对象属性
- 宽度、高度
- performance
- 位运算
- tostring( ) 与 valueOf( )方法
- JavaScript技术
- 错误
- 异常处理
- 存储
- Cookie与Session
- ES6(未完成)
- Babel转码
- let和const命令
- 变量的解构赋值
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 数组的扩展
- 函数的扩展
- 对象的扩展
- Symbol
- Set 和 Map 数据结构
- proxy
- Reflect
- module
- AJAX
- ES5
- 严格模式
- JSON
- 数组方法
- 对象方法
- 函数方法
- 服务端推送(未完成)
- JavaScript应用
- 复杂判断
- 3D 全景图
- 重载
- 上传(未完成)
- 上传方式
- 文件格式
- 渲染大量数据
- 图片裁剪
- 斐波那契数列
- 编码
- 数组去重
- 浅拷贝、深拷贝
- instanceof
- 模拟 new
- 防抖
- 节流
- 数组扁平化
- sleep函数
- 模拟bind
- 柯里化
- 零碎知识点
- 第四部分 进阶
- 计算机原理
- 数据结构(未完成)
- 算法(未完成)
- 排序算法
- 冒泡排序
- 选择排序
- 插入排序
- 快速排序
- 搜索算法
- 动态规划
- 二叉树
- 浏览器
- 浏览器结构
- 浏览器工作原理
- HTML解析
- CSS解析
- 渲染树构建
- 布局(Layout)
- 渲染
- 浏览器输入 URL 后发生了什么
- 跨域
- 缓存机制
- reflow(回流)和repaint(重绘)
- 渲染层合并
- 编译(未完成)
- Babel
- 设计模式(未完成)
- 函数式编程(未完成)
- 正则表达式(未完成)
- 性能
- 性能分析
- 性能指标
- 首屏加载
- 优化
- 浏览器层面
- HTTP层面
- 代码层面
- 构建层面
- 移动端首屏优化
- 服务器层面
- bigpipe
- 构建工具
- Gulp
- webpack
- Webpack概念
- Webpack工具
- Webpack优化
- Webpack原理
- 实现loader
- 实现plugin
- tapable
- Webpack打包后代码
- rollup.js
- parcel
- 模块化
- ESM
- 安全
- XSS
- CSRF
- 点击劫持
- 中间人攻击
- 密码存储
- 测试(未完成)
- 单元测试
- E2E测试
- 框架测试
- 样式回归测试
- 异步测试
- 自动化测试
- PWA
- PWA官网
- web app manifest
- service worker
- app install banners
- 调试PWA
- PWA教程
- 框架
- MVVM原理
- Vue
- Vue 饿了么整理
- 样式
- 技巧
- Vue音乐播放器
- Vue源码
- Virtual Dom
- computed原理
- 数组绑定原理
- 双向绑定
- nextTick
- keep-alive
- 导航守卫
- 组件通信
- React
- Diff 算法
- Fiber 原理
- batchUpdate
- React 生命周期
- Redux
- 动画(未完成)
- 异常监控、收集(未完成)
- 数据采集
- Sentry
- 贝塞尔曲线
- 视频
- 服务端渲染
- 服务端渲染的利与弊
- Vue SSR
- React SSR
- 客户端
- 离线包
- 第五部分 网络
- 五层协议
- TCP
- UDP
- HTTP
- 方法
- 首部
- 状态码
- 持久连接
- TLS
- content-type
- Redirect
- CSP
- 请求流程
- HTTP/2 及 HTTP/3
- CDN
- DNS
- HTTPDNS
- 第六部分 服务端
- Linux
- Linux命令
- 权限
- XAMPP
- Node.js
- 安装
- Node模块化
- 设置环境变量
- Node的event loop
- 进程
- 全局对象
- 异步IO与事件驱动
- 文件系统
- Node错误处理
- koa
- koa-compose
- koa-router
- Nginx
- Nginx配置文件
- 代理服务
- 负载均衡
- 获取用户IP
- 解决跨域
- 适配PC与移动环境
- 简单的访问限制
- 页面内容修改
- 图片处理
- 合并请求
- PM2
- MongoDB
- MySQL
- 常用MySql命令
- 自动化(未完成)
- docker
- 创建CLI
- 持续集成
- 持续交付
- 持续部署
- Jenkins
- 部署与发布
- 远程登录服务器
- 增强服务器安全等级
- 搭建 Nodejs 生产环境
- 配置 Nginx 实现反向代理
- 管理域名解析
- 配置 PM2 一键部署
- 发布上线
- 部署HTTPS
- Node 应用
- 爬虫(未完成)
- 例子
- 反爬虫
- 中间件
- body-parser
- connect-redis
- cookie-parser
- cors
- csurf
- express-session
- helmet
- ioredis
- log4js(未完成)
- uuid
- errorhandler
- nodeclub源码
- app.js
- config.js
- 消息队列
- RPC
- 性能优化
- 第七部分 总结
- Web服务器
- 目录结构
- 依赖
- 功能
- 代码片段
- 整理
- 知识清单、博客
- 项目、组件、库
- Node代码
- 面试必考
- 91算法
- 第八部分 工作代码总结
- 样式代码
- 框架代码
- 组件代码
- 功能代码
- 通用代码