0

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

 1 year ago
source link: https://www.cnblogs.com/EnSnail/p/17221171.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

1. 效果展示

img

在线查看

2. 开始前说明

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

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

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

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

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

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

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

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

脚手架: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;

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK