从贝塞尔曲线的计算感受数学建模的魅力

内容列表

最近在做前端可视化相关的东西,在完成动画效果时,遇到一个不是很好处理的问题,需要让一个元素在画布上以曲线的轨迹进行运动。因为动画这块之前基本也没有怎么接触过,做的也都是简单的线性动画效果,所以碰到这个需求点的时候觉得是有点难度的。

其实,要真的实现按照一定曲线轨迹运动的效果倒也不难,毕竟圆、椭圆方程在平时做布局计算的时候用的也挺多的。但是,用圆或者椭圆计算曲线相当于是找了个特殊场景,不具备通用性;另一方面,说到曲线的绘制,贝塞尔曲线是绕不开的,这也是非常值得考虑的方案。

动画帧计算

一段动画实际上是由多个静态帧组成的,当帧率达到人眼不可分辨的程度时(比如 60 FPS),就感觉像是一个无缝连续的视频在流畅的播放。而某一静态帧的状态用数学公式来表达如下:

y = F(t) (0 <= t <= 1)

那么对于一个物体从 x0 运动到 x1,如何计算 t 时刻的位置?按照我的思路来看,可以转化为以下数学公式:

F(t) = (x1 - x0)*t + x0 (0 <= t <= 1)

这么算其实是没错的,但这个公式在数学建模的角度来看,其实是不好的,后面以计算贝塞尔曲线上一点的坐标为例解释为什么。这里先给出线性贝塞尔曲线数学公式:

B(t) = (1 - t)P0 + tP1 (0 <= t <= 1)

贝塞尔曲线

贝塞尔曲线(Bézier curve)在工业设计领域是一个非常重要的存在,应用非常广泛,在计算机图形学领域中贝塞尔曲线也有很好的支持,例如 Canvas API 原生就有提供贝塞尔曲线的绘制接口。

CanvasRenderingContext2D.bezierCurveTo()

贝塞尔曲线从概念上来看是很难理解的,如何转化为数学公式来计算贝塞尔曲线,而这个过程是什么样的,刚开始理解起来也是比较抽象的。先来看看其(二次贝塞尔曲线)数学定义:

Quadratic Bezier Curve Formula

其中 P0P1P2 分别为起点、控制点、终点。

那么,有了公式计算二次贝塞尔曲线上一点按理来说已经可以实现了,但在这里之所以用贝塞尔曲线这个比较难理解的数学模型来探讨,其实是为了最终得到一个简化的具有普适性的解决动画帧计算的数学模型。对于熟悉动画计算的人来说,很多文档中对于开头提出的问题给出的数学公式为以下:

F(t) = (1 - t)*x0 + t*x1 (0 <= t <= 1)

这个时候,你会发现无论是以上公式,还是一次、二次或更高次的贝塞尔曲线公式中都有一个类似的元素即 1 - t。当然,公式都是相互推导以不同形式展现的,即:

F(t) = (1 - t)*x0 + t*x1 = (x1 - x0)*t + x0 (0 <= t <= 1)

公式相等是本质,但不同的展现形式蕴含的思维模式不同(或者说有没有利用好数学建模来进行问题的抽象)。以上面的公式来分析,前者表达的是 x0x1 分别在 t 时刻状态的叠加,后者则表达的是起始状态 x0 叠加从 x0 运动到 x1 过程中 t 时刻的状态。从其蕴含的思维模式来分析,前者关注的是结果,后者则先分析过程再得到结果。

其实,说到这里,我觉得一个好的数学建模思维的魅力已经体现出来了,动画帧计算应尽可能的简单且关注核心问题,不要被过程所迷惑,这也是为何很多文档中的公式包含 1 - t 元素的原因。

计算二次贝塞尔曲线上一点的坐标

接下来,结合 wiki 中构建贝塞尔曲线一节的动态图 对二次贝塞尔曲线的形成过程有个直观的理解,利用以上思路对计算二次贝塞尔曲线上一点的坐标这个问题进行数学建模:

给出 P0P1P2 分别为起点、控制点、终点,曲线上的点是控制点由 P0 运动到 P1 过程中点 P01 与终点由 P1 运动到 P2 过程中点 P12 连线上的点,转为数学公式如下:

P01 = (1 - t)P0 + tP1
P12 = (1 - t)P1 + tP2
P012 = (1 - t)P01 + tP12

这样就得到了 t 时刻曲线上的点坐标为 P012,根据推导 P012 其实就等于前面给出的二次贝塞尔曲线的公式 B(t) 。按照这个思路和数学建模的思维,计算三次、四次贝塞尔曲线上点的坐标就很简单,不断叠加 t 时刻的状态即可。

结语

总结成一句话来说,用数学建模的思维把复杂问题高度抽象成简单问题,再用简单的方案去解决复杂的问题。对于动画帧计算来说,就要把问题高度抽象成起始状态与终止状态在 t 时刻状态的叠加,不应该关注过程,最终就可以得出线性轨迹和曲线轨迹的计算本质上都是一个线性插值的过程,无论多么复杂的轨迹问题都是要用线性插值的方案来解决。

参考

相关

Web 前端性能优化:核心概念与指标

说到 Web 的性能优化,可能很多时候我们采取了一些措施,看到了肉眼可见的改进,如果我们可以对其进行测量,确定我们改进的效率(百分比)是不是会更有意义?换句话说,如果我们了解性能瓶颈可能发生的位置,衡量用户体验好坏的指标,做到实时追踪性能变化,我们是不是可以更迅速的采取优化措施?在应用上线前,我们就可以做一些低成本而有高收益的优化工作,进一步提升用户体验。

了解更多

Web 应用:轻量级状态管理工具 zustand

基于 React.js 的 Web 应用如何完成状态管理?社区主流方案是 react-redux,其本质上基于 React 的 Context 特性实现,如果应用足够简单,实际上用 Context 手写一个简单的状态管理工具倒也并不难。不过,考虑到工具的完善性、项目的健壮性,通常采用较好的、成熟的社区方案。在移动端场景下,react-redux 略显臃肿,轻量级状态管理工具 zustand 倒是一个不错的替代方案。

了解更多

Child process API: spawn vs exec

利用 Node.js 编写一些命令行工具、一次性脚本是很方便的,而在这类场景下 child_process API 的 spawnexec 方法的应用则非常常见。在我使用它们时,却不知道该如何进行选择,遂对此进行了探究。

了解更多