💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
[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 - quadratic​CurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/quadraticCurveTo) [MDN - bezierCurveTo](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/bezierCurveTo)