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 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
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) {
|
constructor(scene) {
|
||||||
this.effects = [];
|
this.effects = [];
|
||||||
this._initializeEffects(scene);
|
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) {
|
constructor(scene) {
|
||||||
this.dust = null;
|
this.dust = null;
|
||||||
this._create(scene);
|
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;
|
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];
|
||||||
|
|||||||
@ -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 ---
|
// --- 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();
|
||||||
|
|
||||||
// --- 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 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
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) ---
|
// --- 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);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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.`);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user