用 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年总结》 — 一个一线战场中的开发者,回归到学习的学堂中。一开始这个过程确实遇到了挺多困扰的。一开始无法静心下来学习,因为学习底层的知识确实需要静下心来学。但是坚持了一段时间后,又会发现自己会爱上学习,爱上深挖这些知识。

  • 📖 《前端进阶》 — 这里包含的文章学习内容需要我们拥有 1-2 年前端开发经验后,选择让自己升级到高级前端工程师的学习内容(这里学习的内容是对应阿里 P6 级别的内容)。

  • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

  • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

  • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥


三钻 CSDN认证博客专家 前端 Vue React
—— 起步于PHP,一入前端深似海,最后爱上了前端。Vue、React使用者。专于Web、移动端开发。特别关注产品和UI设计。专心、专注、专研,与同学们一起终身学习。关注我的微信公众号《技术银河》有更多最新知识文章与同学们分享。
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页