函数式编程能干什么(二)-- 用 Rx.js 写个抛物线动画

栏目: 编程语言 · 发布时间: 5年前

内容简介:昨天在掘金看到一篇文章,内容是用原生 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 写个抛物线动画》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

代码大全

代码大全

迈克康奈尔 / 电子工业出版社 / 2006-12 / 148.00元

《代码大全(第2版)(英文版)》由电子工业出版社出版。一起来看看 《代码大全》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具