diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index 3253ebc..8e8308b 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -103,6 +103,12 @@ export class ConfigUI extends SceneFeature { rightContainer.appendChild(row); }; + // Debug Panel Toggle + createToggle('Debug Panel', 'debugPanelEnabled', (enabled) => { + const debugPanel = sceneFeatureManager.features.find(f => f.constructor.name === 'DebugPanel'); + if (debugPanel) debugPanel.setVisibility(enabled); + }); + // Blackout Toggle createToggle('BLACKOUT', 'blackout', (enabled) => { state.blackoutMode = enabled; @@ -481,7 +487,8 @@ export class ConfigUI extends SceneFeature { laserColorMode: 'RUNNING', guestCount: 150, blackout: false, - djHat: 'None' + djHat: 'None', + debugPanelEnabled: false }; for (const key in defaults) { state.config[key] = defaults[key]; diff --git a/party-stage/src/scene/fps-counter.js b/party-stage/src/scene/fps-counter.js index bb94e2f..09f31be 100644 --- a/party-stage/src/scene/fps-counter.js +++ b/party-stage/src/scene/fps-counter.js @@ -1,7 +1,8 @@ import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; +import { state } from '../state.js'; -export class FPSCounter extends SceneFeature { +export class DebugPanel extends SceneFeature { constructor() { super(); this.frames = 0; @@ -15,10 +16,17 @@ export class FPSCounter extends SceneFeature { this.memCanvas = null; this.memCtx = null; this.memHistory = []; + this.musicTextElement = null; + this.musicCanvas = null; + this.musicCtx = null; + this.musicHistory = []; sceneFeatureManager.register(this); } init() { + const graphWidth = 150; + const graphHeight = 80; + // Create the FPS display container this.fpsElement = document.createElement('div'); Object.assign(this.fpsElement.style, { @@ -29,7 +37,8 @@ export class FPSCounter extends SceneFeature { padding: '5px', background: 'rgba(0, 0, 0, 0.5)', pointerEvents: 'none', - userSelect: 'none', + // Allow pointer events on the panel itself if we want to interact, but usually debug panels are pass-through + userSelect: 'none', display: 'flex', flexDirection: 'column', alignItems: 'flex-end' @@ -49,10 +58,10 @@ export class FPSCounter extends SceneFeature { // Graph Canvas this.canvas = document.createElement('canvas'); - this.canvas.width = 100; + this.canvas.width = graphWidth; this.canvas.height = 40; Object.assign(this.canvas.style, { - width: '100px', + width: graphWidth + 'px', height: '40px', background: '#222', border: '1px solid #444' @@ -75,10 +84,10 @@ export class FPSCounter extends SceneFeature { // Memory Graph Canvas this.memCanvas = document.createElement('canvas'); - this.memCanvas.width = 100; + this.memCanvas.width = graphWidth; this.memCanvas.height = 40; Object.assign(this.memCanvas.style, { - width: '100px', + width: graphWidth + 'px', height: '40px', background: '#222', border: '1px solid #444' @@ -86,10 +95,42 @@ export class FPSCounter extends SceneFeature { this.memCtx = this.memCanvas.getContext('2d'); this.fpsElement.appendChild(this.memCanvas); + // Music Text Display + this.musicTextElement = document.createElement('div'); + Object.assign(this.musicTextElement.style, { + color: '#ff00ff', + fontFamily: 'monospace', + fontSize: '16px', + fontWeight: 'bold', + marginBottom: '2px', + marginTop: '5px' + }); + this.musicTextElement.innerText = 'MUSIC'; + this.fpsElement.appendChild(this.musicTextElement); + + // Music Graph Canvas + this.musicCanvas = document.createElement('canvas'); + this.musicCanvas.width = graphWidth; + this.musicCanvas.height = graphHeight; + Object.assign(this.musicCanvas.style, { + width: graphWidth + 'px', + height: graphHeight + 'px', + background: '#222', + border: '1px solid #444' + }); + this.musicCtx = this.musicCanvas.getContext('2d'); + this.fpsElement.appendChild(this.musicCanvas); + document.body.appendChild(this.fpsElement); this.history = new Array(this.canvas.width).fill(0); this.memHistory = new Array(this.memCanvas.width).fill(0); + this.musicHistory = new Array(this.musicCanvas.width).fill({l:0, low:0, high:0, b:0, lt:0, qt:0, m:0.5, bo: false}); + + // Initialize visibility based on config + if (state.config) { + this.setVisibility(state.config.debugPanelEnabled); + } } update(deltaTime) { @@ -99,16 +140,7 @@ export class FPSCounter extends SceneFeature { const segment = 0.25; if (this.timeAccumulator >= segment) { const fps = Math.round(this.frames / this.timeAccumulator); - if (this.textElement) { - this.textElement.innerText = `FPS: ${fps}`; - if (fps >= 58) { - this.textElement.style.color = '#00ff00'; - } else if (fps >= 55) { - this.textElement.style.color = '#ffff00'; - } else { - this.textElement.style.color = '#ff0000'; - } - } + if (this.textElement) this.textElement.innerText = `FPS: ${fps}`; // Update Graph this.history.push(fps); @@ -127,14 +159,52 @@ export class FPSCounter extends SceneFeature { this.memHistory.shift(); } this.drawMemGraph(); + } else if (state.renderer) { + // Fallback for Firefox: Visualize Scene Complexity (Triangles) + const tris = state.renderer.info.render.triangles; + const kTris = Math.round(tris / 1000); + if (this.memTextElement) this.memTextElement.innerText = `TRIS: ${kTris}k`; + + this.memHistory.push(kTris); + if (this.memHistory.length > this.memCanvas.width) { + this.memHistory.shift(); + } + this.drawMemGraph(); } else { - if (this.memTextElement) this.memTextElement.style.display = 'none'; - if (this.memCanvas) this.memCanvas.style.display = 'none'; + if (this.memTextElement) this.memTextElement.innerText = `MEM: N/A`; } this.frames = 0; this.timeAccumulator = 0; } + + // Update Music Graph (Every frame for smoothness) + if (state.music) { + const loudness = state.music.loudness || 0; + const loudnessAvg = state.music.loudnessAverage || 0; + const lows = state.music.loudnessLows || 0; + const highs = state.music.loudnessHighs || 0; + const beat = state.music.beatIntensity || 0; + const thresholds = state.music.thresholds || { loud: 0, quiet: 0 }; + let modeVal = 0.5; + if (state.music.mode === 'Loud') modeVal = 1.0; + else if (state.music.mode === 'Quiet') modeVal = 0.0; + + this.musicHistory.push({ l: loudness, la: loudnessAvg, low: lows, high: highs, b: beat, lt: thresholds.loud, qt: thresholds.quiet, m: modeVal, bo: state.blackoutMode }); + if (this.musicHistory.length > this.musicCanvas.width) { + this.musicHistory.shift(); + } + this.drawMusicGraph(); + + if (this.musicTextElement) { + this.musicTextElement.innerText = `Volume:${loudness.toFixed(2)}\nMode:${state.music.mode}\nBPM:${state.music.bpm}`; + } + } + } + + setVisibility(visible) { + if (!this.fpsElement) return; + this.fpsElement.style.display = visible ? 'flex' : 'none'; } drawGraph() { @@ -155,25 +225,18 @@ export class FPSCounter extends SceneFeature { ctx.stroke(); // Draw Graph + ctx.beginPath(); + ctx.strokeStyle = '#00ff00'; ctx.lineWidth = 1.5; - for (let i = 0; i < this.history.length - 1; i++) { - const val = this.history[i + 1]; - const x1 = i; - const y1 = h - (this.history[i] / maxFps) * h; - const x2 = i + 1; - const y2 = h - (val / maxFps) * h; - - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.lineTo(x2, y2); - - if (val >= 58) ctx.strokeStyle = '#00ff00'; - else if (val >= 55) ctx.strokeStyle = '#ffff00'; - else ctx.strokeStyle = '#ff0000'; - - ctx.stroke(); + for (let i = 0; i < this.history.length; i++) { + const val = this.history[i]; + const x = i; + const y = h - (val / maxFps) * h; + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); } + ctx.stroke(); } drawMemGraph() { @@ -200,6 +263,118 @@ export class FPSCounter extends SceneFeature { } ctx.stroke(); } -} -new FPSCounter(); \ No newline at end of file + drawMusicGraph() { + if (!this.musicCtx) return; + const ctx = this.musicCtx; + const w = this.musicCanvas.width; + const h = this.musicCanvas.height; + + ctx.clearRect(0, 0, w, h); + + // Draw Lows (Brown) + ctx.beginPath(); + ctx.strokeStyle = '#D2691E'; // Chocolate Brown + ctx.lineWidth = 1; + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].low !== undefined ? this.musicHistory[i].low : 0; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw All (Blue) + ctx.beginPath(); + ctx.strokeStyle = '#66aaff'; // Visible Blue + ctx.lineWidth = 1; + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].l; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw Highs (White) + ctx.beginPath(); + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 1; + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].high !== undefined ? this.musicHistory[i].high : 0; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw All Avg (Blue) + ctx.beginPath(); + ctx.strokeStyle = '#8866ff'; + ctx.lineWidth = 1; + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].la; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw Beat Intensity (Magenta) + ctx.beginPath(); + ctx.strokeStyle = '#ff00ff'; + ctx.lineWidth = 1; + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].b; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + + // Draw Active Threshold (Green for Loud/Blackout, Red for Quiet/Normal) + ctx.lineWidth = 1; + + for (let i = 0; i < this.musicHistory.length - 1; i++) { + const curr = this.musicHistory[i]; + const next = this.musicHistory[i+1]; + + ctx.beginPath(); + + if (curr.bo) { + // Blackout Mode: Draw Loud Threshold (Green Dashed) + ctx.strokeStyle = '#00ff00'; + ctx.setLineDash([2, 2]); + ctx.moveTo(i, h - (curr.lt * h)); + ctx.lineTo(i + 1, h - (next.lt * h)); + } else { + // Normal Mode: Draw Quiet Threshold (Red Solid) + ctx.strokeStyle = '#ff0000'; + ctx.setLineDash([]); + ctx.moveTo(i, h - (curr.qt * h)); + ctx.lineTo(i + 1, h - (next.qt * h)); + } + ctx.stroke(); + } + + // Draw Mode (Yellow) + ctx.beginPath(); + ctx.strokeStyle = '#ffff00'; + ctx.setLineDash([1, 2]); + for (let i = 0; i < this.musicHistory.length; i++) { + const val = this.musicHistory[i].m !== undefined ? this.musicHistory[i].m : 0.5; + const x = i; + const y = h - (val * h); + if (i === 0) ctx.moveTo(x, y); + else ctx.lineTo(x, y); + } + ctx.stroke(); + ctx.setLineDash([]); // Reset dash + } +} +new DebugPanel(); \ No newline at end of file diff --git a/party-stage/src/scene/music-player.js b/party-stage/src/scene/music-player.js index e19a92a..11f6085 100644 --- a/party-stage/src/scene/music-player.js +++ b/party-stage/src/scene/music-player.js @@ -11,15 +11,13 @@ export class MusicPlayer extends SceneFeature { this.analyser = null; this.source = null; this.dataArray = null; - this.loudnessHistory = []; sceneFeatureManager.register(this); } init() { state.music.player = document.getElementById('audioPlayer'); state.music.loudness = 0; - state.music.isLoudEnough = false; - + state.music.loudnessAverage = 0; const loadButton = document.getElementById('loadMusicButton'); const fileInput = document.getElementById('musicFileInput'); @@ -58,6 +56,7 @@ export class MusicPlayer extends SceneFeature { this.source.connect(this.analyser); this.analyser.connect(this.audioContext.destination); this.dataArray = new Uint8Array(this.analyser.frequencyBinCount); + state.music.frequencyData = this.dataArray; } // Update State @@ -99,9 +98,6 @@ export class MusicPlayer extends SceneFeature { 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'); } @@ -125,27 +121,7 @@ export class MusicPlayer extends SceneFeature { if (!state.partyStarted || !this.analyser) return; this.analyser.getByteFrequencyData(this.dataArray); - - // --- Calculate current loudness --- - let sum = 0; - for (let i = 0; i < this.dataArray.length; i++) { - sum += this.dataArray[i]; - } - const average = sum / this.dataArray.length; - state.music.loudness = average / 255; // Normalize to 0-1 range - - // --- Track loudness over the last 2 seconds --- - this.loudnessHistory.push(state.music.loudness); - if (this.loudnessHistory.length > 120) { // Assuming ~60fps, 2 seconds of history - this.loudnessHistory.shift(); - } - - // --- Determine if it's loud enough to jump --- - const avgLoudness = this.loudnessHistory.reduce((a, b) => a + b, 0) / this.loudnessHistory.length; - const quietThreshold = 0.1; // Adjust this value based on testing - - state.music.isLoudEnough = avgLoudness > quietThreshold; } } -new MusicPlayer(); \ No newline at end of file +new MusicPlayer(); diff --git a/party-stage/src/scene/music-visualizer.js b/party-stage/src/scene/music-visualizer.js index ef8178b..eb6e287 100644 --- a/party-stage/src/scene/music-visualizer.js +++ b/party-stage/src/scene/music-visualizer.js @@ -1,3 +1,4 @@ +import * as THREE from 'three'; import { state } from '../state.js'; import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; @@ -7,53 +8,158 @@ export class MusicVisualizer extends SceneFeature { super(); this.lastStateChangeTime = 0; this.lastBlackoutMode = false; + this.lastBeatTime = 0; + this.beatIntervals = []; + this.beatThreshold = 0.3; + this.loudnessHistory = []; + this.loudnessLowsHistory = []; sceneFeatureManager.register(this); } init() { // Initialize music state state.music = { - bpm: 120, beatDuration: 60 / 120, measureDuration: (60 / 120) * 4, beatIntensity: 0, measurePulse: 0, isLoudEnough: false, + thresholds: { loud: 0, quiet: 0 }, + mode: 'Normal', + bpm: 50, + loudness: 0, + loudnessLows: 0, + loudnessHighs: 0, + loudnessAverage: 0, + loudnessLowsAverage: 0, + frequencyData: null }; + this.beatPhase = 0; + this.measurePhase = 0; + this.averageLoudness = 0; } update(deltaTime) { if (!state.music || !state.partyStarted) return; + // --- Calculate Loudness from Frequency Data --- + if (state.music.frequencyData) { + const dataArray = state.music.frequencyData; + let sumLows = 0; + let sumHighs = 0; + let sumAll = 0; + const countLows = Math.min(dataArray.length * 0.2, dataArray.length); + const countHighs = Math.min(dataArray.length * 0.6, dataArray.length); + + for (let i = 0; i < dataArray.length; i++) { + sumAll += dataArray[i]; + if (i < countLows) { + sumLows += dataArray[i]; + } + if (i > dataArray.length - countHighs) { + sumHighs += dataArray[i]; + } + } + + const averageLows = sumLows / countLows; + const averageHighs = sumHighs / countHighs; + const averageAll = sumAll / dataArray.length; + + // Normalize to 0-1 range + state.music.loudness = averageAll / 255; + state.music.loudnessLows = averageLows / 255; + state.music.loudnessHighs = averageHighs / 255; + + // --- Track loudness over the last 2 seconds --- + this.loudnessHistory.push(state.music.loudness); + if (this.loudnessHistory.length > 120) this.loudnessHistory.shift(); + + this.loudnessLowsHistory.push(state.music.loudnessLows); + if (this.loudnessLowsHistory.length > 120) this.loudnessLowsHistory.shift(); + + const avgLoudness = this.loudnessHistory.reduce((a, b) => a + b, 0) / this.loudnessHistory.length; + const avgLowsLoudness = this.loudnessLowsHistory.reduce((a, b) => a + b, 0) / this.loudnessLowsHistory.length; + + state.music.loudnessAverage = avgLoudness; + state.music.loudnessLowsAverage = avgLowsLoudness; + state.music.isLoudEnough = avgLoudness > 0.1; + } + const time = state.clock.getElapsedTime(); + const loudness = state.music.loudness || 0; + const loudnessAvg = state.music.loudnessAverage || 0; + this.averageLoudness = loudnessAvg; - // --- Calculate Beat Intensity (pulses every beat) --- - // This creates a sharp attack and slower decay (0 -> 1 -> 0) - const beatProgress = (time % state.music.beatDuration) / state.music.beatDuration; - state.music.beatIntensity = Math.pow(1.0 - beatProgress, 2); + // --- Beat Detection & Auto-BPM --- + this.beatThreshold = Math.max(loudnessAvg * 1.01, this.beatThreshold * 0.96); - // --- Calculate Measure Pulse (spikes every 4 beats) --- - // 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; + if (loudness > this.beatThreshold) { + const now = time; + if (now - this.lastBeatTime > 0.3) { // Min interval (~200 BPM) + const interval = now - this.lastBeatTime; + this.lastBeatTime = now; + this.beatThreshold = loudness * 1.2; // Bump threshold + + // Valid BPM range: 60-180 (interval 1.0s - 0.33s) + if (interval >= 0.33 && interval <= 1.0) { + this.beatIntervals.push(interval); + if (this.beatIntervals.length > 8) this.beatIntervals.shift(); + + const avgInterval = this.beatIntervals.reduce((a, b) => a + b, 0) / this.beatIntervals.length; + + // Smoothly adjust beat duration + state.music.beatDuration = THREE.MathUtils.lerp(state.music.beatDuration, avgInterval, 0.1); + state.music.measureDuration = state.music.beatDuration * 4; + state.music.bpm = Math.round(60 / state.music.beatDuration); + } + + // Sync phase on beat + this.beatPhase = 0; + } + } + + // --- Phase Accumulation --- + this.beatPhase += deltaTime / state.music.beatDuration; + if (this.beatPhase >= 1) this.beatPhase -= 1; + + this.measurePhase += deltaTime / state.music.measureDuration; + if (this.measurePhase >= 1) this.measurePhase -= 1; + + // --- Calculate Beat Intensity --- + state.music.beatIntensity = Math.pow(1.0 - this.beatPhase, 2); + + // --- Calculate Measure Pulse --- + state.music.measurePulse = this.measurePhase < 0.2 ? Math.sin(this.measurePhase * Math.PI * 5) : 0; + + if (state.blackoutMode !== this.lastBlackoutMode) { + this.lastStateChangeTime = time; + this.lastBlackoutMode = state.blackoutMode; + } + + const timeInState = time - this.lastStateChangeTime; + let timeInStateLoud = 0; + let timeInStateQuiet = 0; + + if (state.blackoutMode) { + timeInStateQuiet = timeInState; + } else { + timeInStateLoud = timeInState; + } + + // Dynamic Thresholds + // The threshold to EXIT blackout. It should be a significant spike above the average loudness. + // It starts high and decays, making it easier to exit the longer we're in blackout. + const loudSpikeModif = 1.2; // How much louder than average a "drop" needs to be. + let loudThreshold = this.averageLoudness * loudSpikeModif; + loudThreshold = Math.max(this.averageLoudness + 0.1, loudThreshold - (timeInStateQuiet * 0.05)); + + // The threshold to ENTER blackout, based on a percentage of the song's average loudness. + let quietThreshold = this.averageLoudness * 0.75; + quietThreshold = THREE.MathUtils.clamp(quietThreshold, 0.02, 0.3); // Clamp to a reasonable range. // --- 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) { @@ -67,7 +173,16 @@ export class MusicVisualizer extends SceneFeature { } } else { state.blackoutMode = false; - this.lastBlackoutMode = false; + } + + state.music.thresholds = { loud: loudThreshold, quiet: quietThreshold }; + + if (loudness > loudThreshold) { + state.music.mode = 'Loud'; + } else if (loudness < quietThreshold) { + state.music.mode = 'Quiet'; + } else { + state.music.mode = 'Normal'; } } } diff --git a/party-stage/src/scene/root.js b/party-stage/src/scene/root.js index a7cd076..189fb6c 100644 --- a/party-stage/src/scene/root.js +++ b/party-stage/src/scene/root.js @@ -20,7 +20,7 @@ import { ProjectionScreen } from './projection-screen.js'; import { StageLasers } from './stage-lasers.js'; import { ConfigUI } from './config-ui.js'; import { StageLightBars } from './stage-light-bars.js'; -import { FPSCounter } from './fps-counter.js'; +import { DebugPanel } from './fps-counter.js'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-stage/src/scene/stage-lasers.js b/party-stage/src/scene/stage-lasers.js index f7330b7..1fa5fe5 100644 --- a/party-stage/src/scene/stage-lasers.js +++ b/party-stage/src/scene/stage-lasers.js @@ -132,7 +132,7 @@ export class StageLasers extends SceneFeature { if (this.activationState === 'IDLE') { // Wait for song to pick up before first activation - if (time > this.initialSilenceSeconds && loudness > this.averageLoudness + 0.1) { + if (time > this.initialSilenceSeconds && state.music.isLoudEnough && loudness > this.averageLoudness + 0.1) { this.activationState = 'WARMUP'; this.stateTimer = 1.0; // Warmup duration diff --git a/party-stage/src/scene/stage-torches.js b/party-stage/src/scene/stage-torches.js index d75e568..2c6a9aa 100644 --- a/party-stage/src/scene/stage-torches.js +++ b/party-stage/src/scene/stage-torches.js @@ -111,7 +111,7 @@ export class StageTorches extends SceneFeature { if (!enabled) return; - if (!state.partyStarted) { + if (!state.partyStarted && state.music.isLoudEnough) { this.torches.forEach(torch => { if (torch.light.visible) torch.light.visible = false; if (torch.particles.visible) torch.particles.visible = false; diff --git a/party-stage/src/state.js b/party-stage/src/state.js index 686c983..4b2eb1f 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -15,7 +15,8 @@ export function initState() { lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors guestCount: 150, blackout: false, - djHat: 'None' // 'None', 'Santa', 'Top Hat' + djHat: 'None', // 'None', 'Santa', 'Top Hat' + debugPanelEnabled: false }; try { const saved = localStorage.getItem('partyConfig');