写点什么

用 JavaScript 实现三次贝塞尔动画库 - 前端组件化

用户头像
三钻
关注
发布于: 2021 年 04 月 12 日
用 JavaScript 实现三次贝塞尔动画库 - 前端组件化

这期我们来完善上一期的动画库。在 Animation 类中的 constructor 的参数,我们发现其他的参数都用上了。但是 timingFunction 我们是还没有使用上的。这里我们就来一起处理这个问题。


timingFucntion 这个逻辑主要是用在 Animation 的 run 方法中。如果大家还记得之前的一篇讲解 CSS 动画的文章,里面我们了解到三次贝塞尔曲线。


在三次贝塞尔曲线里面,它的 y 轴就是 progress(进度),而 x 轴就是 time(时间)。一条三次贝塞尔曲线就会从 [0, 0] 坐标到 [1, 1] 坐标移动的轨迹。


我们的 timingFunction 也是这样一个函数,传入我们的时间进度,从而获得一个三次贝塞尔曲线的移动轨迹的进度

TimingFunction

TimingFunction 是一个关于 0 ~ 1 的 time(时间)的函数,通过三次贝塞尔计算返回一个 0~1 的 progress(进度)。


在 CSS 里面我们就有几个库,就比如 linear 这个 timingFunction。那么这里我们也去尝试写一些与三次贝塞尔曲线比较接近的 timingFunction。

Linear

首先我们来实现 Linear 这种动画曲线。这种曲线是相对比较好实现的。它就是一个自身不变的,一比一的 timingFunction。所以代码也是非常的简单:


export let linear = v => v;
复制代码

实现三次贝塞尔函数

linear 种其实没有任何的缓动的效果的,如果我们想实现三次贝塞尔曲线中的 ease、ease-in、ease-out 这样的动画效果的话,我们就需要用到三次贝塞尔曲线中的 “牛顿积分法“ 去求一个时间点的进度值。


所以这里我们需要先实现三次贝塞尔曲线的函数来进行计算。


首先我们可以看看三次贝塞尔曲线的网站:



我们可以看到三次贝塞尔曲线是通过用 4 个参数来进行计算的。而这四个参数就是决定我们动画曲线的效果。


这里我们也不去详细的分析和推论出三次贝塞尔曲线的计算方式了,我们可以直接从 C++ 的库中把这个函数的代码取出来,然后转换这个代码成 JavaScript 然后直接使用即可。


export function cubicBezier(p1x, p1y, p2x, p2y) {  const ZERO_LIMIT = 1e-6;  // Calculate the polynomial coefficients,  // implicit first and last control points are (0,0) and (1,1).  const ax = 3 * p1x - 3 * p2x + 1;  const bx = 3 * p2x - 6 * p1x;  const cx = 3 * p1x;
const ay = 3 * p1y - 3 * p2y + 1; const by = 3 * p2y - 6 * p1y; const cy = 3 * p1y;
function sampleCurveDerivativeX(t) { // `ax t^3 + bx t^2 + cx t` expanded using Horner's rule return (3 * ax * t + 2 * bx) * t + cx; }
function sampleCurveX(t) { return ((ax * t + bx) * t + cx) * t; }
function sampleCurveY(t) { return ((ay * t + by) * t + cy) * t; }
// Given an x value, find a parametric value it came from. function solveCurveX(x) { let t2 = x; let derivative; let x2;
// https://trac.webkit.org/browser/trunk/Source/WebCore/platform/animation // first try a few iterations of Newton's method -- normally very fast. // http://en.wikipedia.org/wikiNewton's_method for (let i = 0; i < 8; i++) { // f(t) - x = 0 x2 = sampleCurveX(t2) - x; if (Math.abs(x2) < ZERO_LIMIT) { return t2; } derivative = sampleCurveDerivativeX(t2); // == 0, failure /* istanbul ignore if */ if (Math.abs(derivative) < ZERO_LIMIT) { break; } t2 -= x2 / derivative; }
// Fall back to the bisection method for reliability. // bisection // http://en.wikipedia.org/wiki/Bisection_method let t1 = 1; /* istanbul ignore next */ let t0 = 0;
/* istanbul ignore next */ t2 = x; /* istanbul ignore next */ while (t1 > t0) { x2 = sampleCurveX(t2) - x; if (Math.abs(x2) < ZERO_LIMIT) { return t2; } if (x2 > 0) { t1 = t2; } else { t0 = t2; } t2 = (t1 + t0) / 2; }
// Failure return t2; }
function solve(x) { return sampleCurveY(solveCurveX(x)); }
return solve;}
复制代码


如果想详细了解这段代码的逻辑和推算过程,可以去了解代码备注中的链接。

实现动画效果库

好,我们有了 cubicBezier 这个函数,我们就可以通过使用它,实现 ease、ease-in、ease-out、ease-in-out 等动画曲线了。


我们可以通过三次贝塞尔曲线的官网,取得每个常用的动画效果的 4 个参数值。然后传入 cubicBezier 这个函数即可获得我们想要的进度值。


每个动画效果的方法实现如下:


Ease


export let ease = cubicBezier(0.25, 0.1, 0.25, 1);
复制代码


Ease In


export let easeIn = cubicBezier(0.42, 0, 1, 1);
复制代码


Ease Out


export let easeOut = cubicBezier(0, 0, 0.58, 1);
复制代码


Ease In Out


export let easeInOut = cubicBezier(0.42, 0, 0.58, 1);
复制代码


最后我们把所有这些动画曲线的函数都放入一个 ease.js 的 JavaScript 文件即可。这样我们 timingFunction 库就完成了。

使用 TimingFunction

有了 timingFunction 的库,我们就可以在 animation-demo.js 中使用这些动画函数来给我们的元素加入动画效果。


在我们使用这个之前,我们 animation.js 中的 timingFunction 和 template 参数都是没有给予默认值的。为了防止不必要的出错,我们可以给他们一个默认值。


我们只需要在 Animation Class 里面加入两行赋予参数默认值的逻辑即可:


constructor(object, property, startValue, endValue, duration, delay, timingFunction, template) {  timingFunction = timingFunction || (v => v);  template = template || (v => v);
this.object = object; this.property = property; this.startValue = startValue; this.endValue = endValue; this.duration = duration; this.timingFunction = timingFunction; this.delay = delay; this.template = template;}
复制代码


接下来我们回到 animation-demo.js,并且通过 ease.js 来引入 ease 动画函数。


import {ease} from './ease.js';
复制代码


然后在 Timeline 添加 Animation 的参数,传入 ease 这个 timingFunction。


tl.add(  new Animation(    document.querySelector('#el').style,    'transform',    0,    500,    2000,    0,    ease,    v => `translate(${v}px)`  ));
复制代码


我们这个 ease 动画函数实现的动画与 CSS 中 transition 的 ease 是否是一样的呢?为了证明我们实现了 CSS 中一样的 ease 动画效果,我们建立多一个 div 并且给它赋予 CSS 的 transition。这样我们就可以直观看到他们之间是否是一样的效果了。


首先在 animation.html 中加入这个 HTML 和 CSS 的代码:


<style>  :root {    --bg-color: #0f0e18;    --sub-bg-color: #2d2f42;    --purple-color: fuchsia;    --blue-color: aqua;  }  body {    background: var(--bg-color);  }  .buttons {    color: var(--purple-color);  }  .purple {    color: var(--purple-color);  }  .blue {    color: var(--blue-color);  }  .box {    width: 100px;    height: 100px;    background-color: aqua;    margin-bottom: 1rem;  }  .ease-box {    background-color: fuchsia !important;  }  button {    background: transparent;    color: var(--blue-color);    border: 1px solid aqua;    padding: 0.5rem 1rem;    cursor: pointer;    transition: all 400ms ease;  }  button:hover {    background: var(--blue-color);    color: #333;  }</style>
<body> <div class="box" id="el"></div> <div class="box ease-box" id="el2"></div> <button id="pause-btn">Pause</button> <button id="resume-btn">Resume</button> <script src="./main.js"></script></body>
复制代码


然后我们回到 animation-demo.js,给 el2 这个元素加入 transition 的属性。


document.querySelector('#el2').style.transition = 'transform 2s ease';document.querySelector('#el2').style.transform = 'translateX(500px)';
复制代码


然后我们来看一下效果:



蓝色的盒子就是我们自己编写的 ease 动画,而紫色的盒子就是 CSS 中的 ease 动画。


我们可以说基本上他们就是一致的,只不过 C++ 和 CSS 中三次贝塞尔曲线的计算有一点的差异导致动画有微小的不一样。但是大致上是一摸一样的。


重点是我们 JavaScript 实现的动画,是可以随时突发暂停,也可以随时触发继续播放的。但是 CSS 的动画是无法达到一样的功能的,它只能一播放就播放到结束。



实现重置

最后我们就去把 Timeline 中的重置功能也给实现了。其实这个功能的逻辑非常简单,它所需要用到的函数我们都有了。


  • 首先需要先暂停这个动画

  • 重置 startTime 开始时间为当前时间

  • 重置 PAUSE_TIME 为 0

  • 重置 PAUSE_START 为 0

  • 重置 ANIMATIONnew Set()

  • 重置 START_TIMESnew Map()

  • 重置 TICK_HANDLER 为 null


reset() {  this.pause();  this[PAUSE_TIME] = 0;  this[PAUSE_START] = 0;  this[ANIMATIONS] = new Set();  this[START_TIMES] = new Map();  this[TICK_HANDLER] = null;}
复制代码


就这样我们就完成了一个比较完善的动画库了。



对时间轴加入状态管理

虽然说我们的动画库和时间线的功能已经是非常完善了。但是其实里面还有一些存在的问题的。比如,我们没有调用 pause 就直接调用了 resume,这有可能会出现一些问题。


所以我们要给这个库安排一个状态管理,让这个类更具有健壮性。

初始化时

首先在 Timeline 实例化的时候,我们注入一个 Initiated 的状态,代表我们 Timeline 是在 初始化完毕 状态了。


constructor() {  this.state = 'Initiated';  this[ANIMATIONS] = new Set();  this[START_TIMES] = new Map();}
复制代码

开始时

在 Timeline 的 start 方法执行的时候,我们就可以判断这个 Timeline 是否被初始化了,如果没有直接断开不继续执行。当然我们这里也可以直接抛一个错误,不过这个根据我们 API 设计风格而定即可。


如果可以执行 Timeline 的启动的话,我们就可以把状态改为 Started。这样我们就让这个 Timeline 变成一个开始之后的状态


start() {    if (!this.state === 'Initiated') return;    this.state = 'Started';    /* ... */}
复制代码

暂停时

暂停状态的判断与开始状态一样,先判断 Timeline 是否已经进入了开始状态,如果没有就直接推出执行。


如果可以执行,就把状态更新为 Paused(已暂停)。


pause() {  if (!this.state === 'Started') return;    this.state = 'Paused';    /* ... */}
复制代码

恢复时

先判断是否在暂停状态,如果不是就直接停止执行,否则更新状态为 Started(开始状态)。


resume() {  if (!this.state === 'Paused') return;    this.state = 'Started';    /* ... */}
复制代码

重置时

重置是可以随时执行的,并不需要任何的前天条件的。这里我们只需要把状态更新为 Initiated 即可。


reset() {  this.pause();    this.state = 'Initiated';    /* ... */}
复制代码


最后的添加时是不需要任何的拦截和状态更变的。所以 add 方法我们就可以不用动它了。



最后

在管理上来说,我们其实应该把 animation 相关的文件都放入一个新的项目里面的,但是这里我们就不详细做这部分了。同学自行进行整理即可。


这样我们就把 animation library(动画库)完整的实现了。


下一篇文章我们就会去把这个 animation 库与我们的 carousel(轮播图)结合来使用。最终用这个做一个完整的自定义组件库。


我是来自《技术银河》的三钻,一位正在重塑知识的技术人。下期再见。



⭐️ 三哥推荐

开源项目推荐

Hexo Theme Aurora

最近更新到了版本 1.3.0 谢谢各位的支持和持续的反馈和建议~



最近博主在全面投入开发一个可以 “迈向未来的” Hexo 主题,以极光为主题的博客主题。


如果你是一个开发者,做一个个人博客也是你简历上的一个亮光点。而如果你有一个超级炫酷的博客,那就更加是亮上加亮了,简直就闪闪发光。


如果喜欢这个主题,可以在 Github 上给我点个 🌟 让彼此都发光吧~


主题 Github 地址:https://github.com/auroral-ui/hexo-theme-aurora

主题使用文档:https://aurora.tridiamond.tech/zh/





博主开始在 B 站直播学习,欢迎过来《直播间》一起学习。


我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!


学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و




发布于: 2021 年 04 月 12 日阅读数: 46
用户头像

三钻

关注

微信公众号《技术银河》 2020.05.18 加入

起步于PHP,一入前端深似海,最后爱上了前端。Vue、React使用者。专于Web、移动端开发。特别关注产品和UI设计。专心、专注、专研,与同学们一起终身学习。

评论

发布
暂无评论
用 JavaScript 实现三次贝塞尔动画库 - 前端组件化