Files
the-nexus/modules/warp.js
Alexander Whitestone cbfacdfe19
Some checks failed
Deploy Nexus / deploy (push) Failing after 3s
Staging Smoke Test / smoke-test (push) Successful in 1s
refactor: split app.js (5416 lines) into 21 modules — hard cap 1000 lines/file
app.js: 5416 → 528 lines (entry point, animation loop, event wiring)
modules/state.js: shared mutable state object
modules/constants.js: color palette
modules/matrix-rain.js: matrix rain canvas effect
modules/scene-setup.js: scene, camera, renderer, lighting, stars
modules/platform.js: glass platform, perlin noise, floating island, clouds
modules/heatmap.js: commit heatmap
modules/sigil.js: Timmy sigil
modules/controls.js: mouse, overview, zoom, photo mode
modules/effects.js: energy beam, sovereignty meter, rune ring
modules/earth.js: holographic earth
modules/warp.js: warp tunnel, crystals, lightning
modules/dual-brain.js: dual-brain holographic panel
modules/audio.js: Web Audio, spatial, portal hums
modules/debug.js: debug mode, websocket, session export
modules/celebrations.js: easter egg, shockwave, fireworks
modules/portals.js: portal loading
modules/bookshelves.js: floating bookshelves, spine textures
modules/oath.js: The Oath interactive SOUL.md
modules/panels.js: agent status board, LoRA panel
modules/weather.js: weather system, portal health
modules/extras.js: gravity zones, speech, timelapse, bitcoin

Largest file: 528 lines (app.js). No file exceeds 1000.
All files pass node --check. No refactoring — mechanical split only.
2026-03-24 15:12:22 -04:00

327 lines
11 KiB
JavaScript

// === WARP TUNNEL + CRYSTALS + LIGHTNING + BATCAVE + DUAL-BRAIN ===
import * as THREE from 'three';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { NEXUS } from './constants.js';
import { scene, camera, renderer } from './scene-setup.js';
import { composer } from './controls.js';
import { zoneIntensity } from './heatmap.js';
import { S } from './state.js';
// === WARP TUNNEL EFFECT ===
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 const warpPass = new ShaderPass(WarpShader);
warpPass.enabled = false;
composer.addPass(warpPass);
export function startWarp(portalMesh) {
S.isWarping = true;
S.warpNavigated = false;
S.warpStartTime = clock.getElapsedTime();
warpPass.enabled = true;
warpPass.uniforms['time'].value = 0.0;
warpPass.uniforms['progress'].value = 0.0;
if (portalMesh) {
S.warpDestinationUrl = portalMesh.userData.destinationUrl || null;
S.warpPortalColor = portalMesh.userData.portalColor
? portalMesh.userData.portalColor.clone()
: new THREE.Color(0x4488ff);
} else {
S.warpDestinationUrl = null;
S.warpPortalColor = new THREE.Color(0x4488ff);
}
warpPass.uniforms['portalColor'].value = S.warpPortalColor;
}
// clock is created here and exported
export const clock = new THREE.Clock();
// === FLOATING CRYSTALS & LIGHTNING ARCS ===
const CRYSTAL_COUNT = 5;
const CRYSTAL_BASE_POSITIONS = [
new THREE.Vector3(-4.5, 3.2, -3.8),
new THREE.Vector3( 4.8, 2.8, -4.0),
new THREE.Vector3(-5.5, 4.0, 1.5),
new THREE.Vector3( 5.2, 3.5, 2.0),
new THREE.Vector3( 0.0, 5.0, -5.5),
];
export const CRYSTAL_COLORS = [0xff6440, 0x40a0ff, 0x40ff8c, 0xc840ff, 0xffd700];
const crystalGroupObj = new THREE.Group();
scene.add(crystalGroupObj);
export const crystals = [];
for (let i = 0; i < CRYSTAL_COUNT; i++) {
const geo = new THREE.OctahedronGeometry(0.35, 0);
const color = CRYSTAL_COLORS[i];
const mat = new THREE.MeshStandardMaterial({
color,
emissive: new THREE.Color(color).multiplyScalar(0.6),
roughness: 0.05,
metalness: 0.3,
transparent: true,
opacity: 0.88,
});
const mesh = new THREE.Mesh(geo, mat);
const basePos = CRYSTAL_BASE_POSITIONS[i].clone();
mesh.position.copy(basePos);
mesh.userData.zoomLabel = 'Crystal';
crystalGroupObj.add(mesh);
const light = new THREE.PointLight(color, 0.3, 6);
light.position.copy(basePos);
crystalGroupObj.add(light);
crystals.push({ mesh, light, basePos, floatPhase: (i / CRYSTAL_COUNT) * Math.PI * 2, flashStartTime: -999 });
}
// Lightning arc pool
export const LIGHTNING_POOL_SIZE = 6;
const LIGHTNING_SEGMENTS = 8;
export const LIGHTNING_REFRESH_MS = 130;
export const lightningArcs = [];
export const lightningArcMeta = [];
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const positions = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.LineBasicMaterial({
color: 0x88ccff, transparent: true, opacity: 0.0,
blending: THREE.AdditiveBlending, depthWrite: false,
});
const arc = new THREE.Line(geo, mat);
scene.add(arc);
lightningArcs.push(arc);
lightningArcMeta.push({ active: false, baseOpacity: 0, srcIdx: 0, dstIdx: 0 });
}
function buildLightningPath(start, end, jagAmount) {
const out = new Float32Array((LIGHTNING_SEGMENTS + 1) * 3);
for (let s = 0; s <= LIGHTNING_SEGMENTS; s++) {
const t = s / LIGHTNING_SEGMENTS;
const x = start.x + (end.x - start.x) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
const y = start.y + (end.y - start.y) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
const z = start.z + (end.z - start.z) * t + (s > 0 && s < LIGHTNING_SEGMENTS ? (Math.random() - 0.5) * jagAmount : 0);
out[s * 3] = x; out[s * 3 + 1] = y; out[s * 3 + 2] = z;
}
return out;
}
export function totalActivity() {
const vals = Object.values(zoneIntensity);
return vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
}
export function lerpColor(colorA, colorB, t) {
const ar = (colorA >> 16) & 0xff, ag = (colorA >> 8) & 0xff, ab = colorA & 0xff;
const br = (colorB >> 16) & 0xff, bg = (colorB >> 8) & 0xff, bb = colorB & 0xff;
const r = Math.round(ar + (br - ar) * t);
const g = Math.round(ag + (bg - ag) * t);
const b = Math.round(ab + (bb - ab) * t);
return (r << 16) | (g << 8) | b;
}
export function updateLightningArcs(elapsed) {
const activity = totalActivity();
const activeCount = Math.round(activity * LIGHTNING_POOL_SIZE);
for (let i = 0; i < LIGHTNING_POOL_SIZE; i++) {
const arc = lightningArcs[i];
const meta = lightningArcMeta[i];
if (i >= activeCount) {
arc.material.opacity = 0;
meta.active = false;
continue;
}
const a = Math.floor(Math.random() * CRYSTAL_COUNT);
let b = Math.floor(Math.random() * (CRYSTAL_COUNT - 1));
if (b >= a) b++;
const jagAmount = 0.45 + activity * 0.85;
const path = buildLightningPath(crystals[a].mesh.position, crystals[b].mesh.position, jagAmount);
const attr = arc.geometry.attributes.position;
attr.array.set(path);
attr.needsUpdate = true;
arc.material.color.setHex(lerpColor(CRYSTAL_COLORS[a], CRYSTAL_COLORS[b], 0.5));
const base = (0.35 + Math.random() * 0.55) * Math.min(activity * 1.5, 1.0);
arc.material.opacity = base;
meta.active = true;
meta.baseOpacity = base;
meta.srcIdx = a;
meta.dstIdx = b;
crystals[a].flashStartTime = elapsed;
crystals[b].flashStartTime = elapsed;
}
}
// === BATCAVE AREA ===
const BATCAVE_ORIGIN = new THREE.Vector3(-10, 0, -8);
export const batcaveGroup = new THREE.Group();
batcaveGroup.position.copy(BATCAVE_ORIGIN);
scene.add(batcaveGroup);
const batcaveProbeTarget = new THREE.WebGLCubeRenderTarget(128, {
type: THREE.HalfFloatType,
generateMipmaps: true,
minFilter: THREE.LinearMipmapLinearFilter,
});
export const batcaveProbe = new THREE.CubeCamera(0.1, 80, batcaveProbeTarget);
batcaveProbe.position.set(0, 1.2, -1);
batcaveGroup.add(batcaveProbe);
const batcaveFloorMat = new THREE.MeshStandardMaterial({
color: 0x0d1520, metalness: 0.92, roughness: 0.08, envMapIntensity: 1.4,
});
const batcaveWallMat = new THREE.MeshStandardMaterial({
color: 0x0a1828, metalness: 0.85, roughness: 0.15,
emissive: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.03),
envMapIntensity: 1.2,
});
const batcaveConsoleMat = new THREE.MeshStandardMaterial({
color: 0x060e16, metalness: 0.95, roughness: 0.05, envMapIntensity: 1.6,
});
export const batcaveMetallicMats = [batcaveFloorMat, batcaveWallMat, batcaveConsoleMat];
export const batcaveProbeTarget_texture = batcaveProbeTarget;
const batcaveFloor = new THREE.Mesh(new THREE.BoxGeometry(6, 0.08, 6), batcaveFloorMat);
batcaveFloor.position.y = -0.04;
batcaveGroup.add(batcaveFloor);
const batcaveBackWall = new THREE.Mesh(new THREE.BoxGeometry(6, 3, 0.1), batcaveWallMat);
batcaveBackWall.position.set(0, 1.5, -3);
batcaveGroup.add(batcaveBackWall);
const batcaveLeftWall = new THREE.Mesh(new THREE.BoxGeometry(0.1, 3, 6), batcaveWallMat);
batcaveLeftWall.position.set(-3, 1.5, 0);
batcaveGroup.add(batcaveLeftWall);
const batcaveConsoleBase = new THREE.Mesh(new THREE.BoxGeometry(3, 0.7, 1.2), batcaveConsoleMat);
batcaveConsoleBase.position.set(0, 0.35, -1.5);
batcaveGroup.add(batcaveConsoleBase);
const batcaveScreenBezel = new THREE.Mesh(new THREE.BoxGeometry(2.6, 1.4, 0.06), batcaveConsoleMat);
batcaveScreenBezel.position.set(0, 1.4, -2.08);
batcaveScreenBezel.rotation.x = Math.PI * 0.08;
batcaveGroup.add(batcaveScreenBezel);
const batcaveScreenGlow = new THREE.Mesh(
new THREE.PlaneGeometry(2.2, 1.1),
new THREE.MeshBasicMaterial({
color: new THREE.Color(NEXUS.colors.accent).multiplyScalar(0.65),
transparent: true, opacity: 0.82,
})
);
batcaveScreenGlow.position.set(0, 1.4, -2.05);
batcaveScreenGlow.rotation.x = Math.PI * 0.08;
batcaveGroup.add(batcaveScreenGlow);
const batcaveLight = new THREE.PointLight(NEXUS.colors.accent, 0.9, 14);
batcaveLight.position.set(0, 2.8, -1);
batcaveGroup.add(batcaveLight);
const batcaveCeilingStrip = new THREE.Mesh(
new THREE.BoxGeometry(4.2, 0.05, 0.14),
new THREE.MeshStandardMaterial({
color: NEXUS.colors.accent,
emissive: new THREE.Color(NEXUS.colors.accent),
emissiveIntensity: 1.1,
})
);
batcaveCeilingStrip.position.set(0, 2.95, -1.2);
batcaveGroup.add(batcaveCeilingStrip);
batcaveGroup.traverse(obj => {
if (obj.isMesh) obj.userData.zoomLabel = 'Batcave';
});