diff --git a/theme_3d_systems/static/src/js/three/hero_network_scene.js b/theme_3d_systems/static/src/js/three/hero_network_scene.js new file mode 100644 index 0000000..9159ad3 --- /dev/null +++ b/theme_3d_systems/static/src/js/three/hero_network_scene.js @@ -0,0 +1,158 @@ +/** @odoo-module **/ + +import { loadThree } from './three_core_loader'; + +/** + * High-performance 3D Network Scene (Three.js WebGL) + * Features interconnected nodes, floating geometry, and mouse parallax. + */ +export class HeroNetworkScene { + constructor(containerEl, options = {}) { + this.container = containerEl; + this.options = Object.assign({ + particleCount: 65, + lineDistance: 130, + primaryColor: 0x00f2fe, + secondaryColor: 0x7928ca, + }, options); + + this.scene = null; + this.camera = null; + this.renderer = null; + this.particles = []; + this.linesMesh = null; + this.animationFrameId = null; + this.mouseX = 0; + this.mouseY = 0; + this.targetX = 0; + this.targetY = 0; + this.isDestroyed = false; + + this.init(); + } + + async init() { + const THREE = await loadThree(); + if (this.isDestroyed || !this.container) return; + + const width = this.container.clientWidth || window.innerWidth; + const height = this.container.clientHeight || window.innerHeight; + + // Scene & Camera + this.scene = new THREE.Scene(); + this.camera = new THREE.PerspectiveCamera(60, width / height, 1, 1000); + this.camera.position.z = 320; + + // Renderer + this.renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true, powerPreference: 'high-performance' }); + this.renderer.setSize(width, height); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + this.container.appendChild(this.renderer.domElement); + + // Nodes & Geometry + const particleGeo = new THREE.SphereGeometry(1.8, 12, 12); + const particleMat = new THREE.MeshBasicMaterial({ color: this.options.primaryColor }); + + for (let i = 0; i < this.options.particleCount; i++) { + const mesh = new THREE.Mesh(particleGeo, particleMat); + mesh.position.x = (Math.random() - 0.5) * 450; + mesh.position.y = (Math.random() - 0.5) * 350; + mesh.position.z = (Math.random() - 0.5) * 250; + + mesh.velocity = { + x: (Math.random() - 0.5) * 0.4, + y: (Math.random() - 0.5) * 0.4, + z: (Math.random() - 0.5) * 0.4, + }; + + this.scene.add(mesh); + this.particles.push(mesh); + } + + // Lines setup + const lineMat = new THREE.LineBasicMaterial({ + color: this.options.primaryColor, + transparent: true, + opacity: 0.25, + }); + const lineGeo = new THREE.BufferGeometry(); + this.linesMesh = new THREE.LineSegments(lineGeo, lineMat); + this.scene.add(this.linesMesh); + + // Mouse move listener for parallax + this.onMouseMove = (e) => { + this.mouseX = (e.clientX - window.innerWidth / 2) * 0.05; + this.mouseY = (e.clientY - window.innerHeight / 2) * 0.05; + }; + window.addEventListener('mousemove', this.onMouseMove, { passive: true }); + + // Resize handler + this.onResize = () => { + if (!this.container || !this.renderer || !this.camera) return; + const w = this.container.clientWidth; + const h = this.container.clientHeight; + this.camera.aspect = w / h; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(w, h); + }; + window.addEventListener('resize', this.onResize, { passive: true }); + + this.animate(THREE); + } + + animate(THREE) { + if (this.isDestroyed) return; + + this.animationFrameId = requestAnimationFrame(() => this.animate(THREE)); + + this.targetX += (this.mouseX - this.targetX) * 0.05; + this.targetY += (this.mouseY - this.targetY) * 0.05; + this.camera.position.x = this.targetX; + this.camera.position.y = -this.targetY; + this.camera.lookAt(this.scene.position); + + const positions = []; + + for (let i = 0; i < this.particles.length; i++) { + const p = this.particles[i]; + p.position.x += p.velocity.x; + p.position.y += p.velocity.y; + p.position.z += p.velocity.z; + + // Boundary wrap + if (Math.abs(p.position.x) > 240) p.velocity.x *= -1; + if (Math.abs(p.position.y) > 180) p.velocity.y *= -1; + if (Math.abs(p.position.z) > 140) p.velocity.z *= -1; + + // Connect lines + for (let j = i + 1; j < this.particles.length; j++) { + const p2 = this.particles[j]; + const dist = p.position.distanceTo(p2.position); + if (dist < this.options.lineDistance) { + positions.push(p.position.x, p.position.y, p.position.z); + positions.push(p2.position.x, p2.position.y, p2.position.z); + } + } + } + + if (this.linesMesh) { + this.linesMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + } + + this.renderer.render(this.scene, this.camera); + } + + destroy() { + this.isDestroyed = true; + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + window.removeEventListener('mousemove', this.onMouseMove); + window.removeEventListener('resize', this.onResize); + + if (this.renderer && this.renderer.domElement && this.renderer.domElement.parentNode) { + this.renderer.domElement.parentNode.removeChild(this.renderer.domElement); + this.renderer.dispose(); + } + } +}