diff --git a/app/components/Hero.tsx b/app/components/Hero.tsx index 2c0d837..e131999 100644 --- a/app/components/Hero.tsx +++ b/app/components/Hero.tsx @@ -174,7 +174,10 @@ export default function Hero() { const response = await fetch( "https://api.github.com/repos/MaaEnd/MaaEnd/releases/latest" ); - if (!response.ok) throw new Error("Failed to fetch release info"); + if (!response.ok) { + console.error("Failed to fetch release info"); + return; + } const data: ReleaseInfo = await response.json(); setReleaseInfo(data); @@ -252,7 +255,7 @@ export default function Hero() { return (
{/* Industrial Background Layer */} @@ -298,7 +301,7 @@ export default function Hero() {
- + {t("hero.title")} @@ -346,95 +349,105 @@ export default function Hero() { initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} - className="flex flex-wrap items-center justify-center gap-3 md:justify-start" + className="flex flex-col gap-3 md:flex-row md:items-center" > - {/* 主下载按钮 - 自动检测系统 */} - - - {/* 更多下载选项按钮 */} - - - +
+ + {/* 更多下载选项按钮 */} +
+ +
+ + + {/* 第二行:Mirror酱 */} +
+ + +
) : ( {/* Right: Interactive Particle Model */} -
+
diff --git a/app/components/hero/InteractiveModelOptimized.tsx b/app/components/hero/InteractiveModelOptimized.tsx index ce5f4d0..a358c6e 100644 --- a/app/components/hero/InteractiveModelOptimized.tsx +++ b/app/components/hero/InteractiveModelOptimized.tsx @@ -1,227 +1,91 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { Canvas, useFrame } from "@react-three/fiber"; -import { Float, OrbitControls, PerspectiveCamera } from "@react-three/drei"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { Canvas, useFrame, useThree } from "@react-three/fiber"; +import { + Environment, + Float, + OrbitControls, + PerspectiveCamera, + useGLTF, +} from "@react-three/drei"; +import { + Bloom, + ChromaticAberration, + EffectComposer, + Noise, + Vignette, +} from "@react-three/postprocessing"; import * as THREE from "three"; import { useTheme } from "next-themes"; -// 性能优化的粒子系统 -const OptimizedArchitectureModel = ({ isDark }: { isDark: boolean }) => { - const count = 5000; // 保持原始粒子数量以维持视觉效果 - const mesh = useRef(null); - - // 使用 useMemo 缓存完整的几何体结构 - const geometry = useMemo(() => { - /* eslint-disable react-hooks/purity */ - // Math.random() is safe here as it's memoized and only runs once on mount - const temp = []; - - // 塔的总高度 - const towerHeight = 8; - const baseY = -4; // 底部位置(往下移动让顶部完整显示) - - // 1. 主塔身 - 底部宽大向上逐渐收窄的锥形结构 (四面体框架) - for (let i = 0; i < count * 0.25; i++) { - const t = Math.random(); // 0 = 底部, 1 = 顶部 - const y = baseY + t * towerHeight * 0.7; - - // 半径随高度减小 (底部2.0, 顶部0.3) - const radius = 2.0 - t * 1.7; - - // 四边形框架的边缘 - const edge = Math.floor(Math.random() * 4); - const baseAngle = (edge / 4) * Math.PI * 2 + Math.PI / 4; - const nextAngle = ((edge + 1) / 4) * Math.PI * 2 + Math.PI / 4; - const lerp = Math.random(); - - const angle = baseAngle + lerp * (nextAngle - baseAngle); - const x = Math.cos(angle) * radius + (Math.random() - 0.5) * 0.05; - const z = Math.sin(angle) * radius + (Math.random() - 0.5) * 0.05; - - temp.push(x, y, z); - } - - // 2. 垂直支撑柱 (四根主柱从底部延伸到顶部) - for (let pillar = 0; pillar < 4; pillar++) { - const angle = (pillar / 4) * Math.PI * 2 + Math.PI / 4; - for (let i = 0; i < count * 0.04; i++) { - const t = Math.random(); - const y = baseY + t * towerHeight * 0.7; - const radius = 2.0 - t * 1.7; - - const x = Math.cos(angle) * radius + (Math.random() - 0.5) * 0.08; - const z = Math.sin(angle) * radius + (Math.random() - 0.5) * 0.08; - - temp.push(x, y, z); - } - } - - // 3. 水平横梁结构 (多层平台) - const platforms = [0.0, 0.25, 0.5, 0.7]; - for (const pLevel of platforms) { - const y = baseY + pLevel * towerHeight * 0.7; - const radius = 2.0 - pLevel * 1.7; - - for (let i = 0; i < count * 0.03; i++) { - const angle = Math.random() * Math.PI * 2; - const r = radius * (0.3 + Math.random() * 0.7); - - const x = Math.cos(angle) * r + (Math.random() - 0.5) * 0.05; - const z = Math.sin(angle) * r + (Math.random() - 0.5) * 0.05; - - temp.push(x, y + (Math.random() - 0.5) * 0.1, z); - } - } - - // 4. 能量环 (围绕塔身的悬浮光环) - const energyRings = [ - { y: baseY + towerHeight * 0.3, radius: 2.8, particles: 0.06 }, - { y: baseY + towerHeight * 0.55, radius: 2.0, particles: 0.05 }, - { y: baseY + towerHeight * 0.75, radius: 1.2, particles: 0.04 }, - ]; - - for (const ring of energyRings) { - for (let i = 0; i < count * ring.particles; i++) { - const angle = Math.random() * Math.PI * 2; - const r = ring.radius + (Math.random() - 0.5) * 0.15; - const x = Math.cos(angle) * r; - const z = Math.sin(angle) * r; - const y = ring.y + (Math.random() - 0.5) * 0.08; - - temp.push(x, y, z); +// MaaEnd 主题色常量 +const THEME_COLORS = { + cyan: "#00F0FF", + cyanDark: "#0891b2", + yellow: "#FFD000", + orangeDark: "#ea580c", +} as const; + +function RelayModel(): React.JSX.Element { + const { scene } = useGLTF("/model/Relay.glb", true); + const modelRef = useRef(null); + + const clonedScene = useMemo(() => { + const cloned = scene.clone(); + cloned.traverse((child) => { + if (!(child instanceof THREE.Mesh) || !child.material) return; + + child.material = child.material.clone(); + const material = child.material as THREE.MeshStandardMaterial; + + if (material.transparent) { + material.depthWrite = true; + material.alphaTest = 0.1; } - } - - // 5. 顶部天线结构 (细长的能量发射器) - const antennaBase = baseY + towerHeight * 0.7; - const antennaHeight = towerHeight * 0.35; - - // 主天线柱 - for (let i = 0; i < count * 0.08; i++) { - const t = Math.random(); - const y = antennaBase + t * antennaHeight; - - // 天线越往上越细 - const radius = 0.15 - t * 0.12; - const angle = Math.random() * Math.PI * 2; - - const x = Math.cos(angle) * radius + (Math.random() - 0.5) * 0.03; - const z = Math.sin(angle) * radius + (Math.random() - 0.5) * 0.03; - - temp.push(x, y, z); - } - - // 6. 天线顶部能量球 - const topY = antennaBase + antennaHeight; - for (let i = 0; i < count * 0.04; i++) { - const theta = Math.random() * Math.PI * 2; - const phi = Math.random() * Math.PI; - const r = 0.25 + Math.random() * 0.1; - - const x = r * Math.sin(phi) * Math.cos(theta); - const y = topY + r * Math.cos(phi); - const z = r * Math.sin(phi) * Math.sin(theta); - - temp.push(x, y, z); - } - - // 7. 斜向支撑结构 (X形交叉支撑) - for (let level = 0; level < 3; level++) { - const t1 = level * 0.25; - const t2 = (level + 1) * 0.25; - - for (let cross = 0; cross < 4; cross++) { - const angle1 = (cross / 4) * Math.PI * 2 + Math.PI / 4; - const angle2 = ((cross + 1) / 4) * Math.PI * 2 + Math.PI / 4; - for (let i = 0; i < count * 0.01; i++) { - const lerp = Math.random(); - const t = t1 + lerp * (t2 - t1); - const y = baseY + t * towerHeight * 0.7; + material.side = THREE.DoubleSide; - const radius = 2.0 - t * 1.7; - const angle = angle1 + lerp * (angle2 - angle1); + const hasEmissive = + material.emissive && + (material.emissive.r > 0 || + material.emissive.g > 0 || + material.emissive.b > 0); - const x = Math.cos(angle) * radius * 0.95; - const z = Math.sin(angle) * radius * 0.95; - - temp.push(x, y, z); - } + if (hasEmissive) { + material.emissive = new THREE.Color(THEME_COLORS.yellow); + material.emissiveIntensity = 3; } - } - - // 8. 悬浮能量粒子 (围绕塔身的浮动粒子) - for (let i = 0; i < count * 0.1; i++) { - const y = baseY + Math.random() * towerHeight; - const distance = 2.5 + Math.random() * 1.5; - const angle = Math.random() * Math.PI * 2; - - const x = Math.cos(angle) * distance; - const z = Math.sin(angle) * distance; - - temp.push(x, y, z); - } - - // 9. 底座结构 (宽大的六边形底座) - for (let i = 0; i < count * 0.06; i++) { - const angle = (Math.floor(Math.random() * 6) / 6) * Math.PI * 2; - const nextAngle = ((Math.floor(Math.random() * 6) + 1) / 6) * Math.PI * 2; - const lerp = Math.random(); - const finalAngle = angle + lerp * (nextAngle - angle); - - const radius = 2.2 + Math.random() * 0.3; - const x = Math.cos(finalAngle) * radius; - const z = Math.sin(finalAngle) * radius; - const y = baseY + (Math.random() - 0.5) * 0.2; - - temp.push(x, y, z); - } - - const geometry = new THREE.BufferGeometry(); - geometry.setAttribute( - "position", - new THREE.BufferAttribute(new Float32Array(temp), 3) - ); - return geometry; - /* eslint-enable react-hooks/purity */ - }, [count]); - - // 优化材质,减少重新创建 - const material = useMemo(() => { - return new THREE.PointsMaterial({ - size: 0.05, - color: isDark ? "#FFD000" : "#00BFFF", - transparent: true, - opacity: 0.8, - sizeAttenuation: true, - blending: isDark ? THREE.AdditiveBlending : THREE.NormalBlending, }); - }, [isDark]); + return cloned; + }, [scene]); - // 优化动画循环 - 保持原始旋转速度 useFrame(() => { - if (!mesh.current) return; - // Technical rotation - 保持原始速度 - mesh.current.rotation.y -= 0.002; + if (modelRef.current) { + modelRef.current.rotation.y -= 0.002; + } }); - // 清理几何体和材质 - useEffect(() => { - return () => { - geometry.dispose(); - material.dispose(); - }; - }, [geometry, material]); + return ( + + ); +} - return ; -}; +useGLTF.preload("/model/Relay.glb", true); -// 简化的全息场组件 -const OptimizedHoloField = ({ isDark }: { isDark: boolean }) => { +function OptimizedHoloField({ + isDark, +}: { + isDark: boolean; +}): React.JSX.Element { const ref = useRef(null); - // 缓存几何体 + // 几何体只需创建一次 const geometries = useMemo( () => ({ outer: new THREE.CylinderGeometry(1.5, 3.5, 10, 6, 1, true), @@ -230,20 +94,20 @@ const OptimizedHoloField = ({ isDark }: { isDark: boolean }) => { [] ); - // 缓存材质 + // 材质随主题变化 const materials = useMemo( () => ({ outer: new THREE.MeshBasicMaterial({ - color: isDark ? "#00F0FF" : "#008fa6", + color: isDark ? THEME_COLORS.cyan : THEME_COLORS.cyanDark, wireframe: true, - opacity: 0.04, + opacity: isDark ? 0.04 : 0.15, transparent: true, side: THREE.DoubleSide, }), inner: new THREE.MeshBasicMaterial({ - color: "#FFD000", + color: isDark ? THEME_COLORS.yellow : THEME_COLORS.orangeDark, wireframe: true, - opacity: 0.03, + opacity: isDark ? 0.03 : 0.12, transparent: true, side: THREE.DoubleSide, }), @@ -251,19 +115,27 @@ const OptimizedHoloField = ({ isDark }: { isDark: boolean }) => { [isDark] ); + // 材质清理 + useEffect(() => { + return () => { + materials.outer.dispose(); + materials.inner.dispose(); + }; + }, [materials]); + useFrame((state, delta) => { - if (ref.current) { - ref.current.rotation.y += delta * 0.5; - } + if (!ref.current) return; + ref.current.rotation.y += delta * 0.5; + ref.current.position.y = Math.sin(state.clock.elapsedTime * 0.5) * 0.2; }); - // 清理资源 + // 组件卸载时清理几何体 useEffect(() => { return () => { - Object.values(geometries).forEach((geo) => geo.dispose()); - Object.values(materials).forEach((mat) => mat.dispose()); + geometries.outer.dispose(); + geometries.inner.dispose(); }; - }, [geometries, materials]); + }, [geometries]); return ( @@ -279,47 +151,96 @@ const OptimizedHoloField = ({ isDark }: { isDark: boolean }) => { /> ); -}; +} + +function SceneLighting({ isDark }: { isDark: boolean }): React.JSX.Element { + const { gl } = useThree(); + + useEffect(() => { + // eslint-disable-next-line react-hooks/immutability + gl.toneMappingExposure = isDark ? 1.5 : 1.0; + }, [isDark, gl]); + + return ( + <> + + + + {isDark ? ( + <> + + + + + ) : ( + <> + + + + )} + + ); +} -// 主组件 -export default function InteractiveModelOptimized() { +export default function InteractiveModelOptimized(): React.JSX.Element | null { const { theme, systemTheme } = useTheme(); const [mounted, setMounted] = useState(false); const [isVisible, setIsVisible] = useState(false); const containerRef = useRef(null); - // 延迟挂载以避免水合不匹配 useEffect(() => { const timeoutId = setTimeout(() => setMounted(true), 0); return () => clearTimeout(timeoutId); }, []); - // 使用 Intersection Observer 优化性能 useEffect(() => { if (!mounted || !containerRef.current) return; const observer = new IntersectionObserver( - ([entry]) => { - setIsVisible(entry.isIntersecting); - }, + ([entry]) => setIsVisible(entry.isIntersecting), { threshold: 0.1 } ); observer.observe(containerRef.current); - return () => observer.disconnect(); }, [mounted]); - // 计算有效主题 const currentTheme = theme === "system" ? systemTheme : theme; const isDark = currentTheme === "dark"; - // 防止水合不匹配 if (!mounted) { return
; } - // 只有在可见时才渲染 Canvas if (!isVisible) { return (
- + - {/* 优化的光照 */} - - - + + + + + + + + - {/* 简化的 HUD 覆盖层 */} -
- {/* 角落括号 */} -
-
-
-
- - {/* 中心聚焦环 - 使用 CSS 动画而不是 Framer Motion */} -
-
+ +
+ ); +} + +function HudOverlay(): React.JSX.Element { + return ( +
+
+
+
+
+
); } diff --git a/package.json b/package.json index ad7a09d..f6861c9 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,15 @@ "dependencies": { "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", + "@react-three/postprocessing": "^3.0.4", + "baseline-browser-mapping": "^2.9.19", "clsx": "^2.1.1", "framer-motion": "^12.23.24", "i18next": "^25.7.1", "lucide-react": "^0.555.0", "next": "16.0.7", "next-themes": "^0.4.6", + "postprocessing": "^6.38.2", "react": "19.2.0", "react-dom": "19.2.0", "react-i18next": "^16.4.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 363f467..d1730db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@react-three/fiber': specifier: ^9.4.0 version: 9.4.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.181.2) + '@react-three/postprocessing': + specifier: ^3.0.4 + version: 3.0.4(@react-three/fiber@9.4.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.181.2))(@types/three@0.181.0)(react@19.2.0)(three@0.181.2) + baseline-browser-mapping: + specifier: ^2.9.19 + version: 2.9.19 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -32,6 +38,9 @@ importers: next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + postprocessing: + specifier: ^6.38.2 + version: 6.38.2(three@0.181.2) react: specifier: 19.2.0 version: 19.2.0 @@ -507,6 +516,13 @@ packages: react-native: optional: true + '@react-three/postprocessing@3.0.4': + resolution: {integrity: sha512-e4+F5xtudDYvhxx3y0NtWXpZbwvQ0x1zdOXWTbXMK6fFLVDd4qucN90YaaStanZGS4Bd5siQm0lGL/5ogf8iDQ==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19.0 + three: '>= 0.156.0' + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -910,8 +926,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.8.32: - resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==} + baseline-browser-mapping@2.9.19: + resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true bidi-js@1.0.3: @@ -1758,6 +1774,12 @@ packages: '@types/three': '>=0.134.0' three: '>=0.134.0' + maath@0.6.0: + resolution: {integrity: sha512-dSb2xQuP7vDnaYqfoKzlApeRcR2xtN8/f7WV/TMAkBC8552TwTLtOO0JTcSygkYMjNDPoo6V01jTw/aPi4JrMw==} + peerDependencies: + '@types/three': '>=0.144.0' + three: '>=0.144.0' + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1804,6 +1826,12 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + n8ao@1.10.1: + resolution: {integrity: sha512-hhI1pC+BfOZBV1KMwynBrVlIm8wqLxj/abAWhF2nZ0qQKyzTSQa1QtLVS2veRiuoBQXojxobcnp0oe+PUoxf/w==} + peerDependencies: + postprocessing: '>=6.30.0' + three: '>=0.137' + nano-spawn@2.0.0: resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} engines: {node: '>=20.17'} @@ -1946,6 +1974,11 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postprocessing@6.38.2: + resolution: {integrity: sha512-7DwuT7Tkst41ZjSj287g7C9c5/D3Xx5rMgBosg0dadbUPoZD2HNzkadKPol1d2PJAoI9f+Jeh1/v9YfLzpFGVw==} + peerDependencies: + three: '>= 0.157.0 < 0.183.0' + potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} @@ -2912,6 +2945,17 @@ snapshots: - '@types/react' - immer + '@react-three/postprocessing@3.0.4(@react-three/fiber@9.4.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.181.2))(@types/three@0.181.0)(react@19.2.0)(three@0.181.2)': + dependencies: + '@react-three/fiber': 9.4.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.181.2) + maath: 0.6.0(@types/three@0.181.0)(three@0.181.2) + n8ao: 1.10.1(postprocessing@6.38.2(three@0.181.2))(three@0.181.2) + postprocessing: 6.38.2(three@0.181.2) + react: 19.2.0 + three: 0.181.2 + transitivePeerDependencies: + - '@types/three' + '@rtsao/scc@1.1.0': {} '@swc/helpers@0.5.15': @@ -3310,7 +3354,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.8.32: {} + baseline-browser-mapping@2.9.19: {} bidi-js@1.0.3: dependencies: @@ -3331,7 +3375,7 @@ snapshots: browserslist@4.28.0: dependencies: - baseline-browser-mapping: 2.8.32 + baseline-browser-mapping: 2.9.19 caniuse-lite: 1.0.30001757 electron-to-chromium: 1.5.262 node-releases: 2.0.27 @@ -4277,6 +4321,11 @@ snapshots: '@types/three': 0.181.0 three: 0.181.2 + maath@0.6.0(@types/three@0.181.0)(three@0.181.2): + dependencies: + '@types/three': 0.181.0 + three: 0.181.2 + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -4316,6 +4365,11 @@ snapshots: ms@2.1.3: {} + n8ao@1.10.1(postprocessing@6.38.2(three@0.181.2))(three@0.181.2): + dependencies: + postprocessing: 6.38.2(three@0.181.2) + three: 0.181.2 + nano-spawn@2.0.0: {} nanoid@3.3.11: {} @@ -4455,6 +4509,10 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postprocessing@6.38.2(three@0.181.2): + dependencies: + three: 0.181.2 + potpack@1.0.2: {} prelude-ls@1.2.1: {} diff --git a/public/model/Relay.glb b/public/model/Relay.glb new file mode 100644 index 0000000..3845f64 Binary files /dev/null and b/public/model/Relay.glb differ