diff --git a/app.js b/app.js index 4902f2f..de26262 100644 --- a/app.js +++ b/app.js @@ -21,7 +21,16 @@ const scene = new THREE.Scene(); scene.background = new THREE.Color(NEXUS.colors.bg); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); -camera.position.set(0, 0, 5); +camera.position.set(0, 6, 11); + +// === LIGHTING === +// Required for MeshStandardMaterial / MeshPhysicalMaterial used on the platform. +const ambientLight = new THREE.AmbientLight(0x0a1428, 1.4); +scene.add(ambientLight); + +const overheadLight = new THREE.PointLight(0x8899bb, 0.6, 60); +overheadLight.position.set(0, 25, 0); +scene.add(overheadLight); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); @@ -116,6 +125,92 @@ function buildConstellationLines() { const constellationLines = buildConstellationLines(); scene.add(constellationLines); +// === GLASS PLATFORM === +// Central floating platform with transparent glass-floor sections revealing the void (star field) below. + +const glassPlatformGroup = new THREE.Group(); + +// Dark metallic frame material +const platformFrameMat = new THREE.MeshStandardMaterial({ + color: 0x0a1828, + metalness: 0.9, + roughness: 0.1, + emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.06), +}); + +// Outer solid rim (flat ring) +const platformRimGeo = new THREE.RingGeometry(4.7, 5.3, 64); +const platformRim = new THREE.Mesh(platformRimGeo, platformFrameMat); +platformRim.rotation.x = -Math.PI / 2; +glassPlatformGroup.add(platformRim); + +// Raised border torus for visible 3-D thickness +const borderTorusGeo = new THREE.TorusGeometry(5.0, 0.1, 6, 64); +const borderTorus = new THREE.Mesh(borderTorusGeo, platformFrameMat); +borderTorus.rotation.x = Math.PI / 2; +glassPlatformGroup.add(borderTorus); + +// Glass tile material — highly transmissive to reveal the void below +const glassTileMat = new THREE.MeshPhysicalMaterial({ + color: new THREE.Color(NEXUS.colors.accent), + transparent: true, + opacity: 0.09, + roughness: 0.0, + metalness: 0.0, + transmission: 0.92, + thickness: 0.06, + side: THREE.DoubleSide, + depthWrite: false, +}); + +// Edge glow — bright accent outline on each tile +const glassEdgeBaseMat = new THREE.LineBasicMaterial({ + color: NEXUS.colors.accent, + transparent: true, + opacity: 0.55, +}); + +const GLASS_TILE_SIZE = 0.85; +const GLASS_TILE_GAP = 0.14; +const GLASS_TILE_STEP = GLASS_TILE_SIZE + GLASS_TILE_GAP; +const GLASS_RADIUS = 4.55; + +const tileGeo = new THREE.PlaneGeometry(GLASS_TILE_SIZE, GLASS_TILE_SIZE); +const tileEdgeGeo = new THREE.EdgesGeometry(tileGeo); + +/** @type {Array<{mat: THREE.LineBasicMaterial, distFromCenter: number}>} */ +const glassEdgeMaterials = []; + +for (let row = -5; row <= 5; row++) { + for (let col = -5; col <= 5; col++) { + const x = col * GLASS_TILE_STEP; + const z = row * GLASS_TILE_STEP; + const distFromCenter = Math.sqrt(x * x + z * z); + if (distFromCenter > GLASS_RADIUS) continue; + + // Transparent glass tile + const tile = new THREE.Mesh(tileGeo, glassTileMat.clone()); + tile.rotation.x = -Math.PI / 2; + tile.position.set(x, 0, z); + glassPlatformGroup.add(tile); + + // Glowing edge lines + const mat = glassEdgeBaseMat.clone(); + const edges = new THREE.LineSegments(tileEdgeGeo, mat); + edges.rotation.x = -Math.PI / 2; + edges.position.set(x, 0.002, z); + glassPlatformGroup.add(edges); + glassEdgeMaterials.push({ mat, distFromCenter }); + } +} + +// Void shimmer — faint point light below the glass, emphasising the infinite depth +const voidLight = new THREE.PointLight(NEXUS.colors.accent, 0.5, 14); +voidLight.position.set(0, -3.5, 0); +glassPlatformGroup.add(voidLight); + +scene.add(glassPlatformGroup); + // === MOUSE-DRIVEN ROTATION === let mouseX = 0; let mouseY = 0; @@ -131,7 +226,7 @@ document.addEventListener('mousemove', (/** @type {MouseEvent} */ e) => { let overviewMode = false; let overviewT = 0; // 0 = normal view, 1 = overview -const NORMAL_CAM = new THREE.Vector3(0, 0, 5); +const NORMAL_CAM = new THREE.Vector3(0, 6, 11); const OVERVIEW_CAM = new THREE.Vector3(0, 200, 0.1); // overhead; tiny Z offset avoids gimbal lock const overviewIndicator = document.getElementById('overview-indicator'); @@ -250,6 +345,14 @@ function animate() { // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + // Glass platform — ripple edge glow outward from centre + for (const { mat, distFromCenter } of glassEdgeMaterials) { + const phase = elapsed * 1.1 - distFromCenter * 0.18; + mat.opacity = 0.25 + Math.sin(phase) * 0.22; + } + // Pulse the void light below + voidLight.intensity = 0.35 + Math.sin(elapsed * 1.4) * 0.2; + if (photoMode) { orbitControls.update(); composer.render();