使用原生js通过缓动函数实现抽奖转盘动画

avatar
前端开发工程师 @bigo

file

本文首发于:github.com/bigo-fronte… 欢迎关注、转载。

前言

最近接到抽奖转盘活动页开发的需求,由于转盘样式UI比较特殊,使用开源的组件又比较难定制,而转盘转动的特效其实实现起来并不复杂,因此自己使用原生的js利用缓动动画原理开发了转盘特效,在开发过程中对缓动函数相关的知识点进行的一些梳理,总结了转盘动画实现的原理,希望给大家有一些帮助。

什么是缓动函数

事物的运动遵循一定的规律,从生活上汽车的加速、减速、球体上抛,自由落体等可以看出,它不是一直线性的运动,而是有一定规律的加速或减速的过程,而缓动函数就是描述这一种运动的特性,我们从而总结出一套数学公式,可以利用它来描述物体运动规律,在游戏、动画开发方面有好多的应用场景。

常见的缓动函数有:

线性运动

物体同一时间内运动的距离相同,简言而知可以将它看做是匀速运动,无缓动效果

linear

缓入

物体开始时移动好慢,然后逐渐加快,同一时间频率范围内,移动距离增大,可以看做是加速缓动

ease-in

缓出

物体开始时移动好快,然后逐渐减慢,同一时间频率范围内,移动距离减少,可以看做是减速缓动

ease-out

缓入缓出

这是缓入和缓出两者的结合,物体先加速后减速,符合物体的自然运动规律,这个也是我在开发抽奖转盘运动动画所应用的缓动函数

ease-in-out

作为业务开发,掌握应用缓动的公式就可以了,这里简单地介绍缓动公式如何使用。缓动函数的公式分为2个版本,原版和修改版,原版需要传入4个参数,修改版对比原版传参只需一个,但得出的结果是个比例值,需要对总移动距离相乘才获得当前位置。

以缓入二次方函数为例:

// 原版
function QuadIn(t, b, c, d) {
 return c * (t /= d) * t + b;
}

// t:缓动开始时间
// b:缓动开始位置
// c:缓动移动的距离
// d:缓动持续的时间

// 结果值为当前位置值
// 修改版
function QuadIn(p) {
 return p * p;
}

// p: 当前已执行的时间 / 总时间

// 结果值需要与总移动距离相乘才获得当前位置

原版缓动公式集合:github.com/gdsmith/jqu…
修改版缓动公式集合:github.com/gdsmith/jqu…
缓动函数速查:easings.net/cn

使用css实现缓动函数

虽然本篇文章主要是讲述利用js来实现缓动函数动画,但也不妨碍多去了解其他的实现方案,常见的动画实现除了js实现外,css是我们首选的方法,优点是降低代码复杂性,浏览器专门优化,运行流畅,性能高,但缺点是缺少灵活性,不能在更细粒度上去控制动画的展现。我们通常用css来指定元素的动画运动函数和运动时间,从整体上去描述运动过程,只需简单的定义就可以让一个静态元素动起来了。

transition

过渡动画,强调的是开始到结束过程,只需要定义物体的开始和结束态,使用transition-timing-function属性定义缓动动画名称即可。

.el {
  transition-timing-function: ease-in-out;
}

animation

关键帧动画,强调的是控制关键帧之间的运动,通过keyframe定义关键帧的状态,使用animation-timing-function属性定义缓动动画名称即可。

.el {
  animation-timing-function: ease-in-out;
}

使用三次贝塞尔曲线定义缓动函数

css的动画属性除了使用缓动函数的名称来定义之外,还可以通过三次贝塞尔曲线函数cubic-bezier()定义。

三次贝塞尔曲线由四个点 P0,P1,P2 和 P3 定义,其中P0和P3分别代表起始点(0, 0)和终结点(1, 1),P1和P2是另外两个锚点,通过调整P1和P2便可勾画出一条曲线,这条曲线便是缓动函数所描述的运动特征。

cubic-bezier

.el {
  transition-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
  animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
}

三次贝塞尔曲线制作: cubic-bezier.com/

利用原生js实现缓动函数

接下来是本文的重点,使用js来实现缓动特效。动画的实现原理其实就是在每一帧中实现细微的运动,通过时间轴不间断操作就能实现流程的动画展示效果,基于这个原理我们首先需要在浏览器中实现动画循环函数。

动画循环函数

动画循环函数是一帧中的操作,以接近屏幕刷新频率执行

  • setTimeout实现
const rAF =  (callback) => {
  return window.setTimeout(callback, 1000 / 60)
}
const amination = () => {

  // 退出循环条件
  if () {
    return 
  }
  rAF(amination);
}

rAF(amination);
  • setInterval实现
const rAF =  (callback) => {
  return window.setInterval(callback, 1000 / 60)
}

const amination = () => {

  // 退出循环条件
  if () {
    return window.clearInterval(this.timer);
  }
}

this.timer = rAF(amination);

  • 使用浏览器提供的requestAnimationFrame方法
const amination = () => {

  // 退出循环条件
  if () {
    return 
  }
  window.requestAnimationFrame(amination);
};

window.requestAnimationFrame(amination);

优先使用window.requestAnimationFrame实现,浏览器能强制按照屏幕刷新率执行,不会出现丢帧的现象

转盘运动过程

转盘的过程主要分为3个部分,1、启动加速 2、匀速 3、减速停止,接下来逐一使用js实现

drawPanel

加速阶段

转盘启动到旋转到最大速度是一个旋转速度由慢到快的加速过程,我们可以使用缓入函数对旋转速度进行处理

// 缓入函数
function eaeIsn(t, b, c, d) {
  if (t >= d) t = d;
  return c * (t /= d) * t + b;
}

// 开始时间
const startTime = Date.now();

// 最大速度,将每帧旋转的角度为看做速率
const maxSpeed = 20;

// 加速持续时间
const holdTime = 3000;

// 时间段的起点
const timeStartPoint = 0;

// 旋转角度
this.deg = 0;

function animation() {
  // 当前使用的时间段
  const currentTime = Date.now() - startTime;

  // 获取当前帧的速度
  const curSpeed = easeIn(currentTime, timeStartPoint, maxSpeed, holdTime);

  // 旋转角度
  this.deg += curSpeed;

  // 优化结果值,取360度的余数结果即为当前位置
  this.deg = this.deg % 360;

  window.requestAnimationFrame(animation)
}

window.requestAnimationFrame(animation);

匀速阶段

该阶段保持速度不变即可

减速阶段

转盘从旋转最大速度到停止是一个旋转速度由快到慢的减速过程,我们可以使用缓出函数进行处理

这一过程并仅仅是将转盘速度减速到停止这么简单,还需要考虑转盘停止到指定的区域,获取对应的奖品。这里处理的逻辑是从停止的指定区域来反推出总共需要旋转的角度,然后使用缓出函数对旋转路程进行处理,下面是整个动画处理过程

// 缓入函数
function eaeIsn(t, b, c, d) {
  if (t >= d) t = d;
  return c * (t /= d) * t + b;
}

// 缓出函数
function easeOut (t, b, c, d) {
  if (t >= d) t = d;
  return -c * (t /= d) * (t - 2) + b;
}

// 开始时间
const startTime = Date.now();

// 最大速度
const maxSpeed = 20;

// 加速持续时间
const startHoldTime = 2000;

// 时间段的起点
const timeStartPoint = 0;

// 减速持续时间
const endHoldTime = 3000;

// 旋转角度
this.deg = 0;

// 停止区域索引
this.stopIndex = 0;

// 动画循环总次数,用来计算fps
this.progress = 0;

// 减速开始时间
this.endTime = 0;

// 每个奖品所占的角度
this.prizeDeg = 45;

function animation() {
  // 当前使用的时间段
  const currentTime = Date.now() - startTime;

  // 获取当前帧的速度
  const curSpeed = easeIn(currentTime, timeStartPoint, maxSpeed, holdTime);

  // 旋转角度
  this.deg += curSpeed;

  // 优化结果值,取360度的余数结果即为当前位置
  this.deg = this.deg % 360;

  // 检测到stopIndex有值,此时知道抽到的奖品区间范围,开始实行减速,计算减速总路程
  if (this.stopIndex > 0) {

    // 计算屏幕刷新帧率
    const fps = currentTime / this.progress;
    this.endTime = Date.now();

    // 开始减速时所处的位置
    this.stopDeg = this.deg;
    let i = 0;
    while(++i) {
      // 结合开始减速时所处的位置和结束时所处的位置计算旋转总路程
      const endDeg = 360 * i - this.stopIndex * this.prizeDeg - this.stopDeg;

      // 计算刚开始第一帧旋转的角度,也就是初始速度
      const curSpeed = easeOut(fps, stopDeg, endDeg, endHoldTime) - this.stopDeg;

      // 当初始速度与当前旋转最大速度相等,即可获取总共需要旋转的角度
      if (curSpeed >= maxSpeed) {
        this.endDeg = endDeg;
        break;
      }
    }

    // 开始减速
    return slowDown();
  }

  window.requestAnimationFrame(animation)
}

function slowDown() {
  window.requestAnimationFrame(function() {
    const currentTime = Date.now() - this.endTime;

    // 减速完成
    if (currentTime >= endHoldTime) {
      return;
    }

    // 缓出减速
    this.deg = easeOut(currentTime, this.stopDeg, this.endDeg, endHoldTime) % 360;
    this.slowDown();
  })
}

window.requestAnimationFrame(animation);

以上是转盘实现的核心代码,重点要解决加速到减速过程要平滑过渡并且结束的时候要落在指定位置上

性能优化

此外还对动画进行了一些性能优化,旋转用到了transform的rotate属性,因而使用will-change属性,提前告知浏览器元素将要发生什么变化,对可能的操作进行优化,提高动画的执行效率

.el {
  will-change: transform;
}

欢迎大家留言讨论,祝工作顺利、生活愉快!

我是bigo前端,下期见。