内容简介:昨天在掘金看到一篇文章,内容是用原生 JS 写抛物线动画。看完觉得挺有趣,很适合用 Rx.js 来重现,于是有了这篇文章。本文默认你已经掌握了 Rx.js 的基本概念和操作。若你还没掌握,推荐先看一些入门资料。动画的本质就是页面元素随着时间的持续,在特定时间点改变自身在页面中的坐标位置。这个很适合用响应式编程中“流”的概念来表达。我们需要将动画的持续时间(本文只考虑时间限定的情况)内根据浏览器
昨天在掘金看到一篇文章,内容是用原生 JS 写抛物线动画。看完觉得挺有趣,很适合用 Rx.js 来重现,于是有了这篇文章。
本文默认你已经掌握了 Rx.js 的基本概念和操作。若你还没掌握,推荐先看一些入门资料。
动画的本质就是页面元素随着时间的持续,在特定时间点改变自身在页面中的坐标位置。这个很适合用响应式编程中“流”的概念来表达。我们需要将动画的持续时间(本文只考虑时间限定的情况)内根据浏览器 requestAnimateFrame
API 所允许的时间点映射成一个个节点,然后在这一个个节点中改变物体的位置。这个关键一步做好了,剩下的诸如 easing
曲线和加速度等都好解决了。
来看怎么解决第一个问题。先上代码:
// 首先我就把所有 Observable 和操作符导入了,接下来就省略了 import { interval, animationFrameScheduler, fromEvent, defer, merge } from "rxjs"; import { map, takeWhile, tap, flatMap } from "rxjs/operators"; function duration(ms) { return defer(() => { const start = Date.now(); return interval(0, animationFrameScheduler).pipe( map(() => (Date.now() - start) / ms), takeWhile(n => n <= 1) ); }); } 复制代码
defer
的作用是,只有当被订阅时,它才会根据提供给它的 Observable 工厂函数,生成新的 Observable。这样做的目的是, duration
需要为每一个订阅者提供新的 Observable。等下会看到它会在不同的地方被订阅。
首先在 defer
里面的 Observable 工厂函数前面记录当前时间戳。接下来下一行, interval
的作用是相隔指定时间段,释放一个行为(这个比较抽象,可以理解成告诉管道的下一个接收者要开始做事了)。 interval
接受两个参数,第二个参数是 Scheduler。默认的 Scheduler 是 async
,这里我们需要提供 animationFrameScheduler
。这样做的意思是,告诉 interval
每隔 0s 释放一次行为,这个行为由 animationFrameScheduler
调控。事实上后者不会真的每 0s 就释放一次,而是会通过 requestAnimationFrame
来获取浏览器的空闲时间(下一帧渲染之前),只有当浏览器有空了才会响应 interval
的指令。
然后接下来进入管道,第一个 map
意思是,把 interval
的指令映射成一个时间比例,该时间比例由当前时间,减去 interval
生成之前的时间,然后除以总时间,得到的是当前时间点占总时间长的比率。 takeWhile
指定一旦这个时间比例超过 1,就把 Observable 停掉。举个例子,本来指定了 3 秒,但是时间过了 4 秒,4/3 就大于 1 了,超过了动画指定时长。
最重要的部分就处理完了。
接下来计算每个时间点物体应该移动的距离:
const distance = d => t => d * t; 复制代码
参数 d 指的是总距离,t 指的是时间比率,就是我们在上一步算出来的。两者相乘就是每个时间点物体移动的距离了。注意,函数式编程里面的函数都要柯里化(回调函数不一定)。这样做的好处等下会看到。
然后取到 DOM 上的目标元素,对其进行位移:
const targetDiv = document.querySelector(".target"); const moveRight$ = duration(2000).pipe( map(distance(1000)), tap(x => (targetDiv.style.left = x + "px")) ); const moveDown$ = duration(2000).pipe( map(distance(700)), tap(y => (targetDiv.style.top = y + "px")) ); 复制代码
这里写了两个流,分别是右移和下移,右移 1000px, 下移 700px。注意到我们把总距离传给 distance
函数后,它会返回新的函数,等着管道上游给它传时间比例 t,这就是柯里化的作用。
然后我们把两个流合并,就可以让物体同时右移和下移,也就是让它走对角线。
merge(moveRight$, moveDown$).subscribe() 复制代码
动画的第一阶段写完了,此时目标物体会从左上角到右下角做匀速直线运动。接下来我们要加上抛物线轨迹和重力加速度效果。
思考一下,抛物线的轨迹是水平移动和垂直移动速度不一致导致的,而加速度是由两者的速率变化导致的。前者可以用两者的函数关系来体现,后者可以用两者各自的 easing
函数来体现。我查了一下主流的 easing
函数,仿写了两个。
第一个是 easeInQuad
:
const easeInQuad = t => t * t; 复制代码
第二个是 easeInQuint
:
const easeInQuint = t => t * t * t * t * t * t; 复制代码
可以看出两者的函数关系是 y = Math.pow(x, 3)
,刚好是个抛物线。若想定制加速度和抛物线轨迹,也可以自己写。
接下来只用把 interval
里面的时间比例应用于各自的 easing
函数就行了。然后再加个按钮,只有点击按钮后,动画才开始。
一步到位完整代码:
const targetDiv = document.querySelector(".target"); const startBtn = document.querySelector("#start"); const startClick$ = fromEvent(startBtn, "click"); const easeInQuad = t => t * t; const easeInQuint = t => t * t * t * t * t * t; function duration(ms) { return defer(() => { const start = Date.now(); return interval(0, animationFrameScheduler).pipe( map(() => (Date.now() - start) / ms), takeWhile(n => n <= 1) ); }); } const distance = d => t => d * t; const moveDown$ = duration(1500).pipe( map(easeInQuint), map(distance(700)), tap(y => (targetDiv.style.top = y + "px")) ); const moveRight$ = duration(1500).pipe( map(easeInQuad), map(distance(1000)), tap(x => (targetDiv.style.left = x + "px")) ); startClick$.pipe( flatMap(() => merge(moveRight$, moveDown$)) ).subscribe() 复制代码
线上效果在这里
以上所述就是小编给大家介绍的《函数式编程能干什么(二)-- 用 Rx.js 写个抛物线动画》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。