Feature: Music playback

This commit is contained in:
Dejvino
2025-11-23 20:33:32 +01:00
parent 141b3acf72
commit 451d4ea261
14 changed files with 255 additions and 54 deletions
+17 -8
View File
@@ -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();
+22 -1
View File
@@ -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();
+19 -1
View File
@@ -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();
@@ -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();
+92
View File
@@ -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();
@@ -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();
+22 -2
View File
@@ -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();
+1
View File
@@ -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 ---
+30 -1
View File
@@ -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();