From 72a2fe7de204d704dae89b7584cbd50fbcc4e001 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 4 Jan 2026 10:09:04 +0000 Subject: [PATCH] Feature: blackout to emphasize music drops and calm sections --- party-stage/src/scene/config-ui.js | 6 ++++ party-stage/src/scene/music-console.js | 7 ++++- party-stage/src/scene/music-visualizer.js | 35 +++++++++++++++++++++ party-stage/src/scene/projection-screen.js | 13 ++++++++ party-stage/src/scene/stage-lasers.js | 36 ++++++++++++++++------ party-stage/src/scene/stage-light-bars.js | 7 +++++ party-stage/src/scene/stage-lights.js | 5 ++- party-stage/src/scene/stage-torches.js | 2 +- party-stage/src/state.js | 2 ++ 9 files changed, 101 insertions(+), 12 deletions(-) diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index 96b8fca..3253ebc 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -103,6 +103,11 @@ export class ConfigUI extends SceneFeature { rightContainer.appendChild(row); }; + // Blackout Toggle + createToggle('BLACKOUT', 'blackout', (enabled) => { + state.blackoutMode = enabled; + }); + // Torches Toggle createToggle('Stage Torches', 'torchesEnabled'); @@ -475,6 +480,7 @@ export class ConfigUI extends SceneFeature { lightBarsEnabled: true, laserColorMode: 'RUNNING', guestCount: 150, + blackout: false, djHat: 'None' }; for (const key in defaults) { diff --git a/party-stage/src/scene/music-console.js b/party-stage/src/scene/music-console.js index 4f201cb..2c75316 100644 --- a/party-stage/src/scene/music-console.js +++ b/party-stage/src/scene/music-console.js @@ -186,6 +186,11 @@ export class MusicConsole extends SceneFeature { const beatIntensity = state.music.beatIntensity; this.lights.forEach(light => { + if (state.blackoutMode) { + light.mesh.material.color.copy(light.offColor); + return; + } + if (time > light.nextToggle) { // Toggle state light.active = !light.active; @@ -207,7 +212,7 @@ export class MusicConsole extends SceneFeature { // Update Front LED Array if (this.frontLedMesh) { - if (!state.config.consoleRGBEnabled) { + if (!state.config.consoleRGBEnabled || state.blackoutMode) { this.frontLedMesh.visible = false; return; } diff --git a/party-stage/src/scene/music-visualizer.js b/party-stage/src/scene/music-visualizer.js index adeb1bf..ef8178b 100644 --- a/party-stage/src/scene/music-visualizer.js +++ b/party-stage/src/scene/music-visualizer.js @@ -5,6 +5,8 @@ import sceneFeatureManager from './SceneFeatureManager.js'; export class MusicVisualizer extends SceneFeature { constructor() { super(); + this.lastStateChangeTime = 0; + this.lastBlackoutMode = false; sceneFeatureManager.register(this); } @@ -34,6 +36,39 @@ export class MusicVisualizer extends SceneFeature { // This creates a very sharp spike for the torch flame effect const measureProgress = (time % state.music.measureDuration) / state.music.measureDuration; state.music.measurePulse = measureProgress < 0.2 ? Math.sin(measureProgress * Math.PI * 5) : 0; + + // --- Auto-Blackout Logic --- + // If blackout is active, monitor for loud events (The Drop) to disable it. + if (state.config.blackout) { + if (state.blackoutMode !== this.lastBlackoutMode) { + this.lastStateChangeTime = time; + this.lastBlackoutMode = state.blackoutMode; + } + + const timeInState = time - this.lastStateChangeTime; + + // Dynamic Thresholds + // Exit blackout: Start high 0.8 and decrease over time + let loudThreshold = Math.max(0.4, 0.8 - (timeInState * 0.05)); + + // Enter blackout: Start low and increase over time + let quietThreshold = Math.min(0.2, 0.05 + (timeInState * 0.01)); + + const beatThreshold = 0.8; + + if (state.blackoutMode) { + if (state.music.loudness > loudThreshold && state.music.beatIntensity > beatThreshold) { + state.blackoutMode = false; + } + } else { + if (state.music.loudness < quietThreshold) { + state.blackoutMode = true; + } + } + } else { + state.blackoutMode = false; + this.lastBlackoutMode = false; + } } } diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 5318632..85ea702 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -257,6 +257,10 @@ export class ProjectionScreen extends SceneFeature { const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0; state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5); + if (state.blackoutMode) { + state.screenLight.intensity = 0; + } + // Update color buffer const colors = state.config.lightBarColors; const colorCount = colors ? Math.min(colors.length, 16) : 0; @@ -277,6 +281,10 @@ export class ProjectionScreen extends SceneFeature { if (s.mesh.material.uniforms.u_colorCount) { s.mesh.material.uniforms.u_colorCount.value = colorCount; } + if (s.mesh.material.uniforms.u_opacity) { + const targetOpacity = state.blackoutMode ? 0.1 : state.screenOpacity; + s.mesh.material.uniforms.u_opacity.value = targetOpacity; + } } }); } @@ -463,6 +471,11 @@ export function updateScreenEffect() { material.uniforms.u_time.value = state.clock.getElapsedTime(); material.uniforms.u_effect_type.value = state.screenEffect.type; material.uniforms.u_effect_strength.value = strength; + + if (material.uniforms.u_opacity) { + const targetOpacity = state.blackoutMode ? 0.1 : (state.screenOpacity !== undefined ? state.screenOpacity : 0.7); + material.uniforms.u_opacity.value = targetOpacity; + } if (progress >= 1.0) { material.uniforms.u_effect_type.value = 0.0; diff --git a/party-stage/src/scene/stage-lasers.js b/party-stage/src/scene/stage-lasers.js index 605a08b..f7330b7 100644 --- a/party-stage/src/scene/stage-lasers.js +++ b/party-stage/src/scene/stage-lasers.js @@ -14,6 +14,8 @@ export class StageLasers extends SceneFeature { this.stateTimer = 0; this.initialSilenceSeconds = 10; this.currentCycleMode = 'RUNNING'; + this.dummyColor = new THREE.Color(); + this.areLasersVisible = true; sceneFeatureManager.register(this); } @@ -103,14 +105,20 @@ export class StageLasers extends SceneFeature { update(deltaTime) { if (!state.partyStarted) { - this.lasers.forEach(l => l.mesh.visible = false); + if (this.areLasersVisible) { + this.lasers.forEach(l => l.mesh.visible = false); + this.areLasersVisible = false; + } return; } const time = state.clock.getElapsedTime(); - if (!state.config.lasersEnabled) { - this.lasers.forEach(l => l.mesh.visible = false); + if (!state.config.lasersEnabled || state.blackoutMode) { + if (this.areLasersVisible) { + this.lasers.forEach(l => l.mesh.visible = false); + this.areLasersVisible = false; + } if (state.laserData) state.laserData.count = 0; return; } @@ -156,6 +164,7 @@ export class StageLasers extends SceneFeature { if (this.stateTimer <= 0) { this.activationState = 'COOLDOWN'; this.stateTimer = 4.0; // Cooldown duration + isActive = false; } } else if (this.activationState === 'COOLDOWN') { this.stateTimer -= deltaTime; @@ -166,10 +175,18 @@ export class StageLasers extends SceneFeature { } if (!isActive) { - this.lasers.forEach(l => l.mesh.visible = false); + if (this.areLasersVisible) { + this.lasers.forEach(l => l.mesh.visible = false); + this.areLasersVisible = false; + } return; } + if (!this.areLasersVisible) { + this.lasers.forEach(l => l.mesh.visible = true); + this.areLasersVisible = true; + } + // --- Pattern Logic --- if (time - this.lastPatternChange > 8) { // Change every 8 seconds this.pattern = (this.pattern + 1) % 4; @@ -192,8 +209,6 @@ export class StageLasers extends SceneFeature { } this.lasers.forEach(l => { - l.mesh.visible = !['IDLE', 'COOLDOWN'].includes(this.activationState); - let currentIntensity = intensity; let flareScale = 1.0; @@ -235,8 +250,8 @@ export class StageLasers extends SceneFeature { } else { // Fallback if no palette: Cycle hue const hue = (time * 0.1) % 1; - const c = new THREE.Color().setHSL(hue, 1.0, 0.5); - colorHex = c.getHex(); + this.dummyColor.setHSL(hue, 1.0, 0.5); + colorHex = this.dummyColor.getHex(); } l.mesh.material.color.set(colorHex); @@ -279,7 +294,10 @@ export class StageLasers extends SceneFeature { } onPartyEnd() { - this.lasers.forEach(l => l.mesh.visible = false); + if (this.areLasersVisible) { + this.lasers.forEach(l => l.mesh.visible = false); + this.areLasersVisible = false; + } } } diff --git a/party-stage/src/scene/stage-light-bars.js b/party-stage/src/scene/stage-light-bars.js index dd1e350..e32d3b9 100644 --- a/party-stage/src/scene/stage-light-bars.js +++ b/party-stage/src/scene/stage-light-bars.js @@ -114,6 +114,13 @@ export class StageLightBars extends SceneFeature { update(deltaTime) { if (!state.partyStarted) return; if (!state.config.lightBarsEnabled) return; + + if (state.blackoutMode) { + this.bars.forEach(bar => { + bar.material.emissiveIntensity = 0; + }); + return; + } const time = state.clock.getElapsedTime(); const beatIntensity = state.music ? state.music.beatIntensity : 0; diff --git a/party-stage/src/scene/stage-lights.js b/party-stage/src/scene/stage-lights.js index 9e3330c..2ecc48a 100644 --- a/party-stage/src/scene/stage-lights.js +++ b/party-stage/src/scene/stage-lights.js @@ -136,7 +136,10 @@ export class StageLights extends SceneFeature { this.focusPoint.lerp(this.targetFocusPoint, deltaTime * 2.0); // Update each light - const intensity = state.music ? 20 + state.music.beatIntensity * 150 : 50; + let intensity = state.music ? 20 + state.music.beatIntensity * 150 : 50; + if (state.blackoutMode) { + intensity = 0; + } const spread = 0.2 + (state.music ? state.music.beatIntensity * 0.4 : 0); const bounce = state.music ? state.music.beatIntensity * 0.5 : 0; diff --git a/party-stage/src/scene/stage-torches.js b/party-stage/src/scene/stage-torches.js index 474019a..d75e568 100644 --- a/party-stage/src/scene/stage-torches.js +++ b/party-stage/src/scene/stage-torches.js @@ -104,7 +104,7 @@ export class StageTorches extends SceneFeature { } update(deltaTime) { - const enabled = state.config.torchesEnabled; + const enabled = state.config.torchesEnabled && !state.blackoutMode; this.torches.forEach(torch => { if (torch.group.visible !== enabled) torch.group.visible = enabled; }); diff --git a/party-stage/src/state.js b/party-stage/src/state.js index 635fcd6..686c983 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -14,6 +14,7 @@ export function initState() { laserColorMode: 'RUNNING', // 'SINGLE', 'RANDOM', 'RUNNING', 'ANY' lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors guestCount: 150, + blackout: false, djHat: 'None' // 'None', 'Santa', 'Top Hat' }; try { @@ -61,6 +62,7 @@ export function initState() { debugLight: false, // Turn on light helpers debugCamera: false, // Turn on camera helpers partyStarted: false, + blackoutMode: false, // Feature Configuration config: config,