From 384fe5c83f2ca113f8d2618367acd572bc453733 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:47:01 -0400 Subject: [PATCH] feat: Add warp tunnel effect for portals (#250) Implements a swirling vortex transition when entering portals. This includes: - Loading portal data from portals.json. - Rendering 3D portal objects in the Three.js scene. - Implementing collision detection between the camera and portals. - Developing a custom Three.js shader for the warp effect. - Triggering and animating the warp effect on portal entry. Fixes #250 --- app.js | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/app.js b/app.js index b396094..0bd5f92 100644 --- a/app.js +++ b/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} */ + + +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 === -- 2.43.0