diff --git a/tv-player/index.html b/tv-player/index.html index 1ae2421..c74d339 100644 --- a/tv-player/index.html +++ b/tv-player/index.html @@ -84,6 +84,20 @@ 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(); + + // --- Configuration --- + const ROOM_SIZE = roomSize; + const FLIGHT_HEIGHT_MIN = 0.5; // Min height for flying + const FLIGHT_HEIGHT_MAX = roomHeight * 0.9; // Max height for flying + const FLY_FLIGHT_SPEED_FACTOR = 0.01; // How quickly 't' increases per frame + const DAMPING_FACTOR = 0.05; + const FLY_WAIT_BASE = 1000; + const FLY_LAND_CHANCE = 0.3; + // --- Utility: Random Color (seeded) --- function getRandomColor() { const hue = seededRandom(); @@ -92,6 +106,15 @@ return new THREE.Color().setHSL(hue, saturation, lightness).getHex(); } + /** + * Converts degrees to radians. + * @param {number} degrees + * @returns {number} + */ + function degToRad(degrees) { + return degrees * (Math.PI / 180); + } + // --- Seedable Random Number Generator (Mulberry32) --- let seed = 12345; // Default seed, will be overridden per shelf function seededRandom() { @@ -108,7 +131,7 @@ scene.background = new THREE.Color(0x000000); // 2. Camera Setup - const FOV = 55; + const FOV = 65; camera = new THREE.PerspectiveCamera(FOV, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(0, 1.5, 4); @@ -312,6 +335,8 @@ topPanel.castShadow = true; shelfGroup.add(topPanel); + landingSurfaces.push(topPanel); + // 2. Individual Shelves & Books const internalHeight = shelfHeight - (2 * woodThickness); const shelfSpacing = internalHeight / numShelves; @@ -519,6 +544,8 @@ floor.receiveShadow = true; scene.add(floor); + landingSurfaces.push(floor); + createTvSet(-roomSize/2 + 1.2, -roomSize/2 + 0.8, Math.PI * 0.1); // --- 5. Lamp (On the table, right side) --- @@ -568,6 +595,8 @@ scene.add(lampGroup); + landingSurfaces.push(shadeMesh); + // --- 7. Old Camera (On the table) --- const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.2); const cameraLens = new THREE.CylinderGeometry(0.08, 0.08, 0.05, 12); @@ -601,6 +630,8 @@ createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.2, 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); + + setupFlies(); } // --- Dust Particle System Function --- @@ -762,6 +793,181 @@ playVideoByIndex(0); } + 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)); + } + + /** + * 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); + } + } + + /** + * 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; + } + + // Advance curve progression + data.t += data.speed; + + // Check for landing readiness during the flight path + data.landCheckTimer--; + + if (data.t >= 1) { + // Path finished + + if (data.state === 'landing') { + data.state = 'landed'; + data.landTimer = FLY_WAIT_BASE + Math.random() * 1000; // Land for a random duration + 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); + + 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); + 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 + } + } + + // 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; + } + } + }); + } + // --- Animation Loop --- function animate() { requestAnimationFrame(animate); @@ -863,6 +1069,8 @@ `Tape ${currentVideoIndex + 1} of ${videoUrls.length}. Time: ${currentTime} / ${duration}`; } } + + updateFlies(); // RENDER! renderer.render(scene, camera);