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();
}