Refactoring: move effects to a manager, chunk animate loop contents

This commit is contained in:
Dejvino 2025-11-13 21:45:11 +01:00
parent 043552f36c
commit 75c87c9d03
8 changed files with 215 additions and 248 deletions

View File

@ -51,6 +51,7 @@
<script src="./src/video-player.js"></script> <script src="./src/video-player.js"></script>
<script src="./src/effects_dust.js"></script> <script src="./src/effects_dust.js"></script>
<script src="./src/effects_flies.js"></script> <script src="./src/effects_flies.js"></script>
<script src="./src/EffectsManager.js"></script>
<script src="./src/vcr-display.js"></script> <script src="./src/vcr-display.js"></script>
<script src="./src/animate.js"></script> <script src="./src/animate.js"></script>
<script src="./src/init.js"></script> <script src="./src/init.js"></script>

View File

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

View File

@ -1,20 +1,4 @@
// --- Animation Loop --- function updateCamera() {
function animate() {
requestAnimationFrame(animate);
// 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 globalTime = Date.now() * 0.00005;
const lookAtTime = Date.now() * 0.00003; const lookAtTime = Date.now() * 0.00003;
@ -45,13 +29,10 @@ function animate() {
const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude; const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude;
// Apply lookAt to the subtly shifted target // Apply lookAt to the subtly shifted target
camera.lookAt( camera.lookAt(baseTargetX + lookOffsetX, baseTargetY + lookOffsetY, baseTargetZ);
baseTargetX + lookOffsetX, }
baseTargetY + lookOffsetY,
baseTargetZ
);
// 3. Lamp Flicker Effect function updateLampFlicker() {
const flickerChance = 0.995; const flickerChance = 0.995;
const restoreRate = 0.15; const restoreRate = 0.15;
@ -66,55 +47,51 @@ function animate() {
lampLightSpot.intensity = lampLightIntensity; lampLightSpot.intensity = lampLightIntensity;
lampLightPoint.intensity = lampLightIntensity; lampLightPoint.intensity = lampLightIntensity;
} }
}
// 4. Screen Light Pulse and Movement Effect (Updated) function updateScreenLight() {
if (isVideoLoaded && screenLight.intensity > 0) { 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; const pulseTarget = originalScreenIntensity + (Math.random() - 0.5) * screenIntensityPulse;
// Smoothly interpolate towards the new target fluctuation
screenLight.intensity = THREE.MathUtils.lerp(screenLight.intensity, pulseTarget, 0.1); 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 lightTime = Date.now() * 0.0001;
const radius = 0.01; const radius = 0.01;
const centerX = 0; const centerX = 0;
const centerY = 1.5; 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.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.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) { if (videoTexture) {
videoTexture.needsUpdate = true; 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; const currentTime = baseTime + videoElement.currentTime;
// Simulate playback time
if (Math.abs(currentTime - lastUpdateTime) > 0.1) { if (Math.abs(currentTime - lastUpdateTime) > 0.1) {
updateVcrDisplay(currentTime); updateVcrDisplay(currentTime);
lastUpdateTime = currentTime; lastUpdateTime = currentTime;
} }
// Blink the colon every second
if (currentTime - lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds if (currentTime - lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds
blinkState = !blinkState; blinkState = !blinkState;
lastBlinkToggleTime = currentTime; lastBlinkToggleTime = currentTime;
} }
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
effectsManager.update();
updateCamera();
updateLampFlicker();
updateScreenLight();
updateVideo();
updateVcr();
// RENDER! // RENDER!
renderer.render(scene, camera); renderer.render(scene, camera);

View File

@ -1,5 +1,10 @@
// --- Dust Particle System Function --- class DustEffect {
function createDust() { constructor(scene) {
this.dust = null;
this._create(scene);
}
_create(scene) {
const particleCount = 2000; const particleCount = 2000;
const particlesGeometry = new THREE.BufferGeometry(); const particlesGeometry = new THREE.BufferGeometry();
const positions = []; const positions = [];
@ -11,7 +16,6 @@ function createDust() {
(Math.random() - 0.5) * 15 (Math.random() - 0.5) * 15
); );
} }
// Use THREE.Float32BufferAttribute to correctly set the position attribute
particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
const particleMaterial = new THREE.PointsMaterial({ const particleMaterial = new THREE.PointsMaterial({
@ -22,7 +26,20 @@ function createDust() {
blending: THREE.AdditiveBlending blending: THREE.AdditiveBlending
}); });
dust = new THREE.Points(particlesGeometry, particleMaterial); this.dust = new THREE.Points(particlesGeometry, particleMaterial);
// Dust particles generally don't cast or receive shadows in this context scene.add(this.dust);
scene.add(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;
}
}
} }

View File

@ -1,174 +1,129 @@
function randomFlyTarget() { const FLIES_COUNT = 2;
class FliesEffect {
constructor(scene) {
this.flies = [];
this._setupFlies(scene);
}
_randomFlyTarget() {
return new THREE.Vector3( return new THREE.Vector3(
(Math.random() - 0.5) * (ROOM_SIZE - 1), (Math.random() - 0.5) * (ROOM_SIZE - 1),
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN), FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
(Math.random() - 0.5) * (ROOM_SIZE - 1)); (Math.random() - 0.5) * (ROOM_SIZE - 1)
} );
}
/** _createFlyMesh() {
* Creates a single fly mesh (small cone/tetrahedron).
* @returns {THREE.Group}
*/
function createFlyMesh() {
const flyGroup = new THREE.Group(); const flyGroup = new THREE.Group();
const flyMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 50 });
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 bodyGeometry = new THREE.ConeGeometry(0.01, 0.02, 3);
const body = new THREE.Mesh(bodyGeometry, flyMaterial); const body = new THREE.Mesh(bodyGeometry, flyMaterial);
body.rotation.x = degToRad(90); // Point nose in Z direction body.rotation.x = degToRad(90);
body.castShadow = true; body.castShadow = true;
body.receiveShadow = true; body.receiveShadow = true;
flyGroup.add(body); flyGroup.add(body);
// Initial state and parameters for the fly
flyGroup.userData = { flyGroup.userData = {
state: 'flying', // 'flying' or 'landed' state: 'flying',
landTimer: 0, landTimer: 0,
t: 0, // Curve progression t parameter (0 to 1) t: 0,
speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01, speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01,
curve: null, curve: null,
landCheckTimer: 0, landCheckTimer: 0,
oscillationTime: Math.random() * 100, // For smooth y-axis buzzing oscillationTime: Math.random() * 100,
}; };
// Initial random position flyGroup.position.copy(this._randomFlyTarget());
flyGroup.position = randomFlyTarget();
return flyGroup; return flyGroup;
} }
_createFlyCurve(fly, endPoint) {
/**
* 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(); const startPoint = fly.position.clone();
// Calculate the midpoint
const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5); 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 offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5;
const offsetAngle = Math.random() * Math.PI * 2; 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( const controlPoint = new THREE.Vector3(
midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5, midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5,
midPoint.y + Math.random() * 0.5 + 0.5, midPoint.y + Math.random() * 0.5 + 0.5,
midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5 midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5
); );
fly.userData.curve = new THREE.QuadraticBezierCurve3( fly.userData.curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint);
startPoint, fly.userData.t = 0;
controlPoint, fly.userData.landCheckTimer = 50 + Math.random() * 50;
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);
} }
}
/** _setupFlies(scene) {
* Updates the position and state of the flies using Bezier curves. for (let i = 0; i < FLIES_COUNT; i++) {
*/ const fly = this._createFlyMesh();
function updateFlies() { scene.add(fly);
flies.forEach(fly => { this.flies.push(fly);
}
}
update() {
this.flies.forEach(fly => {
const data = fly.userData; const data = fly.userData;
if (data.state === 'flying' || data.state === 'landing') { if (data.state === 'flying' || data.state === 'landing') {
if (!data.curve) { if (!data.curve) {
// Initialize the first curve const newTargetPos = this._randomFlyTarget();
const newTargetPos = randomFlyTarget(); this._createFlyCurve(fly, newTargetPos);
createFlyCurve(fly, newTargetPos);
data.t = 0; data.t = 0;
} }
// Advance curve progression
data.t += data.speed; data.t += data.speed;
// Check for landing readiness during the flight path
data.landCheckTimer--; data.landCheckTimer--;
if (data.t >= 1) { if (data.t >= 1) {
// Path finished
if (data.state === 'landing') { if (data.state === 'landing') {
data.state = 'landed'; data.state = 'landed';
data.landTimer = FLY_WAIT_BASE + Math.random() * 1000; // Land for a random duration data.landTimer = FLY_WAIT_BASE + Math.random() * 1000;
data.t = 0; data.t = 0;
return; // Stop updates for this fly return;
} }
// 1. Check for landing decision
if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) { 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)); raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
const intersects = raycaster.intersectObjects(landingSurfaces, false); const intersects = raycaster.intersectObjects(landingSurfaces, false);
if (intersects.length > 0) { if (intersects.length > 0) {
const intersect = intersects[0]; const intersect = intersects[0];
data.state = 'landing'; data.state = 'landing';
// Land slightly above the surface let newTargetPos = new THREE.Vector3(
let newTargetPos = new THREE.Vector3(intersect.point.x, intersect.point.x,
intersect.point.y + 0.05, intersect.point.y + 0.05,
intersect.point.z); intersect.point.z
// const newTargetPos = randomFlyTarget(); );
createFlyCurve(fly, newTargetPos); this._createFlyCurve(fly, newTargetPos);
data.t = 0; data.t = 0;
} }
} }
if (data.state !== 'landing') { if (data.state !== 'landing') {
// 2. If not landing, generate a new random flight path const newTargetPos = this._randomFlyTarget();
const newTargetPos = randomFlyTarget(); this._createFlyCurve(fly, newTargetPos);
createFlyCurve(fly, newTargetPos); data.t = 0;
data.t = 0; // Reset T for the new curve
} }
} }
// Set position along the curve
fly.position.copy(data.curve.getPoint(Math.min(data.t, 1))); 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(); const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize();
fly.rotation.y = Math.atan2(tangent.x, tangent.z); fly.rotation.y = Math.atan2(tangent.x, tangent.z);
// Add slight Y oscillation for buzzing feel (on top of curve)
data.oscillationTime += 0.1; data.oscillationTime += 0.1;
fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01; fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01;
} else if (data.state === 'landed') { } else if (data.state === 'landed') {
// --- Landed State ---
data.landTimer--; data.landTimer--;
if (data.landTimer <= 0) { if (data.landTimer <= 0) {
// Take off: Generate new flight curve from current landed position
data.state = 'flying'; data.state = 'flying';
const newTargetPos = this._randomFlyTarget();
const newTargetPos = randomFlyTarget(); this._createFlyCurve(fly, newTargetPos);
createFlyCurve(fly, newTargetPos);
data.t = 0; data.t = 0;
} }
} }
}); });
}
} }

View File

@ -1,5 +1,5 @@
// --- Global Variables --- // --- 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 // VCR Display related variables
let simulatedPlaybackTime = 0; let simulatedPlaybackTime = 0;
@ -23,9 +23,6 @@ const loadTapeButton = document.getElementById('loadTapeButton');
const loader = new THREE.TextureLoader(); const loader = new THREE.TextureLoader();
const debugLight = false; const debugLight = false;
const FLIES_COUNT = 2; // Flies
const flies = [];
let landingSurfaces = []; // Array to hold floor and table for fly landings let landingSurfaces = []; // Array to hold floor and table for fly landings
const raycaster = new THREE.Raycaster(); const raycaster = new THREE.Raycaster();

View File

@ -30,8 +30,8 @@ function init() {
// 5. Build the entire scene with TV and surrounding objects // 5. Build the entire scene with TV and surrounding objects
createSceneObjects(); createSceneObjects();
// 6. Create the Dust Particle System // 6. Initialize all visual effects via the manager
createDust(); effectsManager = new EffectsManager(scene);
// 7. Create the Room Walls and Ceiling // 7. Create the Room Walls and Ceiling
createRoomWalls(); createRoomWalls();

View File

@ -567,5 +567,4 @@ function createSceneObjects() {
createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.7, Math.PI/2, 0); createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.7, Math.PI/2, 0);
createBookshelf(roomSize/2 * 0.7, -roomSize/2+0.3, 0, 1); createBookshelf(roomSize/2 * 0.7, -roomSize/2+0.3, 0, 1);
setupFlies();
} }