music-video-gen/tv-player/index.html
2025-11-08 16:05:55 +01:00

691 lines
30 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Retro TV Player</title>
<!-- Load Tailwind CSS for styling --><script src="https://cdn.tailwindcss.com"></script>
<script>
// Configure Tailwind for the button
tailwind.config = {
theme: {
extend: {
colors: {
'tape-red': '#cc3333',
},
}
}
}
</script>
<!-- Load Three.js for 3D rendering --><script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* Dark room aesthetic */
body {
background-color: #0d0d10;
margin: 0;
overflow: hidden;
font-family: 'Inter', sans-serif;
}
canvas {
display: block;
}
/* Custom styles for the Load Tape button */
.tape-button {
transition: all 0.2s;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.tape-button:active {
transform: translateY(1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
</style>
</head>
<body>
<!-- Hidden Video Element --><video id="video" playsinline muted class="hidden"></video>
<!-- Controls for loading video --><div id="controls" class="fixed bottom-4 left-1/2 transform -translate-x-1/2 z-20 flex flex-col items-center space-y-2">
<!-- Hidden File Input that will be triggered by the button --><input type="file" id="fileInput" accept="video/mp4" class="hidden" multiple>
<div class="flex space-x-4">
<!-- Load Tapes Button --><button id="loadTapeButton" class="tape-button px-8 py-3 bg-tape-red text-white font-bold text-lg uppercase tracking-wider rounded-lg hover:bg-red-700 transition duration-150">
Load tapes
</button>
<!-- Next Tape Button (still allows manual skip) --><button id="nextTapeButton" class="tape-button px-6 py-3 bg-gray-600 text-white font-bold text-lg uppercase tracking-wider rounded-lg opacity-50 cursor-not-allowed" disabled>
Next (0/0)
</button>
</div>
<!-- Status message area --><p id="status" class="text-sm text-yellow-300 text-center font-mono opacity-80">Ready.</p>
</div>
<!-- 3D Canvas will be injected here by Three.js --><script>
// --- Global Variables ---
let scene, camera, renderer, tvScreen, videoTexture, dust, screenLight, lampLight;
let isVideoLoaded = false;
let videoUrls = []; // Array to hold all video URLs
let currentVideoIndex = -1; // Index of the currently playing video
const originalLampIntensity = 1.5; // Base intensity for the flickering lamp
const originalScreenIntensity = 1.5; // Base intensity for the screen glow
const container = document.body;
const videoElement = document.getElementById('video');
const fileInput = document.getElementById('fileInput');
const statusText = document.getElementById('status');
const loadTapeButton = document.getElementById('loadTapeButton');
const nextTapeButton = document.getElementById('nextTapeButton');
const loader = new THREE.TextureLoader();
const debugLight = false;
// --- Initialization ---
function init() {
// 1. Scene Setup (Dark, Ambient)
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
// 2. Camera Setup
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 1.5, 4);
// 3. Renderer Setup
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// Enable shadows on the renderer
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
container.appendChild(renderer.domElement);
// 4. Lighting (Minimal and focused)
const ambientLight = new THREE.AmbientLight(0x111111);
scene.add(ambientLight);
// Light from the screen (initially low intensity, will increase when video loads)
screenLight = new THREE.PointLight(0xffffff, 0.1, 10);
screenLight.position.set(0, 1.7, 1.2);
// 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 = 10;
scene.add(screenLight);
// 5. Build the entire scene with TV and surrounding objects
createSceneObjects();
// 6. Create the Dust Particle System
createDust();
// 7. Create the Room Walls and Ceiling
createRoomWalls();
// --- 8. Debug Visualization Helpers ---
// Visual aids for the light source positions
if (debugLight && THREE.PointLightHelper) {
const screenHelper = new THREE.PointLightHelper(screenLight, 0.1, 0xff0000); // Red for screen
scene.add(screenHelper);
// Lamp Helper will now work since lampLight is added to the scene
const lampHelper = new THREE.PointLightHelper(lampLight, 0.1, 0x00ff00); // Green for lamp
scene.add(lampHelper);
}
// 9. Event Listeners
window.addEventListener('resize', onWindowResize, false);
fileInput.addEventListener('change', loadVideoFile);
// Button logic
loadTapeButton.addEventListener('click', () => {
fileInput.click();
});
nextTapeButton.addEventListener('click', playNextVideo);
// Auto-advance to the next video when the current one finishes.
videoElement.addEventListener('ended', playNextVideo);
// Start the animation loop
animate();
}
// --- Room Walls Function ---
function createRoomWalls() {
const wallTexture = loader.load('./textures/wall.jpg');
wallTexture.wrapS = THREE.RepeatWrapping;
wallTexture.wrapT = THREE.RepeatWrapping;
// USING MeshPhongMaterial for specular highlights on walls
const wallMaterial = new THREE.MeshPhongMaterial({
map: wallTexture,
side: THREE.FrontSide,
shininess: 5,
specular: 0x111111 // Subtle reflection
});
const roomSize = 15;
const roomHeight = 8;
// 1. Back Wall (behind the TV)
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial);
backWall.position.set(0, roomHeight / 2, -roomSize / 2);
backWall.receiveShadow = true;
scene.add(backWall);
// 2. Front Wall (behind the camera)
const frontWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial);
frontWall.position.set(0, roomHeight / 2, roomSize / 2);
frontWall.rotation.y = Math.PI;
frontWall.receiveShadow = true;
scene.add(frontWall);
// 3. Left Wall
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial);
leftWall.rotation.y = Math.PI / 2;
leftWall.position.set(-roomSize / 2, roomHeight / 2, 0);
leftWall.receiveShadow = true;
scene.add(leftWall);
// 4. Right Wall
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial);
rightWall.rotation.y = -Math.PI / 2;
rightWall.position.set(roomSize / 2, roomHeight / 2, 0);
rightWall.receiveShadow = true;
scene.add(rightWall);
// 5. Ceiling
const ceilingGeometry = new THREE.PlaneGeometry(roomSize, roomSize);
const ceilingTexture = wallTexture;
ceilingTexture.repeat.set(4, 4);
// USING MeshPhongMaterial
const ceilingMaterial = new THREE.MeshPhongMaterial({
map: ceilingTexture,
side: THREE.FrontSide,
shininess: 5,
specular: 0x111111
});
const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
ceiling.rotation.x = Math.PI / 2;
ceiling.position.set(0, roomHeight, 0);
ceiling.receiveShadow = true;
scene.add(ceiling);
// --- 6. Add a Window to the Back Wall ---
const windowWidth = 1.5;
const windowHeight = 1.2;
const windowGeometry = new THREE.PlaneGeometry(windowWidth, windowHeight);
const nightSkyMaterial = new THREE.MeshBasicMaterial({
color: 0x0a1a3a,
emissive: 0x0a1a3a,
emissiveIntensity: 0.5,
side: THREE.FrontSide
});
const windowPane = new THREE.Mesh(windowGeometry, nightSkyMaterial);
const windowZ = -roomSize / 2 + 0.001;
windowPane.position.set(-3.5, roomHeight * 0.5 + 1.5, windowZ);
scene.add(windowPane);
// USING MeshPhongMaterial for the frame
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x4d3934, shininess: 10 });
// Horizontal bar
const hBarGeometry = new THREE.BoxGeometry(windowWidth + 0.1, 0.05, 0.05);
const hBar = new THREE.Mesh(hBarGeometry, frameMaterial);
hBar.position.set(windowPane.position.x, windowPane.position.y, windowZ);
hBar.castShadow = true;
scene.add(hBar);
// Vertical bar
const vBarGeometry = new THREE.BoxGeometry(0.05, windowHeight + 0.1, 0.05);
const vBar = new THREE.Mesh(vBarGeometry, frameMaterial);
vBar.position.set(windowPane.position.x, windowPane.position.y, windowZ);
vBar.castShadow = true;
scene.add(vBar);
}
// --- 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 floorMaterial = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, 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);
// --- 2. Table (TV stand) ---
const tableGeometry = new THREE.BoxGeometry(4.0, 0.7, 2.5);
const table = new THREE.Mesh(tableGeometry, darkWood);
table.position.y = 0.35;
table.castShadow = true;
table.receiveShadow = true;
scene.add(table);
// --- 3. The TV Set ---
const cabinetGeometry = new THREE.BoxGeometry(2.8, 2, 1.5);
const cabinet = new THREE.Mesh(cabinetGeometry, tvPlastic);
cabinet.position.y = 1.7;
cabinet.castShadow = true;
cabinet.receiveShadow = true;
scene.add(cabinet);
// Screen Frame
const frameGeometry = new THREE.BoxGeometry(2.3, 1.6, 0.2);
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 20 });
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
frame.position.set(0, 1.7, 0.68);
frame.castShadow = true;
frame.receiveShadow = true;
scene.add(frame);
// --- 4. Curved Screen (CRT Effect) ---
const screenRadius = 3.0; // Radius for the subtle curve
const screenWidth = 2.3;
const screenHeight = 1.5;
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, 1.7, -2);
scene.add(tvScreen);
// --- 5. Lamp (On the table, right side) ---
const lampBase = new THREE.CylinderGeometry(0.1, 0.2, 0.1, 12);
const lampPole = new THREE.CylinderGeometry(0.05, 0.05, 0.6, 8);
const lampShade = new THREE.ConeGeometry(0.3, 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;
poleMesh.position.y = 0.3;
shadeMesh.position.y = 0.8 + 0.1;
shadeMesh.rotation.x = Math.PI;
const lampGroup = new THREE.Group();
lampGroup.add(baseMesh, poleMesh, shadeMesh);
lampGroup.position.set(2.5, 0.7, -0.6);
// Lamp Light (Warm Glow) - Configured to cast shadows
lampLight = new THREE.PointLight(0xffaa00, originalLampIntensity, 4);
lampLight.position.set(2.5, 1.35, -0.6);
lampLight.castShadow = true;
// Optimization: Reduced map size and far plane to ease resource burden
lampLight.shadow.mapSize.width = 512;
lampLight.shadow.mapSize.height = 512;
lampLight.shadow.camera.near = 0.1;
lampLight.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
// Crucially, add the lamp group and its light to the scene
scene.add(lampGroup, lampLight);
// --- 6. Vase with a Flower (On the table, left side) ---
const vaseGeometry = new THREE.CylinderGeometry(0.2, 0.15, 0.4, 12);
const vaseMaterial = new THREE.MeshPhongMaterial({ color: 0x356644, shininess: 15 });
const vase = new THREE.Mesh(vaseGeometry, vaseMaterial);
vase.position.set(0, -0.2, 0);
vase.castShadow = true; vase.receiveShadow = true;
// Flower
const flowerStem = new THREE.CylinderGeometry(0.01, 0.01, 0.3, 8);
const flowerHead = new THREE.SphereGeometry(0.08, 10, 10);
const stemMaterial = new THREE.MeshPhongMaterial({ color: 0x228B22, shininess: 10 });
const headMaterial = new THREE.MeshPhongMaterial({ color: 0xdd2222, shininess: 30 });
const stem = new THREE.Mesh(flowerStem, stemMaterial);
stem.position.y = 0.1;
const head = new THREE.Mesh(flowerHead, headMaterial);
head.position.y = 0.3;
stem.castShadow = true; head.castShadow = true;
stem.receiveShadow = true; head.receiveShadow = true;
const flowerGroup = new THREE.Group();
flowerGroup.add(stem, head, vase);
flowerGroup.position.set(-1.65, 1.1, 1);
scene.add(flowerGroup);
// --- 7. Old Camera (On the table) ---
const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.2);
const cameraLens = new THREE.CylinderGeometry(0.08, 0.08, 0.05, 12);
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;
cameraMesh.add(lensMesh);
cameraMesh.position.set(-2.0, 0.7 + 0.15, -0.4);
cameraMesh.rotation.y = -Math.PI / 10;
cameraMesh.castShadow = true; cameraMesh.receiveShadow = true;
scene.add(cameraMesh);
// --- 8. Pizza Box (On the table) ---
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.7 + 0.025, 0.8);
pizzaBox.rotation.y = Math.PI / 5;
pizzaBox.castShadow = true; pizzaBox.receiveShadow = true;
scene.add(pizzaBox);
}
// --- 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 '--:--';
const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60);
const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(remainingSeconds).padStart(2, '0');
return `${paddedMinutes}:${paddedSeconds}`;
}
// --- Helper function to update the control buttons' state and text ---
function updateControls() {
const total = videoUrls.length;
const current = currentVideoIndex + 1;
if (total > 1) {
nextTapeButton.disabled = false;
nextTapeButton.classList.remove('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
nextTapeButton.classList.add('bg-tape-red', 'hover:bg-red-700');
} else {
nextTapeButton.disabled = true;
nextTapeButton.classList.add('opacity-50', 'cursor-not-allowed', 'bg-gray-600');
nextTapeButton.classList.remove('bg-tape-red', 'hover:bg-red-700');
}
// Always show the current tape count
nextTapeButton.textContent = `Next (${current}/${total})`;
}
// --- Play video by index ---
function playVideoByIndex(index) {
if (index < 0 || index >= videoUrls.length) {
statusText.textContent = 'End of playlist reached. Reload tapes to start again.';
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
return;
}
currentVideoIndex = index;
const url = videoUrls[index];
// Dispose of previous texture to free resources
if (videoTexture) {
videoTexture.dispose();
videoTexture = null;
}
videoElement.src = url;
videoElement.muted = true;
videoElement.load();
// Set loop property: only loop if it's the only video loaded
videoElement.loop = videoUrls.length === 1;
videoElement.onloadeddata = () => {
// 1. Create the Three.js texture
videoTexture = new THREE.VideoTexture(videoElement);
videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBAFormat;
videoTexture.needsUpdate = true;
// 2. Apply the video texture to the screen mesh
tvScreen.material.dispose();
tvScreen.material = new THREE.MeshBasicMaterial({ map: videoTexture });
tvScreen.material.needsUpdate = true;
// 3. Start playback
videoElement.play().then(() => {
isVideoLoaded = true;
// Use the defined base intensity for screen glow
screenLight.intensity = originalScreenIntensity;
// Initial status message with tape count
statusText.textContent = `Playing tape ${currentVideoIndex + 1} of ${videoUrls.length}.`;
updateControls();
}).catch(error => {
screenLight.intensity = 0.5; // Dim the light if playback fails
statusText.textContent = `Playback blocked for tape ${currentVideoIndex + 1}. Click Next Tape to try again.`;
console.error('Playback Error: Could not start video playback.', error);
});
};
videoElement.onerror = (e) => {
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
statusText.textContent = `Error loading tape ${currentVideoIndex + 1}.`;
console.error('Video Load Error:', e);
};
}
// --- Cycle to the next video ---
function playNextVideo() {
if (videoUrls.length > 0) {
// Determine the next index, cycling back to 0 if we reach the end
let nextIndex = (currentVideoIndex + 1) % videoUrls.length;
playVideoByIndex(nextIndex);
}
}
// --- Video Loading Logic (handles multiple files) ---
function loadVideoFile(event) {
const files = event.target.files;
if (files.length === 0) {
statusText.textContent = 'File selection cancelled.';
return;
}
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks
videoUrls.forEach(url => URL.revokeObjectURL(url));
videoUrls = [];
// 2. Populate the new videoUrls array
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('video/')) {
videoUrls.push(URL.createObjectURL(file));
}
}
if (videoUrls.length === 0) {
statusText.textContent = 'No valid video files selected.';
updateControls();
return;
}
// 3. Start playback of the first video
statusText.textContent = `Loaded ${videoUrls.length} tapes. Starting playback...`;
playVideoByIndex(0);
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
// 1. Dust animation: slow downward drift
if (dust) {
const positions = dust.geometry.attributes.position.array;
for (let i = 1; i < positions.length; i += 3) {
positions[i] -= 0.001;
if (positions[i] < -2) {
positions[i] = 8;
}
}
dust.geometry.attributes.position.needsUpdate = true;
}
// 2. Camera movement (Gentle, random hovering)
const globalTime = Date.now() * 0.00005;
const lookAtTime = Date.now() * 0.00003;
const camAmplitude = 0.6;
const lookAmplitude = 0.05;
// Base Camera Position in front of the TV
const baseX = 0;
const baseY = 1.5;
const baseZ = 4;
// Base LookAt target (Center of the screen)
const baseTargetX = 0;
const baseTargetY = 1.7;
const baseTargetZ = 0.96;
// Camera Position Offsets (Drift)
const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude;
const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.4;
camera.position.x = baseX + camOffsetX;
camera.position.y = baseY + camOffsetY;
camera.position.z = baseZ;
// LookAt Target Offsets (Subtle Gaze Shift)
const lookOffsetX = Math.sin(lookAtTime * 1.5) * lookAmplitude;
const lookOffsetY = Math.cos(lookAtTime * 1.2) * lookAmplitude;
// Apply lookAt to the subtly shifted target
camera.lookAt(
baseTargetX + lookOffsetX,
baseTargetY + lookOffsetY,
baseTargetZ
);
// 3. Lamp Flicker Effect
const flickerChance = 0.995;
const restoreRate = 0.15;
if (Math.random() > flickerChance) {
// Flickers quickly to a dimmer random value (between 0.3 and 1.05)
lampLight.intensity = originalLampIntensity * (0.3 + Math.random() * 0.7);
} else if (lampLight.intensity < originalLampIntensity) {
// Smoothly restore original intensity
lampLight.intensity = THREE.MathUtils.lerp(lampLight.intensity, originalLampIntensity, restoreRate);
}
// 4. Screen Light Pulse and Movement Effect (Updated)
if (isVideoLoaded && screenLight.intensity > 0) {
// A. Pulse Effect (Intensity Fluctuation)
// Generate a small random fluctuation for the pulse (Range: 1.35 to 1.65 around base 1.5)
const pulseTarget = originalScreenIntensity + (Math.random() - 0.5) * 0.7;
// Smoothly interpolate towards the new target fluctuation
screenLight.intensity = THREE.MathUtils.lerp(screenLight.intensity, pulseTarget, 0.1);
// B. Movement Effect (Subtle circle around the screen center - circling the room area)
const lightTime = Date.now() * 0.002;
const radius = 0.05; // Small circle radius (5 cm)
const centerX = 0;
const centerY = 1.7;
const centerZ = 1.2; // Use the updated Z position of the light source
// Move the light in a subtle, erratic circle
screenLight.position.x = centerX + Math.cos(lightTime) * radius;
screenLight.position.y = centerY + Math.sin(lightTime * 1.5) * radius * 0.5; // Slightly different freq for Y
screenLight.position.z = centerZ; // Keep Z constant at the screen light plane
}
// 5. Update video texture (essential to grab the next frame)
if (videoTexture) {
videoTexture.needsUpdate = true;
// Update time display in the animation loop
if (isVideoLoaded && videoElement.readyState >= 3) {
const currentTime = formatTime(videoElement.currentTime);
const duration = formatTime(videoElement.duration);
statusText.textContent =
`Tape ${currentVideoIndex + 1} of ${videoUrls.length}. Time: ${currentTime} / ${duration}`;
}
}
// RENDER!
renderer.render(scene, camera);
}
// --- Window Resize Handler ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Start everything on window load
window.onload = init;
</script>
</body>
</html>