feat: Add warp tunnel effect for portals (#250) #301
210
app.js
210
app.js
@@ -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 ===
|
||||
|
||||
|
||||
Reference in New Issue
Block a user