Feature: image slideshow instead of videos
This commit is contained in:
parent
a6c8d9c8c6
commit
9aa12c3c33
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user