Feature: visualization selector
This commit is contained in:
parent
9aa12c3c33
commit
c6324d3ccd
@ -5,6 +5,7 @@ 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';
|
import { showToast } from '../core/ui-utils.js';
|
||||||
|
import { visualizerLibrary } from '../visualizers/index.js';
|
||||||
|
|
||||||
export class ConfigUI extends SceneFeature {
|
export class ConfigUI extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -110,6 +111,54 @@ export class ConfigUI extends SceneFeature {
|
|||||||
if (debugPanel) debugPanel.setVisibility(enabled);
|
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
|
// Blackout Toggle
|
||||||
createToggle('BLACKOUT', 'blackout');
|
createToggle('BLACKOUT', 'blackout');
|
||||||
|
|
||||||
@ -539,7 +588,9 @@ export class ConfigUI extends SceneFeature {
|
|||||||
guestCount: 150,
|
guestCount: 150,
|
||||||
blackout: true,
|
blackout: true,
|
||||||
djHat: 'None',
|
djHat: 'None',
|
||||||
debugPanelEnabled: false
|
debugPanelEnabled: false,
|
||||||
|
visualizerType: 'Classic Wave',
|
||||||
|
visualizerSeed: Math.random() * 1000
|
||||||
};
|
};
|
||||||
for (const key in defaults) {
|
for (const key in defaults) {
|
||||||
state.config[key] = defaults[key];
|
state.config[key] = defaults[key];
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import * as THREE from 'three';
|
|||||||
import { state } from '../state.js';
|
import { state } from '../state.js';
|
||||||
import { SceneFeature } from './SceneFeature.js';
|
import { SceneFeature } from './SceneFeature.js';
|
||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
|
import { visualizerLibrary } from '../visualizers/index.js';
|
||||||
|
|
||||||
// --- Shaders for Screen Effects ---
|
// --- Shaders for Screen Effects ---
|
||||||
const screenVertexShader = `
|
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;
|
let projectionScreenInstance = null;
|
||||||
|
|
||||||
export class ProjectionScreen extends SceneFeature {
|
export class ProjectionScreen extends SceneFeature {
|
||||||
@ -333,6 +272,9 @@ 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_seed) {
|
||||||
|
s.mesh.material.uniforms.u_seed.value = state.config.visualizerSeed || 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else if (this.isSlideshowActive) {
|
} else if (this.isSlideshowActive) {
|
||||||
@ -386,6 +328,11 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
activateVisualizer() {
|
activateVisualizer() {
|
||||||
this.isVisualizerActive = true;
|
this.isVisualizerActive = true;
|
||||||
state.tvScreenPowered = 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({
|
const material = new THREE.ShaderMaterial({
|
||||||
uniforms: {
|
uniforms: {
|
||||||
u_time: { value: 0.0 },
|
u_time: { value: 0.0 },
|
||||||
@ -393,10 +340,11 @@ export class ProjectionScreen extends SceneFeature {
|
|||||||
u_opacity: { value: state.screenOpacity },
|
u_opacity: { value: state.screenOpacity },
|
||||||
u_resolution: { value: new THREE.Vector2(1, 1) }, // Placeholder, set in applyMaterialToAll
|
u_resolution: { value: new THREE.Vector2(1, 1) }, // Placeholder, set in applyMaterialToAll
|
||||||
u_colors: { value: this.colorBuffer },
|
u_colors: { value: this.colorBuffer },
|
||||||
u_colorCount: { value: 0 }
|
u_colorCount: { value: 0 },
|
||||||
|
u_seed: { value: state.config.visualizerSeed || 0 }
|
||||||
},
|
},
|
||||||
vertexShader: screenVertexShader,
|
vertexShader: screenVertexShader,
|
||||||
fragmentShader: visualizerFragmentShader,
|
fragmentShader: fragShader,
|
||||||
side: THREE.DoubleSide,
|
side: THREE.DoubleSide,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
derivatives: true
|
derivatives: true
|
||||||
|
|||||||
@ -16,7 +16,9 @@ export function initState() {
|
|||||||
guestCount: 150,
|
guestCount: 150,
|
||||||
blackout: false,
|
blackout: false,
|
||||||
djHat: 'None', // 'None', 'Santa', 'Top Hat'
|
djHat: 'None', // 'None', 'Santa', 'Top Hat'
|
||||||
debugPanelEnabled: false
|
debugPanelEnabled: false,
|
||||||
|
visualizerType: 'Classic Wave',
|
||||||
|
visualizerSeed: Math.random() * 1000
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const saved = localStorage.getItem('partyConfig');
|
const saved = localStorage.getItem('partyConfig');
|
||||||
|
|||||||
74
party-stage/src/visualizers/ClassicVisualizer.js
Normal file
74
party-stage/src/visualizers/ClassicVisualizer.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { VisualizerInterface } from './VisualizerInterface.js';
|
||||||
|
|
||||||
|
const fragmentShader = `
|
||||||
|
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;
|
||||||
|
uniform float u_seed;
|
||||||
|
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 + u_seed);
|
||||||
|
float beatWave = sin(angle * 5.0 + u_time + u_seed * 0.1) * u_beat;
|
||||||
|
|
||||||
|
float hue = fract(u_time * 0.1 + d * 0.2 + u_seed * 0.01);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class ClassicVisualizer extends VisualizerInterface {
|
||||||
|
getName() {
|
||||||
|
return "Classic Wave";
|
||||||
|
}
|
||||||
|
|
||||||
|
getFragmentShader() {
|
||||||
|
return fragmentShader;
|
||||||
|
}
|
||||||
|
}
|
||||||
72
party-stage/src/visualizers/NebulaVisualizer.js
Normal file
72
party-stage/src/visualizers/NebulaVisualizer.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { VisualizerInterface } from './VisualizerInterface.js';
|
||||||
|
|
||||||
|
const fragmentShader = `
|
||||||
|
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;
|
||||||
|
uniform float u_seed;
|
||||||
|
varying vec2 vUv;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
vec2 p = (uv - 0.5) * 2.0;
|
||||||
|
float r = length(p);
|
||||||
|
float a = atan(p.y, p.x);
|
||||||
|
|
||||||
|
float t = u_time * 0.5 + u_seed;
|
||||||
|
|
||||||
|
float noise = sin(p.x * 10.0 + t) * cos(p.y * 10.0 - t) * 0.5 + 0.5;
|
||||||
|
float nebula = sin(r * 15.0 - t * 2.0 + noise * 5.0) * 0.5 + 0.5;
|
||||||
|
|
||||||
|
vec3 color1 = vec3(0.1, 0.0, 0.3);
|
||||||
|
vec3 color2 = vec3(0.8, 0.2, 0.5);
|
||||||
|
|
||||||
|
if (u_colorCount >= 2) {
|
||||||
|
color1 = u_colors[0];
|
||||||
|
color2 = u_colors[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 finalColor = mix(color1, color2, nebula);
|
||||||
|
|
||||||
|
// Add some pulsing highlights based on beat
|
||||||
|
float ring = smoothstep(0.4, 0.5, abs(fract(r * 2.0 - t * 4.0) - 0.5));
|
||||||
|
finalColor += color2 * ring * u_beat * 0.5;
|
||||||
|
|
||||||
|
// Edge glow
|
||||||
|
float edge = 1.0 - smoothstep(0.3, 0.5, r);
|
||||||
|
finalColor += color1 * edge * (0.2 + u_beat * 0.3);
|
||||||
|
|
||||||
|
finalColor *= (0.4 + u_beat * 0.6);
|
||||||
|
|
||||||
|
gl_FragColor = vec4(finalColor, mask * u_opacity);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export class NebulaVisualizer extends VisualizerInterface {
|
||||||
|
getName() {
|
||||||
|
return "Deep Nebula";
|
||||||
|
}
|
||||||
|
|
||||||
|
getFragmentShader() {
|
||||||
|
return fragmentShader;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
party-stage/src/visualizers/VisualizerInterface.js
Normal file
9
party-stage/src/visualizers/VisualizerInterface.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export class VisualizerInterface {
|
||||||
|
getName() {
|
||||||
|
return "Generic Visualizer";
|
||||||
|
}
|
||||||
|
|
||||||
|
getFragmentShader() {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
9
party-stage/src/visualizers/index.js
Normal file
9
party-stage/src/visualizers/index.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { ClassicVisualizer } from './ClassicVisualizer.js';
|
||||||
|
import { NebulaVisualizer } from './NebulaVisualizer.js';
|
||||||
|
|
||||||
|
export const visualizerLibrary = [
|
||||||
|
new ClassicVisualizer(),
|
||||||
|
new NebulaVisualizer(),
|
||||||
|
];
|
||||||
|
|
||||||
|
export default visualizerLibrary;
|
||||||
Loading…
Reference in New Issue
Block a user