重构: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-one:Ant 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 聚合分散动画的更多相关文章
- 最简单实用的JQuery实现banner图中的text打字动画效果!!!
下面,就让小博详细介绍如何实现上面GIF实现的banner图中的文字动画效果,最简单实用的方法(鉴于代码量较小,就内嵌在一个HTML文件中了): 首先,我们要在header导入一个jQuery,并新建 ...
- Android5.1开机LOGO与开机动画【转】
本文转载自:http://blog.csdn.net/u014770862/article/details/52624627 android5.1中,开机LOGO部分和之前版本的并不相同,主要区别在于 ...
- jQuery中的事件与动画 (你的明天Via Via)
众所周知,页面在加载时,会触发load事件:当用户单击某个按钮时,会触发该按钮的click事件. 这些事件就像日常生活中,人们按下开关,灯就亮了(或者灭了),往游戏机里投入游戏币就可以启动游戏一样, ...
- ScrollMe – 在网页中加入各种滚动动画效果
ScrollMe 是一款 jQuery 插件,用于给网页添加简单的滚动效果.当你向下滚动页面的时候,ScrollMe 可以缩放,旋转和平移页面上的元素.它易于设置,不需要任何自定义的 JavaScri ...
- jQuery中的事件和动画——《锋利的jQuery》(第2版)读书笔记2
第4章 jQuery中的事件和动画 jQuery中的事件 加载DOM $(document).ready(function(){ // 编写代码... }); 可以简写成: $(function( ...
- CocoStudio基础教程(3)在程序中处理cocoStudio导出动画
1.概述 使用cocoStudio可以方便的制作动画,接下来的工作就是在我们的程序中使用制作的动画.这篇中,我将使用程序将两个动画连接起来 2.关联到项目 运行脚本创建我们的项目,将导出的动画.UI放 ...
- 在MongoDB中实现聚合函数 (转)
随着组织产生的数据爆炸性增长,从GB到TB,从TB到PB,传统的数据库已经无法通过垂直扩展来管理如此之大数据.传统方法存储和处理数据的成本将会随着数据量增长而显著增加.这使得很多组织都在寻找一种经济的 ...
- jQuery中的事件与动画<思维导图>
Javascript和HTML之间的交互是通过用户和浏览器操作页面时引发的事件来处理的.当文档或者它的某些元素发生某些变化或操作时,浏览器会自动生成一个事件.例如当浏览器装载完一个文档后,会生成事件. ...
- 重构 ORM 中的 Sql 生成
Rafy 领域实体框架设计 - 重构 ORM 中的 Sql 生成 前言 Rafy 领域实体框架作为一个使用领域驱动设计作为指导思想的开发框架,必然要处理领域实体到数据库表之间的映射,即包含了 OR ...
- DedeCMS中实现在顶层banner中显示自定义登录信息
一.需求描述 dedeCMS自带的模板中有互动中心模块,如下图所示: 由于会员登陆对我来说不是网站的重要模块且默认DedeCMS的会员中心模块的初始化很慢,常会显示“正在载入中,请稍候...”, 所以 ...
随机推荐
- UniCode 下char*转CString ,利用MultiByteToWideChar进行转换,中文乱码的解决方案
//计算char *数组大小,以字节为单位,一个汉字占两个字节 int charLen = strlen(sText); //计算多字节字符的大小,按字符计算. int len = MultiByte ...
- 我的第三次JAVA作业
------------恢复内容开始------------ 1.对象与对象引用的区别是什么? 请举例说明 创建对象被分配在堆中,对象引用分配在栈中. eg. new FighterPlane(); ...
- 常用的DOS指令及部分快捷键
常用的DOS指令及部分快捷键 1.dos打开方式 win + R打开运行,输入cmd,打开dos 2.常用的Dos指令 a.切换盘号 方法 直接输入对应盘加" :" D: ...
- 关于html中元素和布局的笔记
一.元素类型 css标准文档流:默认的网页从左到右,从上到下的排列方式显示出网页效果 类型: 1.块级元素:(div,p,table--) a.独占一行 b.可以设置宽度和高度 c.可以设置左右居中( ...
- 记一次 windows 10 系统 idea 【ctrl + shift + f】快捷键失效的问题
快捷键失效,首先想到的就是和其它软件设置的快捷键冲突了,把其它软件都关了之后,发现还是不行.最后发现原来是搜狗输入法中设置了,关掉之后就可以了.
- Windows 10 ~ Jenkins 安装
首先: jenkins是由java写的,所以在使用之前请安装好JDK(最好安装JDK1.8) 下载jenkins.war包并放到一个自己创建的目录D:\jenkins下:https://mirrors ...
- OpenCV 4.5.2环境配置 + 图片灰度化处理
一,OpenCV环境配置 注意:以下配置内容为Android开发环境配置好的基础上的OpenCV配置环境 1.官网下载OpenCV的sdk包,下载的是4.5.2的Android版本 Releases ...
- 使用MailKit发送邮件
MailKit的项目地址:https://github.com/jstedfast/MailKit 使用: 1 定义发送邮件所需要的model或者dto,该model可根据个人的需要进行修改 1 pu ...
- cat、more、less、tail、head文件查看指令辨析
1.cat 简介 cat [OPTION]... [FILE]... cat 可以将多个文本连接起来并输出,当省略输入文件或输入文件用字符-替代时,读取标准输入 常用参数 -n \(~~~~\)输出行 ...
- JavaScript的Object.defineProperty( )方法
Object.defineProperty方法可以在一个对象上定义一个新的属性,或者修改该对象原有的属性,并返回该对象. 基础的语法格式如下: 1 var data = {}//定义一个对象 2 Ob ...