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