From 293834b704db1c51295102793462cf66170e5b37 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Mon, 17 Nov 2025 14:38:06 +0100 Subject: [PATCH] Feature: TV screen CRT has a warm up and power down sequence --- tv-player/src/core/animate.js | 5 +-- tv-player/src/core/video-player.js | 11 +++--- tv-player/src/scene/tv-set.js | 47 ++++++++++++++++++++++++- tv-player/src/shaders/screen-shaders.js | 37 +++++++++++++++++-- tv-player/src/state.js | 9 +++++ 5 files changed, 99 insertions(+), 10 deletions(-) diff --git a/tv-player/src/core/animate.js b/tv-player/src/core/animate.js index 9f523a9..46bb393 100644 --- a/tv-player/src/core/animate.js +++ b/tv-player/src/core/animate.js @@ -1,8 +1,8 @@ import * as THREE from 'three'; -import { updateVcrDisplay } from '../scene/vcr-display.js'; import { updateDoor } from '../scene/door.js'; +import { updateVcrDisplay } from '../scene/vcr-display.js'; import { state } from '../state.js'; - +import { updateScreenEffect } from '../scene/tv-set.js' function updateCamera() { const globalTime = Date.now() * 0.00003; @@ -166,6 +166,7 @@ export function animate() { updateBooks(); updateDoor(); updatePictureFrame(); + updateScreenEffect(); // RENDER! state.renderer.render(state.scene, state.camera); diff --git a/tv-player/src/core/video-player.js b/tv-player/src/core/video-player.js index 4b028fc..e1f596f 100644 --- a/tv-player/src/core/video-player.js +++ b/tv-player/src/core/video-player.js @@ -1,6 +1,6 @@ import * as THREE from 'three'; import { state } from '../state.js'; -import { turnTvScreenOff, turnTvScreenOn } from '../scene/tv-set.js'; +import { turnTvScreenOff, turnTvScreenOn, setScreenEffect } from '../scene/tv-set.js'; // --- Play video by index --- export function playVideoByIndex(index) { @@ -15,8 +15,10 @@ export function playVideoByIndex(index) { if (index < 0 || index >= state.videoUrls.length) { console.info('End of playlist reached. Reload tapes to start again.'); - state.screenLight.intensity = 0.0; - turnTvScreenOff(); + setScreenEffect(2, () => { // Power-down effect + state.screenLight.intensity = 0.0; + turnTvScreenOff(); + }); state.isVideoLoaded = false; state.lastUpdateTime = -1; // force VCR to redraw return; @@ -41,7 +43,8 @@ export function playVideoByIndex(index) { // 2. Apply the video texture to the screen mesh turnTvScreenOn(); - // 3. Start playback + // 3. Start playback and trigger the warm-up effect simultaneously + setScreenEffect(1); // Trigger warm-up state.videoElement.play().then(() => { state.isVideoLoaded = true; // Use the defined base intensity for screen glow diff --git a/tv-player/src/scene/tv-set.js b/tv-player/src/scene/tv-set.js index 4a6e531..bd88eec 100644 --- a/tv-player/src/scene/tv-set.js +++ b/tv-player/src/scene/tv-set.js @@ -181,7 +181,9 @@ export function turnTvScreenOn() { state.tvScreen.material = new THREE.ShaderMaterial({ uniforms: { - videoTexture: { value: state.videoTexture } + videoTexture: { value: state.videoTexture }, + u_effect_type: { value: 0.0 }, + u_effect_strength: { value: 0.0 }, }, vertexShader: screenVertexShader, fragmentShader: screenFragmentShader, @@ -189,4 +191,47 @@ export function turnTvScreenOn() { }); state.tvScreen.material.needsUpdate = true; + setScreenEffect(1); // Trigger warm-up +} + +/** + * Controls the warm-up and power-down effects on the TV screen. + * @param {number} effectType - 0 normal, 1 for warm-up, 2 for power-down. + * @param {function} onComplete - Optional callback when the animation finishes. + */ +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; +} + +/** + * Updates the screen effect animation. Should be called in the main render loop. + */ +export function updateScreenEffect() { + if (!state.screenEffect.active) return; + + const material = state.tvScreen.material; + if (!material.uniforms) return; + + const elapsedTime = (state.clock.getElapsedTime() * 1000) - state.screenEffect.startTime; + const progress = Math.min(elapsedTime / state.screenEffect.duration, 1.0); + + const easedProgress = state.screenEffect.easing(progress); + + material.uniforms.u_effect_type.value = state.screenEffect.type; + material.uniforms.u_effect_strength.value = easedProgress; + + if (progress >= 1.0) { + state.screenEffect.active = false; + material.uniforms.u_effect_strength.value = (state.screenEffect.type === 2) ? 1.0 : 0.0; // Final state + if (state.screenEffect.onComplete) { + state.screenEffect.onComplete(); + } + material.uniforms.u_effect_type.value = 0.0; // Reset effect type + } } \ No newline at end of file diff --git a/tv-player/src/shaders/screen-shaders.js b/tv-player/src/shaders/screen-shaders.js index 70319f9..be15ff5 100644 --- a/tv-player/src/shaders/screen-shaders.js +++ b/tv-player/src/shaders/screen-shaders.js @@ -10,10 +10,41 @@ void main() { export const screenFragmentShader = ` varying vec2 vUv; uniform sampler2D videoTexture; +uniform float u_effect_type; // 0: none, 1: warmup, 2: powerdown +uniform float u_effect_strength; // 0.0 to 1.0 void main() { - // Sample the video texture - gl_FragColor = texture2D(videoTexture, vUv); + vec2 centeredUv = vUv - 0.5; + vec4 finalColor; + + if (u_effect_type < 0.5) { // No effect + finalColor = texture2D(videoTexture, vUv); + } else if (u_effect_type < 1.5) { // Warm-up effect + // A bright dot expands to reveal the screen content + float effectRadius = u_effect_strength * 0.75; // Max radius of 0.75 (sqrt(0.5*0.5 + 0.5*0.5)) + float distanceToCenter = length(centeredUv); + + // Smoothly transition the edge of the circle + float vignette = smoothstep(effectRadius, effectRadius - 0.1, distanceToCenter); + vec4 videoColor = texture2D(videoTexture, vUv); + + finalColor = videoColor * vignette * u_effect_strength; // Fade in brightness + + } else { // Power-down effect + // The image collapses into a bright horizontal line and fades + float collapseFactor = 1.0 - u_effect_strength; + + // Squeeze the UVs vertically + vec2 squeezedUv = vec2(vUv.x, 0.5 + (vUv.y - 0.5) * collapseFactor); + vec4 videoColor = texture2D(videoTexture, squeezedUv); + + // Create a bright glow where the line is + float lineGlow = pow(1.0 - abs(centeredUv.y) / (0.5 * collapseFactor + 0.01), 20.0); + + finalColor = videoColor * collapseFactor + vec4(0.8, 0.9, 1.0, 1.0) * lineGlow * u_effect_strength; + + } + + gl_FragColor = finalColor; } `; - diff --git a/tv-player/src/state.js b/tv-player/src/state.js index 8cb5106..22db407 100644 --- a/tv-player/src/state.js +++ b/tv-player/src/state.js @@ -8,12 +8,21 @@ export function initState() { scene: null, camera: null, renderer: null, + clock: new THREE.Clock(), tvScreen: null, videoTexture: null, screenLight: null, lampLightPoint: null, lampLightSpot: null, effectsManager: null, + screenEffect: { + active: false, + type: 0, + startTime: 0, + duration: 1000, // in ms + onComplete: null, + easing: (t) => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t, // easeInOutQuad + }, // VCR Display lastUpdateTime: -1,