• 重构:banner 中 logo 聚合分散动画


    1. 效果展示


    img

    在线查看

    2. 开始前说明

    效果实现参考源码:Logo 聚集与散开

    原效果代码基于 react jsx 类组件实现。依赖旧,代码冗余。

    我将基于此进行重构,重构目标:

    • 基于最新依赖包,用 ts + hook 实现效果
    • 简化 dom 结构及样式
    • 支持响应式

    重构应该在还原的基础上,用更好的方式实现相同的效果。如果能让功能更完善,那就更好了。

    在重构的过程中,注意理解:

    • 严格模式
    • 获取不到最新数据,setState 异步更新,useRef 同步最新数据
    • 类组件生命周期,如何转换为 hook
    • canvas 上绘图获取图像数据,并对数据进行处理

    3. 重构

    说明:后面都是代码,对代码感兴趣的可以与源码比较一下;对效果感兴趣的,希望对你有帮助!

    脚手架:vite-react+ts

    3.1 删除多余文件及代码,只留最简单的结构

    • 修改入口文件 main.tsx 为:
    import ReactDOM from "react-dom/client";
    import App from "./App";
    
    ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
      <App />
    );
    

    注意:这儿删除了严格模式

    • 删除 index.css

    • 修改 App.tsx 为:

    import "./App.css";
    
    function App() {
      return (
        <div className="App">
          
        div>
      );
    }
    
    export default App;
    
    • 修改 App.css 为:
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    

    3.3 安装依赖

    yarn add rc-tween-one lodash-es -S
    yarn add @types/lodash-es -D
    

    rc-tween-oneAnt Motion 的一个动效组件

    3.4 重构代码

    APP.tsx

    import TweenOne from "rc-tween-one";
    import LogoAnimate from "./logoAnimate";
    import "./App.css";
    
    function App() {
      return (
        <div className="App">
          <div className="banner">
            <div className="content">
              <TweenOne
                animation={{ opacity: 0, y: -30, type: "from", delay: 500 }}
                className="title"
              >
                logo 聚合分散
              TweenOne>
            div>
    
            <LogoAnimate />
          div>
        div>
      );
    }
    
    export default App;
    

    App.css

    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    .banner {
      width: 100%;
      height: 100vh;
      overflow: hidden;
      background: linear-gradient(135deg, #35aef8 0%, #7681ff 76%, #7681ff 76%);
      position: relative;
      display: flex;
      align-items: center;
      justify-content: space-evenly;
    }
    
    .banner .content {
      height: 35%;
      color: #fff;
    }
    .banner .content .title {
      font-size: 40px;
      background: linear-gradient(yellow, white);
      -webkit-background-clip: text;
      color: transparent;
    }
    
    .banner .logo-box {
      width: 300px;
      height: 330px;
    }
    .banner .logo-box * {
      pointer-events: none;
    }
    .banner .logo-box img {
      margin-left: 70px;
      transform: scale(1.5);
      margin-top: 60px;
      opacity: 0.4;
    }
    .banner .logo-box .point-wrap {
      position: absolute;
    }
    .banner .logo-box .point-wrap .point {
      border-radius: 100%;
    }
    
    @media screen and (max-width: 767px) {
      .banner {
        flex-direction: column;
      }
      .banner .content {
        order: 1;
      }
    }
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }
    
    .banner {
      width: 100%;
      height: 100vh;
      overflow: hidden;
      background: linear-gradient(135deg, #35aef8 0%, #7681ff 76%, #7681ff 76%);
      position: relative;
      display: flex;
      align-items: center;
      justify-content: space-evenly;
    }
    
    .banner .content {
      height: 35%;
      color: #fff;
    }
    .banner .content .title {
      font-size: 30px;
    }
    
    .banner .logo-box {
      width: 300px;
      height: 330px;
    }
    .banner .logo-box * {
      pointer-events: none;
    }
    .banner .logo-box img {
      margin-left: 70px;
      transform: scale(1.5);
      margin-top: 60px;
      opacity: 0.4;
    }
    .banner .logo-box .point-wrap {
      position: absolute;
    }
    .banner .logo-box .point-wrap .point {
      border-radius: 100%;
    }
    
    @media screen and (max-width: 767px) {
      .banner {
        flex-direction: column;
      }
      .banner .content {
        order: 1;
      }
    }
    

    重点重构文件 logoAnimate.tsx

    import React, { useRef, useState, useEffect } from "react";
    import TweenOne, { Ticker } from "rc-tween-one";
    import type { IAnimObject } from "rc-tween-one";
    import { cloneDeep, delay } from "lodash-es";
    
    type Point = {
      wrapStyle: {
        left: number;
        top: number;
      };
      style: {
        width: number;
        height: number;
        opacity: number;
        backgroundColor: string;
      };
      animation: IAnimObject;
    };
    
    const logoAnimate = () => {
      const data = {
        image:
          "https://imagev2.xmcdn.com/storages/f390-audiofreehighqps/4C/D1/GKwRIDoHwne3AABEqQH4FjLV.png",
        w: 200, // 图片实际的宽度
        h: 200, // 图片实际的高度
        scale: 1.5, // 显示时需要的缩放比例
        pointSizeMin: 10, // 显示时圆点最小的大小
      };
    
      const intervalRef = useRef<string | null>(null);
      const intervalTime = 5000;
      const initAnimateTime = 800;
    
      const logoBoxRef = useRef<HTMLDivElement>(null);
    
      // 聚合:true,保证永远拿到的是最新的数据,useState是异步的,在interval中拿不到
      const gatherRef = useRef(true);
    
      // 数据变更,促使dom变更
      const [points, setPoints] = useState<Point[]>([]);
    
      // 同步 points 数据,保证永远拿到的是最新的数据,useState是异步的,在interval中拿不到
      const pointsRef = useRef(points);
      useEffect(() => {
        pointsRef.current = points;
      }, [points]);
    
      const setDataToDom = (imgData: Uint8ClampedArray, w: number, h: number) => {
        const pointArr: { x: number; y: number; r: number }[] = [];
        const num = Math.round(w / 10);
        for (let i = 0; i < w; i += num) {
          for (let j = 0; j < h; j += num) {
            const index = (i + j * w) * 4 + 3;
            if (imgData[index] > 150) {
              pointArr.push({
                x: i,
                y: j,
                r: Math.random() * data.pointSizeMin + 12
              });
            }
          }
        }
    
        const newPoints = pointArr.map((item, i) => {
          const opacity = Math.random() * 0.4 + 0.1;
    
          const point: Point = {
            wrapStyle: { left: item.x * data.scale, top: item.y * data.scale },
            style: {
              width: item.r * data.scale,
              height: item.r * data.scale,
              opacity: opacity,
              backgroundColor: `rgb(${Math.round(Math.random() * 95 + 160)}, 255, 255)`,
            },
            animation: {
              y: (Math.random() * 2 - 1) * 10 || 5,
              x: (Math.random() * 2 - 1) * 5 || 2.5,
              delay: Math.random() * 1000,
              repeat: -1,
              duration: 3000,
              ease: "easeInOutQuad",
            },
          };
          return point;
        });
    
        delay(() => {
          setPoints(newPoints);
        }, initAnimateTime + 150);
    
        intervalRef.current = Ticker.interval(updateTweenData, intervalTime);
      };
    
      const createPointData = () => {
        const { w, h } = data;
    
        const canvas = document.createElement("canvas");
        const ctx = canvas.getContext("2d");
        if (!ctx) return;
    
        ctx.clearRect(0, 0, w, h);
        canvas.width = w;
        canvas.height = h;
    
        const img = new Image();
        img.crossOrigin = "anonymous";
        img.src = data.image;
        img.onload = () => {
          ctx.drawImage(img, 0, 0);
          const data = ctx.getImageData(0, 0, w, h).data;
          setDataToDom(data, w, h);
        };
      };
    
      useEffect(() => {
        createPointData();
    
        return () => {
          removeInterval();
        };
      }, []);
    
      // 分散数据
      const disperseData = () => {
        if (!logoBoxRef.current || !logoBoxRef.current.parentElement) return;
    
        const rect = logoBoxRef.current.parentElement.getBoundingClientRect();
        const boxRect = logoBoxRef.current.getBoundingClientRect();
        const boxTop = boxRect.top - rect.top;
        const boxLeft = boxRect.left - rect.left;
    
        const newPoints = cloneDeep(pointsRef.current).map((item) => ({
          ...item,
          animation: {
            x: Math.random() * rect.width - boxLeft - item.wrapStyle.left,
            y: Math.random() * rect.height - boxTop - item.wrapStyle.top,
            opacity: Math.random() * 0.2 + 0.1,
            scale: Math.random() * 2.4 + 0.1,
            duration: Math.random() * 500 + 500,
            ease: "easeInOutQuint",
          },
        }));
        setPoints(newPoints);
      };
    
      // 聚合数据
      const gatherData = () => {
        const newPoints = cloneDeep(pointsRef.current).map((item) => ({
          ...item,
          animation: {
            x: 0,
            y: 0,
            opacity: Math.random() * 0.2 + 0.1,
            scale: 1,
            delay: Math.random() * 500,
            duration: 800,
            ease: "easeInOutQuint",
          },
        }));
        setPoints(newPoints);
      };
    
      const updateTweenData = () => {
        gatherRef.current ? disperseData() : gatherData();
        gatherRef.current = !gatherRef.current;
      };
    
      const removeInterval = () => {
        if (intervalRef.current) {
          Ticker.clear(intervalRef.current);
          intervalRef.current = null;
        }
      };
      const onMouseEnter = () => {
        if (!gatherRef.current) {
          updateTweenData();
        }
        removeInterval();
      };
    
      const onMouseLeave = () => {
        if (gatherRef.current) {
          updateTweenData();
        }
        intervalRef.current = Ticker.interval(updateTweenData, intervalTime);
      };
    
      return (
        <>
          {points.length === 0 ? (
            <TweenOne
              className="logo-box"
              animation={{
                opacity: 0.8,
                scale: 1.5,
                rotate: 35,
                type: "from",
                duration: initAnimateTime,
              }}
            >
              <img key="img" src={data.image} alt="" />
            TweenOne>
          ) : (
            <TweenOne
              animation={{ opacity: 0, type: "from", duration: 800 }}
              className="logo-box"
              onMouseEnter={onMouseEnter}
              onMouseLeave={onMouseLeave}
              ref={logoBoxRef}
            >
              {points.map((item, i) => (
                <TweenOne className="point-wrap" key={i} style={item.wrapStyle}>
                  <TweenOne
                    className="point"
                    style={item.style}
                    animation={item.animation}
                  />
                TweenOne>
              ))}
            TweenOne>
          )}
        
      );
    };
    
    export default logoAnimate;
    
  • 相关阅读:
    【深度学习实践】文本图片去水印
    【无标题】
    计算机毕业设计SSM订餐系统【附源码数据库】
    第二十一章 构建和配置 Nginx (UNIX® Linux macOS) - 为CSP构建Nginx的过程
    大家都能看得懂的源码之ahooks useInfiniteScroll
    JFROG CLI改为API Key方式上传
    .net6+aspose.words导出word并转pdf
    PostgreSQL如何支持PL/Python过程语言
    [React] Context上下文的使用
    【代码精读】optee中如何添加一个外设
  • 原文地址:https://www.cnblogs.com/EnSnail/p/17221171.html