Compare commits
8 Commits
03474298a9
...
8b2d1aed53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b2d1aed53 | ||
|
|
8bf82f5f8a | ||
|
|
7296715a5e | ||
|
|
edbed229b3 | ||
|
|
47b7645046 | ||
|
|
6b292b32ad | ||
|
|
dcf12771d6 | ||
|
|
066fcc26cc |
@ -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';
|
||||
@ -9,12 +10,14 @@ export class ConfigUI extends SceneFeature {
|
||||
super();
|
||||
sceneFeatureManager.register(this);
|
||||
this.toggles = {};
|
||||
this.containers = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
const container = document.createElement('div');
|
||||
container.id = 'config-ui';
|
||||
Object.assign(container.style, {
|
||||
// --- Left Panel: Party Schedule ---
|
||||
const leftContainer = document.createElement('div');
|
||||
leftContainer.id = 'config-ui-left';
|
||||
Object.assign(leftContainer.style, {
|
||||
position: 'absolute',
|
||||
top: '70px',
|
||||
left: '20px',
|
||||
@ -29,6 +32,47 @@ export class ConfigUI extends SceneFeature {
|
||||
gap: '10px',
|
||||
minWidth: '200px'
|
||||
});
|
||||
this.containers.push(leftContainer);
|
||||
|
||||
const heading = document.createElement('h3');
|
||||
heading.innerText = 'Party schedule';
|
||||
Object.assign(heading.style, {
|
||||
margin: '0 0 15px 0',
|
||||
textAlign: 'center',
|
||||
borderBottom: '1px solid #555',
|
||||
paddingBottom: '10px'
|
||||
});
|
||||
leftContainer.appendChild(heading);
|
||||
|
||||
// --- Right Panel: Party Venue ---
|
||||
const rightContainer = document.createElement('div');
|
||||
rightContainer.id = 'config-ui-right';
|
||||
Object.assign(rightContainer.style, {
|
||||
position: 'absolute',
|
||||
top: '70px',
|
||||
right: '20px',
|
||||
zIndex: '1000',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
padding: '15px',
|
||||
borderRadius: '8px',
|
||||
color: 'white',
|
||||
fontFamily: 'sans-serif',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
minWidth: '200px'
|
||||
});
|
||||
this.containers.push(rightContainer);
|
||||
|
||||
const venueHeading = document.createElement('h3');
|
||||
venueHeading.innerText = 'Party venue';
|
||||
Object.assign(venueHeading.style, {
|
||||
margin: '0 0 15px 0',
|
||||
textAlign: 'center',
|
||||
borderBottom: '1px solid #555',
|
||||
paddingBottom: '10px'
|
||||
});
|
||||
rightContainer.appendChild(venueHeading);
|
||||
|
||||
const saveConfig = () => {
|
||||
localStorage.setItem('partyConfig', JSON.stringify(state.config));
|
||||
@ -56,24 +100,170 @@ export class ConfigUI extends SceneFeature {
|
||||
this.toggles[configKey] = { checkbox: chk, callback: onChange };
|
||||
row.appendChild(lbl);
|
||||
row.appendChild(chk);
|
||||
container.appendChild(row);
|
||||
rightContainer.appendChild(row);
|
||||
};
|
||||
|
||||
// Torches Toggle
|
||||
createToggle('Stage Torches', 'torchesEnabled', (enabled) => {
|
||||
const torches = sceneFeatureManager.features.find(f => f.constructor.name === 'StageTorches');
|
||||
if (torches && torches.group) torches.group.visible = enabled;
|
||||
});
|
||||
createToggle('Stage Torches', 'torchesEnabled');
|
||||
|
||||
// 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');
|
||||
|
||||
// Music Console Toggle
|
||||
createToggle('Music Console', 'consoleEnabled', (enabled) => {
|
||||
const consoleFeature = sceneFeatureManager.features.find(f => f.constructor.name === 'MusicConsole');
|
||||
if (consoleFeature && consoleFeature.group) consoleFeature.group.visible = enabled;
|
||||
});
|
||||
|
||||
// Console RGB Toggle
|
||||
createToggle('Console RGB Panel', 'consoleRGBEnabled');
|
||||
|
||||
// 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, {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '5px',
|
||||
marginTop: '5px',
|
||||
paddingTop: '5px',
|
||||
borderTop: '1px solid #444'
|
||||
});
|
||||
|
||||
const colorLabel = document.createElement('label');
|
||||
colorLabel.innerText = 'Stage colors';
|
||||
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();
|
||||
this.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();
|
||||
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();
|
||||
};
|
||||
|
||||
colorControls.appendChild(colorPicker);
|
||||
colorControls.appendChild(addColorBtn);
|
||||
colorControls.appendChild(clearColorsBtn);
|
||||
colorControls.appendChild(randomizeColorsBtn);
|
||||
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';
|
||||
guestRow.style.alignItems = 'center';
|
||||
guestRow.style.justifyContent = 'space-between';
|
||||
|
||||
const guestLabel = document.createElement('label');
|
||||
guestLabel.innerText = 'Guest Count';
|
||||
|
||||
const guestInput = document.createElement('input');
|
||||
guestInput.type = 'number';
|
||||
guestInput.min = '0';
|
||||
guestInput.max = '500';
|
||||
guestInput.value = state.config.guestCount;
|
||||
guestInput.style.width = '60px';
|
||||
guestInput.onchange = (e) => {
|
||||
const val = parseInt(e.target.value, 10);
|
||||
state.config.guestCount = val;
|
||||
saveConfig();
|
||||
const guestsFeature = sceneFeatureManager.features.find(f => f.constructor.name === 'PartyGuests');
|
||||
if (guestsFeature) guestsFeature.setGuestCount(val);
|
||||
};
|
||||
this.guestInput = guestInput;
|
||||
|
||||
guestRow.appendChild(guestLabel);
|
||||
guestRow.appendChild(guestInput);
|
||||
rightContainer.appendChild(guestRow);
|
||||
|
||||
// DJ Hat Selector
|
||||
const hatRow = document.createElement('div');
|
||||
hatRow.style.display = 'flex';
|
||||
@ -99,14 +289,11 @@ export class ConfigUI extends SceneFeature {
|
||||
|
||||
hatRow.appendChild(hatLabel);
|
||||
hatRow.appendChild(hatSelect);
|
||||
container.appendChild(hatRow);
|
||||
rightContainer.appendChild(hatRow);
|
||||
|
||||
// --- Status & Control Section ---
|
||||
const statusContainer = document.createElement('div');
|
||||
Object.assign(statusContainer.style, {
|
||||
marginTop: '15px',
|
||||
paddingTop: '10px',
|
||||
borderTop: '1px solid #555',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '8px'
|
||||
@ -143,7 +330,7 @@ export class ConfigUI extends SceneFeature {
|
||||
marginTop: '10px',
|
||||
padding: '8px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#555',
|
||||
backgroundColor: '#ff9800', // Default orange
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@ -168,6 +355,7 @@ export class ConfigUI extends SceneFeature {
|
||||
loadPosterBtn.onclick = () => {
|
||||
posterInput.click();
|
||||
};
|
||||
this.loadPosterBtn = loadPosterBtn;
|
||||
statusContainer.appendChild(loadPosterBtn);
|
||||
|
||||
// Load Tapes Button
|
||||
@ -178,7 +366,7 @@ export class ConfigUI extends SceneFeature {
|
||||
marginTop: '10px',
|
||||
padding: '8px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#555',
|
||||
backgroundColor: '#ff9800', // Default orange
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@ -194,7 +382,7 @@ export class ConfigUI extends SceneFeature {
|
||||
marginTop: '10px',
|
||||
padding: '8px',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: '#555',
|
||||
backgroundColor: '#ff9800', // Default orange
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
@ -204,6 +392,7 @@ export class ConfigUI extends SceneFeature {
|
||||
const fileInput = document.getElementById('musicFileInput');
|
||||
if (fileInput) fileInput.click();
|
||||
};
|
||||
this.chooseSongBtn = chooseSongBtn;
|
||||
statusContainer.appendChild(chooseSongBtn);
|
||||
|
||||
// Start Party Button
|
||||
@ -214,8 +403,8 @@ export class ConfigUI extends SceneFeature {
|
||||
marginTop: '10px',
|
||||
padding: '10px',
|
||||
cursor: 'not-allowed',
|
||||
backgroundColor: '#333',
|
||||
color: '#777',
|
||||
backgroundColor: '#dc3545', // Default red
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
fontSize: '16px',
|
||||
@ -227,7 +416,7 @@ export class ConfigUI extends SceneFeature {
|
||||
if (musicPlayer) musicPlayer.startSequence();
|
||||
};
|
||||
statusContainer.appendChild(this.startButton);
|
||||
container.appendChild(statusContainer);
|
||||
leftContainer.appendChild(statusContainer);
|
||||
|
||||
// Reset Button
|
||||
const resetBtn = document.createElement('button');
|
||||
@ -281,6 +470,11 @@ export class ConfigUI extends SceneFeature {
|
||||
lasersEnabled: true,
|
||||
sideScreensEnabled: true,
|
||||
consoleRGBEnabled: true,
|
||||
consoleEnabled: true,
|
||||
gameboyEnabled: false,
|
||||
lightBarsEnabled: true,
|
||||
laserColorMode: 'RUNNING',
|
||||
guestCount: 150,
|
||||
djHat: 'None'
|
||||
};
|
||||
for (const key in defaults) {
|
||||
@ -290,14 +484,17 @@ export class ConfigUI extends SceneFeature {
|
||||
if (this.toggles[key].callback) this.toggles[key].callback(defaults[key]);
|
||||
}
|
||||
}
|
||||
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();
|
||||
};
|
||||
container.appendChild(resetBtn);
|
||||
leftContainer.appendChild(resetBtn);
|
||||
|
||||
document.body.appendChild(container);
|
||||
this.container = container;
|
||||
document.body.appendChild(leftContainer);
|
||||
document.body.appendChild(rightContainer);
|
||||
this.updateStatus();
|
||||
this.updateColorList();
|
||||
|
||||
// Restore poster
|
||||
MediaStorage.getPoster().then(file => {
|
||||
@ -308,26 +505,59 @@ 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;
|
||||
|
||||
const orange = '#ff9800';
|
||||
const green = '#28a745';
|
||||
const red = '#dc3545';
|
||||
|
||||
// Update Song Info
|
||||
if (state.music && state.music.songTitle) {
|
||||
this.songLabel.innerText = `Song: ${state.music.songTitle}`;
|
||||
this.songLabel.style.color = '#fff';
|
||||
|
||||
this.startButton.disabled = false;
|
||||
this.startButton.style.backgroundColor = '#28a745';
|
||||
this.startButton.style.backgroundColor = green;
|
||||
this.startButton.style.color = 'white';
|
||||
this.startButton.style.cursor = 'pointer';
|
||||
this.startButton.style.opacity = '1';
|
||||
|
||||
if (this.chooseSongBtn) this.chooseSongBtn.style.backgroundColor = green;
|
||||
} else {
|
||||
this.songLabel.innerText = 'Song: (None)';
|
||||
this.songLabel.style.color = '#aaa';
|
||||
|
||||
this.startButton.disabled = true;
|
||||
this.startButton.style.backgroundColor = '#333';
|
||||
this.startButton.style.color = '#777';
|
||||
this.startButton.style.backgroundColor = red;
|
||||
this.startButton.style.color = 'white';
|
||||
this.startButton.style.opacity = '0.6';
|
||||
this.startButton.style.cursor = 'not-allowed';
|
||||
|
||||
if (this.chooseSongBtn) this.chooseSongBtn.style.backgroundColor = orange;
|
||||
}
|
||||
|
||||
if (this.loadPosterBtn) {
|
||||
this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange;
|
||||
}
|
||||
if (state.loadTapeButton) {
|
||||
state.loadTapeButton.style.backgroundColor = (state.videoUrls && state.videoUrls.length > 0) ? green : orange;
|
||||
}
|
||||
|
||||
// Update Tape List
|
||||
@ -356,10 +586,10 @@ export class ConfigUI extends SceneFeature {
|
||||
}
|
||||
|
||||
onPartyStart() {
|
||||
if (this.container) this.container.style.display = 'none';
|
||||
this.containers.forEach(c => c.style.display = 'none');
|
||||
}
|
||||
|
||||
onPartyEnd() {
|
||||
if (this.container) this.container.style.display = 'flex';
|
||||
this.containers.forEach(c => c.style.display = 'flex');
|
||||
}
|
||||
}
|
||||
@ -142,6 +142,42 @@ export class DJ extends SceneFeature {
|
||||
topHatGroup.visible = false;
|
||||
this.head.add(topHatGroup);
|
||||
this.hats['Top Hat'] = topHatGroup;
|
||||
|
||||
// --- Gameboy / Portable Console ---
|
||||
const gbGroup = new THREE.Group();
|
||||
// Body
|
||||
const gbGeo = new THREE.BoxGeometry(0.2, 0.3, 0.05);
|
||||
const gbMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.4 });
|
||||
const gbBody = new THREE.Mesh(gbGeo, gbMat);
|
||||
gbGroup.add(gbBody);
|
||||
|
||||
// Screen (Emissive)
|
||||
const screenGeo = new THREE.PlaneGeometry(0.16, 0.12);
|
||||
this.gbScreenMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
|
||||
const screen = new THREE.Mesh(screenGeo, this.gbScreenMat);
|
||||
screen.position.set(0, 0.05, 0.026);
|
||||
gbGroup.add(screen);
|
||||
|
||||
// Knobs/Buttons
|
||||
const knobGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.02, 8);
|
||||
const knobMat = new THREE.MeshStandardMaterial({ color: 0xcccccc });
|
||||
|
||||
const k1 = new THREE.Mesh(knobGeo, knobMat);
|
||||
k1.rotation.x = Math.PI/2;
|
||||
k1.position.set(-0.05, -0.08, 0.026);
|
||||
gbGroup.add(k1);
|
||||
|
||||
const k2 = new THREE.Mesh(knobGeo, knobMat);
|
||||
k2.rotation.x = Math.PI/2;
|
||||
k2.position.set(0.05, -0.08, 0.026);
|
||||
gbGroup.add(k2);
|
||||
|
||||
// Attach to Left Arm (Hand position)
|
||||
gbGroup.position.set(0, -0.9, 0.1);
|
||||
gbGroup.rotation.x = -Math.PI / 4; // Tilted up
|
||||
gbGroup.rotation.z = Math.PI; // Upside down
|
||||
this.leftArm.add(gbGroup);
|
||||
this.gameboy = gbGroup;
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
@ -162,9 +198,16 @@ export class DJ extends SceneFeature {
|
||||
// Update Arm State
|
||||
this.armTimer -= deltaTime;
|
||||
if (this.armTimer <= 0) {
|
||||
this.armState = Math.floor(Math.random() * 5);
|
||||
this.armState = Math.floor(Math.random() * 4); // 0-3: Dance moves
|
||||
this.armTimer = 2 + Math.random() * 4;
|
||||
if (Math.random() < 0.3) this.armState = 4; // Twiddling some more
|
||||
|
||||
const rand = Math.random();
|
||||
// Gameboy Fiddling
|
||||
if (state.config.gameboyEnabled && rand < 0.35) {
|
||||
this.armState = 5;
|
||||
} else if (state.config.consoleEnabled && rand < 0.65) {
|
||||
this.armState = 4; // Console Twiddling
|
||||
}
|
||||
}
|
||||
|
||||
const upAngle = Math.PI * 0.85;
|
||||
@ -176,12 +219,19 @@ export class DJ extends SceneFeature {
|
||||
let targetLeftX = 0;
|
||||
let targetRightX = 0;
|
||||
|
||||
if (this.armState === 4) {
|
||||
if (this.armState === 4 && state.config.consoleEnabled) {
|
||||
// Twiddling
|
||||
targetLeftZ = 0.2;
|
||||
targetRightZ = 0.2;
|
||||
targetLeftX = twiddleX;
|
||||
targetRightX = twiddleX;
|
||||
} else if (this.armState === 5 && state.config.gameboyEnabled) {
|
||||
targetLeftZ = -0.3;
|
||||
targetRightZ = -0.5; // Cross inwards
|
||||
const timeRotX = Math.cos(time * 0.5) * 0.3;
|
||||
const consoleRotX = state.config.consoleEnabled ? -0.5 : 0;
|
||||
targetLeftX = -1.0 + timeRotX + consoleRotX; // Lift up
|
||||
targetRightX = -1.1 + timeRotX + consoleRotX; // Lift up
|
||||
} else {
|
||||
targetLeftZ = (this.armState === 1 || this.armState === 3) ? upAngle : downAngle;
|
||||
targetRightZ = (this.armState === 2 || this.armState === 3) ? upAngle : downAngle;
|
||||
@ -192,12 +242,18 @@ export class DJ extends SceneFeature {
|
||||
this.currentLeftAngleX = THREE.MathUtils.lerp(this.currentLeftAngleX, targetLeftX, deltaTime * 5);
|
||||
this.currentRightAngleX = THREE.MathUtils.lerp(this.currentRightAngleX, targetRightX, deltaTime * 5);
|
||||
|
||||
if (this.armState === 4) {
|
||||
if (this.armState === 4 && state.config.consoleEnabled) {
|
||||
const t = time * 15;
|
||||
this.leftArm.rotation.z = -this.currentLeftAngle + Math.cos(t) * 0.05;
|
||||
this.rightArm.rotation.z = this.currentRightAngle + Math.sin(t) * 0.05;
|
||||
this.leftArm.rotation.x = this.currentLeftAngleX + Math.sin(t) * 0.1;
|
||||
this.rightArm.rotation.x = this.currentRightAngleX + Math.cos(t) * 0.1;
|
||||
} else if (this.armState === 5 && state.config.gameboyEnabled) {
|
||||
const t = time * 20;
|
||||
this.leftArm.rotation.z = -this.currentLeftAngle + Math.sin(t * 0.2) * 0.01;
|
||||
this.rightArm.rotation.z = this.currentRightAngle + Math.sin(t) * 0.05;
|
||||
this.leftArm.rotation.x = this.currentLeftAngleX + Math.cos(t * 0.2) * 0.01;
|
||||
this.rightArm.rotation.x = this.currentRightAngleX + Math.cos(t) * 0.05;
|
||||
} else {
|
||||
const wave = Math.sin(time * 8) * 0.1;
|
||||
const beatBounce = beatIntensity * 0.2;
|
||||
@ -215,7 +271,8 @@ export class DJ extends SceneFeature {
|
||||
this.moveTimer -= deltaTime;
|
||||
if (this.moveTimer <= 0) {
|
||||
this.state = 'MOVING';
|
||||
this.targetX = (Math.random() - 0.5) * 2.5;
|
||||
const range = state.config.consoleEnabled ? 2.5 : 10.0;
|
||||
this.targetX = (Math.random() - 0.5) * range;
|
||||
}
|
||||
} else if (this.state === 'MOVING') {
|
||||
const speed = 1.5;
|
||||
@ -232,6 +289,15 @@ export class DJ extends SceneFeature {
|
||||
for (const [name, mesh] of Object.entries(this.hats)) {
|
||||
mesh.visible = (state.config.djHat === name);
|
||||
}
|
||||
|
||||
// Update Gameboy
|
||||
if (this.gameboy) {
|
||||
this.gameboy.visible = state.config.gameboyEnabled;
|
||||
if (this.gameboy.visible && state.music) {
|
||||
const hue = (time * 0.5) % 1;
|
||||
this.gbScreenMat.color.setHSL(hue, 1.0, 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPartyStart() {
|
||||
|
||||
@ -21,6 +21,8 @@ export class MusicConsole extends SceneFeature {
|
||||
// Position on stage, centered
|
||||
group.position.set(0, stageY, -16.5);
|
||||
state.scene.add(group);
|
||||
this.group = group;
|
||||
this.group.visible = state.config.consoleEnabled;
|
||||
|
||||
// 1. The Stand/Table Body
|
||||
const standGeo = new THREE.BoxGeometry(consoleWidth, consoleHeight, consoleDepth);
|
||||
@ -226,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++;
|
||||
}
|
||||
|
||||
@ -77,10 +77,12 @@ export class MusicPlayer extends SceneFeature {
|
||||
|
||||
startSequence() {
|
||||
const uiContainer = document.getElementById('ui-container');
|
||||
const configUI = document.getElementById('config-ui');
|
||||
const configUILeft = document.getElementById('config-ui-left');
|
||||
const configUIRight = document.getElementById('config-ui-right');
|
||||
|
||||
if (uiContainer) uiContainer.style.display = 'none';
|
||||
if (configUI) configUI.style.display = 'none';
|
||||
if (configUILeft) configUILeft.style.display = 'none';
|
||||
if (configUIRight) configUIRight.style.display = 'none';
|
||||
if (state.loadTapeButton) state.loadTapeButton.classList.add('hidden');
|
||||
|
||||
showStandbyScreen();
|
||||
|
||||
@ -7,7 +7,6 @@ import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
const stageHeight = 1.5;
|
||||
const stageDepth = 5;
|
||||
const length = 25;
|
||||
const numGuests = 150;
|
||||
const moveSpeed = 0.8;
|
||||
const movementArea = { x: 15, z: length, y: 0, centerZ: -2 };
|
||||
const jumpChance = 0.01;
|
||||
@ -25,6 +24,7 @@ export class PartyGuests extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
this.guests = [];
|
||||
this.guestPool = []; // Store all created guests to reuse them
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
@ -36,96 +36,115 @@ export class PartyGuests extends SceneFeature {
|
||||
const headGeo = new THREE.SphereGeometry(0.25, 16, 16);
|
||||
// Arm: Ellipsoid (Scaled Sphere)
|
||||
const armGeo = new THREE.SphereGeometry(0.12, 16, 16);
|
||||
|
||||
this.geometries = { bodyGeo, headGeo, armGeo };
|
||||
|
||||
const createGuests = () => {
|
||||
for (let i = 0; i < numGuests; i++) {
|
||||
// Random Color
|
||||
// Dark gray-blue shades
|
||||
const color = new THREE.Color().setHSL(
|
||||
0.6 + (Math.random() * 0.1 - 0.05), // Hue around 0.6 (blue)
|
||||
0.1 + Math.random() * 0.1, // Low saturation
|
||||
0.01 + Math.random() * 0.05 // Much darker lightness
|
||||
);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: color,
|
||||
roughness: 0.6,
|
||||
metalness: 0.1,
|
||||
});
|
||||
// Initialize with config count
|
||||
this.setGuestCount(state.config.guestCount);
|
||||
}
|
||||
|
||||
const group = new THREE.Group();
|
||||
createGuest() {
|
||||
// Random Color
|
||||
// Dark gray-blue shades
|
||||
const color = new THREE.Color().setHSL(
|
||||
0.6 + (Math.random() * 0.1 - 0.05), // Hue around 0.6 (blue)
|
||||
0.1 + Math.random() * 0.1, // Low saturation
|
||||
0.01 + Math.random() * 0.05 // Much darker lightness
|
||||
);
|
||||
const material = new THREE.MeshStandardMaterial({
|
||||
color: color,
|
||||
roughness: 0.6,
|
||||
metalness: 0.1,
|
||||
});
|
||||
|
||||
const scale = 0.85 + Math.random() * 0.3;
|
||||
group.scale.setScalar(scale);
|
||||
const group = new THREE.Group();
|
||||
|
||||
// Body
|
||||
const body = new THREE.Mesh(bodyGeo, material);
|
||||
body.position.y = 0.7; // Center of capsule (0.8 length + 0.3*2 radius = 1.4 total height. Center at 0.7)
|
||||
body.castShadow = true;
|
||||
body.receiveShadow = true;
|
||||
group.add(body);
|
||||
const scale = 0.85 + Math.random() * 0.3;
|
||||
group.scale.setScalar(scale);
|
||||
|
||||
// Head
|
||||
const head = new THREE.Mesh(headGeo, material);
|
||||
head.position.y = 1.55; // Top of body
|
||||
head.castShadow = true;
|
||||
head.receiveShadow = true;
|
||||
group.add(head);
|
||||
// Body
|
||||
const body = new THREE.Mesh(this.geometries.bodyGeo, material);
|
||||
body.position.y = 0.7;
|
||||
body.castShadow = true;
|
||||
body.receiveShadow = true;
|
||||
group.add(body);
|
||||
|
||||
// Arms
|
||||
const createArm = (isLeft) => {
|
||||
const pivot = new THREE.Group();
|
||||
// Shoulder position
|
||||
pivot.position.set(isLeft ? -0.35 : 0.35, 1.3, 0);
|
||||
|
||||
const arm = new THREE.Mesh(armGeo, material);
|
||||
arm.scale.set(1, 3.5, 1); // Ellipsoid
|
||||
arm.position.y = -0.4; // Hang down from pivot
|
||||
arm.castShadow = true;
|
||||
arm.receiveShadow = true;
|
||||
|
||||
pivot.add(arm);
|
||||
return pivot;
|
||||
};
|
||||
// Head
|
||||
const head = new THREE.Mesh(this.geometries.headGeo, material);
|
||||
head.position.y = 1.55;
|
||||
head.castShadow = true;
|
||||
head.receiveShadow = true;
|
||||
group.add(head);
|
||||
|
||||
const leftArm = createArm(true);
|
||||
const rightArm = createArm(false);
|
||||
|
||||
group.add(leftArm);
|
||||
group.add(rightArm);
|
||||
|
||||
// Position
|
||||
const pos = new THREE.Vector3(
|
||||
(Math.random() - 0.5) * movementArea.x,
|
||||
0,
|
||||
movementArea.centerZ + ( rushIn
|
||||
? (((Math.random()-0.5) * length * 0.6) - length * 0.3)
|
||||
: ((Math.random()-0.5) * length))
|
||||
);
|
||||
|
||||
group.position.copy(pos);
|
||||
group.visible = false;
|
||||
state.scene.add(group);
|
||||
|
||||
this.guests.push({
|
||||
mesh: group,
|
||||
leftArm,
|
||||
rightArm,
|
||||
state: 'WAITING',
|
||||
targetPosition: pos.clone(),
|
||||
waitStartTime: 0,
|
||||
waitTime: 3 + Math.random() * 4, // Wait longer: 3-7 seconds
|
||||
isJumping: false,
|
||||
jumpStartTime: 0,
|
||||
jumpHeight: 0,
|
||||
shouldRaiseArms: false,
|
||||
handsUpTimer: 0,
|
||||
handsRaisedType: 'BOTH',
|
||||
randomOffset: Math.random() * 100
|
||||
});
|
||||
}
|
||||
// Arms
|
||||
const createArm = (isLeft) => {
|
||||
const pivot = new THREE.Group();
|
||||
// Shoulder position
|
||||
pivot.position.set(isLeft ? -0.35 : 0.35, 1.3, 0);
|
||||
|
||||
const arm = new THREE.Mesh(this.geometries.armGeo, material);
|
||||
arm.scale.set(1, 3.5, 1); // Ellipsoid
|
||||
arm.position.y = -0.4; // Hang down from pivot
|
||||
arm.castShadow = true;
|
||||
arm.receiveShadow = true;
|
||||
|
||||
pivot.add(arm);
|
||||
return pivot;
|
||||
};
|
||||
|
||||
createGuests();
|
||||
const leftArm = createArm(true);
|
||||
const rightArm = createArm(false);
|
||||
|
||||
group.add(leftArm);
|
||||
group.add(rightArm);
|
||||
|
||||
// Position
|
||||
const pos = new THREE.Vector3(
|
||||
(Math.random() - 0.5) * movementArea.x,
|
||||
0,
|
||||
movementArea.centerZ + ( rushIn
|
||||
? (((Math.random()-0.5) * length * 0.6) - length * 0.3)
|
||||
: ((Math.random()-0.5) * length))
|
||||
);
|
||||
|
||||
group.position.copy(pos);
|
||||
group.visible = false;
|
||||
state.scene.add(group);
|
||||
|
||||
return {
|
||||
mesh: group,
|
||||
leftArm,
|
||||
rightArm,
|
||||
state: 'WAITING',
|
||||
targetPosition: pos.clone(),
|
||||
waitStartTime: 0,
|
||||
waitTime: 3 + Math.random() * 4,
|
||||
isJumping: false,
|
||||
jumpStartTime: 0,
|
||||
jumpHeight: 0,
|
||||
shouldRaiseArms: false,
|
||||
handsUpTimer: 0,
|
||||
handsRaisedType: 'BOTH',
|
||||
randomOffset: Math.random() * 100
|
||||
};
|
||||
}
|
||||
|
||||
setGuestCount(count) {
|
||||
// Ensure we have enough guests in the pool
|
||||
while (this.guestPool.length < count) {
|
||||
this.guestPool.push(this.createGuest());
|
||||
}
|
||||
|
||||
// Update visibility and active list
|
||||
this.guests = [];
|
||||
this.guestPool.forEach((guest, index) => {
|
||||
if (index < count) {
|
||||
guest.mesh.visible = state.partyStarted; // Only visible if party started
|
||||
this.guests.push(guest);
|
||||
} else {
|
||||
guest.mesh.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
@ -280,7 +299,7 @@ export class PartyGuests extends SceneFeature {
|
||||
onPartyStart() {
|
||||
const stageFrontZ = -40 / 2 + 5 + 5; // In front of the stage
|
||||
this.guests.forEach((guestObj, index) => {
|
||||
guestObj.mesh.visible = true;
|
||||
guestObj.mesh.visible = true; // Make sure active guests are visible
|
||||
// Rush to the stage
|
||||
guestObj.state = 'MOVING';
|
||||
if (index % 2 === 0) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 ---
|
||||
|
||||
@ -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 ---
|
||||
|
||||
163
party-stage/src/scene/stage-light-bars.js
Normal file
163
party-stage/src/scene/stage-light-bars.js
Normal file
@ -0,0 +1,163 @@
|
||||
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 = [];
|
||||
this.mode = 'STATIC'; // 'STATIC' or 'CHASE'
|
||||
this.lastModeChange = 0;
|
||||
this.chaseOffset = 0;
|
||||
this.staticIndices = [];
|
||||
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
|
||||
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.95), new THREE.Vector3(0.1, 1.5, 0.1));
|
||||
// Right Front
|
||||
this.createBar(new THREE.Vector3(5.45, 0.7, -14.95), new THREE.Vector3(0.1, 1.5, 0.1));
|
||||
|
||||
// 3. Overhead Beam Bars
|
||||
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(-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) {
|
||||
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) {
|
||||
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) => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
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.2;
|
||||
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();
|
||||
@ -82,10 +82,29 @@ export class StageLights extends SceneFeature {
|
||||
baseX: x
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize static random state for pre-party
|
||||
this.staticColor = new THREE.Color().setHSL(Math.random(), 0.8, 0.5);
|
||||
this.focusPoint.set((Math.random() - 0.5) * 20, Math.random() * 2, -10 + (Math.random() - 0.5) * 10);
|
||||
this.targetFocusPoint.copy(this.focusPoint);
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
const time = state.clock.getElapsedTime();
|
||||
|
||||
if (!state.partyStarted) {
|
||||
this.lights.forEach((item) => {
|
||||
const targetX = this.focusPoint.x + (item.baseX * 0.2);
|
||||
item.target.position.set(targetX, this.focusPoint.y, this.focusPoint.z);
|
||||
item.fixture.lookAt(targetX, this.focusPoint.y, this.focusPoint.z);
|
||||
item.light.intensity = 30;
|
||||
if (this.staticColor) {
|
||||
item.light.color.copy(this.staticColor);
|
||||
item.lens.material.color.copy(this.staticColor);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Change target area logic
|
||||
let shouldChange = false;
|
||||
@ -119,21 +138,29 @@ export class StageLights extends SceneFeature {
|
||||
// Update each light
|
||||
const intensity = state.music ? 20 + state.music.beatIntensity * 150 : 50;
|
||||
|
||||
const hue = (time * 0.2) % 1;
|
||||
const color = new THREE.Color().setHSL(hue, 0.8, 0.5);
|
||||
|
||||
const spread = 0.2 + (state.music ? state.music.beatIntensity * 0.4 : 0);
|
||||
const bounce = state.music ? state.music.beatIntensity * 0.5 : 0;
|
||||
const palette = state.config.lightBarColors;
|
||||
|
||||
this.lights.forEach((item) => {
|
||||
this.lights.forEach((item, index) => {
|
||||
// Converge lights on focus point, but keep slight X offset for spread
|
||||
const targetX = this.focusPoint.x + (item.baseX * spread);
|
||||
|
||||
item.target.position.set(targetX, this.focusPoint.y + bounce, this.focusPoint.z);
|
||||
item.fixture.lookAt(targetX, this.focusPoint.y, this.focusPoint.z);
|
||||
item.light.intensity = intensity;
|
||||
item.light.color.copy(color);
|
||||
item.lens.material.color.copy(color);
|
||||
|
||||
if (palette && palette.length > 0) {
|
||||
const colorIndex = Math.floor(time * 0.5) % palette.length;
|
||||
const c = new THREE.Color(palette[colorIndex]);
|
||||
item.light.color.copy(c);
|
||||
item.lens.material.color.copy(c);
|
||||
} else {
|
||||
const hue = (time * 0.2) % 1;
|
||||
const c = new THREE.Color().setHSL(hue, 0.8, 0.5);
|
||||
item.light.color.copy(c);
|
||||
item.lens.material.color.copy(c);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export class StageTorches extends SceneFeature {
|
||||
createTorch(position) {
|
||||
const torchGroup = new THREE.Group();
|
||||
torchGroup.position.copy(position);
|
||||
torchGroup.visible = false; // Start invisible
|
||||
torchGroup.visible = state.config.torchesEnabled;
|
||||
|
||||
// --- Torch Holder ---
|
||||
const holderMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.6, metalness: 0.5 });
|
||||
@ -55,6 +55,7 @@ export class StageTorches extends SceneFeature {
|
||||
pointLight.castShadow = true;
|
||||
pointLight.shadow.mapSize.width = 128;
|
||||
pointLight.shadow.mapSize.height = 128;
|
||||
pointLight.visible = false;
|
||||
torchGroup.add(pointLight);
|
||||
|
||||
// --- Particle System for Fire ---
|
||||
@ -82,6 +83,7 @@ export class StageTorches extends SceneFeature {
|
||||
}
|
||||
particles.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
const particleSystem = new THREE.Points(particles, particleMaterial);
|
||||
particleSystem.visible = false;
|
||||
torchGroup.add(particleSystem);
|
||||
|
||||
return { group: torchGroup, light: pointLight, particles: particleSystem, particleData: particleData };
|
||||
@ -102,9 +104,25 @@ export class StageTorches extends SceneFeature {
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
if (!state.partyStarted) return;
|
||||
const enabled = state.config.torchesEnabled;
|
||||
this.torches.forEach(torch => {
|
||||
if (torch.group.visible !== enabled) torch.group.visible = enabled;
|
||||
});
|
||||
|
||||
if (!enabled) 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;
|
||||
}
|
||||
|
||||
this.torches.forEach(torch => {
|
||||
if (!torch.light.visible) torch.light.visible = true;
|
||||
if (!torch.particles.visible) torch.particles.visible = true;
|
||||
|
||||
let measurePulse = 0;
|
||||
if (state.music) {
|
||||
measurePulse = state.music.measurePulse * 2.0; // Make flames jump higher
|
||||
@ -156,8 +174,14 @@ export class StageTorches extends SceneFeature {
|
||||
|
||||
onPartyStart() {
|
||||
this.torches.forEach(torch => {
|
||||
torch.group.visible = true;
|
||||
this.resetParticles(torch);
|
||||
if (state.config.torchesEnabled) {
|
||||
torch.group.visible = true;
|
||||
torch.light.visible = true;
|
||||
torch.particles.visible = true;
|
||||
this.resetParticles(torch);
|
||||
} else {
|
||||
torch.group.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,12 @@ export function initState() {
|
||||
lasersEnabled: true,
|
||||
sideScreensEnabled: true,
|
||||
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'
|
||||
};
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user