重構:banner 中 logo 聚合分散動畫

来源:https://www.cnblogs.com/EnSnail/archive/2023/03/16/17221171.html
-Advertisement-
Play Games

1. 效果展示 線上查看 2. 開始前說明 效果實現參考源碼:Logo 聚集與散開 原效果代碼基於 react jsx 類組件實現。依賴舊,代碼冗餘。 我將基於此進行重構,重構目標: 基於最新依賴包,用 ts + hook 實現效果 簡化 dom 結構及樣式 支持響應式 重構應該在還原的基礎上,用更 ...


1. 效果展示


img

線上查看

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;

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 近期將ERP後臺從MSSQL SERVER過渡到了MYSQL,確實經歷了一番波折,轉換過程雖然極其痛苦,這裡也不賣慘了。將過程記錄一下,有人願意的話共同學習。 前面分享過操作系統和資料庫的安裝,倒是沒啥需要註意的地方,前面說的極其痛苦,是從數據導完開始的,暫時還體會不到,本篇介紹一下如何將... ...
  • 作者:京東零售 佟恩 NutUI 是一款京東風格的移動端組件庫。NutUI 目前支持 Vue 和 React技術棧,支持Taro多端適配。 本次,是2月的一個示例輸出,希望對你有幫助! 2月,我們對組件交互、issue修複、增加示例上做了急行軍,共合併70+PR,修複近40個issue。這裡我們選取 ...
  • Notion是一個功能強大的筆記應用程式,有許多優點,包括: 用戶友好的界面 跨平臺支持 可以結構化組織筆記 多人協作 可以添加多種類型的媒體文件 可以添加評論和任務 這些優點使Notion成為一個廣泛使用的筆記應用程式,適用於個人和團隊使用。 但是,對於重度Notion用戶,想直接發佈文章到公眾號 ...
  • Vue框架快速上手 前端環境準備 編碼工具:VSCode 依賴管理:NPM 項目構建:VueCli Vue框架介紹 Vue是一套用於構建用戶界面的漸進式框架。 Vue.js提供了MVVM數據綁定和一個可組合的組件系統,具有簡單、靈活的API。 其目標是通過儘可能簡單的API實現響應式的數據綁定和可組 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 你是否知道,JavaScript中有一種原生的方法來做對象的深拷貝? 本文我們要介紹的是 structuredClone 函數,它是內置在 JavaScript 運行時中的: const calendarEvent = { title: ...
  • hash 和 history 區別: 外觀上:hash的路由在url中帶有#號 功能上: hash雖然在url中,但是請求不會包裹它,對後端不會產生任何影響,改變hash不會重新載入頁面。 history是利用了html5 history interface中新增的pushState()和repla ...
  • JavaScript 實現非同步任務迴圈順序執行 需求場景:數組的元素作為非同步任務的參數,迴圈遍歷該數組,並執行非同步任務。 一、錯誤的實現 簡單的錯誤實現 // 非同步任務的參數數組 const arr = [1, 2, 3, 4]; // 非同步任務函數 function task(params, ca ...
  • 迭代器模式(Iterator Pattern):提供一種方法順序訪問一個聚合對象中的各個元素,而不需要暴露該對象的內部表示。在JavaScript中,可以使用迭代器模式來操作數組或類數組對象。 在迭代器模式中,集合對象包含一個方法,用於返回一個迭代器,該迭代器可以按順序訪問該集合中的元素。迭代器提供 ...
一周排行
    -Advertisement-
    Play Games
  • Dapr Outbox 是1.12中的功能。 本文只介紹Dapr Outbox 執行流程,Dapr Outbox基本用法請閱讀官方文檔 。本文中appID=order-processor,topic=orders 本文前提知識:熟悉Dapr狀態管理、Dapr發佈訂閱和Outbox 模式。 Outbo ...
  • 引言 在前幾章我們深度講解了單元測試和集成測試的基礎知識,這一章我們來講解一下代碼覆蓋率,代碼覆蓋率是單元測試運行的度量值,覆蓋率通常以百分比表示,用於衡量代碼被測試覆蓋的程度,幫助開發人員評估測試用例的質量和代碼的健壯性。常見的覆蓋率包括語句覆蓋率(Line Coverage)、分支覆蓋率(Bra ...
  • 前言 本文介紹瞭如何使用S7.NET庫實現對西門子PLC DB塊數據的讀寫,記錄了使用電腦模擬,模擬PLC,自至完成測試的詳細流程,並重點介紹了在這個過程中的易錯點,供參考。 用到的軟體: 1.Windows環境下鏈路層網路訪問的行業標準工具(WinPcap_4_1_3.exe)下載鏈接:http ...
  • 從依賴倒置原則(Dependency Inversion Principle, DIP)到控制反轉(Inversion of Control, IoC)再到依賴註入(Dependency Injection, DI)的演進過程,我們可以理解為一種逐步抽象和解耦的設計思想。這種思想在C#等面向對象的編 ...
  • 關於Python中的私有屬性和私有方法 Python對於類的成員沒有嚴格的訪問控制限制,這與其他面相對對象語言有區別。關於私有屬性和私有方法,有如下要點: 1、通常我們約定,兩個下劃線開頭的屬性是私有的(private)。其他為公共的(public); 2、類內部可以訪問私有屬性(方法); 3、類外 ...
  • C++ 訪問說明符 訪問說明符是 C++ 中控制類成員(屬性和方法)可訪問性的關鍵字。它們用於封裝類數據並保護其免受意外修改或濫用。 三種訪問說明符: public:允許從類外部的任何地方訪問成員。 private:僅允許在類內部訪問成員。 protected:允許在類內部及其派生類中訪問成員。 示 ...
  • 寫這個隨筆說一下C++的static_cast和dynamic_cast用在子類與父類的指針轉換時的一些事宜。首先,【static_cast,dynamic_cast】【父類指針,子類指針】,兩兩一組,共有4種組合:用 static_cast 父類轉子類、用 static_cast 子類轉父類、使用 ...
  • /******************************************************************************************************** * * * 設計雙向鏈表的介面 * * * * Copyright (c) 2023-2 ...
  • 相信接觸過spring做開發的小伙伴們一定使用過@ComponentScan註解 @ComponentScan("com.wangm.lifecycle") public class AppConfig { } @ComponentScan指定basePackage,將包下的類按照一定規則註冊成Be ...
  • 操作系統 :CentOS 7.6_x64 opensips版本: 2.4.9 python版本:2.7.5 python作為腳本語言,使用起來很方便,查了下opensips的文檔,支持使用python腳本寫邏輯代碼。今天整理下CentOS7環境下opensips2.4.9的python模塊筆記及使用 ...