Feature: image slideshow instead of videos

This commit is contained in:
Dejvino 2026-05-24 13:45:10 +02:00
parent a6c8d9c8c6
commit 9aa12c3c33
2 changed files with 153 additions and 6 deletions

View File

@ -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');
li.innerText = '(No tapes loaded)';
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 media loaded)';
this.tapeList.appendChild(li);
}
}

View File

@ -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,11 +361,15 @@ 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) {
if (state.isSlideshowLoaded) {
this.activateSlideshow();
} else {
this.activateVisualizer();
}
}
}
onPartyEnd() {
// Show load button if no video is loaded
@ -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({