From 451d4ea2611be4f8bb98274ddba208731edfe027 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 23 Nov 2025 20:33:32 +0100 Subject: [PATCH] Feature: Music playback --- party-cathedral/index.html | 12 +++ party-cathedral/src/core/animate.js | 41 ++------- party-cathedral/src/effects/EffectsManager.js | 4 +- party-cathedral/src/effects/dust.js | 12 +-- party-cathedral/src/scene/camera-manager.js | 25 +++-- party-cathedral/src/scene/dancers.js | 23 ++++- party-cathedral/src/scene/light-ball.js | 20 +++- .../src/scene/medieval-musicians.js | 21 ++++- party-cathedral/src/scene/music-player.js | 92 +++++++++++++++++++ party-cathedral/src/scene/music-visualizer.js | 2 +- party-cathedral/src/scene/party-guests.js | 24 ++++- party-cathedral/src/scene/root.js | 1 + party-cathedral/src/scene/stage-torches.js | 31 ++++++- party-cathedral/src/state.js | 1 + 14 files changed, 255 insertions(+), 54 deletions(-) create mode 100644 party-cathedral/src/scene/music-player.js diff --git a/party-cathedral/index.html b/party-cathedral/index.html index 513050a..6006e98 100644 --- a/party-cathedral/index.html +++ b/party-cathedral/index.html @@ -22,6 +22,18 @@ +
+
+ + +
+ +
+ diff --git a/party-cathedral/src/core/animate.js b/party-cathedral/src/core/animate.js index d07ad2a..3bf1218 100644 --- a/party-cathedral/src/core/animate.js +++ b/party-cathedral/src/core/animate.js @@ -4,54 +4,33 @@ import { onResizePostprocessing } from './postprocessing.js'; import { updateScreenEffect } from '../scene/magic-mirror.js' import sceneFeatureManager from '../scene/SceneFeatureManager.js'; -function updateScreenLight() { - if (state.isVideoLoaded && state.screenLight.intensity > 0) { - const pulseTarget = state.originalScreenIntensity + (Math.random() - 0.5) * state.screenIntensityPulse; - state.screenLight.intensity = THREE.MathUtils.lerp(state.screenLight.intensity, pulseTarget, 0.1); - - const lightTime = Date.now() * 0.0001; - const radius = 0.01; - const centerX = 0; - const centerY = 1.5; - - state.screenLight.position.x = centerX + Math.cos(lightTime) * radius; - state.screenLight.position.y = centerY + Math.sin(lightTime * 1.5) * radius * 0.5; // Slightly different freq for Y - } -} - function updateShaderTime() { if (state.tvScreen && state.tvScreen.material.uniforms && state.tvScreen.material.uniforms.u_time) { state.tvScreen.material.uniforms.u_time.value = state.clock.getElapsedTime(); } } -function updateVideo() { - if (state.videoTexture) { - state.videoTexture.needsUpdate = true; - } -} - - // --- Animation Loop --- let lastTime = -1; export function animate() { requestAnimationFrame(animate); let deltaTime = 0; - if (lastTime !== -1) { + if (lastTime === -1) { + lastTime = state.clock.getElapsedTime(); + deltaTime = 1; + } else { const newTime = state.clock.getElapsedTime(); deltaTime = newTime - lastTime; lastTime = newTime; - } else { - lastTime = state.clock.getElapsedTime(); } - sceneFeatureManager.update(deltaTime); - state.effectsManager.update(); - updateScreenLight(); - updateVideo(); - updateShaderTime(); - updateScreenEffect(); + if (deltaTime > 0) { + sceneFeatureManager.update(deltaTime); + state.effectsManager.update(deltaTime); + updateShaderTime(); + updateScreenEffect(); + } // RENDER! if (state.composer) { diff --git a/party-cathedral/src/effects/EffectsManager.js b/party-cathedral/src/effects/EffectsManager.js index cabc022..14a8054 100644 --- a/party-cathedral/src/effects/EffectsManager.js +++ b/party-cathedral/src/effects/EffectsManager.js @@ -16,7 +16,7 @@ export class EffectsManager { this.effects.push(effect); } - update() { - this.effects.forEach(effect => effect.update()); + update(deltaTime) { + this.effects.forEach(effect => effect.update(deltaTime)); } } \ No newline at end of file diff --git a/party-cathedral/src/effects/dust.js b/party-cathedral/src/effects/dust.js index e407bbc..b8942b2 100644 --- a/party-cathedral/src/effects/dust.js +++ b/party-cathedral/src/effects/dust.js @@ -7,15 +7,15 @@ export class DustEffect { } _create(scene) { - const particleCount = 2000; + const particleCount = 3000; const particlesGeometry = new THREE.BufferGeometry(); const positions = []; for (let i = 0; i < particleCount; i++) { positions.push( (Math.random() - 0.5) * 15, - Math.random() * 10, - (Math.random() - 0.5) * 15 + Math.random() * 8, + (Math.random() - 0.5) * 45 ); } particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); @@ -32,11 +32,11 @@ export class DustEffect { scene.add(this.dust); } - update() { - if (this.dust) { + update(deltaTime) { + if (deltaTime && this.dust) { const positions = this.dust.geometry.attributes.position.array; for (let i = 1; i < positions.length; i += 3) { - positions[i] -= 0.001; + positions[i] -= deltaTime * 0.006; if (positions[i] < -2) { positions[i] = 8; } diff --git a/party-cathedral/src/scene/camera-manager.js b/party-cathedral/src/scene/camera-manager.js index 93c7b0c..26e7c21 100644 --- a/party-cathedral/src/scene/camera-manager.js +++ b/party-cathedral/src/scene/camera-manager.js @@ -39,8 +39,8 @@ export class CameraManager extends SceneFeature { }); // --- Static Camera 2: Right Aisle View --- - const staticCam2 = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 100); - staticCam2.position.set(5, 4, -12); + const staticCam2 = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100); + staticCam2.position.set(5, 4, -6); staticCam2.lookAt(0, 1.5, -18); // Look at the stage this.cameras.push({ camera: staticCam2, @@ -84,7 +84,9 @@ export class CameraManager extends SceneFeature { } // This is the logic moved from animate.js - updateDynamicCamera() { + updateDynamicCamera(timeDiff) { + if (!state.partyStarted) return; + const globalTime = Date.now() * 0.0001; const lookAtTime = Date.now() * 0.0002; @@ -129,11 +131,13 @@ export class CameraManager extends SceneFeature { const time = state.clock.getElapsedTime(); // Handle camera switching - if (time > this.lastSwitchTime + this.switchInterval) { - const newIndex = Math.floor(Math.random() * this.cameras.length); - this.switchCamera(newIndex); - this.lastSwitchTime = time; - this.switchInterval = minSwitchInterval + Math.random() * (maxSwitchInterval - minSwitchInterval); + if (state.partyStarted) { + if (time > this.lastSwitchTime + this.switchInterval) { + const newIndex = Math.floor(Math.random() * this.cameras.length); + this.switchCamera(newIndex); + this.lastSwitchTime = time; + this.switchInterval = minSwitchInterval + Math.random() * (maxSwitchInterval - minSwitchInterval); + } } // Update the currently active camera if it has an update function @@ -142,6 +146,11 @@ export class CameraManager extends SceneFeature { activeCamData.update(); } } + + onPartyStart() { + // Start the camera switching timer only when the party starts + this.lastSwitchTime = state.clock.getElapsedTime(); + } } new CameraManager(); \ No newline at end of file diff --git a/party-cathedral/src/scene/dancers.js b/party-cathedral/src/scene/dancers.js index c77314f..101e436 100644 --- a/party-cathedral/src/scene/dancers.js +++ b/party-cathedral/src/scene/dancers.js @@ -75,6 +75,7 @@ export class Dancers extends SceneFeature { const material = materials[index % materials.length]; const dancer = new THREE.Mesh(geometry, material); dancer.position.copy(pos); + dancer.visible = false; // Start invisible state.scene.add(dancer); this.dancers.push({ @@ -92,6 +93,7 @@ export class Dancers extends SceneFeature { // --- Jumping State --- isJumping: false, jumpStartTime: 0, + }); }); }; @@ -100,7 +102,7 @@ export class Dancers extends SceneFeature { } update(deltaTime) { - if (this.dancers.length === 0) return; + if (this.dancers.length === 0 || !state.partyStarted) return; const cameraPosition = new THREE.Vector3(); state.camera.getWorldPosition(cameraPosition); @@ -181,6 +183,25 @@ export class Dancers extends SceneFeature { } }); } + + onPartyStart() { + this.dancers.forEach(dancerObj => { + dancerObj.mesh.visible = true; + // Teleport to stage + dancerObj.state = 'WAITING'; + dancerObj.mesh.position.y = dancerObj.baseY; + dancerObj.waitStartTime = state.clock.getElapsedTime(); + }); + } + + onPartyEnd() { + this.dancers.forEach(dancerObj => { + dancerObj.isJumping = false; + //dancerObj.mesh.visible = false; + dancerObj.state = 'WAITING'; + dancerObj.waitStartTime = state.clock.getElapsedTime(); + }); + } } new Dancers(); \ No newline at end of file diff --git a/party-cathedral/src/scene/light-ball.js b/party-cathedral/src/scene/light-ball.js index f4753f5..bc9dc57 100644 --- a/party-cathedral/src/scene/light-ball.js +++ b/party-cathedral/src/scene/light-ball.js @@ -28,9 +28,11 @@ export class LightBall extends SceneFeature { const ball = new THREE.Mesh(ballGeometry, ballMaterial); ball.castShadow = false; ball.receiveShadow = false; + ball.visible = false; // Start invisible // --- Create the Light --- const light = new THREE.PointLight(color, lightIntensity, length / 1.5); + light.visible = false; // Start invisible // --- Initial Position --- ball.position.set( @@ -40,7 +42,7 @@ export class LightBall extends SceneFeature { ); light.position.copy(ball.position); - //state.scene.add(ball); + //state.scene.add(ball); // no need to show the ball state.scene.add(light); this.lightBalls.push({ @@ -54,6 +56,8 @@ export class LightBall extends SceneFeature { } update(deltaTime) { + if (!state.partyStarted) return; + const time = state.clock.getElapsedTime(); this.lightBalls.forEach(lb => { const { mesh, light, driftSpeed, offset } = lb; @@ -72,6 +76,20 @@ export class LightBall extends SceneFeature { } }); } + + onPartyStart() { + this.lightBalls.forEach(lb => { + //lb.mesh.visible = true; // no visible ball + lb.light.visible = true; + }); + } + + onPartyEnd() { + this.lightBalls.forEach(lb => { + lb.mesh.visible = false; + lb.light.visible = false; + }); + } } new LightBall(); \ No newline at end of file diff --git a/party-cathedral/src/scene/medieval-musicians.js b/party-cathedral/src/scene/medieval-musicians.js index 6f52513..b964b6f 100644 --- a/party-cathedral/src/scene/medieval-musicians.js +++ b/party-cathedral/src/scene/medieval-musicians.js @@ -92,6 +92,7 @@ export class MedievalMusicians extends SceneFeature { // Randomly pick one of the created materials const material = materials[Math.floor(index % materials.length)]; const musician = new THREE.Mesh(geometry, material); + musician.visible = false; // Start invisible musician.position.copy(pos); state.scene.add(musician); @@ -122,7 +123,7 @@ export class MedievalMusicians extends SceneFeature { update(deltaTime) { // Billboard effect: make each musician face the camera - if (this.musicians.length > 0) { + if (this.musicians.length > 0 && state.partyStarted) { const cameraPosition = new THREE.Vector3(); state.camera.getWorldPosition(cameraPosition); @@ -257,6 +258,24 @@ export class MedievalMusicians extends SceneFeature { }); } } + + onPartyStart() { + this.musicians.forEach(musicianObj => { + musicianObj.mesh.visible = true; + // Teleport to stage + musicianObj.state = 'WAITING'; + musicianObj.waitStartTime = state.clock.getElapsedTime(); + }); + } + + onPartyEnd() { + this.musicians.forEach(musicianObj => { + musicianObj.isJumping = false; + musicianObj.state = 'WAITING'; + musicianObj.waitStartTime = state.clock.getElapsedTime(); + //musicianObj.mesh.visible = false; + }); + } } new MedievalMusicians(); \ No newline at end of file diff --git a/party-cathedral/src/scene/music-player.js b/party-cathedral/src/scene/music-player.js new file mode 100644 index 0000000..8fb8a8c --- /dev/null +++ b/party-cathedral/src/scene/music-player.js @@ -0,0 +1,92 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class MusicPlayer extends SceneFeature { + constructor() { + super(); + sceneFeatureManager.register(this); + } + + init() { + state.music.player = document.getElementById('audioPlayer'); + const loadButton = document.getElementById('loadMusicButton'); + const fileInput = document.getElementById('musicFileInput'); + const uiContainer = document.getElementById('ui-container'); + const metadataContainer = document.getElementById('metadata-container'); + const songTitleElement = document.getElementById('song-title'); + + loadButton.addEventListener('click', () => { + fileInput.click(); + }); + + fileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (file) { + // Hide the main button + loadButton.style.display = 'none'; + + // Show metadata + songTitleElement.textContent = file.name.replace(/\.[^/.]+$/, ""); // Show filename without extension + metadataContainer.classList.remove('hidden'); + + const url = URL.createObjectURL(file); + state.music.player.src = url; + + // Wait 5 seconds, then start the party + setTimeout(() => { + metadataContainer.classList.add('hidden'); + this.startParty(); + }, 5000); + } + }); + + state.music.player.addEventListener('ended', () => { + this.stopParty(); + uiContainer.style.display = 'flex'; // Show the button again + }); + + state.clock.stop(); + } + + startParty() { + state.clock.start(); + state.music.player.play(); + document.getElementById('ui-container').style.display = 'none'; + state.partyStarted = true; + + // You could add BPM detection here in the future + // For now, we use the fixed BPM + + // Trigger 'start' event for other features + this.notifyFeatures('onPartyStart'); + } + + stopParty() { + state.clock.stop(); + state.partyStarted = false; + setTimeout(() => { + const startButton = document.getElementById('loadMusicButton'); + startButton.style.display = 'block'; + startButton.textContent = "Party some more?" + }, 5000); + // Trigger 'end' event for other features + this.notifyFeatures('onPartyEnd'); + } + + notifyFeatures(methodName) { + sceneFeatureManager.features.forEach(feature => { + if (typeof feature[methodName] === 'function') { + feature[methodName](); + } + }); + } + + update(deltaTime) { + // The music player updates itself via events, + // but this could be used for real-time analysis in the future. + } +} + +new MusicPlayer(); \ No newline at end of file diff --git a/party-cathedral/src/scene/music-visualizer.js b/party-cathedral/src/scene/music-visualizer.js index c359c56..535caf6 100644 --- a/party-cathedral/src/scene/music-visualizer.js +++ b/party-cathedral/src/scene/music-visualizer.js @@ -20,7 +20,7 @@ export class MusicVisualizer extends SceneFeature { } update(deltaTime) { - if (!state.music) return; + if (!state.music || !state.partyStarted) return; const time = state.clock.getElapsedTime(); diff --git a/party-cathedral/src/scene/party-guests.js b/party-cathedral/src/scene/party-guests.js index aee0520..f62abf7 100644 --- a/party-cathedral/src/scene/party-guests.js +++ b/party-cathedral/src/scene/party-guests.js @@ -71,6 +71,7 @@ export class PartyGuests extends SceneFeature { guestHeight / 2, (Math.random() * 20) - 2 // Position them in the main hall ); + guest.visible = false; // Start invisible guest.position.copy(pos); state.scene.add(guest); @@ -92,14 +93,14 @@ export class PartyGuests extends SceneFeature { } update(deltaTime) { - if (this.guests.length === 0) return; + if (this.guests.length === 0 || !state.partyStarted) 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: 2 }; + const movementArea = { x: 10, z: 20, y: 0, centerZ: -4 }; const jumpChance = 0.05; // Jump way more const jumpDuration = 0.5; const jumpHeight = 0.1; @@ -166,6 +167,25 @@ export class PartyGuests extends SceneFeature { } }); } + + onPartyStart() { + const stageFrontZ = -40 / 2 + 5 + 5; // In front of the stage + this.guests.forEach((guestObj, index) => { + guestObj.mesh.visible = true; + // Rush to the stage + guestObj.state = 'MOVING'; + if (index % 2 === 0) { + guestObj.targetPosition.z = stageFrontZ + (Math.random() - 0.5) * 5; + } + }); + } + + onPartyEnd() { + this.guests.forEach(guestObj => { + guestObj.isJumping = false; + guestObj.state = 'WAITING'; + }); + } } 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 3a429c8..8cb9ecb 100644 --- a/party-cathedral/src/scene/root.js +++ b/party-cathedral/src/scene/root.js @@ -16,6 +16,7 @@ import { MusicVisualizer } from './music-visualizer.js'; import { RoseWindowLight } from './rose-window-light.js'; import { RoseWindowLightshafts } from './rose-window-lightshafts.js'; import { StainedGlass } from './stained-glass-window.js'; +import { MusicPlayer } from './music-player.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-cathedral/src/scene/stage-torches.js b/party-cathedral/src/scene/stage-torches.js index d6573f5..b59e157 100644 --- a/party-cathedral/src/scene/stage-torches.js +++ b/party-cathedral/src/scene/stage-torches.js @@ -38,6 +38,7 @@ export class StageTorches extends SceneFeature { createTorch(position) { const torchGroup = new THREE.Group(); torchGroup.position.copy(position); + torchGroup.visible = false; // Start invisible // --- Torch Holder --- const holderMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6, metalness: 0.5 }); @@ -86,7 +87,23 @@ export class StageTorches extends SceneFeature { return { group: torchGroup, light: pointLight, particles: particleSystem, particleData: particleData }; } + resetParticles(torch) { + const positions = torch.particles.geometry.attributes.position.array; + for (let i = 0; i < torch.particleData.length; i++) { + const data = torch.particleData[i]; + // Reset particle + positions[i * 3] = 0; + positions[i * 3 + 1] = 1; + positions[i * 3 + 2] = 0; + data.life = Math.random() * 1.0; + data.velocity.y = Math.random() * 1.5; + } + torch.particles.geometry.attributes.position.needsUpdate = true; + } + update(deltaTime) { + if (!state.partyStarted) return; + this.torches.forEach(torch => { let measurePulse = 0; if (state.music) { @@ -100,7 +117,7 @@ export class StageTorches extends SceneFeature { const data = torch.particleData[i]; data.life -= deltaTime; const yVelocity = data.velocity.y; - if (data.life <= 0) { + if (data.life <= 0 || positions[i * 3 + 1] < 0) { // Reset particle positions[i * 3] = 0; positions[i * 3 + 1] = 1; @@ -130,6 +147,18 @@ export class StageTorches extends SceneFeature { torch.light.position.y = lightPositionBaseY + averageY; }); } + + onPartyStart() { + this.torches.forEach(torch => { + torch.group.visible = true; + this.resetParticles(torch); + }); + } + + onPartyEnd() { + this.torches.forEach(torch => { + }); + } } new StageTorches(); \ No newline at end of file diff --git a/party-cathedral/src/state.js b/party-cathedral/src/state.js index 4846e6c..abacc87 100644 --- a/party-cathedral/src/state.js +++ b/party-cathedral/src/state.js @@ -40,6 +40,7 @@ export function initState() { roomHeight: 3, debugLight: false, // Turn on light helpers debugCamera: false, // Turn on camera helpers + partyStarted: false, // DOM Elements container: document.body,