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; } } }