From 9aa12c3c33089826ad5f6b458b7bd1109532a3d8 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Sun, 24 May 2026 13:45:10 +0200 Subject: [PATCH] Feature: image slideshow instead of videos --- party-stage/src/scene/config-ui.js | 72 +++++++++++++++++- party-stage/src/scene/projection-screen.js | 87 +++++++++++++++++++++- 2 files changed, 153 insertions(+), 6 deletions(-) diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index 62dc434..5f42122 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -396,6 +396,41 @@ export class ConfigUI extends SceneFeature { statusContainer.appendChild(loadTapesBtn); state.loadTapeButton = loadTapesBtn; + // Load Slideshow Button + const loadSlideshowBtn = document.createElement('button'); + loadSlideshowBtn.innerText = 'Load Slideshow'; + Object.assign(loadSlideshowBtn.style, { + marginTop: '10px', + padding: '8px', + cursor: 'pointer', + backgroundColor: '#ff9800', + color: 'white', + border: 'none', + borderRadius: '4px', + fontSize: '14px' + }); + + const slideshowInput = document.createElement('input'); + slideshowInput.type = 'file'; + slideshowInput.multiple = true; + slideshowInput.accept = 'image/*'; + slideshowInput.style.display = 'none'; + slideshowInput.onchange = (e) => { + const files = Array.from(e.target.files); + if (files.length > 0) { + if (state.imageUrls) state.imageUrls.forEach(url => URL.revokeObjectURL(url)); + state.imageUrls = files.map(f => URL.createObjectURL(f)); + state.imageFilenames = files.map(f => f.name); + state.isSlideshowLoaded = true; + this.updateStatus(); + showToast(`Loaded ${files.length} images`, 'success'); + } + }; + document.body.appendChild(slideshowInput); + loadSlideshowBtn.onclick = () => slideshowInput.click(); + this.loadSlideshowBtn = loadSlideshowBtn; + statusContainer.appendChild(loadSlideshowBtn); + // Choose Song Button const chooseSongBtn = document.createElement('button'); chooseSongBtn.innerText = 'Choose Song'; @@ -464,6 +499,12 @@ export class ConfigUI extends SceneFeature { state.videoFilenames = []; state.isVideoLoaded = false; state.currentVideoIndex = -1; + if (state.imageUrls) { + state.imageUrls.forEach(url => URL.revokeObjectURL(url)); + } + state.imageUrls = []; + state.imageFilenames = []; + state.isSlideshowLoaded = false; if (state.loadTapeButton) { state.loadTapeButton.innerText = 'Load Tapes'; state.loadTapeButton.classList.remove('hidden'); @@ -586,8 +627,13 @@ export class ConfigUI extends SceneFeature { state.loadTapeButton.style.backgroundColor = hasTapes ? green : orange; state.loadTapeButton.innerText = hasTapes ? 'Change Tapes' : 'Load Tapes'; } + if (this.loadSlideshowBtn) { + const hasImages = state.imageUrls && state.imageUrls.length > 0; + this.loadSlideshowBtn.style.backgroundColor = hasImages ? green : orange; + this.loadSlideshowBtn.innerText = hasImages ? 'Change Slideshow' : 'Load Slideshow'; + } - // Update Tape List + // Update Media List this.tapeList.innerHTML = ''; if (state.videoUrls && state.videoUrls.length > 0) { state.videoUrls.forEach((url, index) => { @@ -605,9 +651,29 @@ export class ConfigUI extends SceneFeature { } this.tapeList.appendChild(li); }); - } else { + } + + if (state.imageUrls && state.imageUrls.length > 0) { + state.imageUrls.forEach((url, index) => { + const li = document.createElement('li'); + let name = `Image ${index + 1}`; + if (state.imageFilenames && state.imageFilenames[index]) { + name = state.imageFilenames[index]; + } + + if (name.length > 25) { + li.innerText = name.substring(0, 22) + '...'; + li.title = name; + } else { + li.innerText = name; + } + this.tapeList.appendChild(li); + }); + } + + if ((!state.videoUrls || state.videoUrls.length === 0) && (!state.imageUrls || state.imageUrls.length === 0)) { const li = document.createElement('li'); - li.innerText = '(No tapes loaded)'; + li.innerText = '(No media loaded)'; this.tapeList.appendChild(li); } } diff --git a/party-stage/src/scene/projection-screen.js b/party-stage/src/scene/projection-screen.js index 9dd54df..9277800 100644 --- a/party-stage/src/scene/projection-screen.js +++ b/party-stage/src/scene/projection-screen.js @@ -128,6 +128,11 @@ export class ProjectionScreen extends SceneFeature { super(); projectionScreenInstance = this; this.isVisualizerActive = false; + this.isSlideshowActive = false; + this.slideshowTimer = 0; + this.currentImageIndex = 0; + this.slideshowDuration = 5.0; // seconds per image + this.imageTextures = {}; this.screens = []; this.blackoutLerp = 0; this.colorBuffer = new Float32Array(16 * 3); @@ -147,6 +152,10 @@ export class ProjectionScreen extends SceneFeature { } init() { + state.imageUrls = state.imageUrls || []; + state.imageFilenames = state.imageFilenames || []; + state.isSlideshowLoaded = state.isSlideshowLoaded || false; + // --- Initialize State --- state.tvScreenPowered = false; state.screenEffect = { @@ -247,6 +256,18 @@ export class ProjectionScreen extends SceneFeature { update(deltaTime) { updateScreenEffect(); + // --- Slideshow Cycling Logic --- + if (this.isSlideshowActive && state.imageUrls && state.imageUrls.length > 0) { + this.slideshowTimer += deltaTime; + if (this.slideshowTimer >= this.slideshowDuration) { + this.slideshowTimer = 0; + if (state.imageUrls.length > 1) { + this.currentImageIndex = (this.currentImageIndex + 1) % state.imageUrls.length; + this.updateSlideshowTexture(); + } + } + } + // --- Blackout Transition Logic --- const targetBlackout = state.blackoutMode ? 1.0 : 0.0; const enterBlackoutSpeed = 0.5; // slow light up @@ -314,6 +335,8 @@ export class ProjectionScreen extends SceneFeature { } } }); + } else if (this.isSlideshowActive) { + state.screenLight.intensity = state.originalScreenIntensity * dimFactor; } else { // Video Mode state.screenLight.intensity = state.originalScreenIntensity * dimFactor; @@ -338,9 +361,13 @@ export class ProjectionScreen extends SceneFeature { // Hide load button during playback if (state.loadTapeButton) state.loadTapeButton.classList.add('hidden'); - // If no video loaded, start visualizer + // Priority: Video > Slideshow > Visualizer if (!state.isVideoLoaded) { - this.activateVisualizer(); + if (state.isSlideshowLoaded) { + this.activateSlideshow(); + } else { + this.activateVisualizer(); + } } } @@ -351,6 +378,9 @@ export class ProjectionScreen extends SceneFeature { if (this.isVisualizerActive) { this.deactivateVisualizer(); } + if (this.isSlideshowActive) { + this.deactivateSlideshow(); + } } activateVisualizer() { @@ -382,6 +412,54 @@ export class ProjectionScreen extends SceneFeature { turnTvScreenOff(); } + activateSlideshow() { + this.isSlideshowActive = true; + this.isVisualizerActive = false; + this.currentImageIndex = 0; + this.slideshowTimer = 0; + state.tvScreenPowered = true; + + const material = new THREE.ShaderMaterial({ + uniforms: { + videoTexture: { value: null }, + u_effect_type: { value: 0.0 }, + u_effect_strength: { value: 0.0 }, + u_time: { value: 0.0 }, + u_opacity: { value: state.screenOpacity !== undefined ? state.screenOpacity : 0.7 }, + u_resolution: { value: new THREE.Vector2(1, 1) } + }, + vertexShader: screenVertexShader, + fragmentShader: screenFragmentShader, + side: THREE.DoubleSide, + transparent: true, + derivatives: true + }); + + this.applyMaterialToAll(material); + this.updateSlideshowTexture(); + this.setAllVisible(true); + setScreenEffect(1); // Power on effect + } + + deactivateSlideshow() { + this.isSlideshowActive = false; + turnTvScreenOff(); + } + + updateSlideshowTexture() { + if (!state.imageUrls || state.imageUrls.length === 0) return; + const url = state.imageUrls[this.currentImageIndex]; + if (!this.imageTextures[url]) { + this.imageTextures[url] = new THREE.TextureLoader().load(url); + this.imageTextures[url].colorSpace = THREE.SRGBColorSpace; + } + this.screens.forEach(s => { + if (s.mesh.material && s.mesh.material.uniforms && s.mesh.material.uniforms.videoTexture) { + s.mesh.material.uniforms.videoTexture.value = this.imageTextures[url]; + } + }); + } + activateStandby() { this.isVisualizerActive = false; @@ -507,7 +585,10 @@ export function showStandbyScreen() { } export function turnTvScreenOn() { - if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false; + if (projectionScreenInstance) { + projectionScreenInstance.isVisualizerActive = false; + projectionScreenInstance.isSlideshowActive = false; + } // Switch to ShaderMaterial for video playback const material = new THREE.ShaderMaterial({