草庐IT

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

ESnail 2023-03-28 原文

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

有关重构:banner 中 logo 聚合分散动画的更多相关文章

  1. ruby-on-rails - 如何重构 "shared"方法? - 2

    我正在使用RubyonRails3.2.2,我想从我的模型/类中“提取”一些方法。也就是说,在不止一个类/模型中,我有一些方法(注意:方法与用户授权相关,并被命名为“CRUD方式”),这些方法实际上是相同的;所以我认为DRY方法是将这些方法放在“共享”模块或类似的东西中。实现该目标的常见且正确的方法是什么?例如,我应该将“共享”代码放在哪里(在哪些目录和文件中)?如何在我的类/模型中包含提到的方法?你有什么建议?注意:我正在寻找“RubyonRails制作东西的方式”。 最佳答案 一种流行的方法是使用ActiveSupport关注点

  2. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

  3. ruby-on-rails - 在 haml View 中重构条件 - 2

    除了可访问性标准不鼓励使用这一事实指向当前页面的链接,我应该怎么做重构以下View代码?#navigation%ul.tabbed-ifcurrent_page?(new_profile_path)%li{:class=>"current_page_item"}=link_tot("new_profile"),new_profile_path-else%li=link_tot("new_profile"),new_profile_path-ifcurrent_page?(profiles_path)%li{:class=>"current_page_item"}=link_tot("p

  4. ruby - 需要重构为新的 Ruby 1.9 哈希语法 - 2

    这个问题在这里已经有了答案:HashsyntaxinRuby[duplicate](1个回答)关闭5年前。我有一个Recipe,其中包含以下未通过lint测试的代码:service'apache'dosupports:status=>true,:restart=>true,:reload=>trueend失败并出现错误:UsethenewRuby1.9hashsyntax.supports:status=>true,:restart=>true,:reload=>true不确定新语法是什么样的...有人可以帮忙吗?

  5. ruby - 重构条件变量赋值 - 2

    我正在做一个项目。目前我有一个相当大的条件语句,它根据一些输入参数为变量赋值。所以,我有这样的东西。ifsomeconditionx=somevalueelsifanotherconditionx=adifferentvalue...重构它的最佳方法是什么?我希望我最终会得到类似的东西x=somevalueifsomecondition||anothervalueifanothercondition这种事情有规律吗? 最佳答案 只需将赋值放在if之外即可。x=ifsomeconditionsomevalueelsifanotherc

  6. ruby - Rails Elasticsearch 聚合 - 2

    不知何故,我似乎无法获得包含我的聚合的响应...使用curl它按预期工作:HBZUMB01$curl-XPOST"http://localhost:9200/contents/_search"-d'{"size":0,"aggs":{"sport_count":{"value_count":{"field":"dwid"}}}}'我收到回复:{"took":4,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":90,"max_score":0.0,"hits":[]},"a

  7. c# - Ruby 等效于 C# Linq 聚合方法 - 2

    什么是Linq聚合方法的ruby​​等价物。它的工作原理是这样的varfactorial=new[]{1,2,3,4,5}.Aggregate((acc,i)=>acc*i);每次将数组序列中的值传递给lambda时,变量acc都会累积。 最佳答案 这在数学以及几乎所有编程语言中通常称为折叠。它是更普遍的变形概念的一个实例。Ruby从Smalltalk中继承了这个特性的名称,它被称为inject:into:(像aCollectioninject:aStartValueinto:aBlock一样使用。)所以,在Ruby中,它称为inj

  8. LVGL V8动画 - 2

    动画/*INITIALIZEANANIMATION 初始化一个动画*-----------------------*/lv_anim_ta;lv_anim_init(&a);/*MANDATORYSETTINGS 必选设置*------------------*//*Setthe"animator"function 设置“动画”功能*/lv_anim_set_exec_cb(&a,(lv_anim_exec_xcb_t)lv_obj_set_x);/*Setthe"animator"function*/lv_anim_set_var(&a,obj);/*Lengthoftheanim

  9. ruby - 重构 Ruby : Converting string array to int array - 2

    我正在重构一个西洋跳棋程序,我正在尝试将玩家移动请求(例如以“3、3、5、5”的形式)处理到一个int数组中。我有以下方法,但感觉不像我所知道的那样像Ruby:deftranslate_move_request_to_coordinates(move_request)return_array=[]coords_array=move_request.chomp.split(',')coords_array.each_with_indexdo|i,x|return_array[x]=i.to_iendreturn_arrayend我用它进行了以下RSpec测试。it"translatesa

  10. ruby - 如何重构这个 6 行方法以使其更具可读性? - 2

    我正试图在这里清理这个非常丑陋的方法,它迫切需要重构,但我不确定哪种结构最能做到这一点(即case语句,或者只是一个精心格式化的ifthen语句)乍一看,这似乎是一个理想的放置case语句的地方,带有一些放置得很好的when,但我的理解是case语句只能用于单个变量,而不是两个变量,以及使用散列或数组尝试这些语句的irb的各种摆弄在这里也没有太多说明。你会怎么做?在检查这样的多个bool值时,Ruby中是否有任何常见的技巧来避免这样的代码?defhas_just_one_kind_of_thing?(item,controller)if(controller=='foos'&&item

随机推荐