Compare commits
13 Commits
8b2d1aed53
...
3c93956ff8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c93956ff8 | ||
|
|
db682996f0 | ||
|
|
f79215cc15 | ||
|
|
7e55f98da9 | ||
|
|
767460e08c | ||
|
|
31debfe151 | ||
|
|
ee6ae3a688 | ||
|
|
b2166fe26a | ||
|
|
997986917c | ||
|
|
cae867fd9f | ||
|
|
7c18a3db11 | ||
|
|
4cd869791f | ||
|
|
72a2fe7de2 |
@ -103,6 +103,15 @@ 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');
|
||||
|
||||
// Torches Toggle
|
||||
createToggle('Stage Torches', 'torchesEnabled');
|
||||
|
||||
@ -475,7 +484,9 @@ export class ConfigUI extends SceneFeature {
|
||||
lightBarsEnabled: true,
|
||||
laserColorMode: 'RUNNING',
|
||||
guestCount: 150,
|
||||
djHat: 'None'
|
||||
blackout: true,
|
||||
djHat: 'None',
|
||||
debugPanelEnabled: false
|
||||
};
|
||||
for (const key in defaults) {
|
||||
state.config[key] = defaults[key];
|
||||
|
||||
475
party-stage/src/scene/fps-counter.js
Normal file
475
party-stage/src/scene/fps-counter.js
Normal file
@ -0,0 +1,475 @@
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
import { state } from '../state.js';
|
||||
|
||||
export class DebugPanel extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
this.frames = 0;
|
||||
this.timeAccumulator = 0;
|
||||
this.fpsElement = null;
|
||||
this.textElement = null;
|
||||
this.canvas = null;
|
||||
this.ctx = null;
|
||||
this.history = [];
|
||||
this.memTextElement = null;
|
||||
this.memCanvas = null;
|
||||
this.memCtx = null;
|
||||
this.memHistory = [];
|
||||
this.musicTextElement = null;
|
||||
this.musicCanvas = null;
|
||||
this.musicCtx = null;
|
||||
this.spectrumTextElement = null;
|
||||
this.spectrumCanvas = null;
|
||||
this.spectrumCtx = 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, {
|
||||
position: 'absolute',
|
||||
top: '0',
|
||||
right: '0',
|
||||
zIndex: '10000',
|
||||
padding: '5px',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
pointerEvents: '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'
|
||||
});
|
||||
|
||||
// Text Display
|
||||
this.textElement = document.createElement('div');
|
||||
Object.assign(this.textElement.style, {
|
||||
color: '#00ff00',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2px'
|
||||
});
|
||||
this.textElement.innerText = 'FPS: --';
|
||||
this.fpsElement.appendChild(this.textElement);
|
||||
|
||||
// Graph Canvas
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.width = graphWidth;
|
||||
this.canvas.height = 40;
|
||||
Object.assign(this.canvas.style, {
|
||||
width: graphWidth + 'px',
|
||||
height: '40px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
});
|
||||
this.ctx = this.canvas.getContext('2d');
|
||||
this.fpsElement.appendChild(this.canvas);
|
||||
|
||||
// Memory Text Display
|
||||
this.memTextElement = document.createElement('div');
|
||||
Object.assign(this.memTextElement.style, {
|
||||
color: '#00ffff',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2px',
|
||||
marginTop: '5px'
|
||||
});
|
||||
this.memTextElement.innerText = 'MEM: --';
|
||||
this.fpsElement.appendChild(this.memTextElement);
|
||||
|
||||
// Memory Graph Canvas
|
||||
this.memCanvas = document.createElement('canvas');
|
||||
this.memCanvas.width = graphWidth;
|
||||
this.memCanvas.height = 40;
|
||||
Object.assign(this.memCanvas.style, {
|
||||
width: graphWidth + 'px',
|
||||
height: '40px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
});
|
||||
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);
|
||||
|
||||
// Spectrum Text Display
|
||||
this.spectrumTextElement = document.createElement('div');
|
||||
Object.assign(this.spectrumTextElement.style, {
|
||||
color: '#ffff00',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '16px',
|
||||
fontWeight: 'bold',
|
||||
marginBottom: '2px',
|
||||
marginTop: '5px'
|
||||
});
|
||||
this.spectrumTextElement.innerText = 'SPECTRUM';
|
||||
this.fpsElement.appendChild(this.spectrumTextElement);
|
||||
|
||||
// Spectrum Graph Canvas
|
||||
this.spectrumCanvas = document.createElement('canvas');
|
||||
this.spectrumCanvas.width = graphWidth;
|
||||
this.spectrumCanvas.height = 60;
|
||||
Object.assign(this.spectrumCanvas.style, {
|
||||
width: graphWidth + 'px',
|
||||
height: '60px',
|
||||
background: '#222',
|
||||
border: '1px solid #444'
|
||||
});
|
||||
this.spectrumCtx = this.spectrumCanvas.getContext('2d');
|
||||
this.fpsElement.appendChild(this.spectrumCanvas);
|
||||
|
||||
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, la:0, low:0, lla:0, high:0, b:0, lt:0, qt:0, m:0.5, bo: false, al: 0, bt: 0});
|
||||
|
||||
// Initialize visibility based on config
|
||||
if (state.config) {
|
||||
this.setVisibility(state.config.debugPanelEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
this.frames++;
|
||||
this.timeAccumulator += deltaTime;
|
||||
|
||||
const segment = 0.25;
|
||||
if (this.timeAccumulator >= segment) {
|
||||
const fps = Math.round(this.frames / this.timeAccumulator);
|
||||
if (this.textElement) this.textElement.innerText = `FPS: ${fps}`;
|
||||
|
||||
// Update Graph
|
||||
this.history.push(fps);
|
||||
if (this.history.length > this.canvas.width) {
|
||||
this.history.shift();
|
||||
}
|
||||
this.drawGraph();
|
||||
|
||||
// Update Memory
|
||||
if (performance && performance.memory) {
|
||||
const mem = performance.memory.usedJSHeapSize / 1048576; // MB
|
||||
if (this.memTextElement) this.memTextElement.innerText = `MEM: ${Math.round(mem)} MB`;
|
||||
|
||||
this.memHistory.push(mem);
|
||||
if (this.memHistory.length > this.memCanvas.width) {
|
||||
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.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 lowsAvg = state.music.loudnessLowsAverage || 0;
|
||||
const avgLoudness = state.music.averageLoudness || 0;
|
||||
const highs = state.music.loudnessHighs || 0;
|
||||
const beat = state.music.beatIntensity || 0;
|
||||
const beatThreshold = state.music.beatThreshold || 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, lla: lowsAvg, high: highs, b: beat, lt: thresholds.loud, qt: thresholds.quiet, m: modeVal, bo: state.blackoutMode, al: avgLoudness, bt: beatThreshold });
|
||||
if (this.musicHistory.length > this.musicCanvas.width) {
|
||||
this.musicHistory.shift();
|
||||
}
|
||||
this.drawMusicGraph();
|
||||
this.drawSpectrumGraph();
|
||||
|
||||
if (this.musicTextElement) {
|
||||
this.musicTextElement.innerText = `Volume: ${loudness.toFixed(2)}\nBPM: ${state.music.bpm}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setVisibility(visible) {
|
||||
if (!this.fpsElement) return;
|
||||
this.fpsElement.style.display = visible ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
drawGraph() {
|
||||
if (!this.ctx) return;
|
||||
const ctx = this.ctx;
|
||||
const w = this.canvas.width;
|
||||
const h = this.canvas.height;
|
||||
const maxFps = 70;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw 60 FPS reference line
|
||||
const y60 = h - (60 / maxFps) * h;
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#555';
|
||||
ctx.moveTo(0, y60);
|
||||
ctx.lineTo(w, y60);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw Graph
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
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() {
|
||||
if (!this.memCtx) return;
|
||||
const ctx = this.memCtx;
|
||||
const w = this.memCanvas.width;
|
||||
const h = this.memCanvas.height;
|
||||
// Auto-scale max memory
|
||||
const maxMem = Math.max(...this.memHistory, 100) * 1.1;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
// Draw Graph
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#00ffff';
|
||||
ctx.lineWidth = 1.5;
|
||||
|
||||
for (let i = 0; i < this.memHistory.length; i++) {
|
||||
const val = this.memHistory[i];
|
||||
const x = i;
|
||||
const y = h - (val / maxMem) * h;
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
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 Lows Avg (Darker Brown)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#8B4513'; // Saddle Brown
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].lla !== undefined ? this.musicHistory[i].lla : 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 Average Loudness (Pink)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#FF69B4'; // HotPink
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].al !== undefined ? this.musicHistory[i].al : 0;
|
||||
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 Beat Threshold (Cyan)
|
||||
ctx.beginPath();
|
||||
ctx.strokeStyle = '#00ffff';
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < this.musicHistory.length; i++) {
|
||||
const val = this.musicHistory[i].bt !== undefined ? this.musicHistory[i].bt : 0;
|
||||
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
|
||||
}
|
||||
|
||||
drawSpectrumGraph() {
|
||||
if (!this.spectrumCtx || !state.music || !state.music.frequencyData) return;
|
||||
const ctx = this.spectrumCtx;
|
||||
const w = this.spectrumCanvas.width;
|
||||
const h = this.spectrumCanvas.height;
|
||||
const data = state.music.frequencyData;
|
||||
const bufferLength = data.length/2;
|
||||
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const barWidth = w / bufferLength;
|
||||
let x = 0;
|
||||
|
||||
for(let i = 0; i < bufferLength; i++) {
|
||||
const val = (data[i] / 255);
|
||||
const barHeight = val * h;
|
||||
const hue = (i / bufferLength) * 260; // Red to Purple
|
||||
ctx.fillStyle = `hsl(${hue}, 100%, 50%)`;
|
||||
ctx.fillRect(x, h - barHeight, barWidth + 1, barHeight);
|
||||
x += barWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
new DebugPanel();
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -11,15 +11,18 @@ 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');
|
||||
if (!state.music.player) {
|
||||
state.music.player = document.createElement('audio');
|
||||
state.music.player.id = 'audioPlayer';
|
||||
document.body.appendChild(state.music.player);
|
||||
}
|
||||
state.music.loudness = 0;
|
||||
state.music.isLoudEnough = false;
|
||||
|
||||
state.music.loudnessAverage = 0;
|
||||
const loadButton = document.getElementById('loadMusicButton');
|
||||
const fileInput = document.getElementById('musicFileInput');
|
||||
|
||||
@ -53,11 +56,12 @@ export class MusicPlayer extends SceneFeature {
|
||||
if (!this.audioContext) {
|
||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
this.analyser = this.audioContext.createAnalyser();
|
||||
this.analyser.fftSize = 128; // Lower resolution is fine for loudness
|
||||
this.analyser.fftSize = 1024; // Higher resolution for better bass detection
|
||||
this.source = this.audioContext.createMediaElementSource(state.music.player);
|
||||
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
|
||||
@ -76,6 +80,10 @@ export class MusicPlayer extends SceneFeature {
|
||||
}
|
||||
|
||||
startSequence() {
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
const uiContainer = document.getElementById('ui-container');
|
||||
const configUILeft = document.getElementById('config-ui-left');
|
||||
const configUIRight = document.getElementById('config-ui-right');
|
||||
@ -99,9 +107,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 +130,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();
|
||||
new MusicPlayer();
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
@ -5,35 +6,192 @@ import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
export class MusicVisualizer extends SceneFeature {
|
||||
constructor() {
|
||||
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: 1,
|
||||
loudness: 0,
|
||||
loudnessLows: 0,
|
||||
loudnessHighs: 0,
|
||||
loudnessAverage: 0,
|
||||
loudnessLowsAverage: 0,
|
||||
frequencyData: null,
|
||||
beatThreshold: 0
|
||||
};
|
||||
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.floor(Math.max(2, dataArray.length * 0.2)); // Focus on bottom 10% for bass
|
||||
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 X seconds ---
|
||||
const loudnessHistorySeconds = 0.5;
|
||||
this.loudnessHistory.push(state.music.loudness);
|
||||
if (this.loudnessHistory.length > loudnessHistorySeconds*60) this.loudnessHistory.shift();
|
||||
|
||||
this.loudnessLowsHistory.push(state.music.loudnessLows);
|
||||
if (this.loudnessLowsHistory.length > loudnessHistorySeconds*60) 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.loudnessLows || 0;
|
||||
const loudnessAvg = state.music.loudnessLowsAverage || 0;
|
||||
const songTime = state.music.player ? state.music.player.currentTime : 0;
|
||||
this.averageLoudness = THREE.MathUtils.lerp(this.averageLoudness, loudnessAvg, deltaTime * (songTime < 5 ? 0.5 : 0.05));
|
||||
state.music.averageLoudness = this.averageLoudness;
|
||||
|
||||
// --- 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 ---
|
||||
// Use Lows (Bass) for clearer beat detection
|
||||
const beatSignal = state.music.loudnessLows || 0;
|
||||
const beatAvg = state.music.loudnessLowsAverage || 0;
|
||||
|
||||
// --- 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;
|
||||
// Decay threshold based on time
|
||||
this.beatThreshold -= deltaTime * 0.5;
|
||||
const thresholdFloor = Math.max(0.15, beatAvg * 1.1);
|
||||
if (this.beatThreshold < thresholdFloor) this.beatThreshold = thresholdFloor;
|
||||
|
||||
state.music.beatThreshold = this.beatThreshold;
|
||||
|
||||
const now = time;
|
||||
const lastBeatInterval = now - this.lastBeatTime;
|
||||
if (beatSignal > this.beatThreshold) {
|
||||
if (lastBeatInterval > 0.25) { // Min interval (~240 BPM)
|
||||
this.lastBeatTime = now;
|
||||
this.beatThreshold = beatSignal * 1.2; // Bump threshold
|
||||
|
||||
// Add only valid BPM range into history of beats
|
||||
if (lastBeatInterval >= 0.2 && lastBeatInterval <= 1.5) {
|
||||
this.beatIntervals.push(lastBeatInterval);
|
||||
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 ---
|
||||
const realBeatFactor = lastBeatInterval < 1.0 ? 1.0 : ((lastBeatInterval > 5) ? 0.0 : (5 - lastBeatInterval)/5);
|
||||
state.music.beatIntensity = Math.pow(1.0 - this.beatPhase, 2) * realBeatFactor;
|
||||
|
||||
// --- 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
|
||||
// Enter blackout if loudness falls below 80% of average
|
||||
let quietThreshold = this.averageLoudness * 0.8;
|
||||
|
||||
// Exit blackout if loudness returns to normal (average) or above
|
||||
let loudThreshold = this.averageLoudness;
|
||||
|
||||
// --- Auto-Blackout Logic ---
|
||||
// If blackout is active, monitor for loud events (The Drop) to disable it.
|
||||
if (state.config.blackout) {
|
||||
if (state.blackoutMode) {
|
||||
if (loudness >= loudThreshold) {
|
||||
state.blackoutMode = false;
|
||||
}
|
||||
} else {
|
||||
if (loudness < quietThreshold) {
|
||||
state.blackoutMode = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
state.blackoutMode = 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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -152,6 +152,21 @@ export class PartyGuests extends SceneFeature {
|
||||
|
||||
const time = state.clock.getElapsedTime();
|
||||
const minDistance = 0.8; // Minimum distance to maintain
|
||||
const minDistSq = minDistance * minDistance;
|
||||
|
||||
// --- Spatial Grid Optimization ---
|
||||
const cellSize = 2.0;
|
||||
const grid = {};
|
||||
|
||||
// Populate grid
|
||||
for (let i = 0; i < this.guests.length; i++) {
|
||||
const g = this.guests[i];
|
||||
const cx = Math.floor(g.mesh.position.x / cellSize);
|
||||
const cz = Math.floor(g.mesh.position.z / cellSize);
|
||||
const key = cx + ':' + cz;
|
||||
if (!grid[key]) grid[key] = [];
|
||||
grid[key].push(i);
|
||||
}
|
||||
|
||||
this.guests.forEach((guestObj, i) => {
|
||||
const { mesh, leftArm, rightArm } = guestObj;
|
||||
@ -160,19 +175,31 @@ export class PartyGuests extends SceneFeature {
|
||||
let separationX = 0;
|
||||
let separationZ = 0;
|
||||
|
||||
for (let j = 0; j < this.guests.length; j++) {
|
||||
if (i === j) continue;
|
||||
const otherMesh = this.guests[j].mesh;
|
||||
|
||||
const dx = mesh.position.x - otherMesh.position.x;
|
||||
const dz = mesh.position.z - otherMesh.position.z;
|
||||
const distSq = dx*dx + dz*dz;
|
||||
|
||||
if (distSq < minDistance * minDistance && distSq > 0.0001) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
const force = (minDistance - dist) / minDistance;
|
||||
separationX += (dx / dist) * force;
|
||||
separationZ += (dz / dist) * force;
|
||||
const cx = Math.floor(mesh.position.x / cellSize);
|
||||
const cz = Math.floor(mesh.position.z / cellSize);
|
||||
|
||||
for (let ox = -1; ox <= 1; ox++) {
|
||||
for (let oz = -1; oz <= 1; oz++) {
|
||||
const key = (cx + ox) + ':' + (cz + oz);
|
||||
const cell = grid[key];
|
||||
if (!cell) continue;
|
||||
|
||||
for (let k = 0; k < cell.length; k++) {
|
||||
const j = cell[k];
|
||||
if (i === j) continue;
|
||||
|
||||
const otherMesh = this.guests[j].mesh;
|
||||
const dx = mesh.position.x - otherMesh.position.x;
|
||||
const dz = mesh.position.z - otherMesh.position.z;
|
||||
const distSq = dx*dx + dz*dz;
|
||||
|
||||
if (distSq < minDistSq && distSq > 0.0001) {
|
||||
const dist = Math.sqrt(distSq);
|
||||
const force = (minDistance - dist) / minDistance;
|
||||
separationX += (dx / dist) * force;
|
||||
separationZ += (dz / dist) * force;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -130,6 +130,18 @@ export class ProjectionScreen extends SceneFeature {
|
||||
this.isVisualizerActive = false;
|
||||
this.screens = [];
|
||||
this.colorBuffer = new Float32Array(16 * 3);
|
||||
|
||||
this.standbyState = {
|
||||
active: false,
|
||||
scrollX: 0,
|
||||
text: '',
|
||||
header: '',
|
||||
subtext: ''
|
||||
};
|
||||
this.standbyCanvas = null;
|
||||
this.standbyCtx = null;
|
||||
this.standbyTexture = null;
|
||||
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
@ -232,6 +244,10 @@ export class ProjectionScreen extends SceneFeature {
|
||||
|
||||
update(deltaTime) {
|
||||
updateScreenEffect();
|
||||
|
||||
if (this.standbyState.active) {
|
||||
this.renderStandbyFrame();
|
||||
}
|
||||
|
||||
// Wobble Logic
|
||||
const time = state.clock.getElapsedTime();
|
||||
@ -257,6 +273,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 +297,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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -329,68 +353,129 @@ export class ProjectionScreen extends SceneFeature {
|
||||
this.isVisualizerActive = false;
|
||||
turnTvScreenOff();
|
||||
}
|
||||
|
||||
activateStandby() {
|
||||
this.isVisualizerActive = false;
|
||||
|
||||
if (!this.standbyCanvas) {
|
||||
this.standbyCanvas = document.createElement('canvas');
|
||||
this.standbyCanvas.width = 1024;
|
||||
this.standbyCanvas.height = 576;
|
||||
this.standbyCtx = this.standbyCanvas.getContext('2d');
|
||||
this.standbyTexture = new THREE.CanvasTexture(this.standbyCanvas);
|
||||
this.standbyTexture.minFilter = THREE.LinearFilter;
|
||||
this.standbyTexture.magFilter = THREE.LinearFilter;
|
||||
}
|
||||
|
||||
const songTitle = (state.music && state.music.songTitle) ? state.music.songTitle.toUpperCase() : "";
|
||||
|
||||
this.standbyState = {
|
||||
active: true,
|
||||
scrollX: 0,
|
||||
text: songTitle,
|
||||
header: "PLEASE STAND BY",
|
||||
subtext: "WAITING FOR PARTY START"
|
||||
};
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: this.standbyTexture,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
this.applyMaterialToAll(material);
|
||||
this.setAllVisible(true);
|
||||
state.screenLight.intensity = 0.5;
|
||||
|
||||
// Measure text to set initial scroll
|
||||
this.standbyCtx.font = 'bold 80px monospace';
|
||||
const textWidth = this.standbyCtx.measureText(this.standbyState.text).width;
|
||||
if (textWidth > this.standbyCanvas.width * 0.9) {
|
||||
this.standbyState.scrollX = this.standbyCanvas.width;
|
||||
}
|
||||
|
||||
this.renderStandbyFrame();
|
||||
}
|
||||
|
||||
renderStandbyFrame() {
|
||||
if (!this.standbyState.active || !this.standbyCtx) return;
|
||||
|
||||
const ctx = this.standbyCtx;
|
||||
const width = this.standbyCanvas.width;
|
||||
const height = this.standbyCanvas.height;
|
||||
|
||||
// Draw Background
|
||||
const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff'];
|
||||
const barWidth = width / colors.length;
|
||||
colors.forEach((color, i) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * barWidth, 0, barWidth, height);
|
||||
});
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
// Header
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = 'bold 40px monospace';
|
||||
ctx.fillText(this.standbyState.header, width / 2, height * 0.2);
|
||||
|
||||
// Song Title
|
||||
const text = this.standbyState.text;
|
||||
ctx.font = 'bold 80px monospace';
|
||||
const textMetrics = ctx.measureText(text);
|
||||
const textWidth = textMetrics.width;
|
||||
const centerY = height * 0.5;
|
||||
|
||||
if (textWidth > width * 0.9) {
|
||||
// Scroll
|
||||
this.standbyState.scrollX -= 3; // Speed
|
||||
if (this.standbyState.scrollX < -textWidth) {
|
||||
this.standbyState.scrollX = width;
|
||||
}
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(text, this.standbyState.scrollX, centerY);
|
||||
} else {
|
||||
// Center
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(text, width / 2, centerY);
|
||||
}
|
||||
|
||||
// Subtext
|
||||
ctx.font = '30px monospace';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(this.standbyState.subtext, width / 2, height * 0.8);
|
||||
|
||||
if (this.standbyTexture) this.standbyTexture.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Exported Control Functions ---
|
||||
|
||||
export function showStandbyScreen() {
|
||||
if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false;
|
||||
|
||||
let texture;
|
||||
if (projectionScreenInstance) {
|
||||
projectionScreenInstance.isVisualizerActive = false;
|
||||
projectionScreenInstance.standbyState.active = false;
|
||||
}
|
||||
|
||||
if (state.posterImage) {
|
||||
texture = new THREE.TextureLoader().load(state.posterImage);
|
||||
} else {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 576;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Draw Color Bars
|
||||
const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff'];
|
||||
const barWidth = canvas.width / colors.length;
|
||||
colors.forEach((color, i) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * barWidth, 0, barWidth, canvas.height);
|
||||
const texture = new THREE.TextureLoader().load(state.posterImage);
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
// Semi-transparent overlay
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Text settings
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
|
||||
// Song Title
|
||||
let text = "PLEASE STAND BY";
|
||||
if (state.music && state.music.songTitle) {
|
||||
text = state.music.songTitle.toUpperCase();
|
||||
if (projectionScreenInstance) {
|
||||
projectionScreenInstance.applyMaterialToAll(material);
|
||||
projectionScreenInstance.setAllVisible(true);
|
||||
}
|
||||
|
||||
ctx.font = 'bold 60px monospace';
|
||||
if (ctx.measureText(text).width > canvas.width * 0.9) {
|
||||
ctx.font = 'bold 40px monospace';
|
||||
}
|
||||
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - 20);
|
||||
|
||||
// Subtext
|
||||
ctx.font = '30px monospace';
|
||||
ctx.fillStyle = '#cccccc';
|
||||
ctx.fillText("WAITING FOR PARTY START", canvas.width / 2, canvas.height / 2 + 50);
|
||||
|
||||
texture = new THREE.CanvasTexture(canvas);
|
||||
state.screenLight.intensity = 0.5;
|
||||
return;
|
||||
}
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
if (projectionScreenInstance) projectionScreenInstance.applyMaterialToAll(material);
|
||||
if (projectionScreenInstance) projectionScreenInstance.setAllVisible(true);
|
||||
|
||||
state.screenLight.intensity = 0.5;
|
||||
if (projectionScreenInstance) {
|
||||
projectionScreenInstance.activateStandby();
|
||||
}
|
||||
}
|
||||
|
||||
export function turnTvScreenOn() {
|
||||
@ -460,13 +545,24 @@ export function updateScreenEffect() {
|
||||
const material = s.mesh.material;
|
||||
if (!material || !material.uniforms) return;
|
||||
|
||||
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_time) {
|
||||
material.uniforms.u_time.value = state.clock.getElapsedTime();
|
||||
}
|
||||
if (material.uniforms.u_effect_type) {
|
||||
material.uniforms.u_effect_type.value = state.screenEffect.type;
|
||||
}
|
||||
if (material.uniforms.u_effect_strength) {
|
||||
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;
|
||||
material.uniforms.u_effect_strength.value = 0.0;
|
||||
if (material.uniforms.u_effect_type) material.uniforms.u_effect_type.value = 0.0;
|
||||
if (material.uniforms.u_effect_strength) material.uniforms.u_effect_strength.value = 0.0;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -20,6 +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 { DebugPanel } from './fps-counter.js';
|
||||
// Scene Features ^^^
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -124,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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -104,23 +104,17 @@ export class StageTorches extends SceneFeature {
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
const enabled = state.config.torchesEnabled;
|
||||
const configEnabled = state.config.torchesEnabled;
|
||||
this.torches.forEach(torch => {
|
||||
if (torch.group.visible !== enabled) torch.group.visible = enabled;
|
||||
if (torch.group.visible !== configEnabled) torch.group.visible = configEnabled;
|
||||
});
|
||||
|
||||
if (!enabled) return;
|
||||
if (!configEnabled) return;
|
||||
|
||||
if (!state.partyStarted) {
|
||||
this.torches.forEach(torch => {
|
||||
if (torch.light.visible) torch.light.visible = false;
|
||||
if (torch.particles.visible) torch.particles.visible = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
const fireActive = !state.blackoutMode && !(!state.partyStarted && state.music.isLoudEnough);
|
||||
|
||||
this.torches.forEach(torch => {
|
||||
if (!torch.light.visible) torch.light.visible = true;
|
||||
if (torch.light.visible !== fireActive) torch.light.visible = fireActive;
|
||||
if (!torch.particles.visible) torch.particles.visible = true;
|
||||
|
||||
let measurePulse = 0;
|
||||
@ -134,26 +128,35 @@ export class StageTorches extends SceneFeature {
|
||||
// --- Animate Particles ---
|
||||
const positions = torch.particles.geometry.attributes.position.array;
|
||||
let averageY = 0;
|
||||
let activeParticleCount = 0;
|
||||
for (let i = 0; i < torch.particleData.length; i++) {
|
||||
const data = torch.particleData[i];
|
||||
data.life -= deltaTime;
|
||||
const yVelocity = data.velocity.y;
|
||||
if (data.life <= 0 || positions[i * 3 + 1] < 0) {
|
||||
// Reset particle
|
||||
positions[i * 3] = (Math.random() - 0.5) * 0.2;
|
||||
positions[i * 3 + 1] = 1;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2;
|
||||
data.life = Math.random() * 1.0;
|
||||
data.velocity.y = Math.random() * 1.2 + measurePulse;
|
||||
if (fireActive) {
|
||||
// Reset particle
|
||||
positions[i * 3] = (Math.random() - 0.5) * 0.2;
|
||||
positions[i * 3 + 1] = 1;
|
||||
positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2;
|
||||
data.life = Math.random() * 1.0;
|
||||
data.velocity.y = Math.random() * 1.2 + measurePulse;
|
||||
} else {
|
||||
// Stop producing: move out of view
|
||||
positions[i * 3 + 1] = -100;
|
||||
}
|
||||
} else {
|
||||
// Update position
|
||||
positions[i * 3] += data.velocity.x * deltaTime;
|
||||
positions[i * 3 + 1] += yVelocity * deltaTime;
|
||||
positions[i * 3 + 2] += data.velocity.z * deltaTime;
|
||||
}
|
||||
averageY += positions[i * 3 + 1];
|
||||
if (positions[i * 3 + 1] > -50) {
|
||||
averageY += positions[i * 3 + 1];
|
||||
activeParticleCount++;
|
||||
}
|
||||
}
|
||||
averageY = averageY / positions.length;
|
||||
averageY = activeParticleCount > 0 ? averageY / activeParticleCount : 0;
|
||||
torch.particles.geometry.attributes.position.needsUpdate = true;
|
||||
|
||||
// --- Flicker Light ---
|
||||
|
||||
@ -14,7 +14,9 @@ export function initState() {
|
||||
laserColorMode: 'RUNNING', // 'SINGLE', 'RANDOM', 'RUNNING', 'ANY'
|
||||
lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors
|
||||
guestCount: 150,
|
||||
djHat: 'None' // 'None', 'Santa', 'Top Hat'
|
||||
blackout: false,
|
||||
djHat: 'None', // 'None', 'Santa', 'Top Hat'
|
||||
debugPanelEnabled: false
|
||||
};
|
||||
try {
|
||||
const saved = localStorage.getItem('partyConfig');
|
||||
@ -61,6 +63,7 @@ export function initState() {
|
||||
debugLight: false, // Turn on light helpers
|
||||
debugCamera: false, // Turn on camera helpers
|
||||
partyStarted: false,
|
||||
blackoutMode: false,
|
||||
|
||||
// Feature Configuration
|
||||
config: config,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user