Compare commits
No commits in common. "8d52d8902459b5d0ced6a04a847c44bf71404d09" and "8390ac15b1385152937998f95d1bfc07acb19029" have entirely different histories.
8d52d89024
...
8390ac15b1
298
index1.html
298
index1.html
|
@ -1,298 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Water-like Cursor Ripples — Strong Chromatic Dispersion</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<style>
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
body {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, "Helvetica Neue", Arial;
|
|
||||||
background: #000000; /* black background */
|
|
||||||
color: #ffefcc; /* keep nav/footer readable */
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
nav {
|
|
||||||
position: fixed; top: 0; left: 0; right: 0;
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
padding: 16px 24px; z-index: 2; pointer-events: none;
|
|
||||||
}
|
|
||||||
.nav-left, .nav-right { display: flex; gap: 16px; align-items: center; pointer-events: auto; }
|
|
||||||
.logo { font-weight: 700; letter-spacing: 0.5px; }
|
|
||||||
.nav-right button {
|
|
||||||
background: transparent; color: #ffefcc; border: 1px solid #ffefcc66;
|
|
||||||
border-radius: 999px; padding: 8px 14px; cursor: pointer;
|
|
||||||
}
|
|
||||||
footer {
|
|
||||||
position: fixed; bottom: 0; left: 0; right: 0;
|
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
|
||||||
padding: 16px 24px; z-index: 2; pointer-events: none;
|
|
||||||
}
|
|
||||||
footer .title { width: 40%; font-size: clamp(24px, 6vw, 64px); line-height: 1.1; font-weight: 800; }
|
|
||||||
footer .links { display: flex; gap: 20px; pointer-events: auto; }
|
|
||||||
canvas.webgl { position: fixed; inset: 0; width: 100vw; height: 100vh; display: block; z-index: 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<nav>
|
|
||||||
<div class="nav-left">
|
|
||||||
<div class="logo">YP</div>
|
|
||||||
<p>Work</p><p>About</p><p>Contact</p>
|
|
||||||
</div>
|
|
||||||
<div class="nav-right"><button>Get Started</button></div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
<div class="title">YP</div>
|
|
||||||
<div class="links">
|
|
||||||
<a href="#" style="color:#ffefcc;">Twitter</a>
|
|
||||||
<a href="#" style="color:#ffefcc;">Instagram</a>
|
|
||||||
<a href="#" style="color:#ffefcc;">Discord</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<canvas class="webgl"></canvas>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
import * as THREE from 'https://unpkg.com/three@0.160.0/build/three.module.js';
|
|
||||||
|
|
||||||
// Config
|
|
||||||
const DPR_MAX = 2;
|
|
||||||
const BASE_TEXT = 'YOUNG PANDAS';
|
|
||||||
const BG_COLOR = '#000000'; // black background on canvas texture
|
|
||||||
const TEXT_COLOR = '#ffffff'; // white center text
|
|
||||||
|
|
||||||
// Renderer
|
|
||||||
const canvas = document.querySelector('canvas.webgl');
|
|
||||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, DPR_MAX));
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
|
|
||||||
// Scenes & Camera
|
|
||||||
const simScene = new THREE.Scene();
|
|
||||||
const mainScene = new THREE.Scene();
|
|
||||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
||||||
|
|
||||||
// Render targets
|
|
||||||
let width = Math.floor(window.innerWidth * renderer.getPixelRatio());
|
|
||||||
let height = Math.floor(window.innerHeight * renderer.getPixelRatio());
|
|
||||||
const rtOptions = {
|
|
||||||
minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter,
|
|
||||||
wrapS: THREE.ClampToEdgeWrapping, wrapT: THREE.ClampToEdgeWrapping,
|
|
||||||
type: THREE.HalfFloatType ?? THREE.FloatType, format: THREE.RGBAFormat,
|
|
||||||
depthBuffer: false, stencilBuffer: false
|
|
||||||
};
|
|
||||||
let rta = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
|
||||||
let rtb = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
|
||||||
|
|
||||||
// Text Canvas -> Texture
|
|
||||||
let textCanvas, textCtx, textTexture;
|
|
||||||
function makeTextTexture() {
|
|
||||||
const dpr = renderer.getPixelRatio();
|
|
||||||
width = Math.floor(window.innerWidth * dpr);
|
|
||||||
height = Math.floor(window.innerHeight * dpr);
|
|
||||||
|
|
||||||
textCanvas = document.createElement('canvas');
|
|
||||||
textCanvas.width = width; textCanvas.height = height;
|
|
||||||
textCtx = textCanvas.getContext('2d', { alpha: true });
|
|
||||||
|
|
||||||
// Black background
|
|
||||||
textCtx.fillStyle = BG_COLOR;
|
|
||||||
textCtx.fillRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// Orange center text
|
|
||||||
const fontPx = Math.floor(Math.min(width, height) * 0.18);
|
|
||||||
textCtx.fillStyle = TEXT_COLOR;
|
|
||||||
textCtx.textAlign = 'center'; textCtx.textBaseline = 'middle';
|
|
||||||
textCtx.font = `800 ${fontPx}px Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial`;
|
|
||||||
textCtx.fillText(BASE_TEXT, width * 0.5, height * 0.5);
|
|
||||||
|
|
||||||
if (textTexture) textTexture.dispose();
|
|
||||||
textTexture = new THREE.CanvasTexture(textCanvas);
|
|
||||||
textTexture.needsUpdate = true;
|
|
||||||
textTexture.minFilter = THREE.LinearFilter;
|
|
||||||
textTexture.magFilter = THREE.LinearFilter;
|
|
||||||
textTexture.format = THREE.RGBAFormat;
|
|
||||||
}
|
|
||||||
makeTextTexture();
|
|
||||||
|
|
||||||
// Geometry
|
|
||||||
const quad = new THREE.PlaneGeometry(2, 2);
|
|
||||||
|
|
||||||
// Shaders
|
|
||||||
const passThroughVert = `
|
|
||||||
varying vec2 vUv;
|
|
||||||
void main(){ vUv = uv; gl_Position = vec4(position, 1.0); }
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Ripple simulation (tight brush, fast decay)
|
|
||||||
const simFrag = `
|
|
||||||
precision highp float;
|
|
||||||
varying vec2 vUv;
|
|
||||||
uniform sampler2D uTexture;
|
|
||||||
uniform vec2 uResolution;
|
|
||||||
uniform vec2 uMouse;
|
|
||||||
uniform float uTime;
|
|
||||||
void main(){
|
|
||||||
vec2 texel = 1.0 / uResolution;
|
|
||||||
vec2 data = texture2D(uTexture, vUv).xy;
|
|
||||||
float h = data.x;
|
|
||||||
float hPrev = data.y;
|
|
||||||
|
|
||||||
float hL = texture2D(uTexture, vUv - vec2(texel.x, 0.0)).x;
|
|
||||||
float hR = texture2D(uTexture, vUv + vec2(texel.x, 0.0)).x;
|
|
||||||
float hT = texture2D(uTexture, vUv + vec2(0.0, texel.y)).x;
|
|
||||||
float hB = texture2D(uTexture, vUv - vec2(0.0, texel.y)).x;
|
|
||||||
float sum = hL + hR + hT + hB;
|
|
||||||
|
|
||||||
float hNew = (sum * 0.5 - hPrev);
|
|
||||||
hNew *= 0.985;
|
|
||||||
|
|
||||||
vec2 frag = vUv * uResolution;
|
|
||||||
float radius = 8.0;
|
|
||||||
float dist = length(frag - uMouse);
|
|
||||||
float impulse = exp(-dist*dist/(2.0*radius*radius));
|
|
||||||
hNew += 0.25 * impulse;
|
|
||||||
|
|
||||||
hNew = mix(hNew, 0.0, 0.01);
|
|
||||||
gl_FragColor = vec4(hNew, h, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Render with strong, masked chromatic dispersion (offsets in pixels)
|
|
||||||
const renderFrag = `
|
|
||||||
precision highp float;
|
|
||||||
varying vec2 vUv;
|
|
||||||
uniform sampler2D uSim;
|
|
||||||
uniform sampler2D uText;
|
|
||||||
uniform vec2 uResolution;
|
|
||||||
uniform float uRefract;
|
|
||||||
uniform float uCAPixels; // RGB shift in pixels (large for strong separation)
|
|
||||||
uniform vec2 uCAMask; // gradient thresholds for where CA applies
|
|
||||||
|
|
||||||
void main(){
|
|
||||||
vec2 texel = 1.0 / uResolution;
|
|
||||||
|
|
||||||
// Height gradients
|
|
||||||
float hX = texture2D(uSim, vUv + vec2(texel.x, 0.0)).x
|
|
||||||
- texture2D(uSim, vUv - vec2(texel.x, 0.0)).x;
|
|
||||||
float hY = texture2D(uSim, vUv + vec2(0.0, texel.y)).x
|
|
||||||
- texture2D(uSim, vUv - vec2(0.0, texel.y)).x;
|
|
||||||
|
|
||||||
// Normal and base refraction
|
|
||||||
vec3 normal = normalize(vec3(-hX, -hY, 1.0));
|
|
||||||
vec2 baseUV = vUv + normal.xy * uRefract;
|
|
||||||
|
|
||||||
// Ripple strength mask (lower thresholds -> more CA on ripples)
|
|
||||||
float g = length(vec2(hX, hY));
|
|
||||||
float mask = smoothstep(uCAMask.x, uCAMask.y, g);
|
|
||||||
|
|
||||||
// Direction along gradient and large pixel-based offsets
|
|
||||||
vec2 dir = normalize(vec2(hX, hY) + 1e-6);
|
|
||||||
vec2 px = texel * uCAPixels;
|
|
||||||
|
|
||||||
// Exaggerated dispersion: push red/blue more than green
|
|
||||||
// Red goes +dir, Blue goes -dir, Green slightly centered/offset
|
|
||||||
vec2 rUV = baseUV + dir * (px * 1.30);
|
|
||||||
vec2 gUV = baseUV + dir * (px * 0.20);
|
|
||||||
vec2 bUV = baseUV - dir * (px * 1.35);
|
|
||||||
|
|
||||||
float r = texture2D(uText, rUV).r;
|
|
||||||
float gC = texture2D(uText, gUV).g;
|
|
||||||
float b = texture2D(uText, bUV).b;
|
|
||||||
|
|
||||||
vec4 caColor = vec4(r, gC, b, 1.0);
|
|
||||||
vec4 base = texture2D(uText, baseUV);
|
|
||||||
|
|
||||||
// Mix CA in only on ripples
|
|
||||||
vec4 color = mix(base, caColor, mask);
|
|
||||||
|
|
||||||
// Mild lighting accent
|
|
||||||
float light = dot(normal, normalize(vec3(0.0, 0.0, 1.0)));
|
|
||||||
color.rgb += 0.08 * light;
|
|
||||||
|
|
||||||
gl_FragColor = color;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Materials & meshes
|
|
||||||
const simUniforms = {
|
|
||||||
uTexture: { value: rta.texture },
|
|
||||||
uResolution: { value: new THREE.Vector2(width, height) },
|
|
||||||
uMouse: { value: new THREE.Vector2(-1.0, -1.0) },
|
|
||||||
uTime: { value: 0 }
|
|
||||||
};
|
|
||||||
const simMat = new THREE.ShaderMaterial({ vertexShader: passThroughVert, fragmentShader: simFrag, uniforms: simUniforms });
|
|
||||||
simScene.add(new THREE.Mesh(quad, simMat));
|
|
||||||
|
|
||||||
const renderUniforms = {
|
|
||||||
uSim: { value: rta.texture },
|
|
||||||
uText: { value: textTexture },
|
|
||||||
uResolution: { value: new THREE.Vector2(width, height) },
|
|
||||||
uRefract: { value: 0.8 },
|
|
||||||
uCAPixels: { value: 8.0 }, // big shift: increase to 10–14 for even more
|
|
||||||
uCAMask: { value: new THREE.Vector2(0.00008, 0.0005) } // lower => CA engages more
|
|
||||||
};
|
|
||||||
const renderMat = new THREE.ShaderMaterial({
|
|
||||||
vertexShader: passThroughVert, fragmentShader: renderFrag, uniforms: renderUniforms, transparent: true
|
|
||||||
});
|
|
||||||
mainScene.add(new THREE.Mesh(quad, renderMat));
|
|
||||||
|
|
||||||
// Mouse input
|
|
||||||
const mouse = new THREE.Vector2(-1, -1);
|
|
||||||
function setMouseFromEvent(e){
|
|
||||||
const dpr = renderer.getPixelRatio();
|
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
|
||||||
const x = (e.clientX - rect.left) * dpr;
|
|
||||||
const y = (e.clientY - rect.top) * dpr;
|
|
||||||
mouse.set(x, (rect.height * dpr) - y);
|
|
||||||
}
|
|
||||||
renderer.domElement.addEventListener('mousemove', (e)=>{ setMouseFromEvent(e); simUniforms.uMouse.value.copy(mouse); });
|
|
||||||
renderer.domElement.addEventListener('mouseleave', ()=>{ simUniforms.uMouse.value.set(-1.0, -1.0); });
|
|
||||||
|
|
||||||
// Resize
|
|
||||||
function onResize(){
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, DPR_MAX));
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
const dpr = renderer.getPixelRatio();
|
|
||||||
width = Math.floor(window.innerWidth * dpr);
|
|
||||||
height = Math.floor(window.innerHeight * dpr);
|
|
||||||
|
|
||||||
rta.dispose(); rtb.dispose();
|
|
||||||
rta = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
|
||||||
rtb = new THREE.WebGLRenderTarget(width, height, rtOptions);
|
|
||||||
|
|
||||||
simUniforms.uResolution.value.set(width, height);
|
|
||||||
renderUniforms.uResolution.value.set(width, height);
|
|
||||||
|
|
||||||
makeTextTexture();
|
|
||||||
renderUniforms.uText.value = textTexture;
|
|
||||||
}
|
|
||||||
window.addEventListener('resize', onResize);
|
|
||||||
|
|
||||||
// Animate
|
|
||||||
const clock = new THREE.Clock();
|
|
||||||
function animate(){
|
|
||||||
simUniforms.uTime.value = clock.getElapsedTime();
|
|
||||||
|
|
||||||
// Sim: rta -> rtb
|
|
||||||
simUniforms.uTexture.value = rta.texture;
|
|
||||||
renderer.setRenderTarget(rtb);
|
|
||||||
renderer.render(simScene, camera);
|
|
||||||
|
|
||||||
// Render with latest sim
|
|
||||||
renderUniforms.uSim.value = rtb.texture;
|
|
||||||
renderer.setRenderTarget(null);
|
|
||||||
renderer.render(mainScene, camera);
|
|
||||||
|
|
||||||
// Swap
|
|
||||||
const tmp = rta; rta = rtb; rtb = tmp;
|
|
||||||
|
|
||||||
requestAnimationFrame(animate);
|
|
||||||
}
|
|
||||||
animate();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,106 +0,0 @@
|
||||||
// Bold scene roughness animation state
|
|
||||||
export let boldRoughnessAnimation = {
|
|
||||||
isActive: false,
|
|
||||||
startTime: 0,
|
|
||||||
delayDuration: 1.0, // 1 second delay (will be dynamic)
|
|
||||||
transitionDuration: 1.0, // 1 second transition
|
|
||||||
startRoughness: 0.5,
|
|
||||||
endRoughness: 0.05,
|
|
||||||
materials: [] // Store references to bold materials
|
|
||||||
};
|
|
||||||
|
|
||||||
// Innovation glass animation state
|
|
||||||
export let innovationGlassAnimation = {
|
|
||||||
isActive: false,
|
|
||||||
startTime: 0,
|
|
||||||
transitionDuration: 0.2,
|
|
||||||
startIor: 1.0,
|
|
||||||
endIor: 2.0,
|
|
||||||
startThickness: 1.0,
|
|
||||||
endThickness: 2.0,
|
|
||||||
materials: [] // Store references to innovation glass materials
|
|
||||||
};
|
|
||||||
|
|
||||||
// Start/restart bold roughness animation with optional delay control
|
|
||||||
export function startBoldRoughnessAnimation(withDelay = true) {
|
|
||||||
console.log('Starting/restarting bold roughness animation');
|
|
||||||
// Reset all bold glass materials to starting roughness value
|
|
||||||
boldRoughnessAnimation.materials.forEach(material => {
|
|
||||||
material.roughness = boldRoughnessAnimation.startRoughness;
|
|
||||||
material.needsUpdate = true;
|
|
||||||
});
|
|
||||||
boldRoughnessAnimation.isActive = true;
|
|
||||||
boldRoughnessAnimation.startTime = performance.now();
|
|
||||||
// Set delayDuration based on withDelay parameter
|
|
||||||
boldRoughnessAnimation.delayDuration = withDelay ? 1.0 : 0.0;
|
|
||||||
console.log('Bold roughness animation started with delay:', withDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start innovation glass animation
|
|
||||||
export function startInnovationGlassAnimation() {
|
|
||||||
console.log('Starting innovation glass animation');
|
|
||||||
// Reset all innovation glass materials to starting values
|
|
||||||
innovationGlassAnimation.materials.forEach(material => {
|
|
||||||
material.ior = innovationGlassAnimation.startIor;
|
|
||||||
material.thickness = innovationGlassAnimation.startThickness;
|
|
||||||
material.needsUpdate = true;
|
|
||||||
});
|
|
||||||
innovationGlassAnimation.isActive = true;
|
|
||||||
innovationGlassAnimation.startTime = performance.now();
|
|
||||||
console.log('Innovation glass animation started');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateBoldRoughnessAnimation() {
|
|
||||||
if (boldRoughnessAnimation.isActive) {
|
|
||||||
const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000;
|
|
||||||
if (elapsed >= boldRoughnessAnimation.delayDuration) {
|
|
||||||
// Delay period is over, start roughness transition
|
|
||||||
const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration;
|
|
||||||
const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1);
|
|
||||||
// Smooth easing function (ease-in-out)
|
|
||||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
|
||||||
const easedProgress = easeInOut(transitionProgress);
|
|
||||||
// Interpolate roughness from 0.5 to 0.05
|
|
||||||
const currentRoughness = boldRoughnessAnimation.startRoughness +
|
|
||||||
(boldRoughnessAnimation.endRoughness - boldRoughnessAnimation.startRoughness) * easedProgress;
|
|
||||||
// Apply to all bold materials
|
|
||||||
boldRoughnessAnimation.materials.forEach(material => {
|
|
||||||
material.roughness = currentRoughness;
|
|
||||||
material.needsUpdate = true;
|
|
||||||
});
|
|
||||||
// End animation when complete
|
|
||||||
if (transitionProgress >= 1) {
|
|
||||||
boldRoughnessAnimation.isActive = false;
|
|
||||||
console.log('Bold roughness animation completed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateInnovationGlassAnimation() {
|
|
||||||
if (innovationGlassAnimation.isActive) {
|
|
||||||
const elapsed = (performance.now() - innovationGlassAnimation.startTime) / 1000;
|
|
||||||
const transitionProgress = Math.min(elapsed / innovationGlassAnimation.transitionDuration, 1);
|
|
||||||
// Smooth easing function (ease-in-out)
|
|
||||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
|
||||||
const easedProgress = easeInOut(transitionProgress);
|
|
||||||
// Interpolate IOR from 1.0 to 2.0
|
|
||||||
const currentIor = innovationGlassAnimation.startIor +
|
|
||||||
(innovationGlassAnimation.endIor - innovationGlassAnimation.startIor) * easedProgress;
|
|
||||||
// Interpolate thickness from 1.0 to 2.0
|
|
||||||
const currentThickness = innovationGlassAnimation.startThickness +
|
|
||||||
(innovationGlassAnimation.endThickness - innovationGlassAnimation.startThickness) * easedProgress;
|
|
||||||
// Apply to all innovation glass materials
|
|
||||||
innovationGlassAnimation.materials.forEach(material => {
|
|
||||||
material.ior = currentIor;
|
|
||||||
material.thickness = currentThickness;
|
|
||||||
material.needsUpdate = true;
|
|
||||||
});
|
|
||||||
// End animation when complete
|
|
||||||
if (transitionProgress >= 1) {
|
|
||||||
innovationGlassAnimation.isActive = false;
|
|
||||||
console.log('Innovation glass animation completed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,289 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
// Enhanced ripple simulation with multiple trailing ripples and lighting
|
|
||||||
|
|
||||||
const FluidSimShader = {
|
|
||||||
uniforms: {
|
|
||||||
tPrev: { value: null },
|
|
||||||
iResolution: { value: new THREE.Vector2() },
|
|
||||||
iTime: { value: 0.0 },
|
|
||||||
mouse: { value: new THREE.Vector3(-1, -1, 0.0) },
|
|
||||||
dissipation: { value: 0.950 }, // Slightly more persistent for trails
|
|
||||||
tension: { value: 2.2 }, // Higher tension for stronger ripples
|
|
||||||
radius: { value: 20.0 }, // Larger splat radius
|
|
||||||
trailLength: { value: 5 }, // Number of trailing ripples
|
|
||||||
},
|
|
||||||
|
|
||||||
vertexShader: `
|
|
||||||
varying vec2 vUv;
|
|
||||||
void main() {
|
|
||||||
vUv = uv;
|
|
||||||
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
fragmentShader: `
|
|
||||||
precision highp float;
|
|
||||||
varying vec2 vUv;
|
|
||||||
uniform sampler2D tPrev;
|
|
||||||
uniform vec2 iResolution;
|
|
||||||
uniform float iTime;
|
|
||||||
uniform vec3 mouse;
|
|
||||||
uniform float dissipation;
|
|
||||||
uniform float tension;
|
|
||||||
uniform float radius;
|
|
||||||
uniform float trailLength;
|
|
||||||
|
|
||||||
vec2 readRG(vec2 uv) {
|
|
||||||
vec4 c = texture2D(tPrev, uv);
|
|
||||||
return c.rg;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 texel = 1.0 / iResolution;
|
|
||||||
vec2 currPrev = readRG(vUv);
|
|
||||||
float curr = currPrev.r;
|
|
||||||
float prev = currPrev.g;
|
|
||||||
|
|
||||||
// Enhanced 8-neighbor laplacian for stronger ripples
|
|
||||||
float up = readRG(vUv + vec2(0.0, texel.y)).r;
|
|
||||||
float down = readRG(vUv + vec2(0.0, -texel.y)).r;
|
|
||||||
float right = readRG(vUv + vec2( texel.x, 0.0)).r;
|
|
||||||
float left = readRG(vUv + vec2(-texel.x, 0.0)).r;
|
|
||||||
|
|
||||||
// Diagonal neighbors for smoother ripples
|
|
||||||
float upLeft = readRG(vUv + vec2(-texel.x, texel.y)).r;
|
|
||||||
float upRight = readRG(vUv + vec2( texel.x, texel.y)).r;
|
|
||||||
float downLeft = readRG(vUv + vec2(-texel.x, -texel.y)).r;
|
|
||||||
float downRight = readRG(vUv + vec2( texel.x, -texel.y)).r;
|
|
||||||
|
|
||||||
// Enhanced laplacian with diagonal weights
|
|
||||||
float lap = (up + down + left + right) * 0.2 +
|
|
||||||
(upLeft + upRight + downLeft + downRight) * 0.05 - curr;
|
|
||||||
|
|
||||||
// Wave equation with enhanced parameters
|
|
||||||
float next = curr + (curr - prev) * dissipation + lap * tension;
|
|
||||||
|
|
||||||
// Multiple trailing ripples from mouse movement
|
|
||||||
if (mouse.z > 0.0001) {
|
|
||||||
vec2 uvPx = vUv * iResolution;
|
|
||||||
vec2 d = uvPx - mouse.xy;
|
|
||||||
float dist = length(d);
|
|
||||||
|
|
||||||
// Create multiple concentric ripples
|
|
||||||
for (float i = 0.0; i < 4.0; i++) {
|
|
||||||
if (i >= trailLength) break;
|
|
||||||
|
|
||||||
float offset = i * radius * 0.4; // Spacing between ripples
|
|
||||||
float r = radius + offset;
|
|
||||||
float timeOffset = i * 0.3; // Temporal offset for trailing effect
|
|
||||||
|
|
||||||
// Gaussian with sine wave for ripple pattern
|
|
||||||
float g = exp(-pow(dist - offset, 2.0) / (r * r * 0.5));
|
|
||||||
float ripple = sin(dist * 0.2 - iTime * 8.0 + timeOffset) * g;
|
|
||||||
|
|
||||||
// Diminishing strength for trailing ripples
|
|
||||||
float strength = mouse.z * (1.0 - i * 0.2) * 0.8;
|
|
||||||
next += ripple * strength;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_FragColor = vec4(next, curr, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enhanced distortion shader with dynamic lighting and ripple whiteness
|
|
||||||
export const FluidDistortionShader = {
|
|
||||||
uniforms: {
|
|
||||||
tDiffuse: { value: null },
|
|
||||||
tSim: { value: null },
|
|
||||||
iResolution: { value: new THREE.Vector2() },
|
|
||||||
amount: { value: 0.12 }, // Stronger base distortion
|
|
||||||
chromaticAmount: { value: 0.015 }, // Enhanced chromatic aberration
|
|
||||||
lightPosition: { value: new THREE.Vector3(0.5, 0.5, 1.0) }, // Light position
|
|
||||||
lightIntensity: { value: 1.5 }, // Light brightness
|
|
||||||
lightColor: { value: new THREE.Color(0.8, 0.9, 1.0) }, // Cool light color
|
|
||||||
normalStrength: { value: 2.0 }, // How pronounced the lighting effect is
|
|
||||||
ambientLight: { value: 0.15 }, // Base ambient lighting
|
|
||||||
rippleWhiteness: { value: 0.15 }, // Amount of white tint for ripples
|
|
||||||
rippleBrightness: { value: 1.8 }, // Brightness multiplier for ripple areas
|
|
||||||
},
|
|
||||||
|
|
||||||
vertexShader: `
|
|
||||||
varying vec2 vUv;
|
|
||||||
void main() {
|
|
||||||
vUv = uv;
|
|
||||||
gl_Position = vec4(position.xy, 0.0, 1.0);
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
|
|
||||||
fragmentShader: `
|
|
||||||
precision highp float;
|
|
||||||
varying vec2 vUv;
|
|
||||||
uniform sampler2D tDiffuse;
|
|
||||||
uniform sampler2D tSim;
|
|
||||||
uniform vec2 iResolution;
|
|
||||||
uniform float amount;
|
|
||||||
uniform float chromaticAmount;
|
|
||||||
uniform vec3 lightPosition;
|
|
||||||
uniform float lightIntensity;
|
|
||||||
uniform vec3 lightColor;
|
|
||||||
uniform float normalStrength;
|
|
||||||
uniform float ambientLight;
|
|
||||||
uniform float rippleWhiteness;
|
|
||||||
uniform float rippleBrightness;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 texel = 1.0 / iResolution;
|
|
||||||
|
|
||||||
// Sample height field for normal calculation
|
|
||||||
float hC = texture2D(tSim, vUv).r;
|
|
||||||
float hL = texture2D(tSim, vUv - vec2(texel.x, 0.0)).r;
|
|
||||||
float hR = texture2D(tSim, vUv + vec2(texel.x, 0.0)).r;
|
|
||||||
float hD = texture2D(tSim, vUv - vec2(0.0, texel.y)).r;
|
|
||||||
float hU = texture2D(tSim, vUv + vec2(0.0, texel.y)).r;
|
|
||||||
|
|
||||||
// Calculate gradient and normal
|
|
||||||
vec2 grad = vec2(hR - hL, hU - hD) * normalStrength;
|
|
||||||
vec3 normal = normalize(vec3(-grad.x, -grad.y, 1.0));
|
|
||||||
|
|
||||||
// Enhanced distortion with trailing effect
|
|
||||||
vec2 baseOffset = grad * amount;
|
|
||||||
|
|
||||||
// Add subtle trailing distortion based on height
|
|
||||||
vec2 trailOffset = grad * abs(hC) * amount * 0.3;
|
|
||||||
vec2 totalOffset = baseOffset + trailOffset;
|
|
||||||
|
|
||||||
// Chromatic aberration with enhanced separation
|
|
||||||
vec2 chromaticOffset = grad * chromaticAmount;
|
|
||||||
vec2 uvR = vUv + totalOffset + chromaticOffset;
|
|
||||||
vec2 uvG = vUv + totalOffset;
|
|
||||||
vec2 uvB = vUv + totalOffset - chromaticOffset;
|
|
||||||
|
|
||||||
// Clamp UVs
|
|
||||||
uvR = clamp(uvR, vec2(0.0), vec2(1.0));
|
|
||||||
uvG = clamp(uvG, vec2(0.0), vec2(1.0));
|
|
||||||
uvB = clamp(uvB, vec2(0.0), vec2(1.0));
|
|
||||||
|
|
||||||
// Sample distorted colors
|
|
||||||
float r = texture2D(tDiffuse, uvR).r;
|
|
||||||
float g = texture2D(tDiffuse, uvG).g;
|
|
||||||
float b = texture2D(tDiffuse, uvB).b;
|
|
||||||
vec3 distortedColor = vec3(r, g, b);
|
|
||||||
|
|
||||||
// Dynamic lighting calculation
|
|
||||||
vec3 lightDir = normalize(vec3(lightPosition.xy - vUv, lightPosition.z));
|
|
||||||
float NdotL = max(dot(normal, lightDir), 0.0);
|
|
||||||
|
|
||||||
// Create rim lighting effect for ripples
|
|
||||||
float rimLight = pow(1.0 - abs(dot(normal, vec3(0.0, 0.0, 1.0))), 2.0);
|
|
||||||
|
|
||||||
// Combine lighting effects
|
|
||||||
vec3 lighting = lightColor * (NdotL * lightIntensity + rimLight * 0.3) + ambientLight;
|
|
||||||
|
|
||||||
// Calculate ripple intensity for both lighting and whiteness
|
|
||||||
float rippleIntensity = abs(hC) + length(grad) * 0.5;
|
|
||||||
rippleIntensity = clamp(rippleIntensity, 0.0, 1.0);
|
|
||||||
|
|
||||||
// Apply lighting selectively - stronger where there are ripples
|
|
||||||
vec3 litColor = mix(distortedColor, distortedColor * lighting, rippleIntensity);
|
|
||||||
|
|
||||||
// Add white tint to ripples for visibility over black areas
|
|
||||||
vec3 whiteColor = vec3(1.0, 1.0, 1.0);
|
|
||||||
|
|
||||||
// Create a smooth falloff for the whiteness effect
|
|
||||||
float whiteIntensity = smoothstep(0.0, 0.3, rippleIntensity) * rippleWhiteness;
|
|
||||||
|
|
||||||
// Blend in the white tint
|
|
||||||
vec3 rippleColor = mix(litColor, whiteColor, whiteIntensity);
|
|
||||||
|
|
||||||
// Brighten areas with ripples
|
|
||||||
rippleColor = mix(rippleColor, rippleColor * rippleBrightness, rippleIntensity * 0.5);
|
|
||||||
|
|
||||||
gl_FragColor = vec4(rippleColor, 1.0);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
};
|
|
||||||
|
|
||||||
// Enhanced fluid simulation factory
|
|
||||||
export function createFluidSimulation(renderer, dpr = 1) {
|
|
||||||
const simScene = new THREE.Scene();
|
|
||||||
const simCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
|
||||||
|
|
||||||
const quad = new THREE.Mesh(
|
|
||||||
new THREE.PlaneGeometry(2, 2),
|
|
||||||
new THREE.ShaderMaterial({
|
|
||||||
uniforms: THREE.UniformsUtils.clone(FluidSimShader.uniforms),
|
|
||||||
vertexShader: FluidSimShader.vertexShader,
|
|
||||||
fragmentShader: FluidSimShader.fragmentShader,
|
|
||||||
depthTest: false,
|
|
||||||
depthWrite: false
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
simScene.add(quad);
|
|
||||||
|
|
||||||
// Higher precision for better ripple quality
|
|
||||||
const params = {
|
|
||||||
minFilter: THREE.LinearFilter,
|
|
||||||
magFilter: THREE.LinearFilter,
|
|
||||||
format: THREE.RGBAFormat,
|
|
||||||
type: THREE.FloatType, // Use float for better precision
|
|
||||||
depthBuffer: false,
|
|
||||||
stencilBuffer: false
|
|
||||||
};
|
|
||||||
|
|
||||||
let width = Math.max(2, Math.floor(window.innerWidth * dpr));
|
|
||||||
let height = Math.max(2, Math.floor(window.innerHeight * dpr));
|
|
||||||
|
|
||||||
let rtA = new THREE.WebGLRenderTarget(width, height, params);
|
|
||||||
let rtB = new THREE.WebGLRenderTarget(width, height, params);
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
renderer.setRenderTarget(rtA);
|
|
||||||
renderer.clear();
|
|
||||||
renderer.setRenderTarget(rtB);
|
|
||||||
renderer.clear();
|
|
||||||
renderer.setRenderTarget(null);
|
|
||||||
|
|
||||||
quad.material.uniforms.iResolution.value.set(width, height);
|
|
||||||
quad.material.uniforms.tPrev.value = rtA.texture;
|
|
||||||
|
|
||||||
function swap() {
|
|
||||||
const tmp = rtA; rtA = rtB; rtB = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
function update(mouseX, mouseY, strength, timeSec) {
|
|
||||||
quad.material.uniforms.iTime.value = timeSec;
|
|
||||||
|
|
||||||
if (mouseX < 0.0 || mouseY < 0.0) {
|
|
||||||
quad.material.uniforms.mouse.value.set(-1, -1, 0.0);
|
|
||||||
} else {
|
|
||||||
// Enhanced strength for better trailing effect
|
|
||||||
const enhancedStrength = Math.max(0.0, Math.min(1.0, strength * 1.5));
|
|
||||||
quad.material.uniforms.mouse.value.set(mouseX, mouseY, enhancedStrength);
|
|
||||||
}
|
|
||||||
|
|
||||||
quad.material.uniforms.tPrev.value = rtA.texture;
|
|
||||||
renderer.setRenderTarget(rtB);
|
|
||||||
renderer.render(simScene, simCamera);
|
|
||||||
renderer.setRenderTarget(null);
|
|
||||||
|
|
||||||
swap();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTexture() {
|
|
||||||
return rtA.texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize(w, h, newDpr = dpr) {
|
|
||||||
width = Math.max(2, Math.floor(w * newDpr));
|
|
||||||
height = Math.max(2, Math.floor(h * newDpr));
|
|
||||||
rtA.setSize(width, height);
|
|
||||||
rtB.setSize(width, height);
|
|
||||||
quad.material.uniforms.iResolution.value.set(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { update, getTexture, resize };
|
|
||||||
}
|
|
341
src/innovation.js
Normal file
341
src/innovation.js
Normal file
|
@ -0,0 +1,341 @@
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||||
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||||
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||||
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
|
camera.setFocalLength(50);
|
||||||
|
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
let isTwisting = false;
|
||||||
|
let twistProgress = 0;
|
||||||
|
const twistSpeed = 0.05; // Adjust speed
|
||||||
|
const twistStrength = 0.3; // Adjust strength
|
||||||
|
let scrollCount = 0;
|
||||||
|
const scrollThreshold = 20; // Number of scroll events to trigger the animation
|
||||||
|
|
||||||
|
|
||||||
|
// Renderer setup
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setClearColor(0x000000);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1.2;
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.physicallyCorrectLights = true;
|
||||||
|
|
||||||
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Post-processing: Bloom
|
||||||
|
const composer = new EffectComposer(renderer);
|
||||||
|
const renderPass = new RenderPass(scene, camera);
|
||||||
|
composer.addPass(renderPass);
|
||||||
|
|
||||||
|
const bloomPass = new UnrealBloomPass(
|
||||||
|
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||||
|
1.0, // strength
|
||||||
|
0.45, // radius
|
||||||
|
0.85 // threshold
|
||||||
|
);
|
||||||
|
composer.addPass(bloomPass);
|
||||||
|
|
||||||
|
// Video texture for emissive "screen"-like effect on orange material
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = '/shader-flash.webm';
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.preload = 'auto';
|
||||||
|
|
||||||
|
const videoTexture = new THREE.VideoTexture(video);
|
||||||
|
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
videoTexture.generateMipmaps = false;
|
||||||
|
videoTexture.minFilter = THREE.LinearFilter;
|
||||||
|
videoTexture.magFilter = THREE.LinearFilter;
|
||||||
|
|
||||||
|
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
||||||
|
video.play().catch(() => {});
|
||||||
|
|
||||||
|
// Local procedural environment for better PBR response (no network)
|
||||||
|
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||||
|
const roomEnv = new RoomEnvironment();
|
||||||
|
scene.environment = pmrem.fromScene(roomEnv).texture;
|
||||||
|
pmrem.dispose();
|
||||||
|
roomEnv.dispose();
|
||||||
|
scene.environment = null; // This will make the renderer's clear color visible again
|
||||||
|
|
||||||
|
// Lighting is authored below.
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
||||||
|
hemiLight.position.set(0, 20, 0);
|
||||||
|
scene.add(hemiLight);
|
||||||
|
|
||||||
|
// // Key light (main directional) - angled to avoid direct reflection
|
||||||
|
// const keyLight = new THREE.DirectionalLight(0xffffff, 2.0);
|
||||||
|
// keyLight.position.set(12, 8, 8);
|
||||||
|
// keyLight.castShadow = true;
|
||||||
|
// keyLight.shadow.mapSize.width = 2048;
|
||||||
|
// keyLight.shadow.mapSize.height = 2048;
|
||||||
|
// scene.add(keyLight);
|
||||||
|
|
||||||
|
// Fill light (opposite side) - angled
|
||||||
|
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
|
fillLight.position.set(-12, 6, -8);
|
||||||
|
scene.add(fillLight);
|
||||||
|
|
||||||
|
// Top light - angled to avoid direct downward reflection
|
||||||
|
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
topLight.position.set(5, 15, 5);
|
||||||
|
scene.add(topLight);
|
||||||
|
|
||||||
|
// Bottom light - angled upward
|
||||||
|
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
bottomLight.position.set(-3, -8, 3);
|
||||||
|
scene.add(bottomLight);
|
||||||
|
|
||||||
|
// Side lights for even illumination - angled
|
||||||
|
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
leftLight.position.set(-12, 2, 5);
|
||||||
|
scene.add(leftLight);
|
||||||
|
|
||||||
|
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
rightLight.position.set(12, 2, -5);
|
||||||
|
scene.add(rightLight);
|
||||||
|
|
||||||
|
// Front and back lights - angled to avoid direct camera reflection
|
||||||
|
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
frontLight.position.set(8, 4, 12);
|
||||||
|
scene.add(frontLight);
|
||||||
|
|
||||||
|
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
backLight.position.set(-8, 4, -12);
|
||||||
|
scene.add(backLight);
|
||||||
|
|
||||||
|
// Reduced camera light
|
||||||
|
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
||||||
|
camera.add(cameraLight);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
let mixer = null;
|
||||||
|
|
||||||
|
loader.load('/innovation.glb', (gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
scene.add(model);
|
||||||
|
|
||||||
|
// --- Define and Apply Materials ---
|
||||||
|
const glassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.2,
|
||||||
|
roughness: 0.05,
|
||||||
|
transmission: 1,
|
||||||
|
ior: 2,
|
||||||
|
thickness: 2,
|
||||||
|
clearcoat: 1.0,
|
||||||
|
clearcoatRoughness: 0.1,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 0.8,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0x000000),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
alphaTest: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff8600, metalness: 0.05, roughness: 0.4,
|
||||||
|
envMapIntensity: 0, emissive: new THREE.Color(0xffad47),
|
||||||
|
emissiveMap: videoTexture, emissiveIntensity: 2.25
|
||||||
|
});
|
||||||
|
|
||||||
|
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
||||||
|
const targetGlassNames = ['Cube.alt90.df'];
|
||||||
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
const nameMatches = (name, targets) => {
|
||||||
|
const clean = sanitize(name);
|
||||||
|
return targets.some((t) => {
|
||||||
|
const ct = sanitize(t);
|
||||||
|
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
if (nameMatches(object.name, targetGlassNames)) {
|
||||||
|
// Create outer glass shell
|
||||||
|
object.material = glassMaterial.clone();
|
||||||
|
object.material.side = THREE.DoubleSide;
|
||||||
|
object.material.depthWrite = false;
|
||||||
|
object.renderOrder = 2; // Render outer glass last
|
||||||
|
|
||||||
|
// Create inner glass shell for better depth perception
|
||||||
|
const innerShell = object.clone();
|
||||||
|
innerShell.material = glassMaterial.clone();
|
||||||
|
innerShell.material.side = THREE.DoubleSide;
|
||||||
|
innerShell.material.depthWrite = false;
|
||||||
|
innerShell.material.thickness = 4; // Thinner inner layer
|
||||||
|
innerShell.material.transmission = 0.8; // More transparent inner layer
|
||||||
|
innerShell.renderOrder = 1; // Render inner glass before outer
|
||||||
|
|
||||||
|
// Scale inner shell slightly smaller
|
||||||
|
innerShell.scale.multiplyScalar(0.95);
|
||||||
|
object.parent.add(innerShell);
|
||||||
|
|
||||||
|
} else if (nameMatches(object.name, orangeMeshes)) {
|
||||||
|
object.material = lightOrangeMaterial.clone();
|
||||||
|
object.renderOrder = 0; // Render orange objects first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compute bounds for camera framing
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
|
||||||
|
// Set up animations
|
||||||
|
if (gltf.animations && gltf.animations.length > 0) {
|
||||||
|
mixer = new THREE.AnimationMixer(model);
|
||||||
|
gltf.animations.forEach((clip) => {
|
||||||
|
mixer.clipAction(clip).play();
|
||||||
|
});
|
||||||
|
mixer.timeScale = 3.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position camera
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
camera.position.set(center.x, center.y, center.z + maxDim * 2);
|
||||||
|
controls.target.copy(center);
|
||||||
|
controls.update();
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error('Error loading model:', error);
|
||||||
|
});
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
|
function onMouseScroll(event) {
|
||||||
|
// Only count scrolls if the animation is not already running
|
||||||
|
if (!isTwisting) {
|
||||||
|
// You can check event.deltaY to determine scroll direction
|
||||||
|
if (event.deltaY !== 0) {
|
||||||
|
scrollCount++;
|
||||||
|
console.log(`Scroll count: ${scrollCount}`); // For debugging
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollCount >= scrollThreshold) {
|
||||||
|
isTwisting = true;
|
||||||
|
twistProgress = 0;
|
||||||
|
scrollCount = 0; // Reset the counter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function twistMesh(mesh, progress) {
|
||||||
|
if (!mesh || !mesh.geometry || !mesh.geometry.attributes.position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = mesh.geometry.attributes.position;
|
||||||
|
|
||||||
|
// Store original positions on the first run
|
||||||
|
if (!mesh.geometry.userData.originalPositions) {
|
||||||
|
mesh.geometry.userData.originalPositions = new Float32Array(positions.array);
|
||||||
|
|
||||||
|
// Also store bounding box data
|
||||||
|
const box = new THREE.Box3().setFromObject(mesh);
|
||||||
|
mesh.geometry.userData.bounds = {
|
||||||
|
size: box.getSize(new THREE.Vector3()),
|
||||||
|
center: box.getCenter(new THREE.Vector3())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = mesh.geometry.userData.originalPositions;
|
||||||
|
const { size, center } = mesh.geometry.userData.bounds;
|
||||||
|
const totalHeight = size.y; // Use Y-size for the twist axis
|
||||||
|
|
||||||
|
for (let i = 0; i < positions.count; i++) {
|
||||||
|
const x = original[i * 3];
|
||||||
|
const y = original[i * 3 + 1];
|
||||||
|
const z = original[i * 3 + 2];
|
||||||
|
|
||||||
|
// Normalize the y-position from 0 to 1 based on the mesh's height
|
||||||
|
const normalizedY = (y - center.y + totalHeight / 2) / totalHeight;
|
||||||
|
|
||||||
|
// Calculate the twist angle based on normalized y and progress
|
||||||
|
const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI;
|
||||||
|
|
||||||
|
// Apply rotation to the X and Z coordinates
|
||||||
|
positions.setX(i, x * Math.cos(twistAngle) - z * Math.sin(twistAngle));
|
||||||
|
positions.setY(i, y); // Y remains unchanged as it's the axis of rotation
|
||||||
|
positions.setZ(i, x * Math.sin(twistAngle) + z * Math.cos(twistAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.needsUpdate = true;
|
||||||
|
mesh.geometry.computeVertexNormals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the click event listener
|
||||||
|
window.addEventListener('wheel', onMouseScroll, {passive: true});
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
const delta = clock.getDelta();
|
||||||
|
if (mixer) mixer.update(delta);
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
|
||||||
|
// The main loop for the twisting animation
|
||||||
|
if (isTwisting) {
|
||||||
|
twistProgress += twistSpeed;
|
||||||
|
if (twistProgress > 1.0) {
|
||||||
|
twistProgress = 1.0;
|
||||||
|
isTwisting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Traverse the entire scene to find all meshes to twist
|
||||||
|
scene.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
twistMesh(object, twistProgress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
animate();
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
composer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
643
src/main copy.js
Normal file
643
src/main copy.js
Normal file
|
@ -0,0 +1,643 @@
|
||||||
|
import './style.css'
|
||||||
|
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||||
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||||
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||||
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||||
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
|
|
||||||
|
// Scene setup
|
||||||
|
const scene = new THREE.Scene();
|
||||||
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
|
camera.setFocalLength(50);
|
||||||
|
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
|
||||||
|
// Transition state management
|
||||||
|
let currentScene = 0; // 0: innovation, 1: agility, 2: storytelling
|
||||||
|
let isTransitioning = false;
|
||||||
|
let isTwisting = false;
|
||||||
|
let twistProgress = 0;
|
||||||
|
const twistSpeed = 0.02; // Easily adjustable twist speed
|
||||||
|
const twistStrength = 0.3;
|
||||||
|
const fadeSpeed = 1; // Easily adjustable fade speed
|
||||||
|
const transitionDuration = 1; // Easily adjustable transition duration (seconds)
|
||||||
|
let scrollCount = 0;
|
||||||
|
const scrollThreshold = 10; // Changed to 10 as requested
|
||||||
|
let transitionStartTime = 0;
|
||||||
|
|
||||||
|
// Scene objects
|
||||||
|
let currentModel = null;
|
||||||
|
let nextModel = null;
|
||||||
|
let mixer = null;
|
||||||
|
let nextMixer = null;
|
||||||
|
let autoRotationAngle = 0;
|
||||||
|
|
||||||
|
// Renderer setup
|
||||||
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
|
|
||||||
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
renderer.setClearColor(0x000000);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1.2;
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.physicallyCorrectLights = true;
|
||||||
|
|
||||||
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Post-processing: Bloom
|
||||||
|
const composer = new EffectComposer(renderer);
|
||||||
|
const renderPass = new RenderPass(scene, camera);
|
||||||
|
composer.addPass(renderPass);
|
||||||
|
|
||||||
|
const bloomPass = new UnrealBloomPass(
|
||||||
|
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||||
|
1.0, // strength
|
||||||
|
0.45, // radius
|
||||||
|
0.85 // threshold
|
||||||
|
);
|
||||||
|
composer.addPass(bloomPass);
|
||||||
|
|
||||||
|
// Video texture for emissive "screen"-like effect on orange material
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = '/shader-flash.webm';
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.preload = 'auto';
|
||||||
|
|
||||||
|
const videoTexture = new THREE.VideoTexture(video);
|
||||||
|
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
videoTexture.generateMipmaps = false;
|
||||||
|
videoTexture.minFilter = THREE.LinearFilter;
|
||||||
|
videoTexture.magFilter = THREE.LinearFilter;
|
||||||
|
|
||||||
|
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
||||||
|
video.play().catch(() => {});
|
||||||
|
|
||||||
|
// Local procedural environment for better PBR response (no network)
|
||||||
|
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||||
|
const roomEnv = new RoomEnvironment();
|
||||||
|
scene.environment = pmrem.fromScene(roomEnv).texture;
|
||||||
|
pmrem.dispose();
|
||||||
|
roomEnv.dispose();
|
||||||
|
scene.environment = null; // This will make the renderer's clear color visible again
|
||||||
|
|
||||||
|
// Consistent Lighting Setup
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
|
||||||
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
||||||
|
hemiLight.position.set(0, 20, 0);
|
||||||
|
scene.add(hemiLight);
|
||||||
|
|
||||||
|
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
|
fillLight.position.set(-12, 6, -8);
|
||||||
|
scene.add(fillLight);
|
||||||
|
|
||||||
|
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
topLight.position.set(5, 15, 5);
|
||||||
|
scene.add(topLight);
|
||||||
|
|
||||||
|
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
bottomLight.position.set(-3, -8, 3);
|
||||||
|
scene.add(bottomLight);
|
||||||
|
|
||||||
|
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
leftLight.position.set(-12, 2, 5);
|
||||||
|
scene.add(leftLight);
|
||||||
|
|
||||||
|
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
rightLight.position.set(12, 2, -5);
|
||||||
|
scene.add(rightLight);
|
||||||
|
|
||||||
|
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
frontLight.position.set(8, 4, 12);
|
||||||
|
scene.add(frontLight);
|
||||||
|
|
||||||
|
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
backLight.position.set(-8, 4, -12);
|
||||||
|
scene.add(backLight);
|
||||||
|
|
||||||
|
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
||||||
|
camera.add(cameraLight);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
// Controls
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
|
||||||
|
// Material definitions
|
||||||
|
// Clear thick glass for innovation
|
||||||
|
const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.2,
|
||||||
|
roughness: 0.05,
|
||||||
|
transmission: 1,
|
||||||
|
ior: 2,
|
||||||
|
thickness: 2,
|
||||||
|
clearcoat: 1.0,
|
||||||
|
clearcoatRoughness: 0.1,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 0.8,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0x000000),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
alphaTest: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slightly frosted glass for agility and storytelling
|
||||||
|
const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.0,
|
||||||
|
roughness: 0.25,
|
||||||
|
transmission: 1.0,
|
||||||
|
ior: 1.5,
|
||||||
|
thickness: 2.0,
|
||||||
|
clearcoat: 0.75,
|
||||||
|
clearcoatRoughness: 0.25,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 1.5,
|
||||||
|
envMapIntensity: 1.25,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0xffffff),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
|
||||||
|
// Orange material with video shader for innovation
|
||||||
|
const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff8600,
|
||||||
|
metalness: 0.05,
|
||||||
|
roughness: 0.4,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
emissive: new THREE.Color(0xffad47),
|
||||||
|
emissiveMap: videoTexture,
|
||||||
|
emissiveIntensity: 2.25
|
||||||
|
});
|
||||||
|
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
|
||||||
|
// Apply materials based on model type
|
||||||
|
function applyMaterials(model, modelType) {
|
||||||
|
console.log(`=== Material Assignment Debug for ${modelType} ===`);
|
||||||
|
let meshCount = 0;
|
||||||
|
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
meshCount++;
|
||||||
|
console.log(`Found mesh: "${object.name}"`);
|
||||||
|
|
||||||
|
const previousMaterial = object.material;
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
|
||||||
|
if (modelType === 'innovation') {
|
||||||
|
// Innovation-specific material logic
|
||||||
|
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
||||||
|
const targetGlassNames = ['Cube.alt90.df'];
|
||||||
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
const nameMatches = (name, targets) => {
|
||||||
|
const clean = sanitize(name);
|
||||||
|
return targets.some((t) => {
|
||||||
|
const ct = sanitize(t);
|
||||||
|
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (nameMatches(object.name, targetGlassNames)) {
|
||||||
|
// Create outer glass shell with innovation-specific material
|
||||||
|
object.material = innovationGlassMaterial.clone();
|
||||||
|
object.material.side = THREE.DoubleSide;
|
||||||
|
object.material.depthWrite = false;
|
||||||
|
object.renderOrder = 2;
|
||||||
|
|
||||||
|
// Create inner glass shell
|
||||||
|
const innerShell = object.clone();
|
||||||
|
innerShell.material = innovationGlassMaterial.clone();
|
||||||
|
innerShell.material.side = THREE.DoubleSide;
|
||||||
|
innerShell.material.depthWrite = false;
|
||||||
|
innerShell.material.thickness = 4;
|
||||||
|
innerShell.material.transmission = 0.8;
|
||||||
|
innerShell.renderOrder = 1;
|
||||||
|
innerShell.scale.multiplyScalar(0.95);
|
||||||
|
object.parent.add(innerShell);
|
||||||
|
|
||||||
|
} else if (nameMatches(object.name, orangeMeshes)) {
|
||||||
|
object.material = lightOrangeMaterial.clone();
|
||||||
|
object.renderOrder = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Agility and Storytelling use frosted glass material for all meshes
|
||||||
|
if (object.name.startsWith('base')) {
|
||||||
|
console.log(` → Applying frosted glass material to "${object.name}"`);
|
||||||
|
object.material = frostedGlassMaterial.clone();
|
||||||
|
} else {
|
||||||
|
console.log(` → Applying frosted glass material (fallback) to "${object.name}"`);
|
||||||
|
object.material = frostedGlassMaterial.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object.material.needsUpdate = true;
|
||||||
|
|
||||||
|
// Cleanup previous materials
|
||||||
|
if (Array.isArray(previousMaterial)) {
|
||||||
|
previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose());
|
||||||
|
} else if (previousMaterial && previousMaterial.dispose) {
|
||||||
|
previousMaterial.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Total meshes processed: ${meshCount}`);
|
||||||
|
console.log(`=== End Material Assignment Debug for ${modelType} ===`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center and frame model with camera
|
||||||
|
function centerAndFrameModel(model, targetCamera = camera) {
|
||||||
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
|
model.position.sub(center);
|
||||||
|
model.updateMatrixWorld(true);
|
||||||
|
|
||||||
|
const size = box.getSize(new THREE.Vector3());
|
||||||
|
const maxDim = Math.max(size.x, size.y, size.z);
|
||||||
|
|
||||||
|
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||||
|
if (!isTransitioning) {
|
||||||
|
targetCamera.position.set(0, 0, maxDim * 2);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
controls.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup animations based on model type
|
||||||
|
function setupAnimations(model, gltf, modelType) {
|
||||||
|
if (gltf.animations && gltf.animations.length > 0) {
|
||||||
|
const animMixer = new THREE.AnimationMixer(model);
|
||||||
|
|
||||||
|
gltf.animations.forEach((clip) => {
|
||||||
|
const action = animMixer.clipAction(clip);
|
||||||
|
|
||||||
|
if (modelType === 'innovation') {
|
||||||
|
// PingPong loop for innovation
|
||||||
|
action.loop = THREE.LoopPingPong;
|
||||||
|
action.play();
|
||||||
|
} else if (modelType === 'agility') {
|
||||||
|
// Regular loop for agility
|
||||||
|
action.loop = THREE.LoopRepeat;
|
||||||
|
action.play();
|
||||||
|
} else if (modelType === 'storytelling') {
|
||||||
|
// Play once for storytelling
|
||||||
|
action.loop = THREE.LoopOnce;
|
||||||
|
action.clampWhenFinished = true;
|
||||||
|
action.play();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (modelType === 'innovation') {
|
||||||
|
animMixer.timeScale = 3.0; // Keep existing timeScale for innovation
|
||||||
|
}
|
||||||
|
|
||||||
|
return animMixer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load model function
|
||||||
|
function loadModel(filename, modelType, onLoadCallback) {
|
||||||
|
loader.load(`/${filename}`, (gltf) => {
|
||||||
|
const model = gltf.scene;
|
||||||
|
|
||||||
|
// Apply materials
|
||||||
|
applyMaterials(model, modelType);
|
||||||
|
|
||||||
|
// Setup animations
|
||||||
|
const animMixer = setupAnimations(model, gltf, modelType);
|
||||||
|
|
||||||
|
// Center and frame model
|
||||||
|
centerAndFrameModel(model);
|
||||||
|
|
||||||
|
if (onLoadCallback) {
|
||||||
|
onLoadCallback(model, animMixer);
|
||||||
|
}
|
||||||
|
}, undefined, (error) => {
|
||||||
|
console.error(`Error loading ${filename}:`, error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial innovation model
|
||||||
|
loadModel('innovation.glb', 'innovation', (model, animMixer) => {
|
||||||
|
currentModel = model;
|
||||||
|
mixer = animMixer;
|
||||||
|
scene.add(currentModel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Twist animation function
|
||||||
|
function twistMesh(mesh, progress) {
|
||||||
|
if (!mesh || !mesh.geometry || !mesh.geometry.attributes.position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = mesh.geometry.attributes.position;
|
||||||
|
|
||||||
|
// Store original positions on the first run
|
||||||
|
if (!mesh.geometry.userData.originalPositions) {
|
||||||
|
mesh.geometry.userData.originalPositions = new Float32Array(positions.array);
|
||||||
|
|
||||||
|
// Also store bounding box data
|
||||||
|
const box = new THREE.Box3().setFromObject(mesh);
|
||||||
|
mesh.geometry.userData.bounds = {
|
||||||
|
size: box.getSize(new THREE.Vector3()),
|
||||||
|
center: box.getCenter(new THREE.Vector3())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const original = mesh.geometry.userData.originalPositions;
|
||||||
|
const { size, center } = mesh.geometry.userData.bounds;
|
||||||
|
const totalHeight = size.y; // Use Y-size for the twist axis
|
||||||
|
|
||||||
|
for (let i = 0; i < positions.count; i++) {
|
||||||
|
const x = original[i * 3];
|
||||||
|
const y = original[i * 3 + 1];
|
||||||
|
const z = original[i * 3 + 2];
|
||||||
|
|
||||||
|
// Normalize the y-position from 0 to 1 based on the mesh's height
|
||||||
|
const normalizedY = (y - center.y + totalHeight / 2) / totalHeight;
|
||||||
|
|
||||||
|
// Calculate the twist angle based on normalized y and progress
|
||||||
|
const twistAngle = normalizedY * progress * twistStrength * 2 * Math.PI;
|
||||||
|
|
||||||
|
// Apply rotation to the X and Z coordinates
|
||||||
|
positions.setX(i, x * Math.cos(twistAngle) - z * Math.sin(twistAngle));
|
||||||
|
positions.setY(i, y); // Y remains unchanged as it's the axis of rotation
|
||||||
|
positions.setZ(i, x * Math.sin(twistAngle) + z * Math.cos(twistAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.needsUpdate = true;
|
||||||
|
mesh.geometry.computeVertexNormals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mesh geometry to original state
|
||||||
|
function resetMeshGeometry(mesh) {
|
||||||
|
if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = mesh.geometry.attributes.position;
|
||||||
|
const original = mesh.geometry.userData.originalPositions;
|
||||||
|
|
||||||
|
for (let i = 0; i < positions.count; i++) {
|
||||||
|
positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
positions.needsUpdate = true;
|
||||||
|
mesh.geometry.computeVertexNormals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transition to next scene
|
||||||
|
function startTransition() {
|
||||||
|
if (isTransitioning || currentScene >= 2) return;
|
||||||
|
|
||||||
|
isTransitioning = true;
|
||||||
|
isTwisting = true;
|
||||||
|
twistProgress = 0;
|
||||||
|
transitionStartTime = performance.now();
|
||||||
|
|
||||||
|
// Load next model
|
||||||
|
let nextModelFile = '';
|
||||||
|
let nextModelType = '';
|
||||||
|
|
||||||
|
if (currentScene === 0) {
|
||||||
|
nextModelFile = 'agility.glb';
|
||||||
|
nextModelType = 'agility';
|
||||||
|
} else if (currentScene === 1) {
|
||||||
|
nextModelFile = 'storytelling.glb';
|
||||||
|
nextModelType = 'storytelling';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextModelFile) {
|
||||||
|
loadModel(nextModelFile, nextModelType, (model, animMixer) => {
|
||||||
|
nextModel = model;
|
||||||
|
nextMixer = animMixer;
|
||||||
|
|
||||||
|
// Start next model as invisible and positioned below
|
||||||
|
nextModel.position.y = -10;
|
||||||
|
nextModel.traverse((obj) => {
|
||||||
|
if (obj.material) {
|
||||||
|
if (Array.isArray(obj.material)) {
|
||||||
|
obj.material.forEach(mat => {
|
||||||
|
mat.transparent = true;
|
||||||
|
mat.opacity = 0;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.transparent = true;
|
||||||
|
obj.material.opacity = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.add(nextModel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transition animation
|
||||||
|
function updateTransition(deltaTime) {
|
||||||
|
if (!isTransitioning) return;
|
||||||
|
|
||||||
|
const elapsed = (performance.now() - transitionStartTime) / 1000;
|
||||||
|
const transitionProgress = Math.min(elapsed / transitionDuration, 1);
|
||||||
|
|
||||||
|
// Smooth easing function (ease-in-out)
|
||||||
|
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||||
|
const easedProgress = easeInOut(transitionProgress);
|
||||||
|
|
||||||
|
if (currentModel) {
|
||||||
|
// Move current model up and fade out
|
||||||
|
currentModel.position.y = easedProgress * 10;
|
||||||
|
|
||||||
|
currentModel.traverse((obj) => {
|
||||||
|
if (obj.material) {
|
||||||
|
const targetOpacity = 1 - easedProgress;
|
||||||
|
if (Array.isArray(obj.material)) {
|
||||||
|
obj.material.forEach(mat => {
|
||||||
|
mat.transparent = true;
|
||||||
|
mat.opacity = targetOpacity;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.transparent = true;
|
||||||
|
obj.material.opacity = targetOpacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextModel) {
|
||||||
|
// Move next model to center and fade in
|
||||||
|
nextModel.position.y = -10 + (easedProgress * 10);
|
||||||
|
|
||||||
|
nextModel.traverse((obj) => {
|
||||||
|
if (obj.material) {
|
||||||
|
const targetOpacity = easedProgress;
|
||||||
|
if (Array.isArray(obj.material)) {
|
||||||
|
obj.material.forEach(mat => {
|
||||||
|
mat.transparent = true;
|
||||||
|
mat.opacity = targetOpacity;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.transparent = true;
|
||||||
|
obj.material.opacity = targetOpacity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Complete transition
|
||||||
|
if (transitionProgress >= 1) {
|
||||||
|
// Remove current model
|
||||||
|
if (currentModel) {
|
||||||
|
scene.remove(currentModel);
|
||||||
|
|
||||||
|
// Clean up geometry user data
|
||||||
|
currentModel.traverse((obj) => {
|
||||||
|
if (obj.geometry && obj.geometry.userData.originalPositions) {
|
||||||
|
delete obj.geometry.userData.originalPositions;
|
||||||
|
delete obj.geometry.userData.bounds;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch to next model
|
||||||
|
if (nextModel) {
|
||||||
|
currentModel = nextModel;
|
||||||
|
mixer = nextMixer;
|
||||||
|
|
||||||
|
// Reset position and opacity
|
||||||
|
currentModel.position.y = 0;
|
||||||
|
currentModel.traverse((obj) => {
|
||||||
|
if (obj.material) {
|
||||||
|
if (Array.isArray(obj.material)) {
|
||||||
|
obj.material.forEach(mat => {
|
||||||
|
mat.opacity = 1;
|
||||||
|
if (currentScene === 2) { // Keep transparency for storytelling glass
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
} else {
|
||||||
|
mat.transparent = mat.transmission > 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
obj.material.opacity = 1;
|
||||||
|
if (currentScene === 2) { // Keep transparency for storytelling glass
|
||||||
|
obj.material.transparent = obj.material.transmission > 0;
|
||||||
|
} else {
|
||||||
|
obj.material.transparent = obj.material.transmission > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nextModel = null;
|
||||||
|
nextMixer = null;
|
||||||
|
isTransitioning = false;
|
||||||
|
isTwisting = false;
|
||||||
|
twistProgress = 0;
|
||||||
|
currentScene++;
|
||||||
|
scrollCount = 0;
|
||||||
|
|
||||||
|
console.log(`Transition complete. Current scene: ${currentScene}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll event handler
|
||||||
|
function onMouseScroll(event) {
|
||||||
|
// Only count downward scrolls and if not currently transitioning
|
||||||
|
if (!isTransitioning && event.deltaY > 0) {
|
||||||
|
scrollCount++;
|
||||||
|
console.log(`Scroll count: ${scrollCount}`);
|
||||||
|
|
||||||
|
if (scrollCount >= scrollThreshold) {
|
||||||
|
startTransition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach scroll event listener
|
||||||
|
window.addEventListener('wheel', onMouseScroll, {passive: true});
|
||||||
|
|
||||||
|
// Animation loop
|
||||||
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
|
function animate() {
|
||||||
|
requestAnimationFrame(animate);
|
||||||
|
|
||||||
|
const delta = clock.getDelta();
|
||||||
|
|
||||||
|
// Update mixers
|
||||||
|
if (mixer) mixer.update(delta);
|
||||||
|
if (nextMixer) nextMixer.update(delta);
|
||||||
|
|
||||||
|
// Update transition
|
||||||
|
if (isTransitioning) {
|
||||||
|
updateTransition(delta);
|
||||||
|
|
||||||
|
// Apply twist during transition
|
||||||
|
if (isTwisting && currentModel) {
|
||||||
|
twistProgress += twistSpeed;
|
||||||
|
if (twistProgress > 1.0) {
|
||||||
|
twistProgress = 1.0;
|
||||||
|
|
||||||
|
// Reset geometry after twist completes
|
||||||
|
// currentModel.traverse((object) => {
|
||||||
|
// if (object.isMesh) {
|
||||||
|
// resetMeshGeometry(object);
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
isTwisting = false;
|
||||||
|
} else {
|
||||||
|
// Apply twist to current model
|
||||||
|
currentModel.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
twistMesh(object, twistProgress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turntable rotation for current model
|
||||||
|
if (currentModel && !isTransitioning) {
|
||||||
|
autoRotationAngle += delta * 0.5;
|
||||||
|
currentModel.rotation.y = autoRotationAngle;
|
||||||
|
}
|
||||||
|
|
||||||
|
controls.update();
|
||||||
|
composer.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
animate();
|
||||||
|
|
||||||
|
// Handle window resize
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
|
camera.updateProjectionMatrix();
|
||||||
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
composer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
});
|
919
src/main.js
919
src/main.js
|
@ -1,174 +1,827 @@
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { SceneLoader } from './sceneLoader.js';
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||||
import { createScene, setupLighting, setupControls } from './sceneSetup.js';
|
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
||||||
import { createModelFromPreloaded } from './modelManager.js';
|
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
||||||
import {
|
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
||||||
currentModel,
|
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
||||||
nextModel,
|
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
||||||
mixer,
|
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
||||||
nextMixer,
|
|
||||||
isTransitioning,
|
|
||||||
updateTransition,
|
|
||||||
onMouseScroll,
|
|
||||||
setCurrentModel,
|
|
||||||
setMixer
|
|
||||||
} from './transitionManager.js';
|
|
||||||
import {
|
|
||||||
startBoldRoughnessAnimation,
|
|
||||||
updateBoldRoughnessAnimation,
|
|
||||||
updateInnovationGlassAnimation
|
|
||||||
} from './animationManager.js';
|
|
||||||
|
|
||||||
// Fluid distortion imports
|
// Loading Manager
|
||||||
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
|
class SceneLoader {
|
||||||
import { createFluidSimulation, FluidDistortionShader } from './fluidDistortion.js';
|
constructor() {
|
||||||
|
this.loadingScreen = document.getElementById('loading-screen');
|
||||||
|
this.loadingText = document.getElementById('loading-text');
|
||||||
|
this.loadingProgressBar = document.getElementById('loading-progress-bar');
|
||||||
|
this.loadingPercentage = document.getElementById('loading-percentage');
|
||||||
|
this.modelsToLoad = [
|
||||||
|
{ file: 'bold.glb', type: 'bold' },
|
||||||
|
{ file: 'innovation.glb', type: 'innovation' },
|
||||||
|
{ file: 'agility.glb', type: 'agility' },
|
||||||
|
{ file: 'storytelling.glb', type: 'storytelling' }
|
||||||
|
];
|
||||||
|
this.loadedModels = {};
|
||||||
|
this.loadedCount = 0;
|
||||||
|
this.totalModels = this.modelsToLoad.length;
|
||||||
|
}
|
||||||
|
|
||||||
// Starfield import
|
setLoadingMessage(message) {
|
||||||
import { createStarfield } from './starfield.js';
|
this.loadingText.textContent = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgress(progress) {
|
||||||
|
const percentage = Math.round(progress * 100);
|
||||||
|
this.loadingProgressBar.style.width = `${percentage}%`;
|
||||||
|
this.loadingPercentage.textContent = `${percentage}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
hideLoadingScreen() {
|
||||||
|
this.loadingScreen.classList.add('hidden');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.loadingScreen.style.display = 'none';
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadAllModels() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const loader = new GLTFLoader();
|
||||||
|
const dracoLoader = new DRACOLoader();
|
||||||
|
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
||||||
|
loader.setDRACOLoader(dracoLoader);
|
||||||
|
this.modelsToLoad.forEach((modelInfo, index) => {
|
||||||
|
this.setLoadingMessage(`Loading experience...`);
|
||||||
|
loader.load(`/${modelInfo.file}`,
|
||||||
|
(gltf) => {
|
||||||
|
this.loadedModels[modelInfo.type] = {
|
||||||
|
scene: gltf.scene,
|
||||||
|
animations: gltf.animations,
|
||||||
|
gltf: gltf
|
||||||
|
};
|
||||||
|
this.loadedCount++;
|
||||||
|
const progress = this.loadedCount / this.totalModels;
|
||||||
|
this.updateProgress(progress);
|
||||||
|
if (this.loadedCount === this.totalModels) {
|
||||||
|
this.setLoadingMessage('Initializing Experience...');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.hideLoadingScreen();
|
||||||
|
resolve(this.loadedModels);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(progress) => {
|
||||||
|
const fileProgress = progress.loaded / progress.total;
|
||||||
|
const totalProgress = (this.loadedCount + fileProgress) / this.totalModels;
|
||||||
|
this.updateProgress(totalProgress);
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error(`Error loading ${modelInfo.file}:`, error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize loader
|
// Initialize loader
|
||||||
const sceneLoader = new SceneLoader();
|
const sceneLoader = new SceneLoader();
|
||||||
sceneLoader.setLoadingMessage('Preparing Your Experience...');
|
sceneLoader.setLoadingMessage('Preparing Your Experience...');
|
||||||
|
|
||||||
// Create scene components
|
// Scene setup
|
||||||
const { scene, camera, renderer, composer } = createScene();
|
const scene = new THREE.Scene();
|
||||||
setupLighting(scene, camera);
|
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||||
const controls = setupControls(camera, renderer);
|
camera.setFocalLength(50);
|
||||||
|
const raycaster = new THREE.Raycaster();
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
|
||||||
// Create starfield
|
// Transition state management
|
||||||
const starfield = createStarfield(scene);
|
let currentScene = 0; // 0: bold, 1: innovation, 2: agility, 3: storytelling
|
||||||
|
let isTransitioning = false;
|
||||||
|
const fadeSpeed = 1; // Easily adjustable fade speed
|
||||||
|
const transitionDuration = 1; // Easily adjustable transition duration (seconds)
|
||||||
|
let scrollDownCount = 0;
|
||||||
|
let scrollUpCount = 0;
|
||||||
|
const scrollThreshold = 10; // Changed to 10 as requested
|
||||||
|
let transitionStartTime = 0;
|
||||||
|
let transitionDirection = 1; // 1 for forward, -1 for backward
|
||||||
|
|
||||||
|
// Camera-relative transition vectors
|
||||||
|
let transitionUpVector = new THREE.Vector3();
|
||||||
|
let transitionDownVector = new THREE.Vector3();
|
||||||
|
const transitionDistance = 50; // Increased distance for more dramatic transitions
|
||||||
|
|
||||||
|
// Scene objects
|
||||||
|
let currentModel = null;
|
||||||
|
let nextModel = null;
|
||||||
|
let mixer = null;
|
||||||
|
let nextMixer = null;
|
||||||
|
let autoRotationAngle = 0;
|
||||||
|
|
||||||
// Turntable animation settings
|
// Turntable animation settings
|
||||||
const turntableSpeed = 0.5;
|
const turntableSpeed = 0.5; // Rotation speed (radians per second)
|
||||||
|
|
||||||
// Store preloaded models
|
// Store preloaded models
|
||||||
let preloadedModels = {};
|
let preloadedModels = {};
|
||||||
|
|
||||||
// Enhanced fluid simulation + distortion pass
|
// Bold scene roughness animation state
|
||||||
const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2);
|
let boldRoughnessAnimation = {
|
||||||
const fluid = createFluidSimulation(renderer, dpr);
|
isActive: false,
|
||||||
|
startTime: 0,
|
||||||
const distortionPass = new ShaderPass(FluidDistortionShader);
|
delayDuration: 1.0, // 1 second delay (will be dynamic)
|
||||||
distortionPass.material.uniforms.tSim.value = fluid.getTexture();
|
transitionDuration: 1.0, // 1 second transition
|
||||||
distortionPass.material.uniforms.iResolution.value.set(window.innerWidth * dpr, window.innerHeight * dpr);
|
startRoughness: 0.5,
|
||||||
distortionPass.material.uniforms.amount.value = 0.005; // Stronger distortion
|
endRoughness: 0.05,
|
||||||
distortionPass.material.uniforms.chromaticAmount.value = 0.002; // Enhanced chromatic aberration
|
materials: [] // Store references to bold materials
|
||||||
|
|
||||||
// Enhanced lighting parameters
|
|
||||||
distortionPass.material.uniforms.lightIntensity.value = 0;
|
|
||||||
distortionPass.material.uniforms.lightColor.value.set(1, 1, 1);
|
|
||||||
distortionPass.material.uniforms.normalStrength.value = 2.0;
|
|
||||||
distortionPass.material.uniforms.ambientLight.value = 1;
|
|
||||||
|
|
||||||
// New ripple whiteness parameters
|
|
||||||
distortionPass.material.uniforms.rippleWhiteness.value = 0.025; // Amount of white tint
|
|
||||||
distortionPass.material.uniforms.rippleBrightness.value = 1; // Brightness boost for ripples
|
|
||||||
|
|
||||||
composer.addPass(distortionPass);
|
|
||||||
|
|
||||||
// Enhanced pointer tracking
|
|
||||||
const pointer = {
|
|
||||||
x: -1,
|
|
||||||
y: -1,
|
|
||||||
strength: 0.0,
|
|
||||||
prevX: -1,
|
|
||||||
prevY: -1,
|
|
||||||
trail: [], // Store trail positions for enhanced effect
|
|
||||||
maxTrailLength: 5
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mouse coordinates for starfield
|
// Innovation glass animation state
|
||||||
const mouse = new THREE.Vector2();
|
let innovationGlassAnimation = {
|
||||||
|
isActive: false,
|
||||||
|
startTime: 0,
|
||||||
|
transitionDuration: 0.2,
|
||||||
|
startIor: 1.0,
|
||||||
|
endIor: 2.0,
|
||||||
|
startThickness: 1.0,
|
||||||
|
endThickness: 2.0,
|
||||||
|
materials: [] // Store references to innovation glass materials
|
||||||
|
};
|
||||||
|
|
||||||
function toSimPixels(e) {
|
// Renderer setup
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||||
const x = (e.clientX - rect.left) * dpr;
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
||||||
const y = (rect.height - (e.clientY - rect.top)) * dpr;
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
return { x, y };
|
renderer.setClearColor(0x000000);
|
||||||
|
renderer.shadowMap.enabled = true;
|
||||||
|
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||||
|
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||||
|
renderer.toneMappingExposure = 1.2;
|
||||||
|
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||||
|
renderer.physicallyCorrectLights = true;
|
||||||
|
document.body.appendChild(renderer.domElement);
|
||||||
|
|
||||||
|
// Post-processing: Bloom
|
||||||
|
const composer = new EffectComposer(renderer);
|
||||||
|
const renderPass = new RenderPass(scene, camera);
|
||||||
|
composer.addPass(renderPass);
|
||||||
|
const bloomPass = new UnrealBloomPass(
|
||||||
|
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
||||||
|
1.0, // strength
|
||||||
|
0.45, // radius
|
||||||
|
0.85 // threshold
|
||||||
|
);
|
||||||
|
composer.addPass(bloomPass);
|
||||||
|
|
||||||
|
// Video texture for emissive "screen"-like effect on orange material
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = '/shader-flash.webm';
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
video.autoplay = true;
|
||||||
|
video.preload = 'auto';
|
||||||
|
const videoTexture = new THREE.VideoTexture(video);
|
||||||
|
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
||||||
|
videoTexture.generateMipmaps = false;
|
||||||
|
videoTexture.minFilter = THREE.LinearFilter;
|
||||||
|
videoTexture.magFilter = THREE.LinearFilter;
|
||||||
|
|
||||||
|
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
||||||
|
video.play().catch(() => { });
|
||||||
|
|
||||||
|
// Local procedural environment for better PBR response (no network)
|
||||||
|
const pmrem = new THREE.PMREMGenerator(renderer);
|
||||||
|
const roomEnv = new RoomEnvironment();
|
||||||
|
scene.environment = pmrem.fromScene(roomEnv).texture;
|
||||||
|
pmrem.dispose();
|
||||||
|
roomEnv.dispose();
|
||||||
|
scene.environment = null; // This will make the renderer's clear color visible again
|
||||||
|
|
||||||
|
// Consistent Lighting Setup
|
||||||
|
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
||||||
|
scene.add(ambientLight);
|
||||||
|
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
||||||
|
hemiLight.position.set(0, 20, 0);
|
||||||
|
scene.add(hemiLight);
|
||||||
|
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
||||||
|
fillLight.position.set(-12, 6, -8);
|
||||||
|
scene.add(fillLight);
|
||||||
|
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
||||||
|
topLight.position.set(5, 15, 5);
|
||||||
|
scene.add(topLight);
|
||||||
|
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
bottomLight.position.set(-3, -8, 3);
|
||||||
|
scene.add(bottomLight);
|
||||||
|
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
leftLight.position.set(-12, 2, 5);
|
||||||
|
scene.add(leftLight);
|
||||||
|
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
||||||
|
rightLight.position.set(12, 2, -5);
|
||||||
|
scene.add(rightLight);
|
||||||
|
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
frontLight.position.set(8, 4, 12);
|
||||||
|
scene.add(frontLight);
|
||||||
|
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||||
|
backLight.position.set(-8, 4, -12);
|
||||||
|
scene.add(backLight);
|
||||||
|
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
||||||
|
camera.add(cameraLight);
|
||||||
|
scene.add(camera);
|
||||||
|
|
||||||
|
// Controls with zoom disabled and camera constraints
|
||||||
|
const controls = new OrbitControls(camera, renderer.domElement);
|
||||||
|
controls.enableDamping = true;
|
||||||
|
controls.dampingFactor = 0.25;
|
||||||
|
controls.enableZoom = false; // Disable zoom
|
||||||
|
|
||||||
|
// Add camera constraints to prevent extreme angles
|
||||||
|
controls.maxPolarAngle = Math.PI * 0.8; // Prevent looking too far up
|
||||||
|
controls.minPolarAngle = Math.PI * 0.2; // Prevent looking too far down
|
||||||
|
console.log('Orbit controls initialized with camera constraints');
|
||||||
|
|
||||||
|
// Material definitions
|
||||||
|
|
||||||
|
// Bold glass material (starts rough, will transition to clear)
|
||||||
|
const boldGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.2,
|
||||||
|
roughness: 0.5, // Start with rough glass
|
||||||
|
transmission: 1,
|
||||||
|
ior: 2,
|
||||||
|
thickness: 2,
|
||||||
|
clearcoat: 1.0,
|
||||||
|
clearcoatRoughness: 0.1,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 0.8,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0xffffff),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
alphaTest: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Orange wireframe material for bold Cubewire mesh
|
||||||
|
const boldWireframeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff8600,
|
||||||
|
metalness: 0.05,
|
||||||
|
roughness: 0.5
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear thick glass for innovation (starts with animated values)
|
||||||
|
const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.2,
|
||||||
|
roughness: 0.05,
|
||||||
|
transmission: 1,
|
||||||
|
ior: 1.0, // Will animate from 1 to 2
|
||||||
|
thickness: 1.0, // Will animate from 1 to 2
|
||||||
|
clearcoat: 1.0,
|
||||||
|
clearcoatRoughness: 0.1,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 0.8,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0x000000),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
alphaTest: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slightly frosted glass for agility and storytelling
|
||||||
|
const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({
|
||||||
|
color: 0xffffff,
|
||||||
|
metalness: 0.0,
|
||||||
|
roughness: 0.25,
|
||||||
|
transmission: 1.0,
|
||||||
|
ior: 1.5,
|
||||||
|
thickness: 2.0,
|
||||||
|
clearcoat: 0.75,
|
||||||
|
clearcoatRoughness: 0.25,
|
||||||
|
attenuationColor: new THREE.Color(0xffffff),
|
||||||
|
attenuationDistance: 1.5,
|
||||||
|
envMapIntensity: 1.25,
|
||||||
|
specularIntensity: 1.0,
|
||||||
|
specularColor: new THREE.Color(0xffffff),
|
||||||
|
transparent: true,
|
||||||
|
depthWrite: false,
|
||||||
|
side: THREE.DoubleSide
|
||||||
|
});
|
||||||
|
|
||||||
|
// Orange material with video shader for innovation
|
||||||
|
const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
||||||
|
color: 0xff8600,
|
||||||
|
metalness: 0.05,
|
||||||
|
roughness: 0.4,
|
||||||
|
envMapIntensity: 0,
|
||||||
|
emissive: new THREE.Color(0xffad47),
|
||||||
|
emissiveMap: videoTexture,
|
||||||
|
emissiveIntensity: 2.25
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate camera-relative transition vectors for diagonal movement
|
||||||
|
function calculateTransitionVectors() {
|
||||||
|
// Get camera's world direction
|
||||||
|
const cameraDirection = new THREE.Vector3();
|
||||||
|
camera.getWorldDirection(cameraDirection);
|
||||||
|
// Get world up vector
|
||||||
|
const worldUp = new THREE.Vector3(0, 1, 0);
|
||||||
|
// Calculate camera's left vector - BACK TO ORIGINAL (this gave correct left direction)
|
||||||
|
const cameraLeft = new THREE.Vector3();
|
||||||
|
cameraLeft.crossVectors(worldUp, cameraDirection).normalize();
|
||||||
|
// Calculate camera's local up vector
|
||||||
|
const cameraUp = new THREE.Vector3();
|
||||||
|
cameraUp.crossVectors(cameraLeft, cameraDirection).normalize();
|
||||||
|
// Blend camera up with world up - BUT NEGATE to flip up/down direction
|
||||||
|
const blendedUp = new THREE.Vector3();
|
||||||
|
blendedUp.addVectors(
|
||||||
|
cameraUp.clone().multiplyScalar(0.5),
|
||||||
|
worldUp.clone().multiplyScalar(0.5)
|
||||||
|
).normalize().negate(); // ADD .negate() here to flip up to down
|
||||||
|
// Create diagonal vector (up-left)
|
||||||
|
const diagonalUpLeft = new THREE.Vector3();
|
||||||
|
diagonalUpLeft.addVectors(
|
||||||
|
blendedUp.clone().multiplyScalar(0.5),
|
||||||
|
cameraLeft.clone().multiplyScalar(0.5)
|
||||||
|
).normalize();
|
||||||
|
// Set transition vectors
|
||||||
|
transitionUpVector = diagonalUpLeft.clone().multiplyScalar(transitionDistance);
|
||||||
|
transitionDownVector = diagonalUpLeft.clone().multiplyScalar(-transitionDistance);
|
||||||
|
console.log('Diagonal transition vectors calculated with distance:', transitionDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.domElement.addEventListener('pointermove', (e) => {
|
// Apply materials based on model type
|
||||||
const { x, y } = toSimPixels(e);
|
function applyMaterials(model, modelType) {
|
||||||
const dx = (pointer.prevX < 0) ? 0 : Math.abs(x - pointer.prevX);
|
console.log(`=== Material Assignment Debug for ${modelType} ===`);
|
||||||
const dy = (pointer.prevY < 0) ? 0 : Math.abs(y - pointer.prevY);
|
let meshCount = 0;
|
||||||
const speed = Math.min(Math.sqrt(dx * dx + dy * dy) / (6.0 * dpr), 1.0); // More sensitive
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
meshCount++;
|
||||||
|
console.log(`Found mesh: "${object.name}"`);
|
||||||
|
const previousMaterial = object.material;
|
||||||
|
object.castShadow = true;
|
||||||
|
object.receiveShadow = true;
|
||||||
|
if (modelType === 'bold') {
|
||||||
|
// Bold-specific material logic
|
||||||
|
if (object.name === 'Cube') {
|
||||||
|
console.log(` → Applying bold glass material to "${object.name}"`);
|
||||||
|
object.material = boldGlassMaterial.clone();
|
||||||
|
object.material.side = THREE.DoubleSide;
|
||||||
|
object.material.depthWrite = false;
|
||||||
|
object.renderOrder = 2;
|
||||||
|
// Store material reference for roughness animation
|
||||||
|
boldRoughnessAnimation.materials.push(object.material);
|
||||||
|
} else if (object.name === 'Cubewire') {
|
||||||
|
console.log(` → Applying wireframe material to "${object.name}"`);
|
||||||
|
object.material = boldWireframeMaterial.clone();
|
||||||
|
object.renderOrder = 1;
|
||||||
|
} else {
|
||||||
|
console.log(` → Applying bold glass material (fallback) to "${object.name}"`);
|
||||||
|
object.material = boldGlassMaterial.clone();
|
||||||
|
// Store material reference for roughness animation
|
||||||
|
boldRoughnessAnimation.materials.push(object.material);
|
||||||
|
}
|
||||||
|
} else if (modelType === 'innovation') {
|
||||||
|
// Innovation-specific material logic
|
||||||
|
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
||||||
|
const targetGlassNames = ['Cube.alt90.df'];
|
||||||
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||||
|
const nameMatches = (name, targets) => {
|
||||||
|
const clean = sanitize(name);
|
||||||
|
return targets.some((t) => {
|
||||||
|
const ct = sanitize(t);
|
||||||
|
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (nameMatches(object.name, targetGlassNames)) {
|
||||||
|
// Create outer glass shell with innovation-specific material
|
||||||
|
object.material = innovationGlassMaterial.clone();
|
||||||
|
object.material.side = THREE.DoubleSide;
|
||||||
|
object.material.depthWrite = false;
|
||||||
|
object.renderOrder = 2;
|
||||||
|
// Store material reference for animation
|
||||||
|
innovationGlassAnimation.materials.push(object.material);
|
||||||
|
// Create inner glass shell
|
||||||
|
const innerShell = object.clone();
|
||||||
|
innerShell.material = innovationGlassMaterial.clone();
|
||||||
|
innerShell.material.side = THREE.DoubleSide;
|
||||||
|
innerShell.material.depthWrite = false;
|
||||||
|
innerShell.material.transmission = 0.8;
|
||||||
|
innerShell.renderOrder = 1;
|
||||||
|
innerShell.scale.multiplyScalar(0.95);
|
||||||
|
// Store inner shell material reference for animation too
|
||||||
|
innovationGlassAnimation.materials.push(innerShell.material);
|
||||||
|
object.parent.add(innerShell);
|
||||||
|
} else if (nameMatches(object.name, orangeMeshes)) {
|
||||||
|
object.material = lightOrangeMaterial.clone();
|
||||||
|
object.renderOrder = 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Agility and Storytelling use frosted glass material for all meshes
|
||||||
|
if (object.name.startsWith('base')) {
|
||||||
|
console.log(` → Applying frosted glass material to "${object.name}"`);
|
||||||
|
object.material = frostedGlassMaterial.clone();
|
||||||
|
} else {
|
||||||
|
console.log(` → Applying frosted glass material (fallback) to "${object.name}"`);
|
||||||
|
object.material = frostedGlassMaterial.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
object.material.needsUpdate = true;
|
||||||
|
// Cleanup previous materials
|
||||||
|
if (Array.isArray(previousMaterial)) {
|
||||||
|
previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose());
|
||||||
|
} else if (previousMaterial && previousMaterial.dispose) {
|
||||||
|
previousMaterial.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
console.log(`Total meshes processed: ${meshCount}`);
|
||||||
|
console.log(`=== End Material Assignment Debug for ${modelType} ===`);
|
||||||
|
}
|
||||||
|
|
||||||
pointer.x = x;
|
// Center and frame model with camera
|
||||||
pointer.y = y;
|
function centerAndFrameModel(model, targetCamera = camera) {
|
||||||
pointer.strength = speed * 1.2; // Enhanced strength
|
const box = new THREE.Box3().setFromObject(model);
|
||||||
pointer.prevX = x;
|
const center = box.getCenter(new THREE.Vector3());
|
||||||
pointer.prevY = y;
|
model.position.sub(center);
|
||||||
|
model.updateMatrixWorld(true);
|
||||||
|
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
||||||
|
// Use fixed camera distance that's further away from the origin
|
||||||
|
if (!isTransitioning) {
|
||||||
|
const fixedCameraDistance = 50; // Fixed distance, much further than before
|
||||||
|
// Calculate isometric-like position with 35-degree angles
|
||||||
|
const angle = 35 * Math.PI / 180; // Convert 35 degrees to radians
|
||||||
|
const cosAngle = Math.cos(angle);
|
||||||
|
const x = fixedCameraDistance * cosAngle;
|
||||||
|
const y = fixedCameraDistance * cosAngle;
|
||||||
|
const z = fixedCameraDistance * cosAngle;
|
||||||
|
targetCamera.position.set(x, y, z);
|
||||||
|
controls.target.set(0, 0, 0);
|
||||||
|
// Set distance limits to lock the camera at this distance
|
||||||
|
controls.minDistance = fixedCameraDistance;
|
||||||
|
controls.maxDistance = fixedCameraDistance;
|
||||||
|
controls.update();
|
||||||
|
console.log(`Camera positioned at: x=${x}, y=${y}, z=${z}, distance=${fixedCameraDistance}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update light position to follow cursor
|
// Setup animations based on model type
|
||||||
const rect = renderer.domElement.getBoundingClientRect();
|
function setupAnimations(model, gltf, modelType) {
|
||||||
const normalizedX = (e.clientX - rect.left) / rect.width;
|
if (gltf.animations && gltf.animations.length > 0) {
|
||||||
const normalizedY = 1.0 - (e.clientY - rect.top) / rect.height; // Flip Y
|
const animMixer = new THREE.AnimationMixer(model);
|
||||||
distortionPass.material.uniforms.lightPosition.value.set(normalizedX, normalizedY, 1.0);
|
gltf.animations.forEach((clip) => {
|
||||||
|
const action = animMixer.clipAction(clip);
|
||||||
|
if (modelType === 'bold') {
|
||||||
|
// Play once for bold
|
||||||
|
action.loop = THREE.LoopOnce;
|
||||||
|
action.clampWhenFinished = true;
|
||||||
|
action.play();
|
||||||
|
console.log(`Bold animation started: ${clip.name}`);
|
||||||
|
} else if (modelType === 'innovation') {
|
||||||
|
// PingPong loop for innovation
|
||||||
|
action.loop = THREE.LoopPingPong;
|
||||||
|
action.play();
|
||||||
|
console.log(`Innovation animation started: ${clip.name} (PingPong)`);
|
||||||
|
} else if (modelType === 'agility') {
|
||||||
|
// Regular loop for agility
|
||||||
|
action.loop = THREE.LoopRepeat;
|
||||||
|
action.play();
|
||||||
|
console.log(`Agility animation started: ${clip.name} (Loop)`);
|
||||||
|
} else if (modelType === 'storytelling') {
|
||||||
|
// Play once for storytelling
|
||||||
|
action.loop = THREE.LoopOnce;
|
||||||
|
action.clampWhenFinished = true;
|
||||||
|
action.play();
|
||||||
|
console.log(`Storytelling animation started: ${clip.name}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (modelType === 'innovation') {
|
||||||
|
animMixer.timeScale = 3.0; // Keep existing timeScale for innovation
|
||||||
|
console.log('Innovation animation timeScale set to 3.0');
|
||||||
|
}
|
||||||
|
return animMixer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Update mouse coordinates for starfield
|
// Create model from preloaded data - FIXED: Always create fresh geometry
|
||||||
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
function createModelFromPreloaded(modelType) {
|
||||||
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
const preloadedData = preloadedModels[modelType];
|
||||||
}, { passive: true });
|
if (!preloadedData) {
|
||||||
|
console.error(`Preloaded model not found: ${modelType}`);
|
||||||
|
return { model: null, animMixer: null };
|
||||||
|
}
|
||||||
|
console.log(`Creating model from preloaded data: ${modelType}`);
|
||||||
|
// Clear animation materials arrays when creating new models
|
||||||
|
if (modelType === 'bold') {
|
||||||
|
boldRoughnessAnimation.materials = [];
|
||||||
|
} else if (modelType === 'innovation') {
|
||||||
|
innovationGlassAnimation.materials = [];
|
||||||
|
}
|
||||||
|
// Clone the scene deeply to ensure fresh geometry
|
||||||
|
const model = preloadedData.scene.clone(true);
|
||||||
|
// IMPORTANT: Clone all geometries to ensure they're independent
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh && object.geometry) {
|
||||||
|
object.geometry = object.geometry.clone();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Apply materials
|
||||||
|
applyMaterials(model, modelType);
|
||||||
|
// Setup animations
|
||||||
|
const animMixer = setupAnimations(model, preloadedData.gltf, modelType);
|
||||||
|
// Center and frame model
|
||||||
|
centerAndFrameModel(model);
|
||||||
|
console.log(`Model created successfully: ${modelType}`);
|
||||||
|
return { model, animMixer };
|
||||||
|
}
|
||||||
|
|
||||||
renderer.domElement.addEventListener('pointerleave', () => {
|
// Start/restart bold roughness animation with optional delay control
|
||||||
pointer.x = -1;
|
function startBoldRoughnessAnimation(withDelay = true) {
|
||||||
pointer.y = -1;
|
console.log('Starting/restarting bold roughness animation');
|
||||||
pointer.strength = 0.0;
|
// Reset all bold glass materials to starting roughness value
|
||||||
mouse.x = -999;
|
boldRoughnessAnimation.materials.forEach(material => {
|
||||||
mouse.y = -999;
|
material.roughness = boldRoughnessAnimation.startRoughness;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
});
|
||||||
|
boldRoughnessAnimation.isActive = true;
|
||||||
|
boldRoughnessAnimation.startTime = performance.now();
|
||||||
|
// Set delayDuration based on withDelay parameter
|
||||||
|
boldRoughnessAnimation.delayDuration = withDelay ? 1.0 : 0.0;
|
||||||
|
console.log('Bold roughness animation started with delay:', withDelay);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset light to center when mouse leaves
|
// Initialize first scene after all models are loaded
|
||||||
distortionPass.material.uniforms.lightPosition.value.set(0.5, 0.5, 1.0);
|
|
||||||
}, { passive: true });
|
|
||||||
|
|
||||||
// Initialize first scene
|
|
||||||
function initializeScene() {
|
function initializeScene() {
|
||||||
console.log('Initializing first scene (bold)');
|
console.log('Initializing first scene (bold)');
|
||||||
const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls);
|
const { model, animMixer } = createModelFromPreloaded('bold');
|
||||||
setCurrentModel(model);
|
currentModel = model;
|
||||||
setMixer(animMixer);
|
mixer = animMixer;
|
||||||
scene.add(currentModel);
|
scene.add(currentModel);
|
||||||
|
// Start the roughness animation for bold scene with delay
|
||||||
startBoldRoughnessAnimation(true);
|
startBoldRoughnessAnimation(true);
|
||||||
console.log('Bold scene initialized');
|
console.log('Bold scene initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start innovation glass animation
|
||||||
|
function startInnovationGlassAnimation() {
|
||||||
|
console.log('Starting innovation glass animation');
|
||||||
|
// Reset all innovation glass materials to starting values
|
||||||
|
innovationGlassAnimation.materials.forEach(material => {
|
||||||
|
material.ior = innovationGlassAnimation.startIor;
|
||||||
|
material.thickness = innovationGlassAnimation.startThickness;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
});
|
||||||
|
innovationGlassAnimation.isActive = true;
|
||||||
|
innovationGlassAnimation.startTime = performance.now();
|
||||||
|
console.log('Innovation glass animation started');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset mesh geometry to original state
|
||||||
|
function resetMeshGeometry(mesh) {
|
||||||
|
if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const positions = mesh.geometry.attributes.position;
|
||||||
|
const original = mesh.geometry.userData.originalPositions;
|
||||||
|
for (let i = 0; i < positions.count; i++) {
|
||||||
|
positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]);
|
||||||
|
}
|
||||||
|
positions.needsUpdate = true;
|
||||||
|
mesh.geometry.computeVertexNormals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXED: Clean up geometry data completely
|
||||||
|
function cleanupGeometryData(model) {
|
||||||
|
if (!model) return;
|
||||||
|
model.traverse((object) => {
|
||||||
|
if (object.isMesh && object.geometry && object.geometry.userData) {
|
||||||
|
delete object.geometry.userData.originalPositions;
|
||||||
|
delete object.geometry.userData.originalWorldPositions;
|
||||||
|
delete object.geometry.userData.inverseWorldMatrix;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start transition to next or previous scene
|
||||||
|
function startTransition(direction = 1) {
|
||||||
|
if (isTransitioning) return;
|
||||||
|
// Check bounds - now 4 scenes (0-3)
|
||||||
|
if (direction > 0 && currentScene >= 3) return; // Can't go forward from storytelling
|
||||||
|
if (direction < 0 && currentScene <= 0) return; // Can't go backward from bold
|
||||||
|
console.log(`Starting diagonal transition: direction=${direction}, currentScene=${currentScene}`);
|
||||||
|
// Calculate camera-relative diagonal transition vectors
|
||||||
|
calculateTransitionVectors();
|
||||||
|
isTransitioning = true;
|
||||||
|
transitionStartTime = performance.now();
|
||||||
|
transitionDirection = direction;
|
||||||
|
// Determine next model based on direction and current scene
|
||||||
|
let nextModelType = '';
|
||||||
|
if (direction > 0) {
|
||||||
|
// Moving forward
|
||||||
|
if (currentScene === 0) {
|
||||||
|
nextModelType = 'innovation';
|
||||||
|
} else if (currentScene === 1) {
|
||||||
|
nextModelType = 'agility';
|
||||||
|
} else if (currentScene === 2) {
|
||||||
|
nextModelType = 'storytelling';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Moving backward
|
||||||
|
if (currentScene === 1) {
|
||||||
|
nextModelType = 'bold';
|
||||||
|
} else if (currentScene === 2) {
|
||||||
|
nextModelType = 'innovation';
|
||||||
|
} else if (currentScene === 3) {
|
||||||
|
nextModelType = 'agility';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log(`Next model type: ${nextModelType}`);
|
||||||
|
if (nextModelType) {
|
||||||
|
const { model, animMixer } = createModelFromPreloaded(nextModelType);
|
||||||
|
nextModel = model;
|
||||||
|
nextMixer = animMixer;
|
||||||
|
// Position next model based on transition direction
|
||||||
|
if (transitionDirection === 1) {
|
||||||
|
// Forward: next model starts from diagonal down position (bottom-right)
|
||||||
|
nextModel.position.copy(transitionDownVector);
|
||||||
|
console.log(`Next model positioned at diagonal down vector (bottom-right): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||||
|
} else {
|
||||||
|
// Backward: next model starts from diagonal up position (top-left)
|
||||||
|
nextModel.position.copy(transitionUpVector);
|
||||||
|
console.log(`Next model positioned at diagonal up vector (top-left): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
||||||
|
}
|
||||||
|
// Add next model to scene without opacity changes - it will appear instantly when it enters the camera view
|
||||||
|
scene.add(nextModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update transition animation
|
||||||
|
function updateTransition(deltaTime) {
|
||||||
|
if (!isTransitioning) return;
|
||||||
|
const elapsed = (performance.now() - transitionStartTime) / 1000;
|
||||||
|
const transitionProgress = Math.min(elapsed / transitionDuration, 1);
|
||||||
|
// Smooth easing function (ease-in-out)
|
||||||
|
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||||
|
const easedProgress = easeInOut(transitionProgress);
|
||||||
|
if (currentModel) {
|
||||||
|
// Move current model along diagonal vector based on transition direction
|
||||||
|
let moveVector;
|
||||||
|
if (transitionDirection === 1) {
|
||||||
|
// Forward: current model moves top-left
|
||||||
|
moveVector = transitionUpVector.clone().multiplyScalar(easedProgress);
|
||||||
|
console.log('Current model moving top-left (forward transition)');
|
||||||
|
} else {
|
||||||
|
// Backward: current model moves bottom-right
|
||||||
|
moveVector = transitionDownVector.clone().multiplyScalar(easedProgress);
|
||||||
|
console.log('Current model moving bottom-right (backward transition)');
|
||||||
|
}
|
||||||
|
currentModel.position.copy(moveVector);
|
||||||
|
}
|
||||||
|
if (nextModel) {
|
||||||
|
// Move next model from diagonal vector to center based on transition direction
|
||||||
|
let moveVector;
|
||||||
|
if (transitionDirection === 1) {
|
||||||
|
// Forward: next model moves from bottom-right to center
|
||||||
|
moveVector = transitionDownVector.clone().multiplyScalar(1 - easedProgress);
|
||||||
|
console.log('Next model moving from bottom-right to center (forward transition)');
|
||||||
|
} else {
|
||||||
|
// Backward: next model moves from top-left to center
|
||||||
|
moveVector = transitionUpVector.clone().multiplyScalar(1 - easedProgress);
|
||||||
|
console.log('Next model moving from top-left to center (backward transition)');
|
||||||
|
}
|
||||||
|
nextModel.position.copy(moveVector);
|
||||||
|
}
|
||||||
|
// Complete transition
|
||||||
|
if (transitionProgress >= 1) {
|
||||||
|
console.log('Diagonal transition animation complete');
|
||||||
|
// FIXED: Reset geometry before removing the model
|
||||||
|
if (currentModel) {
|
||||||
|
// Reset all geometry to original state before removal
|
||||||
|
currentModel.traverse((object) => {
|
||||||
|
if (object.isMesh) {
|
||||||
|
resetMeshGeometry(object);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Clean up geometry user data completely
|
||||||
|
cleanupGeometryData(currentModel);
|
||||||
|
scene.remove(currentModel);
|
||||||
|
console.log('Previous model removed from scene');
|
||||||
|
}
|
||||||
|
// Switch to next model
|
||||||
|
if (nextModel) {
|
||||||
|
currentModel = nextModel;
|
||||||
|
mixer = nextMixer;
|
||||||
|
// Reset position to center
|
||||||
|
currentModel.position.set(0, 0, 0);
|
||||||
|
}
|
||||||
|
nextModel = null;
|
||||||
|
nextMixer = null;
|
||||||
|
isTransitioning = false;
|
||||||
|
currentScene += transitionDirection; // Update scene based on direction
|
||||||
|
scrollDownCount = 0;
|
||||||
|
scrollUpCount = 0;
|
||||||
|
// Start animations based on current scene
|
||||||
|
if (currentScene === 0) {
|
||||||
|
// Restart bold roughness animation when returning to bold section WITHOUT delay
|
||||||
|
startBoldRoughnessAnimation(false);
|
||||||
|
} else if (currentScene === 1) {
|
||||||
|
startInnovationGlassAnimation();
|
||||||
|
}
|
||||||
|
console.log(`Diagonal transition complete. Current scene: ${currentScene}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll event handler
|
||||||
|
function onMouseScroll(event) {
|
||||||
|
if (isTransitioning) return;
|
||||||
|
if (event.deltaY > 0) {
|
||||||
|
// Scrolling down - move forward
|
||||||
|
scrollDownCount++;
|
||||||
|
scrollUpCount = 0; // Reset up count
|
||||||
|
console.log(`Scroll down count: ${scrollDownCount}`);
|
||||||
|
if (scrollDownCount >= scrollThreshold) {
|
||||||
|
startTransition(1); // Forward direction
|
||||||
|
}
|
||||||
|
} else if (event.deltaY < 0) {
|
||||||
|
// Scrolling up - move backward
|
||||||
|
scrollUpCount++;
|
||||||
|
scrollDownCount = 0; // Reset down count
|
||||||
|
console.log(`Scroll up count: ${scrollUpCount}`);
|
||||||
|
if (scrollUpCount >= scrollThreshold) {
|
||||||
|
startTransition(-1); // Backward direction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Animation loop
|
// Animation loop
|
||||||
const clock = new THREE.Clock();
|
const clock = new THREE.Clock();
|
||||||
|
|
||||||
function animate() {
|
function animate() {
|
||||||
requestAnimationFrame(animate);
|
requestAnimationFrame(animate);
|
||||||
const delta = clock.getDelta();
|
const delta = clock.getDelta();
|
||||||
|
|
||||||
// Update mixers
|
// Update mixers
|
||||||
if (mixer) mixer.update(delta);
|
if (mixer) mixer.update(delta);
|
||||||
if (nextMixer) nextMixer.update(delta);
|
if (nextMixer) nextMixer.update(delta);
|
||||||
|
|
||||||
// Update transition
|
// Update transition
|
||||||
if (isTransitioning) {
|
if (isTransitioning) {
|
||||||
updateTransition(delta, scene);
|
updateTransition(delta);
|
||||||
}
|
}
|
||||||
|
// Turntable rotation animation
|
||||||
// Turntable rotation
|
|
||||||
if (currentModel) {
|
if (currentModel) {
|
||||||
currentModel.rotation.y += turntableSpeed * delta;
|
currentModel.rotation.y += turntableSpeed * delta;
|
||||||
}
|
}
|
||||||
if (nextModel) {
|
if (nextModel) {
|
||||||
nextModel.rotation.y += turntableSpeed * delta;
|
nextModel.rotation.y += turntableSpeed * delta;
|
||||||
}
|
}
|
||||||
|
// Update bold roughness animation
|
||||||
// Update material animations
|
if (boldRoughnessAnimation.isActive) {
|
||||||
updateBoldRoughnessAnimation();
|
const elapsed = (performance.now() - boldRoughnessAnimation.startTime) / 1000;
|
||||||
updateInnovationGlassAnimation();
|
if (elapsed >= boldRoughnessAnimation.delayDuration) {
|
||||||
|
// Delay period is over, start roughness transition
|
||||||
// Animate stars with cursor interaction
|
const transitionElapsed = elapsed - boldRoughnessAnimation.delayDuration;
|
||||||
starfield.animateStars(camera, mouse, delta);
|
const transitionProgress = Math.min(transitionElapsed / boldRoughnessAnimation.transitionDuration, 1);
|
||||||
|
// Smooth easing function (ease-in-out)
|
||||||
// Update enhanced fluid sim
|
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||||
const nowSec = performance.now() / 1000;
|
const easedProgress = easeInOut(transitionProgress);
|
||||||
fluid.update(pointer.x, pointer.y, pointer.strength, nowSec);
|
// Interpolate roughness from 0.5 to 0.05
|
||||||
distortionPass.material.uniforms.tSim.value = fluid.getTexture();
|
const currentRoughness = boldRoughnessAnimation.startRoughness +
|
||||||
|
(boldRoughnessAnimation.endRoughness - boldRoughnessAnimation.startRoughness) * easedProgress;
|
||||||
|
// Apply to all bold materials
|
||||||
|
boldRoughnessAnimation.materials.forEach(material => {
|
||||||
|
material.roughness = currentRoughness;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
});
|
||||||
|
// End animation when complete
|
||||||
|
if (transitionProgress >= 1) {
|
||||||
|
boldRoughnessAnimation.isActive = false;
|
||||||
|
console.log('Bold roughness animation completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Update innovation glass animation
|
||||||
|
if (innovationGlassAnimation.isActive) {
|
||||||
|
const elapsed = (performance.now() - innovationGlassAnimation.startTime) / 1000;
|
||||||
|
const transitionProgress = Math.min(elapsed / innovationGlassAnimation.transitionDuration, 1);
|
||||||
|
// Smooth easing function (ease-in-out)
|
||||||
|
const easeInOut = (t) => t * t * (3 - 2 * t);
|
||||||
|
const easedProgress = easeInOut(transitionProgress);
|
||||||
|
// Interpolate IOR from 1.0 to 2.0
|
||||||
|
const currentIor = innovationGlassAnimation.startIor +
|
||||||
|
(innovationGlassAnimation.endIor - innovationGlassAnimation.startIor) * easedProgress;
|
||||||
|
// Interpolate thickness from 1.0 to 2.0
|
||||||
|
const currentThickness = innovationGlassAnimation.startThickness +
|
||||||
|
(innovationGlassAnimation.endThickness - innovationGlassAnimation.startThickness) * easedProgress;
|
||||||
|
// Apply to all innovation glass materials
|
||||||
|
innovationGlassAnimation.materials.forEach(material => {
|
||||||
|
material.ior = currentIor;
|
||||||
|
material.thickness = currentThickness;
|
||||||
|
material.needsUpdate = true;
|
||||||
|
});
|
||||||
|
// End animation when complete
|
||||||
|
if (transitionProgress >= 1) {
|
||||||
|
innovationGlassAnimation.isActive = false;
|
||||||
|
console.log('Innovation glass animation completed');
|
||||||
|
}
|
||||||
|
}
|
||||||
controls.update();
|
controls.update();
|
||||||
composer.render();
|
composer.render();
|
||||||
}
|
}
|
||||||
|
@ -177,18 +830,17 @@ function animate() {
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
console.log('Starting application initialization');
|
console.log('Starting application initialization');
|
||||||
|
// Load all models first
|
||||||
preloadedModels = await sceneLoader.loadAllModels();
|
preloadedModels = await sceneLoader.loadAllModels();
|
||||||
console.log('All models loaded successfully');
|
console.log('All models loaded successfully');
|
||||||
|
// Initialize the first scene
|
||||||
initializeScene();
|
initializeScene();
|
||||||
|
// Start the animation loop
|
||||||
animate();
|
animate();
|
||||||
console.log('Animation loop started');
|
console.log('Animation loop started');
|
||||||
|
// Attach scroll event listener
|
||||||
window.addEventListener('wheel', (event) => {
|
window.addEventListener('wheel', onMouseScroll, { passive: true });
|
||||||
onMouseScroll(event, preloadedModels, scene, camera, controls);
|
|
||||||
}, { passive: true });
|
|
||||||
console.log('Scroll event listener attached');
|
console.log('Scroll event listener attached');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize scene:', error);
|
console.error('Failed to initialize scene:', error);
|
||||||
sceneLoader.setLoadingMessage('Error loading experience. Please refresh.');
|
sceneLoader.setLoadingMessage('Error loading experience. Please refresh.');
|
||||||
|
@ -198,17 +850,10 @@ async function init() {
|
||||||
// Handle window resize
|
// Handle window resize
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
console.log('Window resized');
|
console.log('Window resized');
|
||||||
const w = window.innerWidth;
|
camera.aspect = window.innerWidth / window.innerHeight;
|
||||||
const h = window.innerHeight;
|
|
||||||
|
|
||||||
camera.aspect = w / h;
|
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
renderer.setSize(w, h);
|
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||||
composer.setSize(w, h);
|
composer.setSize(window.innerWidth, window.innerHeight);
|
||||||
|
|
||||||
const pixelRatio = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2);
|
|
||||||
distortionPass.material.uniforms.iResolution.value.set(w * pixelRatio, h * pixelRatio);
|
|
||||||
fluid.resize(w, h, pixelRatio);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the application
|
// Start the application
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
// Video texture for emissive "screen"-like effect on orange material
|
|
||||||
export const video = document.createElement('video');
|
|
||||||
video.src = '/shader-flash.webm';
|
|
||||||
video.muted = true;
|
|
||||||
video.loop = true;
|
|
||||||
video.playsInline = true;
|
|
||||||
video.autoplay = true;
|
|
||||||
video.preload = 'auto';
|
|
||||||
|
|
||||||
export const videoTexture = new THREE.VideoTexture(video);
|
|
||||||
videoTexture.colorSpace = THREE.SRGBColorSpace;
|
|
||||||
videoTexture.generateMipmaps = false;
|
|
||||||
videoTexture.minFilter = THREE.LinearFilter;
|
|
||||||
videoTexture.magFilter = THREE.LinearFilter;
|
|
||||||
|
|
||||||
// Ensure autoplay starts (muted autoplay is commonly allowed)
|
|
||||||
video.play().catch(() => { });
|
|
||||||
|
|
||||||
// Bold glass material (starts rough, will transition to clear)
|
|
||||||
export const boldGlassMaterial = new THREE.MeshPhysicalMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
metalness: 0.2,
|
|
||||||
roughness: 0.5,
|
|
||||||
transmission: 1,
|
|
||||||
ior: 2,
|
|
||||||
thickness: 2,
|
|
||||||
clearcoat: 1.0,
|
|
||||||
clearcoatRoughness: 0.1,
|
|
||||||
attenuationColor: new THREE.Color(0xffffff),
|
|
||||||
attenuationDistance: 0.8,
|
|
||||||
envMapIntensity: 0,
|
|
||||||
specularIntensity: 1.0,
|
|
||||||
specularColor: new THREE.Color(0xffffff),
|
|
||||||
transparent: true,
|
|
||||||
depthWrite: false,
|
|
||||||
alphaTest: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Orange wireframe material for bold Cubewire mesh
|
|
||||||
export const boldWireframeMaterial = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xff8600,
|
|
||||||
metalness: 0.05,
|
|
||||||
roughness: 0.5
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear thick glass for innovation (starts with animated values)
|
|
||||||
export const innovationGlassMaterial = new THREE.MeshPhysicalMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
metalness: 0.2,
|
|
||||||
roughness: 0.05,
|
|
||||||
transmission: 1,
|
|
||||||
ior: 1.0,
|
|
||||||
thickness: 1.0,
|
|
||||||
clearcoat: 1.0,
|
|
||||||
clearcoatRoughness: 0.1,
|
|
||||||
attenuationColor: new THREE.Color(0xffffff),
|
|
||||||
attenuationDistance: 0.8,
|
|
||||||
envMapIntensity: 0,
|
|
||||||
specularIntensity: 1.0,
|
|
||||||
specularColor: new THREE.Color(0x000000),
|
|
||||||
transparent: true,
|
|
||||||
depthWrite: false,
|
|
||||||
alphaTest: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
// Slightly frosted glass for agility and storytelling
|
|
||||||
export const frostedGlassMaterial = new THREE.MeshPhysicalMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
metalness: 0.0,
|
|
||||||
roughness: 0.25,
|
|
||||||
transmission: 1.0,
|
|
||||||
ior: 1.5,
|
|
||||||
thickness: 2.0,
|
|
||||||
clearcoat: 0.75,
|
|
||||||
clearcoatRoughness: 0.25,
|
|
||||||
attenuationColor: new THREE.Color(0xffffff),
|
|
||||||
attenuationDistance: 1.5,
|
|
||||||
envMapIntensity: 1.25,
|
|
||||||
specularIntensity: 1.0,
|
|
||||||
specularColor: new THREE.Color(0xffffff),
|
|
||||||
transparent: true,
|
|
||||||
depthWrite: false,
|
|
||||||
side: THREE.DoubleSide
|
|
||||||
});
|
|
||||||
|
|
||||||
// Orange material with video shader for innovation
|
|
||||||
export const lightOrangeMaterial = new THREE.MeshStandardMaterial({
|
|
||||||
color: 0xff8600,
|
|
||||||
metalness: 0.05,
|
|
||||||
roughness: 0.4,
|
|
||||||
envMapIntensity: 0,
|
|
||||||
emissive: new THREE.Color(0xffad47),
|
|
||||||
emissiveMap: videoTexture,
|
|
||||||
emissiveIntensity: 2.25
|
|
||||||
});
|
|
|
@ -1,229 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import {
|
|
||||||
boldGlassMaterial,
|
|
||||||
boldWireframeMaterial,
|
|
||||||
innovationGlassMaterial,
|
|
||||||
frostedGlassMaterial,
|
|
||||||
lightOrangeMaterial
|
|
||||||
} from './materialDefinitions.js';
|
|
||||||
import { boldRoughnessAnimation, innovationGlassAnimation } from './animationManager.js';
|
|
||||||
|
|
||||||
// Apply materials based on model type
|
|
||||||
export function applyMaterials(model, modelType) {
|
|
||||||
console.log(`=== Material Assignment Debug for ${modelType} ===`);
|
|
||||||
let meshCount = 0;
|
|
||||||
model.traverse((object) => {
|
|
||||||
if (object.isMesh) {
|
|
||||||
meshCount++;
|
|
||||||
console.log(`Found mesh: "${object.name}"`);
|
|
||||||
const previousMaterial = object.material;
|
|
||||||
object.castShadow = true;
|
|
||||||
object.receiveShadow = true;
|
|
||||||
|
|
||||||
if (modelType === 'bold') {
|
|
||||||
// Bold-specific material logic
|
|
||||||
if (object.name === 'Cube') {
|
|
||||||
console.log(` → Applying bold glass material to "${object.name}"`);
|
|
||||||
object.material = boldGlassMaterial.clone();
|
|
||||||
object.material.side = THREE.DoubleSide;
|
|
||||||
object.material.depthWrite = false;
|
|
||||||
object.renderOrder = 2;
|
|
||||||
// Store material reference for roughness animation
|
|
||||||
boldRoughnessAnimation.materials.push(object.material);
|
|
||||||
} else if (object.name === 'Cubewire') {
|
|
||||||
console.log(` → Applying wireframe material to "${object.name}"`);
|
|
||||||
object.material = boldWireframeMaterial.clone();
|
|
||||||
object.renderOrder = 1;
|
|
||||||
} else {
|
|
||||||
console.log(` → Applying bold glass material (fallback) to "${object.name}"`);
|
|
||||||
object.material = boldGlassMaterial.clone();
|
|
||||||
// Store material reference for roughness animation
|
|
||||||
boldRoughnessAnimation.materials.push(object.material);
|
|
||||||
}
|
|
||||||
} else if (modelType === 'innovation') {
|
|
||||||
// Innovation-specific material logic
|
|
||||||
const orangeMeshes = ['dblsc', 'ec', 'gemini', 'infinity', 'star', 'dpd'];
|
|
||||||
const targetGlassNames = ['Cube.alt90.df'];
|
|
||||||
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
||||||
const nameMatches = (name, targets) => {
|
|
||||||
const clean = sanitize(name);
|
|
||||||
return targets.some((t) => {
|
|
||||||
const ct = sanitize(t);
|
|
||||||
return clean === ct || clean.includes(ct) || ct.includes(clean);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (nameMatches(object.name, targetGlassNames)) {
|
|
||||||
// Create outer glass shell with innovation-specific material
|
|
||||||
object.material = innovationGlassMaterial.clone();
|
|
||||||
object.material.side = THREE.DoubleSide;
|
|
||||||
object.material.depthWrite = false;
|
|
||||||
object.renderOrder = 2;
|
|
||||||
// Store material reference for animation
|
|
||||||
innovationGlassAnimation.materials.push(object.material);
|
|
||||||
// Create inner glass shell
|
|
||||||
const innerShell = object.clone();
|
|
||||||
innerShell.material = innovationGlassMaterial.clone();
|
|
||||||
innerShell.material.side = THREE.DoubleSide;
|
|
||||||
innerShell.material.depthWrite = false;
|
|
||||||
innerShell.material.transmission = 0.8;
|
|
||||||
innerShell.renderOrder = 1;
|
|
||||||
innerShell.scale.multiplyScalar(0.95);
|
|
||||||
// Store inner shell material reference for animation too
|
|
||||||
innovationGlassAnimation.materials.push(innerShell.material);
|
|
||||||
object.parent.add(innerShell);
|
|
||||||
} else if (nameMatches(object.name, orangeMeshes)) {
|
|
||||||
object.material = lightOrangeMaterial.clone();
|
|
||||||
object.renderOrder = 0;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Agility and Storytelling use frosted glass material for all meshes
|
|
||||||
if (object.name.startsWith('base')) {
|
|
||||||
console.log(` → Applying frosted glass material to "${object.name}"`);
|
|
||||||
object.material = frostedGlassMaterial.clone();
|
|
||||||
} else {
|
|
||||||
console.log(` → Applying frosted glass material (fallback) to "${object.name}"`);
|
|
||||||
object.material = frostedGlassMaterial.clone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
object.material.needsUpdate = true;
|
|
||||||
// Cleanup previous materials
|
|
||||||
if (Array.isArray(previousMaterial)) {
|
|
||||||
previousMaterial.forEach((mat) => mat && mat.dispose && mat.dispose());
|
|
||||||
} else if (previousMaterial && previousMaterial.dispose) {
|
|
||||||
previousMaterial.dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log(`Total meshes processed: ${meshCount}`);
|
|
||||||
console.log(`=== End Material Assignment Debug for ${modelType} ===`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center and frame model with camera
|
|
||||||
export function centerAndFrameModel(model, targetCamera, controls) {
|
|
||||||
const box = new THREE.Box3().setFromObject(model);
|
|
||||||
const center = box.getCenter(new THREE.Vector3());
|
|
||||||
model.position.sub(center);
|
|
||||||
model.updateMatrixWorld(true);
|
|
||||||
|
|
||||||
// Only set camera position if it's not already positioned (avoid reset during transitions)
|
|
||||||
// Use fixed camera distance that's further away from the origin
|
|
||||||
const fixedCameraDistance = 50; // Fixed distance, much further than before
|
|
||||||
// Calculate isometric-like position with 35-degree angles
|
|
||||||
const angle = 35 * Math.PI / 180; // Convert 35 degrees to radians
|
|
||||||
const cosAngle = Math.cos(angle);
|
|
||||||
const x = fixedCameraDistance * cosAngle;
|
|
||||||
const y = fixedCameraDistance * cosAngle;
|
|
||||||
const z = fixedCameraDistance * cosAngle;
|
|
||||||
|
|
||||||
targetCamera.position.set(x, y, z);
|
|
||||||
controls.target.set(0, 0, 0);
|
|
||||||
// Set distance limits to lock the camera at this distance
|
|
||||||
controls.minDistance = fixedCameraDistance;
|
|
||||||
controls.maxDistance = fixedCameraDistance;
|
|
||||||
controls.update();
|
|
||||||
|
|
||||||
console.log(`Camera positioned at: x=${x}, y=${y}, z=${z}, distance=${fixedCameraDistance}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Setup animations based on model type
|
|
||||||
export function setupAnimations(model, gltf, modelType) {
|
|
||||||
if (gltf.animations && gltf.animations.length > 0) {
|
|
||||||
const animMixer = new THREE.AnimationMixer(model);
|
|
||||||
gltf.animations.forEach((clip) => {
|
|
||||||
const action = animMixer.clipAction(clip);
|
|
||||||
if (modelType === 'bold') {
|
|
||||||
// Play once for bold
|
|
||||||
action.loop = THREE.LoopOnce;
|
|
||||||
action.clampWhenFinished = true;
|
|
||||||
action.play();
|
|
||||||
console.log(`Bold animation started: ${clip.name}`);
|
|
||||||
} else if (modelType === 'innovation') {
|
|
||||||
// PingPong loop for innovation
|
|
||||||
action.loop = THREE.LoopPingPong;
|
|
||||||
action.play();
|
|
||||||
console.log(`Innovation animation started: ${clip.name} (PingPong)`);
|
|
||||||
} else if (modelType === 'agility') {
|
|
||||||
// Regular loop for agility
|
|
||||||
action.loop = THREE.LoopRepeat;
|
|
||||||
action.play();
|
|
||||||
console.log(`Agility animation started: ${clip.name} (Loop)`);
|
|
||||||
} else if (modelType === 'storytelling') {
|
|
||||||
// Play once for storytelling
|
|
||||||
action.loop = THREE.LoopOnce;
|
|
||||||
action.clampWhenFinished = true;
|
|
||||||
action.play();
|
|
||||||
console.log(`Storytelling animation started: ${clip.name}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (modelType === 'innovation') {
|
|
||||||
animMixer.timeScale = 3.0; // Keep existing timeScale for innovation
|
|
||||||
console.log('Innovation animation timeScale set to 3.0');
|
|
||||||
}
|
|
||||||
return animMixer;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset mesh geometry to original state
|
|
||||||
export function resetMeshGeometry(mesh) {
|
|
||||||
if (!mesh || !mesh.geometry || !mesh.geometry.userData.originalPositions) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const positions = mesh.geometry.attributes.position;
|
|
||||||
const original = mesh.geometry.userData.originalPositions;
|
|
||||||
for (let i = 0; i < positions.count; i++) {
|
|
||||||
positions.setXYZ(i, original[i * 3], original[i * 3 + 1], original[i * 3 + 2]);
|
|
||||||
}
|
|
||||||
positions.needsUpdate = true;
|
|
||||||
mesh.geometry.computeVertexNormals();
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXED: Clean up geometry data completely
|
|
||||||
export function cleanupGeometryData(model) {
|
|
||||||
if (!model) return;
|
|
||||||
model.traverse((object) => {
|
|
||||||
if (object.isMesh && object.geometry && object.geometry.userData) {
|
|
||||||
delete object.geometry.userData.originalPositions;
|
|
||||||
delete object.geometry.userData.originalWorldPositions;
|
|
||||||
delete object.geometry.userData.inverseWorldMatrix;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create model from preloaded data - FIXED: Always create fresh geometry
|
|
||||||
export function createModelFromPreloaded(modelType, preloadedModels, camera, controls) {
|
|
||||||
const preloadedData = preloadedModels[modelType];
|
|
||||||
if (!preloadedData) {
|
|
||||||
console.error(`Preloaded model not found: ${modelType}`);
|
|
||||||
return { model: null, animMixer: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Creating model from preloaded data: ${modelType}`);
|
|
||||||
// Clear animation materials arrays when creating new models
|
|
||||||
if (modelType === 'bold') {
|
|
||||||
boldRoughnessAnimation.materials = [];
|
|
||||||
} else if (modelType === 'innovation') {
|
|
||||||
innovationGlassAnimation.materials = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone the scene deeply to ensure fresh geometry
|
|
||||||
const model = preloadedData.scene.clone(true);
|
|
||||||
// IMPORTANT: Clone all geometries to ensure they're independent
|
|
||||||
model.traverse((object) => {
|
|
||||||
if (object.isMesh && object.geometry) {
|
|
||||||
object.geometry = object.geometry.clone();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply materials
|
|
||||||
applyMaterials(model, modelType);
|
|
||||||
// Setup animations
|
|
||||||
const animMixer = setupAnimations(model, preloadedData.gltf, modelType);
|
|
||||||
// Center and frame model
|
|
||||||
centerAndFrameModel(model, camera, controls);
|
|
||||||
|
|
||||||
console.log(`Model created successfully: ${modelType}`);
|
|
||||||
return { model, animMixer };
|
|
||||||
}
|
|
|
@ -1,78 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
||||||
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
||||||
|
|
||||||
export class SceneLoader {
|
|
||||||
constructor() {
|
|
||||||
this.loadingScreen = document.getElementById('loading-screen');
|
|
||||||
this.loadingText = document.getElementById('loading-text');
|
|
||||||
this.loadingProgressBar = document.getElementById('loading-progress-bar');
|
|
||||||
this.loadingPercentage = document.getElementById('loading-percentage');
|
|
||||||
this.modelsToLoad = [
|
|
||||||
{ file: 'bold.glb', type: 'bold' },
|
|
||||||
{ file: 'innovation.glb', type: 'innovation' },
|
|
||||||
{ file: 'agility.glb', type: 'agility' },
|
|
||||||
{ file: 'storytelling.glb', type: 'storytelling' }
|
|
||||||
];
|
|
||||||
this.loadedModels = {};
|
|
||||||
this.loadedCount = 0;
|
|
||||||
this.totalModels = this.modelsToLoad.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingMessage(message) {
|
|
||||||
this.loadingText.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateProgress(progress) {
|
|
||||||
const percentage = Math.round(progress * 100);
|
|
||||||
this.loadingProgressBar.style.width = `${percentage}%`;
|
|
||||||
this.loadingPercentage.textContent = `${percentage}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
hideLoadingScreen() {
|
|
||||||
this.loadingScreen.classList.add('hidden');
|
|
||||||
setTimeout(() => {
|
|
||||||
this.loadingScreen.style.display = 'none';
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadAllModels() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const loader = new GLTFLoader();
|
|
||||||
const dracoLoader = new DRACOLoader();
|
|
||||||
dracoLoader.setDecoderPath('node_modules/three/examples/jsm/libs/draco/');
|
|
||||||
loader.setDRACOLoader(dracoLoader);
|
|
||||||
|
|
||||||
this.modelsToLoad.forEach((modelInfo, index) => {
|
|
||||||
this.setLoadingMessage(`Loading experience...`);
|
|
||||||
loader.load(`/${modelInfo.file}`,
|
|
||||||
(gltf) => {
|
|
||||||
this.loadedModels[modelInfo.type] = {
|
|
||||||
scene: gltf.scene,
|
|
||||||
animations: gltf.animations,
|
|
||||||
gltf: gltf
|
|
||||||
};
|
|
||||||
this.loadedCount++;
|
|
||||||
const progress = this.loadedCount / this.totalModels;
|
|
||||||
this.updateProgress(progress);
|
|
||||||
if (this.loadedCount === this.totalModels) {
|
|
||||||
this.setLoadingMessage('Initializing Experience...');
|
|
||||||
setTimeout(() => {
|
|
||||||
this.hideLoadingScreen();
|
|
||||||
resolve(this.loadedModels);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
(progress) => {
|
|
||||||
const fileProgress = progress.loaded / progress.total;
|
|
||||||
const totalProgress = (this.loadedCount + fileProgress) / this.totalModels;
|
|
||||||
this.updateProgress(totalProgress);
|
|
||||||
},
|
|
||||||
(error) => {
|
|
||||||
console.error(`Error loading ${modelInfo.file}:`, error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,108 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
|
|
||||||
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
|
|
||||||
import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
|
|
||||||
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
|
|
||||||
import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';
|
|
||||||
|
|
||||||
export function createScene() {
|
|
||||||
// Scene setup
|
|
||||||
const scene = new THREE.Scene();
|
|
||||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
|
||||||
camera.setFocalLength(50);
|
|
||||||
const raycaster = new THREE.Raycaster();
|
|
||||||
const mouse = new THREE.Vector2();
|
|
||||||
|
|
||||||
// Renderer setup
|
|
||||||
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
renderer.setClearColor(0x000000);
|
|
||||||
renderer.shadowMap.enabled = true;
|
|
||||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
|
||||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
|
||||||
renderer.toneMappingExposure = 1.2;
|
|
||||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
|
||||||
renderer.physicallyCorrectLights = true;
|
|
||||||
document.body.appendChild(renderer.domElement);
|
|
||||||
|
|
||||||
// Post-processing: Bloom
|
|
||||||
const composer = new EffectComposer(renderer);
|
|
||||||
const renderPass = new RenderPass(scene, camera);
|
|
||||||
composer.addPass(renderPass);
|
|
||||||
|
|
||||||
const bloomPass = new UnrealBloomPass(
|
|
||||||
new THREE.Vector2(window.innerWidth, window.innerHeight),
|
|
||||||
1.0, // strength
|
|
||||||
0.45, // radius
|
|
||||||
0.85 // threshold
|
|
||||||
);
|
|
||||||
composer.addPass(bloomPass);
|
|
||||||
|
|
||||||
// Local procedural environment for better PBR response (no network)
|
|
||||||
const pmrem = new THREE.PMREMGenerator(renderer);
|
|
||||||
const roomEnv = new RoomEnvironment();
|
|
||||||
scene.environment = pmrem.fromScene(roomEnv).texture;
|
|
||||||
pmrem.dispose();
|
|
||||||
roomEnv.dispose();
|
|
||||||
scene.environment = null; // This will make the renderer's clear color visible again
|
|
||||||
|
|
||||||
return { scene, camera, renderer, composer, raycaster, mouse };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupLighting(scene, camera) {
|
|
||||||
// Consistent Lighting Setup
|
|
||||||
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
|
|
||||||
scene.add(ambientLight);
|
|
||||||
|
|
||||||
const hemiLight = new THREE.HemisphereLight(0xffffff, 0x666666, 1.5);
|
|
||||||
hemiLight.position.set(0, 20, 0);
|
|
||||||
scene.add(hemiLight);
|
|
||||||
|
|
||||||
const fillLight = new THREE.DirectionalLight(0xffffff, 1.2);
|
|
||||||
fillLight.position.set(-12, 6, -8);
|
|
||||||
scene.add(fillLight);
|
|
||||||
|
|
||||||
const topLight = new THREE.DirectionalLight(0xffffff, 1.5);
|
|
||||||
topLight.position.set(5, 15, 5);
|
|
||||||
scene.add(topLight);
|
|
||||||
|
|
||||||
const bottomLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
||||||
bottomLight.position.set(-3, -8, 3);
|
|
||||||
scene.add(bottomLight);
|
|
||||||
|
|
||||||
const leftLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
||||||
leftLight.position.set(-12, 2, 5);
|
|
||||||
scene.add(leftLight);
|
|
||||||
|
|
||||||
const rightLight = new THREE.DirectionalLight(0xffffff, 1.0);
|
|
||||||
rightLight.position.set(12, 2, -5);
|
|
||||||
scene.add(rightLight);
|
|
||||||
|
|
||||||
const frontLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
||||||
frontLight.position.set(8, 4, 12);
|
|
||||||
scene.add(frontLight);
|
|
||||||
|
|
||||||
const backLight = new THREE.DirectionalLight(0xffffff, 0.8);
|
|
||||||
backLight.position.set(-8, 4, -12);
|
|
||||||
scene.add(backLight);
|
|
||||||
|
|
||||||
const cameraLight = new THREE.PointLight(0xffffff, 0.8, 0, 2);
|
|
||||||
camera.add(cameraLight);
|
|
||||||
scene.add(camera);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setupControls(camera, renderer) {
|
|
||||||
// Controls with zoom disabled and camera constraints
|
|
||||||
const controls = new OrbitControls(camera, renderer.domElement);
|
|
||||||
controls.enableDamping = true;
|
|
||||||
controls.dampingFactor = 0.25;
|
|
||||||
controls.enableZoom = false; // Disable zoom
|
|
||||||
|
|
||||||
// Add camera constraints to prevent extreme angles
|
|
||||||
controls.maxPolarAngle = Math.PI * 0.8; // Prevent looking too far up
|
|
||||||
controls.minPolarAngle = Math.PI * 0.2; // Prevent looking too far down
|
|
||||||
|
|
||||||
console.log('Orbit controls initialized with camera constraints');
|
|
||||||
return controls;
|
|
||||||
}
|
|
328
src/starfield.js
328
src/starfield.js
|
@ -1,328 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
|
|
||||||
export function createStarfield(scene) {
|
|
||||||
const starCount = 12000;
|
|
||||||
const starDistance = 300;
|
|
||||||
|
|
||||||
// Create geometry for stars
|
|
||||||
const starGeometry = new THREE.BufferGeometry();
|
|
||||||
const starPositions = new Float32Array(starCount * 3);
|
|
||||||
const starSizes = new Float32Array(starCount);
|
|
||||||
|
|
||||||
// Store original positions, current positions, and sizes
|
|
||||||
const originalPositions = new Float32Array(starCount * 3);
|
|
||||||
const currentPositions = new Float32Array(starCount * 3);
|
|
||||||
const originalSizes = new Float32Array(starCount);
|
|
||||||
const currentSizes = new Float32Array(starCount);
|
|
||||||
|
|
||||||
// Generate random positions in a sphere around the scene
|
|
||||||
for (let i = 0; i < starCount; i++) {
|
|
||||||
const i3 = i * 3;
|
|
||||||
|
|
||||||
const radius = Math.random() * starDistance + 50;
|
|
||||||
const theta = Math.random() * Math.PI * 2;
|
|
||||||
const phi = Math.acos(2 * Math.random() - 1);
|
|
||||||
|
|
||||||
const x = radius * Math.sin(phi) * Math.cos(theta);
|
|
||||||
const y = radius * Math.sin(phi) * Math.sin(theta);
|
|
||||||
const z = radius * Math.cos(phi);
|
|
||||||
|
|
||||||
// Store both original and current positions
|
|
||||||
originalPositions[i3] = x;
|
|
||||||
originalPositions[i3 + 1] = y;
|
|
||||||
originalPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
currentPositions[i3] = x;
|
|
||||||
currentPositions[i3 + 1] = y;
|
|
||||||
currentPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
starPositions[i3] = x;
|
|
||||||
starPositions[i3 + 1] = y;
|
|
||||||
starPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
// Store original and current sizes
|
|
||||||
const baseSize = Math.random() * 0.2 + 0.1;
|
|
||||||
originalSizes[i] = baseSize;
|
|
||||||
currentSizes[i] = baseSize;
|
|
||||||
starSizes[i] = baseSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
starGeometry.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
|
|
||||||
starGeometry.setAttribute('size', new THREE.BufferAttribute(starSizes, 1));
|
|
||||||
|
|
||||||
// Star material with size attenuation
|
|
||||||
const starMaterial = new THREE.PointsMaterial({
|
|
||||||
color: 0xffffff,
|
|
||||||
size: 0.3,
|
|
||||||
sizeAttenuation: true,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.8,
|
|
||||||
vertexColors: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const stars = new THREE.Points(starGeometry, starMaterial);
|
|
||||||
scene.add(stars);
|
|
||||||
|
|
||||||
// Distant stars layer
|
|
||||||
const distantStarCount = 4000;
|
|
||||||
const distantStarGeometry = new THREE.BufferGeometry();
|
|
||||||
const distantStarPositions = new Float32Array(distantStarCount * 3);
|
|
||||||
const distantStarSizes = new Float32Array(distantStarCount);
|
|
||||||
const distantOriginalPositions = new Float32Array(distantStarCount * 3);
|
|
||||||
const distantCurrentPositions = new Float32Array(distantStarCount * 3);
|
|
||||||
const distantOriginalSizes = new Float32Array(distantStarCount);
|
|
||||||
const distantCurrentSizes = new Float32Array(distantStarCount);
|
|
||||||
|
|
||||||
for (let i = 0; i < distantStarCount; i++) {
|
|
||||||
const i3 = i * 3;
|
|
||||||
|
|
||||||
const radius = Math.random() * 200 + starDistance;
|
|
||||||
const theta = Math.random() * Math.PI * 2;
|
|
||||||
const phi = Math.acos(2 * Math.random() - 1);
|
|
||||||
|
|
||||||
const x = radius * Math.sin(phi) * Math.cos(theta);
|
|
||||||
const y = radius * Math.sin(phi) * Math.sin(theta);
|
|
||||||
const z = radius * Math.cos(phi);
|
|
||||||
|
|
||||||
distantOriginalPositions[i3] = x;
|
|
||||||
distantOriginalPositions[i3 + 1] = y;
|
|
||||||
distantOriginalPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
distantCurrentPositions[i3] = x;
|
|
||||||
distantCurrentPositions[i3 + 1] = y;
|
|
||||||
distantCurrentPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
distantStarPositions[i3] = x;
|
|
||||||
distantStarPositions[i3 + 1] = y;
|
|
||||||
distantStarPositions[i3 + 2] = z;
|
|
||||||
|
|
||||||
// Store original and current sizes for distant stars
|
|
||||||
const baseSize = Math.random() * 0.1 + 0.05;
|
|
||||||
distantOriginalSizes[i] = baseSize;
|
|
||||||
distantCurrentSizes[i] = baseSize;
|
|
||||||
distantStarSizes[i] = baseSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
distantStarGeometry.setAttribute('position', new THREE.BufferAttribute(distantStarPositions, 3));
|
|
||||||
distantStarGeometry.setAttribute('size', new THREE.BufferAttribute(distantStarSizes, 1));
|
|
||||||
|
|
||||||
const distantStarMaterial = new THREE.PointsMaterial({
|
|
||||||
color: 0xccccff,
|
|
||||||
size: 0.15,
|
|
||||||
sizeAttenuation: true,
|
|
||||||
transparent: true,
|
|
||||||
opacity: 0.4
|
|
||||||
});
|
|
||||||
|
|
||||||
const distantStars = new THREE.Points(distantStarGeometry, distantStarMaterial);
|
|
||||||
scene.add(distantStars);
|
|
||||||
|
|
||||||
// Animation parameters
|
|
||||||
const movementAmplitude = 2;
|
|
||||||
const repulsionRadius = 400;
|
|
||||||
const repulsionStrength = 5;
|
|
||||||
const interpolationSpeed = 5;
|
|
||||||
|
|
||||||
// NEW: Cursor brightness parameters
|
|
||||||
const brightnessRadius = 600; // Radius for size increase effect
|
|
||||||
const maxSizeMultiplier = 400.0; // Maximum size increase (4x original size)
|
|
||||||
const sizeInterpolationSpeed = 100.0; // Speed of size changes
|
|
||||||
|
|
||||||
// Raycaster for mouse position in 3D space
|
|
||||||
const raycaster = new THREE.Raycaster();
|
|
||||||
const mouseWorldPos = new THREE.Vector3();
|
|
||||||
|
|
||||||
function animateStars(camera, mouse, deltaTime) {
|
|
||||||
const time = Date.now() * 0.0003;
|
|
||||||
|
|
||||||
// Get mouse position in world space
|
|
||||||
if (mouse && camera) {
|
|
||||||
raycaster.setFromCamera(mouse, camera);
|
|
||||||
// Project mouse to a plane at distance 0 from camera
|
|
||||||
const distance = 100;
|
|
||||||
mouseWorldPos.copy(raycaster.ray.direction).multiplyScalar(distance).add(raycaster.ray.origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update close stars
|
|
||||||
const positions = starGeometry.attributes.position.array;
|
|
||||||
const sizes = starGeometry.attributes.size.array;
|
|
||||||
|
|
||||||
for (let i = 0; i < starCount; i++) {
|
|
||||||
const i3 = i * 3;
|
|
||||||
|
|
||||||
// Get original position
|
|
||||||
const origX = originalPositions[i3];
|
|
||||||
const origY = originalPositions[i3 + 1];
|
|
||||||
const origZ = originalPositions[i3 + 2];
|
|
||||||
|
|
||||||
// Add gentle oscillating movement
|
|
||||||
const offsetX = Math.sin(time + i * 0.01) * movementAmplitude;
|
|
||||||
const offsetY = Math.cos(time * 0.7 + i * 0.02) * movementAmplitude;
|
|
||||||
const offsetZ = Math.sin(time * 0.5 + i * 0.015) * movementAmplitude;
|
|
||||||
|
|
||||||
let targetX = origX + offsetX;
|
|
||||||
let targetY = origY + offsetY;
|
|
||||||
let targetZ = origZ + offsetZ;
|
|
||||||
|
|
||||||
// Cursor repulsion
|
|
||||||
if (mouse) {
|
|
||||||
const dx = targetX - mouseWorldPos.x;
|
|
||||||
const dy = targetY - mouseWorldPos.y;
|
|
||||||
const dz = targetZ - mouseWorldPos.z;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
|
|
||||||
if (distance < repulsionRadius && distance > 0) {
|
|
||||||
const force = (1 - distance / repulsionRadius) * repulsionStrength;
|
|
||||||
const nx = dx / distance;
|
|
||||||
const ny = dy / distance;
|
|
||||||
const nz = dz / distance;
|
|
||||||
|
|
||||||
targetX += nx * force;
|
|
||||||
targetY += ny * force;
|
|
||||||
targetZ += nz * force;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smooth interpolation to target position
|
|
||||||
const currentX = currentPositions[i3];
|
|
||||||
const currentY = currentPositions[i3 + 1];
|
|
||||||
const currentZ = currentPositions[i3 + 2];
|
|
||||||
|
|
||||||
const lerpFactor = Math.min(interpolationSpeed * deltaTime, 1.0);
|
|
||||||
|
|
||||||
currentPositions[i3] = THREE.MathUtils.lerp(currentX, targetX, lerpFactor);
|
|
||||||
currentPositions[i3 + 1] = THREE.MathUtils.lerp(currentY, targetY, lerpFactor);
|
|
||||||
currentPositions[i3 + 2] = THREE.MathUtils.lerp(currentZ, targetZ, lerpFactor);
|
|
||||||
|
|
||||||
// Update geometry positions
|
|
||||||
positions[i3] = currentPositions[i3];
|
|
||||||
positions[i3 + 1] = currentPositions[i3 + 1];
|
|
||||||
positions[i3 + 2] = currentPositions[i3 + 2];
|
|
||||||
|
|
||||||
// NEW: Calculate size based on cursor proximity
|
|
||||||
let targetSize = originalSizes[i];
|
|
||||||
|
|
||||||
if (mouse) {
|
|
||||||
const finalX = currentPositions[i3];
|
|
||||||
const finalY = currentPositions[i3 + 1];
|
|
||||||
const finalZ = currentPositions[i3 + 2];
|
|
||||||
|
|
||||||
const dx = finalX - mouseWorldPos.x;
|
|
||||||
const dy = finalY - mouseWorldPos.y;
|
|
||||||
const dz = finalZ - mouseWorldPos.z;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
|
|
||||||
if (distance < brightnessRadius) {
|
|
||||||
// Calculate size multiplier based on distance (closer = bigger)
|
|
||||||
const proximityFactor = 1 - (distance / brightnessRadius);
|
|
||||||
const sizeMultiplier = 1 + (proximityFactor * (maxSizeMultiplier - 1));
|
|
||||||
targetSize = originalSizes[i] * sizeMultiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smooth interpolation for size changes
|
|
||||||
const sizeLerpFactor = Math.min(sizeInterpolationSpeed * deltaTime, 1.0);
|
|
||||||
currentSizes[i] = THREE.MathUtils.lerp(currentSizes[i], targetSize, sizeLerpFactor);
|
|
||||||
sizes[i] = currentSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update distant stars (less affected by cursor)
|
|
||||||
const distantPositions = distantStarGeometry.attributes.position.array;
|
|
||||||
const distantSizes = distantStarGeometry.attributes.size.array;
|
|
||||||
|
|
||||||
for (let i = 0; i < distantStarCount; i++) {
|
|
||||||
const i3 = i * 3;
|
|
||||||
|
|
||||||
const origX = distantOriginalPositions[i3];
|
|
||||||
const origY = distantOriginalPositions[i3 + 1];
|
|
||||||
const origZ = distantOriginalPositions[i3 + 2];
|
|
||||||
|
|
||||||
// Gentler movement for distant stars
|
|
||||||
const offsetX = Math.sin(time * 0.5 + i * 0.005) * movementAmplitude * 0.3;
|
|
||||||
const offsetY = Math.cos(time * 0.3 + i * 0.008) * movementAmplitude * 0.3;
|
|
||||||
const offsetZ = Math.sin(time * 0.4 + i * 0.006) * movementAmplitude * 0.3;
|
|
||||||
|
|
||||||
let targetX = origX + offsetX;
|
|
||||||
let targetY = origY + offsetY;
|
|
||||||
let targetZ = origZ + offsetZ;
|
|
||||||
|
|
||||||
// Weaker cursor repulsion for distant stars
|
|
||||||
if (mouse) {
|
|
||||||
const dx = targetX - mouseWorldPos.x;
|
|
||||||
const dy = targetY - mouseWorldPos.y;
|
|
||||||
const dz = targetZ - mouseWorldPos.z;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
|
|
||||||
if (distance < repulsionRadius * 1.5 && distance > 0) {
|
|
||||||
const force = (1 - distance / (repulsionRadius * 1.5)) * repulsionStrength * 0.3;
|
|
||||||
const nx = dx / distance;
|
|
||||||
const ny = dy / distance;
|
|
||||||
const nz = dz / distance;
|
|
||||||
|
|
||||||
targetX += nx * force;
|
|
||||||
targetY += ny * force;
|
|
||||||
targetZ += nz * force;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smooth interpolation for positions
|
|
||||||
const currentX = distantCurrentPositions[i3];
|
|
||||||
const currentY = distantCurrentPositions[i3 + 1];
|
|
||||||
const currentZ = distantCurrentPositions[i3 + 2];
|
|
||||||
|
|
||||||
const lerpFactor = Math.min(interpolationSpeed * deltaTime * 0.7, 1.0);
|
|
||||||
|
|
||||||
distantCurrentPositions[i3] = THREE.MathUtils.lerp(currentX, targetX, lerpFactor);
|
|
||||||
distantCurrentPositions[i3 + 1] = THREE.MathUtils.lerp(currentY, targetY, lerpFactor);
|
|
||||||
distantCurrentPositions[i3 + 2] = THREE.MathUtils.lerp(currentZ, targetZ, lerpFactor);
|
|
||||||
|
|
||||||
distantPositions[i3] = distantCurrentPositions[i3];
|
|
||||||
distantPositions[i3 + 1] = distantCurrentPositions[i3 + 1];
|
|
||||||
distantPositions[i3 + 2] = distantCurrentPositions[i3 + 2];
|
|
||||||
|
|
||||||
// NEW: Size effect for distant stars (weaker)
|
|
||||||
let targetSize = distantOriginalSizes[i];
|
|
||||||
|
|
||||||
if (mouse) {
|
|
||||||
const finalX = distantCurrentPositions[i3];
|
|
||||||
const finalY = distantCurrentPositions[i3 + 1];
|
|
||||||
const finalZ = distantCurrentPositions[i3 + 2];
|
|
||||||
|
|
||||||
const dx = finalX - mouseWorldPos.x;
|
|
||||||
const dy = finalY - mouseWorldPos.y;
|
|
||||||
const dz = finalZ - mouseWorldPos.z;
|
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
||||||
|
|
||||||
if (distance < brightnessRadius * 1.2) {
|
|
||||||
// Weaker effect for distant stars
|
|
||||||
const proximityFactor = 1 - (distance / (brightnessRadius * 1.2));
|
|
||||||
const sizeMultiplier = 1 + (proximityFactor * (maxSizeMultiplier * 0.5 - 1));
|
|
||||||
targetSize = distantOriginalSizes[i] * sizeMultiplier;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Smooth interpolation for distant star sizes
|
|
||||||
const sizeLerpFactor = Math.min(sizeInterpolationSpeed * deltaTime * 0.8, 1.0);
|
|
||||||
distantCurrentSizes[i] = THREE.MathUtils.lerp(distantCurrentSizes[i], targetSize, sizeLerpFactor);
|
|
||||||
distantSizes[i] = distantCurrentSizes[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark geometry for update
|
|
||||||
starGeometry.attributes.position.needsUpdate = true;
|
|
||||||
starGeometry.attributes.size.needsUpdate = true;
|
|
||||||
distantStarGeometry.attributes.position.needsUpdate = true;
|
|
||||||
distantStarGeometry.attributes.size.needsUpdate = true;
|
|
||||||
|
|
||||||
// Subtle twinkling
|
|
||||||
starMaterial.opacity = 0.6 + Math.sin(time * 2) * 0.2;
|
|
||||||
distantStarMaterial.opacity = 0.3 + Math.sin(time * 1.5 + 1) * 0.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
stars,
|
|
||||||
distantStars,
|
|
||||||
animateStars,
|
|
||||||
starMaterial,
|
|
||||||
distantStarMaterial
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,239 +0,0 @@
|
||||||
import * as THREE from 'three';
|
|
||||||
import { createModelFromPreloaded, resetMeshGeometry, cleanupGeometryData } from './modelManager.js';
|
|
||||||
import { startBoldRoughnessAnimation, startInnovationGlassAnimation } from './animationManager.js';
|
|
||||||
|
|
||||||
// Transition state management
|
|
||||||
export let currentScene = 0; // 0: bold, 1: innovation, 2: agility, 3: storytelling
|
|
||||||
export let isTransitioning = false;
|
|
||||||
export const fadeSpeed = 1; // Easily adjustable fade speed
|
|
||||||
export const transitionDuration = 1; // Easily adjustable transition duration (seconds)
|
|
||||||
export let scrollDownCount = 0;
|
|
||||||
export let scrollUpCount = 0;
|
|
||||||
export const scrollThreshold = 10; // Changed to 10 as requested
|
|
||||||
export let transitionStartTime = 0;
|
|
||||||
export let transitionDirection = 1; // 1 for forward, -1 for backward
|
|
||||||
|
|
||||||
// Camera-relative transition vectors
|
|
||||||
export let transitionUpVector = new THREE.Vector3();
|
|
||||||
export let transitionDownVector = new THREE.Vector3();
|
|
||||||
export const transitionDistance = 50; // Increased distance for more dramatic transitions
|
|
||||||
|
|
||||||
// Scene objects
|
|
||||||
export let currentModel = null;
|
|
||||||
export let nextModel = null;
|
|
||||||
export let mixer = null;
|
|
||||||
export let nextMixer = null;
|
|
||||||
export let autoRotationAngle = 0;
|
|
||||||
|
|
||||||
// Setter functions to modify exported variables safely
|
|
||||||
export function setCurrentModel(model) {
|
|
||||||
currentModel = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setMixer(animMixer) {
|
|
||||||
mixer = animMixer;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setNextModel(model) {
|
|
||||||
nextModel = model;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setNextMixer(animMixer) {
|
|
||||||
nextMixer = animMixer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate camera-relative transition vectors for diagonal movement
|
|
||||||
export function calculateTransitionVectors(camera) {
|
|
||||||
// Get camera's world direction
|
|
||||||
const cameraDirection = new THREE.Vector3();
|
|
||||||
camera.getWorldDirection(cameraDirection);
|
|
||||||
// Get world up vector
|
|
||||||
const worldUp = new THREE.Vector3(0, 1, 0);
|
|
||||||
// Calculate camera's left vector - BACK TO ORIGINAL (this gave correct left direction)
|
|
||||||
const cameraLeft = new THREE.Vector3();
|
|
||||||
cameraLeft.crossVectors(worldUp, cameraDirection).normalize();
|
|
||||||
// Calculate camera's local up vector
|
|
||||||
const cameraUp = new THREE.Vector3();
|
|
||||||
cameraUp.crossVectors(cameraLeft, cameraDirection).normalize();
|
|
||||||
// Blend camera up with world up - BUT NEGATE to flip up/down direction
|
|
||||||
const blendedUp = new THREE.Vector3();
|
|
||||||
blendedUp.addVectors(
|
|
||||||
cameraUp.clone().multiplyScalar(0.5),
|
|
||||||
worldUp.clone().multiplyScalar(0.5)
|
|
||||||
).normalize().negate(); // ADD .negate() here to flip up to down
|
|
||||||
// Create diagonal vector (up-left)
|
|
||||||
const diagonalUpLeft = new THREE.Vector3();
|
|
||||||
diagonalUpLeft.addVectors(
|
|
||||||
blendedUp.clone().multiplyScalar(0.5),
|
|
||||||
cameraLeft.clone().multiplyScalar(0.5)
|
|
||||||
).normalize();
|
|
||||||
// Set transition vectors
|
|
||||||
transitionUpVector = diagonalUpLeft.clone().multiplyScalar(transitionDistance);
|
|
||||||
transitionDownVector = diagonalUpLeft.clone().multiplyScalar(-transitionDistance);
|
|
||||||
console.log('Diagonal transition vectors calculated with distance:', transitionDistance);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start transition to next or previous scene
|
|
||||||
export function startTransition(direction = 1, preloadedModels, scene, camera, controls) {
|
|
||||||
if (isTransitioning) return;
|
|
||||||
// Check bounds - now 4 scenes (0-3)
|
|
||||||
if (direction > 0 && currentScene >= 3) return; // Can't go forward from storytelling
|
|
||||||
if (direction < 0 && currentScene <= 0) return; // Can't go backward from bold
|
|
||||||
|
|
||||||
console.log(`Starting diagonal transition: direction=${direction}, currentScene=${currentScene}`);
|
|
||||||
// Calculate camera-relative diagonal transition vectors
|
|
||||||
calculateTransitionVectors(camera);
|
|
||||||
|
|
||||||
isTransitioning = true;
|
|
||||||
transitionStartTime = performance.now();
|
|
||||||
transitionDirection = direction;
|
|
||||||
|
|
||||||
// Determine next model based on direction and current scene
|
|
||||||
let nextModelType = '';
|
|
||||||
if (direction > 0) {
|
|
||||||
// Moving forward
|
|
||||||
if (currentScene === 0) {
|
|
||||||
nextModelType = 'innovation';
|
|
||||||
} else if (currentScene === 1) {
|
|
||||||
nextModelType = 'agility';
|
|
||||||
} else if (currentScene === 2) {
|
|
||||||
nextModelType = 'storytelling';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Moving backward
|
|
||||||
if (currentScene === 1) {
|
|
||||||
nextModelType = 'bold';
|
|
||||||
} else if (currentScene === 2) {
|
|
||||||
nextModelType = 'innovation';
|
|
||||||
} else if (currentScene === 3) {
|
|
||||||
nextModelType = 'agility';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Next model type: ${nextModelType}`);
|
|
||||||
if (nextModelType) {
|
|
||||||
const { model, animMixer } = createModelFromPreloaded(nextModelType, preloadedModels, camera, controls);
|
|
||||||
nextModel = model;
|
|
||||||
nextMixer = animMixer;
|
|
||||||
|
|
||||||
// Position next model based on transition direction
|
|
||||||
if (transitionDirection === 1) {
|
|
||||||
// Forward: next model starts from diagonal down position (bottom-right)
|
|
||||||
nextModel.position.copy(transitionDownVector);
|
|
||||||
console.log(`Next model positioned at diagonal down vector (bottom-right): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
|
||||||
} else {
|
|
||||||
// Backward: next model starts from diagonal up position (top-left)
|
|
||||||
nextModel.position.copy(transitionUpVector);
|
|
||||||
console.log(`Next model positioned at diagonal up vector (top-left): x=${nextModel.position.x}, y=${nextModel.position.y}, z=${nextModel.position.z}`);
|
|
||||||
}
|
|
||||||
// Add next model to scene without opacity changes - it will appear instantly when it enters the camera view
|
|
||||||
scene.add(nextModel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update transition animation
|
|
||||||
export function updateTransition(deltaTime, scene) {
|
|
||||||
if (!isTransitioning) return;
|
|
||||||
|
|
||||||
const elapsed = (performance.now() - transitionStartTime) / 1000;
|
|
||||||
const transitionProgress = Math.min(elapsed / transitionDuration, 1);
|
|
||||||
// Smooth easing function (ease-in-out)
|
|
||||||
const easeInOut = (t) => t * t * (3 - 2 * t);
|
|
||||||
const easedProgress = easeInOut(transitionProgress);
|
|
||||||
|
|
||||||
if (currentModel) {
|
|
||||||
// Move current model along diagonal vector based on transition direction
|
|
||||||
let moveVector;
|
|
||||||
if (transitionDirection === 1) {
|
|
||||||
// Forward: current model moves top-left
|
|
||||||
moveVector = transitionUpVector.clone().multiplyScalar(easedProgress);
|
|
||||||
console.log('Current model moving top-left (forward transition)');
|
|
||||||
} else {
|
|
||||||
// Backward: current model moves bottom-right
|
|
||||||
moveVector = transitionDownVector.clone().multiplyScalar(easedProgress);
|
|
||||||
console.log('Current model moving bottom-right (backward transition)');
|
|
||||||
}
|
|
||||||
currentModel.position.copy(moveVector);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextModel) {
|
|
||||||
// Move next model from diagonal vector to center based on transition direction
|
|
||||||
let moveVector;
|
|
||||||
if (transitionDirection === 1) {
|
|
||||||
// Forward: next model moves from bottom-right to center
|
|
||||||
moveVector = transitionDownVector.clone().multiplyScalar(1 - easedProgress);
|
|
||||||
console.log('Next model moving from bottom-right to center (forward transition)');
|
|
||||||
} else {
|
|
||||||
// Backward: next model moves from top-left to center
|
|
||||||
moveVector = transitionUpVector.clone().multiplyScalar(1 - easedProgress);
|
|
||||||
console.log('Next model moving from top-left to center (backward transition)');
|
|
||||||
}
|
|
||||||
nextModel.position.copy(moveVector);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete transition
|
|
||||||
if (transitionProgress >= 1) {
|
|
||||||
console.log('Diagonal transition animation complete');
|
|
||||||
// FIXED: Reset geometry before removing the model
|
|
||||||
if (currentModel) {
|
|
||||||
// Reset all geometry to original state before removal
|
|
||||||
currentModel.traverse((object) => {
|
|
||||||
if (object.isMesh) {
|
|
||||||
resetMeshGeometry(object);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Clean up geometry user data completely
|
|
||||||
cleanupGeometryData(currentModel);
|
|
||||||
scene.remove(currentModel);
|
|
||||||
console.log('Previous model removed from scene');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to next model
|
|
||||||
if (nextModel) {
|
|
||||||
currentModel = nextModel;
|
|
||||||
mixer = nextMixer;
|
|
||||||
// Reset position to center
|
|
||||||
currentModel.position.set(0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextModel = null;
|
|
||||||
nextMixer = null;
|
|
||||||
isTransitioning = false;
|
|
||||||
currentScene += transitionDirection; // Update scene based on direction
|
|
||||||
scrollDownCount = 0;
|
|
||||||
scrollUpCount = 0;
|
|
||||||
|
|
||||||
// Start animations based on current scene
|
|
||||||
if (currentScene === 0) {
|
|
||||||
// Restart bold roughness animation when returning to bold section WITHOUT delay
|
|
||||||
startBoldRoughnessAnimation(false);
|
|
||||||
} else if (currentScene === 1) {
|
|
||||||
startInnovationGlassAnimation();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Diagonal transition complete. Current scene: ${currentScene}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scroll event handler
|
|
||||||
export function onMouseScroll(event, preloadedModels, scene, camera, controls) {
|
|
||||||
if (isTransitioning) return;
|
|
||||||
|
|
||||||
if (event.deltaY > 0) {
|
|
||||||
// Scrolling down - move forward
|
|
||||||
scrollDownCount++;
|
|
||||||
scrollUpCount = 0; // Reset up count
|
|
||||||
console.log(`Scroll down count: ${scrollDownCount}`);
|
|
||||||
if (scrollDownCount >= scrollThreshold) {
|
|
||||||
startTransition(1, preloadedModels, scene, camera, controls); // Forward direction
|
|
||||||
}
|
|
||||||
} else if (event.deltaY < 0) {
|
|
||||||
// Scrolling up - move backward
|
|
||||||
scrollUpCount++;
|
|
||||||
scrollDownCount = 0; // Reset down count
|
|
||||||
console.log(`Scroll up count: ${scrollUpCount}`);
|
|
||||||
if (scrollUpCount >= scrollThreshold) {
|
|
||||||
startTransition(-1, preloadedModels, scene, camera, controls); // Backward direction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue