fluid effect overlay added
This commit is contained in:
parent
1391d2b5cf
commit
42e873cda5
298
index1.html
Normal file
298
index1.html
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
<!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>
|
226
src/fluidDistortion.js
Normal file
226
src/fluidDistortion.js
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
// Lightweight ripple simulation: stores current (R) and previous (G) height,
|
||||||
|
// updates with a damped wave equation + a mouse "splat".
|
||||||
|
const FluidSimShader = {
|
||||||
|
uniforms: {
|
||||||
|
tPrev: { value: null }, // previous state texture (RG)
|
||||||
|
iResolution: { value: new THREE.Vector2() }, // render-target resolution in pixels
|
||||||
|
iTime: { value: 0.0 },
|
||||||
|
mouse: { value: new THREE.Vector3(-1, -1, 0.0) }, // x,y in pixels, z=strength
|
||||||
|
dissipation: { value: 0.996 }, // global damping
|
||||||
|
tension: { value: 0.5 }, // wave speed coefficient
|
||||||
|
radius: { value: 18.0 }, // splat radius in pixels
|
||||||
|
},
|
||||||
|
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; // mouse.xy in pixels, mouse.z = strength
|
||||||
|
uniform float dissipation; // 0..1
|
||||||
|
uniform float tension; // ~0.25..1.0
|
||||||
|
uniform float radius; // pixels
|
||||||
|
|
||||||
|
// Read RG channels: R = current height, G = previous height
|
||||||
|
vec2 readRG(vec2 uv) {
|
||||||
|
vec4 c = texture2D(tPrev, uv);
|
||||||
|
return c.rg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Texel size
|
||||||
|
vec2 texel = 1.0 / iResolution;
|
||||||
|
|
||||||
|
// Current and previous heights at this pixel
|
||||||
|
vec2 currPrev = readRG(vUv);
|
||||||
|
float curr = currPrev.r;
|
||||||
|
float prev = currPrev.g;
|
||||||
|
|
||||||
|
// 4-neighbor laplacian on the "current" height field
|
||||||
|
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;
|
||||||
|
|
||||||
|
float lap = (up + down + left + right - 4.0 * curr);
|
||||||
|
|
||||||
|
// Wave equation with damping: next = curr + (curr - prev) * dissipation + lap * tension
|
||||||
|
float next = curr + (curr - prev) * dissipation + lap * tension;
|
||||||
|
|
||||||
|
// Mouse "splat" - add a Gaussian bump near the pointer when in bounds
|
||||||
|
if (mouse.z > 0.0001) {
|
||||||
|
vec2 uvPx = vUv * iResolution;
|
||||||
|
vec2 d = uvPx - mouse.xy;
|
||||||
|
// Gaussian falloff in pixel space
|
||||||
|
float r = radius;
|
||||||
|
float g = exp(-dot(d, d) / max(1.0, (r * r)));
|
||||||
|
// Scale by strength; sign controls up/down displacement
|
||||||
|
next += g * mouse.z * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pack next and curr into RG for the next step
|
||||||
|
gl_FragColor = vec4(next, curr, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Screen-space distortion shader with chromatic aberration
|
||||||
|
export const FluidDistortionShader = {
|
||||||
|
uniforms: {
|
||||||
|
tDiffuse: { value: null }, // input scene color
|
||||||
|
tSim: { value: null }, // ripple height texture (R = height)
|
||||||
|
iResolution: { value: new THREE.Vector2() }, // pixels
|
||||||
|
amount: { value: 0.065 }, // UV offset scale
|
||||||
|
chromaticAmount: { value: 0.008 } // chromatic aberration strength
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 texel = 1.0 / iResolution;
|
||||||
|
|
||||||
|
// Central differences on the height field to estimate normal/gradient
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Gradient
|
||||||
|
vec2 grad = vec2(hR - hL, hU - hD);
|
||||||
|
|
||||||
|
// Base distortion offset
|
||||||
|
vec2 baseOffset = grad * amount;
|
||||||
|
|
||||||
|
// Chromatic aberration: sample R, G, B at slightly different offsets
|
||||||
|
vec2 chromaticOffset = grad * chromaticAmount;
|
||||||
|
|
||||||
|
// Red channel - offset in gradient direction
|
||||||
|
vec2 uvR = vUv + baseOffset + chromaticOffset;
|
||||||
|
|
||||||
|
// Green channel - no additional chromatic offset (center)
|
||||||
|
vec2 uvG = vUv + baseOffset;
|
||||||
|
|
||||||
|
// Blue channel - offset opposite to gradient direction
|
||||||
|
vec2 uvB = vUv + baseOffset - chromaticOffset;
|
||||||
|
|
||||||
|
// Clamp all UVs to avoid sampling outside
|
||||||
|
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 each channel separately
|
||||||
|
float r = texture2D(tDiffuse, uvR).r;
|
||||||
|
float g = texture2D(tDiffuse, uvG).g;
|
||||||
|
float b = texture2D(tDiffuse, uvB).b;
|
||||||
|
|
||||||
|
gl_FragColor = vec4(r, g, b, 1.0);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Factory to create/update the simulation
|
||||||
|
export function createFluidSimulation(renderer, dpr = 1) {
|
||||||
|
const simScene = new THREE.Scene();
|
||||||
|
const simCamera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
|
||||||
|
|
||||||
|
// Fullscreen quad
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create ping-pong targets
|
||||||
|
const params = {
|
||||||
|
minFilter: THREE.LinearFilter,
|
||||||
|
magFilter: THREE.LinearFilter,
|
||||||
|
format: THREE.RGBAFormat,
|
||||||
|
type: THREE.UnsignedByteType, // portable and sufficient for this use
|
||||||
|
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 empty
|
||||||
|
renderer.setRenderTarget(rtA);
|
||||||
|
renderer.clear();
|
||||||
|
renderer.setRenderTarget(rtB);
|
||||||
|
renderer.clear();
|
||||||
|
renderer.setRenderTarget(null);
|
||||||
|
|
||||||
|
// Init uniforms
|
||||||
|
quad.material.uniforms.iResolution.value.set(width, height);
|
||||||
|
quad.material.uniforms.tPrev.value = rtA.texture;
|
||||||
|
|
||||||
|
// Swap helper
|
||||||
|
function swap() {
|
||||||
|
const tmp = rtA; rtA = rtB; rtB = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// External API
|
||||||
|
function update(mouseX, mouseY, strength, timeSec) {
|
||||||
|
// Update uniforms
|
||||||
|
quad.material.uniforms.iTime.value = timeSec;
|
||||||
|
// Mouse: if offscreen (negative), set strength 0
|
||||||
|
if (mouseX < 0.0 || mouseY < 0.0) {
|
||||||
|
quad.material.uniforms.mouse.value.set(-1, -1, 0.0);
|
||||||
|
} else {
|
||||||
|
quad.material.uniforms.mouse.value.set(mouseX, mouseY, Math.max(0.0, Math.min(1.0, strength)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render step: read rtA into shader, write next state into rtB
|
||||||
|
quad.material.uniforms.tPrev.value = rtA.texture;
|
||||||
|
renderer.setRenderTarget(rtB);
|
||||||
|
renderer.render(simScene, simCamera);
|
||||||
|
renderer.setRenderTarget(null);
|
||||||
|
|
||||||
|
// Next frame reads the freshly written state
|
||||||
|
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 };
|
||||||
|
}
|
121
src/main.js
121
src/main.js
|
@ -1,26 +1,32 @@
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
|
|
||||||
import { SceneLoader } from './sceneLoader.js';
|
import { SceneLoader } from './sceneLoader.js';
|
||||||
import { createScene, setupLighting, setupControls } from './sceneSetup.js';
|
import { createScene, setupLighting, setupControls } from './sceneSetup.js';
|
||||||
import { createModelFromPreloaded } from './modelManager.js';
|
import { createModelFromPreloaded } from './modelManager.js';
|
||||||
import {
|
import {
|
||||||
currentModel,
|
currentModel,
|
||||||
nextModel,
|
nextModel,
|
||||||
mixer,
|
mixer,
|
||||||
nextMixer,
|
nextMixer,
|
||||||
isTransitioning,
|
isTransitioning,
|
||||||
updateTransition,
|
updateTransition,
|
||||||
onMouseScroll,
|
onMouseScroll,
|
||||||
setCurrentModel,
|
setCurrentModel,
|
||||||
setMixer
|
setMixer
|
||||||
} from './transitionManager.js';
|
} from './transitionManager.js';
|
||||||
import {
|
import {
|
||||||
startBoldRoughnessAnimation,
|
startBoldRoughnessAnimation,
|
||||||
updateBoldRoughnessAnimation,
|
updateBoldRoughnessAnimation,
|
||||||
updateInnovationGlassAnimation
|
updateInnovationGlassAnimation
|
||||||
} from './animationManager.js';
|
} from './animationManager.js';
|
||||||
|
|
||||||
|
// Fluid distortion imports
|
||||||
|
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
|
||||||
|
import { createFluidSimulation, FluidDistortionShader } from './fluidDistortion.js';
|
||||||
|
|
||||||
|
// Starfield import
|
||||||
|
import { createStarfield } from './starfield.js';
|
||||||
|
|
||||||
// Initialize loader
|
// Initialize loader
|
||||||
const sceneLoader = new SceneLoader();
|
const sceneLoader = new SceneLoader();
|
||||||
sceneLoader.setLoadingMessage('Preparing Your Experience...');
|
sceneLoader.setLoadingMessage('Preparing Your Experience...');
|
||||||
|
@ -30,21 +36,83 @@ const { scene, camera, renderer, composer } = createScene();
|
||||||
setupLighting(scene, camera);
|
setupLighting(scene, camera);
|
||||||
const controls = setupControls(camera, renderer);
|
const controls = setupControls(camera, renderer);
|
||||||
|
|
||||||
|
// Create starfield
|
||||||
|
const starfield = createStarfield(scene);
|
||||||
|
|
||||||
// Turntable animation settings
|
// Turntable animation settings
|
||||||
const turntableSpeed = 0.5; // Rotation speed (radians per second)
|
const turntableSpeed = 0.5;
|
||||||
|
|
||||||
// Store preloaded models
|
// Store preloaded models
|
||||||
let preloadedModels = {};
|
let preloadedModels = {};
|
||||||
|
|
||||||
|
// Fluid simulation + distortion pass
|
||||||
|
const dpr = renderer.getPixelRatio ? renderer.getPixelRatio() : Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
const fluid = createFluidSimulation(renderer, dpr);
|
||||||
|
|
||||||
|
const distortionPass = new ShaderPass(FluidDistortionShader);
|
||||||
|
distortionPass.material.uniforms.tSim.value = fluid.getTexture();
|
||||||
|
distortionPass.material.uniforms.iResolution.value.set(window.innerWidth * dpr, window.innerHeight * dpr);
|
||||||
|
distortionPass.material.uniforms.amount.value = 0.100;
|
||||||
|
distortionPass.material.uniforms.chromaticAmount.value = 0.050;
|
||||||
|
|
||||||
|
composer.addPass(distortionPass);
|
||||||
|
|
||||||
|
// Pointer tracking for both fluid simulation and starfield
|
||||||
|
const pointer = {
|
||||||
|
x: -1,
|
||||||
|
y: -1,
|
||||||
|
strength: 0.0,
|
||||||
|
prevX: -1,
|
||||||
|
prevY: -1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mouse coordinates for starfield (normalized device coordinates)
|
||||||
|
const mouse = new THREE.Vector2();
|
||||||
|
|
||||||
|
function toSimPixels(e) {
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
const x = (e.clientX - rect.left) * dpr;
|
||||||
|
const y = (rect.height - (e.clientY - rect.top)) * dpr;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('pointermove', (e) => {
|
||||||
|
const { x, y } = toSimPixels(e);
|
||||||
|
const dx = (pointer.prevX < 0) ? 0 : Math.abs(x - pointer.prevX);
|
||||||
|
const dy = (pointer.prevY < 0) ? 0 : Math.abs(y - pointer.prevY);
|
||||||
|
const speed = Math.min(Math.sqrt(dx * dx + dy * dy) / (8.0 * dpr), 1.0);
|
||||||
|
|
||||||
|
pointer.x = x;
|
||||||
|
pointer.y = y;
|
||||||
|
pointer.strength = speed * 0.85;
|
||||||
|
pointer.prevX = x;
|
||||||
|
pointer.prevY = y;
|
||||||
|
|
||||||
|
// Update mouse coordinates for starfield (NDC: -1 to +1)
|
||||||
|
const rect = renderer.domElement.getBoundingClientRect();
|
||||||
|
mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
|
||||||
|
mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
renderer.domElement.addEventListener('pointerleave', () => {
|
||||||
|
pointer.x = -1;
|
||||||
|
pointer.y = -1;
|
||||||
|
pointer.strength = 0.0;
|
||||||
|
|
||||||
|
// Clear mouse for starfield
|
||||||
|
mouse.x = -999; // Move off-screen
|
||||||
|
mouse.y = -999;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
// Initialize first scene after all models are loaded
|
// Initialize first scene after all models are loaded
|
||||||
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', preloadedModels, camera, controls);
|
||||||
// Use setter functions instead of direct assignment
|
|
||||||
setCurrentModel(model);
|
setCurrentModel(model);
|
||||||
setMixer(animMixer);
|
setMixer(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');
|
||||||
}
|
}
|
||||||
|
@ -72,10 +140,18 @@ function animate() {
|
||||||
nextModel.rotation.y += turntableSpeed * delta;
|
nextModel.rotation.y += turntableSpeed * delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update animations
|
// Update material animations
|
||||||
updateBoldRoughnessAnimation();
|
updateBoldRoughnessAnimation();
|
||||||
updateInnovationGlassAnimation();
|
updateInnovationGlassAnimation();
|
||||||
|
|
||||||
|
// Animate stars with cursor interaction
|
||||||
|
starfield.animateStars(camera, mouse, delta);
|
||||||
|
|
||||||
|
// Update fluid sim and refresh distortion pass input
|
||||||
|
const nowSec = performance.now() / 1000;
|
||||||
|
fluid.update(pointer.x, pointer.y, pointer.strength, nowSec);
|
||||||
|
distortionPass.material.uniforms.tSim.value = fluid.getTexture();
|
||||||
|
|
||||||
controls.update();
|
controls.update();
|
||||||
composer.render();
|
composer.render();
|
||||||
}
|
}
|
||||||
|
@ -84,15 +160,14 @@ 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', (event) => {
|
||||||
onMouseScroll(event, preloadedModels, scene, camera, controls);
|
onMouseScroll(event, preloadedModels, scene, camera, controls);
|
||||||
}, { passive: true });
|
}, { passive: true });
|
||||||
|
@ -106,10 +181,18 @@ async function init() {
|
||||||
// Handle window resize
|
// Handle window resize
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
console.log('Window resized');
|
console.log('Window resized');
|
||||||
camera.aspect = window.innerWidth / window.innerHeight;
|
const w = window.innerWidth;
|
||||||
|
const h = window.innerHeight;
|
||||||
|
|
||||||
|
camera.aspect = w / h;
|
||||||
camera.updateProjectionMatrix();
|
camera.updateProjectionMatrix();
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
composer.setSize(window.innerWidth, window.innerHeight);
|
renderer.setSize(w, h);
|
||||||
|
composer.setSize(w, h);
|
||||||
|
|
||||||
|
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
|
||||||
|
|
328
src/starfield.js
Normal file
328
src/starfield.js
Normal file
|
@ -0,0 +1,328 @@
|
||||||
|
import * as THREE from 'three';
|
||||||
|
|
||||||
|
export function createStarfield(scene) {
|
||||||
|
const starCount = 8000;
|
||||||
|
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 = 60; // Radius for size increase effect
|
||||||
|
const maxSizeMultiplier = 4.0; // Maximum size increase (4x original size)
|
||||||
|
const sizeInterpolationSpeed = 3.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
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue