Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="particle-container" ref="container"> | |
| <canvas ref="canvas" class="particle-canvas"></canvas> | |
| </div> | |
| </template> | |
| <script setup lang="ts"> | |
| import { ref, onMounted, onUnmounted } from 'vue' | |
| // 定义组件名称 | |
| defineOptions({ | |
| name: 'ParticleEffect' | |
| }) | |
| const container = ref<HTMLElement>() | |
| const canvas = ref<HTMLCanvasElement>() | |
| let animationId: number | |
| let particles: Particle[] = [] | |
| interface Particle { | |
| x: number | |
| y: number | |
| vx: number | |
| vy: number | |
| size: number | |
| opacity: number | |
| color: string | |
| } | |
| const colors = [ | |
| 'rgba(26, 26, 26, 0.12)', | |
| 'rgba(26, 26, 26, 0.08)', | |
| 'rgba(92, 92, 92, 0.1)', | |
| 'rgba(138, 138, 138, 0.08)', | |
| 'rgba(26, 26, 26, 0.06)' | |
| ] | |
| const createParticle = (): Particle => { | |
| return { | |
| x: Math.random() * (canvas.value?.width || 0), | |
| y: Math.random() * (canvas.value?.height || 0), | |
| vx: (Math.random() - 0.5) * 2, | |
| vy: (Math.random() - 0.5) * 2, | |
| size: Math.random() * 3 + 1, | |
| opacity: Math.random() * 0.5 + 0.2, | |
| color: colors[Math.floor(Math.random() * colors.length)] ?? colors[0]! | |
| } | |
| } | |
| const initParticles = () => { | |
| particles = [] | |
| for (let i = 0; i < 50; i++) { | |
| particles.push(createParticle()) | |
| } | |
| } | |
| const updateParticles = () => { | |
| if (!canvas.value) return | |
| const ctx = canvas.value.getContext('2d') | |
| if (!ctx) return | |
| ctx.clearRect(0, 0, canvas.value.width, canvas.value.height) | |
| particles.forEach((particle, index) => { | |
| // 更新位置 | |
| particle.x += particle.vx | |
| particle.y += particle.vy | |
| // 边界检测 | |
| if (particle.x < 0 || particle.x > canvas.value!.width) { | |
| particle.vx *= -1 | |
| } | |
| if (particle.y < 0 || particle.y > canvas.value!.height) { | |
| particle.vy *= -1 | |
| } | |
| // 绘制粒子 | |
| ctx.beginPath() | |
| ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2) | |
| ctx.fillStyle = particle.color.replace(/[\d.]+\)$/g, `${particle.opacity})`) | |
| ctx.fill() | |
| // 绘制连线 | |
| particles.slice(index + 1).forEach(otherParticle => { | |
| const dx = particle.x - otherParticle.x | |
| const dy = particle.y - otherParticle.y | |
| const distance = Math.sqrt(dx * dx + dy * dy) | |
| if (distance < 100) { | |
| ctx.beginPath() | |
| ctx.moveTo(particle.x, particle.y) | |
| ctx.lineTo(otherParticle.x, otherParticle.y) | |
| ctx.strokeStyle = `rgba(102, 126, 234, ${0.1 * (1 - distance / 100)})` | |
| ctx.lineWidth = 1 | |
| ctx.stroke() | |
| } | |
| }) | |
| }) | |
| animationId = requestAnimationFrame(updateParticles) | |
| } | |
| const resizeCanvas = () => { | |
| if (!canvas.value || !container.value) return | |
| const rect = container.value.getBoundingClientRect() | |
| canvas.value.width = rect.width | |
| canvas.value.height = rect.height | |
| } | |
| onMounted(() => { | |
| if (!canvas.value || !container.value) return | |
| resizeCanvas() | |
| initParticles() | |
| updateParticles() | |
| window.addEventListener('resize', resizeCanvas) | |
| }) | |
| onUnmounted(() => { | |
| if (animationId) { | |
| cancelAnimationFrame(animationId) | |
| } | |
| window.removeEventListener('resize', resizeCanvas) | |
| }) | |
| </script> | |
| <style scoped> | |
| .particle-container { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .particle-canvas { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| </style> | |