Files
the-nexus/modules/core/scene.js
Perplexity Computer 675b61d65e
All checks were successful
CI / validate (pull_request) Successful in 14s
CI / auto-merge (pull_request) Successful in 0s
refactor: modularize app.js into ES module architecture
Split the monolithic 5393-line app.js into 32 focused ES modules under
modules/ with a thin ~330-line orchestrator. No bundler required — runs
in-browser via import maps.

Module structure:
  core/     — scene, ticker, state, theme, audio
  data/     — gitea, weather, bitcoin, loaders
  terrain/  — stars, clouds, island
  effects/  — matrix-rain, energy-beam, lightning, shockwave, rune-ring, gravity-zones
  panels/   — heatmap, sigil, sovereignty, dual-brain, batcave, earth, agent-board, lora-panel
  portals/  — portal-system, commit-banners
  narrative/ — bookshelves, oath, chat
  utils/    — perlin

All files pass node --check. No new dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 18:12:53 +00:00

197 lines
6.7 KiB
JavaScript

// modules/core/scene.js — Three.js scene setup
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { BokehPass } from 'three/addons/postprocessing/BokehPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { THEME } from './theme.js';
export let scene, camera, renderer, composer, orbitControls, bokehPass;
export const raycaster = new THREE.Raycaster();
export const forwardVector = new THREE.Vector3();
export const clock = new THREE.Clock();
// Loading manager
export const loadedAssets = new Map();
export const loadingManager = new THREE.LoadingManager();
// Placeholder texture
let placeholderTexture;
// Lights (exported for oath dimming)
export let ambientLight, overheadLight;
// Warp shader pass
export let warpPass;
const WarpShader = {
uniforms: {
'tDiffuse': { value: null },
'time': { value: 0.0 },
'progress': { value: 0.0 },
'portalColor': { value: new THREE.Color(0x4488ff) },
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float time;
uniform float progress;
uniform vec3 portalColor;
varying vec2 vUv;
#define PI 3.14159265358979
void main() {
vec2 uv = vUv;
vec2 center = vec2(0.5, 0.5);
vec2 dir = uv - center;
float dist = length(dir);
float angle = atan(dir.y, dir.x);
float intensity = sin(progress * PI);
float zoom = 1.0 + intensity * 3.0;
vec2 zoomedUV = center + dir / zoom;
float swirl = intensity * 5.0 * max(0.0, 1.0 - dist * 2.0);
float twisted = angle + swirl;
vec2 swirlUV = center + vec2(cos(twisted), sin(twisted)) * dist / (1.0 + intensity * 1.8);
vec2 warpUV = mix(zoomedUV, swirlUV, 0.6);
warpUV = clamp(warpUV, vec2(0.001), vec2(0.999));
float aber = intensity * 0.018;
vec2 aberDir = normalize(dir + vec2(0.001));
float rVal = texture2D(tDiffuse, clamp(warpUV + aberDir * aber, vec2(0.0), vec2(1.0))).r;
float gVal = texture2D(tDiffuse, warpUV).g;
float bVal = texture2D(tDiffuse, clamp(warpUV - aberDir * aber, vec2(0.0), vec2(1.0))).b;
vec4 color = vec4(rVal, gVal, bVal, 1.0);
float numLines = 28.0;
float lineAngleFrac = fract((angle / (2.0 * PI) + 0.5) * numLines + time * 4.0);
float lineSharp = pow(max(0.0, 1.0 - abs(lineAngleFrac - 0.5) * 16.0), 3.0);
float radialFade = max(0.0, 1.0 - dist * 2.2);
float speedLine = lineSharp * radialFade * intensity * 1.8;
float lineAngleFrac2 = fract((angle / (2.0 * PI) + 0.5) * 14.0 - time * 2.5);
float lineSharp2 = pow(max(0.0, 1.0 - abs(lineAngleFrac2 - 0.5) * 12.0), 3.0);
float speedLine2 = lineSharp2 * radialFade * intensity * 0.9;
float rimDist = abs(dist - 0.08 * intensity);
float rimGlow = pow(max(0.0, 1.0 - rimDist * 40.0), 2.0) * intensity;
color.rgb = mix(color.rgb, portalColor, intensity * 0.45);
color.rgb += portalColor * (speedLine + speedLine2);
color.rgb += vec3(1.0) * rimGlow * 0.8;
float bloom = pow(max(0.0, 1.0 - dist / (0.18 * intensity + 0.001)), 2.0) * intensity;
color.rgb += portalColor * bloom * 2.5 + vec3(1.0) * bloom * 0.6;
float vignette = smoothstep(0.5, 0.2, dist) * intensity * 0.5;
color.rgb *= 1.0 - vignette * 0.4;
float flash = smoothstep(0.82, 1.0, progress);
color.rgb = mix(color.rgb, vec3(1.0), flash);
gl_FragColor = color;
}
`,
};
export function initScene(onLoadComplete) {
// Loading manager setup
loadingManager.onLoad = () => {
document.getElementById('loading-bar').style.width = '100%';
document.getElementById('loading').style.display = 'none';
if (onLoadComplete) onLoadComplete();
};
loadingManager.onProgress = (url, itemsLoaded, itemsTotal) => {
const progress = (itemsLoaded / itemsTotal) * 100;
document.getElementById('loading-bar').style.width = `${progress}%`;
};
// Placeholder texture
const _placeholderCanvas = document.createElement('canvas');
_placeholderCanvas.width = 64;
_placeholderCanvas.height = 64;
const _placeholderCtx = _placeholderCanvas.getContext('2d');
_placeholderCtx.fillStyle = '#0a0a18';
_placeholderCtx.fillRect(0, 0, 64, 64);
placeholderTexture = new THREE.CanvasTexture(_placeholderCanvas);
loadedAssets.set('placeholder-texture', placeholderTexture);
loadingManager.itemStart('placeholder-texture');
loadingManager.itemEnd('placeholder-texture');
// Scene
scene = new THREE.Scene();
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000);
camera.position.set(0, 6, 11);
// Renderer — alpha:true so matrix rain canvas shows through
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setClearColor(0x000000, 0);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// Lights
ambientLight = new THREE.AmbientLight(0x0a1428, 1.4);
scene.add(ambientLight);
overheadLight = new THREE.SpotLight(0x8899bb, 0.6, 80, Math.PI / 3.5, 0.5, 1.0);
overheadLight.position.set(0, 25, 0);
overheadLight.target.position.set(0, 0, 0);
overheadLight.castShadow = true;
overheadLight.shadow.mapSize.set(2048, 2048);
overheadLight.shadow.camera.near = 5;
overheadLight.shadow.camera.far = 60;
overheadLight.shadow.bias = -0.001;
scene.add(overheadLight);
scene.add(overheadLight.target);
// Post-processing
composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
bokehPass = new BokehPass(scene, camera, {
focus: 5.0,
aperture: 0.00015,
maxblur: 0.004,
});
composer.addPass(bokehPass);
// Warp pass
warpPass = new ShaderPass(WarpShader);
warpPass.enabled = false;
composer.addPass(warpPass);
// Controls
orbitControls = new OrbitControls(camera, renderer.domElement);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.05;
orbitControls.enabled = false;
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
composer.setSize(window.innerWidth, window.innerHeight);
});
return { scene, camera, renderer, composer, orbitControls };
}