这期我们来完善上一期的动画库。在 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 中的重置功能也给实现了。其实这个功能的逻辑非常简单,它所需要用到的函数我们都有了。
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 站直播学习,欢迎过来《直播间》一起学习。
我们在这里互相监督,互相鼓励,互相努力走上人生学习之路,让学习改变我们生活!
学习的路上,很枯燥,很寂寞,但是希望这样可以给我们彼此带来多一点陪伴,多一点鼓励。我们一起加油吧! (๑ •̀ㅂ•́)و
评论