music-video-gen/magic-mirror/src/scene/tv-set.js
2025-11-19 22:11:10 +01:00

250 lines
9.0 KiB
JavaScript

import * as THREE from 'three';
import { state } from '../state.js';
import { createVcr } from './vcr.js';
import { screenVertexShader, screenFragmentShader } from '../shaders/screen-shaders.js';
export function createTvSet(x, z, rotY) {
// --- Materials (MeshPhongMaterial) ---
const tvPlastic = new THREE.MeshPhongMaterial({ color: 0x4d4d4d, shininess: 30 });
const tvGroup = new THREE.Group();
// --- TV Table Dimensions & Material ---
const woodColor = 0x5a3e36; // Dark brown wood
const tableHeight = 0.7; // Height from floor to top surface
const tableWidth = 2.0;
const tableDepth = 1.0;
const legThickness = 0.05;
const shelfThickness = 0.03;
// Use standard material for realistic shadowing
const material = new THREE.MeshStandardMaterial({ color: woodColor, roughness: 0.8, metalness: 0.1 });
// VCR gap dimensions calculation
const shelfGap = 0.2; // Height of the VCR opening
const shelfY = tableHeight - shelfGap - (shelfThickness / 2); // Y position of the bottom shelf
// 2. Table Top
const topGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
const tableTop = new THREE.Mesh(topGeometry, material);
tableTop.position.set(0, tableHeight, 0);
tableTop.castShadow = true;
tableTop.receiveShadow = true;
tvGroup.add(tableTop);
// 3. VCR Shelf (Middle Shelf)
const shelfGeometry = new THREE.BoxGeometry(tableWidth, shelfThickness, tableDepth);
const vcrShelf = new THREE.Mesh(shelfGeometry, material);
vcrShelf.position.set(0, shelfY, 0);
vcrShelf.castShadow = true;
vcrShelf.receiveShadow = true;
tvGroup.add(vcrShelf);
// 4. Side Walls for VCR Compartment (NEW CODE)
const wallHeight = shelfGap; // Height is the gap itself
const wallThickness = shelfThickness; // Reuse the shelf thickness for the wall width/depth
const wallGeometry = new THREE.BoxGeometry(wallThickness, wallHeight, tableDepth);
// Calculate the Y center position for the wall
const wallYCenter = tableHeight - (shelfThickness / 2) - (wallHeight / 2);
// Calculate the X position to be flush with the table sides
const wallXPosition = (tableWidth / 2) - (wallThickness / 2);
// Left Wall
const sideWallLeft = new THREE.Mesh(wallGeometry, material);
sideWallLeft.position.set(-wallXPosition, wallYCenter, 0);
sideWallLeft.castShadow = true;
sideWallLeft.receiveShadow = true;
tvGroup.add(sideWallLeft);
// Right Wall
const sideWallRight = new THREE.Mesh(wallGeometry, material);
sideWallRight.position.set(wallXPosition, wallYCenter, 0);
sideWallRight.castShadow = true;
sideWallRight.receiveShadow = true;
tvGroup.add(sideWallRight);
// 5. Legs
const legHeight = shelfY; // Legs go from the floor (y=0) to the shelf (y=shelfY)
const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
// Utility function to create and position a leg
const createLeg = (x, z) => {
const leg = new THREE.Mesh(legGeometry, material);
// Position the leg so the center is at half its height
leg.position.set(x, legHeight / 2, z);
leg.castShadow = true;
leg.receiveShadow = true;
return leg;
};
// Calculate offsets for positioning the legs near the corners
const offset = (tableWidth / 2) - (legThickness * 2);
const depthOffset = (tableDepth / 2) - (legThickness * 2);
// Front Left
tvGroup.add(createLeg(-offset, depthOffset));
// Front Right
tvGroup.add(createLeg(offset, depthOffset));
// Back Left
tvGroup.add(createLeg(-offset, -depthOffset));
// Back Right
tvGroup.add(createLeg(offset, -depthOffset));
// --- 2. The TV box ---
const cabinetGeometry = new THREE.BoxGeometry(1.9, 1.5, 1.0);
const cabinet = new THREE.Mesh(cabinetGeometry, tvPlastic);
cabinet.position.y = 1.51;
cabinet.castShadow = true;
cabinet.receiveShadow = true;
tvGroup.add(cabinet);
// --- 3. Screen Frame ---
const frameGeometry = new THREE.BoxGeometry(1.7, 1.3, 0.1);
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 20 });
const frame = new THREE.Mesh(frameGeometry, frameMaterial);
frame.position.set(0, 1.5, 0.68);
frame.castShadow = true;
frame.receiveShadow = true;
tvGroup.add(frame);
// --- 4. Curved Screen (CRT Effect) ---
const screenRadius = 3.0; // Radius for the subtle curve
const screenWidth = 1.6;
const screenHeight = 1.2;
const thetaLength = screenWidth / screenRadius; // Calculate angle needed for the arc
// Use CylinderGeometry as a segment
const screenGeometry = new THREE.CylinderGeometry(
screenRadius, screenRadius,
screenHeight, // Cylinder height is the vertical dimension of the screen
32,
1,
true,
(Math.PI / 2) - (thetaLength / 2), // Start angle to center the arc
thetaLength // Arc length (width)
);
// Rotate the cylinder segment:
// 1. Rotate around X-axis by 90 degrees to lay the height (Y) along Z (depth).
//screenGeometry.rotateX(Math.PI / 2);
// 2. Rotate around Y-axis by 90 degrees to align the segment's arc across the X-axis (width).
screenGeometry.rotateY(-Math.PI/2);
const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
state.tvScreen = new THREE.Mesh(screenGeometry, screenMaterial);
// Position the curved screen
state.tvScreen.position.set(0.0, 1.5, -2.1);
setTvScreenOffMaterial();
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)
state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
state.screenLight.position.set(0, 1.5, 1.0);
// Screen light casts shadows
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);
state.scene.add(tvGroup);
}
function setTvScreenOffMaterial() {
if (state.tvScreen.material) {
state.tvScreen.material.dispose();
}
state.tvScreen.material = new THREE.MeshPhongMaterial({
color: 0x203530,
shininess: 45,
specular: 0x111111,
});
state.tvScreen.material.needsUpdate = true;
}
export function turnTvScreenOff() {
if (state.tvScreenPowered) {
state.tvScreenPowered = false;
setScreenEffect(2, () => {
setTvScreenOffMaterial();
state.screenLight.intensity = 0.0;
}); // Trigger power down
}
}
export function turnTvScreenOn() {
if (state.tvScreen.material) {
state.tvScreen.material.dispose();
}
state.tvScreen.material = new THREE.ShaderMaterial({
uniforms: {
videoTexture: { value: state.videoTexture },
u_effect_type: { value: 0.0 },
u_effect_strength: { value: 0.0 },
},
vertexShader: screenVertexShader,
fragmentShader: screenFragmentShader,
transparent: true,
});
state.tvScreen.material.needsUpdate = true;
if (!state.tvScreenPowered) {
state.tvScreenPowered = true;
setScreenEffect(1); // Trigger warm-up
}
}
/**
* Controls the warm-up and power-down effects on the TV screen.
* @param {number} effectType - 0 normal, 1 for warm-up, 2 for power-down.
* @param {function} onComplete - Optional callback when the animation finishes.
*/
export function setScreenEffect(effectType, onComplete) {
const material = state.tvScreen.material;
if (!material.uniforms) return;
state.screenEffect.active = true;
state.screenEffect.type = effectType;
state.screenEffect.startTime = state.clock.getElapsedTime() * 1000;
state.screenEffect.onComplete = onComplete;
}
/**
* Updates the screen effect animation. Should be called in the main render loop.
*/
export function updateScreenEffect() {
if (!state.screenEffect.active) return;
const material = state.tvScreen.material;
if (!material.uniforms) return;
const elapsedTime = (state.clock.getElapsedTime() * 1000) - state.screenEffect.startTime;
const progress = Math.min(elapsedTime / state.screenEffect.duration, 1.0);
const easedProgress = state.screenEffect.easing(progress);
material.uniforms.u_effect_type.value = state.screenEffect.type;
material.uniforms.u_effect_strength.value = easedProgress;
if (progress >= 1.0) {
state.screenEffect.active = false;
material.uniforms.u_effect_strength.value = (state.screenEffect.type === 2) ? 1.0 : 0.0; // Final state
if (state.screenEffect.onComplete) {
state.screenEffect.onComplete();
}
material.uniforms.u_effect_type.value = 0.0; // Reset effect type
}
}