From 1875f050c68b7a739ecd56427369646ee900887d Mon Sep 17 00:00:00 2001 From: Teriuihi Date: Sun, 22 Jun 2025 00:55:38 +0200 Subject: [PATCH] Add orbit controls and mouse interaction to `ParticlesComponent` Integrated Three.js `OrbitControls` for smoother camera navigation and implemented new mouse interaction methods (`onMouseDown`, `onMouseUp`, `onMouseMove`) for enhanced usability. Adjusted scene, camera, and renderer setup for better responsiveness. --- .../app/particles/particles.component.scss | 3 + .../src/app/particles/particles.component.ts | 104 ++++++++++++++++-- 2 files changed, 97 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/particles/particles.component.scss b/frontend/src/app/particles/particles.component.scss index 2674064..ebb1f73 100644 --- a/frontend/src/app/particles/particles.component.scss +++ b/frontend/src/app/particles/particles.component.scss @@ -11,6 +11,9 @@ border-radius: 4px; overflow: hidden; background-color: #f0f0f0; + display: flex; + justify-content: center; + align-items: center; } .plane-controls { diff --git a/frontend/src/app/particles/particles.component.ts b/frontend/src/app/particles/particles.component.ts index 80b9c0f..b93eb53 100644 --- a/frontend/src/app/particles/particles.component.ts +++ b/frontend/src/app/particles/particles.component.ts @@ -12,6 +12,7 @@ import {MatCardModule} from '@angular/material/card'; import {MatIconModule} from '@angular/material/icon'; import {HeaderComponent} from '../header/header.component'; import * as THREE from 'three'; +import {OrbitControls} from 'three/examples/jsm/controls/OrbitControls.js'; // Define particle types export enum ParticleType { @@ -77,6 +78,7 @@ export class ParticlesComponent implements OnInit, AfterViewInit { private scene!: THREE.Scene; private camera!: THREE.PerspectiveCamera; private renderer!: THREE.WebGLRenderer; + private controls!: OrbitControls; private playerModel!: THREE.Group; private intersectionPlane!: THREE.Mesh; private particles: THREE.Mesh[] = []; @@ -127,16 +129,34 @@ export class ParticlesComponent implements OnInit, AfterViewInit { this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0xf0f0f0); + // Get container dimensions + const containerWidth = this.rendererContainer.nativeElement.clientWidth; + const containerHeight = 400; // Fixed height as defined in CSS + // Create camera - this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + this.camera = new THREE.PerspectiveCamera(75, containerWidth / containerHeight, 0.1, 1000); this.camera.position.set(0, 1, 3); this.camera.lookAt(0, 1, 0); // Create renderer this.renderer = new THREE.WebGLRenderer({antialias: true}); - this.renderer.setSize(window.innerWidth * 0.6, window.innerHeight * 0.6); + this.renderer.setSize(containerWidth, containerHeight); + + // Center the canvas in the container + this.renderer.domElement.style.display = 'block'; + this.renderer.domElement.style.margin = 'auto'; + this.rendererContainer.nativeElement.appendChild(this.renderer.domElement); + // Initialize orbit controls + this.controls = new OrbitControls(this.camera, this.renderer.domElement); + this.controls.enableDamping = true; // Add smooth damping effect + this.controls.dampingFactor = 0.05; + this.controls.minDistance = 2; // Minimum zoom distance + this.controls.maxDistance = 10; // Maximum zoom distance + this.controls.target.set(0, 1, 0); // Set target to player's center + this.controls.update(); + // Add lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); this.scene.add(ambientLight); @@ -152,7 +172,9 @@ export class ParticlesComponent implements OnInit, AfterViewInit { this.createIntersectionPlane(); // Add event listeners - this.renderer.domElement.addEventListener('click', this.onMouseClick.bind(this)); + this.renderer.domElement.addEventListener('mousedown', this.onMouseDown.bind(this)); + this.renderer.domElement.addEventListener('mouseup', this.onMouseUp.bind(this)); + this.renderer.domElement.addEventListener('mousemove', this.onMouseMove.bind(this)); window.addEventListener('resize', this.onWindowResize.bind(this)); } @@ -203,7 +225,8 @@ export class ParticlesComponent implements OnInit, AfterViewInit { // Create the intersection plane private createIntersectionPlane(): void { - const planeGeometry = new THREE.PlaneGeometry(2, 2); + // Make the plane larger to cover 1 block above and below the player (3 blocks tall total) + const planeGeometry = new THREE.PlaneGeometry(2, 3); const planeMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, @@ -213,6 +236,8 @@ export class ParticlesComponent implements OnInit, AfterViewInit { this.intersectionPlane = new THREE.Mesh(planeGeometry, planeMaterial); this.intersectionPlane.position.z = 0; + // Center the plane vertically with the player (player is about 2 blocks tall) + this.intersectionPlane.position.y = 1; this.scene.add(this.intersectionPlane); } @@ -226,8 +251,35 @@ export class ParticlesComponent implements OnInit, AfterViewInit { this.intersectionPlane.position.z = zPosition; } + // Track if mouse is being dragged + private isDragging = false; + private mouseDownTime = 0; + + // Handle mouse down event + private onMouseDown(event: MouseEvent): void { + this.isDragging = false; + this.mouseDownTime = Date.now(); + } + + // Handle mouse up event + private onMouseUp(event: MouseEvent): void { + // If mouse was down for less than 200ms and didn't move much, consider it a click, not a drag + if (Date.now() - this.mouseDownTime < 200 && !this.isDragging) { + this.handlePlaneClick(event); + } + this.isDragging = false; + } + + // Handle mouse move event + private onMouseMove(event: MouseEvent): void { + // If mouse moves while button is pressed, it's a drag + if (event.buttons > 0) { + this.isDragging = true; + } + } + // Handle mouse click on the plane - private onMouseClick(event: MouseEvent): void { + private handlePlaneClick(event: MouseEvent): void { // Calculate mouse position in normalized device coordinates const rect = this.renderer.domElement.getBoundingClientRect(); this.mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; @@ -245,6 +297,11 @@ export class ParticlesComponent implements OnInit, AfterViewInit { } } + // Legacy handler for backward compatibility + private onMouseClick(event: MouseEvent): void { + // This is now handled by onMouseUp + } + // Add a particle at the specified position private addParticle(x: number, y: number, z: number): void { // Create a visual representation of the particle @@ -280,18 +337,45 @@ export class ParticlesComponent implements OnInit, AfterViewInit { // Handle window resize private onWindowResize(): void { - this.camera.aspect = window.innerWidth / window.innerHeight; + const containerWidth = this.rendererContainer.nativeElement.clientWidth; + const containerHeight = 400; // Fixed height as defined in CSS + + this.camera.aspect = containerWidth / containerHeight; this.camera.updateProjectionMatrix(); - this.renderer.setSize(window.innerWidth * 0.6, window.innerHeight * 0.6); + this.renderer.setSize(containerWidth, containerHeight); } // Animation loop private animate(): void { requestAnimationFrame(this.animate.bind(this)); - // Rotate player model slightly for better view - if (this.playerModel) { - this.playerModel.rotation.y += 0.005; + // Update controls + if (this.controls) { + this.controls.update(); + } + + // Update plane orientation based on camera position + if (this.intersectionPlane && this.camera) { + // Calculate the angle between camera and player (in the XZ plane) + const cameraAngle = Math.atan2( + this.camera.position.x, + this.camera.position.z + ); + + // Determine which quadrant the camera is in (0-90, 90-180, 180-270, 270-360 degrees) + const quadrant = Math.floor((cameraAngle + Math.PI) / (Math.PI / 2)) % 4; + + // Rotate the plane to face the camera + if (quadrant === 0) { + this.intersectionPlane.rotation.y = 0; // Camera in front + } else if (quadrant === 1) { + this.intersectionPlane.rotation.y = Math.PI / 2; // Camera on right + } else if (quadrant === 2) { + this.intersectionPlane.rotation.y = Math.PI; // Camera behind + } else { + this.intersectionPlane.rotation.y = -Math.PI / 2; // Camera on left + } + } this.renderer.render(this.scene, this.camera);