From 7296715a5e87c50e619231c60963e3cbc9a806ac Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 4 Jan 2026 07:04:28 +0000 Subject: [PATCH] Feature: stage light bars --- party-stage/src/scene/config-ui.js | 79 ++++++++++++++++ party-stage/src/scene/root.js | 1 + party-stage/src/scene/stage-light-bars.js | 109 ++++++++++++++++++++++ party-stage/src/state.js | 1 + 4 files changed, 190 insertions(+) create mode 100644 party-stage/src/scene/stage-light-bars.js diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index bfcde75..c539b1f 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -123,6 +123,68 @@ export class ConfigUI extends SceneFeature { // Gameboy Toggle createToggle('Gameboy', 'gameboyEnabled'); + // --- Light Bar Colors --- + const colorContainer = document.createElement('div'); + Object.assign(colorContainer.style, { + display: 'flex', + flexDirection: 'column', + gap: '5px', + marginTop: '5px', + paddingTop: '5px', + borderTop: '1px solid #444' + }); + + const colorLabel = document.createElement('label'); + colorLabel.innerText = 'Stage Light Bars'; + colorContainer.appendChild(colorLabel); + + const colorControls = document.createElement('div'); + colorControls.style.display = 'flex'; + colorControls.style.gap = '5px'; + + const colorPicker = document.createElement('input'); + colorPicker.type = 'color'; + colorPicker.value = '#ff00ff'; + colorPicker.style.width = '40px'; + colorPicker.style.border = 'none'; + colorPicker.style.cursor = 'pointer'; + + const addColorBtn = document.createElement('button'); + addColorBtn.innerText = '+'; + addColorBtn.style.cursor = 'pointer'; + addColorBtn.onclick = () => { + state.config.lightBarColors.push(colorPicker.value); + saveConfig(); + updateColorList(); + const lightBars = sceneFeatureManager.features.find(f => f.constructor.name === 'StageLightBars'); + if (lightBars) lightBars.refreshColors(); + }; + + const clearColorsBtn = document.createElement('button'); + clearColorsBtn.innerText = 'Clear'; + clearColorsBtn.style.cursor = 'pointer'; + clearColorsBtn.onclick = () => { + state.config.lightBarColors = []; + saveConfig(); + updateColorList(); + const lightBars = sceneFeatureManager.features.find(f => f.constructor.name === 'StageLightBars'); + if (lightBars) lightBars.refreshColors(); + }; + + colorControls.appendChild(colorPicker); + colorControls.appendChild(addColorBtn); + colorControls.appendChild(clearColorsBtn); + colorContainer.appendChild(colorControls); + + const colorList = document.createElement('div'); + colorList.style.display = 'flex'; + colorList.style.flexWrap = 'wrap'; + colorList.style.gap = '4px'; + colorList.style.marginTop = '4px'; + this.colorList = colorList; + colorContainer.appendChild(colorList); + rightContainer.appendChild(colorContainer); + // Guest Count Input const guestRow = document.createElement('div'); guestRow.style.display = 'flex'; @@ -378,6 +440,7 @@ export class ConfigUI extends SceneFeature { document.body.appendChild(leftContainer); document.body.appendChild(rightContainer); this.updateStatus(); + this.updateColorList(); // Restore poster MediaStorage.getPoster().then(file => { @@ -388,6 +451,22 @@ export class ConfigUI extends SceneFeature { }); } + updateColorList() { + if (!this.colorList) return; + this.colorList.innerHTML = ''; + state.config.lightBarColors.forEach(color => { + const swatch = document.createElement('div'); + Object.assign(swatch.style, { + width: '15px', + height: '15px', + backgroundColor: color, + border: '1px solid #fff', + borderRadius: '2px' + }); + this.colorList.appendChild(swatch); + }); + } + updateStatus() { if (!this.songLabel) return; diff --git a/party-stage/src/scene/root.js b/party-stage/src/scene/root.js index 6044c88..50e7622 100644 --- a/party-stage/src/scene/root.js +++ b/party-stage/src/scene/root.js @@ -19,6 +19,7 @@ import { DJ } from './dj.js'; 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'; // Scene Features ^^^ // --- Scene Modeling Function --- diff --git a/party-stage/src/scene/stage-light-bars.js b/party-stage/src/scene/stage-light-bars.js new file mode 100644 index 0000000..e957af9 --- /dev/null +++ b/party-stage/src/scene/stage-light-bars.js @@ -0,0 +1,109 @@ +import * as THREE from 'three'; +import { state } from '../state.js'; +import { SceneFeature } from './SceneFeature.js'; +import sceneFeatureManager from './SceneFeatureManager.js'; + +export class StageLightBars extends SceneFeature { + constructor() { + super(); + this.bars = []; + sceneFeatureManager.register(this); + } + + init() { + // Shared Geometry/Material setup + // We use individual materials to allow different colors per bar if needed, + // 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)); + + // 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)); + // Right Front + this.createBar(new THREE.Vector3(5.45, 0.7, -14.8), 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)); + + // 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.applyColors(); + } + + createBar(position, size) { + const geometry = new THREE.BoxGeometry(size.x, size.y, size.z); + const material = new THREE.MeshStandardMaterial({ + color: 0xffffff, + emissive: 0xffffff, + emissiveIntensity: 1.0, + roughness: 0.4, + metalness: 0.8 + }); + const mesh = new THREE.Mesh(geometry, material); + mesh.position.copy(position); + mesh.castShadow = true; // Maybe cast shadow? + state.scene.add(mesh); + + this.bars.push({ mesh, material }); + } + + applyColors() { + const colors = state.config.lightBarColors; + if (!colors || colors.length === 0) { + // Default off or white if list is empty + 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]; + 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. + + if (!state.partyStarted) return; + + const time = state.clock.getElapsedTime(); + let beatIntensity = 0; + if (state.music) { + beatIntensity = state.music.beatIntensity; + } + + // Pulsate + const baseIntensity = 0.1; + const pulse = Math.sin(time * 2.0) * 0.3 + 0.3; // Breathing + const beatFlash = beatIntensity * 2.0; // Sharp flash on beat + + const totalIntensity = baseIntensity + pulse + beatFlash; + + this.bars.forEach(bar => { + bar.material.emissiveIntensity = totalIntensity; + }); + } + + refreshColors() { + this.applyColors(); + } +} + +new StageLightBars(); diff --git a/party-stage/src/state.js b/party-stage/src/state.js index 84c3ef2..e6156ba 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -10,6 +10,7 @@ export function initState() { consoleRGBEnabled: true, consoleEnabled: true, gameboyEnabled: false, + lightBarColors: ['#ff00ff', '#00ffff', '#ffff00'], // Default neon colors guestCount: 150, djHat: 'None' // 'None', 'Santa', 'Top Hat' };