|
1 | 1 | class ParticleBackground { |
2 | 2 | constructor(canvasId) { |
3 | 3 | this.canvas = document.getElementById(canvasId); |
4 | | - this.scene = null; |
5 | | - this.camera = null; |
6 | | - this.renderer = null; |
7 | | - this.particles = null; |
8 | | - this.animationId = null; |
| 4 | + if (!this.canvas) return; |
| 5 | + |
| 6 | + this.ctx = this.canvas.getContext('2d'); |
| 7 | + this.particles = []; |
9 | 8 | this.mouseX = 0; |
10 | 9 | this.mouseY = 0; |
11 | | - this.colors = [ |
12 | | - 0xed751c, 0x0ea5e9, 0xd946ef, |
13 | | - 0x10b981, 0xf59e0b, 0x8b5cf6 |
14 | | - ]; |
15 | 10 |
|
16 | | - this.init(); |
| 11 | + this.resize(); |
| 12 | + this.initParticles(); |
| 13 | + this.bindEvents(); |
| 14 | + this.animate(); |
17 | 15 | } |
18 | 16 |
|
19 | | - init() { |
20 | | - if (!this.canvas) return; |
21 | | - |
22 | | - const width = window.innerWidth; |
23 | | - const height = window.innerHeight; |
24 | | - |
25 | | - this.scene = new THREE.Scene(); |
26 | | - this.camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000); |
27 | | - this.camera.position.z = 50; |
28 | | - |
29 | | - this.renderer = new THREE.WebGLRenderer({ |
30 | | - canvas: this.canvas, |
31 | | - alpha: true, |
32 | | - antialias: true |
33 | | - }); |
34 | | - this.renderer.setSize(width, height); |
35 | | - this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); |
36 | | - |
37 | | - this.createParticles(); |
38 | | - |
39 | | - window.addEventListener('resize', () => this.onResize()); |
40 | | - window.addEventListener('mousemove', (e) => this.onMouseMove(e)); |
41 | | - |
42 | | - this.animate(); |
| 17 | + resize() { |
| 18 | + this.canvas.width = window.innerWidth; |
| 19 | + this.canvas.height = window.innerHeight; |
43 | 20 | } |
44 | 21 |
|
45 | | - createParticles() { |
46 | | - const particleCount = 300; |
47 | | - const positions = new Float32Array(particleCount * 3); |
48 | | - const colors = new Float32Array(particleCount * 3); |
49 | | - |
50 | | - for (let i = 0; i < particleCount; i++) { |
51 | | - positions[i * 3] = (Math.random() - 0.5) * 100; |
52 | | - positions[i * 3 + 1] = (Math.random() - 0.5) * 100; |
53 | | - positions[i * 3 + 2] = (Math.random() - 0.5) * 50; |
54 | | - |
55 | | - const color = new THREE.Color(this.colors[Math.floor(Math.random() * this.colors.length)]); |
56 | | - colors[i * 3] = color.r; |
57 | | - colors[i * 3 + 1] = color.g; |
58 | | - colors[i * 3 + 2] = color.b; |
| 22 | + initParticles() { |
| 23 | + const count = Math.floor((this.canvas.width * this.canvas.height) / 15000); |
| 24 | + for (let i = 0; i < count; i++) { |
| 25 | + this.particles.push({ |
| 26 | + x: Math.random() * this.canvas.width, |
| 27 | + y: Math.random() * this.canvas.height, |
| 28 | + vx: (Math.random() - 0.5) * 0.5, |
| 29 | + vy: (Math.random() - 0.5) * 0.5, |
| 30 | + size: Math.random() * 2 + 1, |
| 31 | + alpha: Math.random() * 0.5 + 0.2, |
| 32 | + color: this.getRandomColor() |
| 33 | + }); |
59 | 34 | } |
60 | | - |
61 | | - const geometry = new THREE.BufferGeometry(); |
62 | | - geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); |
63 | | - geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); |
64 | | - |
65 | | - const material = new THREE.PointsMaterial({ |
66 | | - size: 2, |
67 | | - vertexColors: true, |
68 | | - transparent: true, |
69 | | - opacity: 0.8, |
70 | | - sizeAttenuation: true, |
71 | | - blending: THREE.AdditiveBlending |
72 | | - }); |
73 | | - |
74 | | - this.particles = new THREE.Points(geometry, material); |
75 | | - this.scene.add(this.particles); |
76 | 35 | } |
77 | 36 |
|
78 | | - onResize() { |
79 | | - const width = window.innerWidth; |
80 | | - const height = window.innerHeight; |
81 | | - |
82 | | - this.camera.aspect = width / height; |
83 | | - this.camera.updateProjectionMatrix(); |
84 | | - this.renderer.setSize(width, height); |
| 37 | + getRandomColor() { |
| 38 | + const colors = [ |
| 39 | + '139, 92, 246', |
| 40 | + '217, 70, 239', |
| 41 | + '244, 114, 182', |
| 42 | + '59, 130, 246', |
| 43 | + '6, 182, 212', |
| 44 | + '16, 185, 129' |
| 45 | + ]; |
| 46 | + return colors[Math.floor(Math.random() * colors.length)]; |
85 | 47 | } |
86 | 48 |
|
87 | | - onMouseMove(event) { |
88 | | - this.mouseX = (event.clientX / window.innerWidth) * 2 - 1; |
89 | | - this.mouseY = -(event.clientY / window.innerHeight) * 2 + 1; |
| 49 | + bindEvents() { |
| 50 | + window.addEventListener('resize', () => this.resize()); |
| 51 | + |
| 52 | + document.addEventListener('mousemove', (e) => { |
| 53 | + this.mouseX = e.clientX; |
| 54 | + this.mouseY = e.clientY; |
| 55 | + }); |
90 | 56 | } |
91 | 57 |
|
92 | 58 | animate() { |
93 | | - this.animationId = requestAnimationFrame(() => this.animate()); |
| 59 | + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); |
94 | 60 |
|
95 | | - if (this.particles) { |
96 | | - this.particles.rotation.x += 0.0005; |
97 | | - this.particles.rotation.y += 0.0008; |
| 61 | + this.particles.forEach(p => { |
| 62 | + p.x += p.vx; |
| 63 | + p.y += p.vy; |
98 | 64 |
|
99 | | - this.particles.rotation.x += (this.mouseY * 0.5 - this.particles.rotation.x) * 0.02; |
100 | | - this.particles.rotation.y += (this.mouseX * 0.5 - this.particles.rotation.y) * 0.02; |
101 | | - } |
| 65 | + if (p.x < 0 || p.x > this.canvas.width) p.vx *= -1; |
| 66 | + if (p.y < 0 || p.y > this.canvas.height) p.vy *= -1; |
| 67 | + |
| 68 | + const dx = this.mouseX - p.x; |
| 69 | + const dy = this.mouseY - p.y; |
| 70 | + const dist = Math.sqrt(dx * dx + dy * dy); |
| 71 | + |
| 72 | + if (dist < 150) { |
| 73 | + const force = (150 - dist) / 150; |
| 74 | + p.vx += (dx / dist) * force * 0.02; |
| 75 | + p.vy += (dy / dist) * force * 0.02; |
| 76 | + } |
| 77 | + |
| 78 | + this.ctx.beginPath(); |
| 79 | + this.ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); |
| 80 | + this.ctx.fillStyle = `rgba(${p.color}, ${p.alpha})`; |
| 81 | + this.ctx.fill(); |
| 82 | + }); |
102 | 83 |
|
103 | | - this.renderer.render(this.scene, this.camera); |
104 | | - } |
105 | | - |
106 | | - destroy() { |
107 | | - if (this.animationId) { |
108 | | - cancelAnimationFrame(this.animationId); |
109 | | - } |
110 | | - if (this.renderer) { |
111 | | - this.renderer.dispose(); |
112 | | - } |
113 | | - window.removeEventListener('resize', this.onResize); |
114 | | - window.removeEventListener('mousemove', this.onMouseMove); |
| 84 | + this.particles.forEach((p1, i) => { |
| 85 | + this.particles.slice(i + 1).forEach(p2 => { |
| 86 | + const dx = p1.x - p2.x; |
| 87 | + const dy = p1.y - p2.y; |
| 88 | + const dist = Math.sqrt(dx * dx + dy * dy); |
| 89 | + |
| 90 | + if (dist < 120) { |
| 91 | + this.ctx.beginPath(); |
| 92 | + this.ctx.moveTo(p1.x, p1.y); |
| 93 | + this.ctx.lineTo(p2.x, p2.y); |
| 94 | + this.ctx.strokeStyle = `rgba(${p1.color}, ${0.1 * (1 - dist / 120)})`; |
| 95 | + this.ctx.stroke(); |
| 96 | + } |
| 97 | + }); |
| 98 | + }); |
| 99 | + |
| 100 | + requestAnimationFrame(() => this.animate()); |
115 | 101 | } |
116 | 102 | } |
117 | | - |
118 | | -window.ParticleBackground = ParticleBackground; |
0 commit comments