From 35e8fda1b815c526bfb4156434a59bdbb54ab7dd Mon Sep 17 00:00:00 2001 From: Cat Tom Date: Wed, 26 Mar 2025 12:35:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=20App.vue=20?= =?UTF-8?q?=E5=92=8C=20ParticleBackground=20=E7=BB=84=E4=BB=B6=E7=9A=84?= =?UTF-8?q?=E5=8A=A8=E7=94=BB=E6=95=88=E6=9E=9C=EF=BC=8C=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E7=B2=92=E5=AD=90=E9=85=8D=E7=BD=AE=E5=92=8C=E8=BF=90=E5=8A=A8?= =?UTF-8?q?=E5=8F=82=E6=95=B0=EF=BC=8C=E6=8F=90=E5=8D=87=E6=80=A7=E8=83=BD?= =?UTF-8?q?=E5=92=8C=E8=A7=86=E8=A7=89=E8=A1=A8=E7=8E=B0=EF=BC=8C=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E5=A2=9E=E5=8A=A0=E8=8A=82=E6=B5=81=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E4=BB=A5=E4=BC=98=E5=8C=96=E5=8A=A8=E7=94=BB=E5=88=B7=E6=96=B0?= =?UTF-8?q?=E7=8E=87=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 44 +++++++++++++++------- src/components/ParticleBackground.vue | 53 +++++++++++++++------------ src/composables/useParticles.js | 41 ++++++++++++++++----- 3 files changed, 92 insertions(+), 46 deletions(-) diff --git a/src/App.vue b/src/App.vue index bc3cb9e..d8c953d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -112,9 +112,11 @@ onMounted(() => { height: 60px; background: linear-gradient(45deg, rgba(255,255,255,0.4), rgba(255,255,255,0.1)); border-radius: 20% 60% 40% 80%; - animation: float 20s linear infinite; + will-change: transform; + transform: translateZ(0); + animation: float 25s linear infinite; animation-delay: var(--delay); - opacity: 0.6; + opacity: 0.4; backdrop-filter: blur(2px); } @@ -124,9 +126,11 @@ onMounted(() => { height: 40px; background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8), rgba(255,255,255,0.1)); border-radius: 50%; - animation: bubble 15s ease-in-out infinite; + will-change: transform; + transform: translateZ(0); + animation: bubble 20s ease-in-out infinite; animation-delay: var(--delay); - opacity: 0.4; + opacity: 0.3; } @keyframes float { @@ -165,8 +169,11 @@ onMounted(() => { position: absolute; width: 30px; height: 30px; - animation: floatFlower 20s ease-in-out infinite; + will-change: transform; + transform: translateZ(0); + animation: floatFlower 25s ease-in-out infinite; animation-delay: var(--delay); + opacity: 0.5; } .flower-center { @@ -204,9 +211,12 @@ onMounted(() => { height: 40px; background: rgba(255, 255, 255, 0.8); border-radius: 20px; - animation: floatCloud 25s linear infinite; + will-change: transform; + transform: translateZ(0); + animation: floatCloud 30s linear infinite; animation-delay: var(--delay); filter: blur(4px); + opacity: 0.4; } .floating-cloud::before, @@ -246,7 +256,8 @@ onMounted(() => { rgba(176, 224, 230, 0.05) 80%, transparent 100% ); - animation: rotateGlow 30s linear infinite; + animation: rotateGlow 40s linear infinite; + opacity: 0.08; } /* 动画关键帧 */ @@ -314,15 +325,11 @@ onMounted(() => { /* 性能优化 */ @media (prefers-reduced-motion: reduce) { .floating-element, - .floating-bubble { - animation: none; - display: none; - } - + .floating-bubble, .floating-flower, .floating-cloud, .rainbow-glow { - animation: none; + animation-play-state: paused; } } @@ -395,5 +402,16 @@ onMounted(() => { width: 120px; height: 120px; } + + .floating-element, + .floating-bubble, + .floating-flower, + .floating-cloud { + display: none; + } + + .rainbow-glow { + opacity: 0.05; + } } diff --git a/src/components/ParticleBackground.vue b/src/components/ParticleBackground.vue index c6651e4..968bf04 100644 --- a/src/components/ParticleBackground.vue +++ b/src/components/ParticleBackground.vue @@ -12,21 +12,21 @@ const breakpoints = useBreakpoints({ }) const isMobile = breakpoints.smaller('tablet') -const particleCount = isMobile.value ? 1000 : 1800 +const particleCount = isMobile.value ? 800 : 1500 // 优化粒子配置 const config = { - particleSize: 2.5, - systemRadius: 20, - baseSpeed: 0.15, - hoverRadius: 4, + particleSize: isMobile.value ? 2.0 : 2.5, + systemRadius: isMobile.value ? 15 : 20, + baseSpeed: 0.1, + hoverRadius: isMobile.value ? 3 : 4, color: 0xb4e0f7, // 主色调:浅蓝色 colorVariation: [0xffb7d0, 0xa8d8ea, 0xf3e5f5], // 粒子颜色变化 lineColor: 0xa8d8ea, - lineDistance: 3.5, - particleCount: isMobile.value ? 1200 : 2000, - particleOpacity: 0.8, - lineOpacity: 0.2, + lineDistance: isMobile.value ? 2.5 : 3.5, + particleCount: isMobile.value ? 800 : 1500, + particleOpacity: 0.6, + lineOpacity: 0.15, glowSize: 3 } @@ -193,37 +193,44 @@ const createConnections = (positions, colors) => { } const animate = () => { - requestAnimationFrame(animate) - - const time = Date.now() * 0.0001 - particles.material.uniforms.time.value = time - + if (!document.hidden) { + requestAnimationFrame(animate) + + const time = Date.now() * 0.00005 + particles.material.uniforms.time.value = time + + if (time % 2 === 0) { + updateParticles() + } + + renderer.render(scene, camera) + } +} + +const updateParticles = () => { const positions = particles.geometry.attributes.position.array for (let i = 0; i < config.particleCount; i++) { const i3 = i * 3 - // 更自然的运动 - positions[i3] += Math.sin(time + i * 0.1) * 0.02 - positions[i3 + 1] += Math.cos(time + i * 0.05) * 0.02 - positions[i3 + 2] += Math.sin(time * 0.5 + i * 0.07) * 0.02 + positions[i3] += Math.sin(time + i * 0.05) * 0.01 + positions[i3 + 1] += Math.cos(time + i * 0.03) * 0.01 + positions[i3 + 2] += Math.sin(time * 0.3 + i * 0.04) * 0.01 - // 鼠标交互优化 if (mouse.x !== null && mouse.y !== null) { const dx = positions[i3] - mouse.x * 20 const dy = positions[i3 + 1] - mouse.y * 20 const distance = Math.sqrt(dx * dx + dy * dy) if (distance < config.hoverRadius) { - const force = (config.hoverRadius - distance) / config.hoverRadius - positions[i3] += dx * force * 0.03 - positions[i3 + 1] += dy * force * 0.03 + const force = (config.hoverRadius - distance) / config.hoverRadius * 0.8 + positions[i3] += dx * force * 0.02 + positions[i3 + 1] += dy * force * 0.02 } } } particles.geometry.attributes.position.needsUpdate = true - renderer.render(scene, camera) } const handleResize = () => { diff --git a/src/composables/useParticles.js b/src/composables/useParticles.js index 8d217e6..aeee713 100644 --- a/src/composables/useParticles.js +++ b/src/composables/useParticles.js @@ -132,16 +132,33 @@ export function useParticles(options) { } } - // 动画循环 - const animate = () => { - ctx.clearRect(0, 0, canvasSize.value.width, canvasSize.value.height) - - particles.forEach(particle => { - particle.update() - particle.draw() - }) - - drawLines() + // 添加节流控制 + let animationFrame = null + let lastTime = 0 + + const animate = (timestamp) => { + if (!lastTime) lastTime = timestamp + const delta = timestamp - lastTime + + // 限制刷新率,约60fps + if (delta > 16) { + ctx.clearRect(0, 0, canvasSize.value.width, canvasSize.value.height) + + // 批量更新粒子 + const batchSize = 50 + for (let i = 0; i < particles.length; i += batchSize) { + const end = Math.min(i + batchSize, particles.length) + for (let j = i; j < end; j++) { + particles[j].update() + particles[j].draw() + } + } + + drawLines() + lastTime = timestamp + } + + animationFrame = requestAnimationFrame(animate) } // 处理鼠标移动 @@ -184,6 +201,10 @@ export function useParticles(options) { // 清理 onUnmounted(() => { + if (animationFrame) { + cancelAnimationFrame(animationFrame) + } + particles.length = 0 pause() window.removeEventListener('resize', handleResize) if (interactive) {