[claude] Warp tunnel vortex effect when entering portals (#232) #351
122
app.js
122
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);
|
||||
|
||||
Reference in New Issue
Block a user