Spaces:
Runtime error
Runtime error
| 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); | |
| } | |
| } | |
| } |