Feature: Stage colors configurable and used everywhere

This commit is contained in:
Dejvino 2026-01-04 07:26:27 +00:00
parent 7296715a5e
commit 8bf82f5f8a
6 changed files with 230 additions and 38 deletions

View File

@ -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<count; i++) {
newColors.push('#' + new THREE.Color().setHSL(Math.random(), 1.0, 0.5).getHexString());
}
state.config.lightBarColors = newColors;
saveConfig();
this.updateColorList();
const lightBars = sceneFeatureManager.features.find(f => 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();
};

View File

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

View File

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

View File

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

View File

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

View File

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