转自:https://segmentfault.com/a/1190000002416071

前言

前段时间无聊或有聊地做了几个移动端的HTML5游戏。放在不同的移动端平台上进行测试后有了诡异的发现,有些手机的动画会“快”一点,有些手机的动画会“慢”一点,有些慢得还不是一两点。

通过查找资料发现,基于帧的算法(Frame-based)来实现动画会导致不同帧率的平台体验不一致,而基于时间(Time-based)的动画算法可以很好地改良这种情况,让不同帧率的情况下都能达到较为统一的速度上的体验。

本文介绍的就是基于帧动画算法和基于时间动画算法的差异,以及对基于时间算法的改良。

基于帧的动画算法(Frame-based)

相信做过前端的人对使用JavaScript实现动画的原理都很熟悉。现在让你实现一个让一个div从左到右来回移动的JS代码,你可能嗖嗖就写出来了:

    function moveDiv(div, fps) {
var left = 0;
var param = 1; function loop () {
update();
draw();
} function update() {
left += param * 2;
if (left > 300) {
left = 300;
param = -1;
} else if (left < 0) {
left = 0;
param = 1;
}
} function draw() {
div.style.left = left + "px";
} setInterval(loop, 1000 / fps);
}
moveDiv(document.getElementById("div1"), 60);

效果如下:

http://jsfiddle.net/livoras/4taf9hhs/embedded/result,js,html,css/

看看代码,我们让一个div在0 ~ 300px区间内左右来回移动。update计算更新描绘div的位置,draw重新描绘页面上的div。为了方便起见,这里直接使用setInterval作为定时器,实际情况下可以采用你喜欢的setTimeout或者requestAnimationFrame。这里设置每秒钟到更新60次,60fps是人尽皆知的比较适合做动画的帧率。

地球人都知道,JavaScript中的定时器是不准确的。由于JavaScript运行时需要耗费时间,而JavaScript又是单线程的,所以如果一个定时器如果比较耗时的话,是会阻塞下一个定时器的执行。所以即使你这里设置了1000 / 60每秒60帧的帧率,在不同的浏览器平台的差异也会导致实际上你的没有60fps的帧率。

所以上面代码在一个手机上执行的时候可能有60fps的帧率,在另外一个手机上可能就只有30fps,更甚可能只有10fps。

我们模拟一下这种情况会有什么效果发生:

http://jsfiddle.net/livoras/Lcv1jm53/embedded/result,js,html,css/

这完全不对大头!

可以看到三个方块移动速度根本不在同一个channel上。想象一下一个超级马里奥游戏在10fps的情况会怎么样?按跳跃一下,你会看到马里奥以一种太空漫游的姿态在空中抛弧线。

导致这种情况的原因很简单,因为我们计算和绘制每个div位置的时候是在每帧更新,每帧移动2px。在60fps的情况下,我们1秒钟会执行60帧,所以小块每秒钟会移动60 * 2 = 120px;如果是30fps,小块每秒就移动30 * 2 = 60px,以此类推10fps就是每秒移动20px。

三个小块在单位时间内移动的距离不一样!

假如你现在要做一个超级马里奥的游戏,怎么做到可以在不同帧率的情况下让马里奥看起来还是那么迅速且帅气?

解决方案很明显。虽然不同的浏览器平台上的运行差异可能会导致帧率的不一致,但是有一样东西是在任何平台上都一致的,那就是时间。所以我们可以改良我们的算法,不是以帧为基准来更新方块的位置,而是以时间为单位更新。也就是说,我们之前是px/frame,现在换成px/ms

这就是接下来要说的基于时间(Time-based)的动画算法。

基于时间的动画算法(Time-based)

其实思路和实现都很简单。我们计算每一帧离上一帧过去了多少时间,然后根据过去的时间来更新方块的位置。

例如,上面的方块应该每秒钟移动120px,每毫秒移动120 / 1000 = 0.12像素(12px/ms)。如果上一帧方块的位置在left为10px的位置,到了这一帧的时候,假设相对于上一帧来说时间过去了200ms,那在时间上来说在这一帧方块应该移动200ms * 0.12px/ms = 240px。最终位置应该为10 + 240 = 250px。其实就是left = left + detalTime * speed。代码如下:

    function moveDivTimeBased(div, fps) {
var left = 0;
var current = +new Date;
var previous = +new Date;
var param = 1; function loop() {
var current = +new Date;
var dt = current - previous; // 计算时间差
previous = current;
update(dt);
draw()
} function update(dt) {
left += param * (dt * 0.12); // 根据时间差更新位置
if (left > 300) {
left = 300;
param = -1;
} else if (left < 0) {
left = 0;
param = 1;
}
} function draw() {
div.style.left = left + "px";
} setInterval(loop, 1000 / fps);
}

看看效果如何:

http://jsfiddle.net/livoras/8da1nssL/embedded/result,js,html,css/

看起来比上面的好多了,30fps和10fps好像能勉强赶上60fps的步伐。但是时间久了会发现30fps和10fps越来越落后于60fps。(建议先刷新再看看效果会更加明显)

这是因为每次小方块碰到边缘的时候,都会损失掉一部分时间,而且帧率越低的损失越大。看看我们上面的update函数:

      function update(dt) {
left += param * (dt * 0.12); // 根据时间差更新位置
if (left > 300) {
left = 300;
param = -1;
} else if (left < 0) {
left = 0;
param = 1;
}
}

假如我们现在方块的位置在left为290px的位置,这一帧传入的dt为100ms,那么我们left为290 + 100 * 0.12 = 302,但是302大于300,所以left会被设置为300。那么本来用来移动2px的时间就会白白被“抛弃”掉。dt越大,浪费得越多,所以30fps和10fps会比60fps越来越慢。

为了解决这个问题,我们对已有的算法进行改良。

改良基于时间的动画算法

解决思路如下:不一次算整块的时间(dt)移动的距离,而是把dt分成固定的时间片,通过多次update固定的时间片来计算dt时间后应该到什么位置。

比较抽象,我们直接看代码:

    function moveDivTimeBasedImprove(div, fps) {
var left = 0;
var current = +new Date;
var previous = +new Date;
var dt = 1000 / 60;
var acc = 0;
var param = 1; function loop() {
var current = +new Date;
var passed = current - previous;
previous = current;
acc += passed; // 累积过去的时间
while(acc >= dt) { // 当时间大于我们的固定的时间片的时候可以进行更新
update(dt); // 分片更新时间
acc -= dt;
}
draw();
} // update 和 draw 函数不变
setInterval(loop, 1000 / fps);
}

我们先确定一个固定更新的时间片,如固定为60fps时一帧的时间:1000 / 60 = 0.167ms。然后积累过去的时间,然后根据固定时间片分片进行更新。也就说,即使这一帧和上一帧相差过去了100ms,我也会把这100ms分成很多个0.167ms来执行update函数。这样做有两个好处:

  1. 固定的时间片足够小,更新的时候可以减少边缘损失的时间。
  2. 不同帧率,不管你是60,30,还是10fps,也是根据固定时间片来执行update函数,所以即使有损失,不同帧率之间的损失是一样的。那么我们三个方块就可以达到同步移动的效果的了!

看上面的代码,update和draw函数保持不变,而loop函数中,对过去的时间进行了累加,当时间超过固定的片就可以执行update。while循环可以保证更新直到把积累的时间都更新完。

对时间进行积累,然后分固定片更新。这种方式还有一个非常大的好处,如果你的帧率超过了60fps,如达到100fps或者200fps,这时候passed会小于0.167ms,时间就会被积累,积累大于0.167才会执行更新。碉堡的效果就是:不管你的帧率是高还是低,移动速度都可以和60fps情况下的速度同步。

看看最后的效果:

http://jsfiddle.net/livoras/25nut92z/embedded/result,js,html,css/

还是蛮不错的。

总结

基于帧的动画算法会在帧率不同的情况下导致动画体验有较大的差异,所有动画都应该基于时间进行执行。而基于时间的动画算法要注意边缘时间的损失,最好采取积累时间,然后分固定片更新动画的方式。

References

JavaScript基于时间的动画算法的更多相关文章

  1. JavaScript - 基于CSS3动画的实现

    在痛苦的IE8时代,所有的动画都只能基于自己计算相关动画属性,开定时器setTimeout/setInterval轮询动画任务. 而肩负重任的HTML5,早已注意到了日益增强的动画,随着HTML5的降 ...

  2. TOTP:Time-based One-time Password Algorithm(基于时间的一次性密码算法)

    TOTP:Time-based One-time Password Algorithm(基于时间的一次性密码算法) TOTP - Time-based One-time Password Algori ...

  3. 基于脚本的动画的计时控制(“requestAnimationFrame”)(转)

    requestAnimationFrame 方法的支持,该方法通过在系统准备好绘制动画帧时调用该帧,从而为创建动画网页提供了一种更平滑更高效的方法.在此 API 之前,使用 setTimeout 和  ...

  4. 超酷的JavaScript叙事性时间轴(Timeline)类库

    在线演示 Timeline 是我见过的最酷的展示事件随时间发展的javascript实现.你可以基于时间使用讲故事的方式来创建时间轴特效,整个时间轴以幻灯的方式来展示,你可以穿插图片,视频或者是网站, ...

  5. 第四十二课:基于CSS的动画引擎

    由于低版本浏览器不支持css3 animation,因此我们需要根据浏览器来选择不同的动画引擎.如果浏览器支持css3 animation,那么就使用此动画引擎,如果不支持,就使用javascript ...

  6. 基于ReliefF和K-means算法的医学应用实例

    基于ReliefF和K-means算法的医学应用实例 数据挖掘方法的提出,让人们有能力最终认识数据的真正价值,即蕴藏在数据中的信息和知识.数据挖掘 (DataMiriing),指的是从大型数据库或数据 ...

  7. 大数据算法->推荐系统常用算法之基于内容的推荐系统算法

    港真,自己一直非常希望做算法工程师,所以自己现在开始对现在常用的大数据算法进行不断地学习,今天了解到的算法,就是我们生活中无处不在的推荐系统算法. 其实,向别人推荐商品是一个很常见的现象,比如我用了一 ...

  8. 【Win 10 应用开发】UI Composition 札记(七):基于表达式的动画

    上一篇烂文中,老周给大伙伴们介绍过了几个比较好玩的动画.本篇咱们深化主题,说一说基于表达式的动画.这名字好理解,就是你可以用公式 / 等式来产生动画的目标值.比如,你想让某个可视化对象的高度减半,你的 ...

  9. 基于Twitter的Snowflake算法实现分布式高效有序ID生产黑科技(无懈可击)

    参考美团文档:https://tech.meituan.com/2017/04/21/mt-leaf.html Twitter-Snowflake算法产生的背景相当简单,为了满足Twitter每秒上万 ...

随机推荐

  1. 解决SharePoint 2013 designer workflow 在发布的报错“负载平衡没有设置”The workflow files were saved but cannot be run.

    原因是app management service没有设置好,在管理中心把他删掉,重新建一个就可以了 Provision App Management Service In SharePoint 20 ...

  2. Spark GraphX学习资料

    <Spark GraphX 大规模图计算和图挖掘> http://book.51cto.com/art/201408/450049.htm http://www.csdn.net/arti ...

  3. 2015年第14本(英文第10本):The A.B.C. Murders (A.B.C谋杀案)

    书名:The ABC Murders 推荐指数:5星 作者:Agatha Christie 单词数:7万 不重复单词数:不详 首万词不重复单词数:不详 蓝思值:740 阅读时间:2015年7月18日 ...

  4. onWindowFocusChanged

    这个onWindowFocusChanged指的是这个Activity得到或者失去焦点的时候 就会call. 也就是说 如果你想要做一个Activity一加载完毕,就触发什么的话 完全可以用这个!!! ...

  5. 安卓开发_浅谈Android动画(二)

    在学习了四个基本动画之后,现在要学习一些更有用的效果 先给出所有的动画xml <?xml version="1.0" encoding="utf-8"?& ...

  6. Ubuntu14.04下安装Hadoop2.5.1 (单机模式)

    本文地址:http://www.cnblogs.com/archimedes/p/hadoop-standalone-mode.html,转载请注明源地址. 欢迎关注我的个人博客:www.wuyudo ...

  7. 递归练习(C语言)

    本文地址:http://www.cnblogs.com/archimedes/p/recursive-practice.html,转载请注明源地址. 1.炮弹一样的球状物体,能够堆积成一个金字塔,在顶 ...

  8. View的生命周期

    当一个进入一个新viewController的时候,viewController的view的生命周期一般是这样的: 1.先判断内存是否有这个View a.没有的话:生命周期为loadView-> ...

  9. NSArray,NSMutableArray的三种排序

    int main(int argc, const char * argv[]) { @autoreleasepool { //字符串进行排序 NSArray *arr=@[@"b" ...

  10. Swift 中的函数

    学习来自<极客学院:Swift中的函数> 工具:Xcode6.4 直接上基础的示例代码,多敲多体会就会有收获:百看不如一敲,一敲就会 练习一: import Foundation //函数 ...