Feature: visualization selector

This commit is contained in:
Dejvino
2026-05-24 14:08:38 +02:00
parent 9aa12c3c33
commit c6324d3ccd
7 changed files with 231 additions and 66 deletions
+52 -1
View File
@@ -5,6 +5,7 @@ import sceneFeatureManager from './SceneFeatureManager.js';
import { MediaStorage } from '../core/media-storage.js';
import { showStandbyScreen } from './projection-screen.js';
import { showToast } from '../core/ui-utils.js';
import { visualizerLibrary } from '../visualizers/index.js';
export class ConfigUI extends SceneFeature {
constructor() {
@@ -110,6 +111,54 @@ export class ConfigUI extends SceneFeature {
if (debugPanel) debugPanel.setVisibility(enabled);
});
// Visualizer Selection
const vizRow = document.createElement('div');
vizRow.style.display = 'flex';
vizRow.style.alignItems = 'center';
vizRow.style.justifyContent = 'space-between';
vizRow.style.marginTop = '10px';
const vizLabel = document.createElement('label');
vizLabel.innerText = 'Visualizer';
const vizSelect = document.createElement('select');
visualizerLibrary.forEach(viz => {
const opt = document.createElement('option');
opt.value = viz.getName();
opt.innerText = viz.getName();
if (opt.value === state.config.visualizerType) opt.selected = true;
vizSelect.appendChild(opt);
});
vizSelect.onchange = (e) => {
state.config.visualizerType = e.target.value;
saveConfig();
};
vizRow.appendChild(vizLabel);
vizRow.appendChild(vizSelect);
rightContainer.appendChild(vizRow);
// Visualizer Seed
const seedRow = document.createElement('div');
seedRow.style.display = 'flex';
seedRow.style.alignItems = 'center';
seedRow.style.justifyContent = 'space-between';
const seedLabel = document.createElement('label');
seedLabel.innerText = 'Visualizer Seed';
const seedInput = document.createElement('input');
seedInput.type = 'number';
seedInput.step = '0.001';
seedInput.value = state.config.visualizerSeed || 0;
seedInput.style.width = '80px';
seedInput.onchange = (e) => {
state.config.visualizerSeed = parseFloat(e.target.value);
saveConfig();
};
seedRow.appendChild(seedLabel);
seedRow.appendChild(seedInput);
rightContainer.appendChild(seedRow);
// Blackout Toggle
createToggle('BLACKOUT', 'blackout');
@@ -539,7 +588,9 @@ export class ConfigUI extends SceneFeature {
guestCount: 150,
blackout: true,
djHat: 'None',
debugPanelEnabled: false
debugPanelEnabled: false,
visualizerType: 'Classic Wave',
visualizerSeed: Math.random() * 1000
};
for (const key in defaults) {
state.config[key] = defaults[key];
+12 -64
View File
@@ -2,6 +2,7 @@ import * as THREE from 'three';
import { state } from '../state.js';
import { SceneFeature } from './SceneFeature.js';
import sceneFeatureManager from './SceneFeatureManager.js';
import { visualizerLibrary } from '../visualizers/index.js';
// --- Shaders for Screen Effects ---
const screenVertexShader = `
@@ -59,68 +60,6 @@ void main() {
}
`;
const visualizerFragmentShader = `
uniform float u_time;
uniform float u_beat;
uniform float u_opacity;
uniform vec2 u_resolution;
uniform vec3 u_colors[16];
uniform int u_colorCount;
varying vec2 vUv;
vec3 hsv2rgb(vec3 c) {
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void main() {
float ledCountX = u_resolution.x;
float ledCountY = u_resolution.y;
vec2 gridUV = vec2(vUv.x * ledCountX, vUv.y * ledCountY);
vec2 gridDeriv = fwidth(gridUV);
float gridDensity = max(gridDeriv.x, gridDeriv.y);
float blurFactor = smoothstep(0.3, 0.8, gridDensity);
vec2 cell = fract(gridUV);
vec2 uv = mix((floor(gridUV) + 0.5) / vec2(ledCountX, ledCountY), vUv, blurFactor);
float dist = distance(cell, vec2(0.5));
float edgeSoftness = clamp(gridDensity * 1.5, 0.0, 0.5);
float mask = 1.0 - smoothstep(0.35 - edgeSoftness, 0.45 + edgeSoftness, dist);
mask = mix(mask, 1.0, blurFactor);
float d = length(uv - 0.5);
float angle = atan(uv.y - 0.5, uv.x - 0.5);
float wave = sin(d * 20.0 - u_time * 2.0);
float beatWave = sin(angle * 5.0 + u_time) * u_beat;
float hue = fract(u_time * 0.1 + d * 0.2);
float val = 0.5 + 0.5 * sin(wave + beatWave);
vec3 finalColor;
if (u_colorCount > 0) {
float indexFloat = hue * float(u_colorCount);
int index = int(mod(indexFloat, float(u_colorCount)));
vec3 c = vec3(0.0);
for (int i = 0; i < 16; i++) {
if (i == index) {
c = u_colors[i];
break;
}
}
finalColor = c * val;
} else {
finalColor = hsv2rgb(vec3(hue, 0.8, val));
}
gl_FragColor = vec4(finalColor, mask * u_opacity);
}
`;
let projectionScreenInstance = null;
export class ProjectionScreen extends SceneFeature {
@@ -333,6 +272,9 @@ export class ProjectionScreen extends SceneFeature {
if (s.mesh.material.uniforms.u_colorCount) {
s.mesh.material.uniforms.u_colorCount.value = colorCount;
}
if (s.mesh.material.uniforms.u_seed) {
s.mesh.material.uniforms.u_seed.value = state.config.visualizerSeed || 0;
}
}
});
} else if (this.isSlideshowActive) {
@@ -386,6 +328,11 @@ export class ProjectionScreen extends SceneFeature {
activateVisualizer() {
this.isVisualizerActive = true;
state.tvScreenPowered = true;
const visualizerType = state.config.visualizerType || 'Classic Wave';
const visualizer = visualizerLibrary.find(v => v.getName() === visualizerType) || visualizerLibrary[0];
const fragShader = visualizer.getFragmentShader();
const material = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0.0 },
@@ -393,10 +340,11 @@ export class ProjectionScreen extends SceneFeature {
u_opacity: { value: state.screenOpacity },
u_resolution: { value: new THREE.Vector2(1, 1) }, // Placeholder, set in applyMaterialToAll
u_colors: { value: this.colorBuffer },
u_colorCount: { value: 0 }
u_colorCount: { value: 0 },
u_seed: { value: state.config.visualizerSeed || 0 }
},
vertexShader: screenVertexShader,
fragmentShader: visualizerFragmentShader,
fragmentShader: fragShader,
side: THREE.DoubleSide,
transparent: true,
derivatives: true