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;

最新文章

  1. problems during rovio build
  2. freeregex-0.01 使用文档
  3. 【leetcode】Gray Code (middle)
  4. jquery Jsonp 跨域访问
  5. $.trim()函数
  6. 2014 Multi-University Training Contest 10
  7. Json对象序列化与反序列化
  8. Spring Boot 中关于自定义异常处理的套路!
  9. C# Redis 过期机制不生效问题
  10. 前端图片缓存之通过img标签加载GIF只能播放一次问题(转载)
  11. BZOJ2631tree——LCT
  12. 【oneday_onepage】——Tastes differ!
  13. 删除github上个人的repositories的操作步骤
  14. 小型Http服务器
  15. solr学习之一 搜索基本知识
  16. PowerShell实现基于SharePoint的网站HomePage Auto-Create Solution
  17. Android 基础题目
  18. 如何提高 Java 中锁的性能
  19. halcon基础应用和方法经验分享
  20. 路由器如何设置上网(TP-LINK)

热门文章

  1. LoadRunner 常见错误
  2. Flink akka AskTimeoutException问题排查
  3. python菜鸟学习: 6. 字典常用方法
  4. Mysql学习:4、DML命令(增删改查)及密码修改
  5. web测试:test过程中接口报错 &quot;Object reference not set to an instance of an object.&quot;
  6. python机器学习——KNN(K近邻算法)
  7. vue后台管理系统——主页布局
  8. PyCharm 关闭代码风格检查
  9. Yarn API
  10. C#使用JSON相关