diff --git a/tv-player/index.html b/tv-player/index.html
index 146edf6..ba3fc4e 100644
--- a/tv-player/index.html
+++ b/tv-player/index.html
@@ -48,6 +48,9 @@
+
+
+
@@ -124,480 +127,6 @@
animate();
}
- function createBookshelf(x, z, rotationY, uniqueSeed) {
- seed = uniqueSeed; // Reset seed for this specific shelf instance
-
- const shelfHeight = 2.2;
- const shelfDepth = 0.35;
- const shelfWidth = 1.2;
- const numShelves = 6;
- const woodThickness = 0.04;
- const woodColor = 0x5c4033; // Darker, richer wood
-
- const shelfGroup = new THREE.Group();
- shelfGroup.position.set(x, 0, z);
- shelfGroup.rotation.y = rotationY;
-
- const woodMaterial = new THREE.MeshPhongMaterial({ color: woodColor, shininess: 30 });
-
- // 1. Build Frame (Hollow box)
- // Back Panel
- const backGeo = new THREE.BoxGeometry(shelfWidth, shelfHeight, woodThickness);
- const backPanel = new THREE.Mesh(backGeo, woodMaterial);
- backPanel.position.set(0, shelfHeight / 2, -shelfDepth / 2 + woodThickness / 2);
- backPanel.castShadow = true;
- backPanel.receiveShadow = true;
- shelfGroup.add(backPanel);
-
- // Side Panels (Left & Right)
- const sideGeo = new THREE.BoxGeometry(woodThickness, shelfHeight, shelfDepth);
- const leftSide = new THREE.Mesh(sideGeo, woodMaterial);
- leftSide.position.set(-shelfWidth / 2 + woodThickness / 2, shelfHeight / 2, 0);
- leftSide.castShadow = true;
- leftSide.receiveShadow = true;
- shelfGroup.add(leftSide);
-
- const rightSide = new THREE.Mesh(sideGeo, woodMaterial);
- rightSide.position.set(shelfWidth / 2 - woodThickness / 2, shelfHeight / 2, 0);
- rightSide.castShadow = true;
- rightSide.receiveShadow = true;
- shelfGroup.add(rightSide);
-
- // Top & Bottom Panels
- const topBottomGeo = new THREE.BoxGeometry(shelfWidth, woodThickness, shelfDepth);
- const bottomPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
- bottomPanel.position.set(0, woodThickness / 2, 0);
- bottomPanel.receiveShadow = true;
- shelfGroup.add(bottomPanel);
-
- const topPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
- topPanel.position.set(0, shelfHeight - woodThickness / 2, 0);
- topPanel.castShadow = true;
- shelfGroup.add(topPanel);
-
- landingSurfaces.push(topPanel);
-
- // 2. Individual Shelves & Books
- const internalHeight = shelfHeight - (2 * woodThickness);
- const shelfSpacing = internalHeight / numShelves;
- const internalWidth = shelfWidth - (2 * woodThickness);
-
- for (let i = 0; i < numShelves; i++) {
- const currentShelfY = woodThickness + (i * shelfSpacing);
-
- // Shelf board (skip for the very bottom one as we have a bottom panel)
- if (i > 0) {
- const shelfBoard = new THREE.Mesh(
- new THREE.BoxGeometry(internalWidth, woodThickness, shelfDepth - woodThickness), // Slightly shallower to fit inside back panel
- woodMaterial
- );
- shelfBoard.position.set(0, currentShelfY, woodThickness / 2); // Offset forward slightly
- shelfBoard.castShadow = true;
- shelfBoard.receiveShadow = true;
- shelfGroup.add(shelfBoard);
- }
-
- // 3. Procedural Books
- let currentBookX = -internalWidth / 2 + 0.01; // Start at left inside edge
- const shelfSurfaceY = currentShelfY + woodThickness / 2;
-
- while (currentBookX < internalWidth / 2 - 0.05) {
- // sizes vary
- const bookWidth = 0.02 + seededRandom() * 0.05;
- const bookHeight = (shelfSpacing * 0.6) + seededRandom() * (shelfSpacing * 0.1);
- const bookDepth = 0.15 + seededRandom() * 0.03;
-
- if (currentBookX + bookWidth > internalWidth / 2) break;
-
- const bookColor = getRandomColor();
- const bookMat = new THREE.MeshPhongMaterial({ color: bookColor, shininess: 60 });
- const bookGeo = new THREE.BoxGeometry(bookWidth, bookHeight, bookDepth);
- const book = new THREE.Mesh(bookGeo, bookMat);
-
- // Position: Resting on shelf, pushed towards the back with slight random variation
- const depthVariation = seededRandom() * 0.05;
- book.position.set(
- currentBookX + bookWidth / 2,
- shelfSurfaceY + bookHeight / 2,
- -shelfDepth / 2 + woodThickness + bookDepth / 2 + depthVariation
- );
-
- book.castShadow = true;
- book.receiveShadow = true;
- shelfGroup.add(book);
-
- currentBookX += bookWidth + 0.002; // Tiny gap between books
-
- if (seededRandom() > 0.92) {
- currentBookX += bookWidth * 3; // random bigger gaps
- }
- }
- }
-
- scene.add(shelfGroup);
- }
-
- function createDoor(x, z, rotY) {
- const doorGroup = new THREE.Group();
- doorGroup.position.set(x, 1.1, z); // Centered vertically for a 2.2m door
- doorGroup.rotation.set(0, rotY, 0);
-
- // Door Frame
- const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x473e3a }); // Dark wood for frame
- const frameTop = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.1, 0.15), frameMaterial);
- frameTop.position.set(0, 1.15, 0);
- frameTop.castShadow = true;
- doorGroup.add(frameTop);
-
- const frameLeft = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
- frameLeft.position.set(-0.55, 0.05, 0);
- frameLeft.castShadow = true;
- doorGroup.add(frameLeft);
-
- const frameRight = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
- frameRight.position.set(0.55, 0.05, 0);
- frameRight.castShadow = true;
- doorGroup.add(frameRight);
-
- // Main Door Panel
- const doorMaterial = new THREE.MeshPhongMaterial({ color: 0x8b5a2b, shininess: 10 }); // Lighter wood for door
- const door = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.2, 0.08), doorMaterial);
- door.castShadow = true;
- door.receiveShadow = true;
- doorGroup.add(door);
-
- // Door Knob
- const knobMaterial = new THREE.MeshPhongMaterial({ color: 0xd4af37, shininess: 100 }); // Gold/Brass
- const knob = new THREE.Mesh(new THREE.SphereGeometry(0.05, 16, 16), knobMaterial);
- knob.position.set(0.4, 0, 0.06); // Position on the right side of the door
- knob.castShadow = true;
- doorGroup.add(knob);
-
- scene.add(doorGroup);
- }
-
- function createTvSet(x, z, rotY) {
- // --- Materials (MeshPhongMaterial) ---
- const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
- const darkMetal = new THREE.MeshPhongMaterial({
- color: 0x6b6b6b,
- shininess: 80,
- specular: 0x888888
- });
- const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x4d4d4d, shininess: 30 });
-
- const tvGroup = new THREE.Group();
-
- // --- TV Table Dimensions & Material ---
- const woodColor = 0x5a3e36; // Dark brown wood
- const tableHeight = 0.7; // Height from floor to top surface
- const tableWidth = 2.0;
- const tableDepth = 1.0;
- const legThickness = 0.05;
- const shelfThickness = 0.03;
- // Use standard material for realistic shadowing
- const material = new THREE.MeshStandardMaterial({ color: woodColor, roughness: 0.8, metalness: 0.1 });
-
- // VCR gap dimensions calculation
- const shelfGap = 0.2; // Height of the VCR opening
- const shelfY = tableHeight - shelfGap - (shelfThickness / 2); // Y position of the bottom shelf
-
-
- // 2. Table Top
- const topGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
- const tableTop = new THREE.Mesh(topGeometry, material);
- tableTop.position.set(0, tableHeight, 0);
- tableTop.castShadow = true;
- tableTop.receiveShadow = true;
- tvGroup.add(tableTop);
-
- // 3. VCR Shelf (Middle Shelf)
- const shelfGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
- const vcrShelf = new THREE.Mesh(shelfGeometry, material);
- vcrShelf.position.set(0, shelfY, 0);
- vcrShelf.castShadow = true;
- vcrShelf.receiveShadow = true;
- tvGroup.add(vcrShelf);
-
- // 4. Side Walls for VCR Compartment (NEW CODE)
- const wallHeight = shelfGap; // Height is the gap itself
- const wallThickness = shelfThickness; // Reuse the shelf thickness for the wall width/depth
- const wallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, tableDepth);
-
- // Calculate the Y center position for the wall
- const wallYCenter = tableHeight - (shelfThickness / 2) - (wallHeight / 2);
-
- // Calculate the X position to be flush with the table sides
- const wallXPosition = (tableWidth / 2) - (wallThickness / 2);
-
- // Left Wall
- const sideWallLeft = new THREE.Mesh(wallGeometry, material);
- sideWallLeft.position.set(-wallXPosition, wallYCenter, 0);
- sideWallLeft.castShadow = true;
- sideWallLeft.receiveShadow = true;
- tvGroup.add(sideWallLeft);
-
- // Right Wall
- const sideWallRight = new THREE.Mesh(wallGeometry, material);
- sideWallRight.position.set(wallXPosition, wallYCenter, 0);
- sideWallRight.castShadow = true;
- sideWallRight.receiveShadow = true;
- tvGroup.add(sideWallRight);
-
- // 5. Legs
- const legHeight = shelfY; // Legs go from the floor (y=0) to the shelf (y=shelfY)
- const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
-
- // Utility function to create and position a leg
- const createLeg = (x, z) => {
- const leg = new THREE.Mesh(legGeometry, material);
- // Position the leg so the center is at half its height
- leg.position.set(x, legHeight / 2, z);
- leg.castShadow = true;
- leg.receiveShadow = true;
- return leg;
- };
-
- // Calculate offsets for positioning the legs near the corners
- const offset = (tableWidth / 2) - (legThickness * 2);
- const depthOffset = (tableDepth / 2) - (legThickness * 2);
-
- // Front Left
- tvGroup.add(createLeg(-offset, depthOffset));
- // Front Right
- tvGroup.add(createLeg(offset, depthOffset));
- // Back Left
- tvGroup.add(createLeg(-offset, -depthOffset));
- // Back Right
- tvGroup.add(createLeg(offset, -depthOffset));
-
- // --- 2. The TV box ---
- const cabinetGeometry = new THREE.BoxGeometry(1.75, 1.5, 1.0);
- const cabinet = new THREE.Mesh(cabinetGeometry, tvPlastic);
- cabinet.position.y = 1.51;
- cabinet.castShadow = true;
- cabinet.receiveShadow = true;
- tvGroup.add(cabinet);
-
- // --- 3. Screen Frame ---
- const frameGeometry = new THREE.BoxGeometry(1.5, 1.3, 0.1);
- const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 20 });
- const frame = new THREE.Mesh(frameGeometry, frameMaterial);
- frame.position.set(0, 1.5, 0.68);
- frame.castShadow = true;
- frame.receiveShadow = true;
- tvGroup.add(frame);
-
- // --- 4. Curved Screen (CRT Effect) ---
- const screenRadius = 3.0; // Radius for the subtle curve
- const screenWidth = 1.4;
- const screenHeight = 1.2;
- const thetaLength = screenWidth / screenRadius; // Calculate angle needed for the arc
-
- // Use CylinderGeometry as a segment
- const screenGeometry = new THREE.CylinderGeometry(
- screenRadius, screenRadius,
- screenHeight, // Cylinder height is the vertical dimension of the screen
- 32,
- 1,
- true,
- (Math.PI / 2) - (thetaLength / 2), // Start angle to center the arc
- thetaLength // Arc length (width)
- );
-
- // Rotate the cylinder segment:
- // 1. Rotate around X-axis by 90 degrees to lay the height (Y) along Z (depth).
- //screenGeometry.rotateX(Math.PI / 2);
- // 2. Rotate around Y-axis by 90 degrees to align the segment's arc across the X-axis (width).
- screenGeometry.rotateY(-Math.PI/2);
-
- const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
- tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
-
- // Position the curved screen
- tvScreen.position.set(0.0, 1.5, -2.1);
- tvScreen.material = new THREE.MeshPhongMaterial({
- color: 0x0a0a0a, // Deep black
- shininess: 5,
- specular: 0x111111
- });
- tvScreen.material.needsUpdate = true;
- tvGroup.add(tvScreen);
-
- tvGroup.position.set(x, 0, z);
- tvGroup.rotation.y = rotY;
-
- // Light from the screen (initially low intensity, will increase when video loads)
- screenLight = new THREE.PointLight(0xffffff, 0, 10);
- screenLight.position.set(0, 1.5, 1.0);
- // Screen light casts shadows
- screenLight.castShadow = true;
- screenLight.shadow.mapSize.width = 1024;
- screenLight.shadow.mapSize.height = 1024;
- screenLight.shadow.camera.near = 0.2;
- screenLight.shadow.camera.far = 5;
- tvGroup.add(screenLight);
-
- // -- VCR --
- const vcr = createVcr();
- vcr.position.set(-0.3, 0.6, 0.05);
- tvGroup.add(vcr);
-
- scene.add(tvGroup);
- }
-
- // --- Scene Modeling Function ---
- function createSceneObjects() {
- // --- Materials (MeshPhongMaterial) ---
- const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
- const darkMetal = new THREE.MeshPhongMaterial({
- color: 0x6b6b6b,
- shininess: 80,
- specular: 0x888888
- });
- const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x2d251e, shininess: 10 });
-
- // --- 1. Floor ---
- const floorGeometry = new THREE.PlaneGeometry(20, 20);
- const floorTexture = loader.load('./textures/floor.jpg');
- floorTexture.wrapS = THREE.RepeatWrapping;
- floorTexture.wrapT = THREE.RepeatWrapping;
- floorTexture.repeat.set(roomSize, roomSize);
- const floorMaterial = new THREE.MeshPhongMaterial({ map: floorTexture, color: 0x555555, shininess: 5 });
- const floor = new THREE.Mesh(floorGeometry, floorMaterial);
- floor.rotation.x = -Math.PI / 2;
- floor.position.y = 0;
- 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) ---
- const lampBase = new THREE.CylinderGeometry(0.05, 0.2, 0.1, 12);
- const lampPole = new THREE.CylinderGeometry(0.02, 0.02, 1.5, 8);
- const lampShade = new THREE.ConeGeometry(0.2, 0.4, 16);
-
- const baseMesh = new THREE.Mesh(lampBase, darkMetal);
- const poleMesh = new THREE.Mesh(lampPole, darkMetal);
- const shadeMesh = new THREE.Mesh(lampShade, darkMetal);
-
- // Ensure lamp parts cast shadows
- baseMesh.castShadow = true; baseMesh.receiveShadow = true;
- poleMesh.castShadow = true; poleMesh.receiveShadow = true;
- //shadeMesh.castShadow = true; shadeMesh.receiveShadow = true;
-
- baseMesh.position.y = -0.6;
- poleMesh.position.y = 0.0;
- shadeMesh.position.y = 0.8 + 0.1;
- shadeMesh.rotation.x = Math.PI;
-
- // Lamp Light (Warm Glow) - Configured to cast shadows
- lampLightPoint = new THREE.PointLight(0xffaa00, originalLampIntensity, 4);
- lampLightPoint.position.set(-0.01, roomHeight-0.9, 0.01);
- lampLightPoint.castShadow = true;
- // Optimization: Reduced map size and far plane to ease resource burden
- lampLightPoint.shadow.mapSize.width = 512;
- lampLightPoint.shadow.mapSize.height = 512;
- lampLightPoint.shadow.camera.near = 0.1;
- lampLightPoint.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
- lampLightPoint.penumbra = 0.5;
-
- lampLightSpot = new THREE.SpotLight(0xffaa00, originalLampIntensity, 4);
- lampLightSpot.position.set(-0.01, 1.0, 0.01);
- lampLightSpot.target.position.set(0, 5, 0);
- lampLightSpot.castShadow = true;
- // Optimization: Reduced map size and far plane to ease resource burden
- lampLightSpot.shadow.mapSize.width = 512;
- lampLightSpot.shadow.mapSize.height = 512;
- lampLightSpot.shadow.camera.near = 0.1;
- lampLightSpot.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
- lampLightSpot.penumbra = 0.5;
-
- const lampGroup = new THREE.Group();
- lampGroup.add(baseMesh, poleMesh, shadeMesh, lampLightSpot, lampLightSpot.target, lampLightPoint);
- lampGroup.position.set(0.8, 0.7, -roomSize/2+0.5);
-
- scene.add(lampGroup);
-
- landingSurfaces.push(shadeMesh);
-
- // --- 7. Old Camera (On the table) ---
- const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.15);
- const cameraLens = new THREE.CylinderGeometry(0.08, 0.08, 0.05, 12);
- const cameraMaterial = new THREE.MeshPhongMaterial({
- color: 0x333333,
- shininess: 50,
- specular: 0x444444
- });
-
- const cameraMesh = new THREE.Mesh(cameraBody, cameraMaterial);
- const lensMesh = new THREE.Mesh(cameraLens, cameraMaterial);
- lensMesh.position.z = 0.15;
- lensMesh.rotation.x = Math.PI/2;
-
- cameraMesh.add(lensMesh);
- cameraMesh.position.set(-1.7, 0.15, 0.4);
- cameraMesh.rotation.y = -Math.PI / 10;
- cameraMesh.castShadow = true; cameraMesh.receiveShadow = true;
- scene.add(cameraMesh);
-
- // --- 8. Pizza Box ---
- const boxGeometry = new THREE.BoxGeometry(0.5, 0.05, 0.5);
- const boxMaterial = new THREE.MeshPhongMaterial({ color: 0xe0c896, shininess: 5 });
- const pizzaBox = new THREE.Mesh(boxGeometry, boxMaterial);
- pizzaBox.position.set(-1.8, 0.025, -0.8);
- pizzaBox.rotation.y = Math.PI / 5;
- pizzaBox.castShadow = true; pizzaBox.receiveShadow = true;
- scene.add(pizzaBox);
-
- // --- 8. Cassette ---
- const cassetteGeometry = new THREE.BoxGeometry(0.2, 0.05, 0.45);
- const cassetteMaterial = new THREE.MeshPhongMaterial({ color: 0xe0c896, shininess: 5 });
- const cassette = new THREE.Mesh(cassetteGeometry, cassetteMaterial);
- cassette.position.set(-0.5, 0.025, -1.4);
- cassette.rotation.y = Math.PI / 3;
- cassette.castShadow = true; cassette.receiveShadow = true;
- scene.add(cassette);
-
- createDoor(roomSize/2, -roomSize/2 * 0.5, -Math.PI/2);
- 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 ---
- 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
- );
- }
- // 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
- });
-
- dust = new THREE.Points(particlesGeometry, particleMaterial);
- // Dust particles generally don't cast or receive shadows in this context
- scene.add(dust);
- }
-
// --- Helper function to format seconds into MM:SS ---
function formatTime(seconds) {
if (isNaN(seconds) || seconds === Infinity || seconds < 0) return '--:--';
@@ -721,453 +250,6 @@
setTimeout(() => { playVideoByIndex(0); }, startDelay * 1000);
}
- let vcrDisplayLight;
- let simulatedPlaybackTime = 0;
- let lastUpdateTime = -1;
- let baseTime = 0;
- let blinkState = false; // For blinking colon
- let lastBlinkToggleTime = 0;
-
- // --- Segment Display Definitions ---
-
- // Define which segments (indexed 0-6: A, B, C, D, E, F, G) are active for each digit
- // A=Top, B=TR, C=BR, D=Bottom, E=BL, F=TL, G=Middle
- const SEGMENTS = {
- '0': [1, 1, 1, 1, 1, 1, 0],
- '1': [0, 1, 1, 0, 0, 0, 0],
- '2': [1, 1, 0, 1, 1, 0, 1],
- '3': [1, 1, 1, 1, 0, 0, 1],
- '4': [0, 1, 1, 0, 0, 1, 1],
- '5': [1, 0, 1, 1, 0, 1, 1],
- '6': [1, 0, 1, 1, 1, 1, 1],
- '7': [1, 1, 1, 0, 0, 0, 0],
- '8': [1, 1, 1, 1, 1, 1, 1],
- '9': [1, 1, 1, 1, 0, 1, 1],
- ' ': [0, 0, 0, 0, 0, 0, 0]
- };
-
- const SEG_THICKNESS = 3; // Thickness of the segment line in canvas pixels
- const SEG_PADDING = 2; // Padding within a digit segment's box
-
- // Colors for active and inactive segments
- const COLOR_ACTIVE = '#00ff44'; // Bright Fluorescent Green
- const COLOR_INACTIVE = '#1a1a1a'; // Dim dark gray for 'ghost' segments
-
- /**
- * Draws a single 7-segment digit by drawing active segments.
- * Now includes drawing of inactive (ghost) segments for better readability.
- * @param {CanvasRenderingContext2D} ctx
- * @param {string} digit The digit character (0-9).
- * @param {number} x Left position of the digit area.
- * @param {number} y Top position of the digit area.
- * @param {number} H Total height of the digit area.
- */
- function drawSegmentDigit(ctx, digit, x, y, H) {
- const segments = SEGMENTS[digit] || SEGMENTS[' '];
- const W = H / 2; // Width is half the height for standard aspect ratio
-
- // Segment dimensions relative to W and H
- const hLength = W - 2 * SEG_PADDING;
- // Vertical length calculation: (Total height - 2 paddings - 3 horizontal thicknesses) / 2
- const vLength = (H - (2 * SEG_PADDING) - (3 * SEG_THICKNESS)) / 2;
-
- // Helper to draw horizontal segment (A, G, D)
- const drawH = (index, x_start, y_start) => {
- ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
- ctx.fillRect(x_start + SEG_PADDING, y_start, hLength, SEG_THICKNESS);
- };
-
- // Helper to draw vertical segment (F, B, E, C)
- const drawV = (index, x_start, y_start) => {
- ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
- ctx.fillRect(x_start, y_start, SEG_THICKNESS, vLength);
- };
-
- // Define segment positions
-
- // Horizontal segments
- // A (Top) - index 0
- drawH(0, x, y + SEG_PADDING);
- // G (Middle) - index 6
- drawH(6, x, y + H/2 - SEG_THICKNESS/2);
- // D (Bottom) - index 3
- drawH(3, x, y + H - SEG_PADDING - SEG_THICKNESS);
-
- // Vertical segments (Top Half)
- const topVStart = y + SEG_PADDING + SEG_THICKNESS;
- const rightVStart = x + W - SEG_PADDING - SEG_THICKNESS;
-
- // F (Top-Left) - index 5
- drawV(5, x + SEG_PADDING, topVStart);
-
- // B (Top-Right) - index 1
- drawV(1, rightVStart, topVStart);
-
- // Vertical segments (Bottom Half)
- const bottomVStart = y + H/2 + SEG_THICKNESS/2;
-
- // E (Bottom-Left) - index 4
- drawV(4, x + SEG_PADDING, bottomVStart);
-
- // C (Bottom-Right) - index 2
- drawV(2, rightVStart, bottomVStart);
- }
-
- // Function to draw the colon (two dots), now with blinking logic
- function drawColon(ctx, x, y, H, isVisible) {
- const dotSize = 4;
- ctx.fillStyle = COLOR_ACTIVE;
-
- if (isVisible) {
- // Top dot
- ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
- // Bottom dot
- ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
- } else {
- // Draw inactive colon if not visible, for consistency
- ctx.fillStyle = COLOR_INACTIVE;
- ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
- ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
- }
- }
-
- /**
- * Draws a simple playback arrow (triangle)
- * @param {CanvasRenderingContext2D} ctx
- * @param {number} x Left position of the arrow area.
- * @param {number} y Top position of the arrow area.
- * @param {number} H Total height of the arrow area.
- */
- function drawPlaybackArrow(ctx, x, y, H) {
- const arrowWidth = H * 0.4; // Arrow width relative to digit height
- const arrowHeight = H * 0.4; // Arrow height relative to digit height
-
- ctx.fillStyle = COLOR_ACTIVE;
- ctx.beginPath();
- ctx.moveTo(x, y + H * 0.5 - arrowHeight / 2); // Top point
- ctx.lineTo(x + arrowWidth, y + H * 0.5); // Right point (center)
- ctx.lineTo(x, y + H * 0.5 + arrowHeight / 2); // Bottom point
- ctx.closePath();
- ctx.fill();
- }
-
-
- // Main function to render the entire time string using segments
- function drawSegmentDisplay(ctx, timeString) {
- const canvasWidth = ctx.canvas.width;
- const canvasHeight = ctx.canvas.height;
- const timeStringLength = timeString.length;
-
- // Clear display to dark background
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(0, 0, canvasWidth, canvasHeight);
-
- // Constants for layout
- const charSpacing = 8; // Spacing between digits
- const digitHeight = canvasHeight - 2 * SEG_PADDING;
- const digitWidth = digitHeight / 2 + SEG_PADDING; // Total width slot for one digit
- const colonWidth = 6;
- const arrowWidth = digitHeight * 0.7; // Approx width for the arrow
- const arrowPadding = 10; // Space between arrow and first digit
-
- // Calculate total display width including arrow and spaces
- const totalDisplayWidth = arrowWidth + arrowPadding + (4 * digitWidth) + colonWidth + ((timeStringLength - 1) * charSpacing);
-
- // Calculate starting X to center the display
- let currentX = (canvasWidth - totalDisplayWidth) / 2;
- const currentY = SEG_PADDING;
-
- // Draw Playback Arrow
- if (isVideoLoaded && videoElement.readyState >= 3) {
- drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
- }
- currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
-
- for (let i = 0; i < timeStringLength; i++) {
- const char = timeString[i];
-
- if (char === ':') {
- drawColon(ctx, currentX, currentY, digitHeight, blinkState); // Pass blinkState
- currentX += colonWidth;
- } else if (char >= '0' && char <= '9') {
- drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
- currentX += digitWidth;
- }
-
- // Add spacing only if it's not the last element
- if (i < timeStringLength - 1) {
- currentX += charSpacing;
- }
- }
- }
-
- // --- VCR Display Functions ---
- function createVcrDisplay() {
- const canvas = document.createElement('canvas');
- canvas.width = 160; // Increased width for arrow and better spacing
- canvas.height = 32;
- const ctx = canvas.getContext('2d');
-
- ctx.fillStyle = '#0a0a0a';
- ctx.fillRect(0, 0, canvas.width, canvas.height);
-
- vcrDisplayTexture = new THREE.CanvasTexture(canvas);
- vcrDisplayTexture.needsUpdate = true;
-
- const displayGeometry = new THREE.PlaneGeometry(0.45, 0.1); // Adjust geometry width for new canvas size
- const displayMaterial = new THREE.MeshBasicMaterial({
- map: vcrDisplayTexture,
- side: THREE.FrontSide,
- color: 0xffffff,
- transparent: true,
- emissive: 0x00ff44,
- emissiveIntensity: 0.1
- });
-
- const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
- return displayMesh;
- }
-
- function updateVcrDisplay(time) {
- if (!vcrDisplayTexture) return;
-
- const canvas = vcrDisplayTexture.image;
- const ctx = canvas.getContext('2d');
-
- const timeString = formatTime(time);
-
- // Uses the new segment drawing function with ghosting, including blinkState for colon
- drawSegmentDisplay(ctx, timeString);
-
- vcrDisplayTexture.needsUpdate = true;
- }
-
- // --- VCR Model Function ---
- function createVcr() {
- // Materials
- const vcrBodyMaterial = new THREE.MeshPhongMaterial({
- color: 0x222222, // Dark metallic gray
- shininess: 70,
- specular: 0x444444
- });
- const slotMaterial = new THREE.MeshPhongMaterial({
- color: 0x0a0a0a, // Deep black
- shininess: 5,
- specular: 0x111111
- });
- const floorMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, shininess: 5 });
-
- // VCR Body
- const vcrBodyGeometry = new THREE.BoxGeometry(1.0, 0.2, 0.7);
- const vcrBody = new THREE.Mesh(vcrBodyGeometry, vcrBodyMaterial);
- vcrBody.position.y = 0; // Centered
- vcrBody.castShadow = true;
- vcrBody.receiveShadow = true;
-
- // Cassette Slot / Front Face
- const slotGeometry = new THREE.BoxGeometry(0.9, 0.05, 0.01);
- const slotMesh = new THREE.Mesh(slotGeometry, slotMaterial);
- slotMesh.position.set(0, -0.05, 0.35 + 0.005);
- slotMesh.castShadow = true;
- slotMesh.receiveShadow = true;
-
- // VCR Display
- const displayMesh = createVcrDisplay();
- displayMesh.position.z = 0.35 + 0.005;
- displayMesh.position.x = 0.2; // Adjusted X for arrow
- displayMesh.position.y = 0.03;
-
- // VCR Group
- const vcrGroup = new THREE.Group();
- vcrGroup.add(vcrBody, slotMesh, displayMesh);
- vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor
-
- // Light from the VCR display itself
- vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.5, 1);
- vcrDisplayLight.position.set(0.3, 0.03, 0.35 + 0.05); // Move light slightly closer to VCR surface
- vcrDisplayLight.castShadow = true;
- vcrDisplayLight.shadow.mapSize.width = 256;
- vcrDisplayLight.shadow.mapSize.height = 256;
- vcrGroup.add(vcrDisplayLight);
-
- return vcrGroup;
- }
-
- 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);
diff --git a/tv-player/src/effects_dust.js b/tv-player/src/effects_dust.js
new file mode 100644
index 0000000..69d55a6
--- /dev/null
+++ b/tv-player/src/effects_dust.js
@@ -0,0 +1,28 @@
+// --- 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
+ );
+ }
+ // 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
+ });
+
+ dust = new THREE.Points(particlesGeometry, particleMaterial);
+ // Dust particles generally don't cast or receive shadows in this context
+ scene.add(dust);
+}
\ No newline at end of file
diff --git a/tv-player/src/effects_flies.js b/tv-player/src/effects_flies.js
new file mode 100644
index 0000000..a21fdcb
--- /dev/null
+++ b/tv-player/src/effects_flies.js
@@ -0,0 +1,174 @@
+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;
+ }
+ }
+ });
+}
\ No newline at end of file
diff --git a/tv-player/src/global-variables.js b/tv-player/src/global-variables.js
index ab6455a..b024298 100644
--- a/tv-player/src/global-variables.js
+++ b/tv-player/src/global-variables.js
@@ -1,5 +1,10 @@
// --- Global Variables ---
let scene, camera, renderer, tvScreen, videoTexture, dust, screenLight, lampLightPoint, lampLightSpot;
+
+// VCR Display related variables
+let simulatedPlaybackTime = 0;
+let lastUpdateTime = -1;
+let baseTime = 0;
let isVideoLoaded = false;
let videoUrls = []; // Array to hold all video URLs
let currentVideoIndex = -1; // Index of the currently playing video
diff --git a/tv-player/src/scene.js b/tv-player/src/scene.js
index 42f4699..2641d73 100644
--- a/tv-player/src/scene.js
+++ b/tv-player/src/scene.js
@@ -74,3 +74,498 @@ function createRoomWalls() {
windowPane.position.set(-3.5, roomHeight * 0.5 + 1.5, windowZ);
scene.add(windowPane);
}
+
+function createBookshelf(x, z, rotationY, uniqueSeed) {
+ seed = uniqueSeed; // Reset seed for this specific shelf instance
+
+ const shelfHeight = 2.2;
+ const shelfDepth = 0.35;
+ const shelfWidth = 1.2;
+ const numShelves = 6;
+ const woodThickness = 0.04;
+ const woodColor = 0x5c4033; // Darker, richer wood
+
+ const shelfGroup = new THREE.Group();
+ shelfGroup.position.set(x, 0, z);
+ shelfGroup.rotation.y = rotationY;
+
+ const woodMaterial = new THREE.MeshPhongMaterial({ color: woodColor, shininess: 30 });
+
+ // 1. Build Frame (Hollow box)
+ // Back Panel
+ const backGeo = new THREE.BoxGeometry(shelfWidth, shelfHeight, woodThickness);
+ const backPanel = new THREE.Mesh(backGeo, woodMaterial);
+ backPanel.position.set(0, shelfHeight / 2, -shelfDepth / 2 + woodThickness / 2);
+ backPanel.castShadow = true;
+ backPanel.receiveShadow = true;
+ shelfGroup.add(backPanel);
+
+ // Side Panels (Left & Right)
+ const sideGeo = new THREE.BoxGeometry(woodThickness, shelfHeight, shelfDepth);
+ const leftSide = new THREE.Mesh(sideGeo, woodMaterial);
+ leftSide.position.set(-shelfWidth / 2 + woodThickness / 2, shelfHeight / 2, 0);
+ leftSide.castShadow = true;
+ leftSide.receiveShadow = true;
+ shelfGroup.add(leftSide);
+
+ const rightSide = new THREE.Mesh(sideGeo, woodMaterial);
+ rightSide.position.set(shelfWidth / 2 - woodThickness / 2, shelfHeight / 2, 0);
+ rightSide.castShadow = true;
+ rightSide.receiveShadow = true;
+ shelfGroup.add(rightSide);
+
+ // Top & Bottom Panels
+ const topBottomGeo = new THREE.BoxGeometry(shelfWidth, woodThickness, shelfDepth);
+ const bottomPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
+ bottomPanel.position.set(0, woodThickness / 2, 0);
+ bottomPanel.receiveShadow = true;
+ shelfGroup.add(bottomPanel);
+
+ const topPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
+ topPanel.position.set(0, shelfHeight - woodThickness / 2, 0);
+ topPanel.castShadow = true;
+ shelfGroup.add(topPanel);
+
+ landingSurfaces.push(topPanel);
+
+ // 2. Individual Shelves & Books
+ const internalHeight = shelfHeight - (2 * woodThickness);
+ const shelfSpacing = internalHeight / numShelves;
+ const internalWidth = shelfWidth - (2 * woodThickness);
+
+ for (let i = 0; i < numShelves; i++) {
+ const currentShelfY = woodThickness + (i * shelfSpacing);
+
+ // Shelf board (skip for the very bottom one as we have a bottom panel)
+ if (i > 0) {
+ const shelfBoard = new THREE.Mesh(
+ new THREE.BoxGeometry(internalWidth, woodThickness, shelfDepth - woodThickness), // Slightly shallower to fit inside back panel
+ woodMaterial
+ );
+ shelfBoard.position.set(0, currentShelfY, woodThickness / 2); // Offset forward slightly
+ shelfBoard.castShadow = true;
+ shelfBoard.receiveShadow = true;
+ shelfGroup.add(shelfBoard);
+ }
+
+ // 3. Procedural Books
+ let currentBookX = -internalWidth / 2 + 0.01; // Start at left inside edge
+ const shelfSurfaceY = currentShelfY + woodThickness / 2;
+
+ while (currentBookX < internalWidth / 2 - 0.05) {
+ // sizes vary
+ const bookWidth = 0.02 + seededRandom() * 0.05;
+ const bookHeight = (shelfSpacing * 0.6) + seededRandom() * (shelfSpacing * 0.1);
+ const bookDepth = 0.15 + seededRandom() * 0.03;
+
+ if (currentBookX + bookWidth > internalWidth / 2) break;
+
+ const bookColor = getRandomColor();
+ const bookMat = new THREE.MeshPhongMaterial({ color: bookColor, shininess: 60 });
+ const bookGeo = new THREE.BoxGeometry(bookWidth, bookHeight, bookDepth);
+ const book = new THREE.Mesh(bookGeo, bookMat);
+
+ // Position: Resting on shelf, pushed towards the back with slight random variation
+ const depthVariation = seededRandom() * 0.05;
+ book.position.set(
+ currentBookX + bookWidth / 2,
+ shelfSurfaceY + bookHeight / 2,
+ -shelfDepth / 2 + woodThickness + bookDepth / 2 + depthVariation
+ );
+
+ book.castShadow = true;
+ book.receiveShadow = true;
+ shelfGroup.add(book);
+
+ currentBookX += bookWidth + 0.002; // Tiny gap between books
+
+ if (seededRandom() > 0.92) {
+ currentBookX += bookWidth * 3; // random bigger gaps
+ }
+ }
+ }
+
+ scene.add(shelfGroup);
+}
+
+function createDoor(x, z, rotY) {
+ const doorGroup = new THREE.Group();
+ doorGroup.position.set(x, 1.1, z); // Centered vertically for a 2.2m door
+ doorGroup.rotation.set(0, rotY, 0);
+
+ // Door Frame
+ const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x473e3a }); // Dark wood for frame
+ const frameTop = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.1, 0.15), frameMaterial);
+ frameTop.position.set(0, 1.15, 0);
+ frameTop.castShadow = true;
+ doorGroup.add(frameTop);
+
+ const frameLeft = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
+ frameLeft.position.set(-0.55, 0.05, 0);
+ frameLeft.castShadow = true;
+ doorGroup.add(frameLeft);
+
+ const frameRight = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
+ frameRight.position.set(0.55, 0.05, 0);
+ frameRight.castShadow = true;
+ doorGroup.add(frameRight);
+
+ // Main Door Panel
+ const doorMaterial = new THREE.MeshPhongMaterial({ color: 0x8b5a2b, shininess: 10 }); // Lighter wood for door
+ const door = new THREE.Mesh(new THREE.BoxGeometry(1.0, 2.2, 0.08), doorMaterial);
+ door.castShadow = true;
+ door.receiveShadow = true;
+ doorGroup.add(door);
+
+ // Door Knob
+ const knobMaterial = new THREE.MeshPhongMaterial({ color: 0xd4af37, shininess: 100 }); // Gold/Brass
+ const knob = new THREE.Mesh(new THREE.SphereGeometry(0.05, 16, 16), knobMaterial);
+ knob.position.set(0.4, 0, 0.06); // Position on the right side of the door
+ knob.castShadow = true;
+ doorGroup.add(knob);
+
+ scene.add(doorGroup);
+}
+
+// --- VCR Model Function ---
+function createVcr() {
+ // Materials
+ const vcrBodyMaterial = new THREE.MeshPhongMaterial({
+ color: 0x222222, // Dark metallic gray
+ shininess: 70,
+ specular: 0x444444
+ });
+ const slotMaterial = new THREE.MeshPhongMaterial({
+ color: 0x0a0a0a, // Deep black
+ shininess: 5,
+ specular: 0x111111
+ });
+
+ // VCR Body
+ const vcrBodyGeometry = new THREE.BoxGeometry(1.0, 0.2, 0.7);
+ const vcrBody = new THREE.Mesh(vcrBodyGeometry, vcrBodyMaterial);
+ vcrBody.position.y = 0; // Centered
+ vcrBody.castShadow = true;
+ vcrBody.receiveShadow = true;
+
+ // Cassette Slot / Front Face
+ const slotGeometry = new THREE.BoxGeometry(0.9, 0.05, 0.01);
+ const slotMesh = new THREE.Mesh(slotGeometry, slotMaterial);
+ slotMesh.position.set(0, -0.05, 0.35 + 0.005);
+ slotMesh.castShadow = true;
+ slotMesh.receiveShadow = true;
+
+ // VCR Display
+ const displayMesh = createVcrDisplay();
+ displayMesh.position.z = 0.35 + 0.005;
+ displayMesh.position.x = 0.2; // Adjusted X for arrow
+ displayMesh.position.y = 0.03;
+
+ // VCR Group
+ const vcrGroup = new THREE.Group();
+ vcrGroup.add(vcrBody, slotMesh, displayMesh);
+ vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor
+
+ // Light from the VCR display itself
+ vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.5, 1);
+ vcrDisplayLight.position.set(0.3, 0.03, 0.35 + 0.05); // Move light slightly closer to VCR surface
+ vcrDisplayLight.castShadow = true;
+ vcrDisplayLight.shadow.mapSize.width = 256;
+ vcrDisplayLight.shadow.mapSize.height = 256;
+ vcrGroup.add(vcrDisplayLight);
+
+ return vcrGroup;
+}
+
+function createTvSet(x, z, rotY) {
+ // --- Materials (MeshPhongMaterial) ---
+ const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
+ const darkMetal = new THREE.MeshPhongMaterial({
+ color: 0x6b6b6b,
+ shininess: 80,
+ specular: 0x888888
+ });
+ const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x4d4d4d, shininess: 30 });
+
+ const tvGroup = new THREE.Group();
+
+ // --- TV Table Dimensions & Material ---
+ const woodColor = 0x5a3e36; // Dark brown wood
+ const tableHeight = 0.7; // Height from floor to top surface
+ const tableWidth = 2.0;
+ const tableDepth = 1.0;
+ const legThickness = 0.05;
+ const shelfThickness = 0.03;
+ // Use standard material for realistic shadowing
+ const material = new THREE.MeshStandardMaterial({ color: woodColor, roughness: 0.8, metalness: 0.1 });
+
+ // VCR gap dimensions calculation
+ const shelfGap = 0.2; // Height of the VCR opening
+ const shelfY = tableHeight - shelfGap - (shelfThickness / 2); // Y position of the bottom shelf
+
+
+ // 2. Table Top
+ const topGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
+ const tableTop = new THREE.Mesh(topGeometry, material);
+ tableTop.position.set(0, tableHeight, 0);
+ tableTop.castShadow = true;
+ tableTop.receiveShadow = true;
+ tvGroup.add(tableTop);
+
+ // 3. VCR Shelf (Middle Shelf)
+ const shelfGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
+ const vcrShelf = new THREE.Mesh(shelfGeometry, material);
+ vcrShelf.position.set(0, shelfY, 0);
+ vcrShelf.castShadow = true;
+ vcrShelf.receiveShadow = true;
+ tvGroup.add(vcrShelf);
+
+ // 4. Side Walls for VCR Compartment (NEW CODE)
+ const wallHeight = shelfGap; // Height is the gap itself
+ const wallThickness = shelfThickness; // Reuse the shelf thickness for the wall width/depth
+ const wallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, tableDepth);
+
+ // Calculate the Y center position for the wall
+ const wallYCenter = tableHeight - (shelfThickness / 2) - (wallHeight / 2);
+
+ // Calculate the X position to be flush with the table sides
+ const wallXPosition = (tableWidth / 2) - (wallThickness / 2);
+
+ // Left Wall
+ const sideWallLeft = new THREE.Mesh(wallGeometry, material);
+ sideWallLeft.position.set(-wallXPosition, wallYCenter, 0);
+ sideWallLeft.castShadow = true;
+ sideWallLeft.receiveShadow = true;
+ tvGroup.add(sideWallLeft);
+
+ // Right Wall
+ const sideWallRight = new THREE.Mesh(wallGeometry, material);
+ sideWallRight.position.set(wallXPosition, wallYCenter, 0);
+ sideWallRight.castShadow = true;
+ sideWallRight.receiveShadow = true;
+ tvGroup.add(sideWallRight);
+
+ // 5. Legs
+ const legHeight = shelfY; // Legs go from the floor (y=0) to the shelf (y=shelfY)
+ const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
+
+ // Utility function to create and position a leg
+ const createLeg = (x, z) => {
+ const leg = new THREE.Mesh(legGeometry, material);
+ // Position the leg so the center is at half its height
+ leg.position.set(x, legHeight / 2, z);
+ leg.castShadow = true;
+ leg.receiveShadow = true;
+ return leg;
+ };
+
+ // Calculate offsets for positioning the legs near the corners
+ const offset = (tableWidth / 2) - (legThickness * 2);
+ const depthOffset = (tableDepth / 2) - (legThickness * 2);
+
+ // Front Left
+ tvGroup.add(createLeg(-offset, depthOffset));
+ // Front Right
+ tvGroup.add(createLeg(offset, depthOffset));
+ // Back Left
+ tvGroup.add(createLeg(-offset, -depthOffset));
+ // Back Right
+ tvGroup.add(createLeg(offset, -depthOffset));
+
+ // --- 2. The TV box ---
+ const cabinetGeometry = new THREE.BoxGeometry(1.75, 1.5, 1.0);
+ const cabinet = new THREE.Mesh(cabinetGeometry, tvPlastic);
+ cabinet.position.y = 1.51;
+ cabinet.castShadow = true;
+ cabinet.receiveShadow = true;
+ tvGroup.add(cabinet);
+
+ // --- 3. Screen Frame ---
+ const frameGeometry = new THREE.BoxGeometry(1.5, 1.3, 0.1);
+ const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 20 });
+ const frame = new THREE.Mesh(frameGeometry, frameMaterial);
+ frame.position.set(0, 1.5, 0.68);
+ frame.castShadow = true;
+ frame.receiveShadow = true;
+ tvGroup.add(frame);
+
+ // --- 4. Curved Screen (CRT Effect) ---
+ const screenRadius = 3.0; // Radius for the subtle curve
+ const screenWidth = 1.4;
+ const screenHeight = 1.2;
+ const thetaLength = screenWidth / screenRadius; // Calculate angle needed for the arc
+
+ // Use CylinderGeometry as a segment
+ const screenGeometry = new THREE.CylinderGeometry(
+ screenRadius, screenRadius,
+ screenHeight, // Cylinder height is the vertical dimension of the screen
+ 32,
+ 1,
+ true,
+ (Math.PI / 2) - (thetaLength / 2), // Start angle to center the arc
+ thetaLength // Arc length (width)
+ );
+
+ // Rotate the cylinder segment:
+ // 1. Rotate around X-axis by 90 degrees to lay the height (Y) along Z (depth).
+ //screenGeometry.rotateX(Math.PI / 2);
+ // 2. Rotate around Y-axis by 90 degrees to align the segment's arc across the X-axis (width).
+ screenGeometry.rotateY(-Math.PI/2);
+
+ const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
+ tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
+
+ // Position the curved screen
+ tvScreen.position.set(0.0, 1.5, -2.1);
+ tvScreen.material = new THREE.MeshPhongMaterial({
+ color: 0x0a0a0a, // Deep black
+ shininess: 5,
+ specular: 0x111111
+ });
+ tvScreen.material.needsUpdate = true;
+ tvGroup.add(tvScreen);
+
+ tvGroup.position.set(x, 0, z);
+ tvGroup.rotation.y = rotY;
+
+ // Light from the screen (initially low intensity, will increase when video loads)
+ screenLight = new THREE.PointLight(0xffffff, 0, 10);
+ screenLight.position.set(0, 1.5, 1.0);
+ // Screen light casts shadows
+ screenLight.castShadow = true;
+ screenLight.shadow.mapSize.width = 1024;
+ screenLight.shadow.mapSize.height = 1024;
+ screenLight.shadow.camera.near = 0.2;
+ screenLight.shadow.camera.far = 5;
+ tvGroup.add(screenLight);
+
+ // -- VCR --
+ const vcr = createVcr();
+ vcr.position.set(-0.3, 0.6, 0.05);
+ tvGroup.add(vcr);
+
+ scene.add(tvGroup);
+}
+
+// --- Scene Modeling Function ---
+function createSceneObjects() {
+ // --- Materials (MeshPhongMaterial) ---
+ const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
+ const darkMetal = new THREE.MeshPhongMaterial({
+ color: 0x6b6b6b,
+ shininess: 80,
+ specular: 0x888888
+ });
+ const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x2d251e, shininess: 10 });
+
+ // --- 1. Floor ---
+ const floorGeometry = new THREE.PlaneGeometry(20, 20);
+ const floorTexture = loader.load('./textures/floor.jpg');
+ floorTexture.wrapS = THREE.RepeatWrapping;
+ floorTexture.wrapT = THREE.RepeatWrapping;
+ floorTexture.repeat.set(roomSize, roomSize);
+ const floorMaterial = new THREE.MeshPhongMaterial({ map: floorTexture, color: 0x555555, shininess: 5 });
+ const floor = new THREE.Mesh(floorGeometry, floorMaterial);
+ floor.rotation.x = -Math.PI / 2;
+ floor.position.y = 0;
+ 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) ---
+ const lampBase = new THREE.CylinderGeometry(0.05, 0.2, 0.1, 12);
+ const lampPole = new THREE.CylinderGeometry(0.02, 0.02, 1.5, 8);
+ const lampShade = new THREE.ConeGeometry(0.2, 0.4, 16);
+
+ const baseMesh = new THREE.Mesh(lampBase, darkMetal);
+ const poleMesh = new THREE.Mesh(lampPole, darkMetal);
+ const shadeMesh = new THREE.Mesh(lampShade, darkMetal);
+
+ // Ensure lamp parts cast shadows
+ baseMesh.castShadow = true; baseMesh.receiveShadow = true;
+ poleMesh.castShadow = true; poleMesh.receiveShadow = true;
+ //shadeMesh.castShadow = true; shadeMesh.receiveShadow = true;
+
+ baseMesh.position.y = -0.6;
+ poleMesh.position.y = 0.0;
+ shadeMesh.position.y = 0.8 + 0.1;
+ shadeMesh.rotation.x = Math.PI;
+
+ // Lamp Light (Warm Glow) - Configured to cast shadows
+ lampLightPoint = new THREE.PointLight(0xffaa00, originalLampIntensity, 4);
+ lampLightPoint.position.set(-0.01, roomHeight-0.9, 0.01);
+ lampLightPoint.castShadow = true;
+ // Optimization: Reduced map size and far plane to ease resource burden
+ lampLightPoint.shadow.mapSize.width = 512;
+ lampLightPoint.shadow.mapSize.height = 512;
+ lampLightPoint.shadow.camera.near = 0.1;
+ lampLightPoint.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
+ lampLightPoint.penumbra = 0.5;
+
+ lampLightSpot = new THREE.SpotLight(0xffaa00, originalLampIntensity, 4);
+ lampLightSpot.position.set(-0.01, 1.0, 0.01);
+ lampLightSpot.target.position.set(0, 5, 0);
+ lampLightSpot.castShadow = true;
+ // Optimization: Reduced map size and far plane to ease resource burden
+ lampLightSpot.shadow.mapSize.width = 512;
+ lampLightSpot.shadow.mapSize.height = 512;
+ lampLightSpot.shadow.camera.near = 0.1;
+ lampLightSpot.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
+ lampLightSpot.penumbra = 0.5;
+
+ const lampGroup = new THREE.Group();
+ lampGroup.add(baseMesh, poleMesh, shadeMesh, lampLightSpot, lampLightSpot.target, lampLightPoint);
+ lampGroup.position.set(0.8, 0.7, -roomSize/2+0.5);
+
+ scene.add(lampGroup);
+
+ landingSurfaces.push(shadeMesh);
+
+ // --- 7. Old Camera (On the table) ---
+ const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.15);
+ const cameraLens = new THREE.CylinderGeometry(0.08, 0.08, 0.05, 12);
+ const cameraMaterial = new THREE.MeshPhongMaterial({
+ color: 0x333333,
+ shininess: 50,
+ specular: 0x444444
+ });
+
+ const cameraMesh = new THREE.Mesh(cameraBody, cameraMaterial);
+ const lensMesh = new THREE.Mesh(cameraLens, cameraMaterial);
+ lensMesh.position.z = 0.15;
+ lensMesh.rotation.x = Math.PI/2;
+
+ cameraMesh.add(lensMesh);
+ cameraMesh.position.set(-1.7, 0.15, 0.4);
+ cameraMesh.rotation.y = -Math.PI / 10;
+ cameraMesh.castShadow = true; cameraMesh.receiveShadow = true;
+ scene.add(cameraMesh);
+
+ // --- 8. Pizza Box ---
+ const boxGeometry = new THREE.BoxGeometry(0.5, 0.05, 0.5);
+ const boxMaterial = new THREE.MeshPhongMaterial({ color: 0xe0c896, shininess: 5 });
+ const pizzaBox = new THREE.Mesh(boxGeometry, boxMaterial);
+ pizzaBox.position.set(-1.8, 0.025, -0.8);
+ pizzaBox.rotation.y = Math.PI / 5;
+ pizzaBox.castShadow = true; pizzaBox.receiveShadow = true;
+ scene.add(pizzaBox);
+
+ // --- 8. Cassette ---
+ const cassetteGeometry = new THREE.BoxGeometry(0.2, 0.05, 0.45);
+ const cassetteMaterial = new THREE.MeshPhongMaterial({ color: 0xe0c896, shininess: 5 });
+ const cassette = new THREE.Mesh(cassetteGeometry, cassetteMaterial);
+ cassette.position.set(-0.5, 0.025, -1.4);
+ cassette.rotation.y = Math.PI / 3;
+ cassette.castShadow = true; cassette.receiveShadow = true;
+ scene.add(cassette);
+
+ createDoor(roomSize/2, -roomSize/2 * 0.5, -Math.PI/2);
+ 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();
+}
diff --git a/tv-player/src/vcr-display.js b/tv-player/src/vcr-display.js
new file mode 100644
index 0000000..7c31dc2
--- /dev/null
+++ b/tv-player/src/vcr-display.js
@@ -0,0 +1,216 @@
+let vcrDisplayTexture;
+let blinkState = false;
+let lastBlinkToggleTime = 0;
+// --- Segment Display Definitions ---
+
+// Define which segments (indexed 0-6: A, B, C, D, E, F, G) are active for each digit
+// A=Top, B=TR, C=BR, D=Bottom, E=BL, F=TL, G=Middle
+const SEGMENTS = {
+ '0': [1, 1, 1, 1, 1, 1, 0],
+ '1': [0, 1, 1, 0, 0, 0, 0],
+ '2': [1, 1, 0, 1, 1, 0, 1],
+ '3': [1, 1, 1, 1, 0, 0, 1],
+ '4': [0, 1, 1, 0, 0, 1, 1],
+ '5': [1, 0, 1, 1, 0, 1, 1],
+ '6': [1, 0, 1, 1, 1, 1, 1],
+ '7': [1, 1, 1, 0, 0, 0, 0],
+ '8': [1, 1, 1, 1, 1, 1, 1],
+ '9': [1, 1, 1, 1, 0, 1, 1],
+ ' ': [0, 0, 0, 0, 0, 0, 0]
+};
+
+const SEG_THICKNESS = 3; // Thickness of the segment line in canvas pixels
+const SEG_PADDING = 2; // Padding within a digit segment's box
+
+// Colors for active and inactive segments
+const COLOR_ACTIVE = '#00ff44'; // Bright Fluorescent Green
+const COLOR_INACTIVE = '#1a1a1a'; // Dim dark gray for 'ghost' segments
+
+/**
+ * Draws a single 7-segment digit by drawing active segments.
+ * Now includes drawing of inactive (ghost) segments for better readability.
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {string} digit The digit character (0-9).
+ * @param {number} x Left position of the digit area.
+ * @param {number} y Top position of the digit area.
+ * @param {number} H Total height of the digit area.
+ */
+function drawSegmentDigit(ctx, digit, x, y, H) {
+ const segments = SEGMENTS[digit] || SEGMENTS[' '];
+ const W = H / 2; // Width is half the height for standard aspect ratio
+
+ // Segment dimensions relative to W and H
+ const hLength = W - 2 * SEG_PADDING;
+ // Vertical length calculation: (Total height - 2 paddings - 3 horizontal thicknesses) / 2
+ const vLength = (H - (2 * SEG_PADDING) - (3 * SEG_THICKNESS)) / 2;
+
+ // Helper to draw horizontal segment (A, G, D)
+ const drawH = (index, x_start, y_start) => {
+ ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
+ ctx.fillRect(x_start + SEG_PADDING, y_start, hLength, SEG_THICKNESS);
+ };
+
+ // Helper to draw vertical segment (F, B, E, C)
+ const drawV = (index, x_start, y_start) => {
+ ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
+ ctx.fillRect(x_start, y_start, SEG_THICKNESS, vLength);
+ };
+
+ // Define segment positions
+
+ // Horizontal segments
+ // A (Top) - index 0
+ drawH(0, x, y + SEG_PADDING);
+ // G (Middle) - index 6
+ drawH(6, x, y + H/2 - SEG_THICKNESS/2);
+ // D (Bottom) - index 3
+ drawH(3, x, y + H - SEG_PADDING - SEG_THICKNESS);
+
+ // Vertical segments (Top Half)
+ const topVStart = y + SEG_PADDING + SEG_THICKNESS;
+ const rightVStart = x + W - SEG_PADDING - SEG_THICKNESS;
+
+ // F (Top-Left) - index 5
+ drawV(5, x + SEG_PADDING, topVStart);
+
+ // B (Top-Right) - index 1
+ drawV(1, rightVStart, topVStart);
+
+ // Vertical segments (Bottom Half)
+ const bottomVStart = y + H/2 + SEG_THICKNESS/2;
+
+ // E (Bottom-Left) - index 4
+ drawV(4, x + SEG_PADDING, bottomVStart);
+
+ // C (Bottom-Right) - index 2
+ drawV(2, rightVStart, bottomVStart);
+}
+
+// Function to draw the colon (two dots), now with blinking logic
+function drawColon(ctx, x, y, H, isVisible) {
+ const dotSize = 4;
+ ctx.fillStyle = COLOR_ACTIVE;
+
+ if (isVisible) {
+ // Top dot
+ ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
+ // Bottom dot
+ ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
+ } else {
+ // Draw inactive colon if not visible, for consistency
+ ctx.fillStyle = COLOR_INACTIVE;
+ ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
+ ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
+ }
+}
+
+/**
+ * Draws a simple playback arrow (triangle)
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {number} x Left position of the arrow area.
+ * @param {number} y Top position of the arrow area.
+ * @param {number} H Total height of the arrow area.
+ */
+function drawPlaybackArrow(ctx, x, y, H) {
+ const arrowWidth = H * 0.4; // Arrow width relative to digit height
+ const arrowHeight = H * 0.4; // Arrow height relative to digit height
+
+ ctx.fillStyle = COLOR_ACTIVE;
+ ctx.beginPath();
+ ctx.moveTo(x, y + H * 0.5 - arrowHeight / 2); // Top point
+ ctx.lineTo(x + arrowWidth, y + H * 0.5); // Right point (center)
+ ctx.lineTo(x, y + H * 0.5 + arrowHeight / 2); // Bottom point
+ ctx.closePath();
+ ctx.fill();
+}
+
+
+// Main function to render the entire time string using segments
+function drawSegmentDisplay(ctx, timeString) {
+ const canvasWidth = ctx.canvas.width;
+ const canvasHeight = ctx.canvas.height;
+ const timeStringLength = timeString.length;
+
+ // Clear display to dark background
+ ctx.fillStyle = '#0a0a0a';
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // Constants for layout
+ const charSpacing = 8; // Spacing between digits
+ const digitHeight = canvasHeight - 2 * SEG_PADDING;
+ const digitWidth = digitHeight / 2 + SEG_PADDING; // Total width slot for one digit
+ const colonWidth = 6;
+ const arrowWidth = digitHeight * 0.7; // Approx width for the arrow
+ const arrowPadding = 10; // Space between arrow and first digit
+
+ // Calculate total display width including arrow and spaces
+ const totalDisplayWidth = arrowWidth + arrowPadding + (4 * digitWidth) + colonWidth + ((timeStringLength - 1) * charSpacing);
+
+ // Calculate starting X to center the display
+ let currentX = (canvasWidth - totalDisplayWidth) / 2;
+ const currentY = SEG_PADDING;
+
+ // Draw Playback Arrow
+ if (isVideoLoaded && videoElement.readyState >= 3) {
+ drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
+ }
+ currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
+
+ for (let i = 0; i < timeStringLength; i++) {
+ const char = timeString[i];
+
+ if (char === ':') {
+ drawColon(ctx, currentX, currentY, digitHeight, blinkState); // Pass blinkState
+ currentX += colonWidth;
+ } else if (char >= '0' && char <= '9') {
+ drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
+ currentX += digitWidth;
+ }
+
+ // Add spacing only if it's not the last element
+ if (i < timeStringLength - 1) {
+ currentX += charSpacing;
+ }
+ }
+}
+
+// --- VCR Display Functions ---
+function createVcrDisplay() {
+ const canvas = document.createElement('canvas');
+ canvas.width = 160; // Increased width for arrow and better spacing
+ canvas.height = 32;
+ const ctx = canvas.getContext('2d');
+
+ ctx.fillStyle = '#0a0a0a';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ vcrDisplayTexture = new THREE.CanvasTexture(canvas);
+ vcrDisplayTexture.needsUpdate = true;
+
+ const displayGeometry = new THREE.PlaneGeometry(0.45, 0.1); // Adjust geometry width for new canvas size
+ const displayMaterial = new THREE.MeshBasicMaterial({
+ map: vcrDisplayTexture,
+ side: THREE.FrontSide,
+ color: 0xffffff,
+ transparent: true,
+ emissive: 0x00ff44,
+ emissiveIntensity: 0.1
+ });
+
+ const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
+ return displayMesh;
+}
+
+function updateVcrDisplay(time) {
+ if (!vcrDisplayTexture) return;
+
+ const canvas = vcrDisplayTexture.image;
+ const ctx = canvas.getContext('2d');
+
+ const timeString = formatTime(time);
+
+ // Uses the new segment drawing function with ghosting, including blinkState for colon
+ drawSegmentDisplay(ctx, timeString);
+
+ vcrDisplayTexture.needsUpdate = true;
+}