实践requestAnimationFrame平滑动画

本文源自我的公众号: 实践requestAnimationFrame平滑动画

背景

前端领域实现动画效果通常有这么几种方式: css animationsetTimeout(setInterval)Lottie

  • css animation 动画是通过关键帧(@keyframes) 来实现的,优点是写法比较简单,缺点就是难以获取动画的开始和结束事件。

  • setTimeout 一般通过callback 来控制元素的变化实现动画的,但是定时器动画一直存在两个问题,第一个就是动画的循时间环间隔不好确定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz,1000ms/60);第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行。

  • lottie 一般是使用 canvas 或者 svg 方式来实现动画的, 通过引入配置文件来实现。但是复杂动画会导致配置文件过大,进而导致webpack打包体积过于庞大。

本次要实现能量球的 轨迹运动 动画 及 总能量缩放 动画,以上的方案实践下来均不能实现平滑的效果。后来了解到 H5 中加入了 requestAnimationFrame,该方法是根据浏览器的刷新频率来执行回调方法,可以很好的控制动画的开始和结束事件。

尴尬,简书上传不了视频,如果看效果,请移步到公众号文章地址查看。 https://mp.weixin.qq.com/s/yTex4ewF0cbG1tluaqxKzw

说明

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。

注意:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用window.requestAnimationFrame()

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame() 运行在后台标签页或者隐藏的iframe 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

回调函数会被传入DOMHighResTimeStamp参数,DOMHighResTimeStamp指示当前被 requestAnimationFrame() 排序的回调函数被触发的时间。在同一个帧中的多个回调函数,它们每一个都会接受到一个相同的时间戳,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为1ms(1000μs)。

请确保总是使用第一个参数(或其它获得当前时间的方法)计算每次调用之间的时间间隔,否则动画在高刷新率的屏幕中会运行得更快。

语法

window.requestAnimationFrame(callback);

参数

callback

下一次重绘之前更新动画帧所调用的函数(即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

返回值

一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

范例

以下代码根据回调的时间戳传参(说明:这里是timestamp 表示为从time origin之后到当前调用时经过的时间),确保不同刷新频率的屏幕 都可以在 两秒内停止动画。

const element = document.getElementById('some-element-you-want-to-animate');
let start;

function step(timestamp) {
 if (start === undefined)
   start = timestamp;
 const elapsed = timestamp - start;

 //这里使用`Math.min()`确保元素刚好停在200px的位置。
 element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';

 if (elapsed < 2000) { // 在两秒后停止动画
   window.requestAnimationFrame(step);
 }
}

window.requestAnimationFrame(step);

实践

本次实践收能量平滑动画不考虑高刷新频率的屏幕,暂定为用户使用的都是 一秒钟 60 刷新频次的屏幕。动画分三部分:向下动画、轨迹动画、缩放动画。

  • 下滑动画

收取能量球开始有个向下的动画,每次下滑2px,能量球下滑到 60 px 的时候(下滑动画执行了30次,按照一秒钟刷新60次,动画大约执行0.5秒),则执行向右上角的轨迹动画,用 requestAnimationFrame 可以这么实现:

// 点击能量球事件
 clickBall() {
   this.dropY = 0;
   this.perY = 2;
   requestAnimationFrame(this.dropBall.bind(this));
 }
 // 下滑轨迹动画
 dropBall() {
   if(this.dropY < 60) {
     this.dropY += this.perY;
     this.ballRef.current.style.top = `${this.props.style.top + this.dropY}px`;
     requestAnimationFrame(this.dropBall.bind(this));
   } else {
     // 轨迹动画
     this.parabola();
   }
 }
  • 轨迹动画

轨迹动画,则通过 getBoundingClientRect 方法 分别获取 开始元素(能量球当前位置)坐标 和 结束元素(总能量位置)坐标的位置, 进而计算出需要运动的距离。根据需要的时间和每秒60次的刷新频率,计算出 每次 translate 的距离即可。(可以参照张鑫旭实践的轨迹动画,此处不列举实现方式)。

  • 缩放动画

最后是总能量的缩放动画(这里实现的缩放是 从1.0 放大到1.3, 然后再缩回到1.0):

// 总能量收取事件 及 触发动画效果
 collectPower({energyNum}) {
   this.totalPower += energyNum;
   this.powerRef.current.innerText = this.totalPower;
   this.minuteFlag = false;
   this.scale = 1.0;
   this.perAdd = 0.02;
   requestAnimationFrame(this.scalePower.bind(this));
 }

 // 动画效果
 scalePower() {
   if (this.scale < 1.3 && !this.minuteFlag) {
     this.scale +=  this.perAdd;
     this.powerRef.current.style.transform = `scale(${this.scale})`;
     requestAnimationFrame(this.scalePower.bind(this));
   } else if (this.scale > 1) {
     this.minuteFlag = true;
     this.scale -= this.perAdd;
     this.powerRef.current.style.transform = `scale(${this.scale})`;
     requestAnimationFrame(this.scalePower.bind(this));
   } else {
     this.powerRef.current.style.transform = `scale(1)`;
   }
 }

参考资料:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343