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
+ };
+}