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

Posted ESnail

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了重构:banner 中 logo 聚合分散动画相关的知识,希望对你有一定的参考价值。

1. 效果展示


在线查看

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  />
        </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;

以上是关于重构:banner 中 logo 聚合分散动画的主要内容,如果未能解决你的问题,请参考以下文章

css图片显示问题:banner里的图片在浏览器显示不出来,但是在DW里能显示,上面的logo就没问题

分散收集(克隆+聚合)不起作用

springboot 初始动画

模仿东京首页banner轮播,京东新闻上下滚动动画实现(动画实现)

如何把安卓开机动画,换成谷歌新logo

案例HTML5-立体式动画图片切换banner效果