这篇文章发布于 2017年01月17日,星期二,00:56,归类于 JS实例。 阅读 59924 次, 今日 1 次 31 条评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=5875
本文可全文转载,但需得到原作者书面许可,同时保留原作者和出处,摘要引流则随意。
一、动画算法的故事
人是一种恋旧的动物,即使过了很多年,即使小屁孩已经学会了打酱油,但是,这心中那,总是时不时会想起初恋的那个她,虽未提及,但在心里。
如果动画算法是恋人的话,那也有类似的故事。
当年还是个青涩男孩的时候,什么都是懵懵懂懂,实现效果要么干巴巴,要么拿来主义。直到一次偶然的邂逅,一次冥冥中注定的邂逅,我遇到了她,她是如此简单可爱,让我过目不忘。于是,一下子自己仿佛打开了一扇新的窗,有了她的相伴,实现的效果不再是干巴巴的,而是开始有了绚丽的色彩,开始有了灵动的气息。
后来,我成长了,接触了越来越多的外面的世界,遇到了越来越多是动画算法,这些算法确实很美,实现的效果更佳的炫丽,甚至将她们全部收纳整理成了后宫家族(可参考之前“如何使用Tween.js各类原生动画运动缓动算法”这篇文章),但是,实际开发的时候,使用的最多的还是最初的那个她,不仅是因为她给我带来了很多美好的回忆,更重要的是这么多年过去了,她一直保持着那份简单,和外面的那些妖艳*货完全不一样。
而这个相伴自己最久,最无法忘怀的、使用最多的她就是本文要分享的私藏的动画小算法。
Tween.js中的动画算法虽然标准全面且强大,但是,里面的公式略复杂,根本就记不住,每次使用都要去找一下,甚是麻烦。
而且,平时开发,绝大多数场景下,我们并不需要是那种弹来弹去的浮夸效果,有点缓动就可以,本文要分享的动画小算法就是一个简单的缓动小算法。
二、即插即用的缓动小算法
原理如下:
假设要从数值A变化到数值B,如果是线性运动,则每次移动距离是一样;如果是缓动,每次移动距离不一样。那如何才能不一样呢?很简单,按比例移动就可以。
例如:每次移动剩余距离的一半。
对吧,超容易理解的。
比方说:你和初恋之间距离是64,每秒移动一半,则,你们之间的距离下一秒就是32, 再下一秒就是16,然后8,然后4,然后2,然后1,然后……你们就在一起了。你们在一起的这个过程就是一个典型的先快后慢的缓动运动过程,如下示意图:
用一个简单的公式表示就是:
A = A + (B - A) / 2
翻译一下就是:
我下一秒的位置 = 现在位置 + 现在和初恋之间距离的一半
是不是很好理解。
而上面的A = A + (B - A) / 2
就是本文要介绍的即插即用的缓动小算法。
当然要实际使用还是需要做一点点的处理的,首先,既然是运动,那就离不开定时器,我们可以使用requestAnimationFrame
,对于不支持的浏览器,可以使用下面的兼容代码:
// requestAnimationFrame的兼容处理
if (!window.requestAnimationFrame) {
requestAnimationFrame = function(fn) {
setTimeout(fn, 17);
};
}
下面我们套用一个简单的实例,看看这个算法是如何应用的,就是非常常见的返回顶部效果。很多网站返回顶部要么直接“啪”到顶部,要么动画按部就班没有灵性,这里演示下如何使用寥寥数行代码,实现带缓动的返回顶部效果。点击下面的按钮即可看到返回顶部效果(如果没效果,可能访问的不是原始出处,访问下面地址体验效果,https://www.zhangxinxu.com/wordpress/?p=5875):
点击按钮执行的是下面的backToTop()
方法:
// 滚动到顶部缓动实现 // rate表示缓动速率,默认是2 var backToTop = function (rate) { var doc = document.body.scrollTop? document.body : document.documentElement; var scrollTop = doc.scrollTop; var top = function () { scrollTop = scrollTop + (0 - scrollTop) / (rate || 2); // 临界判断,终止动画 if (scrollTop <= 1) { doc.scrollTop = 0; return; } doc.scrollTop = scrollTop; // 动画gogogo! requestAnimationFrame(top); }; top(); };
其中,代码的核心是:
scrollTop = scrollTop + (0 - scrollTop) / (rate || 2);
scrollTop
表示公式的A, 滚动到顶部滚动高度是0
,因此,上面的0
,实际上就是公式的B, 而公式中的2
表示缓动速率,实际开发的时候是可以灵活调整的,缓动速率范围是1
到无穷大,速率值越小,运动越快。比如说上面的返回顶部效果,我们把缓动速率改成4
,点击下面的按钮感受效果:
可以明显感觉到,返回顶部使用的时间变长了,速度变慢了,缓动体验也更明显了。
等比例靠近理论上最终只会无穷靠近,并不会真正的相等,也就是动画永远没有结束的时候,所以说需要做一个临界判断,也就是距离小到一定数目的时候,直接等于目标值,并终止动画。例如,上面的返回顶部,就是当距离顶部滚动高度小于1
的时候,直接返回顶部,并终止动画。
if (scrollTop < 1) { doc.scrollTop = 0; return; }
我们最近刚上线不久的新版的起点移动站(https://m.qidian.com)的返回顶部效果就是使用的这个缓动小算法。
当然,起点M站使用这个动画小算法的地方不止这一个,比方说首页最近阅读的小球是可以移动的,惯性运动动画效果就是用的该算法,还是书籍阅读页面水平看书模式时候移来移去的翻页效果,也是此算法;垂直模式时候点击翻屏也是该算法等等。
用得非常多,因为简单,不需要查资料直接可以写出来,只要记住“等比例靠近”这个词,就不会忘记该怎么实现,永远忘不了的初恋就是这种感觉。
更新于2019-11-28
实际开发时候有时候不是滚动到顶部,而是任意位置,于是对上面方法改造了下:
// 滚动到顶部缓动实现 // rate表示缓动速率,默认是2 window.scrollTopTo = function (top, callback) { var scrollTop = document.scrollingElement.scrollTop; var rate = 2; var funTop = function () { scrollTop = scrollTop + (top - scrollTop) / rate; // 临界判断,终止动画 if (Math.abs(scrollTop - top) <= 1) { document.scrollingElement.scrollTop = top; callback && callback(); return; } document.scrollingElement.scrollTop = scrollTop; // 动画gogogo! requestAnimationFrame(funTop); }; funTop(); };
其中:
- top
- 必需。Number。垂直方向希望滚动的位置。
- callback
- 必需。Function。滚动到指定位置时候的回调方法。
滚动速率内置了,是2
,因为实际开发速率调整并不常用。
总之,经过改造后,滚动到顶部的这个方法变得实用多了。
三、即插即用的缓动小算法变身
如果项目很多地方使用该算法,每次都写一遍requestAnimationFrame
和边界判断是很啰嗦的,于是,我们可以把算法变个身,例如下面这样:
Math.easeout = function (A, B, rate, callback) { if (A == B || typeof A != 'number') { return; } B = B || 0; rate = rate || 2; var step = function () { A = A + (B - A) / rate; if (A < 1) { callback(B, true); return; } callback(A, false); requestAnimationFrame(step); }; step(); };
其中:
A
是起始位置;B
是目标位置;rate
是缓动速率;callback
是变化的位置回调,支持两个参数,value
和isEnding
,表示当前的位置值(数值)以及是否动画结束了(布尔值);
于是,我们的返回顶部效果可以这么使用:
var doc = document.body.scrollTop? document.body : document.documentElement; Math.easeout(doc.scrollTop, 0, 4, function (value) { doc.scrollTop = value; });
可以看到效果是一样的棒棒哒。
更关键点是我们的算法可以全局无限制重复利用啦!
四、结束语
本文有3个按钮,每个按钮点击都会有带缓动的返回顶部的效果,如果您发现您的浏览器按钮点击没效果,或者没按钮,则多半您访问的不是原出处。
根据我自己的猜测,Tween.js标准的缓动算法中,应该有类似本文的小算法的,应该就是某个easeOut方法。但是,具体实现上还是会有一些差异的,Tween.js是基于时间来做算法的,而本文的Math.easeout算法是基于缓动速率的,最终的时间是不固定的,受起始和终止位置的距离的影响,也正是这个原因,本文的缓动动画算法更好理解更容易记忆,因为现实世界距离是真实的容易感知的,时间是虚幻的无法捕捉的,正好和本文的算法距离驱动而非时间驱动相映射,容易形成共识。
恩,就说这么多吧。
感谢阅读,欢迎交流,更欢迎大力分享,你瞧,我把珍藏的初恋都分享出来了,还不点赞分享支持下。
本文为原创文章,包含脚本行为,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=5875
(本篇完)
- 仿新浪微博返回顶部的js实现(jQuery/MooTools) (0.407)
- jQuery-火焰灯效果导航菜单 (0.227)
- CSS3动画那么强,requestAnimationFrame还有毛线用? (0.227)
- 如何使用Tween.js各类原生动画运动缓动算法 (0.227)
- 页面级可视动画View Transitions API初体验 (0.227)
- 不能落后,好好缕缕CSS滚动动画 (0.227)
- 这啥?CSS calc-size和interpolate-size,真学不动了 (0.227)
- 开源移动端元素拖拽惯性弹动以及下拉加载两个JS (0.187)
- JS检测PNG图片是否有透明背景、抠图等相关处理 (0.163)
- JS实现照片图片变成黑白线条线稿 (0.163)
- jQuery之使用slideToggle实现垂直下拉菜单 (RANDOM - 0.024)
去收藏夹吃灰吧
有bug 改了一下
function easeout(A, B = 0, rate = 2, callback) {
if (A === B || typeof A !== ‘number’) {
return;
}
const step = function() {
A = A + (B – A) / rate;
callback(A, true);
if (Math.abs(A – B) < 1) {
callback(A, false);
return;
}
requestAnimationFrame(step);
};
step();
}
在if判断语句,将if(A<1)改为if(A<B+1)也可以快速解决哦
诙谐幽默,甚是喜欢
ie都不好使啊 机上那个佳能处理的也不好啊
我试过了可以的啊,你是不是使用了DOM2监听按钮点击事件啊?IE不支持DOM2级的事件
旭哥我和我的姐们一直把你认为是神一样的存在,都超级的喜欢你。
+1s 心有感触
这个动画,一直没搞明白,看了这篇后,都懂了,真是好文!
这个只能往上动 而且结束必须是0,我改写了下:
var _easeout = function(start, end, rate, callback) {
var _end = end;
if (start == end || typeof start != ‘number’) {
return;
}
end = end || 0;
rate = rate || 2;
var step = function() {
start = start + (end – start) / rate;
if (Math.abs(start – _end) < 1) {
// if (start < 1) {
console.log('end');
callback(end, true);
return;
}
callback(start, false);
requestAnimationFrame(step);
};
step();
};
调用
var doc = document.body.scrollTop !== undefined ? document.body : document.documentElement;
console.log('start', doc.scrollTop);
console.log('end', end);
_easeout(doc.scrollTop, end, 2, function(value) {
doc.scrollTop = value;
});
结束的判断修改了。还有
var doc = document.body.scrollTop !== undefined ? document.body : document.documentElement;
没有滚动 在顶部时,document.body.scrollTop为0 会取成 document.documentElement; 就不能滚动了。
最后封装的算法有点问题, if (A < 1) 改成 if(Math.abs(A-B) < 1),是否是这样?
你好 那个requestAnimationFrame兼容里面 那个 setTimeout(fn,17) 这个17是随便写一个数字 还是有啥意义啊!
1000/60 ≈ 17
大神 我又百度了一下 我现在的理解是 默认60ms变化一次 1000ms动了17次 应该是这样了 谢谢!
我好像看错了 每秒60帧? 这回差不多吧 还是拿来主义吧。
电脑显示屏幕一般刷新频率每秒(1000ms)60帧,所以1000/60约17ms一帧是屏幕显示的合理时间。。反正我感觉是这样的。。
新年快乐!
张老师,
在Math.easeout中,当B>A的时候函数会一直执行,不会停止;
我的解决办法是
把:
if (A < 1) {
callback(B, true);
return;
}
改成:
if (Math.abs(A – B) < 1) {
callback(B, true);
return;
}
关注点偏移~
初恋!
建议可以来个fibonacci序列的
震精!
棒!
大神,是怎么做到在自己博客网站写点击事件的
自己博客自己管理员当然可以啦~
应该还有退出动画的机制不然会一直执行
最后的按钮没绑执行的方法
原来阮大神的代码也并不都是大神级别的,看到这些小代码让人有一种返璞归真的感觉。新的一年,我也要加油!
btw:rate2和4在效果感觉上差别太小,只是慢了一倍的速率,是否需要用大一点的rate值来说明会更好?
抱歉,张大神。打错字了。
不错,谢谢
按钮无法执行?
移动端我兼容下~