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>
197 lines
6.7 KiB
JavaScript
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 };
|
|
}
|