feat: Add warp tunnel effect for portals (#250) #301

Merged
claude merged 1 commits from gemini/issue-250 into main 2026-03-24 04:47:42 +00:00

210
app.js
View File

@@ -3,6 +3,7 @@ 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 { LoadingManager } from 'three';
// === COLOR PALETTE ===
@@ -44,6 +45,9 @@ 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, 6, 11);
const raycaster = new THREE.Raycaster();
const forwardVector = new THREE.Vector3();
// === LIGHTING ===
// Required for MeshStandardMaterial / MeshPhysicalMaterial used on the platform.
const ambientLight = new THREE.AmbientLight(0x0a1428, 1.4);
@@ -411,6 +415,11 @@ document.addEventListener('keydown', (e) => {
// === PHOTO MODE ===
let photoMode = false;
// Warp effect state
let isWarping = false;
let warpStartTime = 0;
const WARP_DURATION = 1.5; // seconds
// Post-processing composer for depth of field (always-on, subtle)
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
@@ -656,6 +665,64 @@ for (let i = 0; i < RUNE_COUNT; i++) {
runeSprites.push({ sprite, baseAngle, floatPhase: (i / RUNE_COUNT) * Math.PI * 2 });
}
// === WARP TUNNEL EFFECT ===
const WarpShader = {
uniforms: {
'tDiffuse': { value: null },
'time': { value: 0.0 },
'distortionStrength': { value: 0.0 },
},
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 distortionStrength;
varying vec2 vUv;
void main() {
vec2 uv = vUv;
vec2 center = vec2(0.5, 0.5);
// Simple swirling distortion
vec2 dir = uv - center;
float angle = atan(dir.y, dir.x);
float radius = length(dir);
angle += radius * distortionStrength * sin(time * 5.0 + radius * 10.0);
radius *= 1.0 - distortionStrength * 0.1 * sin(time * 3.0 + radius * 5.0);
uv = center + vec2(cos(angle), sin(angle)) * radius;
gl_FragColor = texture2D(tDiffuse, uv);
}
`,
};
let warpPass = new ShaderPass(WarpShader);
warpPass.enabled = false;
composer.addPass(warpPass);
/**
* Triggers the warp tunnel effect.
*/
function startWarp() {
isWarping = true;
warpStartTime = clock.getElapsedTime();
warpPass.enabled = true;
warpPass.uniforms['time'].value = 0.0;
warpPass.uniforms['distortionStrength'].value = 0.0;
}
// === ANIMATION LOOP ===
const clock = new THREE.Clock();
@@ -770,6 +837,39 @@ function animate() {
rune.sprite.material.opacity = 0.65 + Math.sin(elapsed * 1.2 + rune.floatPhase) * 0.2;
}
// Portal collision detection
forwardVector.set(0, 0, -1).applyQuaternion(camera.quaternion);
raycaster.set(camera.position, forwardVector);
const intersects = raycaster.intersectObjects(portalGroup.children);
if (intersects.length > 0) {
const intersectedPortal = intersects[0].object;
console.log(`Entered portal: ${intersectedPortal.name}`);
if (!isWarping) {
startWarp();
}
}
// Warp effect animation
if (isWarping) {
const warpElapsed = elapsed - warpStartTime;
const progress = Math.min(warpElapsed / WARP_DURATION, 1.0);
warpPass.uniforms['time'].value = elapsed;
// Ease in and out distortion
if (progress < 0.5) {
warpPass.uniforms['distortionStrength'].value = progress * 2.0; // 0 to 1
} else {
warpPass.uniforms['distortionStrength'].value = (1.0 - progress) * 2.0; // 1 to 0
}
if (progress >= 1.0) {
isWarping = false;
warpPass.enabled = false;
warpPass.uniforms['distortionStrength'].value = 0.0;
}
}
composer.render();
}
@@ -931,8 +1031,117 @@ window.addEventListener('beforeunload', () => {
// === COMMIT BANNERS ===
const commitBanners = [];
const portalGroup = new THREE.Group();
scene.add(portalGroup);
/**
* Creates 3D representations of portals from the loaded data.
*/
function createPortals() {
const portalGeo = new THREE.TorusGeometry(3.0, 0.2, 16, 100);
portals.forEach(portal => {
const portalMat = new THREE.MeshBasicMaterial({
color: new THREE.Color(portal.color).convertSRGBToLinear(),
transparent: true,
opacity: 0.7,
blending: THREE.AdditiveBlending,
side: THREE.DoubleSide,
});
const portalMesh = new THREE.Mesh(portalGeo, portalMat);
portalMesh.position.set(portal.position.x, portal.position.y + 0.5, portal.position.z);
portalMesh.rotation.y = portal.rotation.y; // Apply Y rotation
portalMesh.rotation.x = Math.PI / 2; // Orient to stand vertically
portalMesh.name = `portal-${portal.id}`;
portalGroup.add(portalMesh);
});
}
// === PORTALS ===
/** @type {Array<Object>} */
let portals = [];
async function loadPortals() {
try {
const res = await fetch('./portals.json');
if (!res.ok) throw new Error('Portals not found');
portals = await res.json();
console.log('Loaded portals:', portals);
createPortals();
} catch (error) {
console.error('Failed to load portals:', error);
}
}
// === AGENT STATUS PANELS (declared early — populated after scene is ready) ===
/** @type {THREE.Sprite[]} */
const agentPanelSprites = [];
/**
@@ -1028,6 +1237,7 @@ async function initCommitBanners() {
}
initCommitBanners();
loadPortals();
// === AGENT STATUS BOARD ===