From 9151aeeb11601cff81a7bce9e19fd04bc929253c Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 01:14:13 -0400 Subject: [PATCH] feat: enhanced warp tunnel vortex effect for portal transitions (#232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace basic swirl shader with full vortex tunnel effect: - Radial speed lines (28 primary + 14 counter-rotating) streaking past - Zoom pull: scene rushes into the vortex mouth - Swirl twist intensifying with portal depth - Chromatic aberration at peak for sci-fi feel - Portal-colored bloom from vortex center - Tunnel rim glow ring - White screen flash at crossing moment - Add progress uniform (0→1) driving bell-curve intensity via sin(progress*PI) - Portal color tints the warp based on each portal's own color - Navigate to portal destination URL at flash peak (progress≥0.88) - Store destinationUrl and portalColor in portal mesh userData - Extend WARP_DURATION from 1.5s → 2.2s for full dramatic effect Fixes #232 --- app.js | 122 +++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 101 insertions(+), 21 deletions(-) diff --git a/app.js b/app.js index 781d3e7..167c204 100644 --- a/app.js +++ b/app.js @@ -1003,7 +1003,10 @@ let photoMode = false; // Warp effect state let isWarping = false; let warpStartTime = 0; -const WARP_DURATION = 1.5; // seconds +const WARP_DURATION = 2.2; // seconds +let warpDestinationUrl = null; +let warpPortalColor = new THREE.Color(0x4488ff); +let warpNavigated = false; // Post-processing composer for depth of field (always-on, subtle) const composer = new EffectComposer(renderer); @@ -1449,9 +1452,10 @@ earthGroup.traverse(obj => { // === WARP TUNNEL EFFECT === const WarpShader = { uniforms: { - 'tDiffuse': { value: null }, - 'time': { value: 0.0 }, - 'distortionStrength': { value: 0.0 }, + 'tDiffuse': { value: null }, + 'time': { value: 0.0 }, + 'progress': { value: 0.0 }, + 'portalColor': { value: new THREE.Color(0x4488ff) }, }, vertexShader: ` @@ -1465,24 +1469,79 @@ const WarpShader = { fragmentShader: ` uniform sampler2D tDiffuse; uniform float time; - uniform float distortionStrength; + 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); - - // Simple swirling distortion vec2 dir = uv - center; + float dist = length(dir); 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); + // Bell-curve intensity peaks at progress=0.5 + float intensity = sin(progress * PI); - uv = center + vec2(cos(angle), sin(angle)) * radius; + // === ZOOM: pull scene into the vortex mouth === + float zoom = 1.0 + intensity * 3.0; + vec2 zoomedUV = center + dir / zoom; - gl_FragColor = texture2D(tDiffuse, uv); + // === SWIRL: spiral twist increasing with intensity === + 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); + + // Blend zoom and swirl + vec2 warpUV = mix(zoomedUV, swirlUV, 0.6); + warpUV = clamp(warpUV, vec2(0.001), vec2(0.999)); + + // === CHROMATIC ABERRATION at peak === + 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); + + // === SPEED LINES: radial streaks flying past === + 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; + + // Secondary slower counter-rotating streaks + 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; + + // === TUNNEL RIM GLOW: bright ring at vortex edge === + float rimDist = abs(dist - 0.08 * intensity); + float rimGlow = pow(max(0.0, 1.0 - rimDist * 40.0), 2.0) * intensity; + + // === PORTAL COLOR TINT === + color.rgb = mix(color.rgb, portalColor, intensity * 0.45); + + // Speed lines in portal color + color.rgb += portalColor * (speedLine + speedLine2); + color.rgb += vec3(1.0) * rimGlow * 0.8; + + // === VORTEX CENTER BLOOM === + 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; + + // === EDGE DARKNESS (tunnel walls) === + float vignette = smoothstep(0.5, 0.2, dist) * intensity * 0.5; + color.rgb *= 1.0 - vignette * 0.4; + + // === WHITE FLASH at the moment of crossing === + float flash = smoothstep(0.82, 1.0, progress); + color.rgb = mix(color.rgb, vec3(1.0), flash); + + gl_FragColor = color; } `, }; @@ -1494,13 +1553,26 @@ composer.addPass(warpPass); /** * Triggers the warp tunnel effect. + * @param {THREE.Mesh|null} portalMesh - The portal mesh being entered (for color + URL) */ -function startWarp() { +function startWarp(portalMesh) { isWarping = true; + warpNavigated = false; warpStartTime = clock.getElapsedTime(); warpPass.enabled = true; warpPass.uniforms['time'].value = 0.0; - warpPass.uniforms['distortionStrength'].value = 0.0; + warpPass.uniforms['progress'].value = 0.0; + + if (portalMesh) { + warpDestinationUrl = portalMesh.userData.destinationUrl || null; + warpPortalColor = portalMesh.userData.portalColor + ? portalMesh.userData.portalColor.clone() + : new THREE.Color(0x4488ff); + } else { + warpDestinationUrl = null; + warpPortalColor = new THREE.Color(0x4488ff); + } + warpPass.uniforms['portalColor'].value = warpPortalColor; } // === FLOATING CRYSTALS & LIGHTNING ARCS === @@ -1880,7 +1952,7 @@ function animate() { const intersectedPortal = intersects[0].object; console.log(`Entered portal: ${intersectedPortal.name}`); if (!isWarping) { - startWarp(); + startWarp(intersectedPortal); } } @@ -1889,17 +1961,23 @@ function animate() { 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 + warpPass.uniforms['progress'].value = progress; + + // Navigate to destination URL at the flash peak (progress ~0.88) + if (!warpNavigated && progress >= 0.88 && warpDestinationUrl) { + warpNavigated = true; + setTimeout(() => { window.location.href = warpDestinationUrl; }, 180); } if (progress >= 1.0) { isWarping = false; warpPass.enabled = false; - warpPass.uniforms['distortionStrength'].value = 0.0; + warpPass.uniforms['progress'].value = 0.0; + // Fallback navigation if URL redirect hasn't fired yet + if (!warpNavigated && warpDestinationUrl) { + warpNavigated = true; + window.location.href = warpDestinationUrl; + } } } @@ -2601,6 +2679,8 @@ function createPortals() { portalMesh.name = `portal-${portal.id}`; + portalMesh.userData.destinationUrl = portal.destination?.url || null; + portalMesh.userData.portalColor = new THREE.Color(portal.color).convertSRGBToLinear(); portalGroup.add(portalMesh); -- 2.43.0