Compare commits
7 Commits
3c93956ff8
...
d00cf96ed7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d00cf96ed7 | ||
|
|
862acfd07f | ||
|
|
c56e07b083 | ||
|
|
eef13e301c | ||
|
|
cf6dda2d35 | ||
|
|
89dc5db53c | ||
|
|
48fa197b69 |
@ -1,8 +1,10 @@
|
|||||||
const DB_NAME = 'PartyMediaDB';
|
const DB_NAME = 'PartyMediaDB';
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 2;
|
||||||
|
let dbInstance = null;
|
||||||
|
|
||||||
export const MediaStorage = {
|
export const MediaStorage = {
|
||||||
open: () => {
|
open: () => {
|
||||||
|
if (dbInstance) return Promise.resolve(dbInstance);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
request.onupgradeneeded = (e) => {
|
request.onupgradeneeded = (e) => {
|
||||||
@ -11,14 +13,23 @@ export const MediaStorage = {
|
|||||||
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
||||||
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
|
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
|
||||||
};
|
};
|
||||||
request.onsuccess = (e) => resolve(e.target.result);
|
request.onsuccess = (e) => {
|
||||||
|
dbInstance = e.target.result;
|
||||||
|
dbInstance.onclose = () => { dbInstance = null; };
|
||||||
|
dbInstance.onversionchange = () => { if (dbInstance) dbInstance.close(); dbInstance = null; };
|
||||||
|
resolve(dbInstance);
|
||||||
|
};
|
||||||
request.onerror = (e) => reject(e);
|
request.onerror = (e) => reject(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveMusic: async (file) => {
|
saveMusic: async (file) => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
const tx = db.transaction('music', 'readwrite');
|
return new Promise((resolve, reject) => {
|
||||||
tx.objectStore('music').put(file, 'currentSong');
|
const tx = db.transaction('music', 'readwrite');
|
||||||
|
tx.oncomplete = () => resolve();
|
||||||
|
tx.onerror = (e) => reject(e.target.error);
|
||||||
|
tx.objectStore('music').put(file, 'currentSong');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getMusic: async () => {
|
getMusic: async () => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
@ -56,6 +67,11 @@ export const MediaStorage = {
|
|||||||
req.onerror = () => resolve(null);
|
req.onerror = () => resolve(null);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deletePoster: async () => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
const tx = db.transaction('poster', 'readwrite');
|
||||||
|
tx.objectStore('poster').delete('currentPoster');
|
||||||
|
},
|
||||||
clear: async () => {
|
clear: async () => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite');
|
const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite');
|
||||||
|
|||||||
34
party-stage/src/core/ui-utils.js
Normal file
34
party-stage/src/core/ui-utils.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.innerText = message;
|
||||||
|
let bg = '#333';
|
||||||
|
if (type === 'error') bg = '#dc3545';
|
||||||
|
else if (type === 'success') bg = '#28a745';
|
||||||
|
else if (type === 'warning') bg = '#ff9800';
|
||||||
|
|
||||||
|
Object.assign(toast.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '30px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
backgroundColor: bg,
|
||||||
|
color: 'white',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
zIndex: '10000',
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
boxShadow: '0 2px 10px rgba(0,0,0,0.5)',
|
||||||
|
opacity: '0',
|
||||||
|
transition: 'opacity 0.5s'
|
||||||
|
});
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => { toast.style.opacity = '1'; });
|
||||||
|
|
||||||
|
const duration = type === 'success' ? 1000 : 3000;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 500);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ 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';
|
import { MediaStorage } from './media-storage.js';
|
||||||
|
import { showToast } from './ui-utils.js';
|
||||||
|
|
||||||
// Register a feature to handle party start
|
// Register a feature to handle party start
|
||||||
sceneFeatureManager.register({
|
sceneFeatureManager.register({
|
||||||
@ -178,6 +179,9 @@ export function loadVideoFile(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processVideoFiles(files);
|
processVideoFiles(files);
|
||||||
|
if (state.videoUrls.length > 0) {
|
||||||
|
showToast(`Loaded ${state.videoUrls.length} tapes`, 'success');
|
||||||
|
}
|
||||||
MediaStorage.saveTapes(files);
|
MediaStorage.saveTapes(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +194,7 @@ function processVideoFiles(files) {
|
|||||||
// 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/') && file.size > 0) {
|
||||||
state.videoUrls.push(URL.createObjectURL(file));
|
state.videoUrls.push(URL.createObjectURL(file));
|
||||||
state.videoFilenames.push(file.name);
|
state.videoFilenames.push(file.name);
|
||||||
}
|
}
|
||||||
@ -198,6 +202,7 @@ function processVideoFiles(files) {
|
|||||||
|
|
||||||
if (state.videoUrls.length === 0) {
|
if (state.videoUrls.length === 0) {
|
||||||
console.info('No valid video files selected.');
|
console.info('No valid video files selected.');
|
||||||
|
showToast('Error: No valid video files loaded.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +217,6 @@ function processVideoFiles(files) {
|
|||||||
startVideoPlayback();
|
startVideoPlayback();
|
||||||
} else {
|
} else {
|
||||||
console.info("Tapes loaded. Waiting for party start...");
|
console.info("Tapes loaded. Waiting for party start...");
|
||||||
if (state.loadTapeButton) state.loadTapeButton.innerText = "Tapes Ready";
|
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import { SceneFeature } from './SceneFeature.js';
|
|||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
import { MediaStorage } from '../core/media-storage.js';
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
import { showStandbyScreen } from './projection-screen.js';
|
import { showStandbyScreen } from './projection-screen.js';
|
||||||
|
import { showToast } from '../core/ui-utils.js';
|
||||||
|
|
||||||
export class ConfigUI extends SceneFeature {
|
export class ConfigUI extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -357,12 +358,23 @@ export class ConfigUI extends SceneFeature {
|
|||||||
state.posterImage = URL.createObjectURL(file);
|
state.posterImage = URL.createObjectURL(file);
|
||||||
MediaStorage.savePoster(file);
|
MediaStorage.savePoster(file);
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
|
showToast('Poster loaded', 'success');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.body.appendChild(posterInput);
|
document.body.appendChild(posterInput);
|
||||||
|
|
||||||
loadPosterBtn.onclick = () => {
|
loadPosterBtn.onclick = () => {
|
||||||
posterInput.click();
|
if (state.posterImage) {
|
||||||
|
URL.revokeObjectURL(state.posterImage);
|
||||||
|
state.posterImage = null;
|
||||||
|
MediaStorage.deletePoster();
|
||||||
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
|
posterInput.value = '';
|
||||||
|
} else {
|
||||||
|
posterInput.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.loadPosterBtn = loadPosterBtn;
|
this.loadPosterBtn = loadPosterBtn;
|
||||||
statusContainer.appendChild(loadPosterBtn);
|
statusContainer.appendChild(loadPosterBtn);
|
||||||
@ -512,6 +524,7 @@ export class ConfigUI extends SceneFeature {
|
|||||||
if (file) {
|
if (file) {
|
||||||
state.posterImage = URL.createObjectURL(file);
|
state.posterImage = URL.createObjectURL(file);
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -566,9 +579,12 @@ export class ConfigUI extends SceneFeature {
|
|||||||
|
|
||||||
if (this.loadPosterBtn) {
|
if (this.loadPosterBtn) {
|
||||||
this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange;
|
this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange;
|
||||||
|
this.loadPosterBtn.innerText = state.posterImage ? 'Clear Poster' : 'Load Poster';
|
||||||
}
|
}
|
||||||
if (state.loadTapeButton) {
|
if (state.loadTapeButton) {
|
||||||
state.loadTapeButton.style.backgroundColor = (state.videoUrls && state.videoUrls.length > 0) ? green : orange;
|
const hasTapes = state.videoUrls && state.videoUrls.length > 0;
|
||||||
|
state.loadTapeButton.style.backgroundColor = hasTapes ? green : orange;
|
||||||
|
state.loadTapeButton.innerText = hasTapes ? 'Change Tapes' : 'Load Tapes';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Tape List
|
// Update Tape List
|
||||||
|
|||||||
@ -455,7 +455,7 @@ export class DebugPanel extends SceneFeature {
|
|||||||
const w = this.spectrumCanvas.width;
|
const w = this.spectrumCanvas.width;
|
||||||
const h = this.spectrumCanvas.height;
|
const h = this.spectrumCanvas.height;
|
||||||
const data = state.music.frequencyData;
|
const data = state.music.frequencyData;
|
||||||
const bufferLength = data.length/2;
|
const bufferLength = data.length;
|
||||||
|
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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';
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
|
import { showToast } from '../core/ui-utils.js';
|
||||||
|
|
||||||
export class MusicPlayer extends SceneFeature {
|
export class MusicPlayer extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -24,18 +25,40 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
state.music.loudness = 0;
|
state.music.loudness = 0;
|
||||||
state.music.loudnessAverage = 0;
|
state.music.loudnessAverage = 0;
|
||||||
const loadButton = document.getElementById('loadMusicButton');
|
const loadButton = document.getElementById('loadMusicButton');
|
||||||
const fileInput = document.getElementById('musicFileInput');
|
|
||||||
|
let fileInput = document.getElementById('musicFileInput');
|
||||||
|
if (!fileInput) {
|
||||||
|
fileInput = document.createElement('input');
|
||||||
|
fileInput.id = 'musicFileInput';
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'audio/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset value to allow re-selecting the same file
|
||||||
|
fileInput.onclick = () => { fileInput.value = ''; };
|
||||||
|
|
||||||
// Hide the big start button as we use ConfigUI now
|
// Hide the big start button as we use ConfigUI now
|
||||||
if (loadButton) loadButton.style.display = 'none';
|
if (loadButton) loadButton.style.display = 'none';
|
||||||
|
|
||||||
fileInput.addEventListener('change', (event) => {
|
fileInput.onchange = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
console.info(`[MusicPlayer] File selected from input: ${file.name} (${file.size} bytes)`);
|
||||||
|
if (file.size === 0) {
|
||||||
|
console.warn('[MusicPlayer] Selected file is empty (0 bytes). Check if file is a cloud placeholder or locked.');
|
||||||
|
showToast(`Error: "${file.name}" is empty (0 bytes). Looks like the browser can't access it.`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.loadMusicFile(file);
|
this.loadMusicFile(file);
|
||||||
MediaStorage.saveMusic(file);
|
MediaStorage.saveMusic(file).then(() => showToast(`Loaded "${file.name}"`, 'success'))
|
||||||
|
.catch(e => {
|
||||||
|
console.warn('[MusicPlayer] Failed to save music:', e);
|
||||||
|
showToast('Warning: Failed to save song to storage.', 'warning');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
state.music.player.addEventListener('ended', () => {
|
state.music.player.addEventListener('ended', () => {
|
||||||
this.stopParty();
|
this.stopParty();
|
||||||
@ -46,12 +69,18 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
// Restore from storage
|
// Restore from storage
|
||||||
MediaStorage.getMusic().then(file => {
|
MediaStorage.getMusic().then(file => {
|
||||||
if (file) {
|
if (file) {
|
||||||
|
console.info(`[MusicPlayer] Restored music from storage: ${file.name} (${file.size} bytes)`);
|
||||||
this.loadMusicFile(file);
|
this.loadMusicFile(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMusicFile(file) {
|
loadMusicFile(file) {
|
||||||
|
if (!file || file.size === 0) {
|
||||||
|
console.warn('[MusicPlayer] loadMusicFile called with invalid or empty file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup Web Audio API if not already done
|
// Setup Web Audio API if not already done
|
||||||
if (!this.audioContext) {
|
if (!this.audioContext) {
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
@ -68,8 +97,13 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
const songName = file.name.replace(/\.[^/.]+$/, "");
|
const songName = file.name.replace(/\.[^/.]+$/, "");
|
||||||
state.music.songTitle = songName;
|
state.music.songTitle = songName;
|
||||||
|
|
||||||
|
if (state.music.player.src && state.music.player.src.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(state.music.player.src);
|
||||||
|
}
|
||||||
|
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
state.music.player.src = url;
|
state.music.player.src = url;
|
||||||
|
console.info(`[MusicPlayer] Audio source set to blob URL: ${url}`);
|
||||||
|
|
||||||
// Update Config UI
|
// Update Config UI
|
||||||
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
||||||
|
|||||||
@ -38,6 +38,17 @@ export class MusicVisualizer extends SceneFeature {
|
|||||||
this.beatPhase = 0;
|
this.beatPhase = 0;
|
||||||
this.measurePhase = 0;
|
this.measurePhase = 0;
|
||||||
this.averageLoudness = 0;
|
this.averageLoudness = 0;
|
||||||
|
this.prevLoudness = 0;
|
||||||
|
this.prevLoudnessLows = 0;
|
||||||
|
this.prevLoudnessHighs = 0;
|
||||||
|
this.beatSignalAvg = 0;
|
||||||
|
|
||||||
|
// 3 Detectors: Lows (Bass), Highs (Snare/Hats), Flux (All)
|
||||||
|
this.detectors = [
|
||||||
|
{ name: 'low', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 1.0 },
|
||||||
|
{ name: 'high', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 0.5 },
|
||||||
|
{ name: 'flux', lastTime: 0, threshold: 0.3, intervals: [], bpm: 120, weight: 0.8 }
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
@ -95,40 +106,77 @@ export class MusicVisualizer extends SceneFeature {
|
|||||||
state.music.averageLoudness = this.averageLoudness;
|
state.music.averageLoudness = this.averageLoudness;
|
||||||
|
|
||||||
// --- Beat Detection & Auto-BPM ---
|
// --- Beat Detection & Auto-BPM ---
|
||||||
// Use Lows (Bass) for clearer beat detection
|
// 1. Calculate derivatives for 3 bands
|
||||||
const beatSignal = state.music.loudnessLows || 0;
|
const dLows = Math.max(0, state.music.loudnessLows - this.prevLoudnessLows);
|
||||||
const beatAvg = state.music.loudnessLowsAverage || 0;
|
const dHighs = Math.max(0, state.music.loudnessHighs - this.prevLoudnessHighs);
|
||||||
|
const dAll = Math.max(0, state.music.loudness - this.prevLoudness);
|
||||||
// Decay threshold based on time
|
|
||||||
this.beatThreshold -= deltaTime * 0.5;
|
this.prevLoudnessLows = state.music.loudnessLows;
|
||||||
const thresholdFloor = Math.max(0.15, beatAvg * 1.1);
|
this.prevLoudnessHighs = state.music.loudnessHighs;
|
||||||
if (this.beatThreshold < thresholdFloor) this.beatThreshold = thresholdFloor;
|
this.prevLoudness = state.music.loudness;
|
||||||
|
|
||||||
state.music.beatThreshold = this.beatThreshold;
|
|
||||||
|
|
||||||
|
const signals = [dLows * 10.0, dHighs * 10.0, dAll * 10.0];
|
||||||
const now = time;
|
const now = time;
|
||||||
const lastBeatInterval = now - this.lastBeatTime;
|
let mainTrigger = false;
|
||||||
if (beatSignal > this.beatThreshold) {
|
|
||||||
if (lastBeatInterval > 0.25) { // Min interval (~240 BPM)
|
|
||||||
this.lastBeatTime = now;
|
|
||||||
this.beatThreshold = beatSignal * 1.2; // Bump threshold
|
|
||||||
|
|
||||||
// Add only valid BPM range into history of beats
|
// 2. Update Detectors
|
||||||
if (lastBeatInterval >= 0.2 && lastBeatInterval <= 1.5) {
|
this.detectors.forEach((d, i) => {
|
||||||
this.beatIntervals.push(lastBeatInterval);
|
const signal = signals[i];
|
||||||
if (this.beatIntervals.length > 8) this.beatIntervals.shift();
|
|
||||||
|
// Decay threshold
|
||||||
|
d.threshold -= deltaTime * 1.5;
|
||||||
|
const floor = 0.15;
|
||||||
|
if (d.threshold < floor) d.threshold = floor;
|
||||||
|
|
||||||
const avgInterval = this.beatIntervals.reduce((a, b) => a + b, 0) / this.beatIntervals.length;
|
if (signal > d.threshold) {
|
||||||
|
const interval = now - d.lastTime;
|
||||||
|
// Debounce (Max 240 BPM)
|
||||||
|
if (interval > 0.25) {
|
||||||
|
d.lastTime = now;
|
||||||
|
d.threshold = signal * 1.2;
|
||||||
|
|
||||||
|
// Valid interval (30 - 300 BPM)
|
||||||
|
if (interval < 2.0) {
|
||||||
|
d.intervals.push(interval);
|
||||||
|
if (d.intervals.length > 8) d.intervals.shift();
|
||||||
|
|
||||||
|
const avgInterval = d.intervals.reduce((a, b) => a + b, 0) / d.intervals.length;
|
||||||
|
d.bpm = 60 / avgInterval;
|
||||||
|
}
|
||||||
|
|
||||||
// Smoothly adjust beat duration
|
if (i === 0) mainTrigger = true; // Bass triggers visual beat
|
||||||
state.music.beatDuration = THREE.MathUtils.lerp(state.music.beatDuration, avgInterval, 0.1);
|
|
||||||
state.music.measureDuration = state.music.beatDuration * 4;
|
|
||||||
state.music.bpm = Math.round(60 / state.music.beatDuration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync phase on beat
|
|
||||||
this.beatPhase = 0;
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. Combine Results
|
||||||
|
let bpmSum = 0;
|
||||||
|
let weightSum = 0;
|
||||||
|
this.detectors.forEach(d => {
|
||||||
|
let bpm = d.bpm;
|
||||||
|
// Normalize to 90-180 range for consensus
|
||||||
|
if (bpm > 0) {
|
||||||
|
while (bpm < 90) bpm *= 2;
|
||||||
|
while (bpm > 180) bpm /= 2;
|
||||||
|
bpmSum += bpm * d.weight;
|
||||||
|
weightSum += d.weight;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (weightSum > 0) {
|
||||||
|
const targetBPM = bpmSum / weightSum;
|
||||||
|
const targetDuration = 60 / targetBPM;
|
||||||
|
state.music.beatDuration = THREE.MathUtils.lerp(state.music.beatDuration, targetDuration, deltaTime * 0.5);
|
||||||
|
state.music.measureDuration = state.music.beatDuration * 4;
|
||||||
|
state.music.bpm = Math.round(60 / state.music.beatDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visuals use the Bass detector for immediate feedback
|
||||||
|
state.music.beatThreshold = this.detectors[0].threshold;
|
||||||
|
const lastBeatInterval = now - this.detectors[0].lastTime;
|
||||||
|
|
||||||
|
if (mainTrigger) {
|
||||||
|
this.beatPhase = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Phase Accumulation ---
|
// --- Phase Accumulation ---
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import sceneFeatureManager from './SceneFeatureManager.js';
|
|||||||
// --- Scene dimensions for positioning ---
|
// --- Scene dimensions for positioning ---
|
||||||
const stageHeight = 1.5;
|
const stageHeight = 1.5;
|
||||||
const stageDepth = 5;
|
const stageDepth = 5;
|
||||||
const length = 25;
|
const length = 24;
|
||||||
const moveSpeed = 0.8;
|
const moveSpeed = 0.8;
|
||||||
const movementArea = { x: 15, z: length, y: 0, centerZ: -2 };
|
const movementArea = { x: 15, z: length, y: 0, centerZ: -2 };
|
||||||
const jumpChance = 0.01;
|
const jumpChance = 0.01;
|
||||||
@ -14,8 +14,8 @@ const jumpDuration = 0.3;
|
|||||||
const jumpHeight = 0.2;
|
const jumpHeight = 0.2;
|
||||||
const jumpVariance = 0.1;
|
const jumpVariance = 0.1;
|
||||||
const rushIn = false;
|
const rushIn = false;
|
||||||
const waitTimeBase = 10;
|
const waitTimeBase = 15;
|
||||||
const waitTimeVariance = 60;
|
const waitTimeVariance = 120;
|
||||||
|
|
||||||
// --- Guest Properties ---
|
// --- Guest Properties ---
|
||||||
const guestHeight = 1.8; // Approx height of the blob+head
|
const guestHeight = 1.8; // Approx height of the blob+head
|
||||||
@ -26,6 +26,45 @@ export class PartyGuests extends SceneFeature {
|
|||||||
this.guests = [];
|
this.guests = [];
|
||||||
this.guestPool = []; // Store all created guests to reuse them
|
this.guestPool = []; // Store all created guests to reuse them
|
||||||
sceneFeatureManager.register(this);
|
sceneFeatureManager.register(this);
|
||||||
|
|
||||||
|
this.organizedMode = null;
|
||||||
|
this.organizedModeTimer = 0;
|
||||||
|
this.wasBlackout = false;
|
||||||
|
this.lastOrganizedLogTime = 0;
|
||||||
|
this.organizedModeMinDuration = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
startOrganizedMode() {
|
||||||
|
this.organizedModeTimer = 10 + Math.random() * 10; // 10-20 seconds
|
||||||
|
this.organizedModeMinDuration = 5.0;
|
||||||
|
|
||||||
|
const music = state.music;
|
||||||
|
this.organizedMode = 'RUNNING'; // Default
|
||||||
|
|
||||||
|
if (music) {
|
||||||
|
if (music.beatIntensity > 0.7) {
|
||||||
|
// High intensity: Moshpit or Jumping
|
||||||
|
this.organizedMode = Math.random() < 0.5 ? 'JUMPING' : 'MOSHPIT';
|
||||||
|
} else if (music.loudnessHighs > 0.6) {
|
||||||
|
this.organizedMode = 'HANDS_UP';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random variation
|
||||||
|
if (Math.random() < 0.4) {
|
||||||
|
const modes = ['RUNNING', 'JUMPING', 'HANDS_UP', 'CIRCLE_PIT', 'MOSHPIT'];
|
||||||
|
this.organizedMode = modes[Math.floor(Math.random() * modes.length)];
|
||||||
|
}
|
||||||
|
console.log(`Organized mode started: ${this.organizedMode}`);
|
||||||
|
|
||||||
|
this.guests.forEach(guest => {
|
||||||
|
let chance = 0.1;
|
||||||
|
// Higher chance if in the middle or front
|
||||||
|
if (Math.abs(guest.mesh.position.x) < 5) chance += 0.3;
|
||||||
|
if (guest.mesh.position.z < -5) chance += 0.3;
|
||||||
|
|
||||||
|
guest.isInOrganizedMode = Math.random() < chance;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
@ -125,7 +164,8 @@ export class PartyGuests extends SceneFeature {
|
|||||||
shouldRaiseArms: false,
|
shouldRaiseArms: false,
|
||||||
handsUpTimer: 0,
|
handsUpTimer: 0,
|
||||||
handsRaisedType: 'BOTH',
|
handsRaisedType: 'BOTH',
|
||||||
randomOffset: Math.random() * 100
|
randomOffset: Math.random() * 100,
|
||||||
|
isInOrganizedMode: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,6 +191,46 @@ export class PartyGuests extends SceneFeature {
|
|||||||
if (this.guests.length === 0 || !state.partyStarted) return;
|
if (this.guests.length === 0 || !state.partyStarted) return;
|
||||||
|
|
||||||
const time = state.clock.getElapsedTime();
|
const time = state.clock.getElapsedTime();
|
||||||
|
|
||||||
|
// --- Organized Mode Logic ---
|
||||||
|
const isBlackout = state.blackoutMode;
|
||||||
|
if (!isBlackout && this.wasBlackout) {
|
||||||
|
// Blackout just ended
|
||||||
|
if (Math.random() < 0.2) {
|
||||||
|
this.startOrganizedMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.wasBlackout = isBlackout;
|
||||||
|
|
||||||
|
if (this.organizedMode) {
|
||||||
|
this.organizedModeTimer -= deltaTime;
|
||||||
|
this.organizedModeMinDuration -= deltaTime;
|
||||||
|
if (time - this.lastOrganizedLogTime > 1.0) {
|
||||||
|
const count = this.guests.filter(g => g.isInOrganizedMode).length;
|
||||||
|
console.log(`Organized Event Active: ${this.organizedMode}. Participants: ${count}`);
|
||||||
|
this.lastOrganizedLogTime = time;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.organizedModeTimer <= 0 || (isBlackout && this.organizedModeMinDuration <= 0)) {
|
||||||
|
console.log(`Organized mode ended: ${this.organizedMode}`);
|
||||||
|
|
||||||
|
this.guests.forEach(guest => {
|
||||||
|
if (guest.isInOrganizedMode) {
|
||||||
|
guest.state = 'MOVING';
|
||||||
|
guest.targetPosition.set(
|
||||||
|
(Math.random() - 0.5) * movementArea.x,
|
||||||
|
0,
|
||||||
|
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
||||||
|
);
|
||||||
|
guest.moveSpeed = moveSpeed * 1.5;
|
||||||
|
guest.isInOrganizedMode = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.organizedMode = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const minDistance = 0.8; // Minimum distance to maintain
|
const minDistance = 0.8; // Minimum distance to maintain
|
||||||
const minDistSq = minDistance * minDistance;
|
const minDistSq = minDistance * minDistance;
|
||||||
|
|
||||||
@ -171,6 +251,35 @@ export class PartyGuests extends SceneFeature {
|
|||||||
this.guests.forEach((guestObj, i) => {
|
this.guests.forEach((guestObj, i) => {
|
||||||
const { mesh, leftArm, rightArm } = guestObj;
|
const { mesh, leftArm, rightArm } = guestObj;
|
||||||
|
|
||||||
|
// Dynamic join/leave logic
|
||||||
|
if (this.organizedMode) {
|
||||||
|
if (guestObj.isInOrganizedMode) {
|
||||||
|
if (Math.random() < 0.001) guestObj.isInOrganizedMode = false;
|
||||||
|
} else {
|
||||||
|
if (Math.random() < 0.001) guestObj.isInOrganizedMode = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
guestObj.isInOrganizedMode = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let currentMoveSpeed = moveSpeed;
|
||||||
|
if (this.organizedMode === 'RUNNING' && guestObj.isInOrganizedMode) {
|
||||||
|
currentMoveSpeed = moveSpeed * 2.0;
|
||||||
|
if (guestObj.state === 'WAITING') {
|
||||||
|
guestObj.waitTime = 0;
|
||||||
|
}
|
||||||
|
} else if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) {
|
||||||
|
currentMoveSpeed = moveSpeed * 2.0;
|
||||||
|
if (guestObj.state === 'WAITING') {
|
||||||
|
guestObj.waitTime = 0;
|
||||||
|
}
|
||||||
|
} else if (this.organizedMode === 'HANDS_UP' && guestObj.isInOrganizedMode) {
|
||||||
|
guestObj.handsUpTimer = 0.5;
|
||||||
|
guestObj.handsRaisedType = 'BOTH';
|
||||||
|
} else if (this.organizedMode && !guestObj.isInOrganizedMode) {
|
||||||
|
currentMoveSpeed = moveSpeed * 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Collision Avoidance ---
|
// --- Collision Avoidance ---
|
||||||
let separationX = 0;
|
let separationX = 0;
|
||||||
let separationZ = 0;
|
let separationZ = 0;
|
||||||
@ -207,6 +316,56 @@ export class PartyGuests extends SceneFeature {
|
|||||||
mesh.position.x += separationX * separationStrength * deltaTime;
|
mesh.position.x += separationX * separationStrength * deltaTime;
|
||||||
mesh.position.z += separationZ * separationStrength * deltaTime;
|
mesh.position.z += separationZ * separationStrength * deltaTime;
|
||||||
|
|
||||||
|
if (this.organizedMode === 'CIRCLE_PIT' && guestObj.isInOrganizedMode) {
|
||||||
|
// --- Circle Pit Logic ---
|
||||||
|
const centerZ = movementArea.centerZ;
|
||||||
|
const radius = 3.5;
|
||||||
|
const speed = 5.0;
|
||||||
|
|
||||||
|
const dx = mesh.position.x;
|
||||||
|
const dz = mesh.position.z - centerZ;
|
||||||
|
const dist = Math.sqrt(dx*dx + dz*dz);
|
||||||
|
|
||||||
|
if (dist > radius + 1.0) {
|
||||||
|
// Run towards circle
|
||||||
|
const dirX = -dx / dist;
|
||||||
|
const dirZ = -dz / dist;
|
||||||
|
|
||||||
|
mesh.position.x += dirX * speed * deltaTime;
|
||||||
|
mesh.position.z += dirZ * speed * deltaTime;
|
||||||
|
|
||||||
|
mesh.rotation.y = Math.atan2(dirX, dirZ);
|
||||||
|
} else {
|
||||||
|
// Run in circle
|
||||||
|
// Tangent vector (counter-clockwise)
|
||||||
|
let vx = -dz;
|
||||||
|
let vz = dx;
|
||||||
|
|
||||||
|
// Normalize
|
||||||
|
if (dist > 0.001) {
|
||||||
|
vx /= dist;
|
||||||
|
vz /= dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move along tangent
|
||||||
|
mesh.position.x += vx * speed * deltaTime;
|
||||||
|
mesh.position.z += vz * speed * deltaTime;
|
||||||
|
|
||||||
|
// Radius Correction (Pull towards ideal circle)
|
||||||
|
const ratio = 1.0 + (radius - dist) * 2.0 * deltaTime;
|
||||||
|
mesh.position.x *= ratio; // Assumes center X is 0
|
||||||
|
mesh.position.z = centerZ + (mesh.position.z - centerZ) * ratio;
|
||||||
|
|
||||||
|
// Face movement direction
|
||||||
|
mesh.rotation.y = Math.atan2(vx, vz);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep state as MOVING so other logic (like jumping) works, but skip standard pathfinding
|
||||||
|
guestObj.state = 'MOVING';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// --- Standard Logic (includes MOSHPIT) ---
|
||||||
|
|
||||||
if (guestObj.state === 'WAITING') {
|
if (guestObj.state === 'WAITING') {
|
||||||
// Face the stage (approx z = -20)
|
// Face the stage (approx z = -20)
|
||||||
const dx = 0 - mesh.position.x;
|
const dx = 0 - mesh.position.x;
|
||||||
@ -226,13 +385,45 @@ export class PartyGuests extends SceneFeature {
|
|||||||
mesh.rotation.z = 0;
|
mesh.rotation.z = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moshpit: Don't wait long
|
||||||
|
if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) {
|
||||||
|
guestObj.waitTime = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
if (time > guestObj.waitStartTime + guestObj.waitTime) {
|
||||||
|
// Gravitate towards stage (negative Z)
|
||||||
|
const minZ = movementArea.centerZ - movementArea.z / 2;
|
||||||
|
const maxZ = movementArea.centerZ + movementArea.z / 2;
|
||||||
|
const zRatio = Math.pow(Math.random(), 1.5); // Bias towards 0 (front)
|
||||||
|
|
||||||
const newTarget = new THREE.Vector3(
|
const newTarget = new THREE.Vector3(
|
||||||
(Math.random() - 0.5) * movementArea.x,
|
(Math.random() - 0.5) * movementArea.x,
|
||||||
0,
|
0,
|
||||||
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
minZ + zRatio * (maxZ - minZ)
|
||||||
);
|
);
|
||||||
guestObj.targetPosition = newTarget;
|
guestObj.targetPosition = newTarget;
|
||||||
|
|
||||||
|
// Moshpit: Target center area more often
|
||||||
|
if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode) {
|
||||||
|
const range = 5.0;
|
||||||
|
guestObj.targetPosition.set(
|
||||||
|
(Math.random() - 0.5) * range,
|
||||||
|
0,
|
||||||
|
movementArea.centerZ + (Math.random() - 0.5) * range
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-participants: Disperse away from center
|
||||||
|
if (this.organizedMode && !guestObj.isInOrganizedMode) {
|
||||||
|
const side = Math.sign(mesh.position.x) || (Math.random() < 0.5 ? 1 : -1);
|
||||||
|
const xOffset = (movementArea.x / 2) * (0.5 + Math.random() * 0.4);
|
||||||
|
guestObj.targetPosition.set(
|
||||||
|
side * xOffset,
|
||||||
|
0,
|
||||||
|
movementArea.centerZ + (Math.random() - 0.5) * movementArea.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
guestObj.state = 'MOVING';
|
guestObj.state = 'MOVING';
|
||||||
}
|
}
|
||||||
} else if (guestObj.state === 'MOVING') {
|
} else if (guestObj.state === 'MOVING') {
|
||||||
@ -240,9 +431,20 @@ export class PartyGuests extends SceneFeature {
|
|||||||
const targetPosFlat = new THREE.Vector3(guestObj.targetPosition.x, 0, guestObj.targetPosition.z);
|
const targetPosFlat = new THREE.Vector3(guestObj.targetPosition.x, 0, guestObj.targetPosition.z);
|
||||||
|
|
||||||
const distance = currentPosFlat.distanceTo(targetPosFlat);
|
const distance = currentPosFlat.distanceTo(targetPosFlat);
|
||||||
|
|
||||||
|
// Moshpit: Change direction frequently
|
||||||
|
if (this.organizedMode === 'MOSHPIT' && guestObj.isInOrganizedMode && Math.random() < 0.05) {
|
||||||
|
const range = 5.0;
|
||||||
|
guestObj.targetPosition.set(
|
||||||
|
(Math.random() - 0.5) * range,
|
||||||
|
0,
|
||||||
|
movementArea.centerZ + (Math.random() - 0.5) * range
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (distance > 0.1) {
|
if (distance > 0.1) {
|
||||||
const direction = targetPosFlat.sub(currentPosFlat).normalize();
|
const direction = targetPosFlat.sub(currentPosFlat).normalize();
|
||||||
mesh.position.add(direction.multiplyScalar(moveSpeed * deltaTime));
|
mesh.position.add(direction.multiplyScalar(currentMoveSpeed * deltaTime));
|
||||||
|
|
||||||
// If moving away from stage (positive Z), drop hands
|
// If moving away from stage (positive Z), drop hands
|
||||||
if (direction.z > 0.1) {
|
if (direction.z > 0.1) {
|
||||||
@ -267,6 +469,13 @@ export class PartyGuests extends SceneFeature {
|
|||||||
guestObj.waitTime = waitTimeBase + Math.random() * waitTimeVariance;
|
guestObj.waitTime = waitTimeBase + Math.random() * waitTimeVariance;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} // End else (Standard Logic)
|
||||||
|
|
||||||
|
// Virtual Barrier to prevent entering the stage
|
||||||
|
const movementAreaLimitZ = movementArea.centerZ - movementArea.z/2;
|
||||||
|
if (mesh.position.z < movementAreaLimitZ) {
|
||||||
|
mesh.position.z = movementAreaLimitZ;
|
||||||
|
}
|
||||||
|
|
||||||
// Update hands up timer
|
// Update hands up timer
|
||||||
if (guestObj.handsUpTimer > 0) {
|
if (guestObj.handsUpTimer > 0) {
|
||||||
@ -283,7 +492,9 @@ export class PartyGuests extends SceneFeature {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
let currentJumpChance = jumpChance * deltaTime; // Base chance over time
|
||||||
if (state.music && state.music.isLoudEnough && state.music.beatIntensity > 0.8) {
|
if (this.organizedMode === 'JUMPING' && guestObj.isInOrganizedMode) {
|
||||||
|
currentJumpChance = 5.0 * deltaTime;
|
||||||
|
} else if (state.music && state.music.isLoudEnough && state.music.beatIntensity > 0.8) {
|
||||||
currentJumpChance = 0.1; // High, fixed chance on the beat
|
currentJumpChance = 0.1; // High, fixed chance on the beat
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -292,7 +503,7 @@ export class PartyGuests extends SceneFeature {
|
|||||||
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
guestObj.jumpHeight = jumpHeight + Math.random() * jumpVariance;
|
||||||
guestObj.jumpStartTime = time;
|
guestObj.jumpStartTime = time;
|
||||||
|
|
||||||
if (Math.random() < 0.5) {
|
if (Math.random() < 0.1) {
|
||||||
guestObj.handsUpTimer = 2.0 + Math.random() * 3.0; // Keep hands up for 2-5 seconds
|
guestObj.handsUpTimer = 2.0 + Math.random() * 3.0; // Keep hands up for 2-5 seconds
|
||||||
const r = Math.random();
|
const r = Math.random();
|
||||||
if (r < 0.33) guestObj.handsRaisedType = 'LEFT';
|
if (r < 0.33) guestObj.handsRaisedType = 'LEFT';
|
||||||
|
|||||||
@ -55,7 +55,7 @@ void main() {
|
|||||||
float mask = 1.0 - smoothstep(0.35 - edgeSoftness, 0.45 + edgeSoftness, dist);
|
float mask = 1.0 - smoothstep(0.35 - edgeSoftness, 0.45 + edgeSoftness, dist);
|
||||||
mask = mix(mask, 1.0, blurFactor);
|
mask = mix(mask, 1.0, blurFactor);
|
||||||
|
|
||||||
gl_FragColor = vec4(color.rgb, mask);
|
gl_FragColor = vec4(color.rgb, mask * u_opacity);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -117,7 +117,7 @@ void main() {
|
|||||||
finalColor = hsv2rgb(vec3(hue, 0.8, val));
|
finalColor = hsv2rgb(vec3(hue, 0.8, val));
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_FragColor = vec4(finalColor, mask);
|
gl_FragColor = vec4(finalColor, mask * u_opacity);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -129,6 +129,7 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
projectionScreenInstance = this;
|
projectionScreenInstance = this;
|
||||||
this.isVisualizerActive = false;
|
this.isVisualizerActive = false;
|
||||||
this.screens = [];
|
this.screens = [];
|
||||||
|
this.blackoutLerp = 0;
|
||||||
this.colorBuffer = new Float32Array(16 * 3);
|
this.colorBuffer = new Float32Array(16 * 3);
|
||||||
|
|
||||||
this.standbyState = {
|
this.standbyState = {
|
||||||
@ -158,6 +159,7 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
};
|
};
|
||||||
state.originalScreenIntensity = 2.0;
|
state.originalScreenIntensity = 2.0;
|
||||||
state.screenOpacity = 0.5;
|
state.screenOpacity = 0.5;
|
||||||
|
state.screenOpacityBlackout = 1.0;
|
||||||
|
|
||||||
// Ensure video element exists
|
// Ensure video element exists
|
||||||
if (!state.videoElement) {
|
if (!state.videoElement) {
|
||||||
@ -245,6 +247,22 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
update(deltaTime) {
|
update(deltaTime) {
|
||||||
updateScreenEffect();
|
updateScreenEffect();
|
||||||
|
|
||||||
|
// --- Blackout Transition Logic ---
|
||||||
|
const targetBlackout = state.blackoutMode ? 1.0 : 0.0;
|
||||||
|
const enterBlackoutSpeed = 0.5; // slow light up
|
||||||
|
const exitBlackoutSpeed = 5.0; // quick fade out
|
||||||
|
|
||||||
|
if (this.blackoutLerp < targetBlackout) {
|
||||||
|
this.blackoutLerp = Math.min(targetBlackout, this.blackoutLerp + deltaTime * enterBlackoutSpeed);
|
||||||
|
} else if (this.blackoutLerp > targetBlackout) {
|
||||||
|
this.blackoutLerp = Math.max(targetBlackout, this.blackoutLerp - deltaTime * exitBlackoutSpeed);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalOpacity = state.screenOpacity !== undefined ? state.screenOpacity : 0.7;
|
||||||
|
const blackoutOpacity = state.screenOpacityBlackout !== undefined ? state.screenOpacityBlackout : 0.2;
|
||||||
|
const currentOpacity = THREE.MathUtils.lerp(normalOpacity, blackoutOpacity, this.blackoutLerp);
|
||||||
|
const dimFactor = 1.0 - this.blackoutLerp;
|
||||||
|
|
||||||
if (this.standbyState.active) {
|
if (this.standbyState.active) {
|
||||||
this.renderStandbyFrame();
|
this.renderStandbyFrame();
|
||||||
}
|
}
|
||||||
@ -272,10 +290,7 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
if (this.isVisualizerActive) {
|
if (this.isVisualizerActive) {
|
||||||
const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0;
|
const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0;
|
||||||
state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5);
|
state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5);
|
||||||
|
state.screenLight.intensity *= dimFactor;
|
||||||
if (state.blackoutMode) {
|
|
||||||
state.screenLight.intensity = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update color buffer
|
// Update color buffer
|
||||||
const colors = state.config.lightBarColors;
|
const colors = state.config.lightBarColors;
|
||||||
@ -297,13 +312,26 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
if (s.mesh.material.uniforms.u_colorCount) {
|
if (s.mesh.material.uniforms.u_colorCount) {
|
||||||
s.mesh.material.uniforms.u_colorCount.value = colorCount;
|
s.mesh.material.uniforms.u_colorCount.value = colorCount;
|
||||||
}
|
}
|
||||||
if (s.mesh.material.uniforms.u_opacity) {
|
|
||||||
const targetOpacity = state.blackoutMode ? 0.1 : state.screenOpacity;
|
|
||||||
s.mesh.material.uniforms.u_opacity.value = targetOpacity;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Video Mode
|
||||||
|
state.screenLight.intensity = state.originalScreenIntensity * dimFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply Opacity to all screens (Visualizer or Video)
|
||||||
|
this.screens.forEach(s => {
|
||||||
|
const material = s.mesh.material;
|
||||||
|
if (material && material.uniforms) {
|
||||||
|
if (material.uniforms.u_opacity) {
|
||||||
|
material.uniforms.u_opacity.value = currentOpacity;
|
||||||
|
}
|
||||||
|
// Ensure time is updated for video shader effects even if visualizer is off
|
||||||
|
if (!this.isVisualizerActive && material.uniforms.u_time) {
|
||||||
|
material.uniforms.u_time.value = state.clock.getElapsedTime();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onPartyStart() {
|
onPartyStart() {
|
||||||
@ -554,11 +582,6 @@ export function updateScreenEffect() {
|
|||||||
if (material.uniforms.u_effect_strength) {
|
if (material.uniforms.u_effect_strength) {
|
||||||
material.uniforms.u_effect_strength.value = strength;
|
material.uniforms.u_effect_strength.value = strength;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (material.uniforms.u_opacity) {
|
|
||||||
const targetOpacity = state.blackoutMode ? 0.1 : (state.screenOpacity !== undefined ? state.screenOpacity : 0.7);
|
|
||||||
material.uniforms.u_opacity.value = targetOpacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress >= 1.0) {
|
if (progress >= 1.0) {
|
||||||
if (material.uniforms.u_effect_type) material.uniforms.u_effect_type.value = 0.0;
|
if (material.uniforms.u_effect_type) material.uniforms.u_effect_type.value = 0.0;
|
||||||
|
|||||||
@ -189,7 +189,7 @@ export class StageLasers extends SceneFeature {
|
|||||||
|
|
||||||
// --- Pattern Logic ---
|
// --- Pattern Logic ---
|
||||||
if (time - this.lastPatternChange > 8) { // Change every 8 seconds
|
if (time - this.lastPatternChange > 8) { // Change every 8 seconds
|
||||||
this.pattern = (this.pattern + 1) % 4;
|
this.pattern = (this.pattern + 1) % 8;
|
||||||
this.lastPatternChange = time;
|
this.lastPatternChange = time;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,6 +285,24 @@ export class StageLasers extends SceneFeature {
|
|||||||
yaw = Math.sin(t * 3 + idx) * 0.6;
|
yaw = Math.sin(t * 3 + idx) * 0.6;
|
||||||
pitch = Math.cos(t * 2.5 + idx * 2) * 0.4;
|
pitch = Math.cos(t * 2.5 + idx * 2) * 0.4;
|
||||||
break;
|
break;
|
||||||
|
case 4: // Vertical Wave
|
||||||
|
yaw = 0;
|
||||||
|
pitch = Math.sin(t * 2 + idx * 0.3) * 0.5;
|
||||||
|
break;
|
||||||
|
case 5: // Cross
|
||||||
|
yaw = Math.sin(t) * 0.5 * (idx % 2 === 0 ? 1 : -1);
|
||||||
|
pitch = Math.cos(t) * 0.2;
|
||||||
|
break;
|
||||||
|
case 6: // Expanding Circle
|
||||||
|
const r6 = (Math.sin(t) + 1.2) * 0.3;
|
||||||
|
const a6 = (Math.PI * 2 / l.totalInBank) * idx + t;
|
||||||
|
yaw = Math.cos(a6) * r6;
|
||||||
|
pitch = Math.sin(a6) * r6;
|
||||||
|
break;
|
||||||
|
case 7: // Fan Sweep
|
||||||
|
yaw = Math.sin(t) * 0.8 + ((idx / l.totalInBank) - 0.5) * 0.5;
|
||||||
|
pitch = 0;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply rotation
|
// Apply rotation
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user