class TrajectoryVisualizer { constructor(containerId) { this.container = document.getElementById(containerId); this.trajectories = []; this.objects = []; this.trails = []; this.isPlaying = false; this.currentFrame = 0; this.maxFrames = 0; this.showTrails = true; this.init(); } init() { // Scene this.scene = new THREE.Scene(); this.scene.fog = new THREE.Fog(0x1a1a2e, 5, 15); // Camera this.camera = new THREE.PerspectiveCamera( 75, this.container.clientWidth / this.container.clientHeight, 0.1, 1000 ); this.camera.position.set(3, 3, 3); this.camera.lookAt(0, 0, 0); // Renderer this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true }); this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); this.renderer.setClearColor(0x1a1a2e); this.container.appendChild(this.renderer.domElement); // Lighting const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); this.scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); directionalLight.position.set(5, 5, 5); this.scene.add(directionalLight); const pointLight = new THREE.PointLight(0x667eea, 1, 100); pointLight.position.set(0, 3, 0); this.scene.add(pointLight); // Grid helper const gridHelper = new THREE.GridHelper(4, 20, 0x444444, 0x222222); gridHelper.position.y = -1; this.scene.add(gridHelper); // Axes helper const axesHelper = new THREE.AxesHelper(2); this.scene.add(axesHelper); // Handle window resize window.addEventListener('resize', () => this.onWindowResize()); // Mouse controls this.setupControls(); // Start animation loop this.animate(); } setupControls() { let isDragging = false; let previousMousePosition = { x: 0, y: 0 }; this.container.addEventListener('mousedown', (e) => { isDragging = true; }); this.container.addEventListener('mousemove', (e) => { if (isDragging) { const deltaX = e.offsetX - previousMousePosition.x; const deltaY = e.offsetY - previousMousePosition.y; const rotationSpeed = 0.005; this.camera.position.applyAxisAngle( new THREE.Vector3(0, 1, 0), deltaX * rotationSpeed ); const lookAt = new THREE.Vector3(0, 0, 0); this.camera.lookAt(lookAt); } previousMousePosition = { x: e.offsetX, y: e.offsetY }; }); this.container.addEventListener('mouseup', () => { isDragging = false; }); // Zoom with mouse wheel this.container.addEventListener('wheel', (e) => { e.preventDefault(); const zoomSpeed = 0.1; const direction = new THREE.Vector3(); this.camera.getWorldDirection(direction); if (e.deltaY < 0) { this.camera.position.addScaledVector(direction, zoomSpeed); } else { this.camera.position.addScaledVector(direction, -zoomSpeed); } }); } loadTrajectories(trajectories) { this.trajectories = trajectories; this.currentFrame = 0; this.isPlaying = false; // Find max frames this.maxFrames = 0; trajectories.forEach(traj => { const maxFrame = Math.max(...traj.points.map(p => p.frame)); if (maxFrame > this.maxFrames) this.maxFrames = maxFrame; }); // Clear existing objects this.objects.forEach(obj => this.scene.remove(obj)); this.trails.forEach(line => this.scene.remove(line)); this.objects = []; this.trails = []; // Create objects for each trajectory const colors = [0xff6b6b, 0x4ecdc4, 0xffe66d, 0x95e1d3, 0xf38181, 0xaa96da, 0xfcbad3, 0xa8e6cf]; trajectories.forEach((traj, index) => { const color = colors[index % colors.length]; const points = traj.points; // Create trail line const positions = new Float32Array(points.length * 3); points.forEach((point, i) => { positions[i * 3] = point.x; positions[i * 3 + 1] = point.y; positions[i * 3 + 2] = point.z; }); const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); const material = new THREE.LineBasicMaterial({ color: color, linewidth: 2 }); const line = new THREE.Line(geometry, material); line.geometry.setDrawRange(0, 0); this.scene.add(line); this.trails.push(line); // Create sphere for current position const sphereGeometry = new THREE.SphereGeometry(0.05, 16, 16); const sphereMaterial = new THREE.MeshPhongMaterial({ color: color, emissive: color, emissiveIntensity: 0.5 }); const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial); sphere.visible = false; this.scene.add(sphere); this.objects.push(sphere); }); } play() { this.isPlaying = true; } pause() { this.isPlaying = false; } reset() { this.currentFrame = 0; this.isPlaying = false; this.objects.forEach(obj => obj.visible = false); this.trails.forEach(trail => { trail.geometry.setDrawRange(0, 0); }); } setShowTrails(show) { this.showTrails = show; this.trails.forEach(trail => trail.visible = show); } update() { if (this.isPlaying && this.trajectories.length > 0) { this.currentFrame++; // Update each trajectory this.trajectories.forEach((traj, idx) => { const points = traj.points; const currentPoint = points.find(p => p.frame === this.currentFrame); if (currentPoint) { const obj = this.objects[idx]; obj.position.set(currentPoint.x, currentPoint.y, currentPoint.z); obj.visible = true; // Update trail if (this.showTrails) { const trail = this.trails[idx]; const visiblePoints = points.filter(p => p.frame <= this.currentFrame); trail.geometry.setDrawRange(0, visiblePoints.length); } } }); // Loop animation if (this.currentFrame >= this.maxFrames) { this.currentFrame = 0; } } } animate() { requestAnimationFrame(() => this.animate()); this.update(); this.renderer.render(this.scene, this.camera); } onWindowResize() { this.camera.aspect = this.container.clientWidth / this.container.clientHeight; this.camera.updateProjectionMatrix(); this.renderer.setSize(this.container.clientWidth, this.container.clientHeight); } destroy() { window.removeEventListener('resize', () => this.onWindowResize()); this.renderer.dispose(); while(this.container.firstChild) { this.container.removeChild(this.container.firstChild); } } }