From 8bf82f5f8a5828582d3412bf7e8930ce79fb3ac3 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 4 Jan 2026 07:26:27 +0000 Subject: [PATCH] Feature: Stage colors configurable and used everywhere --- party-stage/src/scene/config-ui.js | 60 ++++++++++++- party-stage/src/scene/music-console.js | 19 ++-- party-stage/src/scene/projection-screen.js | 42 ++++++++- party-stage/src/scene/stage-lasers.js | 45 ++++++++-- party-stage/src/scene/stage-light-bars.js | 100 ++++++++++++++++----- party-stage/src/state.js | 2 + 6 files changed, 230 insertions(+), 38 deletions(-) diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index c539b1f..96b8fca 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -1,4 +1,5 @@ import { state } from '../state.js'; +import * as THREE from 'three'; import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; import { MediaStorage } from '../core/media-storage.js'; @@ -108,6 +109,33 @@ export class ConfigUI extends SceneFeature { // Lasers Toggle createToggle('Lasers', 'lasersEnabled'); + // Laser Color Mode + const laserModeRow = document.createElement('div'); + laserModeRow.style.display = 'flex'; + laserModeRow.style.alignItems = 'center'; + laserModeRow.style.justifyContent = 'space-between'; + + const laserModeLabel = document.createElement('label'); + laserModeLabel.innerText = 'Laser Colors'; + + const laserModeSelect = document.createElement('select'); + ['SINGLE', 'RANDOM', 'RUNNING', 'ANY'].forEach(mode => { + const opt = document.createElement('option'); + opt.value = mode; + opt.innerText = mode.charAt(0) + mode.slice(1).toLowerCase(); + if (mode === state.config.laserColorMode) opt.selected = true; + laserModeSelect.appendChild(opt); + }); + laserModeSelect.onchange = (e) => { + state.config.laserColorMode = e.target.value; + saveConfig(); + }; + this.laserModeSelect = laserModeSelect; + + laserModeRow.appendChild(laserModeLabel); + laserModeRow.appendChild(laserModeSelect); + rightContainer.appendChild(laserModeRow); + // Side Screens Toggle createToggle('Side Screens', 'sideScreensEnabled'); @@ -123,6 +151,12 @@ export class ConfigUI extends SceneFeature { // Gameboy Toggle createToggle('Gameboy', 'gameboyEnabled'); + // Stage Light Bars Toggle + createToggle('Stage Light Bars', 'lightBarsEnabled', (enabled) => { + const lightBars = sceneFeatureManager.features.find(f => f.constructor.name === 'StageLightBars'); + if (lightBars) lightBars.setVisibility(enabled); + }); + // --- Light Bar Colors --- const colorContainer = document.createElement('div'); Object.assign(colorContainer.style, { @@ -135,7 +169,7 @@ export class ConfigUI extends SceneFeature { }); const colorLabel = document.createElement('label'); - colorLabel.innerText = 'Stage Light Bars'; + colorLabel.innerText = 'Stage colors'; colorContainer.appendChild(colorLabel); const colorControls = document.createElement('div'); @@ -155,7 +189,7 @@ export class ConfigUI extends SceneFeature { addColorBtn.onclick = () => { state.config.lightBarColors.push(colorPicker.value); saveConfig(); - updateColorList(); + this.updateColorList(); const lightBars = sceneFeatureManager.features.find(f => f.constructor.name === 'StageLightBars'); if (lightBars) lightBars.refreshColors(); }; @@ -166,7 +200,23 @@ export class ConfigUI extends SceneFeature { clearColorsBtn.onclick = () => { state.config.lightBarColors = []; saveConfig(); - updateColorList(); + this.updateColorList(); + const lightBars = sceneFeatureManager.features.find(f => f.constructor.name === 'StageLightBars'); + if (lightBars) lightBars.refreshColors(); + }; + + const randomizeColorsBtn = document.createElement('button'); + randomizeColorsBtn.innerText = 'Randomize'; + randomizeColorsBtn.style.cursor = 'pointer'; + randomizeColorsBtn.onclick = () => { + const count = 2 + Math.floor(Math.random() * 5); // 2 to 6 + const newColors = []; + for(let i=0; i f.constructor.name === 'StageLightBars'); if (lightBars) lightBars.refreshColors(); }; @@ -174,6 +224,7 @@ export class ConfigUI extends SceneFeature { colorControls.appendChild(colorPicker); colorControls.appendChild(addColorBtn); colorControls.appendChild(clearColorsBtn); + colorControls.appendChild(randomizeColorsBtn); colorContainer.appendChild(colorControls); const colorList = document.createElement('div'); @@ -421,6 +472,8 @@ export class ConfigUI extends SceneFeature { consoleRGBEnabled: true, consoleEnabled: true, gameboyEnabled: false, + lightBarsEnabled: true, + laserColorMode: 'RUNNING', guestCount: 150, djHat: 'None' }; @@ -432,6 +485,7 @@ export class ConfigUI extends SceneFeature { } } if (this.guestInput) this.guestInput.value = defaults.guestCount; + if (this.laserModeSelect) this.laserModeSelect.value = defaults.laserColorMode; if (this.hatSelect) this.hatSelect.value = defaults.djHat; this.updateStatus(); }; diff --git a/party-stage/src/scene/music-console.js b/party-stage/src/scene/music-console.js index 207f2f9..4f201cb 100644 --- a/party-stage/src/scene/music-console.js +++ b/party-stage/src/scene/music-console.js @@ -228,11 +228,20 @@ export class MusicConsole extends SceneFeature { const intensity = (wave1 + wave2 + wave3) / 3 * 0.5 + 0.5; // Color palette shifting - const hue = (time * 0.1 + u * 0.3 + intensity * 0.2) % 1; - const sat = 0.9; - const light = intensity * (0.1 + beatIntensity * 0.9); // Pulse brightness with beat - - color.setHSL(hue, sat, light); + const palette = state.config.lightBarColors; + if (palette && palette.length > 0) { + const drive = (time * 0.1 + u * 0.3 + intensity * 0.2); + const paletteIndex = Math.floor(((drive % 1) + 1) % 1 * palette.length); + color.set(palette[paletteIndex]); + + const light = intensity * (0.1 + beatIntensity * 0.9); + color.multiplyScalar(light); + } else { + const hue = (time * 0.1 + u * 0.3 + intensity * 0.2) % 1; + const sat = 0.9; + const light = intensity * (0.1 + beatIntensity * 0.9); // Pulse brightness with beat + color.setHSL(hue, sat, light); + } this.frontLedMesh.setColorAt(idx, color); idx++; } diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 20aa25d..5318632 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -64,6 +64,8 @@ uniform float u_time; uniform float u_beat; uniform float u_opacity; uniform vec2 u_resolution; +uniform vec3 u_colors[16]; +uniform int u_colorCount; varying vec2 vUv; vec3 hsv2rgb(vec3 c) { @@ -98,7 +100,24 @@ void main() { float hue = fract(u_time * 0.1 + d * 0.2); float val = 0.5 + 0.5 * sin(wave + beatWave); - gl_FragColor = vec4(hsv2rgb(vec3(hue, 0.8, val)), mask); + + vec3 finalColor; + if (u_colorCount > 0) { + float indexFloat = hue * float(u_colorCount); + int index = int(mod(indexFloat, float(u_colorCount))); + vec3 c = vec3(0.0); + for (int i = 0; i < 16; i++) { + if (i == index) { + c = u_colors[i]; + break; + } + } + finalColor = c * val; + } else { + finalColor = hsv2rgb(vec3(hue, 0.8, val)); + } + + gl_FragColor = vec4(finalColor, mask); } `; @@ -110,6 +129,7 @@ export class ProjectionScreen extends SceneFeature { projectionScreenInstance = this; this.isVisualizerActive = false; this.screens = []; + this.colorBuffer = new Float32Array(16 * 3); sceneFeatureManager.register(this); } @@ -236,11 +256,27 @@ export class ProjectionScreen extends SceneFeature { if (this.isVisualizerActive) { const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0; state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5); + + // Update color buffer + const colors = state.config.lightBarColors; + const colorCount = colors ? Math.min(colors.length, 16) : 0; + + if (colorCount > 0) { + for (let i = 0; i < colorCount; i++) { + const c = new THREE.Color(colors[i]); + this.colorBuffer[i * 3] = c.r; + this.colorBuffer[i * 3 + 1] = c.g; + this.colorBuffer[i * 3 + 2] = c.b; + } + } this.screens.forEach(s => { if (s.mesh.material && s.mesh.material.uniforms && s.mesh.material.uniforms.u_time) { s.mesh.material.uniforms.u_time.value = state.clock.getElapsedTime(); s.mesh.material.uniforms.u_beat.value = beat; + if (s.mesh.material.uniforms.u_colorCount) { + s.mesh.material.uniforms.u_colorCount.value = colorCount; + } } }); } @@ -273,7 +309,9 @@ export class ProjectionScreen extends SceneFeature { u_time: { value: 0.0 }, u_beat: { value: 0.0 }, u_opacity: { value: state.screenOpacity }, - u_resolution: { value: new THREE.Vector2(1, 1) } // Placeholder, set in applyMaterialToAll + u_resolution: { value: new THREE.Vector2(1, 1) }, // Placeholder, set in applyMaterialToAll + u_colors: { value: this.colorBuffer }, + u_colorCount: { value: 0 } }, vertexShader: screenVertexShader, fragmentShader: visualizerFragmentShader, diff --git a/party-stage/src/scene/stage-lasers.js b/party-stage/src/scene/stage-lasers.js index 0d6dbc7..605a08b 100644 --- a/party-stage/src/scene/stage-lasers.js +++ b/party-stage/src/scene/stage-lasers.js @@ -13,6 +13,7 @@ export class StageLasers extends SceneFeature { this.activationState = 'IDLE'; this.stateTimer = 0; this.initialSilenceSeconds = 10; + this.currentCycleMode = 'RUNNING'; sceneFeatureManager.register(this); } @@ -94,7 +95,8 @@ export class StageLasers extends SceneFeature { flare: flare, index: i, totalInBank: count, - bankId: position.x < 0 ? 0 : (position.x > 0 ? 1 : 2) // 0:L, 1:R, 2:C + bankId: position.x < 0 ? 0 : (position.x > 0 ? 1 : 2), // 0:L, 1:R, 2:C + staticColorIndex: Math.floor(Math.random() * 100) }); } } @@ -125,6 +127,10 @@ export class StageLasers extends SceneFeature { if (time > this.initialSilenceSeconds && loudness > this.averageLoudness + 0.1) { this.activationState = 'WARMUP'; this.stateTimer = 1.0; // Warmup duration + + // Pick a random mode for this activation cycle (used if config is 'ANY') + const modes = ['SINGLE', 'RANDOM', 'RUNNING']; + this.currentCycleMode = modes[Math.floor(Math.random() * modes.length)]; } } else if (this.activationState === 'WARMUP') { isActive = true; @@ -172,8 +178,7 @@ export class StageLasers extends SceneFeature { // --- Color & Intensity --- const beat = state.music ? state.music.beatIntensity : 0; - const hue = (time * 0.1) % 1; - const color = new THREE.Color().setHSL(hue, 1.0, 0.5); + let intensity = 0.2 + beat * 0.6; // Strobe Mode: Flash rapidly when beat intensity is high @@ -205,9 +210,39 @@ export class StageLasers extends SceneFeature { flareScale = fade; } - l.mesh.material.color.copy(color); + let colorHex = 0x00ff00; // Default green + const palette = state.config.lightBarColors; + + let mode = state.config.laserColorMode; + if (mode === 'ANY') { + mode = this.currentCycleMode; + } + + if (palette && palette.length > 0) { + if (mode === 'SINGLE') { + colorHex = palette[0]; + } else if (mode === 'RANDOM') { + colorHex = palette[l.staticColorIndex % palette.length]; + } else if (mode === 'RUNNING') { + const offset = Math.floor(time * 4); // Speed of running + const idx = (l.index + offset) % palette.length; + colorHex = palette[idx]; + } else { + // Fallback to running if unknown + const idx = l.index % palette.length; + colorHex = palette[idx]; + } + } 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(); + } + + l.mesh.material.color.set(colorHex); + l.flare.material.color.set(colorHex); + l.mesh.material.opacity = currentIntensity; - l.flare.material.color.copy(color); l.flare.scale.setScalar(flareScale); // --- Movement Calculation --- diff --git a/party-stage/src/scene/stage-light-bars.js b/party-stage/src/scene/stage-light-bars.js index e957af9..dd1e350 100644 --- a/party-stage/src/scene/stage-light-bars.js +++ b/party-stage/src/scene/stage-light-bars.js @@ -7,6 +7,10 @@ export class StageLightBars extends SceneFeature { constructor() { super(); this.bars = []; + this.mode = 'STATIC'; // 'STATIC' or 'CHASE' + this.lastModeChange = 0; + this.chaseOffset = 0; + this.staticIndices = []; sceneFeatureManager.register(this); } @@ -16,25 +20,23 @@ export class StageLightBars extends SceneFeature { // or we can update them dynamically. // 1. Stage Front Edge Bar - // Stage is approx width 11, height 1.5, centered at z=-17.5, depth 5. - // Front edge is at z = -17.5 + 2.5 = -15. - this.createBar(new THREE.Vector3(0, 1.50, -14.8), new THREE.Vector3(11, 0.1, 0.1)); + this.createBar(new THREE.Vector3(0, 1.50, -14.95), new THREE.Vector3(11, 0.1, 0.1)); // 2. Stage Side Bars (Vertical) // Left Front - this.createBar(new THREE.Vector3(-5.45, 0.7, -14.8), new THREE.Vector3(0.1, 1.5, 0.1)); + this.createBar(new THREE.Vector3(-5.45, 0.7, -14.95), new THREE.Vector3(0.1, 1.5, 0.1)); // Right Front - this.createBar(new THREE.Vector3(5.45, 0.7, -14.8), new THREE.Vector3(0.1, 1.5, 0.1)); + this.createBar(new THREE.Vector3(5.45, 0.7, -14.95), new THREE.Vector3(0.1, 1.5, 0.1)); // 3. Overhead Beam Bars - // Beam is at y=9, z=-14, length 24. - this.createBar(new THREE.Vector3(0, 8.7, -14), new THREE.Vector3(24, 0.1, 0.1)); + this.createBar(new THREE.Vector3(0, 8.5, -14), new THREE.Vector3(31, 0.1, 0.1)); // 4. Vertical Truss Bars (Sides of the beam) - this.createBar(new THREE.Vector3(-11.9, 3, -14), new THREE.Vector3(0.2, 12, 0.2)); - this.createBar(new THREE.Vector3(11.9, 3, -14), new THREE.Vector3(0.2, 12, 0.2)); + this.createBar(new THREE.Vector3(-15.9, 3, -14), new THREE.Vector3(0.2, 12, 0.2)); + this.createBar(new THREE.Vector3(15.9, 3, -14), new THREE.Vector3(0.2, 12, 0.2)); this.applyColors(); + this.setVisibility(state.config.lightBarsEnabled); } createBar(position, size) { @@ -57,40 +59,92 @@ export class StageLightBars extends SceneFeature { applyColors() { const colors = state.config.lightBarColors; if (!colors || colors.length === 0) { - // Default off or white if list is empty + this.staticIndices = []; + } else { + // Default distribution (sequential) + this.staticIndices = this.bars.map((_, i) => i % colors.length); + } + this.updateBarColors(); + } + + updateBarColors() { + const colors = state.config.lightBarColors; + if (!colors || colors.length === 0) { this.bars.forEach(bar => { bar.material.color.setHex(0x111111); bar.material.emissive.setHex(0x000000); }); return; } - + this.bars.forEach((bar, index) => { - const colorHex = colors[index % colors.length]; + let colorIndex; + if (this.mode === 'CHASE') { + colorIndex = Math.floor(index + this.chaseOffset) % colors.length; + } else { + // Ensure staticIndices is populated + if (this.staticIndices.length <= index) { + this.staticIndices.push(index % colors.length); + } + colorIndex = this.staticIndices[index] % colors.length; + } + + if (colorIndex < 0) colorIndex += colors.length; + + const colorHex = colors[colorIndex]; const color = new THREE.Color(colorHex); bar.material.color.copy(color); bar.material.emissive.copy(color); }); } - update(deltaTime) { - // Check if colors changed (simple length check or reference check could work, - // but here we can just re-apply if needed or rely on ConfigUI calling a refresh. - // For simplicity, we'll assume ConfigUI updates state and we might poll or be notified. - // Let's just re-apply in update if we want to be reactive to the UI instantly, - // though it's slightly expensive. Better: ConfigUI triggers a refresh. - // For now, we'll just animate intensity. + redistributeColors() { + const colors = state.config.lightBarColors; + if (colors && colors.length > 0) { + this.staticIndices = this.bars.map(() => Math.floor(Math.random() * colors.length)); + } + } + setVisibility(visible) { + this.bars.forEach(bar => { + bar.mesh.visible = visible; + }); + } + + update(deltaTime) { if (!state.partyStarted) return; + if (!state.config.lightBarsEnabled) return; const time = state.clock.getElapsedTime(); - let beatIntensity = 0; - if (state.music) { - beatIntensity = state.music.beatIntensity; + const beatIntensity = state.music ? state.music.beatIntensity : 0; + const isBeat = beatIntensity > 0.8; + + // Mode Switching Logic + if (isBeat && time - this.lastModeChange > 4.0) { + // Chance to switch mode or redistribute + if (Math.random() < 0.3) { + this.mode = this.mode === 'STATIC' ? 'CHASE' : 'STATIC'; + this.lastModeChange = time; + + if (this.mode === 'STATIC') { + this.redistributeColors(); + } + } else if (this.mode === 'STATIC' && Math.random() < 0.5) { + // Redistribute without switching mode + this.redistributeColors(); + this.lastModeChange = time; + } } + // Update Chase + if (this.mode === 'CHASE') { + this.chaseOffset += deltaTime * 4.0; + } + + this.updateBarColors(); + // Pulsate - const baseIntensity = 0.1; + const baseIntensity = 0.2; const pulse = Math.sin(time * 2.0) * 0.3 + 0.3; // Breathing const beatFlash = beatIntensity * 2.0; // Sharp flash on beat diff --git a/party-stage/src/state.js b/party-stage/src/state.js index e6156ba..635fcd6 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -10,6 +10,8 @@ export function initState() { consoleRGBEnabled: true, consoleEnabled: true, gameboyEnabled: false, + lightBarsEnabled: true, + laserColorMode: 'RUNNING', // 'SINGLE', 'RANDOM', 'RUNNING', 'ANY' lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors guestCount: 150, djHat: 'None' // 'None', 'Santa', 'Top Hat'