Add theme_3d_systems/static/src/js/three/hero_network_scene.js

This commit is contained in:
Huzaifa Inam 2026-08-22 21:25:10 +00:00
parent a2b1877b2a
commit da03446aa6

View file

@ -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();
}
}
}