From 75c87c9d03850afb35fb938966ffdc336daefbf8 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Thu, 13 Nov 2025 21:45:11 +0100 Subject: [PATCH] Refactoring: move effects to a manager, chunk animate loop contents --- tv-player/index.html | 1 + tv-player/src/EffectsManager.js | 21 +++ tv-player/src/animate.js | 95 ++++------- tv-player/src/effects_dust.js | 65 ++++--- tv-player/src/effects_flies.js | 271 +++++++++++++----------------- tv-player/src/global-variables.js | 5 +- tv-player/src/init.js | 4 +- tv-player/src/scene.js | 1 - 8 files changed, 215 insertions(+), 248 deletions(-) create mode 100644 tv-player/src/EffectsManager.js diff --git a/tv-player/index.html b/tv-player/index.html index 33a696e..9b7805f 100644 --- a/tv-player/index.html +++ b/tv-player/index.html @@ -51,6 +51,7 @@ + diff --git a/tv-player/src/EffectsManager.js b/tv-player/src/EffectsManager.js new file mode 100644 index 0000000..cd5366a --- /dev/null +++ b/tv-player/src/EffectsManager.js @@ -0,0 +1,21 @@ +class EffectsManager { + constructor(scene) { + this.effects = []; + this._initializeEffects(scene); + } + + _initializeEffects(scene) { + // Add all desired effects here. + // This is now the single place to manage which effects are active. + this.addEffect(new DustEffect(scene)); + this.addEffect(new FliesEffect(scene)); + } + + addEffect(effect) { + this.effects.push(effect); + } + + update() { + this.effects.forEach(effect => effect.update()); + } +} \ No newline at end of file diff --git a/tv-player/src/animate.js b/tv-player/src/animate.js index 5929cf0..8b1434f 100644 --- a/tv-player/src/animate.js +++ b/tv-player/src/animate.js @@ -1,31 +1,15 @@ -// --- Animation Loop --- -function animate() { - requestAnimationFrame(animate); +function updateCamera() { + const globalTime = Date.now() * 0.00005; + const lookAtTime = Date.now() * 0.00003; - // 1. Dust animation: slow downward drift - if (dust) { - const positions = dust.geometry.attributes.position.array; - for (let i = 1; i < positions.length; i += 3) { - positions[i] -= 0.001; - if (positions[i] < -2) { - positions[i] = 8; - } - } - dust.geometry.attributes.position.needsUpdate = true; - } - - // 2. Camera movement (Gentle, random hovering) - const globalTime = Date.now() * 0.00005; - const lookAtTime = Date.now() * 0.00003; - - const camAmplitude = 0.7; - const lookAmplitude = 0.05; + const camAmplitude = 0.7; + const lookAmplitude = 0.05; // Base Camera Position in front of the TV const baseX = -0.5; const baseY = 1.5; const baseZ = 2.5; - + // Base LookAt target (Center of the screen) const baseTargetX = -0.7; const baseTargetY = 1.7; @@ -33,31 +17,28 @@ function animate() { // 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; + const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.4; + const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude * 1.4; camera.position.x = baseX + camOffsetX; camera.position.y = baseY + camOffsetY; - camera.position.z = baseZ + camOffsetZ; + camera.position.z = baseZ + camOffsetZ; // LookAt Target Offsets (Subtle Gaze Shift) const lookOffsetX = Math.sin(lookAtTime * 1.5) * lookAmplitude; const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude; // Apply lookAt to the subtly shifted target - camera.lookAt( - baseTargetX + lookOffsetX, - baseTargetY + lookOffsetY, - baseTargetZ - ); - - // 3. Lamp Flicker Effect - const flickerChance = 0.995; - const restoreRate = 0.15; + 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 = originalLampIntensity * (0.3 + Math.random() * 0.7); + let lampLightIntensity = originalLampIntensity * (0.3 + Math.random() * 0.7); lampLightSpot.intensity = lampLightIntensity; lampLightPoint.intensity = lampLightIntensity; } else if (lampLightPoint.intensity < originalLampIntensity) { @@ -66,55 +47,51 @@ function animate() { lampLightSpot.intensity = lampLightIntensity; lampLightPoint.intensity = lampLightIntensity; } +} - // 4. Screen Light Pulse and Movement Effect (Updated) +function updateScreenLight() { if (isVideoLoaded && screenLight.intensity > 0) { - // A. Pulse Effect (Intensity Fluctuation) - // Generate a small random fluctuation for the pulse (Range: 1.35 to 1.65 around base 1.5) - const pulseTarget = originalScreenIntensity + (Math.random() - 0.5) * screenIntensityPulse; - // Smoothly interpolate towards the new target fluctuation + const pulseTarget = originalScreenIntensity + (Math.random() - 0.5) * screenIntensityPulse; screenLight.intensity = THREE.MathUtils.lerp(screenLight.intensity, pulseTarget, 0.1); - // B. Movement Effect (Subtle circle around the screen center - circling the room area) const lightTime = Date.now() * 0.0001; const radius = 0.01; const centerX = 0; const centerY = 1.5; - //const centerZ = 1.2; // Use the updated Z position of the light source - // Move the light in a subtle, erratic circle screenLight.position.x = centerX + Math.cos(lightTime) * radius; screenLight.position.y = centerY + Math.sin(lightTime * 1.5) * radius * 0.5; // Slightly different freq for Y - //screenLight.position.z = centerZ; // Keep Z constant at the screen light plane } +} - // 5. Update video texture (essential to grab the next frame) +function updateVideo() { if (videoTexture) { videoTexture.needsUpdate = true; - - // Update time display in the animation loop - if (isVideoLoaded && videoElement.readyState >= 3) { - const currentTime = formatTime(videoElement.currentTime); - const duration = formatTime(videoElement.duration); - console.info(`Tape ${currentVideoIndex + 1} of ${videoUrls.length}. Time: ${currentTime} / ${duration}`); - } } +} - updateFlies(); - +function updateVcr() { const currentTime = baseTime + videoElement.currentTime; - - // Simulate playback time - if (Math.abs(currentTime - lastUpdateTime) > 0.1) { + if (Math.abs(currentTime - lastUpdateTime) > 0.1) { updateVcrDisplay(currentTime); lastUpdateTime = currentTime; } - - // Blink the colon every second if (currentTime - lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds blinkState = !blinkState; lastBlinkToggleTime = currentTime; } +} + +// --- Animation Loop --- +function animate() { + requestAnimationFrame(animate); + + effectsManager.update(); + updateCamera(); + updateLampFlicker(); + updateScreenLight(); + updateVideo(); + updateVcr(); // RENDER! renderer.render(scene, camera); diff --git a/tv-player/src/effects_dust.js b/tv-player/src/effects_dust.js index 69d55a6..c298278 100644 --- a/tv-player/src/effects_dust.js +++ b/tv-player/src/effects_dust.js @@ -1,28 +1,45 @@ -// --- Dust Particle System Function --- -function createDust() { - const particleCount = 2000; - const particlesGeometry = new THREE.BufferGeometry(); - const positions = []; - - for (let i = 0; i < particleCount; i++) { - positions.push( - (Math.random() - 0.5) * 15, - Math.random() * 10, - (Math.random() - 0.5) * 15 - ); +class DustEffect { + constructor(scene) { + this.dust = null; + this._create(scene); } - // Use THREE.Float32BufferAttribute to correctly set the position attribute - particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); - const particleMaterial = new THREE.PointsMaterial({ - color: 0xffffff, - size: 0.015, - transparent: true, - opacity: 0.08, - blending: THREE.AdditiveBlending - }); + _create(scene) { + const particleCount = 2000; + const particlesGeometry = new THREE.BufferGeometry(); + const positions = []; - dust = new THREE.Points(particlesGeometry, particleMaterial); - // Dust particles generally don't cast or receive shadows in this context - scene.add(dust); + for (let i = 0; i < particleCount; i++) { + positions.push( + (Math.random() - 0.5) * 15, + Math.random() * 10, + (Math.random() - 0.5) * 15 + ); + } + particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); + + const particleMaterial = new THREE.PointsMaterial({ + color: 0xffffff, + size: 0.015, + transparent: true, + opacity: 0.08, + blending: THREE.AdditiveBlending + }); + + this.dust = new THREE.Points(particlesGeometry, particleMaterial); + scene.add(this.dust); + } + + update() { + if (this.dust) { + const positions = this.dust.geometry.attributes.position.array; + for (let i = 1; i < positions.length; i += 3) { + positions[i] -= 0.001; + if (positions[i] < -2) { + positions[i] = 8; + } + } + this.dust.geometry.attributes.position.needsUpdate = true; + } + } } \ No newline at end of file diff --git a/tv-player/src/effects_flies.js b/tv-player/src/effects_flies.js index a21fdcb..0090889 100644 --- a/tv-player/src/effects_flies.js +++ b/tv-player/src/effects_flies.js @@ -1,174 +1,129 @@ -function randomFlyTarget() { - return new THREE.Vector3( - (Math.random() - 0.5) * (ROOM_SIZE - 1), - FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN), - (Math.random() - 0.5) * (ROOM_SIZE - 1)); -} +const FLIES_COUNT = 2; -/** - * Creates a single fly mesh (small cone/tetrahedron). - * @returns {THREE.Group} - */ -function createFlyMesh() { - const flyGroup = new THREE.Group(); - - const flyMaterial = new THREE.MeshPhongMaterial({ - color: 0x111111, // Dark fly color - shininess: 50, - }); - - // Small Cone/Tetrahedron for a simple shape - const bodyGeometry = new THREE.ConeGeometry(0.01, 0.02, 3); - const body = new THREE.Mesh(bodyGeometry, flyMaterial); - body.rotation.x = degToRad(90); // Point nose in Z direction - - body.castShadow = true; - body.receiveShadow = true; - flyGroup.add(body); - - // Initial state and parameters for the fly - flyGroup.userData = { - state: 'flying', // 'flying' or 'landed' - landTimer: 0, - t: 0, // Curve progression t parameter (0 to 1) - speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01, - curve: null, - landCheckTimer: 0, - oscillationTime: Math.random() * 100, // For smooth y-axis buzzing - }; - - // Initial random position - flyGroup.position = randomFlyTarget(); - - return flyGroup; -} - - -/** - * Creates a new Quadratic Bezier curve for a fly's flight path. - * @param {THREE.Group} fly - The fly mesh group. - * @param {THREE.Vector3} endPoint - The target position for the end of the curve. - */ -function createFlyCurve(fly, endPoint) { - const startPoint = fly.position.clone(); - - // Calculate the midpoint - const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5); - - // Calculate a random offset for the control point to create curvature - const offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5; - const offsetAngle = Math.random() * Math.PI * 2; - - // Displace the control point randomly to create a swooping path. - // Control point y is usually higher than start/end for a nice arc. - const controlPoint = new THREE.Vector3( - midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5, - midPoint.y + Math.random() * 0.5 + 0.5, - midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5 - ); - - fly.userData.curve = new THREE.QuadraticBezierCurve3( - startPoint, - controlPoint, - endPoint - ); - fly.userData.t = 0; // Reset progression - fly.userData.landCheckTimer = 50 + Math.random() * 50; // New landing decision window -} - -/** - * Creates and places the 'flies' meshes. - */ -function setupFlies() { - for (let i = 0; i < FLIES_COUNT; i++) { - const fly = createFlyMesh(); - scene.add(fly); - flies.push(fly); +class FliesEffect { + constructor(scene) { + this.flies = []; + this._setupFlies(scene); } -} -/** - * Updates the position and state of the flies using Bezier curves. - */ -function updateFlies() { - flies.forEach(fly => { - const data = fly.userData; - - if (data.state === 'flying' || data.state === 'landing') { - - if (!data.curve) { - // Initialize the first curve - const newTargetPos = randomFlyTarget(); - createFlyCurve(fly, newTargetPos); - data.t = 0; - } + _randomFlyTarget() { + return new THREE.Vector3( + (Math.random() - 0.5) * (ROOM_SIZE - 1), + FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN), + (Math.random() - 0.5) * (ROOM_SIZE - 1) + ); + } - // Advance curve progression - data.t += data.speed; - - // Check for landing readiness during the flight path - data.landCheckTimer--; + _createFlyMesh() { + const flyGroup = new THREE.Group(); + const flyMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 50 }); + const bodyGeometry = new THREE.ConeGeometry(0.01, 0.02, 3); + const body = new THREE.Mesh(bodyGeometry, flyMaterial); + body.rotation.x = degToRad(90); + body.castShadow = true; + body.receiveShadow = true; + flyGroup.add(body); - if (data.t >= 1) { - // Path finished + flyGroup.userData = { + state: 'flying', + landTimer: 0, + t: 0, + speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01, + curve: null, + landCheckTimer: 0, + oscillationTime: Math.random() * 100, + }; - if (data.state === 'landing') { - data.state = 'landed'; - data.landTimer = FLY_WAIT_BASE + Math.random() * 1000; // Land for a random duration + flyGroup.position.copy(this._randomFlyTarget()); + return flyGroup; + } + + _createFlyCurve(fly, endPoint) { + const startPoint = fly.position.clone(); + const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5); + const offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5; + const offsetAngle = Math.random() * Math.PI * 2; + + const controlPoint = new THREE.Vector3( + midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5, + midPoint.y + Math.random() * 0.5 + 0.5, + midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5 + ); + + fly.userData.curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint); + fly.userData.t = 0; + fly.userData.landCheckTimer = 50 + Math.random() * 50; + } + + _setupFlies(scene) { + for (let i = 0; i < FLIES_COUNT; i++) { + const fly = this._createFlyMesh(); + scene.add(fly); + this.flies.push(fly); + } + } + + update() { + this.flies.forEach(fly => { + const data = fly.userData; + + if (data.state === 'flying' || data.state === 'landing') { + if (!data.curve) { + const newTargetPos = this._randomFlyTarget(); + this._createFlyCurve(fly, newTargetPos); data.t = 0; - return; // Stop updates for this fly } - // 1. Check for landing decision - if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) { - - // Raycast down from the current position to find a landing spot - raycaster.set(fly.position, new THREE.Vector3(0, -1, 0)); - const intersects = raycaster.intersectObjects(landingSurfaces, false); + data.t += data.speed; + data.landCheckTimer--; - if (intersects.length > 0) { - const intersect = intersects[0]; - data.state = 'landing'; - // Land slightly above the surface - let newTargetPos = new THREE.Vector3(intersect.point.x, - intersect.point.y + 0.05, - intersect.point.z); - // const newTargetPos = randomFlyTarget(); - createFlyCurve(fly, newTargetPos); + if (data.t >= 1) { + if (data.state === 'landing') { + data.state = 'landed'; + data.landTimer = FLY_WAIT_BASE + Math.random() * 1000; + data.t = 0; + return; + } + + if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) { + raycaster.set(fly.position, new THREE.Vector3(0, -1, 0)); + const intersects = raycaster.intersectObjects(landingSurfaces, false); + + if (intersects.length > 0) { + const intersect = intersects[0]; + data.state = 'landing'; + let newTargetPos = new THREE.Vector3( + intersect.point.x, + intersect.point.y + 0.05, + intersect.point.z + ); + this._createFlyCurve(fly, newTargetPos); + data.t = 0; + } + } + + if (data.state !== 'landing') { + const newTargetPos = this._randomFlyTarget(); + this._createFlyCurve(fly, newTargetPos); data.t = 0; } } - - if (data.state !== 'landing') { - // 2. If not landing, generate a new random flight path - const newTargetPos = randomFlyTarget(); - createFlyCurve(fly, newTargetPos); - data.t = 0; // Reset T for the new curve + + fly.position.copy(data.curve.getPoint(Math.min(data.t, 1))); + const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize(); + fly.rotation.y = Math.atan2(tangent.x, tangent.z); + data.oscillationTime += 0.1; + fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01; + + } else if (data.state === 'landed') { + data.landTimer--; + if (data.landTimer <= 0) { + data.state = 'flying'; + const newTargetPos = this._randomFlyTarget(); + this._createFlyCurve(fly, newTargetPos); + data.t = 0; } } - - // Set position along the curve - fly.position.copy(data.curve.getPoint(Math.min(data.t, 1))); - - // Set rotation tangent to the curve - const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize(); - fly.rotation.y = Math.atan2(tangent.x, tangent.z); - - // Add slight Y oscillation for buzzing feel (on top of curve) - data.oscillationTime += 0.1; - fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01; - - } else if (data.state === 'landed') { - // --- Landed State --- - data.landTimer--; - if (data.landTimer <= 0) { - // Take off: Generate new flight curve from current landed position - data.state = 'flying'; - - const newTargetPos = randomFlyTarget(); - createFlyCurve(fly, newTargetPos); - data.t = 0; - } - } - }); + }); + } } \ No newline at end of file diff --git a/tv-player/src/global-variables.js b/tv-player/src/global-variables.js index b024298..34a1f32 100644 --- a/tv-player/src/global-variables.js +++ b/tv-player/src/global-variables.js @@ -1,5 +1,5 @@ // --- Global Variables --- -let scene, camera, renderer, tvScreen, videoTexture, dust, screenLight, lampLightPoint, lampLightSpot; +let scene, camera, renderer, tvScreen, videoTexture, screenLight, lampLightPoint, lampLightSpot, effectsManager; // VCR Display related variables let simulatedPlaybackTime = 0; @@ -23,9 +23,6 @@ const loadTapeButton = document.getElementById('loadTapeButton'); const loader = new THREE.TextureLoader(); const debugLight = false; - -const FLIES_COUNT = 2; // Flies -const flies = []; let landingSurfaces = []; // Array to hold floor and table for fly landings const raycaster = new THREE.Raycaster(); diff --git a/tv-player/src/init.js b/tv-player/src/init.js index ad64fe2..99cf6cd 100644 --- a/tv-player/src/init.js +++ b/tv-player/src/init.js @@ -30,8 +30,8 @@ function init() { // 5. Build the entire scene with TV and surrounding objects createSceneObjects(); - // 6. Create the Dust Particle System - createDust(); + // 6. Initialize all visual effects via the manager + effectsManager = new EffectsManager(scene); // 7. Create the Room Walls and Ceiling createRoomWalls(); diff --git a/tv-player/src/scene.js b/tv-player/src/scene.js index 2641d73..c5afa57 100644 --- a/tv-player/src/scene.js +++ b/tv-player/src/scene.js @@ -567,5 +567,4 @@ function createSceneObjects() { createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.7, Math.PI/2, 0); createBookshelf(roomSize/2 * 0.7, -roomSize/2+0.3, 0, 1); - setupFlies(); }