Refactoring: add Vite to serve JS modules

This commit is contained in:
Dejvino 2025-11-16 13:20:33 +01:00
parent 75c87c9d03
commit 1c2a3c6d3a
17 changed files with 1506 additions and 332 deletions

1
tv-player/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

View File

@ -4,9 +4,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Retro TV Player</title> <title>Retro TV Player</title>
<!-- Load Tailwind CSS for styling --><script src="./vendor/tailwind-3.4.17.js" x-src="https://cdn.tailwindcss.com"></script> <!-- Load Tailwind CSS for styling --><script src="/vendor/tailwind-3.4.17.js"></script>
<!-- Load Three.js for 3D rendering --><script src="./vendor/three.min.js" x-src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="./src/tailwind-config.js"></script> <script src="./src/tailwind-config.js"></script>
<style> <style>
/* Dark room aesthetic */ /* Dark room aesthetic */
@ -45,16 +44,8 @@
</div> </div>
</div> </div>
<script src="./src/global-variables.js"></script> <!-- Main entry point for the application -->
<script src="./src/utils.js"></script> <script type="module" src="/src/main.js"></script>
<script src="./src/scene.js"></script>
<script src="./src/video-player.js"></script>
<script src="./src/effects_dust.js"></script>
<script src="./src/effects_flies.js"></script>
<script src="./src/EffectsManager.js"></script>
<script src="./src/vcr-display.js"></script>
<script src="./src/animate.js"></script>
<script src="./src/init.js"></script>
<!-- 3D Canvas will be injected here by Three.js --> <!-- 3D Canvas will be injected here by Three.js -->
</body> </body>

1069
tv-player/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
tv-player/package.json Normal file
View File

@ -0,0 +1,17 @@
{
"name": "tv-player",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"vite": "^7.2.2"
},
"dependencies": {
"three": "^0.181.1"
}
}

View File

@ -1,4 +1,7 @@
class EffectsManager { import { DustEffect } from './effects_dust.js';
import { FliesEffect } from './effects_flies.js';
export class EffectsManager {
constructor(scene) { constructor(scene) {
this.effects = []; this.effects = [];
this._initializeEffects(scene); this._initializeEffects(scene);

View File

@ -1,105 +0,0 @@
function updateCamera() {
const globalTime = Date.now() * 0.00005;
const lookAtTime = Date.now() * 0.00003;
const camAmplitude = 0.7;
const lookAmplitude = 0.05;
// Base Camera Position in front of the TV
const baseX = -0.5;
const baseY = 1.5;
const baseZ = 2.5;
// Base LookAt target (Center of the screen)
const baseTargetX = -0.7;
const baseTargetY = 1.7;
const baseTargetZ = -0.3;
// Camera Position Offsets (Drift)
const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude;
const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.4;
const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude * 1.4;
camera.position.x = baseX + camOffsetX;
camera.position.y = baseY + camOffsetY;
camera.position.z = baseZ + camOffsetZ;
// 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);
}
function updateLampFlicker() {
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)
let lampLightIntensity = originalLampIntensity * (0.3 + Math.random() * 0.7);
lampLightSpot.intensity = lampLightIntensity;
lampLightPoint.intensity = lampLightIntensity;
} else if (lampLightPoint.intensity < originalLampIntensity) {
// Smoothly restore original intensity
let lampLightIntensity = THREE.MathUtils.lerp(lampLightPoint.intensity, originalLampIntensity, restoreRate);
lampLightSpot.intensity = lampLightIntensity;
lampLightPoint.intensity = lampLightIntensity;
}
}
function updateScreenLight() {
if (isVideoLoaded && screenLight.intensity > 0) {
const pulseTarget = originalScreenIntensity + (Math.random() - 0.5) * screenIntensityPulse;
screenLight.intensity = THREE.MathUtils.lerp(screenLight.intensity, pulseTarget, 0.1);
const lightTime = Date.now() * 0.0001;
const radius = 0.01;
const centerX = 0;
const centerY = 1.5;
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
}
}
function updateVideo() {
if (videoTexture) {
videoTexture.needsUpdate = true;
}
}
function updateVcr() {
const currentTime = baseTime + videoElement.currentTime;
if (Math.abs(currentTime - lastUpdateTime) > 0.1) {
updateVcrDisplay(currentTime);
lastUpdateTime = currentTime;
}
if (currentTime - lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds
blinkState = !blinkState;
lastBlinkToggleTime = currentTime;
}
}
// --- Animation Loop ---
function animate() {
requestAnimationFrame(animate);
effectsManager.update();
updateCamera();
updateLampFlicker();
updateScreenLight();
updateVideo();
updateVcr();
// RENDER!
renderer.render(scene, camera);
}
// --- Window Resize Handler ---
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}

View File

@ -0,0 +1,110 @@
import * as THREE from 'three';
import { state } from '../state.js';
import { updateVcrDisplay } from '../vcr-display.js';
function updateCamera() {
const globalTime = Date.now() * 0.00005;
const lookAtTime = Date.now() * 0.00003;
const camAmplitude = 0.7;
const lookAmplitude = 0.05;
// Base Camera Position in front of the TV
const baseX = -0.5;
const baseY = 1.5;
const baseZ = 2.5;
// Base LookAt target (Center of the screen)
const baseTargetX = -0.7;
const baseTargetY = 1.7;
const baseTargetZ = -0.3;
// Camera Position Offsets (Drift)
const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude;
const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.4;
const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude * 1.4;
state.camera.position.x = baseX + camOffsetX;
state.camera.position.y = baseY + camOffsetY;
state.camera.position.z = baseZ + camOffsetZ;
// 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
state.camera.lookAt(baseTargetX + lookOffsetX, baseTargetY + lookOffsetY, baseTargetZ);
}
function updateLampFlicker() {
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)
let lampLightIntensity = state.originalLampIntensity * (0.3 + Math.random() * 0.7);
state.lampLightSpot.intensity = lampLightIntensity;
state.lampLightPoint.intensity = lampLightIntensity;
} else if (state.lampLightPoint.intensity < state.originalLampIntensity) {
// Smoothly restore original intensity
let lampLightIntensity = THREE.MathUtils.lerp(state.lampLightPoint.intensity, state.originalLampIntensity, restoreRate);
state.lampLightSpot.intensity = lampLightIntensity;
state.lampLightPoint.intensity = lampLightIntensity;
}
}
function updateScreenLight() {
if (state.isVideoLoaded && state.screenLight.intensity > 0) {
const pulseTarget = state.originalScreenIntensity + (Math.random() - 0.5) * state.screenIntensityPulse;
state.screenLight.intensity = THREE.MathUtils.lerp(state.screenLight.intensity, pulseTarget, 0.1);
const lightTime = Date.now() * 0.0001;
const radius = 0.01;
const centerX = 0;
const centerY = 1.5;
state.screenLight.position.x = centerX + Math.cos(lightTime) * radius;
state.screenLight.position.y = centerY + Math.sin(lightTime * 1.5) * radius * 0.5; // Slightly different freq for Y
}
}
function updateVideo() {
if (state.videoTexture) {
state.videoTexture.needsUpdate = true;
}
}
function updateVcr() {
const currentTime = state.baseTime + state.videoElement.currentTime;
if (Math.abs(currentTime - state.lastUpdateTime) > 0.1) {
updateVcrDisplay(currentTime);
state.lastUpdateTime = currentTime;
}
if (currentTime - state.lastBlinkToggleTime > 0.5) { // Blink every 0.5 seconds
state.blinkState = !state.blinkState;
state.lastBlinkToggleTime = currentTime;
}
}
// --- Animation Loop ---
export function animate() {
requestAnimationFrame(animate);
state.effectsManager.update();
updateCamera();
updateLampFlicker();
updateScreenLight();
updateVideo();
updateVcr();
// RENDER!
state.renderer.render(state.scene, state.camera);
}
// --- Window Resize Handler ---
export function onWindowResize() {
state.camera.aspect = window.innerWidth / window.innerHeight;
state.camera.updateProjectionMatrix();
state.renderer.setSize(window.innerWidth, window.innerHeight);
}

View File

@ -0,0 +1,62 @@
import * as THREE from 'three';
import { state, initState } from '../state.js';
import { EffectsManager } from '../EffectsManager.js';
import { createSceneObjects } from '../scene.js';
import { animate, onWindowResize } from './animate.js';
import { loadVideoFile, playNextVideo } from '../video-player.js';
// --- Initialization ---
export function init() {
initState();
// 1. Scene Setup (Dark, Ambient)
state.scene = new THREE.Scene();
state.scene.background = new THREE.Color(0x000000);
// 2. Camera Setup
const FOV = 65;
state.camera = new THREE.PerspectiveCamera(FOV, window.innerWidth / window.innerHeight, 0.1, 1000);
state.camera.position.set(0, 1.5, 4);
// 3. Renderer Setup
state.renderer = new THREE.WebGLRenderer({ antialias: true });
state.renderer.setSize(window.innerWidth, window.innerHeight);
state.renderer.setPixelRatio(window.devicePixelRatio);
// Enable shadows on the renderer
state.renderer.shadowMap.enabled = true;
state.renderer.shadowMap.type = THREE.PCFSoftShadowMap; // Softer shadows
state.container.appendChild(state.renderer.domElement);
// 5. Build the entire scene with TV and surrounding objects
createSceneObjects();
// 6. Initialize all visual effects via the manager
state.effectsManager = new EffectsManager(state.scene);
// --- 8. Debug Visualization Helpers ---
// Visual aids for the light source positions
if (state.debugLight && THREE.PointLightHelper) {
const screenHelper = new THREE.PointLightHelper(state.screenLight, 0.1, 0xff0000); // Red for screen
state.scene.add(screenHelper);
// Lamp Helper will now work since lampLight is added to the scene
const lampHelperPoint = new THREE.PointLightHelper(state.lampLightPoint, 0.1, 0x00ff00); // Green for lamp
state.scene.add(lampHelperPoint);
}
// 9. Event Listeners
window.addEventListener('resize', onWindowResize, false);
state.fileInput.addEventListener('change', loadVideoFile);
// Button logic
state.loadTapeButton.addEventListener('click', () => {
state.fileInput.click();
});
// Auto-advance to the next video when the current one finishes.
state.videoElement.addEventListener('ended', playNextVideo);
// Start the animation loop
animate();
}

View File

@ -1,4 +1,6 @@
class DustEffect { import * as THREE from 'three';
export class DustEffect {
constructor(scene) { constructor(scene) {
this.dust = null; this.dust = null;
this._create(scene); this._create(scene);

View File

@ -1,6 +1,17 @@
import * as THREE from 'three';
import { state } from './state.js';
import { degToRad } from './utils.js';
const FLIES_COUNT = 2; const FLIES_COUNT = 2;
class FliesEffect { // --- Configuration ---
const FLIGHT_HEIGHT_MIN = 0.5; // Min height for flying
const FLIGHT_HEIGHT_MAX = 2;//state.roomHeight * 0.9; // Max height for flying
const FLY_FLIGHT_SPEED_FACTOR = 0.01; // How quickly 't' increases per frame
const FLY_WAIT_BASE = 1000;
const FLY_LAND_CHANCE = 0.3;
export class FliesEffect {
constructor(scene) { constructor(scene) {
this.flies = []; this.flies = [];
this._setupFlies(scene); this._setupFlies(scene);
@ -8,9 +19,9 @@ class FliesEffect {
_randomFlyTarget() { _randomFlyTarget() {
return new THREE.Vector3( return new THREE.Vector3(
(Math.random() - 0.5) * (ROOM_SIZE - 1), (Math.random() - 0.5) * (state.roomSize - 1),
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN), FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
(Math.random() - 0.5) * (ROOM_SIZE - 1) (Math.random() - 0.5) * (state.roomSize - 1)
); );
} }
@ -86,8 +97,8 @@ class FliesEffect {
} }
if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) { if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) {
raycaster.set(fly.position, new THREE.Vector3(0, -1, 0)); state.raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
const intersects = raycaster.intersectObjects(landingSurfaces, false); const intersects = state.raycaster.intersectObjects(state.landingSurfaces, false);
if (intersects.length > 0) { if (intersects.length > 0) {
const intersect = intersects[0]; const intersect = intersects[0];

View File

@ -1,67 +0,0 @@
// --- Initialization ---
function init() {
// 1. Scene Setup (Dark, Ambient)
scene = new THREE.Scene();
scene.background = new THREE.Color(0x000000);
// 2. Camera Setup
const FOV = 65;
camera = new THREE.PerspectiveCamera(FOV, 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);
const roomLight = new THREE.PointLight(0xffaa55, 0.05, roomSize);
roomLight.position.set(0, 1.8, 0);
scene.add(roomLight);
// 5. Build the entire scene with TV and surrounding objects
createSceneObjects();
// 6. Initialize all visual effects via the manager
effectsManager = new EffectsManager(scene);
// 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 lampHelperPoint = new THREE.PointLightHelper(lampLightPoint, 0.1, 0x00ff00); // Green for lamp
scene.add(lampHelperPoint);
}
// 9. Event Listeners
window.addEventListener('resize', onWindowResize, false);
fileInput.addEventListener('change', loadVideoFile);
// Button logic
loadTapeButton.addEventListener('click', () => {
fileInput.click();
});
// Auto-advance to the next video when the current one finishes.
videoElement.addEventListener('ended', playNextVideo);
// Start the animation loop
animate();
}
// Start everything on window load
window.onload = init;

6
tv-player/src/main.js Normal file
View File

@ -0,0 +1,6 @@
import * as THREE from 'three';
import './tailwind-config.js';
import { init } from './core/init.js';
// Start everything
init();

View File

@ -1,6 +1,11 @@
import * as THREE from 'three';
import { state } from './state.js';
import { getRandomColor, seededRandom } from './utils.js';
import { createVcrDisplay } from './vcr-display.js';
// --- Room Walls Function --- // --- Room Walls Function ---
function createRoomWalls() { function createRoomWalls() {
const wallTexture = loader.load('./textures/wall.jpg'); const wallTexture = state.loader.load('/textures/wall.jpg');
wallTexture.wrapS = THREE.RepeatWrapping; wallTexture.wrapS = THREE.RepeatWrapping;
wallTexture.wrapT = THREE.RepeatWrapping; wallTexture.wrapT = THREE.RepeatWrapping;
@ -13,34 +18,34 @@ function createRoomWalls() {
}); });
// 1. Back Wall (behind the TV) // 1. Back Wall (behind the TV)
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial); const backWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
backWall.position.set(0, roomHeight / 2, -roomSize / 2); backWall.position.set(0, state.roomHeight / 2, -state.roomSize / 2);
backWall.receiveShadow = true; backWall.receiveShadow = true;
scene.add(backWall); state.scene.add(backWall);
// 2. Front Wall (behind the camera) // 2. Front Wall (behind the camera)
const frontWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial); const frontWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
frontWall.position.set(0, roomHeight / 2, roomSize / 2); frontWall.position.set(0, state.roomHeight / 2, state.roomSize / 2);
frontWall.rotation.y = Math.PI; frontWall.rotation.y = Math.PI;
frontWall.receiveShadow = true; frontWall.receiveShadow = true;
scene.add(frontWall); state.scene.add(frontWall);
// 3. Left Wall // 3. Left Wall
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial); const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
leftWall.rotation.y = Math.PI / 2; leftWall.rotation.y = Math.PI / 2;
leftWall.position.set(-roomSize / 2, roomHeight / 2, 0); leftWall.position.set(-state.roomSize / 2, state.roomHeight / 2, 0);
leftWall.receiveShadow = true; leftWall.receiveShadow = true;
scene.add(leftWall); state.scene.add(leftWall);
// 4. Right Wall // 4. Right Wall
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(roomSize, roomHeight), wallMaterial); const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
rightWall.rotation.y = -Math.PI / 2; rightWall.rotation.y = -Math.PI / 2;
rightWall.position.set(roomSize / 2, roomHeight / 2, 0); rightWall.position.set(state.roomSize / 2, state.roomHeight / 2, 0);
rightWall.receiveShadow = true; rightWall.receiveShadow = true;
scene.add(rightWall); state.scene.add(rightWall);
// 5. Ceiling // 5. Ceiling
const ceilingGeometry = new THREE.PlaneGeometry(roomSize, roomSize); const ceilingGeometry = new THREE.PlaneGeometry(state.roomSize, state.roomSize);
const ceilingTexture = wallTexture; const ceilingTexture = wallTexture;
ceilingTexture.repeat.set(4, 4); ceilingTexture.repeat.set(4, 4);
// USING MeshPhongMaterial // USING MeshPhongMaterial
@ -53,16 +58,16 @@ function createRoomWalls() {
const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial); const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
ceiling.rotation.x = Math.PI / 2; ceiling.rotation.x = Math.PI / 2;
ceiling.position.set(0, roomHeight, 0); ceiling.position.set(0, state.roomHeight, 0);
ceiling.receiveShadow = true; ceiling.receiveShadow = true;
scene.add(ceiling); state.scene.add(ceiling);
// --- 6. Add a Window to the Back Wall --- // --- 6. Add a Window to the Back Wall ---
const windowWidth = 1.5; const windowWidth = 1.5;
const windowHeight = 1.2; const windowHeight = 1.2;
const windowGeometry = new THREE.PlaneGeometry(windowWidth, windowHeight); const windowGeometry = new THREE.PlaneGeometry(windowWidth, windowHeight);
const nightSkyMaterial = new THREE.MeshBasicMaterial({ const nightSkyMaterial = new THREE.MeshPhongMaterial({
color: 0x0a1a3a, color: 0x0a1a3a,
emissive: 0x0a1a3a, emissive: 0x0a1a3a,
emissiveIntensity: 0.5, emissiveIntensity: 0.5,
@ -70,13 +75,13 @@ function createRoomWalls() {
}); });
const windowPane = new THREE.Mesh(windowGeometry, nightSkyMaterial); const windowPane = new THREE.Mesh(windowGeometry, nightSkyMaterial);
const windowZ = -roomSize / 2 + 0.001; const windowZ = -state.roomSize / 2 + 0.001;
windowPane.position.set(-3.5, roomHeight * 0.5 + 1.5, windowZ); windowPane.position.set(-3.5, state.roomHeight * 0.5 + 1.5, windowZ);
scene.add(windowPane); state.scene.add(windowPane);
} }
function createBookshelf(x, z, rotationY, uniqueSeed) { function createBookshelf(x, z, rotationY, uniqueSeed) {
seed = uniqueSeed; // Reset seed for this specific shelf instance state.seed = uniqueSeed; // Reset seed for this specific shelf instance
const shelfHeight = 2.2; const shelfHeight = 2.2;
const shelfDepth = 0.35; const shelfDepth = 0.35;
@ -126,7 +131,7 @@ function createBookshelf(x, z, rotationY, uniqueSeed) {
topPanel.castShadow = true; topPanel.castShadow = true;
shelfGroup.add(topPanel); shelfGroup.add(topPanel);
landingSurfaces.push(topPanel); state.landingSurfaces.push(topPanel);
// 2. Individual Shelves & Books // 2. Individual Shelves & Books
const internalHeight = shelfHeight - (2 * woodThickness); const internalHeight = shelfHeight - (2 * woodThickness);
@ -185,7 +190,7 @@ function createBookshelf(x, z, rotationY, uniqueSeed) {
} }
} }
scene.add(shelfGroup); state.scene.add(shelfGroup);
} }
function createDoor(x, z, rotY) { function createDoor(x, z, rotY) {
@ -224,7 +229,7 @@ function createDoor(x, z, rotY) {
knob.castShadow = true; knob.castShadow = true;
doorGroup.add(knob); doorGroup.add(knob);
scene.add(doorGroup); state.scene.add(doorGroup);
} }
// --- VCR Model Function --- // --- VCR Model Function ---
@ -267,8 +272,8 @@ function createVcr() {
vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor
// Light from the VCR display itself // Light from the VCR display itself
vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.5, 1); const vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.03, 1.8);
vcrDisplayLight.position.set(0.3, 0.03, 0.35 + 0.05); // Move light slightly closer to VCR surface vcrDisplayLight.position.set(0.23, 0.03, 0.35 + 0.03);
vcrDisplayLight.castShadow = true; vcrDisplayLight.castShadow = true;
vcrDisplayLight.shadow.mapSize.width = 256; vcrDisplayLight.shadow.mapSize.width = 256;
vcrDisplayLight.shadow.mapSize.height = 256; vcrDisplayLight.shadow.mapSize.height = 256;
@ -413,42 +418,42 @@ function createTvSet(x, z, rotY) {
screenGeometry.rotateY(-Math.PI/2); screenGeometry.rotateY(-Math.PI/2);
const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 }); const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
tvScreen = new THREE.Mesh(screenGeometry, screenMaterial); state.tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
// Position the curved screen // Position the curved screen
tvScreen.position.set(0.0, 1.5, -2.1); state.tvScreen.position.set(0.0, 1.5, -2.1);
tvScreen.material = new THREE.MeshPhongMaterial({ state.tvScreen.material = new THREE.MeshPhongMaterial({
color: 0x0a0a0a, // Deep black color: 0x0a0a0a, // Deep black
shininess: 5, shininess: 5,
specular: 0x111111 specular: 0x111111
}); });
tvScreen.material.needsUpdate = true; state.tvScreen.material.needsUpdate = true;
tvGroup.add(tvScreen); tvGroup.add(state.tvScreen);
tvGroup.position.set(x, 0, z); tvGroup.position.set(x, 0, z);
tvGroup.rotation.y = rotY; tvGroup.rotation.y = rotY;
// Light from the screen (initially low intensity, will increase when video loads) // Light from the screen (initially low intensity, will increase when video loads)
screenLight = new THREE.PointLight(0xffffff, 0, 10); state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
screenLight.position.set(0, 1.5, 1.0); state.screenLight.position.set(0, 1.5, 1.0);
// Screen light casts shadows // Screen light casts shadows
screenLight.castShadow = true; state.screenLight.castShadow = true;
screenLight.shadow.mapSize.width = 1024; state.screenLight.shadow.mapSize.width = 1024;
screenLight.shadow.mapSize.height = 1024; state.screenLight.shadow.mapSize.height = 1024;
screenLight.shadow.camera.near = 0.2; state.screenLight.shadow.camera.near = 0.2;
screenLight.shadow.camera.far = 5; state.screenLight.shadow.camera.far = 5;
tvGroup.add(screenLight); tvGroup.add(state.screenLight);
// -- VCR -- // -- VCR --
const vcr = createVcr(); const vcr = createVcr();
vcr.position.set(-0.3, 0.6, 0.05); vcr.position.set(-0.3, 0.6, 0.05);
tvGroup.add(vcr); tvGroup.add(vcr);
scene.add(tvGroup); state.scene.add(tvGroup);
} }
// --- Scene Modeling Function --- // --- Scene Modeling Function ---
function createSceneObjects() { export function createSceneObjects() {
// --- Materials (MeshPhongMaterial) --- // --- Materials (MeshPhongMaterial) ---
const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 }); const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
const darkMetal = new THREE.MeshPhongMaterial({ const darkMetal = new THREE.MeshPhongMaterial({
@ -460,22 +465,33 @@ function createSceneObjects() {
// --- 1. Floor --- // --- 1. Floor ---
const floorGeometry = new THREE.PlaneGeometry(20, 20); const floorGeometry = new THREE.PlaneGeometry(20, 20);
const floorTexture = loader.load('./textures/floor.jpg'); const floorTexture = state.loader.load('/textures/floor.jpg');
floorTexture.wrapS = THREE.RepeatWrapping; floorTexture.wrapS = THREE.RepeatWrapping;
floorTexture.wrapT = THREE.RepeatWrapping; floorTexture.wrapT = THREE.RepeatWrapping;
floorTexture.repeat.set(roomSize, roomSize); floorTexture.repeat.set(state.roomSize, state.roomSize);
const floorMaterial = new THREE.MeshPhongMaterial({ map: floorTexture, color: 0x555555, shininess: 5 }); const floorMaterial = new THREE.MeshPhongMaterial({ map: floorTexture, color: 0x555555, shininess: 5 });
const floor = new THREE.Mesh(floorGeometry, floorMaterial); const floor = new THREE.Mesh(floorGeometry, floorMaterial);
floor.rotation.x = -Math.PI / 2; floor.rotation.x = -Math.PI / 2;
floor.position.y = 0; floor.position.y = 0;
floor.receiveShadow = true; floor.receiveShadow = true;
scene.add(floor); state.scene.add(floor);
landingSurfaces.push(floor); state.landingSurfaces.push(floor);
createTvSet(-roomSize/2 + 1.2, -roomSize/2 + 0.8, Math.PI * 0.1); createRoomWalls();
// 3. Lighting (Minimal and focused)
const ambientLight = new THREE.AmbientLight(0x111111, 1);
state.scene.add(ambientLight);
// --- 5. Lamp (On the table, right side) --- const roomLight = new THREE.PointLight(0xffaa55, 0.5, state.roomSize);
roomLight.position.set(0, 1.8, 0);
state.scene.add(roomLight);
createTvSet(-state.roomSize/2 + 1.2, -state.roomSize/2 + 0.8, Math.PI * 0.1);
// --- 5. Standing Lamp ---
const lampBase = new THREE.CylinderGeometry(0.05, 0.2, 0.1, 12); 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 lampPole = new THREE.CylinderGeometry(0.02, 0.02, 1.5, 8);
const lampShade = new THREE.ConeGeometry(0.2, 0.4, 16); const lampShade = new THREE.ConeGeometry(0.2, 0.4, 16);
@ -495,34 +511,34 @@ function createSceneObjects() {
shadeMesh.rotation.x = Math.PI; shadeMesh.rotation.x = Math.PI;
// Lamp Light (Warm Glow) - Configured to cast shadows // Lamp Light (Warm Glow) - Configured to cast shadows
lampLightPoint = new THREE.PointLight(0xffaa00, originalLampIntensity, 4); state.lampLightPoint = new THREE.PointLight(0xffaa00, state.originalLampIntensity, 4);
lampLightPoint.position.set(-0.01, roomHeight-0.9, 0.01); state.lampLightPoint.position.set(-0.01, state.roomHeight-0.9, 0.01);
lampLightPoint.castShadow = true; state.lampLightPoint.castShadow = true;
// Optimization: Reduced map size and far plane to ease resource burden // Optimization: Reduced map size and far plane to ease resource burden
lampLightPoint.shadow.mapSize.width = 512; state.lampLightPoint.shadow.mapSize.width = 512;
lampLightPoint.shadow.mapSize.height = 512; state.lampLightPoint.shadow.mapSize.height = 512;
lampLightPoint.shadow.camera.near = 0.1; state.lampLightPoint.shadow.camera.near = 0.1;
lampLightPoint.shadow.camera.far = 4; // Matches the light's attenuation distance (4) state.lampLightPoint.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
lampLightPoint.penumbra = 0.5; state.lampLightPoint.penumbra = 0.5;
lampLightSpot = new THREE.SpotLight(0xffaa00, originalLampIntensity, 4); state.lampLightSpot = new THREE.SpotLight(0xffaa00, state.originalLampIntensity, 4);
lampLightSpot.position.set(-0.01, 1.0, 0.01); state.lampLightSpot.position.set(-0.01, 1.0, 0.01);
lampLightSpot.target.position.set(0, 5, 0); state.lampLightSpot.target.position.set(0, 5, 0);
lampLightSpot.castShadow = true; state.lampLightSpot.castShadow = true;
// Optimization: Reduced map size and far plane to ease resource burden // Optimization: Reduced map size and far plane to ease resource burden
lampLightSpot.shadow.mapSize.width = 512; state.lampLightSpot.shadow.mapSize.width = 512;
lampLightSpot.shadow.mapSize.height = 512; state.lampLightSpot.shadow.mapSize.height = 512;
lampLightSpot.shadow.camera.near = 0.1; state.lampLightSpot.shadow.camera.near = 0.1;
lampLightSpot.shadow.camera.far = 4; // Matches the light's attenuation distance (4) state.lampLightSpot.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
lampLightSpot.penumbra = 0.5; state.lampLightSpot.penumbra = 1;
const lampGroup = new THREE.Group(); const lampGroup = new THREE.Group();
lampGroup.add(baseMesh, poleMesh, shadeMesh, lampLightSpot, lampLightSpot.target, lampLightPoint); lampGroup.add(baseMesh, poleMesh, shadeMesh, state.lampLightSpot, state.lampLightSpot.target, state.lampLightPoint);
lampGroup.position.set(0.8, 0.7, -roomSize/2+0.5); lampGroup.position.set(0.8, 0.7, -state.roomSize/2+0.5);
scene.add(lampGroup); state.scene.add(lampGroup);
landingSurfaces.push(shadeMesh); state.landingSurfaces.push(shadeMesh);
// --- 7. Old Camera (On the table) --- // --- 7. Old Camera (On the table) ---
const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.15); const cameraBody = new THREE.BoxGeometry(0.4, 0.3, 0.15);
@ -542,7 +558,7 @@ function createSceneObjects() {
cameraMesh.position.set(-1.7, 0.15, 0.4); cameraMesh.position.set(-1.7, 0.15, 0.4);
cameraMesh.rotation.y = -Math.PI / 10; cameraMesh.rotation.y = -Math.PI / 10;
cameraMesh.castShadow = true; cameraMesh.receiveShadow = true; cameraMesh.castShadow = true; cameraMesh.receiveShadow = true;
scene.add(cameraMesh); state.scene.add(cameraMesh);
// --- 8. Pizza Box --- // --- 8. Pizza Box ---
const boxGeometry = new THREE.BoxGeometry(0.5, 0.05, 0.5); const boxGeometry = new THREE.BoxGeometry(0.5, 0.05, 0.5);
@ -551,7 +567,7 @@ function createSceneObjects() {
pizzaBox.position.set(-1.8, 0.025, -0.8); pizzaBox.position.set(-1.8, 0.025, -0.8);
pizzaBox.rotation.y = Math.PI / 5; pizzaBox.rotation.y = Math.PI / 5;
pizzaBox.castShadow = true; pizzaBox.receiveShadow = true; pizzaBox.castShadow = true; pizzaBox.receiveShadow = true;
scene.add(pizzaBox); state.scene.add(pizzaBox);
// --- 8. Cassette --- // --- 8. Cassette ---
const cassetteGeometry = new THREE.BoxGeometry(0.2, 0.05, 0.45); const cassetteGeometry = new THREE.BoxGeometry(0.2, 0.05, 0.45);
@ -560,11 +576,11 @@ function createSceneObjects() {
cassette.position.set(-0.5, 0.025, -1.4); cassette.position.set(-0.5, 0.025, -1.4);
cassette.rotation.y = Math.PI / 3; cassette.rotation.y = Math.PI / 3;
cassette.castShadow = true; cassette.receiveShadow = true; cassette.castShadow = true; cassette.receiveShadow = true;
scene.add(cassette); state.scene.add(cassette);
createDoor(roomSize/2, -roomSize/2 * 0.5, -Math.PI/2); createDoor(state.roomSize/2, -state.roomSize/2 * 0.5, -Math.PI/2);
createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.2, Math.PI/2, 0); createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.2, Math.PI/2, 0);
createBookshelf(-roomSize/2 + 0.2, roomSize/2*0.7, Math.PI/2, 0); createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.7, Math.PI/2, 0);
createBookshelf(roomSize/2 * 0.7, -roomSize/2+0.3, 0, 1); createBookshelf(state.roomSize/2 * 0.7, -state.roomSize/2+0.3, 0, 1);
} }

50
tv-player/src/state.js Normal file
View File

@ -0,0 +1,50 @@
import * as THREE from 'three';
export let state = undefined;
export function initState() {
state = {
// Core Three.js components
scene: null,
camera: null,
renderer: null,
tvScreen: null,
videoTexture: null,
screenLight: null,
lampLightPoint: null,
lampLightSpot: null,
effectsManager: null,
// VCR Display
lastUpdateTime: -1,
baseTime: 0,
blinkState: false,
lastBlinkToggleTime: 0,
vcrDisplayTexture: null,
// Video Playback
isVideoLoaded: false,
videoUrls: [],
currentVideoIndex: -1,
// Scene constants
originalLampIntensity: 0.3,
originalScreenIntensity: 0.2,
screenIntensityPulse: 0.2,
roomSize: 5,
roomHeight: 3,
debugLight: false,
// DOM Elements
container: document.body,
videoElement: document.getElementById('video'),
fileInput: document.getElementById('fileInput'),
loadTapeButton: document.getElementById('loadTapeButton'),
// Utilities
loader: new THREE.TextureLoader(),
landingSurfaces: [],
raycaster: new THREE.Raycaster(),
seed: 12345,
};
}

View File

@ -1,5 +1,8 @@
import * as THREE from 'three';
import { state } from './state.js';
// --- Utility: Random Color (seeded) --- // --- Utility: Random Color (seeded) ---
function getRandomColor() { export function getRandomColor() {
const hue = seededRandom(); const hue = seededRandom();
const saturation = 0.6 + seededRandom() * 0.4; const saturation = 0.6 + seededRandom() * 0.4;
const lightness = 0.3 + seededRandom() * 0.4; const lightness = 0.3 + seededRandom() * 0.4;
@ -11,20 +14,20 @@ function getRandomColor() {
* @param {number} degrees * @param {number} degrees
* @returns {number} * @returns {number}
*/ */
function degToRad(degrees) { export function degToRad(degrees) {
return degrees * (Math.PI / 180); return degrees * (Math.PI / 180);
} }
// --- Seedable Random Number Generator (Mulberry32) --- // --- Seedable Random Number Generator (Mulberry32) ---
function seededRandom() { export function seededRandom() {
let t = seed += 0x6D2B79F5; let t = state.seed += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1); t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61); t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296; return ((t ^ t >>> 14) >>> 0) / 4294967296;
} }
// --- Helper function to format seconds into MM:SS --- // --- Helper function to format seconds into MM:SS ---
function formatTime(seconds) { export function formatTime(seconds) {
if (isNaN(seconds) || seconds === Infinity || seconds < 0) return '--:--'; if (isNaN(seconds) || seconds === Infinity || seconds < 0) return '--:--';
const minutes = Math.floor(seconds / 60); const minutes = Math.floor(seconds / 60);
const remainingSeconds = Math.floor(seconds % 60); const remainingSeconds = Math.floor(seconds % 60);

View File

@ -1,6 +1,7 @@
let vcrDisplayTexture; import * as THREE from 'three';
let blinkState = false; import { state } from './state.js';
let lastBlinkToggleTime = 0; import { formatTime } from './utils.js';
// --- Segment Display Definitions --- // --- Segment Display Definitions ---
// Define which segments (indexed 0-6: A, B, C, D, E, F, G) are active for each digit // Define which segments (indexed 0-6: A, B, C, D, E, F, G) are active for each digit
@ -151,7 +152,7 @@ function drawSegmentDisplay(ctx, timeString) {
const currentY = SEG_PADDING; const currentY = SEG_PADDING;
// Draw Playback Arrow // Draw Playback Arrow
if (isVideoLoaded && videoElement.readyState >= 3) { if (state.isVideoLoaded && state.videoElement.readyState >= 3) {
drawPlaybackArrow(ctx, currentX, currentY, digitHeight); drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
} }
currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
@ -160,7 +161,7 @@ function drawSegmentDisplay(ctx, timeString) {
const char = timeString[i]; const char = timeString[i];
if (char === ':') { if (char === ':') {
drawColon(ctx, currentX, currentY, digitHeight, blinkState); // Pass blinkState drawColon(ctx, currentX, currentY, digitHeight, state.blinkState); // Pass blinkState
currentX += colonWidth; currentX += colonWidth;
} else if (char >= '0' && char <= '9') { } else if (char >= '0' && char <= '9') {
drawSegmentDigit(ctx, char, currentX, currentY, digitHeight); drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
@ -175,7 +176,7 @@ function drawSegmentDisplay(ctx, timeString) {
} }
// --- VCR Display Functions --- // --- VCR Display Functions ---
function createVcrDisplay() { export function createVcrDisplay() {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 160; // Increased width for arrow and better spacing canvas.width = 160; // Increased width for arrow and better spacing
canvas.height = 32; canvas.height = 32;
@ -184,27 +185,28 @@ function createVcrDisplay() {
ctx.fillStyle = '#0a0a0a'; ctx.fillStyle = '#0a0a0a';
ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillRect(0, 0, canvas.width, canvas.height);
vcrDisplayTexture = new THREE.CanvasTexture(canvas); state.vcrDisplayTexture = new THREE.CanvasTexture(canvas);
vcrDisplayTexture.needsUpdate = true; state.vcrDisplayTexture.needsUpdate = true;
const displayGeometry = new THREE.PlaneGeometry(0.45, 0.1); // Adjust geometry width for new canvas size const displayGeometry = new THREE.PlaneGeometry(0.45, 0.1); // Adjust geometry width for new canvas size
const displayMaterial = new THREE.MeshBasicMaterial({ const displayMaterial = new THREE.MeshPhongMaterial({
map: vcrDisplayTexture, map: state.vcrDisplayTexture,
side: THREE.FrontSide, side: THREE.FrontSide,
color: 0xffffff, color: 0xffffff,
transparent: true, transparent: true,
emissive: 0x00ff44, emissive: 0x00ff44,
emissiveIntensity: 0.1 emissiveIntensity: 0.05,
shininess: 0
}); });
const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial); const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
return displayMesh; return displayMesh;
} }
function updateVcrDisplay(time) { export function updateVcrDisplay(time) {
if (!vcrDisplayTexture) return; if (!state.vcrDisplayTexture) return;
const canvas = vcrDisplayTexture.image; const canvas = state.vcrDisplayTexture.image;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
const timeString = formatTime(time); const timeString = formatTime(time);
@ -212,5 +214,5 @@ function updateVcrDisplay(time) {
// Uses the new segment drawing function with ghosting, including blinkState for colon // Uses the new segment drawing function with ghosting, including blinkState for colon
drawSegmentDisplay(ctx, timeString); drawSegmentDisplay(ctx, timeString);
vcrDisplayTexture.needsUpdate = true; state.vcrDisplayTexture.needsUpdate = true;
} }

View File

@ -1,84 +1,87 @@
import * as THREE from 'three';
import { state } from './state.js';
// --- Play video by index --- // --- Play video by index ---
function playVideoByIndex(index) { export function playVideoByIndex(index) {
currentVideoIndex = index; state.currentVideoIndex = index;
const url = videoUrls[index]; const url = state.videoUrls[index];
// Dispose of previous texture to free resources // Dispose of previous texture to free resources
if (videoTexture) { if (state.videoTexture) {
videoTexture.dispose(); state.videoTexture.dispose();
videoTexture = null; state.videoTexture = null;
} }
if (index < 0 || index >= videoUrls.length) { if (index < 0 || index >= state.videoUrls.length) {
console.info('End of playlist reached. Reload tapes to start again.'); console.info('End of playlist reached. Reload tapes to start again.');
screenLight.intensity = 0.0; state.screenLight.intensity = 0.0;
tvScreen.material.dispose(); state.tvScreen.material.dispose();
tvScreen.material = new THREE.MeshPhongMaterial({ state.tvScreen.material = new THREE.MeshPhongMaterial({
color: 0x0a0a0a, // Deep black color: 0x0a0a0a, // Deep black
shininess: 5, shininess: 5,
specular: 0x111111 specular: 0x111111
}); });
tvScreen.material.needsUpdate = true; state.tvScreen.material.needsUpdate = true;
isVideoLoaded = false; state.isVideoLoaded = false;
lastUpdateTime = -1; // force VCR to redraw state.lastUpdateTime = -1; // force VCR to redraw
return; return;
} }
videoElement.src = url; state.videoElement.src = url;
videoElement.muted = true; state.videoElement.muted = true;
videoElement.load(); state.videoElement.load();
// Set loop property: only loop if it's the only video loaded // Set loop property: only loop if it's the only video loaded
videoElement.loop = false; //videoUrls.length === 1; state.videoElement.loop = false; //state.videoUrls.length === 1;
videoElement.onloadeddata = () => { state.videoElement.onloadeddata = () => {
// 1. Create the Three.js texture // 1. Create the Three.js texture
videoTexture = new THREE.VideoTexture(videoElement); state.videoTexture = new THREE.VideoTexture(state.videoElement);
videoTexture.minFilter = THREE.LinearFilter; state.videoTexture.minFilter = THREE.LinearFilter;
videoTexture.magFilter = THREE.LinearFilter; state.videoTexture.magFilter = THREE.LinearFilter;
videoTexture.format = THREE.RGBAFormat; state.videoTexture.format = THREE.RGBAFormat;
videoTexture.needsUpdate = true; state.videoTexture.needsUpdate = true;
// 2. Apply the video texture to the screen mesh // 2. Apply the video texture to the screen mesh
tvScreen.material.dispose(); state.tvScreen.material.dispose();
tvScreen.material = new THREE.MeshBasicMaterial({ map: videoTexture }); state.tvScreen.material = new THREE.MeshBasicMaterial({ map: state.videoTexture });
tvScreen.material.needsUpdate = true; state.tvScreen.material.needsUpdate = true;
// 3. Start playback // 3. Start playback
videoElement.play().then(() => { state.videoElement.play().then(() => {
isVideoLoaded = true; state.isVideoLoaded = true;
// Use the defined base intensity for screen glow // Use the defined base intensity for screen glow
screenLight.intensity = originalScreenIntensity; state.screenLight.intensity = state.originalScreenIntensity;
// Initial status message with tape count // Initial status message with tape count
console.info(`Playing tape ${currentVideoIndex + 1} of ${videoUrls.length}.`); console.info(`Playing tape ${state.currentVideoIndex + 1} of ${state.videoUrls.length}.`);
}).catch(error => { }).catch(error => {
screenLight.intensity = originalScreenIntensity * 0.5; // Dim the light if playback fails state.screenLight.intensity = state.originalScreenIntensity * 0.5; // Dim the light if playback fails
console.error(`Playback blocked for tape ${currentVideoIndex + 1}. Click Next Tape to try again.`); console.error(`Playback blocked for tape ${state.currentVideoIndex + 1}. Click Next Tape to try again.`);
console.error('Playback Error: Could not start video playback.', error); console.error('Playback Error: Could not start video playback.', error);
}); });
}; };
videoElement.onerror = (e) => { state.videoElement.onerror = (e) => {
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map state.screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
console.error(`Error loading tape ${currentVideoIndex + 1}.`); console.error(`Error loading tape ${state.currentVideoIndex + 1}.`);
console.error('Video Load Error:', e); console.error('Video Load Error:', e);
}; };
} }
// --- Cycle to the next video --- // --- Cycle to the next video ---
function playNextVideo() { export function playNextVideo() {
// Determine the next index, cycling back to 0 if we reach the end // Determine the next index, cycling back to 0 if we reach the end
let nextIndex = currentVideoIndex + 1; let nextIndex = state.currentVideoIndex + 1;
if (nextIndex < videoUrls.length) { if (nextIndex < state.videoUrls.length) {
baseTime += videoElement.duration; state.baseTime += state.videoElement.duration;
} }
playVideoByIndex(nextIndex); playVideoByIndex(nextIndex);
} }
// --- Video Loading Logic (handles multiple files) --- // --- Video Loading Logic (handles multiple files) ---
function loadVideoFile(event) { export function loadVideoFile(event) {
const files = event.target.files; const files = event.target.files;
if (files.length === 0) { if (files.length === 0) {
console.info('File selection cancelled.'); console.info('File selection cancelled.');
@ -86,25 +89,25 @@ function loadVideoFile(event) {
} }
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks // 1. Clear previous URLs and revoke object URLs to prevent memory leaks
videoUrls.forEach(url => URL.revokeObjectURL(url)); state.videoUrls.forEach(url => URL.revokeObjectURL(url));
videoUrls = []; state.videoUrls = [];
// 2. Populate the new videoUrls array // 2. Populate the new videoUrls array
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
const file = files[i]; const file = files[i];
if (file.type.startsWith('video/')) { if (file.type.startsWith('video/')) {
videoUrls.push(URL.createObjectURL(file)); state.videoUrls.push(URL.createObjectURL(file));
} }
} }
if (videoUrls.length === 0) { if (state.videoUrls.length === 0) {
console.info('No valid video files selected.'); console.info('No valid video files selected.');
return; return;
} }
// 3. Start playback of the first video // 3. Start playback of the first video
console.info(`Loaded ${videoUrls.length} tapes. Starting playback...`); console.info(`Loaded ${state.videoUrls.length} tapes. Starting playback...`);
loadTapeButton.classList.add("hidden"); state.loadTapeButton.classList.add("hidden");
const startDelay = 5; const startDelay = 5;
console.info(`Video will start in ${startDelay} seconds.`); console.info(`Video will start in ${startDelay} seconds.`);