From 3802ca357fa6d4c6d3c9616f4e80d7f25798c10b Mon Sep 17 00:00:00 2001 From: MythEclipse Date: Wed, 3 Jun 2026 03:07:51 +0700 Subject: [PATCH] feat(frontend): refactor ParticleBackground for performance and type safety --- services/frontend/package.json | 5 +- .../widgets/particles/ParticleBackground.tsx | 140 ++++++++++-------- 2 files changed, 82 insertions(+), 63 deletions(-) diff --git a/services/frontend/package.json b/services/frontend/package.json index 8c76694..7dad222 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -15,6 +15,8 @@ "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.13", + "@react-three/drei": "^9.6.1", + "@react-three/fiber": "^9.6.1", "@tanstack/react-query": "^5.100.14", "clsx": "^2.1.1", "framer-motion": "^12.4.0", @@ -23,8 +25,6 @@ "react": "^19.2.6", "react-dom": "^19.2.6", "tailwind-merge": "^3.6.0", - "@react-three/drei": "^9.6.1", - "@react-three/fiber": "^9.6.1", "three": "^0.174.0" }, "devDependencies": { @@ -32,6 +32,7 @@ "@tailwindcss/postcss": "^4.3.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/three": "^0.184.1", "@vitejs/plugin-react": "^6.0.2", "autoprefixer": "^10.5.0", "postcss": "^8.5.14", diff --git a/services/frontend/src/widgets/particles/ParticleBackground.tsx b/services/frontend/src/widgets/particles/ParticleBackground.tsx index 10ea892..1cd2fc7 100644 --- a/services/frontend/src/widgets/particles/ParticleBackground.tsx +++ b/services/frontend/src/widgets/particles/ParticleBackground.tsx @@ -1,54 +1,73 @@ import { Sparkles } from "@react-three/drei"; -import { Canvas, type ThreeElement, useFrame, useThree } from "@react-three/fiber"; +import { Canvas, useFrame } from "@react-three/fiber"; import { useEffect, useMemo, useRef, useState } from "react"; -import type * as THREE from "three"; -// ─── Sakura petal geometry ─────────────────────────────────────────────────── +// ─── Particle count by viewport ────────────────────────────────────────────── -function createPetalShape() { - const shape = new THREE.Shape(); - // Teardrop/petal shape - shape.moveTo(0, 0.5); - shape.bezierCurveTo(0.3, 0.3, 0.4, -0.1, 0, -0.5); - shape.bezierCurveTo(-0.4, -0.1, -0.3, 0.3, 0, 0.5); - return shape; +function getPetalCount(width: number) { + if (width < 640) return 10; + if (width < 1024) return 20; + return 35; } -const petalShape = createPetalShape(); -const petalGeometry = new THREE.ShapeGeometry(petalShape); +function getSparkleCount(width: number) { + if (width < 640) return 15; + return 30; +} -// ─── Inner particle scene ──────────────────────────────────────────────────── +// ─── Sakura petal mesh ─────────────────────────────────────────────────────── -function Petal({ mouse }: { mouse: React.RefObject }) { - const meshRef = useRef(null); - const initial = useMemo(() => ({ +interface PetalData { + x: number; + y: number; + z: number; + rotSpeed: number; + driftX: number; + driftZ: number; + hue: number; + sat: number; + light: number; + scale: number; +} + +function createPetals(count: number): PetalData[] { + return Array.from({ length: count }, () => ({ x: (Math.random() - 0.5) * 20, y: Math.random() * 20 - 5, z: (Math.random() - 0.5) * 10 - 2, rotSpeed: (Math.random() - 0.5) * 2, driftX: (Math.random() - 0.5) * 0.005, driftZ: (Math.random() - 0.5) * 0.003, - hue: 340 + Math.random() * 30, // pink range: 340–370 - saturation: 60 + Math.random() * 30, - lightness: 65 + Math.random() * 20, - }), []); + hue: 340 + Math.random() * 30, + sat: 60 + Math.random() * 30, + light: 65 + Math.random() * 20, + scale: 0.08 + Math.random() * 0.08, + })); +} - useFrame((_, delta) => { - if (!meshRef.current) return; - const mesh = meshRef.current; +function Petal({ data, mouseRef }: { data: PetalData; mouseRef: React.RefObject<{ x: number; y: number } | null> }) { + const meshRef = useRef(null); + + useFrame((_state, delta) => { + const mesh = meshRef.current as unknown as + | { position: { x: number; y: number; z: number }; rotation: { z: number; x: number } } + | null; + if (!mesh) return; // Slow drift downward mesh.position.y -= delta * 0.3; - mesh.position.x += Math.sin(Date.now() * initial.driftX) * 0.002; - mesh.position.z += Math.cos(Date.now() * initial.driftZ) * 0.001; - mesh.rotation.z += delta * initial.rotSpeed; - mesh.rotation.x += delta * initial.rotSpeed * 0.3; + mesh.position.x += Math.sin(Date.now() * data.driftX) * 0.002; + mesh.position.z += Math.cos(Date.now() * data.driftZ) * 0.001; + mesh.rotation.z += delta * data.rotSpeed; + mesh.rotation.x += delta * data.rotSpeed * 0.3; // Gentle mouse attraction - const dx = mouse.current.x * 3 - mesh.position.x; - const dy = mouse.current.y * 2 - mesh.position.y; - mesh.position.x += dx * 0.0001; - mesh.position.y += dy * 0.0001; + if (mouseRef.current) { + const dx = mouseRef.current.x * 3 - mesh.position.x; + const dy = mouseRef.current.y * 2 - mesh.position.y; + mesh.position.x += dx * 0.0001; + mesh.position.y += dy * 0.0001; + } // Reset when out of view if (mesh.position.y < -10) { @@ -61,64 +80,63 @@ function Petal({ mouse }: { mouse: React.RefObject }) { return ( + ); } +// ─── Scene ─────────────────────────────────────────────────────────────────── + function ParticleScene() { - const mouse = useRef(new THREE.Vector2(0, 0)); - const { size } = useThree(); - const hiddenRef = useRef(false); + const mouseRef = useRef<{ x: number; y: number } | null>(null); + const [viewportWidth, setViewportWidth] = useState( + typeof window !== "undefined" ? window.innerWidth : 1024, + ); useEffect(() => { const onMouse = (e: MouseEvent) => { - // Normalise to -1..1 relative to viewport - mouse.current.x = (e.clientX / window.innerWidth) * 2 - 1; - mouse.current.y = -(e.clientY / window.innerHeight) * 2 + 1; + mouseRef.current = { + x: (e.clientX / window.innerWidth) * 2 - 1, + y: -(e.clientY / window.innerHeight) * 2 + 1, + }; }; window.addEventListener("mousemove", onMouse); - const onVisibility = () => { - hiddenRef.current = document.hidden; - }; - document.addEventListener("visibilitychange", onVisibility); + const onResize = () => setViewportWidth(window.innerWidth); + window.addEventListener("resize", onResize); return () => { window.removeEventListener("mousemove", onMouse); - document.removeEventListener("visibilitychange", onVisibility); + window.removeEventListener("resize", onResize); }; }, []); - // Petal count depends on viewport width (fewer on mobile) - const petalCount = size.width < 640 ? 10 : size.width < 1024 ? 20 : 35; - const petals = useMemo( - () => Array.from({ length: petalCount }, (_, i) => ), - [petalCount], - ); + const petalCount = getPetalCount(viewportWidth); + const sparkleCount = getSparkleCount(viewportWidth); + + const petals = useMemo(() => createPetals(petalCount), [petalCount]); return ( <> - {/* Ambient + directional for soft lighting */} - {/* Petals */} - {!hiddenRef.current && petals} + {petals.map((data, i) => ( + + ))} - {/* Sparkles from drei */}