From 9eaf6861b76315100bcd24781de695602c0b96e5 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:13:06 -0400 Subject: [PATCH] feat: add glowing cursor orb that follows mouse in 3D space Creates a glowing sphere with an additive-blended halo that tracks the cursor by unprojecting mouse NDC coordinates to a fixed depth in front of the camera. The orb smoothly lerps to the cursor position each frame and pulses in opacity/scale for a living, holographic feel. Fixes #106 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/app.js b/app.js index 14b95c0..fdab053 100644 --- a/app.js +++ b/app.js @@ -122,11 +122,59 @@ let mouseY = 0; let targetRotX = 0; let targetRotY = 0; +// Normalized device coords for cursor orb raycasting +const mouseNDC = new THREE.Vector2(); + document.addEventListener('mousemove', (/** @type {MouseEvent} */ e) => { mouseX = (e.clientX / window.innerWidth - 0.5) * 2; mouseY = (e.clientY / window.innerHeight - 0.5) * 2; + mouseNDC.x = mouseX; + mouseNDC.y = -mouseY; }); +// === CURSOR ORB === +// A glowing sphere that follows the cursor projected into 3D space +const ORB_DEPTH = 3.5; // world units in front of camera (z-plane distance) + +const orbGeo = new THREE.SphereGeometry(0.08, 16, 16); +const orbMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.accent, + transparent: true, + opacity: 0.9, +}); +const orbMesh = new THREE.Mesh(orbGeo, orbMat); +scene.add(orbMesh); + +// Outer glow halo — larger sphere, additive blending, low opacity +const haloGeo = new THREE.SphereGeometry(0.22, 16, 16); +const haloMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.accent, + transparent: true, + opacity: 0.18, + side: THREE.BackSide, + blending: THREE.AdditiveBlending, + depthWrite: false, +}); +const haloMesh = new THREE.Mesh(haloGeo, haloMat); +scene.add(haloMesh); + +// Smooth target position for orb +const orbTarget = new THREE.Vector3(); +const orbCurrent = new THREE.Vector3(); +const orbRaycaster = new THREE.Raycaster(); + +/** + * Projects mouse NDC coordinates to a world position at a fixed depth in front of the camera. + * @param {THREE.Vector2} ndc + * @param {number} depth - distance from camera in world units + * @returns {THREE.Vector3} + */ +function unprojectToDepth(ndc, depth) { + orbRaycaster.setFromCamera(ndc, camera); + const dir = orbRaycaster.ray.direction.clone().normalize(); + return orbRaycaster.ray.origin.clone().addScaledVector(dir, depth); +} + // === OVERVIEW MODE (Tab — bird's-eye view of the whole Nexus) === let overviewMode = false; let overviewT = 0; // 0 = normal view, 1 = overview @@ -255,6 +303,19 @@ function animate() { // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + // === UPDATE CURSOR ORB === + // Project cursor to 3D and smoothly lerp the orb toward it + orbTarget.copy(unprojectToDepth(mouseNDC, ORB_DEPTH)); + orbCurrent.lerp(orbTarget, 0.12); + orbMesh.position.copy(orbCurrent); + haloMesh.position.copy(orbCurrent); + + // Pulse: oscillate inner orb opacity and halo scale + const orbPulse = 0.8 + Math.sin(elapsed * 3.0) * 0.2; + orbMat.opacity = 0.7 * orbPulse + 0.2; + const haloScale = 1.0 + Math.sin(elapsed * 2.0) * 0.15; + haloMesh.scale.setScalar(haloScale); + if (photoMode) { orbitControls.update(); } -- 2.43.0