Feature: Config UI with persistent storage to tweak the visualizer
This commit is contained in:
parent
8ed8ea9d34
commit
e7931174de
51
party-stage/src/core/media-storage.js
Normal file
51
party-stage/src/core/media-storage.js
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
const DB_NAME = 'PartyMediaDB';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
|
||||||
|
export const MediaStorage = {
|
||||||
|
open: () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
|
request.onupgradeneeded = (e) => {
|
||||||
|
const db = e.target.result;
|
||||||
|
if (!db.objectStoreNames.contains('music')) db.createObjectStore('music');
|
||||||
|
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
||||||
|
};
|
||||||
|
request.onsuccess = (e) => resolve(e.target.result);
|
||||||
|
request.onerror = (e) => reject(e);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveMusic: async (file) => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
const tx = db.transaction('music', 'readwrite');
|
||||||
|
tx.objectStore('music').put(file, 'currentSong');
|
||||||
|
},
|
||||||
|
getMusic: async () => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = db.transaction('music', 'readonly').objectStore('music').get('currentSong');
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => resolve(null);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
saveTapes: async (files) => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
const tx = db.transaction('tapes', 'readwrite');
|
||||||
|
const store = tx.objectStore('tapes');
|
||||||
|
store.clear();
|
||||||
|
Array.from(files).forEach((file, i) => store.put(file, i));
|
||||||
|
},
|
||||||
|
getTapes: async () => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = db.transaction('tapes', 'readonly').objectStore('tapes').getAll();
|
||||||
|
req.onsuccess = () => resolve(req.result);
|
||||||
|
req.onerror = () => resolve([]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clear: async () => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
const tx = db.transaction(['music', 'tapes'], 'readwrite');
|
||||||
|
tx.objectStore('music').clear();
|
||||||
|
tx.objectStore('tapes').clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js';
|
import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js';
|
||||||
import sceneFeatureManager from '../scene/SceneFeatureManager.js';
|
import sceneFeatureManager from '../scene/SceneFeatureManager.js';
|
||||||
|
import { MediaStorage } from './media-storage.js';
|
||||||
|
|
||||||
// Register a feature to handle party start
|
// Register a feature to handle party start
|
||||||
sceneFeatureManager.register({
|
sceneFeatureManager.register({
|
||||||
@ -159,6 +160,13 @@ export function initVideoUI() {
|
|||||||
state.loadTapeButton = btn;
|
state.loadTapeButton = btn;
|
||||||
}
|
}
|
||||||
state.loadTapeButton.onclick = () => state.fileInput.click();
|
state.loadTapeButton.onclick = () => state.fileInput.click();
|
||||||
|
|
||||||
|
// Restore tapes from storage
|
||||||
|
MediaStorage.getTapes().then(files => {
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
processVideoFiles(files);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Video Loading Logic (handles multiple files) ---
|
// --- Video Loading Logic (handles multiple files) ---
|
||||||
@ -169,15 +177,22 @@ export function loadVideoFile(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processVideoFiles(files);
|
||||||
|
MediaStorage.saveTapes(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
function processVideoFiles(files) {
|
||||||
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks
|
// 1. Clear previous URLs and revoke object URLs to prevent memory leaks
|
||||||
state.videoUrls.forEach(url => URL.revokeObjectURL(url));
|
state.videoUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
state.videoUrls = [];
|
state.videoUrls = [];
|
||||||
|
state.videoFilenames = [];
|
||||||
|
|
||||||
// 2. Populate the new videoUrls array
|
// 2. Populate the new videoUrls array
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
if (file.type.startsWith('video/')) {
|
if (file.type.startsWith('video/')) {
|
||||||
state.videoUrls.push(URL.createObjectURL(file));
|
state.videoUrls.push(URL.createObjectURL(file));
|
||||||
|
state.videoFilenames.push(file.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -186,6 +201,10 @@ export function loadVideoFile(event) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Config UI
|
||||||
|
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
||||||
|
if (configUI) configUI.updateStatus();
|
||||||
|
|
||||||
// 3. Start playback logic
|
// 3. Start playback logic
|
||||||
console.info(`Loaded ${state.videoUrls.length} tapes.`);
|
console.info(`Loaded ${state.videoUrls.length} tapes.`);
|
||||||
|
|
||||||
|
|||||||
318
party-stage/src/scene/config-ui.js
Normal file
318
party-stage/src/scene/config-ui.js
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import { state } from '../state.js';
|
||||||
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
|
import { showStandbyScreen } from './projection-screen.js';
|
||||||
|
|
||||||
|
export class ConfigUI extends SceneFeature {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
sceneFeatureManager.register(this);
|
||||||
|
this.toggles = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.id = 'config-ui';
|
||||||
|
Object.assign(container.style, {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '70px',
|
||||||
|
left: '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'
|
||||||
|
});
|
||||||
|
|
||||||
|
const saveConfig = () => {
|
||||||
|
localStorage.setItem('partyConfig', JSON.stringify(state.config));
|
||||||
|
};
|
||||||
|
|
||||||
|
const createToggle = (label, configKey, onChange) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.style.display = 'flex';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.justifyContent = 'space-between';
|
||||||
|
|
||||||
|
const lbl = document.createElement('label');
|
||||||
|
lbl.innerText = label;
|
||||||
|
|
||||||
|
const chk = document.createElement('input');
|
||||||
|
chk.type = 'checkbox';
|
||||||
|
chk.checked = state.config[configKey];
|
||||||
|
chk.style.cursor = 'pointer';
|
||||||
|
chk.onchange = (e) => {
|
||||||
|
state.config[configKey] = e.target.checked;
|
||||||
|
saveConfig();
|
||||||
|
if (onChange) onChange(e.target.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggles[configKey] = { checkbox: chk, callback: onChange };
|
||||||
|
row.appendChild(lbl);
|
||||||
|
row.appendChild(chk);
|
||||||
|
container.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;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Lasers Toggle
|
||||||
|
createToggle('Lasers', 'lasersEnabled');
|
||||||
|
|
||||||
|
// Side Screens Toggle
|
||||||
|
createToggle('Side Screens', 'sideScreensEnabled');
|
||||||
|
|
||||||
|
// Console RGB Toggle
|
||||||
|
createToggle('Console RGB Panel', 'consoleRGBEnabled');
|
||||||
|
|
||||||
|
// DJ Hat Selector
|
||||||
|
const hatRow = document.createElement('div');
|
||||||
|
hatRow.style.display = 'flex';
|
||||||
|
hatRow.style.alignItems = 'center';
|
||||||
|
hatRow.style.justifyContent = 'space-between';
|
||||||
|
|
||||||
|
const hatLabel = document.createElement('label');
|
||||||
|
hatLabel.innerText = 'DJ Hat';
|
||||||
|
|
||||||
|
const hatSelect = document.createElement('select');
|
||||||
|
['None', 'Santa', 'Top Hat'].forEach(opt => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = opt;
|
||||||
|
option.innerText = opt;
|
||||||
|
if (opt === state.config.djHat) option.selected = true;
|
||||||
|
hatSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
hatSelect.onchange = (e) => {
|
||||||
|
state.config.djHat = e.target.value;
|
||||||
|
saveConfig();
|
||||||
|
};
|
||||||
|
this.hatSelect = hatSelect;
|
||||||
|
|
||||||
|
hatRow.appendChild(hatLabel);
|
||||||
|
hatRow.appendChild(hatSelect);
|
||||||
|
container.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'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Loaded Song Label
|
||||||
|
this.songLabel = document.createElement('div');
|
||||||
|
this.songLabel.innerText = 'Song: (None)';
|
||||||
|
this.songLabel.style.fontSize = '13px';
|
||||||
|
this.songLabel.style.color = '#aaa';
|
||||||
|
statusContainer.appendChild(this.songLabel);
|
||||||
|
|
||||||
|
// Loaded Tapes List
|
||||||
|
const tapesLabel = document.createElement('div');
|
||||||
|
tapesLabel.innerText = 'Loaded Tapes:';
|
||||||
|
tapesLabel.style.fontSize = '13px';
|
||||||
|
statusContainer.appendChild(tapesLabel);
|
||||||
|
|
||||||
|
this.tapeList = document.createElement('ul');
|
||||||
|
Object.assign(this.tapeList.style, {
|
||||||
|
margin: '0',
|
||||||
|
paddingLeft: '20px',
|
||||||
|
fontSize: '12px',
|
||||||
|
color: '#aaa',
|
||||||
|
maxHeight: '100px',
|
||||||
|
overflowY: 'auto'
|
||||||
|
});
|
||||||
|
statusContainer.appendChild(this.tapeList);
|
||||||
|
|
||||||
|
// Load Tapes Button
|
||||||
|
const loadTapesBtn = document.createElement('button');
|
||||||
|
loadTapesBtn.id = 'loadTapeButton';
|
||||||
|
loadTapesBtn.innerText = 'Load Tapes';
|
||||||
|
Object.assign(loadTapesBtn.style, {
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: '#555',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px'
|
||||||
|
});
|
||||||
|
statusContainer.appendChild(loadTapesBtn);
|
||||||
|
state.loadTapeButton = loadTapesBtn;
|
||||||
|
|
||||||
|
// Choose Song Button
|
||||||
|
const chooseSongBtn = document.createElement('button');
|
||||||
|
chooseSongBtn.innerText = 'Choose Song';
|
||||||
|
Object.assign(chooseSongBtn.style, {
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: '#555',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px'
|
||||||
|
});
|
||||||
|
chooseSongBtn.onclick = () => {
|
||||||
|
const fileInput = document.getElementById('musicFileInput');
|
||||||
|
if (fileInput) fileInput.click();
|
||||||
|
};
|
||||||
|
statusContainer.appendChild(chooseSongBtn);
|
||||||
|
|
||||||
|
// Start Party Button
|
||||||
|
this.startButton = document.createElement('button');
|
||||||
|
this.startButton.innerText = 'Start the Party';
|
||||||
|
this.startButton.disabled = true;
|
||||||
|
Object.assign(this.startButton.style, {
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '10px',
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
backgroundColor: '#333',
|
||||||
|
color: '#777',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontWeight: 'bold'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.startButton.onclick = () => {
|
||||||
|
const musicPlayer = sceneFeatureManager.features.find(f => f.constructor.name === 'MusicPlayer');
|
||||||
|
if (musicPlayer) musicPlayer.startSequence();
|
||||||
|
};
|
||||||
|
statusContainer.appendChild(this.startButton);
|
||||||
|
container.appendChild(statusContainer);
|
||||||
|
|
||||||
|
// Reset Button
|
||||||
|
const resetBtn = document.createElement('button');
|
||||||
|
resetBtn.innerText = 'Reset Defaults';
|
||||||
|
Object.assign(resetBtn.style, {
|
||||||
|
marginTop: '10px',
|
||||||
|
padding: '8px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
backgroundColor: '#555',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '4px',
|
||||||
|
fontSize: '14px'
|
||||||
|
});
|
||||||
|
resetBtn.onclick = () => {
|
||||||
|
localStorage.removeItem('partyConfig');
|
||||||
|
MediaStorage.clear();
|
||||||
|
|
||||||
|
// Clear State - Video
|
||||||
|
if (state.videoUrls) {
|
||||||
|
state.videoUrls.forEach(url => URL.revokeObjectURL(url));
|
||||||
|
}
|
||||||
|
state.videoUrls = [];
|
||||||
|
state.videoFilenames = [];
|
||||||
|
state.isVideoLoaded = false;
|
||||||
|
state.currentVideoIndex = -1;
|
||||||
|
if (state.loadTapeButton) {
|
||||||
|
state.loadTapeButton.innerText = 'Load Tapes';
|
||||||
|
state.loadTapeButton.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear State - Music
|
||||||
|
if (state.music) {
|
||||||
|
state.music.songTitle = null;
|
||||||
|
if (state.music.player) {
|
||||||
|
state.music.player.pause();
|
||||||
|
state.music.player.removeAttribute('src');
|
||||||
|
state.music.player.load();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showStandbyScreen();
|
||||||
|
|
||||||
|
const defaults = {
|
||||||
|
torchesEnabled: true,
|
||||||
|
lasersEnabled: true,
|
||||||
|
sideScreensEnabled: true,
|
||||||
|
consoleRGBEnabled: true,
|
||||||
|
djHat: 'None'
|
||||||
|
};
|
||||||
|
for (const key in defaults) {
|
||||||
|
state.config[key] = defaults[key];
|
||||||
|
if (this.toggles[key]) {
|
||||||
|
this.toggles[key].checkbox.checked = defaults[key];
|
||||||
|
if (this.toggles[key].callback) this.toggles[key].callback(defaults[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.hatSelect) this.hatSelect.value = defaults.djHat;
|
||||||
|
this.updateStatus();
|
||||||
|
};
|
||||||
|
container.appendChild(resetBtn);
|
||||||
|
|
||||||
|
document.body.appendChild(container);
|
||||||
|
this.container = container;
|
||||||
|
this.updateStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus() {
|
||||||
|
if (!this.songLabel) return;
|
||||||
|
|
||||||
|
// 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.color = 'white';
|
||||||
|
this.startButton.style.cursor = 'pointer';
|
||||||
|
} 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.cursor = 'not-allowed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Tape List
|
||||||
|
this.tapeList.innerHTML = '';
|
||||||
|
if (state.videoUrls && state.videoUrls.length > 0) {
|
||||||
|
state.videoUrls.forEach((url, index) => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
let name = `Tape ${index + 1}`;
|
||||||
|
if (state.videoFilenames && state.videoFilenames[index]) {
|
||||||
|
name = state.videoFilenames[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name.length > 25) {
|
||||||
|
li.innerText = name.substring(0, 22) + '...';
|
||||||
|
li.title = name;
|
||||||
|
} else {
|
||||||
|
li.innerText = name;
|
||||||
|
}
|
||||||
|
this.tapeList.appendChild(li);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.innerText = '(No tapes loaded)';
|
||||||
|
this.tapeList.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPartyStart() {
|
||||||
|
if (this.container) this.container.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
onPartyEnd() {
|
||||||
|
if (this.container) this.container.style.display = 'flex';
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -113,6 +113,35 @@ export class DJ extends SceneFeature {
|
|||||||
this.currentRightAngle = Math.PI * 0.85;
|
this.currentRightAngle = Math.PI * 0.85;
|
||||||
this.currentLeftAngleX = 0;
|
this.currentLeftAngleX = 0;
|
||||||
this.currentRightAngleX = 0;
|
this.currentRightAngleX = 0;
|
||||||
|
|
||||||
|
// --- Hats ---
|
||||||
|
this.hats = {};
|
||||||
|
|
||||||
|
// Santa Hat
|
||||||
|
const santaGroup = new THREE.Group();
|
||||||
|
const santaCone = new THREE.Mesh(new THREE.ConeGeometry(0.14, 0.3, 16), new THREE.MeshStandardMaterial({ color: 0xff0000 }));
|
||||||
|
santaCone.position.y = 0.15;
|
||||||
|
santaCone.rotation.x = -0.2;
|
||||||
|
const santaRim = new THREE.Mesh(new THREE.TorusGeometry(0.14, 0.04, 8, 16), new THREE.MeshStandardMaterial({ color: 0xffffff }));
|
||||||
|
santaRim.rotation.x = Math.PI/2;
|
||||||
|
const santaBall = new THREE.Mesh(new THREE.SphereGeometry(0.04), new THREE.MeshStandardMaterial({ color: 0xffffff }));
|
||||||
|
santaBall.position.set(0, 0.28, -0.08);
|
||||||
|
santaGroup.add(santaCone, santaRim, santaBall);
|
||||||
|
santaGroup.position.set(0, 0.22, 0);
|
||||||
|
santaGroup.visible = false;
|
||||||
|
this.head.add(santaGroup);
|
||||||
|
this.hats['Santa'] = santaGroup;
|
||||||
|
|
||||||
|
// Top Hat
|
||||||
|
const topHatGroup = new THREE.Group();
|
||||||
|
const brim = new THREE.Mesh(new THREE.CylinderGeometry(0.25, 0.25, 0.02, 16), new THREE.MeshStandardMaterial({ color: 0x111111 }));
|
||||||
|
const cylinder = new THREE.Mesh(new THREE.CylinderGeometry(0.15, 0.15, 0.25, 16), new THREE.MeshStandardMaterial({ color: 0x111111 }));
|
||||||
|
cylinder.position.y = 0.125;
|
||||||
|
topHatGroup.add(brim, cylinder);
|
||||||
|
topHatGroup.position.set(0, 0.22, 0);
|
||||||
|
topHatGroup.visible = false;
|
||||||
|
this.head.add(topHatGroup);
|
||||||
|
this.hats['Top Hat'] = topHatGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
@ -198,6 +227,11 @@ export class DJ extends SceneFeature {
|
|||||||
this.group.position.x += dir * speed * deltaTime;
|
this.group.position.x += dir * speed * deltaTime;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update Hat Visibility
|
||||||
|
for (const [name, mesh] of Object.entries(this.hats)) {
|
||||||
|
mesh.visible = (state.config.djHat === name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onPartyStart() {
|
onPartyStart() {
|
||||||
|
|||||||
@ -205,6 +205,11 @@ export class MusicConsole extends SceneFeature {
|
|||||||
|
|
||||||
// Update Front LED Array
|
// Update Front LED Array
|
||||||
if (this.frontLedMesh) {
|
if (this.frontLedMesh) {
|
||||||
|
if (!state.config.consoleRGBEnabled) {
|
||||||
|
this.frontLedMesh.visible = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.frontLedMesh.visible = true;
|
||||||
const color = new THREE.Color();
|
const color = new THREE.Color();
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { state } from '../state.js';
|
|||||||
import { SceneFeature } from './SceneFeature.js';
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
import { showStandbyScreen } from './projection-screen.js';
|
import { showStandbyScreen } from './projection-screen.js';
|
||||||
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
|
|
||||||
export class MusicPlayer extends SceneFeature {
|
export class MusicPlayer extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -21,54 +22,73 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
|
|
||||||
const loadButton = document.getElementById('loadMusicButton');
|
const loadButton = document.getElementById('loadMusicButton');
|
||||||
const fileInput = document.getElementById('musicFileInput');
|
const fileInput = document.getElementById('musicFileInput');
|
||||||
const uiContainer = document.getElementById('ui-container');
|
|
||||||
const metadataContainer = document.getElementById('metadata-container');
|
|
||||||
const songTitleElement = document.getElementById('song-title');
|
|
||||||
|
|
||||||
loadButton.addEventListener('click', () => {
|
// Hide the big start button as we use ConfigUI now
|
||||||
fileInput.click();
|
if (loadButton) loadButton.style.display = 'none';
|
||||||
});
|
|
||||||
|
|
||||||
fileInput.addEventListener('change', (event) => {
|
fileInput.addEventListener('change', (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
// Setup Web Audio API if not already done
|
this.loadMusicFile(file);
|
||||||
if (!this.audioContext) {
|
MediaStorage.saveMusic(file);
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
||||||
this.analyser = this.audioContext.createAnalyser();
|
|
||||||
this.analyser.fftSize = 128; // Lower resolution is fine for loudness
|
|
||||||
this.source = this.audioContext.createMediaElementSource(state.music.player);
|
|
||||||
this.source.connect(this.analyser);
|
|
||||||
this.analyser.connect(this.audioContext.destination);
|
|
||||||
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide the main button
|
|
||||||
loadButton.style.display = 'none';
|
|
||||||
|
|
||||||
// Show metadata
|
|
||||||
const songName = file.name.replace(/\.[^/.]+$/, "");
|
|
||||||
songTitleElement.textContent = songName; // Show filename without extension
|
|
||||||
state.music.songTitle = songName;
|
|
||||||
|
|
||||||
showStandbyScreen();
|
|
||||||
metadataContainer.classList.add('hidden');
|
|
||||||
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
state.music.player.src = url;
|
|
||||||
|
|
||||||
// Wait 5 seconds, then start the party
|
|
||||||
setTimeout(() => {
|
|
||||||
metadataContainer.classList.add('hidden');
|
|
||||||
this.startParty();
|
|
||||||
}, 5000);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
state.music.player.addEventListener('ended', () => {
|
state.music.player.addEventListener('ended', () => {
|
||||||
this.stopParty();
|
this.stopParty();
|
||||||
uiContainer.style.display = 'flex'; // Show the button again
|
const uiContainer = document.getElementById('ui-container');
|
||||||
|
if (uiContainer) uiContainer.style.display = 'flex'; // Show the button again
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Restore from storage
|
||||||
|
MediaStorage.getMusic().then(file => {
|
||||||
|
if (file) {
|
||||||
|
this.loadMusicFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMusicFile(file) {
|
||||||
|
// Setup Web Audio API if not already done
|
||||||
|
if (!this.audioContext) {
|
||||||
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
this.analyser = this.audioContext.createAnalyser();
|
||||||
|
this.analyser.fftSize = 128; // Lower resolution is fine for loudness
|
||||||
|
this.source = this.audioContext.createMediaElementSource(state.music.player);
|
||||||
|
this.source.connect(this.analyser);
|
||||||
|
this.analyser.connect(this.audioContext.destination);
|
||||||
|
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update State
|
||||||
|
const songName = file.name.replace(/\.[^/.]+$/, "");
|
||||||
|
state.music.songTitle = songName;
|
||||||
|
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
state.music.player.src = url;
|
||||||
|
|
||||||
|
// Update Config UI
|
||||||
|
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
||||||
|
if (configUI) configUI.updateStatus();
|
||||||
|
|
||||||
|
// Update Projection Screen with new song title
|
||||||
|
showStandbyScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
startSequence() {
|
||||||
|
const uiContainer = document.getElementById('ui-container');
|
||||||
|
const configUI = document.getElementById('config-ui');
|
||||||
|
|
||||||
|
if (uiContainer) uiContainer.style.display = 'none';
|
||||||
|
if (configUI) configUI.style.display = 'none';
|
||||||
|
if (state.loadTapeButton) state.loadTapeButton.classList.add('hidden');
|
||||||
|
|
||||||
|
showStandbyScreen();
|
||||||
|
|
||||||
|
// Wait 5 seconds, then start the party
|
||||||
|
setTimeout(() => {
|
||||||
|
this.startParty();
|
||||||
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
startParty() {
|
startParty() {
|
||||||
@ -87,11 +107,6 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
stopParty() {
|
stopParty() {
|
||||||
state.clock.stop();
|
state.clock.stop();
|
||||||
state.partyStarted = false;
|
state.partyStarted = false;
|
||||||
setTimeout(() => {
|
|
||||||
const startButton = document.getElementById('loadMusicButton');
|
|
||||||
startButton.style.display = 'block';
|
|
||||||
startButton.textContent = "Party some more?"
|
|
||||||
}, 5000);
|
|
||||||
// Trigger 'end' event for other features
|
// Trigger 'end' event for other features
|
||||||
this.notifyFeatures('onPartyEnd');
|
this.notifyFeatures('onPartyEnd');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,6 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
createSideScreen(11, 4.0, -15.5, -0.4, 100); // Right (Lower resolution)
|
createSideScreen(11, 4.0, -15.5, -0.4, 100); // Right (Lower resolution)
|
||||||
|
|
||||||
state.tvScreen = this.mesh;
|
state.tvScreen = this.mesh;
|
||||||
this.setAllVisible(false);
|
|
||||||
|
|
||||||
// --- Screen Light ---
|
// --- Screen Light ---
|
||||||
// A light that projects the screen's color/ambiance into the room
|
// A light that projects the screen's color/ambiance into the room
|
||||||
@ -184,10 +183,18 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
state.screenLight.shadow.mapSize.width = 512;
|
state.screenLight.shadow.mapSize.width = 512;
|
||||||
state.screenLight.shadow.mapSize.height = 512;
|
state.screenLight.shadow.mapSize.height = 512;
|
||||||
state.scene.add(state.screenLight);
|
state.scene.add(state.screenLight);
|
||||||
|
|
||||||
|
showStandbyScreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
setAllVisible(visible) {
|
setAllVisible(visible) {
|
||||||
this.screens.forEach(s => s.mesh.visible = visible);
|
this.screens.forEach((s, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
s.mesh.visible = visible;
|
||||||
|
} else {
|
||||||
|
s.mesh.visible = visible && state.config.sideScreensEnabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMaterialToAll(baseMaterial) {
|
applyMaterialToAll(baseMaterial) {
|
||||||
|
|||||||
@ -18,10 +18,12 @@ import { MusicConsole } from './music-console.js';
|
|||||||
import { DJ } from './dj.js';
|
import { DJ } from './dj.js';
|
||||||
import { ProjectionScreen } from './projection-screen.js';
|
import { ProjectionScreen } from './projection-screen.js';
|
||||||
import { StageLasers } from './stage-lasers.js';
|
import { StageLasers } from './stage-lasers.js';
|
||||||
|
import { ConfigUI } from './config-ui.js';
|
||||||
// Scene Features ^^^
|
// Scene Features ^^^
|
||||||
|
|
||||||
// --- Scene Modeling Function ---
|
// --- Scene Modeling Function ---
|
||||||
export function createSceneObjects() {
|
export function createSceneObjects() {
|
||||||
|
new ConfigUI();
|
||||||
sceneFeatureManager.init();
|
sceneFeatureManager.init();
|
||||||
initVideoUI();
|
initVideoUI();
|
||||||
|
|
||||||
|
|||||||
@ -107,6 +107,12 @@ export class StageLasers extends SceneFeature {
|
|||||||
|
|
||||||
const time = state.clock.getElapsedTime();
|
const time = state.clock.getElapsedTime();
|
||||||
|
|
||||||
|
if (!state.config.lasersEnabled) {
|
||||||
|
this.lasers.forEach(l => l.mesh.visible = false);
|
||||||
|
if (state.laserData) state.laserData.count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Loudness Check ---
|
// --- Loudness Check ---
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
if (state.music) {
|
if (state.music) {
|
||||||
|
|||||||
@ -3,6 +3,18 @@ import * as THREE from 'three';
|
|||||||
export let state = undefined;
|
export let state = undefined;
|
||||||
|
|
||||||
export function initState() {
|
export function initState() {
|
||||||
|
let config = {
|
||||||
|
torchesEnabled: true,
|
||||||
|
lasersEnabled: true,
|
||||||
|
sideScreensEnabled: true,
|
||||||
|
consoleRGBEnabled: true,
|
||||||
|
djHat: 'None' // 'None', 'Santa', 'Top Hat'
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const saved = localStorage.getItem('partyConfig');
|
||||||
|
if (saved) config = { ...config, ...JSON.parse(saved) };
|
||||||
|
} catch (e) { console.warn('Error loading config', e); }
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
// Core Three.js components
|
// Core Three.js components
|
||||||
scene: null,
|
scene: null,
|
||||||
@ -30,6 +42,7 @@ export function initState() {
|
|||||||
// Video Playback
|
// Video Playback
|
||||||
isVideoLoaded: false,
|
isVideoLoaded: false,
|
||||||
videoUrls: [],
|
videoUrls: [],
|
||||||
|
videoFilenames: [],
|
||||||
currentVideoIndex: -1,
|
currentVideoIndex: -1,
|
||||||
|
|
||||||
// Scene constants
|
// Scene constants
|
||||||
@ -42,6 +55,9 @@ export function initState() {
|
|||||||
debugCamera: false, // Turn on camera helpers
|
debugCamera: false, // Turn on camera helpers
|
||||||
partyStarted: false,
|
partyStarted: false,
|
||||||
|
|
||||||
|
// Feature Configuration
|
||||||
|
config: config,
|
||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
container: document.body,
|
container: document.body,
|
||||||
videoElement: document.getElementById('video'),
|
videoElement: document.getElementById('video'),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user