diff --git a/party-cathedral/src/core/animate.js b/party-cathedral/src/core/animate.js index 7322458..c38675a 100644 --- a/party-cathedral/src/core/animate.js +++ b/party-cathedral/src/core/animate.js @@ -7,13 +7,13 @@ function updateCamera() { const globalTime = Date.now() * 0.0001; const lookAtTime = Date.now() * 0.0002; - const camAmplitude = 1.0; + const camAmplitude = new THREE.Vector3(1.0, 0.1, 10.0); const lookAmplitude = 8.0; // Base Camera Position in front of the TV const baseX = 0; - const baseY = 1.6; - const baseZ = -10.0; + const baseY = 2.6; + const baseZ = 0.0; // Base LookAt target (Center of the screen) const baseTargetX = 0; @@ -21,9 +21,9 @@ function updateCamera() { const baseTargetZ = -30.0; // Camera Position Offsets (Drift) - const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude; - const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude * 0.1; - const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude; + const camOffsetX = Math.sin(globalTime * 3.1) * camAmplitude.x; + const camOffsetY = Math.cos(globalTime * 2.5) * camAmplitude.y; + const camOffsetZ = Math.cos(globalTime * 3.2) * camAmplitude.z; state.camera.position.x = baseX + camOffsetX; state.camera.position.y = baseY + camOffsetY; diff --git a/party-cathedral/src/scene/party-guests.js b/party-cathedral/src/scene/party-guests.js new file mode 100644 index 0000000..ce4ae1d --- /dev/null +++ b/party-cathedral/src/scene/party-guests.js @@ -0,0 +1,152 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; +const guestTextureUrls = [ + '/textures/guest1.png', + '/textures/guest2.png', + '/textures/guest3.png', + '/textures/guest4.png', +]; + +// --- Scene dimensions for positioning --- +const stageHeight = 1.5; +const stageDepth = 5; +const length = 44; + +// --- Billboard Properties --- +const guestHeight = 2.5; +const guestWidth = 2.5; + +export class PartyGuests extends SceneFeature { + constructor() { + super(); + this.guests = []; + sceneFeatureManager.register(this); + } + + async init() { + const processTexture = (texture) => { + const image = texture.image; + const canvas = document.createElement('canvas'); + canvas.width = image.width; + canvas.height = image.height; + const context = canvas.getContext('2d'); + context.drawImage(image, 0, 0); + const keyPixelData = context.getImageData(0, 0, 1, 1).data; + const keyColor = { r: keyPixelData[0], g: keyPixelData[1], b: keyPixelData[2] }; + const imageData = context.getImageData(0, 0, canvas.width, canvas.height); + const data = imageData.data; + const threshold = 20; + for (let i = 0; i < data.length; i += 4) { + const r = data[i], g = data[i + 1], b = data[i + 2]; + const distance = Math.sqrt(Math.pow(r - keyColor.r, 2) + Math.pow(g - keyColor.g, 2) + Math.pow(b - keyColor.b, 2)); + if (distance < threshold) data[i + 3] = 0; + } + context.putImageData(imageData, 0, 0); + return new THREE.CanvasTexture(canvas); + }; + + const materials = await Promise.all(guestTextureUrls.map(async (url) => { + const texture = await state.loader.loadAsync(url); + const processedTexture = processTexture(texture); + return new THREE.MeshStandardMaterial({ + map: processedTexture, + side: THREE.DoubleSide, + alphaTest: 0.5, + roughness: 0.7, + metalness: 0.1, + }); + })); + + const createGuests = () => { + const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight); + const numGuests = 80; + + for (let i = 0; i < numGuests; i++) { + const material = materials[i % materials.length]; + const guest = new THREE.Mesh(geometry, material); + const pos = new THREE.Vector3( + (Math.random() - 0.5) * 10, + guestHeight / 2, + (Math.random() * 20) - 12 // Position them in the main hall + ); + guest.position.copy(pos); + state.scene.add(guest); + + this.guests.push({ + mesh: guest, + state: 'WAITING', + targetPosition: pos.clone(), + waitStartTime: 0, + waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds + isJumping: false, + jumpStartTime: 0, + }); + } + }; + + createGuests(); + } + + update(deltaTime) { + if (this.guests.length === 0) return; + + const cameraPosition = new THREE.Vector3(); + state.camera.getWorldPosition(cameraPosition); + + const time = state.clock.getElapsedTime(); + const moveSpeed = 1.0; // Move slower + const movementArea = { x: 10, z: 30, y: 0, centerZ: 0 }; + const jumpChance = 0.05; // Jump way more + const jumpDuration = 0.5; + const jumpHeight = 0.1; + const jumpVariance = 0.5; + + this.guests.forEach(guestObj => { + const { mesh } = guestObj; + mesh.lookAt(cameraPosition.x, mesh.position.y, cameraPosition.z); + + if (guestObj.state === 'WAITING') { + if (time > guestObj.waitStartTime + guestObj.waitTime) { + const newTarget = new THREE.Vector3( + (Math.random() - 0.5) * movementArea.x, + movementArea.y + guestHeight / 2, + movementArea.centerZ + (Math.random() - 0.5) * movementArea.z + ); + guestObj.targetPosition = newTarget; + guestObj.state = 'MOVING'; + } + } else if (guestObj.state === 'MOVING') { + const distance = mesh.position.distanceTo(guestObj.targetPosition); + if (distance > 0.1) { + const direction = guestObj.targetPosition.clone().sub(mesh.position).normalize(); + mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime)); + } else { + guestObj.state = 'WAITING'; + guestObj.waitStartTime = time; + guestObj.waitTime = 3 + Math.random() * 4; + } + } + + if (guestObj.isJumping) { + const jumpProgress = (time - guestObj.jumpStartTime) / jumpDuration; + if (jumpProgress < 1) { + const baseHeight = movementArea.y + guestHeight / 2; + mesh.position.y = baseHeight + Math.sin(jumpProgress * Math.PI) * guestObj.jumpHeight; + } else { + guestObj.isJumping = false; + mesh.position.y = movementArea.y + guestHeight / 2; + } + } else { + if (Math.random() < jumpChance) { + guestObj.isJumping = true; + guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance; + guestObj.jumpStartTime = time; + } + } + }); + } +} + +new PartyGuests(); \ No newline at end of file diff --git a/party-cathedral/src/scene/root.js b/party-cathedral/src/scene/root.js index 159974b..d273540 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -8,6 +8,7 @@ import { LightBall } from './light-ball.js'; import { Pews } from './pews.js'; import { Stage } from './stage.js'; import { MedievalMusicians } from './medieval-musicians.js'; +import { PartyGuests } from './party-guests.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-cathedral/textures/guest1.png b/party-cathedral/textures/guest1.png new file mode 100644 index 0000000..3cfe185 Binary files /dev/null and b/party-cathedral/textures/guest1.png differ diff --git a/party-cathedral/textures/guest2.png b/party-cathedral/textures/guest2.png new file mode 100644 index 0000000..957e4ad Binary files /dev/null and b/party-cathedral/textures/guest2.png differ diff --git a/party-cathedral/textures/guest3.png b/party-cathedral/textures/guest3.png new file mode 100644 index 0000000..e52eacf Binary files /dev/null and b/party-cathedral/textures/guest3.png differ diff --git a/party-cathedral/textures/guest4.png b/party-cathedral/textures/guest4.png new file mode 100644 index 0000000..7b230e2 Binary files /dev/null and b/party-cathedral/textures/guest4.png differ