import * as THREE from 'three'; import { updateDoor } from '../scene/door.js'; import { updateVcrDisplay } from '../scene/vcr-display.js'; import { state } from '../state.js'; import { updateScreenEffect } from '../scene/tv-set.js' function updateCamera() { const globalTime = Date.now() * 0.00003; const lookAtTime = Date.now() * 0.0002; const camAmplitude = 0.2; const lookAmplitude = 0.1; // Base Camera Position in front of the TV const baseX = -0.5; const baseY = 1.5; const baseZ = 2.2; // Base LookAt target (Center of the screen) const baseTargetX = -0.7; const baseTargetY = 1.7; const baseTargetZ = -0.3; // Camera Position Offsets (Drift) const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude; const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.4; const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude * 1.4; state.camera.position.x = baseX + camOffsetX; state.camera.position.y = baseY + camOffsetY; state.camera.position.z = baseZ + camOffsetZ; // LookAt Target Offsets (Subtle Gaze Shift) const lookOffsetX = Math.sin(lookAtTime * 1.5) * lookAmplitude * 3; const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude; // Apply lookAt to the subtly shifted target state.camera.lookAt(baseTargetX + lookOffsetX, baseTargetY + lookOffsetY, baseTargetZ); } function updateLampFlicker() { const flickerChance = 0.995; const restoreRate = 0.15; if (Math.random() > flickerChance) { // Flickers quickly to a dimmer random value (between 0.3 and 1.05) let lampLightIntensity = state.originalLampIntensity * (0.3 + Math.random() * 0.7); state.lampLightSpot.intensity = lampLightIntensity; state.lampLightPoint.intensity = lampLightIntensity; } else if (state.lampLightPoint.intensity < state.originalLampIntensity) { // Smoothly restore original intensity let lampLightIntensity = THREE.MathUtils.lerp(state.lampLightPoint.intensity, state.originalLampIntensity, restoreRate); state.lampLightSpot.intensity = lampLightIntensity; state.lampLightPoint.intensity = lampLightIntensity; } } function updateScreenLight() { if (state.isVideoLoaded && state.screenLight.intensity > 0) { const pulseTarget = state.originalScreenIntensity + (Math.random() - 0.5) * state.screenIntensityPulse; state.screenLight.intensity = THREE.MathUtils.lerp(state.screenLight.intensity, pulseTarget, 0.1); const lightTime = Date.now() * 0.0001; const radius = 0.01; const centerX = 0; const centerY = 1.5; state.screenLight.position.x = centerX + Math.cos(lightTime) * radius; state.screenLight.position.y = centerY + Math.sin(lightTime * 1.5) * radius * 0.5; // Slightly different freq for Y } } function updateVideo() { if (state.videoTexture) { state.videoTexture.needsUpdate = true; } } function updateVcr() { const currentTime = state.baseTime + state.videoElement.currentTime; if (Math.abs(currentTime - state.lastUpdateTime) > 0.1) { updateVcrDisplay(currentTime); state.lastUpdateTime = currentTime; } if (currentTime - state.lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds state.blinkState = !state.blinkState; state.lastBlinkToggleTime = currentTime; } } function updateBooks() { const LEVITATE_CHANCE = 0.0003; // Chance for a resting book to start levitating per frame const LEVITATE_DURATION_MIN = 100; // frames const LEVITATE_DURATION_MAX = 300; // frames const LEVITATE_AMPLITUDE = 0.02; // Max vertical displacement const LEVITATE_SPEED_FACTOR = 0.03; // Speed of oscillation const START_RATE = 0.05; // How quickly a book starts to levitate const RETURN_RATE = 0.1; // How quickly a book returns to original position const START_DURATION = 120; // frames for the starting transition const levitation = state.bookLevitation; // Manage the global levitation state if (levitation.state === 'resting') { if (Math.random() < LEVITATE_CHANCE) { levitation.state = 'starting'; levitation.timer = START_DURATION; } } else if (levitation.state === 'starting') { levitation.timer--; if (levitation.timer <= 0) { levitation.state = 'levitating'; levitation.timer = LEVITATE_DURATION_MIN + Math.random() * (LEVITATE_DURATION_MAX - LEVITATE_DURATION_MIN); } } else if (levitation.state === 'levitating') { levitation.timer--; if (levitation.timer <= 0) { levitation.state = 'returning'; } } // Animate books based on the global state let allBooksReturned = true; state.books.forEach(book => { const data = book.userData; if (levitation.state === 'starting') { allBooksReturned = false; book.position.y = THREE.MathUtils.lerp(book.position.y, data.originalY + LEVITATE_AMPLITUDE/2, START_RATE); data.oscillationTime = 0; } else if (levitation.state === 'levitating') { allBooksReturned = false; data.oscillationTime += LEVITATE_SPEED_FACTOR; data.levitateOffset = Math.sin(data.oscillationTime) * LEVITATE_AMPLITUDE; book.position.y = data.originalY + data.levitateOffset + LEVITATE_AMPLITUDE/2; } else if (levitation.state === 'returning') { book.position.y = THREE.MathUtils.lerp(book.position.y, data.originalY, RETURN_RATE); data.levitateOffset = book.position.y - data.originalY; if (Math.abs(data.levitateOffset) > 0.001) { allBooksReturned = false; } } }); if (levitation.state === 'returning' && allBooksReturned) { levitation.state = 'resting'; } } function updatePictureFrame() { state.pictureFrames.forEach((pictureFrame) => { pictureFrame.update(); }); } // --- Animation Loop --- export function animate() { requestAnimationFrame(animate); state.effectsManager.update(); updateCamera(); updateLampFlicker(); updateScreenLight(); updateVideo(); updateVcr(); updateBooks(); updateDoor(); updatePictureFrame(); updateScreenEffect(); // RENDER! state.renderer.render(state.scene, state.camera); } // --- Window Resize Handler --- export function onWindowResize() { state.camera.aspect = window.innerWidth / window.innerHeight; state.camera.updateProjectionMatrix(); state.renderer.setSize(window.innerWidth, window.innerHeight); }