New project: Magic mirror
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
const FRAME_DEPTH = 0.05;
|
||||
const TRANSITION_DURATION = 5000;
|
||||
const IMAGE_CHANGE_CHANCE = 0.0001;
|
||||
|
||||
export class PictureFrame {
|
||||
constructor(scene, { position, width, height, imageUrls, rotationY = 0 }) {
|
||||
if (!imageUrls || imageUrls.length === 0) {
|
||||
throw new Error('PictureFrame requires at least one image URL in the imageUrls array.');
|
||||
}
|
||||
|
||||
this.scene = scene;
|
||||
this.mesh = this._createPictureFrame(width, height, imageUrls, 0.05);
|
||||
|
||||
this.mesh.position.copy(position);
|
||||
this.mesh.rotation.y = rotationY;
|
||||
|
||||
this.isTransitioning = false;
|
||||
this.transitionStartTime = 0;
|
||||
|
||||
this.scene.add(this.mesh);
|
||||
}
|
||||
|
||||
_createPictureFrame(width, height, imageUrls, frameThickness) {
|
||||
const paintingGroup = new THREE.Group();
|
||||
|
||||
// 1. Create the wooden frame
|
||||
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513 }); // SaddleBrown
|
||||
|
||||
const topFrame = new THREE.Mesh(new THREE.BoxGeometry(width + 2 * frameThickness, frameThickness, FRAME_DEPTH), frameMaterial);
|
||||
topFrame.position.y = height / 2 + frameThickness / 2;
|
||||
topFrame.castShadow = true;
|
||||
topFrame.receiveShadow = true;
|
||||
paintingGroup.add(topFrame);
|
||||
|
||||
const bottomFrame = new THREE.Mesh(new THREE.BoxGeometry(width + 2 * frameThickness, frameThickness, FRAME_DEPTH), frameMaterial);
|
||||
bottomFrame.position.y = -height / 2 - frameThickness / 2;
|
||||
bottomFrame.castShadow = true;
|
||||
bottomFrame.receiveShadow = true;
|
||||
paintingGroup.add(bottomFrame);
|
||||
|
||||
const leftFrame = new THREE.Mesh(new THREE.BoxGeometry(frameThickness, height, FRAME_DEPTH), frameMaterial);
|
||||
leftFrame.position.x = -width / 2 - frameThickness / 2;
|
||||
leftFrame.castShadow = true;
|
||||
leftFrame.receiveShadow = true;
|
||||
paintingGroup.add(leftFrame);
|
||||
|
||||
const rightFrame = new THREE.Mesh(new THREE.BoxGeometry(frameThickness, height, FRAME_DEPTH), frameMaterial);
|
||||
rightFrame.position.x = width / 2 + frameThickness / 2;
|
||||
rightFrame.castShadow = true;
|
||||
rightFrame.receiveShadow = true;
|
||||
paintingGroup.add(rightFrame);
|
||||
|
||||
// 2. Create the picture canvases with textures
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
this.textures = imageUrls.map(url => textureLoader.load(url));
|
||||
this.currentTextureIndex = 0;
|
||||
|
||||
const pictureGeometry = new THREE.PlaneGeometry(width, height);
|
||||
|
||||
// Create two picture planes for cross-fading
|
||||
this.pictureBack = new THREE.Mesh(pictureGeometry, new THREE.MeshPhongMaterial({ map: this.textures[this.currentTextureIndex] }));
|
||||
this.pictureBack.position.z = 0.001;
|
||||
this.pictureBack.receiveShadow = true;
|
||||
paintingGroup.add(this.pictureBack);
|
||||
|
||||
this.pictureFront = new THREE.Mesh(pictureGeometry, new THREE.MeshPhongMaterial({ map: this.textures[this.currentTextureIndex], transparent: true, opacity: 0 }));
|
||||
this.pictureFront.position.z = 0.003; // Place slightly in front to avoid z-fighting
|
||||
this.pictureFront.receiveShadow = true;
|
||||
paintingGroup.add(this.pictureFront);
|
||||
|
||||
return paintingGroup;
|
||||
}
|
||||
|
||||
setPicture(index) {
|
||||
if (this.isTransitioning || index === this.currentTextureIndex || index < 0 || index >= this.textures.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTransitioning = true;
|
||||
this.transitionStartTime = Date.now();
|
||||
|
||||
// Front plane fades in with the new texture
|
||||
this.pictureFront.material.map = this.textures[index];
|
||||
this.pictureFront.material.opacity = 0;
|
||||
|
||||
this.nextTextureIndex = index;
|
||||
}
|
||||
|
||||
nextPicture() {
|
||||
this.setPicture((this.currentTextureIndex + 1) % this.textures.length);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (!this.isTransitioning) {
|
||||
if (Math.random() > 1.0 - IMAGE_CHANGE_CHANCE) {
|
||||
this.nextPicture();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedTime = Date.now() - this.transitionStartTime;
|
||||
const progress = Math.min(elapsedTime / TRANSITION_DURATION, 1.0);
|
||||
this.pictureFront.material.opacity = progress;
|
||||
|
||||
if (progress >= 1.0) {
|
||||
this.isTransitioning = false;
|
||||
this.currentTextureIndex = this.nextTextureIndex;
|
||||
|
||||
// Reset for next transition
|
||||
this.pictureBack.material.map = this.textures[this.currentTextureIndex];
|
||||
this.pictureFront.material.opacity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { getRandomColor, seededRandom } from '../utils.js';
|
||||
|
||||
export function createBookshelf(x, z, rotationY, uniqueSeed) {
|
||||
state.seed = uniqueSeed; // Reset seed for this specific shelf instance
|
||||
|
||||
const shelfHeight = 2.2;
|
||||
const shelfDepth = 0.35;
|
||||
const shelfWidth = 1.2;
|
||||
const numShelves = 6;
|
||||
const woodThickness = 0.04;
|
||||
const woodColor = 0x5c4033; // Darker, richer wood
|
||||
|
||||
const shelfGroup = new THREE.Group();
|
||||
shelfGroup.position.set(x, 0, z);
|
||||
shelfGroup.rotation.y = rotationY;
|
||||
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({ color: woodColor, shininess: 30 });
|
||||
|
||||
// 1. Build Frame (Hollow box)
|
||||
// Back Panel
|
||||
const backGeo = new THREE.BoxGeometry(shelfWidth, shelfHeight, woodThickness);
|
||||
const backPanel = new THREE.Mesh(backGeo, woodMaterial);
|
||||
backPanel.position.set(0, shelfHeight / 2, -shelfDepth / 2 + woodThickness / 2);
|
||||
backPanel.castShadow = true;
|
||||
backPanel.receiveShadow = true;
|
||||
shelfGroup.add(backPanel);
|
||||
|
||||
// Side Panels (Left & Right)
|
||||
const sideGeo = new THREE.BoxGeometry(woodThickness, shelfHeight, shelfDepth);
|
||||
const leftSide = new THREE.Mesh(sideGeo, woodMaterial);
|
||||
leftSide.position.set(-shelfWidth / 2 + woodThickness / 2, shelfHeight / 2, 0);
|
||||
leftSide.castShadow = true;
|
||||
leftSide.receiveShadow = true;
|
||||
shelfGroup.add(leftSide);
|
||||
|
||||
const rightSide = new THREE.Mesh(sideGeo, woodMaterial);
|
||||
rightSide.position.set(shelfWidth / 2 - woodThickness / 2, shelfHeight / 2, 0);
|
||||
rightSide.castShadow = true;
|
||||
rightSide.receiveShadow = true;
|
||||
shelfGroup.add(rightSide);
|
||||
|
||||
// Top & Bottom Panels
|
||||
const topBottomGeo = new THREE.BoxGeometry(shelfWidth, woodThickness, shelfDepth);
|
||||
const bottomPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
|
||||
bottomPanel.position.set(0, woodThickness / 2, 0);
|
||||
bottomPanel.receiveShadow = true;
|
||||
shelfGroup.add(bottomPanel);
|
||||
|
||||
const topPanel = new THREE.Mesh(topBottomGeo, woodMaterial);
|
||||
topPanel.position.set(0, shelfHeight - woodThickness / 2, 0);
|
||||
topPanel.castShadow = true;
|
||||
shelfGroup.add(topPanel);
|
||||
|
||||
state.landingSurfaces.push(topPanel);
|
||||
|
||||
// 2. Individual Shelves & Books
|
||||
const internalHeight = shelfHeight - (2 * woodThickness);
|
||||
const shelfSpacing = internalHeight / numShelves;
|
||||
const internalWidth = shelfWidth - (2 * woodThickness);
|
||||
|
||||
for (let i = 0; i < numShelves; i++) {
|
||||
const currentShelfY = woodThickness + (i * shelfSpacing);
|
||||
|
||||
// Shelf board (skip for the very bottom one as we have a bottom panel)
|
||||
if (i > 0) {
|
||||
const shelfBoard = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(internalWidth, woodThickness, shelfDepth - woodThickness), // Slightly shallower to fit inside back panel
|
||||
woodMaterial
|
||||
);
|
||||
shelfBoard.position.set(0, currentShelfY, woodThickness / 2); // Offset forward slightly
|
||||
shelfBoard.castShadow = true;
|
||||
shelfBoard.receiveShadow = true;
|
||||
shelfGroup.add(shelfBoard);
|
||||
}
|
||||
|
||||
// 3. Procedural Books
|
||||
let currentBookX = -internalWidth / 2 + 0.01; // Start at left inside edge
|
||||
const shelfSurfaceY = currentShelfY + woodThickness / 2;
|
||||
|
||||
while (currentBookX < internalWidth / 2 - 0.05) {
|
||||
// sizes vary
|
||||
const bookWidth = 0.02 + seededRandom() * 0.05;
|
||||
const bookHeight = (shelfSpacing * 0.6) + seededRandom() * (shelfSpacing * 0.1);
|
||||
const bookDepth = 0.15 + seededRandom() * 0.03;
|
||||
|
||||
if (currentBookX + bookWidth > internalWidth / 2) break;
|
||||
|
||||
const bookColor = getRandomColor();
|
||||
const bookMat = new THREE.MeshPhongMaterial({ color: bookColor, shininess: 60 });
|
||||
const bookGeo = new THREE.BoxGeometry(bookWidth, bookHeight, bookDepth);
|
||||
const book = new THREE.Mesh(bookGeo, bookMat);
|
||||
|
||||
// Position: Resting on shelf, pushed towards the back with slight random variation
|
||||
const depthVariation = seededRandom() * 0.05;
|
||||
book.position.set(
|
||||
currentBookX + bookWidth / 2,
|
||||
shelfSurfaceY + bookHeight / 2,
|
||||
-shelfDepth / 2 + woodThickness + bookDepth / 2 + depthVariation
|
||||
);
|
||||
|
||||
book.castShadow = true;
|
||||
book.receiveShadow = true;
|
||||
// Store original Y position and animation data
|
||||
book.userData.originalY = book.position.y;
|
||||
book.userData.levitateOffset = 0;
|
||||
book.userData.oscillationTime = Math.random() * Math.PI * 2; // Start at random phase
|
||||
shelfGroup.add(book);
|
||||
|
||||
if (Math.random() > 0.8) {
|
||||
state.books.push(book);
|
||||
}
|
||||
|
||||
currentBookX += bookWidth + 0.002; // Tiny gap between books
|
||||
|
||||
if (seededRandom() > 0.92) {
|
||||
currentBookX += bookWidth * 3; // random bigger gaps
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.scene.add(shelfGroup);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { screenVertexShader, screenFragmentShader } from '../shaders/screen-shaders.js';
|
||||
|
||||
export function createCrystalBall(x, z, rotY) {
|
||||
// --- Materials ---
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({ color: 0x5c4033, shininess: 30 });
|
||||
const standMaterial = new THREE.MeshPhongMaterial({ color: 0x3d2d1d, shininess: 50, specular: 0x444444 });
|
||||
|
||||
const ballGroup = new THREE.Group();
|
||||
|
||||
// --- 1. Small Pedestal Table ---
|
||||
const tableHeight = 0.7;
|
||||
const tableWidth = 1.0;
|
||||
const tableDepth = 1.0;
|
||||
const legThickness = 0.08;
|
||||
|
||||
// Table Top
|
||||
const topGeometry = new THREE.BoxGeometry(tableWidth, 0.05, tableDepth);
|
||||
const tableTop = new THREE.Mesh(topGeometry, woodMaterial);
|
||||
tableTop.position.y = tableHeight;
|
||||
tableTop.castShadow = true;
|
||||
tableTop.receiveShadow = true;
|
||||
ballGroup.add(tableTop);
|
||||
|
||||
// Legs
|
||||
const legHeight = tableHeight;
|
||||
const legGeometry = new THREE.BoxGeometry(legThickness, legHeight, legThickness);
|
||||
const legOffset = (tableWidth / 2) - (legThickness * 1.5);
|
||||
const depthOffset = (tableDepth / 2) - (legThickness * 1.5);
|
||||
|
||||
const createLeg = (lx, lz) => {
|
||||
const leg = new THREE.Mesh(legGeometry, woodMaterial);
|
||||
leg.position.set(lx, legHeight / 2, lz);
|
||||
leg.castShadow = true;
|
||||
leg.receiveShadow = true;
|
||||
return leg;
|
||||
};
|
||||
|
||||
ballGroup.add(createLeg(-legOffset, depthOffset));
|
||||
ballGroup.add(createLeg(legOffset, depthOffset));
|
||||
ballGroup.add(createLeg(-legOffset, -depthOffset));
|
||||
ballGroup.add(createLeg(legOffset, -depthOffset));
|
||||
|
||||
// --- 2. Crystal Ball Stand ---
|
||||
const standBaseGeo = new THREE.CylinderGeometry(0.25, 0.35, 0.1, 16);
|
||||
const standBase = new THREE.Mesh(standBaseGeo, standMaterial);
|
||||
standBase.position.y = tableHeight + 0.05;
|
||||
standBase.castShadow = true;
|
||||
standBase.receiveShadow = true;
|
||||
ballGroup.add(standBase);
|
||||
|
||||
const standNeckGeo = new THREE.CylinderGeometry(0.15, 0.20, 0.2, 12);
|
||||
const standNeck = new THREE.Mesh(standNeckGeo, standMaterial);
|
||||
standNeck.position.y = standBase.position.y + 0.15;
|
||||
standNeck.castShadow = true;
|
||||
standNeck.receiveShadow = true;
|
||||
ballGroup.add(standNeck);
|
||||
|
||||
// --- 3. The Crystal Ball ---
|
||||
const ballRadius = 0.35;
|
||||
const ballGeometry = new THREE.SphereGeometry(ballRadius, 64, 32);
|
||||
|
||||
// The 'tvScreen' from state will now be our crystal ball
|
||||
state.tvScreen = new THREE.Mesh(ballGeometry, new THREE.MeshBasicMaterial({ color: 0x000000 }));
|
||||
state.tvScreen.position.y = standNeck.position.y + 0.15 + ballRadius * 0.5;
|
||||
setCrystalBallOffMaterial(); // Set its initial "off" state
|
||||
ballGroup.add(state.tvScreen);
|
||||
|
||||
// --- 4. Light from the Crystal Ball ---
|
||||
state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
|
||||
state.screenLight.position.copy(state.tvScreen.position);
|
||||
state.screenLight.position.y += 0.1; // Position light slightly above the center
|
||||
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;
|
||||
ballGroup.add(state.screenLight);
|
||||
|
||||
// Position and rotate the entire group
|
||||
ballGroup.position.set(x, 0, z);
|
||||
ballGroup.rotation.y = rotY;
|
||||
|
||||
state.scene.add(ballGroup);
|
||||
}
|
||||
|
||||
function setCrystalBallOffMaterial() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// A slightly reflective, dark, magical-looking material for when it's off
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x100510,
|
||||
shininess: 100,
|
||||
specular: 0xeeeeff,
|
||||
transparent: true,
|
||||
opacity: 0.85
|
||||
});
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function turnTvScreenOff() {
|
||||
if (state.tvScreenPowered) {
|
||||
state.tvScreenPowered = false;
|
||||
setScreenEffect(2, () => {
|
||||
setCrystalBallOffMaterial();
|
||||
state.screenLight.intensity = 0.0;
|
||||
}); // Trigger power down effect
|
||||
}
|
||||
}
|
||||
|
||||
export function turnTvScreenOn() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// Use the same shader material as the TV for video playback
|
||||
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 power on effect
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (state.screenEffect.onComplete) {
|
||||
state.screenEffect.onComplete();
|
||||
}
|
||||
material.uniforms.u_effect_type.value = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
|
||||
let doorGroupPanel;
|
||||
let outsideMaterial; // Declare outsideMaterial globally
|
||||
let glowIntensity = 0.0;
|
||||
let glowDirection = 1; // 1 for increasing, -1 for decreasing
|
||||
|
||||
const DOOR_STATES = {
|
||||
RESTING: 'resting',
|
||||
OPENING: 'opening',
|
||||
CLOSING: 'closing',
|
||||
};
|
||||
let doorState = DOOR_STATES.RESTING;
|
||||
let stateTimer = 0;
|
||||
|
||||
export function createDoor(x, z, rotY) {
|
||||
const doorWidth = 1;
|
||||
|
||||
const doorGroup = new THREE.Group();
|
||||
doorGroup.position.set(x, 1.1, z); // Centered vertically for a 2.2m door
|
||||
doorGroup.rotation.set(0, rotY, 0);
|
||||
|
||||
// Door Frame
|
||||
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x473e3a }); // Dark wood for frame
|
||||
const frameTop = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.1, 0.15), frameMaterial);
|
||||
frameTop.position.set(0, 1.15, 0);
|
||||
frameTop.castShadow = true;
|
||||
doorGroup.add(frameTop);
|
||||
|
||||
const frameLeft = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
|
||||
frameLeft.position.set(-0.55, 0.05, 0);
|
||||
frameLeft.castShadow = true;
|
||||
doorGroup.add(frameLeft);
|
||||
|
||||
const frameRight = new THREE.Mesh(new THREE.BoxGeometry(0.1, 2.3, 0.15), frameMaterial);
|
||||
frameRight.position.set(0.55, 0.05, 0);
|
||||
frameRight.castShadow = true;
|
||||
doorGroup.add(frameRight);
|
||||
|
||||
// Outside darkness
|
||||
outsideMaterial = new THREE.MeshPhongMaterial({ color: 0x150505, emissive: 0x000000, shininess: 50 });
|
||||
const outside = new THREE.Mesh(new THREE.BoxGeometry(doorWidth, 2.2, 0.04), outsideMaterial);
|
||||
outside.position.set(0, 0, 0);
|
||||
doorGroup.add(outside);
|
||||
|
||||
// Door group
|
||||
doorGroupPanel = new THREE.Group();
|
||||
// Main Door Panel
|
||||
const doorMaterial = new THREE.MeshPhongMaterial({ color: 0x8b5a2b, shininess: 10 }); // Lighter wood for door
|
||||
const door = new THREE.Mesh(new THREE.BoxGeometry(doorWidth, 2.2, 0.08), doorMaterial);
|
||||
door.position.set(doorWidth/2, 0, 0);
|
||||
door.castShadow = true;
|
||||
door.receiveShadow = true;
|
||||
doorGroupPanel.add(door);
|
||||
|
||||
// Door Knob
|
||||
const knobMaterial = new THREE.MeshPhongMaterial({ color: 0xd4af37, shininess: 100 }); // Gold/Brass
|
||||
const knob = new THREE.Mesh(new THREE.SphereGeometry(0.05, 16, 16), knobMaterial);
|
||||
knob.position.set(doorWidth/2 + 0.4, 0, 0.06); // Position on the right side of the door
|
||||
knob.castShadow = true;
|
||||
doorGroupPanel.add(knob);
|
||||
doorGroupPanel.position.x = -doorWidth/2;
|
||||
doorGroupPanel.rotation.y = 0;
|
||||
|
||||
doorGroup.add(doorGroupPanel);
|
||||
|
||||
state.scene.add(doorGroup);
|
||||
}
|
||||
|
||||
export function updateDoor() {
|
||||
const speed = 0.0002;
|
||||
const minAngle = 0;
|
||||
const maxAngle = -0.7;
|
||||
|
||||
stateTimer -= 1 / 60; // Assuming 60fps
|
||||
|
||||
if (stateTimer <= 0) {
|
||||
const nextState = Math.random();
|
||||
if (nextState < 0.1) {
|
||||
doorState = DOOR_STATES.RESTING;
|
||||
stateTimer = 2 + Math.random() * 5;
|
||||
} else if (nextState < 0.5) {
|
||||
doorState = DOOR_STATES.OPENING;
|
||||
stateTimer = 2 + Math.random() * 5;
|
||||
} else {
|
||||
doorState = DOOR_STATES.CLOSING;
|
||||
stateTimer = 3 + Math.random() * 2;
|
||||
}
|
||||
}
|
||||
|
||||
switch (doorState) {
|
||||
case DOOR_STATES.OPENING:
|
||||
if (doorGroupPanel.rotation.y > maxAngle) {
|
||||
doorGroupPanel.rotation.y -= speed;
|
||||
}
|
||||
break;
|
||||
case DOOR_STATES.CLOSING:
|
||||
if (doorGroupPanel.rotation.y < minAngle) {
|
||||
doorGroupPanel.rotation.y += speed * 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Outside material pulsating glow
|
||||
if (outsideMaterial) {
|
||||
const glowMin = 0.001;
|
||||
const glowMax = 0.005;
|
||||
const glowSpeed = 0.0001; // Speed of the pulsation
|
||||
glowIntensity += glowDirection * glowSpeed;
|
||||
if (glowIntensity >= glowMax) {
|
||||
glowIntensity = glowMax;
|
||||
glowDirection = -1;
|
||||
} else if (glowIntensity <= glowMin) {
|
||||
glowIntensity = glowMin;
|
||||
glowDirection = 1;
|
||||
}
|
||||
outsideMaterial.emissive.setRGB(glowIntensity, 0, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { screenVertexShader, screenFragmentShader } from '../shaders/screen-shaders.js';
|
||||
|
||||
export function createMagicMirror(x, z, rotY) {
|
||||
// --- Materials ---
|
||||
const frameMaterial = new THREE.MeshPhongMaterial({ color: 0x8B4513, shininess: 40, specular: 0x333333 });
|
||||
const metalMaterial = new THREE.MeshPhongMaterial({ color: 0xd4af37, shininess: 100, specular: 0xeeeeff }); // Gold-like
|
||||
|
||||
const mirrorGroup = new THREE.Group();
|
||||
|
||||
// --- 1. Mirror Stand Base ---
|
||||
const baseWidth = 1.5;
|
||||
const baseHeight = 0.2;
|
||||
const baseDepth = 0.6;
|
||||
const baseGeo = new THREE.BoxGeometry(baseWidth, baseHeight, baseDepth);
|
||||
const base = new THREE.Mesh(baseGeo, frameMaterial);
|
||||
base.position.y = baseHeight / 2;
|
||||
base.castShadow = true;
|
||||
base.receiveShadow = true;
|
||||
mirrorGroup.add(base);
|
||||
|
||||
// --- 2. Stand Uprights ---
|
||||
const uprightHeight = 2.4;
|
||||
const uprightWidth = 0.15;
|
||||
const uprightGeo = new THREE.BoxGeometry(uprightWidth, uprightHeight, uprightWidth);
|
||||
|
||||
const createUpright = (posX) => {
|
||||
const upright = new THREE.Mesh(uprightGeo, frameMaterial);
|
||||
upright.position.set(posX, uprightHeight / 2, 0);
|
||||
upright.castShadow = true;
|
||||
upright.receiveShadow = true;
|
||||
return upright;
|
||||
};
|
||||
|
||||
const uprightOffset = baseWidth / 2 - 0.3;
|
||||
mirrorGroup.add(createUpright(-uprightOffset));
|
||||
mirrorGroup.add(createUpright(uprightOffset));
|
||||
|
||||
// --- 3. The Elliptical Mirror Surface (The "Screen") ---
|
||||
const mirrorRadius = 0.8; // Adjusted radius for scaling
|
||||
const mirrorGeo = new THREE.CircleGeometry(mirrorRadius, 64);
|
||||
|
||||
// The 'tvScreen' from state will now be our mirror surface
|
||||
state.tvScreen = new THREE.Mesh(mirrorGeo, new THREE.MeshBasicMaterial({ color: 0x000000 }));
|
||||
state.tvScreen.position.y = 1.4; // Center height
|
||||
state.tvScreen.position.z = 0.1; // Slightly forward in the frame
|
||||
state.tvScreen.scale.set(1, 1.5, 1); // Scale Y to make it a tall ellipse
|
||||
setMirrorOffMaterial(); // Set its initial "off" state
|
||||
mirrorGroup.add(state.tvScreen);
|
||||
|
||||
// --- 4. Ornate Elliptical Mirror Frame (Torus) ---
|
||||
const frameRadius = mirrorRadius;
|
||||
const frameTubeRadius = 0.04; // Made the rim thinner
|
||||
const frameRingGeo = new THREE.TorusGeometry(frameRadius, frameTubeRadius, 16, 100);
|
||||
const frameRing = new THREE.Mesh(frameRingGeo, metalMaterial);
|
||||
frameRing.position.copy(state.tvScreen.position);
|
||||
frameRing.scale.copy(state.tvScreen.scale); // Apply the same scale to the frame
|
||||
frameRing.castShadow = true;
|
||||
mirrorGroup.add(frameRing);
|
||||
|
||||
// --- 5. Light from the Mirror ---
|
||||
state.screenLight = new THREE.PointLight(0xffffff, 0, 10);
|
||||
state.screenLight.position.copy(state.tvScreen.position);
|
||||
state.screenLight.position.z += 0.3; // Position light in front of the mirror
|
||||
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;
|
||||
mirrorGroup.add(state.screenLight);
|
||||
|
||||
// Position and rotate the entire group
|
||||
mirrorGroup.position.set(x, 0, z);
|
||||
mirrorGroup.rotation.y = rotY;
|
||||
|
||||
state.scene.add(mirrorGroup);
|
||||
}
|
||||
|
||||
function setMirrorOffMaterial() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// A reflective, dark material for when it's off
|
||||
state.tvScreen.material = new THREE.MeshPhongMaterial({
|
||||
color: 0x051020, // Dark blue tint
|
||||
shininess: 100,
|
||||
specular: 0xcccccc,
|
||||
envMap: state.scene.background, // Reflect the room
|
||||
reflectivity: 0.9 // Increased reflectivity
|
||||
});
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
export function turnTvScreenOff() {
|
||||
if (state.tvScreenPowered) {
|
||||
state.tvScreenPowered = false;
|
||||
setScreenEffect(2, () => {
|
||||
setMirrorOffMaterial();
|
||||
state.screenLight.intensity = 0.0;
|
||||
}); // Trigger power down effect
|
||||
}
|
||||
}
|
||||
|
||||
export function turnTvScreenOn() {
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
// Use the shader material for video playback
|
||||
state.tvScreen.material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
videoTexture: { value: state.videoTexture },
|
||||
u_effect_type: { value: 0.0 },
|
||||
u_effect_strength: { value: 0.0 },
|
||||
u_time: { value: 0.0 },
|
||||
},
|
||||
vertexShader: screenVertexShader,
|
||||
fragmentShader: screenFragmentShader,
|
||||
transparent: true,
|
||||
});
|
||||
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
|
||||
if (!state.tvScreenPowered) {
|
||||
state.tvScreenPowered = true;
|
||||
setScreenEffect(1); // Trigger power on effect
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
if (state.screenEffect.onComplete) {
|
||||
state.screenEffect.onComplete();
|
||||
}
|
||||
material.uniforms.u_effect_type.value = 0.0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import wallTextureUrl from '/textures/wall.jpg';
|
||||
|
||||
export function createRoomWalls() {
|
||||
const wallTexture = state.loader.load(wallTextureUrl);
|
||||
wallTexture.wrapS = THREE.RepeatWrapping;
|
||||
wallTexture.wrapT = THREE.RepeatWrapping;
|
||||
|
||||
// USING MeshPhongMaterial for specular highlights on walls
|
||||
const wallMaterial = new THREE.MeshPhongMaterial({
|
||||
map: wallTexture,
|
||||
side: THREE.FrontSide,
|
||||
shininess: 5,
|
||||
specular: 0x111111 // Subtle reflection
|
||||
});
|
||||
|
||||
// 1. Back Wall (behind the TV)
|
||||
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;
|
||||
backWall.name = 'backWall';
|
||||
state.scene.add(backWall);
|
||||
|
||||
// 2. Front Wall (behind the camera)
|
||||
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.name = 'frontWall';
|
||||
frontWall.receiveShadow = true;
|
||||
state.scene.add(frontWall);
|
||||
|
||||
// 3. Left Wall
|
||||
const leftWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
|
||||
leftWall.rotation.y = Math.PI / 2;
|
||||
leftWall.name = 'leftWall';
|
||||
leftWall.position.set(-state.roomSize / 2, state.roomHeight / 2, 0);
|
||||
leftWall.receiveShadow = true;
|
||||
state.scene.add(leftWall);
|
||||
|
||||
// 4. Right Wall
|
||||
const rightWall = new THREE.Mesh(new THREE.PlaneGeometry(state.roomSize, state.roomHeight), wallMaterial);
|
||||
rightWall.name = 'rightWall';
|
||||
rightWall.rotation.y = -Math.PI / 2;
|
||||
rightWall.position.set(state.roomSize / 2, state.roomHeight / 2, 0);
|
||||
rightWall.receiveShadow = true;
|
||||
state.scene.add(rightWall);
|
||||
|
||||
// 5. Ceiling
|
||||
const ceilingGeometry = new THREE.PlaneGeometry(state.roomSize, state.roomSize);
|
||||
const ceilingTexture = wallTexture;
|
||||
ceilingTexture.repeat.set(4, 4);
|
||||
// USING MeshPhongMaterial
|
||||
const ceilingMaterial = new THREE.MeshPhongMaterial({
|
||||
map: ceilingTexture,
|
||||
side: THREE.FrontSide,
|
||||
shininess: 5,
|
||||
specular: 0x111111
|
||||
});
|
||||
|
||||
const ceiling = new THREE.Mesh(ceilingGeometry, ceilingMaterial);
|
||||
ceiling.rotation.x = Math.PI / 2;
|
||||
ceiling.position.set(0, state.roomHeight, 0);
|
||||
ceiling.receiveShadow = true;
|
||||
state.scene.add(ceiling);
|
||||
|
||||
state.crawlSurfaces.push(backWall, leftWall, rightWall);
|
||||
|
||||
// --- 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.MeshPhongMaterial({
|
||||
color: 0x0a1a3a,
|
||||
emissive: 0x0a1a3a,
|
||||
emissiveIntensity: 0.5,
|
||||
side: THREE.FrontSide
|
||||
});
|
||||
const windowPane = new THREE.Mesh(windowGeometry, nightSkyMaterial);
|
||||
|
||||
const windowZ = -state.roomSize / 2 + 0.001;
|
||||
windowPane.position.set(-3.5, state.roomHeight * 0.5 + 1.5, windowZ);
|
||||
state.scene.add(windowPane);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { createRoomWalls } from './room-walls.js';
|
||||
import { createBookshelf } from './bookshelf.js';
|
||||
import { createMagicMirror } from './magic-mirror.js';
|
||||
import { PictureFrame } from './PictureFrame.js';
|
||||
import painting1 from '/textures/painting1.jpg';
|
||||
import painting2 from '/textures/painting2.jpg';
|
||||
import floorTextureUrl from '/textures/stone_floor.png';
|
||||
import tableTextureUrl from '/textures/wood.png';
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
export function createSceneObjects() {
|
||||
// --- Materials (MeshPhongMaterial) ---
|
||||
const darkMetal = new THREE.MeshPhongMaterial({
|
||||
color: 0x6b6b6b,
|
||||
shininess: 80,
|
||||
specular: 0x888888
|
||||
});
|
||||
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({
|
||||
map: state.loader.load(tableTextureUrl),
|
||||
shininess: 10,
|
||||
specular: 0x222222
|
||||
});
|
||||
|
||||
// --- 1. Floor ---
|
||||
const floorGeometry = new THREE.PlaneGeometry(20, 20);
|
||||
const floorTexture = state.loader.load(floorTextureUrl);
|
||||
floorTexture.wrapS = THREE.RepeatWrapping;
|
||||
floorTexture.wrapT = THREE.RepeatWrapping;
|
||||
floorTexture.repeat.set(state.roomSize, state.roomSize);
|
||||
const floorMaterial = new THREE.MeshPhongMaterial({ map: floorTexture, color: 0xaaaaaa, shininess: 5 });
|
||||
const floor = new THREE.Mesh(floorGeometry, floorMaterial);
|
||||
floor.rotation.x = -Math.PI / 2;
|
||||
floor.position.y = 0;
|
||||
floor.receiveShadow = true;
|
||||
state.scene.add(floor);
|
||||
|
||||
state.landingSurfaces.push(floor);
|
||||
|
||||
createRoomWalls();
|
||||
|
||||
// 3. Lighting (Minimal and focused)
|
||||
const ambientLight = new THREE.AmbientLight(0x404040, 1);
|
||||
state.scene.add(ambientLight);
|
||||
|
||||
const roomLight = new THREE.PointLight(0xffdcb4, 0.8, state.roomSize);
|
||||
roomLight.position.set(0, 1.8, 0);
|
||||
state.scene.add(roomLight);
|
||||
|
||||
|
||||
createMagicMirror(0, -state.roomSize/2 + 1.0, 0);
|
||||
|
||||
// --- 5. Candle ---
|
||||
const candleStickGeo = new THREE.CylinderGeometry(0.05, 0.06, 0.3, 12);
|
||||
const candleStick = new THREE.Mesh(candleStickGeo, darkMetal);
|
||||
candleStick.castShadow = true;
|
||||
candleStick.receiveShadow = true;
|
||||
|
||||
// Candle Light (Warm Glow)
|
||||
state.candleLight = new THREE.PointLight(0xffaa00, state.originalLampIntensity, 4);
|
||||
state.candleLight.position.set(0, 0.2, 0);
|
||||
state.candleLight.castShadow = true;
|
||||
state.candleLight.shadow.mapSize.width = 512;
|
||||
state.candleLight.shadow.mapSize.height = 512;
|
||||
state.candleLight.shadow.camera.near = 0.1;
|
||||
state.candleLight.shadow.camera.far = 4;
|
||||
state.candleLight.penumbra = 0.5;
|
||||
|
||||
const candleGroup = new THREE.Group();
|
||||
candleGroup.add(candleStick, state.candleLight);
|
||||
candleGroup.position.set(0.8, 0.15, -state.roomSize/2+0.5);
|
||||
|
||||
state.scene.add(candleGroup);
|
||||
|
||||
// --- 7. Table ---
|
||||
const tableTopGeo = new THREE.BoxGeometry(1.5, 0.1, 0.8);
|
||||
const tableTop = new THREE.Mesh(tableTopGeo, woodMaterial);
|
||||
tableTop.position.y = 0.5;
|
||||
tableTop.castShadow = true;
|
||||
tableTop.receiveShadow = true;
|
||||
|
||||
const table = new THREE.Group();
|
||||
table.add(tableTop); // You could add legs here for more detail
|
||||
table.position.set(-1.7, 0, -0.8);
|
||||
table.rotation.y = Math.PI / 5;
|
||||
state.scene.add(table);
|
||||
|
||||
// --- 8. Timber Frames ---
|
||||
const beamThickness = 0.15;
|
||||
const beamDepth = 0.2;
|
||||
|
||||
// Ceiling Beams
|
||||
const ceilingBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamDepth, beamThickness);
|
||||
const ceilingBeamGeoZ = new THREE.BoxGeometry(beamThickness, beamDepth, state.roomSize);
|
||||
|
||||
const createBeam = (geo, pos, rotY = 0) => {
|
||||
const beam = new THREE.Mesh(geo, woodMaterial);
|
||||
beam.position.copy(pos);
|
||||
beam.rotation.y = rotY;
|
||||
beam.castShadow = true;
|
||||
beam.receiveShadow = true;
|
||||
state.scene.add(beam);
|
||||
return beam;
|
||||
};
|
||||
|
||||
// Create a grid of ceiling beams
|
||||
// Beams along X-axis
|
||||
createBeam(ceilingBeamGeoX, new THREE.Vector3(0, state.roomHeight - beamDepth / 2, 0));
|
||||
createBeam(ceilingBeamGeoX, new THREE.Vector3(0, state.roomHeight - beamDepth / 2, -state.roomSize / 2 + 1.5));
|
||||
createBeam(ceilingBeamGeoX, new THREE.Vector3(0, state.roomHeight - beamDepth / 2, state.roomSize / 2 - 1.5));
|
||||
// Beams along Z-axis
|
||||
createBeam(ceilingBeamGeoZ, new THREE.Vector3(-state.roomSize / 2 + 1.5, state.roomHeight - beamDepth / 2, 0));
|
||||
createBeam(ceilingBeamGeoZ, new THREE.Vector3(state.roomSize / 2 - 1.5, state.roomHeight - beamDepth / 2, 0));
|
||||
|
||||
// Wall Beams (Vertical)
|
||||
const wallBeamGeo = new THREE.BoxGeometry(beamThickness, state.roomHeight, beamDepth);
|
||||
|
||||
// Back Wall
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(-1, state.roomHeight / 2, -state.roomSize / 2 + beamDepth / 2));
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(1, state.roomHeight / 2, -state.roomSize / 2 + beamDepth / 2));
|
||||
|
||||
// Left Wall
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(-state.roomSize / 2 + beamDepth / 2, state.roomHeight / 2, -1), Math.PI / 2);
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(-state.roomSize / 2 + beamDepth / 2, state.roomHeight / 2, 1), Math.PI / 2);
|
||||
|
||||
// Right Wall
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(state.roomSize / 2 - beamDepth / 2, state.roomHeight / 2, -1.5), -Math.PI / 2);
|
||||
createBeam(wallBeamGeo, new THREE.Vector3(state.roomSize / 2 - beamDepth / 2, state.roomHeight / 2, 1.5), -Math.PI / 2);
|
||||
|
||||
// Wall Beams (Horizontal)
|
||||
const wallBeamGeoX = new THREE.BoxGeometry(state.roomSize, beamThickness, beamDepth);
|
||||
const wallBeamGeoZ = new THREE.BoxGeometry(beamDepth, beamThickness, state.roomSize);
|
||||
|
||||
// Back Wall
|
||||
createBeam(wallBeamGeoX, new THREE.Vector3(0, state.roomHeight - 0.5, -state.roomSize / 2 + beamDepth / 2));
|
||||
|
||||
// Left Wall
|
||||
createBeam(wallBeamGeoZ, new THREE.Vector3(-state.roomSize / 2 + beamDepth / 2, state.roomHeight - 0.5, 0));
|
||||
|
||||
// Right Wall
|
||||
createBeam(wallBeamGeoZ, new THREE.Vector3(state.roomSize / 2 - beamDepth / 2, state.roomHeight - 0.5, 0));
|
||||
|
||||
|
||||
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);
|
||||
|
||||
const pictureFrame = new PictureFrame(state.scene, {
|
||||
position: new THREE.Vector3(-state.roomSize/2, 2.0, -state.roomSize/2 + 1.5),
|
||||
width: 1.5,
|
||||
height: 1,
|
||||
imageUrls: [painting1, painting2],
|
||||
rotationY: Math.PI / 2
|
||||
});
|
||||
state.pictureFrames.push(pictureFrame);
|
||||
|
||||
const pictureFrame2 = new PictureFrame(state.scene, {
|
||||
position: new THREE.Vector3(state.roomSize/2, 2.0, 0.3),
|
||||
width: 1.5,
|
||||
height: 1,
|
||||
imageUrls: [painting2, painting1],
|
||||
rotationY: -Math.PI / 2
|
||||
});
|
||||
state.pictureFrames.push(pictureFrame2);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export const screenVertexShader = `
|
||||
varying vec2 vUv;
|
||||
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
export const screenFragmentShader = `
|
||||
varying vec2 vUv;
|
||||
uniform sampler2D videoTexture;
|
||||
|
||||
void main() {
|
||||
// Sample the video texture
|
||||
gl_FragColor = texture2D(videoTexture, vUv);
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,250 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
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
|
||||
// A=Top, B=TR, C=BR, D=Bottom, E=BL, F=TL, G=Middle
|
||||
const SEGMENTS = {
|
||||
'0': [1, 1, 1, 1, 1, 1, 0],
|
||||
'1': [0, 1, 1, 0, 0, 0, 0],
|
||||
'2': [1, 1, 0, 1, 1, 0, 1],
|
||||
'3': [1, 1, 1, 1, 0, 0, 1],
|
||||
'4': [0, 1, 1, 0, 0, 1, 1],
|
||||
'5': [1, 0, 1, 1, 0, 1, 1],
|
||||
'6': [1, 0, 1, 1, 1, 1, 1],
|
||||
'7': [1, 1, 1, 0, 0, 0, 0],
|
||||
'8': [1, 1, 1, 1, 1, 1, 1],
|
||||
'9': [1, 1, 1, 1, 0, 1, 1],
|
||||
' ': [0, 0, 0, 0, 0, 0, 0]
|
||||
};
|
||||
|
||||
const SEG_THICKNESS = 3; // Thickness of the segment line in canvas pixels
|
||||
const SEG_PADDING = 2; // Padding within a digit segment's box
|
||||
|
||||
// Colors for active and inactive segments
|
||||
const COLOR_ACTIVE = '#00ff44'; // Bright Fluorescent Green
|
||||
const COLOR_INACTIVE = '#1a1a1a'; // Dim dark gray for 'ghost' segments
|
||||
|
||||
/**
|
||||
* Draws a single 7-segment digit by drawing active segments.
|
||||
* Now includes drawing of inactive (ghost) segments for better readability.
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {string} digit The digit character (0-9).
|
||||
* @param {number} x Left position of the digit area.
|
||||
* @param {number} y Top position of the digit area.
|
||||
* @param {number} H Total height of the digit area.
|
||||
*/
|
||||
function drawSegmentDigit(ctx, digit, x, y, H) {
|
||||
const segments = SEGMENTS[digit] || SEGMENTS[' '];
|
||||
const W = H / 2; // Width is half the height for standard aspect ratio
|
||||
|
||||
// Segment dimensions relative to W and H
|
||||
const hLength = W - 2 * SEG_PADDING;
|
||||
// Vertical length calculation: (Total height - 2 paddings - 3 horizontal thicknesses) / 2
|
||||
const vLength = (H - (2 * SEG_PADDING) - (3 * SEG_THICKNESS)) / 2;
|
||||
|
||||
// Helper to draw horizontal segment (A, G, D)
|
||||
const drawH = (index, x_start, y_start) => {
|
||||
ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
|
||||
ctx.fillRect(x_start + SEG_PADDING, y_start, hLength, SEG_THICKNESS);
|
||||
};
|
||||
|
||||
// Helper to draw vertical segment (F, B, E, C)
|
||||
const drawV = (index, x_start, y_start) => {
|
||||
ctx.fillStyle = segments[index] ? COLOR_ACTIVE : COLOR_INACTIVE;
|
||||
ctx.fillRect(x_start, y_start, SEG_THICKNESS, vLength);
|
||||
};
|
||||
|
||||
// Define segment positions
|
||||
|
||||
// Horizontal segments
|
||||
// A (Top) - index 0
|
||||
drawH(0, x, y + SEG_PADDING);
|
||||
// G (Middle) - index 6
|
||||
drawH(6, x, y + H/2 - SEG_THICKNESS/2);
|
||||
// D (Bottom) - index 3
|
||||
drawH(3, x, y + H - SEG_PADDING - SEG_THICKNESS);
|
||||
|
||||
// Vertical segments (Top Half)
|
||||
const topVStart = y + SEG_PADDING + SEG_THICKNESS;
|
||||
const rightVStart = x + W - SEG_PADDING - SEG_THICKNESS;
|
||||
|
||||
// F (Top-Left) - index 5
|
||||
drawV(5, x + SEG_PADDING, topVStart);
|
||||
|
||||
// B (Top-Right) - index 1
|
||||
drawV(1, rightVStart, topVStart);
|
||||
|
||||
// Vertical segments (Bottom Half)
|
||||
const bottomVStart = y + H/2 + SEG_THICKNESS/2;
|
||||
|
||||
// E (Bottom-Left) - index 4
|
||||
drawV(4, x + SEG_PADDING, bottomVStart);
|
||||
|
||||
// C (Bottom-Right) - index 2
|
||||
drawV(2, rightVStart, bottomVStart);
|
||||
}
|
||||
|
||||
// Function to draw the colon (two dots), now with blinking logic
|
||||
function drawColon(ctx, x, y, H, isVisible) {
|
||||
const dotSize = 4;
|
||||
ctx.fillStyle = COLOR_ACTIVE;
|
||||
|
||||
if (isVisible) {
|
||||
// Top dot
|
||||
ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
|
||||
// Bottom dot
|
||||
ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
|
||||
} else {
|
||||
// Draw inactive colon if not visible, for consistency
|
||||
ctx.fillStyle = COLOR_INACTIVE;
|
||||
ctx.fillRect(x, y + H * 0.3 - dotSize / 2, dotSize, dotSize);
|
||||
ctx.fillRect(x, y + H * 0.7 - dotSize / 2, dotSize, dotSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a simple playback arrow (triangle)
|
||||
* @param {CanvasRenderingContext2D} ctx
|
||||
* @param {number} x Left position of the arrow area.
|
||||
* @param {number} y Top position of the arrow area.
|
||||
* @param {number} H Total height of the arrow area.
|
||||
*/
|
||||
function drawPlaybackArrow(ctx, x, y, H) {
|
||||
const arrowWidth = H * 0.4; // Arrow width relative to digit height
|
||||
const arrowHeight = H * 0.4; // Arrow height relative to digit height
|
||||
|
||||
ctx.fillStyle = COLOR_ACTIVE;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y + H * 0.5 - arrowHeight / 2); // Top point
|
||||
ctx.lineTo(x + arrowWidth, y + H * 0.5); // Right point (center)
|
||||
ctx.lineTo(x, y + H * 0.5 + arrowHeight / 2); // Bottom point
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
|
||||
// Main function to render the entire time string using segments
|
||||
function drawSegmentDisplay(ctx, timeString) {
|
||||
const canvasWidth = ctx.canvas.width;
|
||||
const canvasHeight = ctx.canvas.height;
|
||||
const timeStringLength = timeString.length;
|
||||
|
||||
// Clear display to dark background
|
||||
ctx.fillStyle = '#0a0a0a';
|
||||
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
|
||||
|
||||
// Constants for layout
|
||||
const charSpacing = 8; // Spacing between digits
|
||||
const digitHeight = canvasHeight - 2 * SEG_PADDING;
|
||||
const digitWidth = digitHeight / 2 + SEG_PADDING; // Total width slot for one digit
|
||||
const colonWidth = 6;
|
||||
const arrowWidth = digitHeight * 0.7; // Approx width for the arrow
|
||||
const arrowPadding = 10; // Space between arrow and first digit
|
||||
|
||||
// Calculate total display width including arrow and spaces
|
||||
const totalDisplayWidth = arrowWidth + arrowPadding + (4 * digitWidth) + colonWidth + ((timeStringLength - 1) * charSpacing);
|
||||
|
||||
// Calculate starting X to center the display
|
||||
let currentX = (canvasWidth - totalDisplayWidth) / 2;
|
||||
const currentY = SEG_PADDING;
|
||||
|
||||
// Draw Playback Arrow
|
||||
if (state.isVideoLoaded && state.videoElement.readyState >= 3) {
|
||||
drawPlaybackArrow(ctx, currentX, currentY, digitHeight);
|
||||
}
|
||||
currentX += arrowWidth + arrowPadding; // Move X after arrow and its padding
|
||||
|
||||
for (let i = 0; i < timeStringLength; i++) {
|
||||
const char = timeString[i];
|
||||
|
||||
if (char === ':') {
|
||||
drawColon(ctx, currentX, currentY, digitHeight, state.blinkState); // Pass blinkState
|
||||
currentX += colonWidth;
|
||||
} else if (char >= '0' && char <= '9') {
|
||||
drawSegmentDigit(ctx, char, currentX, currentY, digitHeight);
|
||||
currentX += digitWidth;
|
||||
}
|
||||
|
||||
// Add spacing only if it's not the last element
|
||||
if (i < timeStringLength - 1) {
|
||||
currentX += charSpacing;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- VCR Display Functions ---
|
||||
export function createVcrDisplay() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 160; // Increased width for arrow and better spacing
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = '#030303';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
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: state.vcrDisplayTexture,
|
||||
side: THREE.FrontSide,
|
||||
color: 0x105a10,
|
||||
});
|
||||
|
||||
const displayMesh = new THREE.Mesh(displayGeometry, displayMaterial);
|
||||
return displayMesh;
|
||||
}
|
||||
|
||||
export function updateVcrDisplay(time) {
|
||||
if (!state.vcrDisplayTexture) return;
|
||||
|
||||
const canvas = state.vcrDisplayTexture.image;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const timeString = formatTime(time);
|
||||
|
||||
// Uses the new segment drawing function with ghosting, including blinkState for colon
|
||||
drawSegmentDisplay(ctx, timeString);
|
||||
|
||||
state.vcrDisplayTexture.needsUpdate = true;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import * as THREE from 'three';
|
||||
import { createVcrDisplay } from './vcr-display.js';
|
||||
|
||||
export function createVcr() {
|
||||
// Materials
|
||||
const vcrBodyMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0x222222, // Dark metallic gray
|
||||
shininess: 70,
|
||||
specular: 0x444444
|
||||
});
|
||||
const slotMaterial = new THREE.MeshPhongMaterial({
|
||||
color: 0x0a0a0a, // Deep black
|
||||
shininess: 5,
|
||||
specular: 0x111111
|
||||
});
|
||||
|
||||
// VCR Body
|
||||
const vcrBodyGeometry = new THREE.BoxGeometry(1.0, 0.2, 0.7);
|
||||
const vcrBody = new THREE.Mesh(vcrBodyGeometry, vcrBodyMaterial);
|
||||
vcrBody.position.y = 0; // Centered
|
||||
vcrBody.castShadow = true;
|
||||
vcrBody.receiveShadow = true;
|
||||
|
||||
// Cassette Slot / Front Face
|
||||
const slotGeometry = new THREE.BoxGeometry(0.9, 0.05, 0.01);
|
||||
const slotMesh = new THREE.Mesh(slotGeometry, slotMaterial);
|
||||
slotMesh.position.set(0, -0.05, 0.35 + 0.005);
|
||||
slotMesh.castShadow = true;
|
||||
slotMesh.receiveShadow = true;
|
||||
|
||||
// VCR Display
|
||||
const displayMesh = createVcrDisplay();
|
||||
displayMesh.position.z = 0.35 + 0.005;
|
||||
displayMesh.position.x = 0.2; // Adjusted X for arrow
|
||||
displayMesh.position.y = 0.03;
|
||||
|
||||
// VCR Group
|
||||
const vcrGroup = new THREE.Group();
|
||||
vcrGroup.add(vcrBody, slotMesh, displayMesh);
|
||||
vcrGroup.position.set(0, 0.1, 0); // Position the whole VCR slightly above the floor
|
||||
|
||||
// Light from the VCR display itself
|
||||
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;
|
||||
vcrGroup.add(vcrDisplayLight);
|
||||
|
||||
return vcrGroup;
|
||||
}
|
||||
Reference in New Issue
Block a user