From e7931174deec95b2a6d2f38336c3aecbc4b63406 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sat, 3 Jan 2026 22:51:13 +0000 Subject: [PATCH] Feature: Config UI with persistent storage to tweak the visualizer --- party-stage/src/core/media-storage.js | 51 ++++ party-stage/src/core/video-player.js | 19 ++ party-stage/src/scene/config-ui.js | 318 +++++++++++++++++++++ party-stage/src/scene/dj.js | 34 +++ party-stage/src/scene/music-console.js | 5 + party-stage/src/scene/music-player.js | 99 ++++--- party-stage/src/scene/projection-screen.js | 11 +- party-stage/src/scene/root.js | 2 + party-stage/src/scene/stage-lasers.js | 6 + party-stage/src/state.js | 16 ++ 10 files changed, 517 insertions(+), 44 deletions(-) create mode 100644 party-stage/src/core/media-storage.js create mode 100644 party-stage/src/scene/config-ui.js diff --git a/party-stage/src/core/media-storage.js b/party-stage/src/core/media-storage.js new file mode 100644 index 0000000..e37d357 --- /dev/null +++ b/party-stage/src/core/media-storage.js @@ -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(); + } +}; \ No newline at end of file diff --git a/party-stage/src/core/video-player.js b/party-stage/src/core/video-player.js index ecefb8e..5fc9f5c 100644 --- a/party-stage/src/core/video-player.js +++ b/party-stage/src/core/video-player.js @@ -2,6 +2,7 @@ import * as THREE from 'three'; import { state } from '../state.js'; import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js'; import sceneFeatureManager from '../scene/SceneFeatureManager.js'; +import { MediaStorage } from './media-storage.js'; // Register a feature to handle party start sceneFeatureManager.register({ @@ -159,6 +160,13 @@ export function initVideoUI() { state.loadTapeButton = btn; } 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) --- @@ -169,15 +177,22 @@ export function loadVideoFile(event) { return; } + processVideoFiles(files); + MediaStorage.saveTapes(files); +} + +function processVideoFiles(files) { // 1. Clear previous URLs and revoke object URLs to prevent memory leaks state.videoUrls.forEach(url => URL.revokeObjectURL(url)); state.videoUrls = []; + state.videoFilenames = []; // 2. Populate the new videoUrls array for (let i = 0; i < files.length; i++) { const file = files[i]; if (file.type.startsWith('video/')) { state.videoUrls.push(URL.createObjectURL(file)); + state.videoFilenames.push(file.name); } } @@ -186,6 +201,10 @@ export function loadVideoFile(event) { return; } + // Update Config UI + const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI'); + if (configUI) configUI.updateStatus(); + // 3. Start playback logic console.info(`Loaded ${state.videoUrls.length} tapes.`); diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js new file mode 100644 index 0000000..280f334 --- /dev/null +++ b/party-stage/src/scene/config-ui.js @@ -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'; + } +} \ No newline at end of file diff --git a/party-stage/src/scene/dj.js b/party-stage/src/scene/dj.js index 53d1a9d..1212603 100644 --- a/party-stage/src/scene/dj.js +++ b/party-stage/src/scene/dj.js @@ -113,6 +113,35 @@ export class DJ extends SceneFeature { this.currentRightAngle = Math.PI * 0.85; this.currentLeftAngleX = 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) { @@ -198,6 +227,11 @@ export class DJ extends SceneFeature { 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() { diff --git a/party-stage/src/scene/music-console.js b/party-stage/src/scene/music-console.js index f4067e3..0b8c2e7 100644 --- a/party-stage/src/scene/music-console.js +++ b/party-stage/src/scene/music-console.js @@ -205,6 +205,11 @@ export class MusicConsole extends SceneFeature { // Update Front LED Array if (this.frontLedMesh) { + if (!state.config.consoleRGBEnabled) { + this.frontLedMesh.visible = false; + return; + } + this.frontLedMesh.visible = true; const color = new THREE.Color(); let idx = 0; diff --git a/party-stage/src/scene/music-player.js b/party-stage/src/scene/music-player.js index d042d58..0286d13 100644 --- a/party-stage/src/scene/music-player.js +++ b/party-stage/src/scene/music-player.js @@ -2,6 +2,7 @@ import { state } from '../state.js'; import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; import { showStandbyScreen } from './projection-screen.js'; +import { MediaStorage } from '../core/media-storage.js'; export class MusicPlayer extends SceneFeature { constructor() { @@ -21,54 +22,73 @@ export class MusicPlayer extends SceneFeature { const loadButton = document.getElementById('loadMusicButton'); 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', () => { - fileInput.click(); - }); + // Hide the big start button as we use ConfigUI now + if (loadButton) loadButton.style.display = 'none'; fileInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (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); - } - - // 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); + this.loadMusicFile(file); + MediaStorage.saveMusic(file); } }); state.music.player.addEventListener('ended', () => { 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() { @@ -87,11 +107,6 @@ export class MusicPlayer extends SceneFeature { stopParty() { state.clock.stop(); 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 this.notifyFeatures('onPartyEnd'); } diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index dd1a6e7..6fe5277 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -174,7 +174,6 @@ export class ProjectionScreen extends SceneFeature { createSideScreen(11, 4.0, -15.5, -0.4, 100); // Right (Lower resolution) state.tvScreen = this.mesh; - this.setAllVisible(false); // --- Screen Light --- // 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.height = 512; state.scene.add(state.screenLight); + + showStandbyScreen(); } 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) { diff --git a/party-stage/src/scene/root.js b/party-stage/src/scene/root.js index aa8554c..6044c88 100644 --- a/party-stage/src/scene/root.js +++ b/party-stage/src/scene/root.js @@ -18,10 +18,12 @@ import { MusicConsole } from './music-console.js'; import { DJ } from './dj.js'; import { ProjectionScreen } from './projection-screen.js'; import { StageLasers } from './stage-lasers.js'; +import { ConfigUI } from './config-ui.js'; // Scene Features ^^^ // --- Scene Modeling Function --- export function createSceneObjects() { + new ConfigUI(); sceneFeatureManager.init(); initVideoUI(); diff --git a/party-stage/src/scene/stage-lasers.js b/party-stage/src/scene/stage-lasers.js index 422235f..0d6dbc7 100644 --- a/party-stage/src/scene/stage-lasers.js +++ b/party-stage/src/scene/stage-lasers.js @@ -107,6 +107,12 @@ export class StageLasers extends SceneFeature { 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 --- let isActive = false; if (state.music) { diff --git a/party-stage/src/state.js b/party-stage/src/state.js index abacc87..b4d4b08 100644 --- a/party-stage/src/state.js +++ b/party-stage/src/state.js @@ -3,6 +3,18 @@ import * as THREE from 'three'; export let state = undefined; 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 = { // Core Three.js components scene: null, @@ -30,6 +42,7 @@ export function initState() { // Video Playback isVideoLoaded: false, videoUrls: [], + videoFilenames: [], currentVideoIndex: -1, // Scene constants @@ -42,6 +55,9 @@ export function initState() { debugCamera: false, // Turn on camera helpers partyStarted: false, + // Feature Configuration + config: config, + // DOM Elements container: document.body, videoElement: document.getElementById('video'),