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; +}