diff --git a/tv-player/src/core/animate.js b/tv-player/src/core/animate.js index 6bcd0a8..3852a9b 100644 --- a/tv-player/src/core/animate.js +++ b/tv-player/src/core/animate.js @@ -147,6 +147,12 @@ function updateBooks() { } } +function updatePictureFrame() { + state.pictureFrames.forEach((pictureFrame) => { + pictureFrame.update(); + }); +} + // --- Animation Loop --- export function animate() { requestAnimationFrame(animate); @@ -159,6 +165,7 @@ export function animate() { updateVcr(); updateBooks(); updateDoor(); + updatePictureFrame(); // RENDER! state.renderer.render(state.scene, state.camera); diff --git a/tv-player/src/scene/PictureFrame.js b/tv-player/src/scene/PictureFrame.js new file mode 100644 index 0000000..eb9ce68 --- /dev/null +++ b/tv-player/src/scene/PictureFrame.js @@ -0,0 +1,115 @@ +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.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.001; // 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; + } + } +} diff --git a/tv-player/src/scene/root.js b/tv-player/src/scene/root.js index a6868d8..0d045fc 100644 --- a/tv-player/src/scene/root.js +++ b/tv-player/src/scene/root.js @@ -4,6 +4,7 @@ import { createRoomWalls } from './room-walls.js'; import { createBookshelf } from './bookshelf.js'; import { createDoor } from './door.js'; import { createTvSet } from './tv-set.js'; +import { PictureFrame } from './PictureFrame.js'; // --- Scene Modeling Function --- export function createSceneObjects() { @@ -133,5 +134,22 @@ export function createSceneObjects() { 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 + 0.1, 2.0, -state.roomSize/2 + 1.5), + width: 1.5, + height: 1, + imageUrls: ['/textures/painting1.jpg', '/textures/painting2.jpg'], + rotationY: Math.PI / 2 + }); + state.pictureFrames.push(pictureFrame); + const pictureFrame2 = new PictureFrame(state.scene, { + position: new THREE.Vector3(state.roomSize/2 - 0.1, 2.0, 0.5), + width: 1.5, + height: 1, + imageUrls: ['/textures/painting2.jpg', '/textures/painting1.jpg'], + rotationY: -Math.PI / 2 + }); + state.pictureFrames.push(pictureFrame2); } diff --git a/tv-player/src/state.js b/tv-player/src/state.js index 8dd6357..8cb5106 100644 --- a/tv-player/src/state.js +++ b/tv-player/src/state.js @@ -50,7 +50,9 @@ export function initState() { timer: 0, }, books: [], // Array to hold all individual book meshes for animation + pictureFrames: [], raycaster: new THREE.Raycaster(), seed: 12345, }; + } diff --git a/tv-player/textures/painting1.jpg b/tv-player/textures/painting1.jpg new file mode 100644 index 0000000..8ff623c Binary files /dev/null and b/tv-player/textures/painting1.jpg differ diff --git a/tv-player/textures/painting2.jpg b/tv-player/textures/painting2.jpg new file mode 100644 index 0000000..5a542cc Binary files /dev/null and b/tv-player/textures/painting2.jpg differ