初级开发者福音:手把手教你实现数字滚动效果~

news2024/12/26 21:18:51

文章目录

  • 一、前言
  • 二、背景知识
  • 三、实现方案
    • Step 1:分析需求
    • Step 2:实现单个数字的滚动效果
    • Step 3:组件接口设计
    • Step 4:完善组件


一、前言

前端数字滚动显示的场景很多,比如抽奖的时候,营造一种马上公布中奖号码的一种紧张感;还比如给随机变化的数字增加动态的效果,营造出来一种生动感…

能够实现数字滚动效果的方法很多,本期我们将围绕 GSAP (GreenSock Animation Platform的简称)来实现。


二、背景知识

本文以开发一个数字滚动组件的形式对特效开发的步骤进行演示,组件基于 React 和 GSAP 实现,并附上了关键部分的代码。

如果您是初次听说 React 或 GSAP 的新手,先学习一些基础知识会有所帮助。

  • React :一个用于构建用户界面的代码库。React 组件就像是一个自定义、可重用的 HTML 元素一样,能够帮助开发者快速有效地构建用户界面。

通过 React course on freeCodeCamp ,了解有关 React 的更多信息

  • GSAP:一个业界知名的动画平台,你可以使用 GSAP 为 JavaScript 可以触及的几乎所有内容制作动画,无论您是想要为 UI、SVG、Three.js 还是 React 组件制作动画,GSAP 都能满足您的需求。

通过 Getting Started with GSAP ,了解有关 GSAP 的更多信息。

最终实现目标效果,如下:

在这里插入图片描述


三、实现方案

Step 1:分析需求

  • 该组件接收一个数字用于展示,在这个数字的值改变后,需要展示一个过场动画,并在指定的时间后滚动至新的数字。
  • 在动画过程中,如果传入数字再次改变,重新播放滚动效果并滚动至新的数字。
  • 如果数字中包含负号、小数点或千分位,符号不进行滚动。

Step 2:实现单个数字的滚动效果

1. 动画分析

首先,HTML 元素应该由两部分组成:一个宽高固定并隐藏溢出部分的舞台,一个从 0 到 9 的数字列表,不停移动数字列表即可实现基础的数字滚动效果;

在大部分场景下,需要让用户产生数字滚动速度快到模糊的感觉,可以给滚动中的数字加上一层模糊滤镜,在滚动时间结束后再撤除;

在滚动时间结束后,需要将特定的数字显示到舞台正中的位置,为了制造一种自然滚动直至停下的感觉,这里有两种做法:

  • 视图驱动:将动画时间分为两个部分,第一个部分将数字列表滚动 n 圈,第二部分从当前位置缓慢滚动至目标数字,在这整个过程中,数字的滚动速度应该是从一个精准计算的初始速度不断递减直至为零的。

  • 数据驱动:预留一个较短的时间播放收尾的动画,用较长的时间以恒定的、较快的速度无限滚动列表,时间到后,不论列表目前滚动到哪个位置,立即将列表移动到目标数字附近并播放一个收尾动画。

2. 准备静态的 HTML 元素

准备舞台和数字列表:

const NumberScrollItem = () => {
  const numbers = new Array(10)
    .fill(null)
    .map((_, index) => index)
    .concat(0)
  return (
    <div className="number-scroll__stage">
      <div className="number-scroll__gimmick ">
        {numbers.map((number) => (
          <div key={uuidV4()}>{number}</div>
        ))}
      </div>
    </div>
  )
}

在这里插入图片描述
提示:这里我们在 0~9 的末尾额外添加了一个 0 ,这样做的目的是让列表从尾部(显示数字 9)切换到头部(显示数字 0)的过程不那么生硬。

3. 让数字列表滚动起来

这里用到的 GSAP API 是 gsap.fromTo()。

只需指定 fromVars(动画的起始属性)、toVars(动画的目标属性)和 duration 即可让 gsap 补齐中间的动画过程,完成一个基础的滚动效果。

const COUNT_OF_NUMBERS = 10 // 隐藏在舞台外的数字个数
const HEIGHT_PER_NUMBER = 32 // 每个数字所占高度
const ONE_LAP = HEIGHT_PER_NUMBER * COUNT_OF_NUMBERS // 列表滚动一圈的位移

const useNumberScroll = (dependencies = []) => {
  const ref = useRef()

  const animate = () => {
    const gimmicSelector = '.number-scroll__gimmick'
    gsap.fromTo(
      gimmicSelector,
      { y: 0 },
      {
        y: -ONE_LAP,
        duration: 3,
        ease: 'none',
      }
    )
  }

  useGSAP({
    animate,
    ref,
    dependencies,
  })

  return ref
}

const NumberScrollItem = () => {
  const ref = useNumberScroll()
  return (
    <div className="number-scroll__stage" ref={ref}>
      {/* ... */}
    </div>
  )
}

提示:useGSAP 是基于 useLayoutEffect 封装的 hook:

const useGSAP = ({ animate, ref, dependencies = [] }) => {
  return useLayoutEffect(() => {
    let ctx = gsap.context(animate, ref)
    return () => {
      ctx.revert()
    }
  }, dependencies)
}

gsap.context 指定了动画的上下文,在 animate 函数中书写的选择器都将作用于指定的元素范围内.

并且其中出现的所有 gsap animation 都可以通过 ctx.revert() 或 ctx.kill() 统一重置或销毁,而无需手动跟踪一堆变量、数组。

在这里插入图片描述
4.让数字列表可以无限滚动

repeat 属性可以控制动画重复的次数,-1 代表无限次。

const animate = () => {
  gsap.fromTo(
    gimmicSelector,
    { y: 0 },
    {
      y: -ONE_LAP,
      duration: 3,
      ease: 'none',
      repeat: -1, // 无限重复
    }
  )
}

提示:duration 表示每一次的动画时间,而不是总的动画时间。

在这里插入图片描述

5. 给滚动中的数字加上一层模糊滤镜

为了让数字在滚动的过程中给用户一种滚动得非常快的感觉,我们需要设置一个较大的滚动速度,并且给滚动中的数字加上一层高斯模糊。

如何控制 gsap 的运动速度取决于我们传入的位移 y 和时间 duration,相当于:speed = y / duration,在 y 确定的情况下,计算一个 duration 传给 gsap 即可。

高斯模糊(利用 CSS filter属性实现) 通常我们可以简单地使用 blur() 函数产生高斯模糊,比如:filter: blur(1px),这里的 1px 指定了 SVG 滤镜 feGaussianBlur的标准差。

const getScrollSpeed = () => 320 // 设置一个较快的速度即可,单位:px / s
const getDuration = (s, v) => s / v // 时间 = 位移 / 速度

const animate = () => {
  gsap.fromTo(
    gimmicSelector,
    { y: 0 },
    {
      y: -ONE_LAP,
      duration: getDuration(ONE_LAP, getScrollSpeed()),
      ease: 'none',
      repeat: -1, // 无限重复
      onStart() {
        // 添加模糊滤镜
        gsap.set(gimmicSelector, {
          css: {
            filter: 'blur(1px)',
          },
        })
      },
    }
  )
}

在这里插入图片描述

6. 让数字列表停止滚动

在做动画分析时,我们提到了缓慢停下急停两种停止滚动的做法,这里我们都尝试一下。

  • 视图驱动

缓慢停下是个很符合现实逻辑的做法,整个运动过程相当于一个匀减速直线运动。

要让数字在指定的时间正好滚动至指定位置并停下,我们就需要设定一个初始速度和负的加速度,使运动结束时速度正好等于零;并且总位移在减去从初始值滚至目标值的部分后,必须是滚动一圈的整数倍,否则停止的位置就会偏离我们的目标值。

下面我们用公式梳理下上述条件:

假设初始速度为 Vο,加速度为 a,总位移为 s,运动时间为 t,至少需要位移的距离为 S1,数字滚动一圈的距离为 S2,则:

  • 指定时间后停下:Vο + at = 0;
  • 停下时总位移减去必要的位移正好是滚动一圈的整数倍:(s - S1) % S2 = 0;
  • 带入位移公式 s = Vοt + at² / 2 可以得到:(Vοt / 2 - S1) % S2 = 0。

其中,S2 是常量,t 和 S1 是已知的,求 Vο 的解,显然 Vο 不止一个解,选择一个较大的速度作为初始运动速度即可。

在确定 Vο 后,带入 a = -(Vο / t) 即可算出对应的加速度。

这一做法确实是可行的,但是显而易见,这种做法会极大增加我们代码的实现难度,而且很难满足一些常见的需求,比如多位数的滚动速度保持一致(因为每个数字的 S1 都不相同)。

  • 数据驱动

与缓慢停下的做法相比,急停的做法无疑简单许多:

1)设定一个播放收尾动画的时间,比如总时长的 75% 播放无限滚动,25% 播放收尾动画;

2)通过 onUpdate(每次动画更新时触发)和 this.pause() 控制无限滚动的时间;

3)为 NumberScrollItem 绑定一个 data-value 属性,表示最后滚动到哪个数字;

4)停止无限滚动后:

  • 撤除滤镜;
  • 通过 HTMLElement.dataset 获取目标数字的值;
  • 用剩下的时间播放一个从目标数字上方滚动至目标数字的动画,使用 gsap Eases 中内置的时间函数 elastic.out()( 类似于 CSS animation-timing-function)使动画效果更有弹性。

提示:第 2 步提到的 this 对应一个 Tween,可以简单地把它理解为一个高性能的属性设置器。

const NumberScrollItem = (props) => {
  const ref = useNumberScroll({
    duration: 5,
  })
  return (
    <div className="number-scroll__stage" ref={ref}>
      <div className="number-scroll__gimmick" data-value={0}>
        {/* ... */}
      </div>
    </div>
  )
}

const ENDING_ANIMATION_PERCENT = 0.25 // 收尾动画的时间占比

const useNumberScroll = ({ duration }, dependencies = []) => {
  const startTime = useRef() // 用于控制无限滚动的时间
  const endingDuration = min([duration * ENDING_ANIMATION_PERCENT, 2]) // 收尾动画最多播放 2 秒
  const loopDuration = duration - endingDuration

  const endAnimate = (ctx) => {
    // 停止无限滚动
    ctx.pause()
    const ele = ctx.targets()[0]
    // 获取目标数字的值
    const value = +ele.dataset.value
    const valueOffset = value === 0 ? 10 : value
    // 撤除滤镜
    gsap.set(ele, {
      css: {
        filter: '',
      },
    })
    // 播放一个从目标数字上方滚动至目标数字的动画
    gsap.fromTo(
      ele,
      {
        y: -((valueOffset - 1) * HEIGHT_PER_NUMBER), // 目标数字的上一个数字
      },
      {
        y: -(valueOffset * HEIGHT_PER_NUMBER),
        duration: endingDuration,
        ease: 'elastic.out(1, 0.3)',
      }
    )
  }

  const animate = () => {
    gsap.fromTo(
      gimmicSelector,
      { y: 0 },
      {
        onStart() {
          startTime.current = Date.now()
        },
        onUpdate() {
          const isEnd = Date.now() - startTime.current >= loopDuration * 1000
          if (isEnd) endAnimate(this)
        },
      }
    )
  }
}

Step 3:组件接口设计

根据需求可以很容易设计出组件接口:

在这里插入图片描述

NumberScroll.propTypes = {
  number: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  duration: PropTypes.number,
}

NumberScroll.defaultProps = {
  number: 0,
  duration: 5,
}

客户端代码:

function App() {
  return (
    <div className="App">
      <NumberScroll number={'1,231,232.00'} duration={5} />
    </div>
  )
}

Step 4:完善组件

  • 支持多位数

1)将传入数字拆分为一个数组;

2)将数组渲染为多个<NumberScroll / >;

3)将参数 value 绑定至 data-value。

const NumberScroll = (props) => {
  const numbers = String(props.number).split('')

  return (
    <div className="number-scroll">
      {numbers.map((item) => (
        <NumberScrollItem key={uuidV4()} value={+item} />
      ))}
    </div>
  )

在这里插入图片描述

  • 支持负数、浮点数、千分位数字

1)新建组件 SymbolItem 用于显示符号;

2)添加判断数字的方法;

3)条件渲染不同的组件。

const SymbolItem = (props) => {
  const { value } = props

  return (
    <div className="number-scroll__stage">
      <div className="number-scroll__gimmick ">{value}</div>
    </div>
  )
}

const isNumber = (value) => /^\d$/g.test(value)

const NumberScroll = (props) => {
  return (
    <div className="number-scroll">
      {numbers.map((item) =>
        isNumber(item) ? (
          <NumberScrollItem key={uuidV4()} value={+item} />
        ) : (
          <SymbolItem key={uuidV4()} value={item} />
        )
      )}
    </div>
  )
}

在这里插入图片描述

  • 支持在传入数字改变时重新滚动标题

1)将 useNumberScroll 从提升至 (避免 props.number 更改时,某位数的值和更改前一样,导致个别数字没有触发动画);

2)将 props.number 作为触发动画的依赖项;

3)为添加一个 className 以区分数字和符号;

4)传给 gsap.fromTo 的选择器只选择类型为数字的子项,使用 ctx.targets().forEach() 逐个为选中的元素添加结尾动画。

const NumberScroll = (props) => {
  // 提升至父组件
  const ref = useNumberScroll(
    {
      duration: props.duration,
    },
    [props.number]
  )

  return (
    <div className="number-scroll" ref={ref}>
      {/* ... */}
    </div>
  )
}

const NumberScrollItem = (props) => {
  return (
    <div className="number-scroll__stage">
      <div
        className="number-scroll__gimmick number-scroll__gimmick--scroll"
        data-value={props.value}
        style={{
          transform: `translateY(-${props.value * HEIGHT_PER_NUMBER}px)`,
        }}
      >
        {/* ... */}
      </div>
    </div>
  )
}

const endAnimate = (ctx) => {
  // 为所有类型为数字的子项添加收尾动画
  ctx.targets().forEach((ele) => {
    const value = +ele.dataset.value
    // end animate
  })
}

const animate = () => {
  // 只选择类型为数字的子项
  const gimmicSelector = '.number-scroll__gimmick--scroll'
  gsap
    .fromTo
    // ...
    ()
}

在这里插入图片描述
经过上述的操作,我们的数字滚动效果就出来了。

那 GSAP 作为一个从 Flash 时代一直发展到今天的专业动画库,是一个不折不扣的“老将”,但在性能上非常出色。

我们希望,通过本期内容的分享,能够让初级前端开发者能使用 GSAP替代 CSS Animation ,实现一些简单的特效以获得更优的开发体验和性能表现,并尽可能减少学习成本。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/154733.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[MySQL从入门到实战环境部署](超详细版)

MySQL从入门到实战环境部署1.部署CentOS1.1部署CenOS虚拟机步骤&#xff08;1&#xff09;基于VirtualBox&#xff08;2&#xff09;下载CentOS1.2环境部署过程2.部署MySQL1.部署CentOS 1.1部署CenOS虚拟机步骤 &#xff08;1&#xff09;基于VirtualBox 下载网址&#xff1…

Docker Compose:Docker Compose部署nacos初始化MySQL

Docker Compose&#xff1a;Docker Compose部署nacos初始化MySQL找初始化sql文件nacos初始化mysql-schema.sql文件内容docker-compose.yml上传到挂载目录运行docker-compose.yml访问nacos找初始化sql文件 先去官网下载nacos安装包 官方github地址&#xff1a;https://github.…

Centos7安装opengauss

安装包下载地址&#xff1a;https://www.opengauss.org/zh/download/注&#xff1a;本文介绍的是轻量版安装先创建一个系统用户&#xff08;opengauss数据库不允许使用 root 用户安装&#xff09;创建用户useradd omm设置密码passwd omm将安装包拷贝并解压到用户家目录 ~/openG…

linux-云服务器数据盘挂载失败导致进入维护模式

已经在华为云、AWS上面吃过这个亏了&#xff0c;老这样可不好&#xff0c;心怦怦跳的。 华为云是由于服务器升级配置后重启&#xff0c;数据盘名称变化导致进入维护模式。AWS则是由于重启后没有挂载上数据盘&#xff0c;手动编辑/etc/fstab文件错误导致进入维护模式。 究其原…

2022年航空发动机行业研究报告

第一章 行业概况 航空发动机制造指主要用来产生拉力或推力使飞机前进的发动机设备。除了产生前进力外&#xff0c;还可以为飞机上的用电设备提供电力&#xff0c;为空调设备等用气设备提供气源。航空发动机制造产业链包括原材料研发、零部件生产制造、分系统和整机制造。 原材…

大智慧同花顺Level2行情数据有什么用

股市L2是大智慧Level2数据。由“上海证券交易所”最新推出的实时行情信息收费服务&#xff0c;主要提供在上海证券交易所上市交易的证券产品的实时交易数据。该行情速度比传统行情快3秒以上&#xff0c;同时包括十档行情、买卖队列、逐笔成交、总买总卖和统计信息等多种新式数据…

Fabric.js 拖放元素进画布

本文简介 点赞 关注 收藏 学会了 学习 Fabric.js&#xff0c;我的建议是看文档不如看 demo。 本文实现的功能&#xff1a;将元素拖进到画布中并生成对应的图形或图片。 效果如下图所示&#xff1a; 思路 要实现以上效果&#xff0c;需要考虑以下几点&#xff1a; 元素有…

婴儿游泳池行业市场经营管理及未来前景展望分析

2023-2029年中国婴儿游泳池行业市场经营管理及未来前景展望报告报告编号&#xff1a;1691316免费目录下载&#xff1a;http://www.cninfo360.com/yjbg/qthy/ly/20230109/1691316.html本报告著作权归博研咨询所有&#xff0c;未经书面许可&#xff0c;任何组织和个人不得以任何形…

PyQt6快速入门-事件处理

事件处理 文章目录 事件处理1、Qt事件介绍2、常用事件函数2.1 paintEvent事件2.2 鼠标事件2.3 窗口大小改变事件2.4 窗口隐藏/关闭/显示事件2.5 键盘按键事件3、事件拦截4、事件过滤器5、事件队列与事件处理1、Qt事件介绍 Qt GUI应用程序的核心是 QApplication 类。 每个GUI应…

Linux 文件 I/O

1.Linux 应用编程中最基础的知识&#xff0c;即文件 I/O&#xff08;Input、 Outout&#xff09; &#xff0c; 文件 I/O 指的是对文件的输入/输出操作&#xff0c;说白了就是对文件的读写操作&#xff1b; Linux 下一切皆文件&#xff0c;文件作为 Linux 系统设计思想的核心理…

java Lambda表达式引用类方法

Lambda表达式和方法引用是一对孪生兄弟 而引用类方法是Lambda支持的方法引用中的一种 引用类方法其实就是引用类的静态方法 直接上代码 首先 我们要创建一个包 包下创建一个接口 我这里叫subInterface 参考代码如下 public interface subInterface {int convelutl(String s…

【RabbitMQ】SpringBoot整合RabbitMQ

文章目录搭建初始环境引入依赖配置配置文件HelloWorld模型使用Work模型使用Fanout 广播模型Route 路由模型Topic 订阅模型(动态路由模型)搭建初始环境 引入依赖 <!--引入与rabbitmq集成依赖--> <dependency><groupId>org.springframework.boot</groupId…

NKOJ P3549 可见的点

分析 这道题乍一看是一道几何,实际上,是一道法雷数列模板题; 首先,他让我们求有多少条可见的线,实际上是让我们求有多少种不同的斜率可以存在,而斜率就是表现为yx\Large\frac{y}{x}xy​的形式;可以发现,只有当yx\Large\frac{y}{x}xy​为最简分数时,才能算作一条可见的线,其他…

Linux中如何使用Htop监控工具?【网络安全】

一、Htop界面展示 “Htop是一个用于Linux/Unix系统的交互式实时进程监控应用程序&#xff0c;也是top命令的替代品&#xff0c;它是所有Linux操作系统上预装的默认进程监控工具。 Htop还有许多其他用户友好的功能&#xff0c;这些功能在top命令下不可用 在Htop中&#xff0c;…

蓝桥杯省赛习题练习(二)

题目来源&#xff1a;2020年真题题集&#xff08;B组&#xff09; 注&#xff1a;代码都是自己写的&#xff0c;不是参考答案&#xff01; 目录1. 门牌制作运行结果2. 既约分数运行结果3. 蛇形填数运行结果4. 跑步锻炼运行结果5. 7段码6. 成绩统计运行结果7. 回文日期1. 门牌制…

5.9、TCP报文段的首部格式

为了实现可靠传输&#xff0c;TCP 采用了面向字节流\color{red}面向字节流面向字节流的方式。 但 TCP 在发送数据时&#xff0c;是从发送缓存取出一部分或全部字节并给其添加一个首部使之成为 TCP报文段\color{red}\texttt{TCP} 报文段TCP报文段后进行发送。 一个 TCP 报文段由…

ELAN设计理念:通过梯度路径分析设计网络设计策略

设计高效、高质量的表达性网络架构一直是深度学习领域最重要的研究课题。当今的大多数网络设计策略都集中于如何集成从不同层提取的特征&#xff0c;以及如何设计计算单元来有效地提取这些特征&#xff0c;从而增强网络的表现力。本文提出了一种新的网络设计策略&#xff0c;即…

Django框架MVT模型工作流程

Django 一、Django介绍 Django是一个开源的Web应用框架&#xff0c;由Python写成。采用了MTV的框架模式&#xff0c;它最初是被用来做CMS&#xff08;内容管理系统&#xff09;软件。 使用Django&#xff0c;程序员可以方便、快捷地创建高品质、易维护、数据库驱动的应用程序…

聚威新材在科创板被暂缓审议:毛利率高于同行,张天荣为董事长

2023年1月10日&#xff0c;上海证券交易所披露的信息显示&#xff0c;上海聚威新材料股份有限公司&#xff08;下称“聚威新材”&#xff09;被科创板上市委员会暂缓审议。据贝多财经了解&#xff0c;聚威新材仍有多个问题需进一步落实事项。 上市委要求聚威新材落实&#xff1…

YOLOv5进行水域游泳者检测系统源码+标注好的数据集

YoloV5对水域中游泳者进行检测 一、说明 1.1 项目说明 完整代码下载地址&#xff1a;YOLOv5进行水域游泳者检测系统源码标注好的数据集 本项目是使用 YoloV5 来训练自己的数据集&#xff0c;所以仅仅展示训练自定义数据集的过程。 本次视频检测的结果已经上传至B站&#x…