Refactoring: add Vite to serve JS modules
This commit is contained in:
parent
75c87c9d03
commit
1c2a3c6d3a
1
tv-player/.gitignore
vendored
Normal file
1
tv-player/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
node_modules
|
||||
@ -4,9 +4,8 @@
|
||||
<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="./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>
|
||||
<style>
|
||||
/* Dark room aesthetic */
|
||||
@ -45,16 +44,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="./src/global-variables.js"></script>
|
||||
<script src="./src/utils.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>
|
||||
<!-- Main entry point for the application -->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
|
||||
<!-- 3D Canvas will be injected here by Three.js -->
|
||||
</body>
|
||||
|
||||
1069
tv-player/package-lock.json
generated
Normal file
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
17
tv-player/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,7 @@
|
||||
class EffectsManager {
|
||||
import { DustEffect } from './effects_dust.js';
|
||||
import { FliesEffect } from './effects_flies.js';
|
||||
|
||||
export class EffectsManager {
|
||||
constructor(scene) {
|
||||
this.effects = [];
|
||||
this._initializeEffects(scene);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
110
tv-player/src/core/animate.js
Normal file
110
tv-player/src/core/animate.js
Normal 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);
|
||||
}
|
||||
62
tv-player/src/core/init.js
Normal file
62
tv-player/src/core/init.js
Normal 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();
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
class DustEffect {
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class DustEffect {
|
||||
constructor(scene) {
|
||||
this.dust = null;
|
||||
this._create(scene);
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from './state.js';
|
||||
import { degToRad } from './utils.js';
|
||||
|
||||
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) {
|
||||
this.flies = [];
|
||||
this._setupFlies(scene);
|
||||
@ -8,9 +19,9 @@ class FliesEffect {
|
||||
|
||||
_randomFlyTarget() {
|
||||
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),
|
||||
(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) {
|
||||
raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
|
||||
const intersects = raycaster.intersectObjects(landingSurfaces, false);
|
||||
state.raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
|
||||
const intersects = state.raycaster.intersectObjects(state.landingSurfaces, false);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
|
||||
@ -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
6
tv-player/src/main.js
Normal file
@ -0,0 +1,6 @@
|
||||
import * as THREE from 'three';
|
||||
import './tailwind-config.js';
|
||||
import { init } from './core/init.js';
|
||||
|
||||
// Start everything
|
||||
init();
|
||||
@ -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 ---
|
||||
function createRoomWalls() {
|
||||
const wallTexture = loader.load('./textures/wall.jpg');
|
||||
const wallTexture = state.loader.load('/textures/wall.jpg');
|
||||
wallTexture.wrapS = THREE.RepeatWrapping;
|
||||
wallTexture.wrapT = THREE.RepeatWrapping;
|
||||
|
||||
@ -13,34 +18,34 @@ function createRoomWalls() {
|
||||
});
|
||||
|
||||
// 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);
|
||||
const backWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
|
||||
backWall.position.set(0, state.roomHeight / 2, -state.roomSize / 2);
|
||||
backWall.receiveShadow = true;
|
||||
scene.add(backWall);
|
||||
state.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);
|
||||
const frontWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
|
||||
frontWall.position.set(0, state.roomHeight / 2, state.roomSize / 2);
|
||||
frontWall.rotation.y = Math.PI;
|
||||
frontWall.receiveShadow = true;
|
||||
scene.add(frontWall);
|
||||
state.scene.add(frontWall);
|
||||
|
||||
// 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.position.set(-roomSize / 2, roomHeight / 2, 0);
|
||||
leftWall.position.set(-state.roomSize / 2, state.roomHeight / 2, 0);
|
||||
leftWall.receiveShadow = true;
|
||||
scene.add(leftWall);
|
||||
state.scene.add(leftWall);
|
||||
|
||||
// 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.position.set(roomSize / 2, roomHeight / 2, 0);
|
||||
rightWall.position.set(state.roomSize / 2, state.roomHeight / 2, 0);
|
||||
rightWall.receiveShadow = true;
|
||||
scene.add(rightWall);
|
||||
state.scene.add(rightWall);
|
||||
|
||||
// 5. Ceiling
|
||||
const ceilingGeometry = new THREE.PlaneGeometry(roomSize, roomSize);
|
||||
const ceilingGeometry = new THREE.PlaneGeometry(state.roomSize, state.roomSize);
|
||||
const ceilingTexture = wallTexture;
|
||||
ceilingTexture.repeat.set(4, 4);
|
||||
// USING MeshPhongMaterial
|
||||
@ -53,16 +58,16 @@ function createRoomWalls() {
|
||||
|
||||
const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
|
||||
ceiling.rotation.x = Math.PI / 2;
|
||||
ceiling.position.set(0, roomHeight, 0);
|
||||
ceiling.position.set(0, state.roomHeight, 0);
|
||||
ceiling.receiveShadow = true;
|
||||
scene.add(ceiling);
|
||||
state.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({
|
||||
const nightSkyMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0x0a1a3a,
|
||||
emissive: 0x0a1a3a,
|
||||
emissiveIntensity: 0.5,
|
||||
@ -70,13 +75,13 @@ function createRoomWalls() {
|
||||
});
|
||||
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);
|
||||
const windowZ = -state.roomSize / 2 + 0.001;
|
||||
windowPane.position.set(-3.5, state.roomHeight * 0.5 + 1.5, windowZ);
|
||||
state.scene.add(windowPane);
|
||||
}
|
||||
|
||||
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 shelfDepth = 0.35;
|
||||
@ -126,7 +131,7 @@ function createBookshelf(x, z, rotationY, uniqueSeed) {
|
||||
topPanel.castShadow = true;
|
||||
shelfGroup.add(topPanel);
|
||||
|
||||
landingSurfaces.push(topPanel);
|
||||
state.landingSurfaces.push(topPanel);
|
||||
|
||||
// 2. Individual Shelves & Books
|
||||
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) {
|
||||
@ -224,7 +229,7 @@ function createDoor(x, z, rotY) {
|
||||
knob.castShadow = true;
|
||||
doorGroup.add(knob);
|
||||
|
||||
scene.add(doorGroup);
|
||||
state.scene.add(doorGroup);
|
||||
}
|
||||
|
||||
// --- VCR Model Function ---
|
||||
@ -267,8 +272,8 @@ function createVcr() {
|
||||
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
|
||||
const vcrDisplayLight = new THREE.PointLight(0x00ff44, 0.03, 1.8);
|
||||
vcrDisplayLight.position.set(0.23, 0.03, 0.35 + 0.03);
|
||||
vcrDisplayLight.castShadow = true;
|
||||
vcrDisplayLight.shadow.mapSize.width = 256;
|
||||
vcrDisplayLight.shadow.mapSize.height = 256;
|
||||
@ -413,42 +418,42 @@ function createTvSet(x, z, rotY) {
|
||||
screenGeometry.rotateY(-Math.PI/2);
|
||||
|
||||
const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
||||
tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
|
||||
state.tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
|
||||
|
||||
// Position the curved screen
|
||||
tvScreen.position.set(0.0, 1.5, -2.1);
|
||||
tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
state.tvScreen.position.set(0.0, 1.5, -2.1);
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x0a0a0a, // Deep black
|
||||
shininess: 5,
|
||||
specular: 0x111111
|
||||
});
|
||||
tvScreen.material.needsUpdate = true;
|
||||
tvGroup.add(tvScreen);
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
tvGroup.add(state.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);
|
||||
state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
|
||||
state.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);
|
||||
state.screenLight.castShadow = true;
|
||||
state.screenLight.shadow.mapSize.width = 1024;
|
||||
state.screenLight.shadow.mapSize.height = 1024;
|
||||
state.screenLight.shadow.camera.near = 0.2;
|
||||
state.screenLight.shadow.camera.far = 5;
|
||||
tvGroup.add(state.screenLight);
|
||||
|
||||
// -- VCR --
|
||||
const vcr = createVcr();
|
||||
vcr.position.set(-0.3, 0.6, 0.05);
|
||||
tvGroup.add(vcr);
|
||||
|
||||
scene.add(tvGroup);
|
||||
state.scene.add(tvGroup);
|
||||
}
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
function createSceneObjects() {
|
||||
export function createSceneObjects() {
|
||||
// --- Materials (MeshPhongMaterial) ---
|
||||
const darkWood = new THREE.MeshPhongMaterial({ color: 0x3d352e, shininess: 10 });
|
||||
const darkMetal = new THREE.MeshPhongMaterial({
|
||||
@ -460,22 +465,33 @@ function createSceneObjects() {
|
||||
|
||||
// --- 1. Floor ---
|
||||
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.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 floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = 0;
|
||||
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();
|
||||
|
||||
// --- 5. Lamp (On the table, right side) ---
|
||||
// 3. Lighting (Minimal and focused)
|
||||
const ambientLight = new THREE.AmbientLight(0x111111, 1);
|
||||
state.scene.add(ambientLight);
|
||||
|
||||
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 lampPole = new THREE.CylinderGeometry(0.02, 0.02, 1.5, 8);
|
||||
const lampShade = new THREE.ConeGeometry(0.2, 0.4, 16);
|
||||
@ -495,34 +511,34 @@ function createSceneObjects() {
|
||||
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;
|
||||
state.lampLightPoint = new THREE.PointLight(0xffaa00, state.originalLampIntensity, 4);
|
||||
state.lampLightPoint.position.set(-0.01, state.roomHeight-0.9, 0.01);
|
||||
state.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;
|
||||
state.lampLightPoint.shadow.mapSize.width = 512;
|
||||
state.lampLightPoint.shadow.mapSize.height = 512;
|
||||
state.lampLightPoint.shadow.camera.near = 0.1;
|
||||
state.lampLightPoint.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
|
||||
state.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;
|
||||
state.lampLightSpot = new THREE.SpotLight(0xffaa00, state.originalLampIntensity, 4);
|
||||
state.lampLightSpot.position.set(-0.01, 1.0, 0.01);
|
||||
state.lampLightSpot.target.position.set(0, 5, 0);
|
||||
state.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;
|
||||
state.lampLightSpot.shadow.mapSize.width = 512;
|
||||
state.lampLightSpot.shadow.mapSize.height = 512;
|
||||
state.lampLightSpot.shadow.camera.near = 0.1;
|
||||
state.lampLightSpot.shadow.camera.far = 4; // Matches the light's attenuation distance (4)
|
||||
state.lampLightSpot.penumbra = 1;
|
||||
|
||||
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);
|
||||
lampGroup.add(baseMesh, poleMesh, shadeMesh, state.lampLightSpot, state.lampLightSpot.target, state.lampLightPoint);
|
||||
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) ---
|
||||
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.rotation.y = -Math.PI / 10;
|
||||
cameraMesh.castShadow = true; cameraMesh.receiveShadow = true;
|
||||
scene.add(cameraMesh);
|
||||
state.scene.add(cameraMesh);
|
||||
|
||||
// --- 8. Pizza Box ---
|
||||
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.rotation.y = Math.PI / 5;
|
||||
pizzaBox.castShadow = true; pizzaBox.receiveShadow = true;
|
||||
scene.add(pizzaBox);
|
||||
state.scene.add(pizzaBox);
|
||||
|
||||
// --- 8. Cassette ---
|
||||
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.rotation.y = Math.PI / 3;
|
||||
cassette.castShadow = true; cassette.receiveShadow = true;
|
||||
scene.add(cassette);
|
||||
state.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);
|
||||
createDoor(state.roomSize/2, -state.roomSize/2 * 0.5, -Math.PI/2);
|
||||
createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.2, Math.PI/2, 0);
|
||||
createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.7, Math.PI/2, 0);
|
||||
createBookshelf(state.roomSize/2 * 0.7, -state.roomSize/2+0.3, 0, 1);
|
||||
|
||||
}
|
||||
|
||||
50
tv-player/src/state.js
Normal file
50
tv-player/src/state.js
Normal 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,
|
||||
};
|
||||
}
|
||||
@ -1,5 +1,8 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from './state.js';
|
||||
|
||||
// --- Utility: Random Color (seeded) ---
|
||||
function getRandomColor() {
|
||||
export function getRandomColor() {
|
||||
const hue = seededRandom();
|
||||
const saturation = 0.6 + seededRandom() * 0.4;
|
||||
const lightness = 0.3 + seededRandom() * 0.4;
|
||||
@ -11,20 +14,20 @@ function getRandomColor() {
|
||||
* @param {number} degrees
|
||||
* @returns {number}
|
||||
*/
|
||||
function degToRad(degrees) {
|
||||
export function degToRad(degrees) {
|
||||
return degrees * (Math.PI / 180);
|
||||
}
|
||||
|
||||
// --- Seedable Random Number Generator (Mulberry32) ---
|
||||
function seededRandom() {
|
||||
let t = seed += 0x6D2B79F5;
|
||||
export function seededRandom() {
|
||||
let t = state.seed += 0x6D2B79F5;
|
||||
t = Math.imul(t ^ t >>> 15, t | 1);
|
||||
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
|
||||
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
||||
}
|
||||
|
||||
// --- Helper function to format seconds into MM:SS ---
|
||||
function formatTime(seconds) {
|
||||
export function formatTime(seconds) {
|
||||
if (isNaN(seconds) || seconds === Infinity || seconds < 0) return '--:--';
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.floor(seconds % 60);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
let vcrDisplayTexture;
|
||||
let blinkState = false;
|
||||
let lastBlinkToggleTime = 0;
|
||||
import * as THREE from 'three';
|
||||
import { state } from './state.js';
|
||||
import { formatTime } from './utils.js';
|
||||
|
||||
// --- Segment Display Definitions ---
|
||||
|
||||
// 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;
|
||||
|
||||
// Draw Playback Arrow
|
||||
if (isVideoLoaded && videoElement.readyState >= 3) {
|
||||
if (state.isVideoLoaded && state.videoElement.readyState >= 3) {
|
||||
drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
|
||||
}
|
||||
currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
|
||||
@ -160,7 +161,7 @@ function drawSegmentDisplay(ctx, timeString) {
|
||||
const char = timeString[i];
|
||||
|
||||
if (char === ':') {
|
||||
drawColon(ctx, currentX, currentY, digitHeight, blinkState); // Pass blinkState
|
||||
drawColon(ctx, currentX, currentY, digitHeight, state.blinkState); // Pass blinkState
|
||||
currentX += colonWidth;
|
||||
} else if (char >= '0' && char <= '9') {
|
||||
drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
|
||||
@ -175,7 +176,7 @@ function drawSegmentDisplay(ctx, timeString) {
|
||||
}
|
||||
|
||||
// --- VCR Display Functions ---
|
||||
function createVcrDisplay() {
|
||||
export function createVcrDisplay() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 160; // Increased width for arrow and better spacing
|
||||
canvas.height = 32;
|
||||
@ -184,27 +185,28 @@ function createVcrDisplay() {
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
vcrDisplayTexture = new THREE.CanvasTexture(canvas);
|
||||
vcrDisplayTexture.needsUpdate = true;
|
||||
state.vcrDisplayTexture = new THREE.CanvasTexture(canvas);
|
||||
state.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,
|
||||
const displayMaterial = new THREE.MeshPhongMaterial({
|
||||
map: state.vcrDisplayTexture,
|
||||
side: THREE.FrontSide,
|
||||
color: 0xffffff,
|
||||
transparent: true,
|
||||
emissive: 0x00ff44,
|
||||
emissiveIntensity: 0.1
|
||||
emissiveIntensity: 0.05,
|
||||
shininess: 0
|
||||
});
|
||||
|
||||
const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
|
||||
return displayMesh;
|
||||
}
|
||||
|
||||
function updateVcrDisplay(time) {
|
||||
if (!vcrDisplayTexture) return;
|
||||
export function updateVcrDisplay(time) {
|
||||
if (!state.vcrDisplayTexture) return;
|
||||
|
||||
const canvas = vcrDisplayTexture.image;
|
||||
const canvas = state.vcrDisplayTexture.image;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const timeString = formatTime(time);
|
||||
@ -212,5 +214,5 @@ function updateVcrDisplay(time) {
|
||||
// Uses the new segment drawing function with ghosting, including blinkState for colon
|
||||
drawSegmentDisplay(ctx, timeString);
|
||||
|
||||
vcrDisplayTexture.needsUpdate = true;
|
||||
state.vcrDisplayTexture.needsUpdate = true;
|
||||
}
|
||||
|
||||
@ -1,84 +1,87 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from './state.js';
|
||||
|
||||
// --- Play video by index ---
|
||||
function playVideoByIndex(index) {
|
||||
currentVideoIndex = index;
|
||||
const url = videoUrls[index];
|
||||
export function playVideoByIndex(index) {
|
||||
state.currentVideoIndex = index;
|
||||
const url = state.videoUrls[index];
|
||||
|
||||
// Dispose of previous texture to free resources
|
||||
if (videoTexture) {
|
||||
videoTexture.dispose();
|
||||
videoTexture = null;
|
||||
if (state.videoTexture) {
|
||||
state.videoTexture.dispose();
|
||||
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.');
|
||||
screenLight.intensity = 0.0;
|
||||
tvScreen.material.dispose();
|
||||
tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
state.screenLight.intensity = 0.0;
|
||||
state.tvScreen.material.dispose();
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x0a0a0a, // Deep black
|
||||
shininess: 5,
|
||||
specular: 0x111111
|
||||
});
|
||||
tvScreen.material.needsUpdate = true;
|
||||
isVideoLoaded = false;
|
||||
lastUpdateTime = -1; // force VCR to redraw
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
state.isVideoLoaded = false;
|
||||
state.lastUpdateTime = -1; // force VCR to redraw
|
||||
return;
|
||||
}
|
||||
|
||||
videoElement.src = url;
|
||||
videoElement.muted = true;
|
||||
videoElement.load();
|
||||
state.videoElement.src = url;
|
||||
state.videoElement.muted = true;
|
||||
state.videoElement.load();
|
||||
|
||||
// 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
|
||||
videoTexture = new THREE.VideoTexture(videoElement);
|
||||
videoTexture.minFilter = THREE.LinearFilter;
|
||||
videoTexture.magFilter = THREE.LinearFilter;
|
||||
videoTexture.format = THREE.RGBAFormat;
|
||||
videoTexture.needsUpdate = true;
|
||||
state.videoTexture = new THREE.VideoTexture(state.videoElement);
|
||||
state.videoTexture.minFilter = THREE.LinearFilter;
|
||||
state.videoTexture.magFilter = THREE.LinearFilter;
|
||||
state.videoTexture.format = THREE.RGBAFormat;
|
||||
state.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;
|
||||
state.tvScreen.material.dispose();
|
||||
state.tvScreen.material = new THREE.MeshBasicMaterial({ map: state.videoTexture });
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
|
||||
// 3. Start playback
|
||||
videoElement.play().then(() => {
|
||||
isVideoLoaded = true;
|
||||
state.videoElement.play().then(() => {
|
||||
state.isVideoLoaded = true;
|
||||
// Use the defined base intensity for screen glow
|
||||
screenLight.intensity = originalScreenIntensity;
|
||||
state.screenLight.intensity = state.originalScreenIntensity;
|
||||
// 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 => {
|
||||
screenLight.intensity = originalScreenIntensity * 0.5; // Dim the light if playback fails
|
||||
console.error(`Playback blocked for tape ${currentVideoIndex + 1}. Click Next Tape to try again.`);
|
||||
state.screenLight.intensity = state.originalScreenIntensity * 0.5; // Dim the light if playback fails
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
videoElement.onerror = (e) => {
|
||||
screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
|
||||
console.error(`Error loading tape ${currentVideoIndex + 1}.`);
|
||||
state.videoElement.onerror = (e) => {
|
||||
state.screenLight.intensity = 0.1; // Keep minimum intensity for shadow map
|
||||
console.error(`Error loading tape ${state.currentVideoIndex + 1}.`);
|
||||
console.error('Video Load Error:', e);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Cycle to the next video ---
|
||||
function playNextVideo() {
|
||||
export function playNextVideo() {
|
||||
// Determine the next index, cycling back to 0 if we reach the end
|
||||
let nextIndex = currentVideoIndex + 1;
|
||||
if (nextIndex < videoUrls.length) {
|
||||
baseTime += videoElement.duration;
|
||||
let nextIndex = state.currentVideoIndex + 1;
|
||||
if (nextIndex < state.videoUrls.length) {
|
||||
state.baseTime += state.videoElement.duration;
|
||||
}
|
||||
playVideoByIndex(nextIndex);
|
||||
}
|
||||
|
||||
|
||||
// --- Video Loading Logic (handles multiple files) ---
|
||||
function loadVideoFile(event) {
|
||||
export function loadVideoFile(event) {
|
||||
const files = event.target.files;
|
||||
if (files.length === 0) {
|
||||
console.info('File selection cancelled.');
|
||||
@ -86,25 +89,25 @@ function loadVideoFile(event) {
|
||||
}
|
||||
|
||||
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks
|
||||
videoUrls.forEach(url => URL.revokeObjectURL(url));
|
||||
videoUrls = [];
|
||||
state.videoUrls.forEach(url => URL.revokeObjectURL(url));
|
||||
state.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));
|
||||
state.videoUrls.push(URL.createObjectURL(file));
|
||||
}
|
||||
}
|
||||
|
||||
if (videoUrls.length === 0) {
|
||||
if (state.videoUrls.length === 0) {
|
||||
console.info('No valid video files selected.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Start playback of the first video
|
||||
console.info(`Loaded ${videoUrls.length} tapes. Starting playback...`);
|
||||
loadTapeButton.classList.add("hidden");
|
||||
console.info(`Loaded ${state.videoUrls.length} tapes. Starting playback...`);
|
||||
state.loadTapeButton.classList.add("hidden");
|
||||
|
||||
const startDelay = 5;
|
||||
console.info(`Video will start in ${startDelay} seconds.`);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user