diff --git a/party-stage/src/effects/dust.js b/party-stage/src/effects/dust.js index b8942b2..6d2f206 100644 --- a/party-stage/src/effects/dust.js +++ b/party-stage/src/effects/dust.js @@ -21,7 +21,7 @@ export class DustEffect { particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); const particleMaterial = new THREE.PointsMaterial({ - color: 0xffffff, + color: 0xaaaaaa, size: 0.015, transparent: true, opacity: 0.08, diff --git a/party-stage/src/scene/party-guests.js b/party-stage/src/scene/party-guests.js index 7f7db1b..c0510fa 100644 --- a/party-stage/src/scene/party-guests.js +++ b/party-stage/src/scene/party-guests.js @@ -61,7 +61,7 @@ export class PartyGuests extends SceneFeature { const createGuests = () => { const geometry = new THREE.PlaneGeometry(guestWidth, guestHeight); - const numGuests = 80; + const numGuests = 150; for (let i = 0; i < numGuests; i++) { const material = materials[i % materials.length]; diff --git a/party-stage/src/scene/root.js b/party-stage/src/scene/root.js index 5247e8a..2a7a828 100644 --- a/party-stage/src/scene/root.js +++ b/party-stage/src/scene/root.js @@ -9,11 +9,10 @@ import { Stage } from './stage.js'; import { PartyGuests } from './party-guests.js'; import { StageTorches } from './stage-torches.js'; import { MusicVisualizer } from './music-visualizer.js'; -import { RoseWindowLight } from './rose-window-light.js'; -import { RoseWindowLightshafts } from './rose-window-lightshafts.js'; import { MusicPlayer } from './music-player.js'; import { WallCurtain } from './wall-curtain.js'; import { ReproWall } from './repro-wall.js'; +import { StageLights } from './stage-lights.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-stage/src/scene/rose-window-light.js b/party-stage/src/scene/rose-window-light.js deleted file mode 100644 index cc6b981..0000000 --- a/party-stage/src/scene/rose-window-light.js +++ /dev/null @@ -1,66 +0,0 @@ -import * as THREE from 'three'; -import { state } from '../state.js'; -import { SceneFeature } from './SceneFeature.js'; -import sceneFeatureManager from './SceneFeatureManager.js'; - -export class RoseWindowLight extends SceneFeature { - constructor() { - super(); - this.spotlight = null; - this.helper = null; - sceneFeatureManager.register(this); - } - - init() { - // --- Dimensions for positioning --- - const length = 40; - const naveHeight = 15; - const stageDepth = 5; - - // --- Create the spotlight --- - this.spotlight = new THREE.SpotLight(0xffffff, 100.0); // White light, high intensity - this.spotlight.position.set(0, naveHeight, -length / 2 + 10); // Position it at the rose window - this.spotlight.angle = Math.PI / 9; // A reasonably focused beam - this.spotlight.penumbra = 0.3; // Soft edges - this.spotlight.decay = 0.7; - this.spotlight.distance = 30; - - this.spotlight.castShadow = false; - this.spotlight.shadow.mapSize.width = 1024; - this.spotlight.shadow.mapSize.height = 1024; - this.spotlight.shadow.camera.near = 1; - this.spotlight.shadow.camera.far = 30; - this.spotlight.shadow.focus = 1; - - // --- Create a target for the spotlight to aim at --- - const targetObject = new THREE.Object3D(); - targetObject.position.set(0, 0, -length / 2 + stageDepth); // Aim at the center of the stage - state.scene.add(targetObject); - this.spotlight.target = targetObject; - - state.scene.add(this.spotlight); - - // --- Add a debug helper --- - if (state.debugLight) { - this.helper = new THREE.SpotLightHelper(this.spotlight); - state.scene.add(this.helper); - } - } - - update(deltaTime) { - if (!this.spotlight) return; - - // Make the light pulse with the music - if (state.music) { - const baseIntensity = 4.0; - this.spotlight.intensity = baseIntensity + state.music.beatIntensity * 1.0; - } - - // Update the helper if it exists - if (this.helper) { - this.helper.update(); - } - } -} - -new RoseWindowLight(); \ No newline at end of file diff --git a/party-stage/src/scene/rose-window-lightshafts.js b/party-stage/src/scene/rose-window-lightshafts.js deleted file mode 100644 index 156e2d3..0000000 --- a/party-stage/src/scene/rose-window-lightshafts.js +++ /dev/null @@ -1,157 +0,0 @@ -import * as THREE from 'three'; -import { state } from '../state.js'; -import { SceneFeature } from './SceneFeature.js'; -import sceneFeatureManager from './SceneFeatureManager.js'; - -export class RoseWindowLightshafts extends SceneFeature { - constructor() { - super(); - this.shafts = []; - sceneFeatureManager.register(this); - } - - init() { - // --- Dimensions for positioning --- - const length = 40; - const naveWidth = 12; - const naveHeight = 15; - const stageDepth = 5; - const stageWidth = naveWidth - 1; - - const roseWindowRadius = naveWidth / 2 - 2; - const roseWindowCenter = new THREE.Vector3(0, naveHeight, -length / 2 - 1.1); - - // --- Procedural Noise Texture for Light Shafts --- - const createNoiseTexture = () => { - const width = 128; - const height = 512; - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const context = canvas.getContext('2d'); - const imageData = context.createImageData(width, height); - const data = imageData.data; - - for (let i = 0; i < data.length; i += 4) { - // Create vertical streaks of noise - const y = Math.floor((i / 4) / width); - const noise = Math.pow(Math.random(), 2.5) * (1 - y / height) * 255; - data[i] = noise; // R - data[i + 1] = noise; // G - data[i + 2] = noise; // B - data[i + 3] = 255; // A - } - context.putImageData(imageData, 0, 0); - return new THREE.CanvasTexture(canvas); - }; - - const baseMaterial = new THREE.MeshBasicMaterial({ - //map: texture, - blending: THREE.AdditiveBlending, - transparent: true, - depthWrite: false, - opacity: 1.0, - color: 0x88aaff, // Give the light a cool blueish tint - }); - - // --- Create multiple thin light shafts --- - const numShafts = 16; - for (let i = 0; i < numShafts; i++) { - const texture = createNoiseTexture(); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - const material = baseMaterial.clone(); // Each shaft needs its own material for individual opacity - material.map = texture; - - const startAngle = Math.random() * Math.PI * 2; - const startRadius = Math.random() * roseWindowRadius; - const startPoint = new THREE.Vector3( - roseWindowCenter.x + Math.cos(startAngle) * startRadius, - roseWindowCenter.y + Math.sin(startAngle) * startRadius, - roseWindowCenter.z - ); - - // Define a linear path on the floor for the beam to travel - const floorStartPoint = new THREE.Vector3( - (Math.random() - 0.5) * stageWidth * 0.75, - 0, - -length / 2 + Math.random() * 8 + 0 - ); - const floorEndPoint = new THREE.Vector3( - (Math.random() - 0.5) * stageWidth * 0.75, - 0, - -length / 2 + Math.random() * 8 + 3 - ); - - const distance = startPoint.distanceTo(floorStartPoint); - const geometry = new THREE.CylinderGeometry(0.01, 0.5 + Math.random() * 0.5, distance, 16, 1, true); - const lightShaft = new THREE.Mesh(geometry, material); - - state.scene.add(lightShaft); - this.shafts.push({ - mesh: lightShaft, - startPoint: startPoint, // The stationary point in the window - endPoint: floorStartPoint.clone(), // The current position of the beam on the floor - floorStartPoint: floorStartPoint, // The start of the sweep path - floorEndPoint: floorEndPoint, // The end of the sweep path - moveSpeed: 0.01 + Math.random() * 0.5, // Each shaft has a different speed - // No 'state' needed anymore - }); - } - } - - update(deltaTime) { - const baseOpacity = 0.1; - - this.shafts.forEach(shaft => { - const { mesh, startPoint, endPoint, floorStartPoint, floorEndPoint, moveSpeed } = shaft; - - // Animate texture for dust motes - mesh.material.map.offset.y += deltaTime * 0.004; - mesh.material.map.offset.x -= deltaTime * 0.02; - - if (mesh.material.map.offset.y >= 0.2) { - mesh.material.map.offset.y -= 0.2; - } - if (mesh.material.map.offset.x <= 0.0) { - mesh.material.map.offset.x += 1.0; - } - - // --- Movement Logic --- - const pathDirection = floorEndPoint.clone().sub(floorStartPoint).normalize(); - const pathLength = floorStartPoint.distanceTo(floorEndPoint); - - // Move the endpoint along its path - endPoint.add(pathDirection.clone().multiplyScalar(moveSpeed * deltaTime)); - - const currentDistance = floorStartPoint.distanceTo(endPoint); - - if (currentDistance >= pathLength) { - // Reached the end, reset to the start - endPoint.copy(floorStartPoint); - } - - // --- Opacity based on Progress --- - const progress = Math.min(currentDistance / pathLength, 1.0); - // Use a sine curve to fade in at the start and out at the end - const fadeOpacity = Math.sin(progress * Math.PI) * baseOpacity; - - // --- Update Mesh Position and Orientation --- - const distance = startPoint.distanceTo(endPoint); - mesh.scale.y = -distance/5; - mesh.position.lerpVectors(startPoint, endPoint, 0.5); - - const quaternion = new THREE.Quaternion(); - const cylinderUp = new THREE.Vector3(0, 1, 0); - const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize(); - quaternion.setFromUnitVectors(cylinderUp, direction); - mesh.quaternion.copy(quaternion); - - // --- Music Visualization --- - const beatPulse = state.music ? state.music.beatIntensity * 0.05 : 0; - mesh.material.opacity = fadeOpacity + beatPulse; - }); - } -} - -new RoseWindowLightshafts(); \ No newline at end of file diff --git a/party-stage/src/scene/stage-lights.js b/party-stage/src/scene/stage-lights.js new file mode 100644 index 0000000..75d6df7 --- /dev/null +++ b/party-stage/src/scene/stage-lights.js @@ -0,0 +1,141 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class StageLights extends SceneFeature { + constructor() { + super(); + this.lights = []; + this.focusPoint = new THREE.Vector3(0, 0, -10); + this.targetFocusPoint = new THREE.Vector3(0, 0, -10); + this.lastChangeTime = 0; + sceneFeatureManager.register(this); + } + + init() { + // 1. Create the Steel Beam + const beamLength = 24; + const beamGeo = new THREE.BoxGeometry(beamLength, 0.5, 0.5); + const beamMat = new THREE.MeshStandardMaterial({ + color: 0x444444, + metalness: 0.9, + roughness: 0.4 + }); + this.beam = new THREE.Mesh(beamGeo, beamMat); + // Positioned high above the front of the stage area + this.beam.position.set(0, 8, -14); + this.beam.castShadow = true; + this.beam.receiveShadow = true; + state.scene.add(this.beam); + + // 2. Create Spotlights + const numLights = 8; + const spacing = beamLength / numLights; + + // Geometry for the light fixture (par can style) + const fixtureGeo = new THREE.CylinderGeometry(0.2, 0.3, 0.6); + // Rotate geometry so -Y (bottom) points to +Z (lookAt direction) + fixtureGeo.rotateX(-Math.PI / 2); + + const fixtureMat = new THREE.MeshStandardMaterial({ color: 0x111111 }); + const lensGeo = new THREE.CircleGeometry(0.18, 32); + + for (let i = 0; i < numLights; i++) { + const x = -beamLength / 2 + spacing/2 + i * spacing; + + // Fixture Mesh + const fixture = new THREE.Mesh(fixtureGeo, fixtureMat); + fixture.position.set(x, 7.5, -14); // Match beam position + state.scene.add(fixture); + + // Lens Mesh + const lensMat = new THREE.MeshBasicMaterial({ color: 0xffffff }); + const lens = new THREE.Mesh(lensGeo, lensMat); + lens.position.set(0, 0, 0.31); + fixture.add(lens); + + // SpotLight + const spotLight = new THREE.SpotLight(0xffffee, 0); + spotLight.position.copy(fixture.position); + spotLight.angle = Math.PI / 6; + spotLight.penumbra = 0.3; + spotLight.decay = 1.5; + spotLight.distance = 60; + spotLight.castShadow = true; + spotLight.shadow.bias = -0.0001; + spotLight.shadow.mapSize.width = 512; + spotLight.shadow.mapSize.height = 512; + + // Target Object + const target = new THREE.Object3D(); + state.scene.add(target); + spotLight.target = target; + + state.scene.add(spotLight); + + this.lights.push({ + light: spotLight, + fixture: fixture, + lens: lens, + target: target, + baseX: x + }); + } + } + + update(deltaTime) { + const time = state.clock.getElapsedTime(); + + // Change target area logic + let shouldChange = false; + if (time < this.lastChangeTime) this.lastChangeTime = time; + + if (state.music && state.partyStarted) { + // Change on the measure (every 4 beats) if enough time has passed + if (state.music.measurePulse > 0.5 && time - this.lastChangeTime > 1.5) { + shouldChange = true; + } + } else if (time - this.lastChangeTime > 3.0) { + shouldChange = true; + } + + if (shouldChange) { + this.lastChangeTime = time; + + // Randomly pick a zone: Stage or Dance Floor + if (Math.random() < 0.4) { + // Stage Area (Z: -20 to -10) + this.targetFocusPoint.set((Math.random() - 0.5) * 15, 1, -15 + (Math.random() - 0.5) * 5); + } else { + // Dance Floor / Guests (Z: -5 to 15) + this.targetFocusPoint.set((Math.random() - 0.5) * 20, 0, 5 + (Math.random() - 0.5) * 15); + } + } + + // Smoothly move the focus point + this.focusPoint.lerp(this.targetFocusPoint, deltaTime * 2.0); + + // Update each light + const intensity = state.music ? 20 + state.music.beatIntensity * 150 : 50; + + const hue = (time * 0.2) % 1; + const color = new THREE.Color().setHSL(hue, 0.8, 0.5); + + const spread = 0.2 + (state.music ? state.music.beatIntensity * 0.4 : 0); + const bounce = state.music ? state.music.beatIntensity * 0.5 : 0; + + this.lights.forEach((item) => { + // Converge lights on focus point, but keep slight X offset for spread + const targetX = this.focusPoint.x + (item.baseX * spread); + + item.target.position.set(targetX, this.focusPoint.y + bounce, this.focusPoint.z); + item.fixture.lookAt(targetX, this.focusPoint.y, this.focusPoint.z); + item.light.intensity = intensity; + item.light.color.copy(color); + item.lens.material.color.copy(color); + }); + } +} + +new StageLights(); \ No newline at end of file