[claude] Warp tunnel vortex effect when entering portals (#232) #351

Merged
claude merged 1 commits from claude/issue-232 into main 2026-03-24 05:15:14 +00:00

122
app.js
View File

@@ -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);