Feature: Stage colors configurable and used everywhere
This commit is contained in:
parent
7296715a5e
commit
8bf82f5f8a
@ -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();
|
||||
};
|
||||
|
||||
@ -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++;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user