From 42e873cda5a56c9ef399aa8795e8fd129941e701 Mon Sep 17 00:00:00 2001 From: Anuj K Date: Tue, 2 Sep 2025 16:42:53 +0530 Subject: [PATCH] fluid effect overlay added --- index1.html | 298 +++++++++++++++++++++++++++++++++++++ src/fluidDistortion.js | 226 ++++++++++++++++++++++++++++ src/main.js | 121 ++++++++++++--- src/starfield.js | 328 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 954 insertions(+), 19 deletions(-) create mode 100644 index1.html create mode 100644 src/fluidDistortion.js create mode 100644 src/starfield.js diff --git a/index1.html b/index1.html new file mode 100644 index 0000000..0d6e82f --- /dev/null +++ b/index1.html @@ -0,0 +1,298 @@ + + + + + Water-like Cursor Ripples — Strong Chromatic Dispersion + + + + + + + + + + + + + diff --git a/src/fluidDistortion.js b/src/fluidDistortion.js new file mode 100644 index 0000000..998f917 --- /dev/null +++ b/src/fluidDistortion.js @@ -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 }; +} diff --git a/src/main.js b/src/main.js index 3a956e0..7d8d509 100644 --- a/src/main.js +++ b/src/main.js @@ -1,26 +1,32 @@ import './style.css' import * as THREE from 'three'; - import { SceneLoader } from './sceneLoader.js'; import { createScene, setupLighting, setupControls } from './sceneSetup.js'; import { createModelFromPreloaded } from './modelManager.js'; -import { - currentModel, - nextModel, - mixer, - nextMixer, +import { + currentModel, + nextModel, + mixer, + nextMixer, isTransitioning, updateTransition, onMouseScroll, setCurrentModel, setMixer } from './transitionManager.js'; -import { +import { startBoldRoughnessAnimation, updateBoldRoughnessAnimation, - updateInnovationGlassAnimation + updateInnovationGlassAnimation } 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 const sceneLoader = new SceneLoader(); sceneLoader.setLoadingMessage('Preparing Your Experience...'); @@ -30,21 +36,83 @@ const { scene, camera, renderer, composer } = createScene(); setupLighting(scene, camera); const controls = setupControls(camera, renderer); +// Create starfield +const starfield = createStarfield(scene); + // Turntable animation settings -const turntableSpeed = 0.5; // Rotation speed (radians per second) +const turntableSpeed = 0.5; // Store preloaded models 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 function initializeScene() { console.log('Initializing first scene (bold)'); const { model, animMixer } = createModelFromPreloaded('bold', preloadedModels, camera, controls); - // Use setter functions instead of direct assignment + setCurrentModel(model); setMixer(animMixer); scene.add(currentModel); - // Start the roughness animation for bold scene with delay + startBoldRoughnessAnimation(true); console.log('Bold scene initialized'); } @@ -72,10 +140,18 @@ function animate() { nextModel.rotation.y += turntableSpeed * delta; } - // Update animations + // Update material animations updateBoldRoughnessAnimation(); 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(); composer.render(); } @@ -84,15 +160,14 @@ function animate() { async function init() { try { console.log('Starting application initialization'); - // Load all models first + preloadedModels = await sceneLoader.loadAllModels(); console.log('All models loaded successfully'); - // Initialize the first scene + initializeScene(); - // Start the animation loop animate(); console.log('Animation loop started'); - // Attach scroll event listener + window.addEventListener('wheel', (event) => { onMouseScroll(event, preloadedModels, scene, camera, controls); }, { passive: true }); @@ -106,10 +181,18 @@ async function init() { // Handle window resize window.addEventListener('resize', () => { console.log('Window resized'); - camera.aspect = window.innerWidth / window.innerHeight; + const w = window.innerWidth; + const h = window.innerHeight; + + camera.aspect = w / h; 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 diff --git a/src/starfield.js b/src/starfield.js new file mode 100644 index 0000000..35924b8 --- /dev/null +++ b/src/starfield.js @@ -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 + }; +}