From 7b6dbb884f4a02c58138a6d41e324849875f9812 Mon Sep 17 00:00:00 2001 From: doublex001 <16157008+doublex001@user.noreply.gitee.com> Date: Mon, 3 Aug 2026 05:00:57 +0800 Subject: [PATCH] =?UTF-8?q?feat(Home):=20=E9=A6=96=E5=B1=8F=E7=B2=92?= =?UTF-8?q?=E5=AD=90=E7=B3=BB=E7=BB=9F=E5=85=A8=E9=9D=A2=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=EF=BC=9A=E5=85=A8=E5=B1=8FCanvas=E7=B2=92=E5=AD=90=E7=BD=91+?= =?UTF-8?q?=E9=82=BB=E8=BF=91=E8=BF=9E=E7=BA=BF+=E9=BC=A0=E6=A0=87?= =?UTF-8?q?=E5=90=B8=E9=99=84+4=E8=89=B2=E9=9B=BE=E5=8C=96=E5=85=89?= =?UTF-8?q?=E6=99=95=E7=90=83+=E7=8E=BB=E7=92=83=E7=90=83=E8=BD=A8?= =?UTF-8?q?=E9=81=9322=E9=A2=974=E8=89=B2=E9=95=BF=E6=8B=96=E5=B0=BE?= =?UTF-8?q?=E5=BD=97=E6=98=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Home.tsx | 242 +++++++++++++++++++++++++++++++++++++--- src/styles/global.css | 154 ++++++++++++++++++++++++- 2 files changed, 377 insertions(+), 19 deletions(-) diff --git a/src/components/Home.tsx b/src/components/Home.tsx index 317ff0e..645f52d 100644 --- a/src/components/Home.tsx +++ b/src/components/Home.tsx @@ -1,9 +1,192 @@ -import type { CSSProperties } from 'react'; +import { useEffect, useRef, type CSSProperties } from 'react'; import { getLocalizedPath, getTranslation, type Locale } from '../i18n/utils'; import { PLACEHOLDER_IMG } from '../placeholder'; import { cssVars } from './css'; import { Icon, statsIcons } from '../icons'; +/** + * 首屏全屏粒子 + 邻近连线 Canvas 背景: + * - 4 色粒子(白/蓝/紫/极光淡粉)约 70~95 颗(按屏幕面积自适应) + * - 邻近粒子间画淡蓝连线(dist < 120)形成柔和"网" + * - 鼠标靠近时粒子轻微吸附,连线高亮 + * - prefers-reduced-motion 下固定为静态散点,不执行动画 + */ +function HeroParticleCanvas() { + const ref = useRef(null); + + useEffect(() => { + const canvas = ref.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + let raf = 0; + let dpr = Math.min(window.devicePixelRatio || 1, 2); + + type P = { + x: number; y: number; + vx: number; vy: number; + r: number; + color: string; + alpha: number; + alphaSpeed: number; + }; + + const PALETTE = [ + '255, 255, 255', // 白星 + '148, 184, 255', // 品牌淡蓝 + '196, 178, 255', // 薰衣草紫 + '255, 180, 215', // 极光淡粉 + ]; + + let W = 0, H = 0; + let particles: P[] = []; + const mouse = { x: -9999, y: -9999, active: false }; + + const resize = () => { + const { clientWidth, clientHeight } = canvas.parentElement || canvas; + W = clientWidth; + H = clientHeight; + dpr = Math.min(window.devicePixelRatio || 1, 2); + canvas.width = Math.floor(W * dpr); + canvas.height = Math.floor(H * dpr); + canvas.style.width = W + 'px'; + canvas.style.height = H + 'px'; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + + const area = W * H; + const count = Math.max(55, Math.min(100, Math.floor(area / 14000))); + particles = Array.from({ length: count }, () => ({ + x: Math.random() * W, + y: Math.random() * H, + vx: (Math.random() - 0.5) * 0.35, + vy: (Math.random() - 0.5) * 0.35, + r: Math.random() * 1.4 + 0.6, + color: PALETTE[Math.floor(Math.random() * PALETTE.length)], + alpha: Math.random() * 0.5 + 0.4, + alphaSpeed: (Math.random() - 0.5) * 0.008, + })); + }; + + const step = () => { + ctx.clearRect(0, 0, W, H); + + // 1) 粒子 + for (let i = 0; i < particles.length; i++) { + const p = particles[i]; + if (!reduced) { + // 鼠标吸附(力度很小,避免头晕) + if (mouse.active) { + const dx = mouse.x - p.x, dy = mouse.y - p.y; + const d2 = dx * dx + dy * dy; + if (d2 < 140 * 140) { + const f = (1 - Math.sqrt(d2) / 140) * 0.04; + p.vx += dx * f * 0.02; + p.vy += dy * f * 0.02; + } + } + p.x += p.vx; + p.y += p.vy; + // 速度阻尼 + p.vx *= 0.992; p.vy *= 0.992; + // 边界反弹 + if (p.x < p.r) { p.x = p.r; p.vx = Math.abs(p.vx); } + if (p.x > W - p.r) { p.x = W - p.r; p.vx = -Math.abs(p.vx); } + if (p.y < p.r) { p.y = p.r; p.vy = Math.abs(p.vy); } + if (p.y > H - p.r) { p.y = H - p.r; p.vy = -Math.abs(p.vy); } + + // 闪烁 alpha + p.alpha += p.alphaSpeed; + if (p.alpha > 0.95 || p.alpha < 0.35) p.alphaSpeed = -p.alphaSpeed; + } + + ctx.beginPath(); + const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 3); + g.addColorStop(0, `rgba(${p.color}, ${p.alpha})`); + g.addColorStop(0.5, `rgba(${p.color}, ${p.alpha * 0.35})`); + g.addColorStop(1, `rgba(${p.color}, 0)`); + ctx.fillStyle = g; + ctx.arc(p.x, p.y, p.r * 3, 0, Math.PI * 2); + ctx.fill(); + } + + // 2) 邻近连线(仅动画模式执行,静态省开销) + if (!reduced) { + const MAX = 120; + for (let i = 0; i < particles.length; i++) { + const a = particles[i]; + for (let j = i + 1; j < particles.length; j++) { + const b = particles[j]; + const dx = a.x - b.x, dy = a.y - b.y; + const d2 = dx * dx + dy * dy; + if (d2 < MAX * MAX) { + const d = Math.sqrt(d2); + const t = 1 - d / MAX; + // 鼠标增强:若 a 或 b 靠近鼠标,连线加亮 + let boost = 0; + if (mouse.active) { + const adx = a.x - mouse.x, ady = a.y - mouse.y; + const bdx = b.x - mouse.x, bdy = b.y - mouse.y; + const near = Math.min(adx * adx + ady * ady, bdx * bdx + bdy * bdy); + if (near < 120 * 120) boost = (1 - Math.sqrt(near) / 120) * 0.45; + } + const alpha = Math.min(0.55, t * 0.28 + boost); + ctx.strokeStyle = `rgba(148, 184, 255, ${alpha})`; + ctx.lineWidth = 0.7; + ctx.beginPath(); + ctx.moveTo(a.x, a.y); + ctx.lineTo(b.x, b.y); + ctx.stroke(); + } + } + } + } + + if (!reduced) raf = requestAnimationFrame(step); + }; + + const onMove = (e: PointerEvent) => { + const rect = canvas.getBoundingClientRect(); + mouse.x = e.clientX - rect.left; + mouse.y = e.clientY - rect.top; + mouse.active = true; + }; + const onLeave = () => { mouse.active = false; mouse.x = -9999; mouse.y = -9999; }; + const onResize = () => { resize(); }; + + resize(); + raf = requestAnimationFrame(step); + window.addEventListener('resize', onResize); + window.addEventListener('pointermove', onMove, { passive: true }); + window.addEventListener('pointerleave', onLeave); + + return () => { + cancelAnimationFrame(raf); + window.removeEventListener('resize', onResize); + window.removeEventListener('pointermove', onMove); + window.removeEventListener('pointerleave', onLeave); + }; + }, []); + + return ( +