返回介绍

帧动画功能

发布于 2024-09-11 01:01:38 字数 9453 浏览 0 评论 0 收藏 0

抖音前端技术官网 的首页加载动画为例,看看这个动画下究竟发生了什么?

首先打开控制台的 network,使用 performance 来录制首页加载的过程,为了能更清晰查看,适当降低 CPU 的性能,调整为 4 x slowdown。

点击控制台左上角的 ⚪,然后刷新页面,可以得到下面的逐帧列表:

从下面的加载图中可以判断出,这个动画总的执行时长为 1.36 s,然后上面的列表中有具体页面加载过程的帧动画变化图,通过按帧查看,可以大概看出这个动画的执行顺序是这样的。

按照从小序列到大序列的顺序,每个元素分别执行了从下往上的平移操作,以及一个透明度从 0 到 1 的过程,加上上面看到每个动画的时长分析都是 1.3s,所以只是对每个元素推迟了不同的动画平移时间,但是它们享有相同的动画时长,针对这个场景应该怎么去实现呢?

针对现在的首页,把 dom 元素简单拆分为 8 个区域,总动画时长定成 1s,其中 1s 的时间可以分为 9 个时间帧,每个区域从对应序列的时间帧开始执行相同的动画效果,最后把所有的帧连起来就是一个完整的帧动画。

定义对应的样式进行绑定,以 fadeInDown1 举例,@keyframes 指向动画的逐帧状态,其中 0% 和 11 % 都是一样的内容,这时候区域处于 y 轴 40px 的位置,然后末尾状态是无区域状态和 1 透明度,这个动画的效果会使得动画从整体时间的 11% 开始执行,到 100 % 完成最终的变化。

这个 11% 是从哪里来的呢?上面提到为每个动画延迟一个帧频率执行,8 个区域,共 9 帧,所以 1 帧的占比为 11% 的总动画时长,每个动画的起始时间(第二个状态值)都比上一个高出 1 帧的比例,这样就可以将整体帧动画串联起来了。

styles/Home.module.scss

.withAnimation {
  .title {
    animation: fadeInDown1 1s;
  }

  .description {
    animation: fadeInDown2 1s;
  }

  .card:nth-of-type(1) {
    animation: fadeInDown3 1s;
  }

  .card:nth-of-type(2) {
    animation: fadeInDown4 1s;
  }

  .card:nth-of-type(3) {
    animation: fadeInDown5 1s;
  }

  .card:nth-of-type(4) {
    animation: fadeInDown6 1s;
  }

  .card:nth-of-type(5) {
    animation: fadeInDown7 1s;
  }

  .card:nth-of-type(6) {
    animation: fadeInDown8 1s;
  }
}

@keyframes fadeInDown1 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  11% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown2 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  22% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown3 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  33% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown4 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  44% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown5 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  55% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown6 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  66% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown7 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  77% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

@keyframes fadeInDown8 {
  0% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  88% {
    transform: translate3d(0, 40px, 0);
    opacity: 0;
  }

  100% {
    -webkit-transform: none;
    transform: none;
    opacity: 1;
  }
}

改造首页 (index.tsx) Dom 类,专门定义一个动画类来存放动画相关的样式,避免对基础样式造成污染。

import {useRef} from 'react';
import type { NextPage } from 'next';
import classNames from 'classnames';
import styles from '@/styles/Home.module.scss';

interface IHomeProps {
  title: string;
  description: string;
  list: {
    label: string;
    info: string;
    link: string;
  }[];
}

const Home: NextPage<IHomeProps> = ({
  title, description, list
}) => {
  const mainRef = useRef<HTMLDivElement>(null);

  return (
    <div className={styles.container}>
      <main className={classNames([styles.main, styles.withAnimation])} ref={mainRef}>
        <h1 className={styles.title}>{title}</h1>
        <p className={styles.description}>{description}</p>
        <div className={styles.grid}>
          {
            list?.map((item, index) => {
              return (
                <div key={index} className={styles.card} onClick={(): void => {
                  window.open(
                    item?.link,
                    "blank",
                    "noopener=yes,noreferrer=yes"
                  );
                }}>
                  <h2>{item?.label}</h2>
                  <p>{item?.info}</p>
                </div>
              )
            })
          }
        </div>
      </main>
    </div>
  )
}

Home.getInitialProps = (context) => {
  return {
    title: "Hello SSR!",
    description: "A Demo for 官网开发实战",
    list: [
      {
        label: "文章 1",
        info: "A test for article1",
        link: "http://localhost:3000/article/1",
      },
      {
        label: "文章 2",
        info: "A test for article2",
        link: "http://localhost:3000/article/2",
      },
      {
        label: "文章 3",
        info: "A test for article3",
        link: "http://localhost:3000/article/3",
      },
      {
        label: "文章 4",
        info: "A test for article4",
        link: "http://localhost:3000/article/4",
      },
      {
        label: "文章 5",
        info: "A test for article5",
        link: "http://localhost:3000/article/5",
      },
      {
        label: "文章 6",
        info: "A test for article6",
        link: "http://localhost:3000/article/6",
      },
    ],
  };

}

export default Home;

然后查看一下效果。

主动触发动画重新播放

在切换主题时,希望能再执行一次加载动画,可以通过 requestAnimationFrame 来实现,它会返回一个回调,强制浏览器在重绘前调用指定的函数来进行动画的更新。

使用这个来改造一下首页,加一个 useEffect 的钩子。

import {useRef, useContext, useEffect} from 'react';
import type { NextPage } from 'next';
import classNames from 'classnames';
import { ThemeContext } from '@/stores/theme';
import styles from '@/styles/Home.module.scss';

interface IHomeProps {
  title: string;
  description: string;
  list: {
    label: string;
    info: string;
    link: string;
  }[];
}

const Home: NextPage<IHomeProps> = ({
  title, description, list
}) => {
  const mainRef = useRef<HTMLDivElement>(null);
  const { theme } = useContext(ThemeContext);
  useEffect(() => {
    mainRef.current?.classList.remove(styles.withAnimation);
    window.requestAnimationFrame(() => {
      mainRef.current?.classList.add(styles.withAnimation);
    });
  }, [theme]);

  return (
    <div className={styles.container}>
      <main className={classNames([styles.main, styles.withAnimation])} ref={mainRef}>
        <h1 className={styles.title}>{title}</h1>
        <p className={styles.description}>{description}</p>
        <div className={styles.grid}>
          {
            list?.map((item, index) => {
              return (
                <div key={index} className={styles.card} onClick={(): void => {
                  window.open(
                    item?.link,
                    "blank",
                    "noopener=yes,noreferrer=yes"
                  );
                }}>
                  <h2>{item?.label}</h2>
                  <p>{item?.info}</p>
                </div>
              )
            })
          }
        </div>
      </main>
    </div>
  )
}

Home.getInitialProps = (context) => {
  return {
    title: "Hello SSR!",
    description: "A Demo for 官网开发实战",
    list: [
      {
        label: "文章 1",
        info: "A test for article1",
        link: "http://localhost:3000/article/1",
      },
      {
        label: "文章 2",
        info: "A test for article2",
        link: "http://localhost:3000/article/2",
      },
      {
        label: "文章 3",
        info: "A test for article3",
        link: "http://localhost:3000/article/3",
      },
      {
        label: "文章 4",
        info: "A test for article4",
        link: "http://localhost:3000/article/4",
      },
      {
        label: "文章 5",
        info: "A test for article5",
        link: "http://localhost:3000/article/5",
      },
      {
        label: "文章 6",
        info: "A test for article6",
        link: "http://localhost:3000/article/6",
      },
    ],
  };

}

export default Home;

在每次 theme 发生变化的时候,主动移除对应的动画类,再通过 requestAnimationFrame 对动画类进重新绑定,达到主动触发动画刷新的效果,现在来看一下最终成品。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文