Feature: blackout to emphasize music drops and calm sections

This commit is contained in:
Dejvino 2026-01-04 10:09:04 +00:00
parent 8b2d1aed53
commit 72a2fe7de2
9 changed files with 101 additions and 12 deletions

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
});

View File

@ -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,