Feature: projection screen playing videos or music visualizer
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { SceneFeature } from './SceneFeature.js';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
|
||||
// --- Shaders for Screen Effects ---
|
||||
const screenVertexShader = `
|
||||
varying vec2 vUv;
|
||||
void main() {
|
||||
vUv = uv;
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
const screenFragmentShader = `
|
||||
uniform sampler2D videoTexture;
|
||||
uniform float u_effect_type;
|
||||
uniform float u_effect_strength;
|
||||
uniform float u_time;
|
||||
varying vec2 vUv;
|
||||
|
||||
float random(vec2 st) {
|
||||
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 uv = vUv;
|
||||
vec4 color = texture2D(videoTexture, uv);
|
||||
|
||||
// Effect 1: Static/Noise (Power On/Off)
|
||||
if (u_effect_type > 0.0) {
|
||||
float noise = random(uv + u_time);
|
||||
vec3 noiseColor = vec3(noise);
|
||||
color.rgb = mix(color.rgb, noiseColor, u_effect_strength);
|
||||
}
|
||||
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`;
|
||||
|
||||
const visualizerFragmentShader = `
|
||||
uniform float u_time;
|
||||
uniform float u_beat;
|
||||
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() {
|
||||
vec2 uv = vUv;
|
||||
float dist = length(uv - 0.5);
|
||||
float angle = atan(uv.y - 0.5, uv.x - 0.5);
|
||||
|
||||
float wave = sin(dist * 20.0 - u_time * 2.0);
|
||||
float beatWave = sin(angle * 5.0 + u_time) * u_beat;
|
||||
|
||||
float hue = fract(u_time * 0.1 + dist * 0.2);
|
||||
float val = 0.5 + 0.5 * sin(wave + beatWave);
|
||||
|
||||
gl_FragColor = vec4(hsv2rgb(vec3(hue, 0.8, val)), 1.0);
|
||||
}
|
||||
`;
|
||||
|
||||
let projectionScreenInstance = null;
|
||||
|
||||
export class ProjectionScreen extends SceneFeature {
|
||||
constructor() {
|
||||
super();
|
||||
projectionScreenInstance = this;
|
||||
this.isVisualizerActive = false;
|
||||
sceneFeatureManager.register(this);
|
||||
}
|
||||
|
||||
init() {
|
||||
// --- Initialize State ---
|
||||
state.tvScreenPowered = false;
|
||||
state.screenEffect = {
|
||||
active: false,
|
||||
type: 0,
|
||||
startTime: 0,
|
||||
duration: 1000,
|
||||
easing: (t) => t,
|
||||
onComplete: null
|
||||
};
|
||||
state.originalScreenIntensity = 2.0;
|
||||
|
||||
// Ensure video element exists
|
||||
if (!state.videoElement) {
|
||||
state.videoElement = document.createElement('video');
|
||||
state.videoElement.crossOrigin = 'anonymous';
|
||||
state.videoElement.playsInline = true;
|
||||
state.videoElement.style.display = 'none';
|
||||
document.body.appendChild(state.videoElement);
|
||||
}
|
||||
|
||||
// --- Create Screen Mesh ---
|
||||
// 16:9 Aspect Ratio, large size
|
||||
const width = 10;
|
||||
const height = width * (9 / 16);
|
||||
const geometry = new THREE.PlaneGeometry(width, height);
|
||||
|
||||
// Initial black material
|
||||
const material = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
||||
|
||||
this.mesh = new THREE.Mesh(geometry, material);
|
||||
// High enough to be seen
|
||||
this.mesh.position.set(0, 5.5, -20.5);
|
||||
state.scene.add(this.mesh);
|
||||
|
||||
state.tvScreen = this.mesh;
|
||||
|
||||
// --- Screen Light ---
|
||||
// A light that projects the screen's color/ambiance into the room
|
||||
state.screenLight = new THREE.PointLight(0xffffff, 0, 25);
|
||||
state.screenLight.position.set(0, 5.5, -18);
|
||||
state.screenLight.castShadow = true;
|
||||
state.screenLight.shadow.mapSize.width = 512;
|
||||
state.screenLight.shadow.mapSize.height = 512;
|
||||
state.scene.add(state.screenLight);
|
||||
}
|
||||
|
||||
update(deltaTime) {
|
||||
updateScreenEffect();
|
||||
|
||||
if (this.isVisualizerActive && state.tvScreen.material.uniforms) {
|
||||
state.tvScreen.material.uniforms.u_time.value = state.clock.getElapsedTime();
|
||||
const beat = (state.music && state.music.beatIntensity) ? state.music.beatIntensity : 0.0;
|
||||
state.tvScreen.material.uniforms.u_beat.value = beat;
|
||||
|
||||
// Sync light to beat
|
||||
state.screenLight.intensity = state.originalScreenIntensity * (0.5 + beat * 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
onPartyStart() {
|
||||
// Hide load button during playback
|
||||
if (state.loadTapeButton) state.loadTapeButton.classList.add('hidden');
|
||||
|
||||
// If no video loaded, start visualizer
|
||||
if (!state.isVideoLoaded) {
|
||||
this.activateVisualizer();
|
||||
}
|
||||
}
|
||||
|
||||
onPartyEnd() {
|
||||
// Show load button if no video is loaded
|
||||
if (state.loadTapeButton && !state.isVideoLoaded) state.loadTapeButton.classList.remove('hidden');
|
||||
|
||||
if (this.isVisualizerActive) {
|
||||
this.deactivateVisualizer();
|
||||
}
|
||||
}
|
||||
|
||||
activateVisualizer() {
|
||||
this.isVisualizerActive = true;
|
||||
state.tvScreen.visible = true;
|
||||
state.tvScreenPowered = true;
|
||||
state.tvScreen.material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
u_time: { value: 0.0 },
|
||||
u_beat: { value: 0.0 }
|
||||
},
|
||||
vertexShader: screenVertexShader,
|
||||
fragmentShader: visualizerFragmentShader,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
state.screenLight.intensity = state.originalScreenIntensity;
|
||||
}
|
||||
|
||||
deactivateVisualizer() {
|
||||
this.isVisualizerActive = false;
|
||||
turnTvScreenOff();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Exported Control Functions ---
|
||||
|
||||
export function turnTvScreenOn() {
|
||||
if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false;
|
||||
|
||||
if (state.tvScreen.material) {
|
||||
state.tvScreen.material.dispose();
|
||||
}
|
||||
|
||||
state.tvScreen.visible = true;
|
||||
|
||||
// Switch to ShaderMaterial for video playback
|
||||
state.tvScreen.material = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
videoTexture: { value: state.videoTexture },
|
||||
u_effect_type: { value: 0.0 },
|
||||
u_effect_strength: { value: 0.0 },
|
||||
u_time: { value: 0.0 },
|
||||
},
|
||||
vertexShader: screenVertexShader,
|
||||
fragmentShader: screenFragmentShader,
|
||||
side: THREE.DoubleSide
|
||||
});
|
||||
|
||||
state.tvScreen.material.needsUpdate = true;
|
||||
|
||||
if (!state.tvScreenPowered) {
|
||||
state.tvScreenPowered = true;
|
||||
setScreenEffect(1); // Trigger power on static effect
|
||||
}
|
||||
}
|
||||
|
||||
export function turnTvScreenOff() {
|
||||
if (state.tvScreenPowered) {
|
||||
state.tvScreenPowered = false;
|
||||
setScreenEffect(2, () => {
|
||||
// Revert to black material or hide
|
||||
state.tvScreen.material = new THREE.MeshBasicMaterial({ color: 0x000000 });
|
||||
state.screenLight.intensity = 0.0;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function setScreenEffect(effectType, onComplete) {
|
||||
const material = state.tvScreen.material;
|
||||
if (!material.uniforms) return;
|
||||
|
||||
state.screenEffect.active = true;
|
||||
state.screenEffect.type = effectType;
|
||||
state.screenEffect.startTime = state.clock.getElapsedTime() * 1000;
|
||||
state.screenEffect.onComplete = onComplete;
|
||||
}
|
||||
|
||||
export function updateScreenEffect() {
|
||||
if (!state.screenEffect || !state.screenEffect.active) return;
|
||||
|
||||
const material = state.tvScreen.material;
|
||||
if (!material || !material.uniforms) return;
|
||||
|
||||
// Update time uniform for noise
|
||||
material.uniforms.u_time.value = state.clock.getElapsedTime();
|
||||
|
||||
const elapsedTime = (state.clock.getElapsedTime() * 1000) - state.screenEffect.startTime;
|
||||
const progress = Math.min(elapsedTime / state.screenEffect.duration, 1.0);
|
||||
|
||||
// Simple linear fade for effect strength
|
||||
// Type 1 (On): 1.0 -> 0.0
|
||||
// Type 2 (Off): 0.0 -> 1.0
|
||||
let strength = progress;
|
||||
if (state.screenEffect.type === 1) {
|
||||
strength = 1.0 - progress;
|
||||
}
|
||||
|
||||
material.uniforms.u_effect_type.value = state.screenEffect.type;
|
||||
material.uniforms.u_effect_strength.value = strength;
|
||||
|
||||
if (progress >= 1.0) {
|
||||
state.screenEffect.active = false;
|
||||
if (state.screenEffect.onComplete) {
|
||||
state.screenEffect.onComplete();
|
||||
}
|
||||
// Reset effect uniforms
|
||||
material.uniforms.u_effect_type.value = 0.0;
|
||||
material.uniforms.u_effect_strength.value = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
new ProjectionScreen();
|
||||
@@ -2,6 +2,7 @@ import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import floorTextureUrl from '/textures/floor.png';
|
||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||
import { initVideoUI } from '../core/video-player.js';
|
||||
// Scene Features registered here:
|
||||
import { CameraManager } from './camera-manager.js';
|
||||
import { LightBall } from './light-ball.js';
|
||||
@@ -15,11 +16,13 @@ import { ReproWall } from './repro-wall.js';
|
||||
import { StageLights } from './stage-lights.js';
|
||||
import { MusicConsole } from './music-console.js';
|
||||
import { DJ } from './dj.js';
|
||||
import { ProjectionScreen } from './projection-screen.js';
|
||||
// Scene Features ^^^
|
||||
|
||||
// --- Scene Modeling Function ---
|
||||
export function createSceneObjects() {
|
||||
sceneFeatureManager.init();
|
||||
initVideoUI();
|
||||
|
||||
// --- Materials (MeshPhongMaterial) ---
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class WallCurtain extends SceneFeature {
|
||||
init() {
|
||||
// --- Curtain Properties ---
|
||||
const naveWidth = 12;
|
||||
const naveHeight = 7;
|
||||
const naveHeight = 10;
|
||||
const stageHeight = 1.5;
|
||||
const curtainWidth = naveWidth; // Span the width of the nave
|
||||
const curtainHeight = naveHeight - stageHeight; // Hang from the ceiling down to the stage
|
||||
@@ -54,7 +54,7 @@ export class WallCurtain extends SceneFeature {
|
||||
};
|
||||
|
||||
// Place a single large curtain behind the stage
|
||||
const backWallZ = -20;
|
||||
const backWallZ = -21;
|
||||
const curtainY = stageHeight + curtainHeight / 2;
|
||||
const curtainPosition = new THREE.Vector3(0, curtainY, backWallZ + 0.1);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user