使用react-spring 优雅实现转盘项目动效

572 阅读5分钟

转盘游戏屡见不鲜,不管是菠菜网站,还是各类电商游戏app,都是一种效果特别好的营销手段。在种类来说的话,无论是圆形转盘,九宫格,还是老虎机,都有各种各样的展现方式,实现难度相对来说也没有那么大,那在这些展现方式中,发挥着较大作用的就是转动过程中的动画效果,动画之所以生动,其中一方面就是符合现实世界流畅自然的运动效果,变化之间有着比较舒服的过渡,本文主要讨论如何实现这种舒服的过渡

转盘动画的基本原理

关于转盘通过旋最终停到指定位置,想必大家都很容易理解,就是360/(奖品个数) *(目标位置索引)(只讨论指针指向单个奖品中间),那么动画如何停下来也有很多种方式,一种就是匀速停止,给人一种戛然而止的感觉,不太符合现实世界中的物理表现形式,那么另一种则是缓慢停止。

缓动函数介绍

提到缓慢停止,我们会想到一个概念叫缓动函数,大家一定无论在css中还是在其他动画效果的实现中都遇到过,缓动函数解决的问题就是在控制一个值在某一段时间的变化量,类似物理学中的加速度。换到转盘的场景里,常规情况下他会匀速到达某一个角度,当我们能把匀速改为变速后,动画效果会自然流畅很多。

常见的实现方案和不足

方案一:CSS实现

一种方法是使用css的easing-function 如设置

.block {
	transition: transform 0.6s cubic-bezier(0.87, 0, 0.13, 1);
}

再根据属性的变化让他自动呈现

方案二:JavaScript定时器实现

第二种是社区比较常见的,使用setInterval的方法去控制变速,比如

// 初始化当前旋转角度为 0
let current = 0
// 定义最终旋转角度为 1800 度
const final = 1800
// 定义减速比例为 0.
let speed = 40
// 设置一个定时器,每 16 毫秒执行一次函数
const timer = setInterval(() => {
  // 如果当前旋转角度大于等于最终旋转角度,清除定时器
  if (current >= final)
    clearInterval(timer)

  current += speed

  // 速度在每次角度计算后,减小0.1,数值仅举例说明,实际可能需要更加精细的调整
  speed -= 0.1
}, 16)

以上代码表示速度变化率为0.1,如果在函数曲线上表示就是v=-0.1t,开口向下的抛物线 速度一开始最大,然后缓慢变小,相比起线性减小,多了一些柔和和流畅性。 当然也有这样写的,

  if(current > 某个值){
  speed = 20
  }
    if(current > 某个值){
  speed = 10
  }

手动控制每一阶段的速度值,这就像是之前学过的分段函数。

缺点

那么这些写法有以下几点不足之处:

  • 边界条件需要自己处理
    • 我要控制速度不能为0,还要保证动画执行完。合理
  • 灵活性差,难以维护
    • 如果角度变化,那对应的增量也需要进行相应的变化
    • 状态难以维护,难以切换不同属性的不同动画形式,需要进行频繁的样式修改

React Spring实现

鉴于此,我们引入react-springreact-spring是一个用于在 React 应用中创建动画效果的库。它基于物理模型实现动画效果。除了常见的可以通过配置摩擦力惯性以及质量控制动画的运动之外,还可以通过传入缓动函数的方式自定义动画效果。 在react-spring中,我们可以通过配置,对颜色,旋转,缩放等各种属性进行缓动,方法也很简单,指定好起始点,设置好对应的缓动函数即可 这里我们通过easing配置项去控制旋转值的变化:

function Component() {
  const award_list = [] // 奖品列表
  // 获取单个奖品旋转角度
  const segmentAngle = 360 / award_list.length
  const [style, api] = useSpring(() => ({
    transform: 'rotate(0deg)',
  }))
  const start = () => {
    const selectedItemIndex = 5 // 假设旋转到第5个商品

    const endRotation = (3 * 360) + segmentAngle * selectedItemIndex
    // 旋转方法
    api.start(() => ({
      transform: `rotate(${endRotation}deg)`,
      config: {
        duration: 5000,
        easing,
      },
    }))
  }

  return <animated.div style={style} />
}
easing: EasingFunction
type EasingFunction = (t: number) => number

我们的目标是通过指定合适的easing函数,让rotate的值实现变速增长

缓动函数详解

easing 函数用于描述动画进度(或时间)和动画的补间值之间的映射关系。easing 函数接收一个参数 t,它表示动画的当前进度,范围从 0 到 1。这个函数返回一个新的值,通常也是在 0 到 1 的范围内,用来决定动画在该进度点上的状态。

所以我们把rotate从0到target度数,映射到了0到1,这样我们只需要看0到1上t的变化就可以了.

所以easing函数中t的变化趋势,就反映了目标属性的变化趋势

例如 const easing = t => t,想必大家都很容易理解,函数图像为一条直线

image.png

所以他的函数为 y = t(0<=t<=1) 变化率为y'=1,是匀速变化也就表现出来以下这种戛然而止的效果:

无标题视频——使用Clipchamp制作.gif

上述的函数图像在我们之前学习css的贝塞尔曲线时会有熟悉,例如在css贝塞尔曲线这个在线网页上呈现的效果,所以在这里我们可以理解为,当t的值越靠近1,斜率越小,y的增量就会越来越小,那动画就能实现这种缓慢停止的效果了, 我们从Easing Functions网站上的easeOutExpo这个效果为例,他的表达式为

y=12(10t)y = 1 - 2^{(-10t)}

函数图像为

image.png

我们可以看到t越靠近1,y到1的变化会越来越慢(斜率越来越小) 当然这里需要做一下边界处理,因为t=1时,y还没等于1,所以在js中的函数形式为:

function easeOutExpo(x: number): number {
  return x === 1 ? 1 : 1 - 2 ** (-10 * x)
}

最终效果则为这样:

无标题视频—.gif

当然这里为了节约时间,我只调了转动2圈,转动时长为2s作为演示,在实际使用中,可以将这两个值加大(如转3圈,时长5s),效果会更好。

此外这个缓动函数网站上还有各种形式的缓动函数,例如你可以选一个先慢后快最后慢的效果(easeInOutExpo),其实这是上述一次函数分段函数的二次形式,大家可以选择不同的函数自己玩一下

image.png

总结与建议

  • 动效的核心是节奏
  • react-spring 提供了一种对属性的过渡动画间做插值解决办法,我们只需要定义好起止点以及相对应的缓动函数,即可实现该效果
  • 除了react-spring外,所有能指定缓动函数的动画库都能实现此效果
  • 底层可能都用了setInterval或者requestanimationframe,那么还是使用封装后的工具更好一些

参考资料