TV Turn Off Animation Experiment

Recreating the classic CRT television turn-off effect using modern CSS transforms and GSAP animations

TV Turn Off Animation Experiment
Animation Engineering
Url
https://codepen.io/francescostella/pen/ONaWvZ
Period

TV Turn Off Animation Experiment

A nostalgic experiment recreating the classic CRT television turn-off effect that many remember from old TVs. This animation demonstrates advanced CSS transforms combined with GSAP timeline management to create a convincing retro effect.

Problem Statement

Modern web interfaces often lack the tactile, nostalgic feedback that physical devices provided. The challenge was to:

Technical Implementation

HTML Structure

<section class="screen">
  <div class="content">
    <h1>Welcome to the TV Experience</h1>
    <p>
      This content behaves like a normal web page but can be "turned off" like
      an old CRT television.
    </p>
    <a href="#demo">Interactive Demo Link</a>
  </div>
</section>

<button id="switcher-tv">Turn on/off</button>

CSS Foundation

$color-text: #e1eef6;
$color-link: #ff5f2e;
$color-link-hover: #fcbe32;
$black: #111111;

body {
  font-family: sans-serif;
  background-color: $black;
  margin: 0;
  padding: 0;
  overflow: hidden;
}

.screen {
  width: 100vw;
  height: 100vh;
  background: radial-gradient(
    ellipse at center,
    #1e3c72 0%,
    #2a5298 50%,
    #1e3c72 100%
  );
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;

  // Simulate CRT scanlines
  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: repeating-linear-gradient(
      0deg,
      transparent,
      transparent 2px,
      rgba(0, 0, 0, 0.1) 2px,
      rgba(0, 0, 0, 0.1) 4px
    );
    pointer-events: none;
  }

  // Subtle CRT curvature effect
  &::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: radial-gradient(
      ellipse at center,
      transparent 70%,
      rgba(0, 0, 0, 0.3) 100%
    );
    pointer-events: none;
  }
}

.content {
  color: $color-text;
  text-align: center;
  z-index: 1;
  max-width: 600px;
  padding: 2rem;

  h1 {
    font-size: 2.5rem;
    margin-bottom: 1rem;
    text-shadow: 0 0 10px rgba(225, 238, 246, 0.5);
  }

  p {
    font-size: 1.2rem;
    line-height: 1.6;
    margin-bottom: 2rem;
  }

  a {
    color: $color-link;
    text-decoration: none;
    padding: 0.8rem 1.5rem;
    border: 2px solid $color-link;
    border-radius: 4px;
    transition: all 0.3s ease;

    &:hover {
      color: $color-link-hover;
      border-color: $color-link-hover;
      box-shadow: 0 0 20px rgba(252, 190, 50, 0.3);
    }
  }
}

button {
  position: fixed;
  right: 20px;
  bottom: 20px;
  padding: 12px 24px;
  background: rgba(255, 255, 255, 0.1);
  color: white;
  border: 2px solid rgba(255, 255, 255, 0.3);
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  backdrop-filter: blur(10px);
  transition: all 0.3s ease;
  z-index: 100;

  &:hover {
    background: rgba(255, 255, 255, 0.2);
    border-color: rgba(255, 255, 255, 0.5);
    transform: translateY(-2px);
  }
}

GSAP Animation Timeline

(function () {
  var SELECTOR_SCREEN_ELEMENT = '.screen';
  var SELECTOR_SWITCHER_TV = '#switcher-tv';

  var isTurnedOn = true;
  var timeline;

  function buildTimeline() {
    timeline = new TimelineMax({
      paused: true,
    });

    // Phase 1: Screen collapses to horizontal line (classic CRT effect)
    timeline
      .to(SELECTOR_SCREEN_ELEMENT, 0.2, {
        width: '100vw',
        height: '2px',
        background: '#ffffff',
        ease: Power2.easeOut,
      })
      // Phase 2: Line shrinks horizontally to center
      .to(SELECTOR_SCREEN_ELEMENT, 0.2, {
        width: '0px',
        left: '50%',
        marginLeft: '0px',
        ease: Power2.easeIn,
      })
      // Phase 3: Final disappearance
      .to(SELECTOR_SCREEN_ELEMENT, 0.1, {
        height: '0px',
        opacity: 0,
        ease: Power2.easeIn,
      });
  }

  function turnOff() {
    if (isTurnedOn) {
      timeline.play();
      isTurnedOn = false;
      document.querySelector(SELECTOR_SWITCHER_TV).textContent = 'Turn on';
    }
  }

  function turnOn() {
    if (!isTurnedOn) {
      timeline.reverse();
      isTurnedOn = true;
      document.querySelector(SELECTOR_SWITCHER_TV).textContent = 'Turn off';
    }
  }

  function toggleTV() {
    if (isTurnedOn) {
      turnOff();
    } else {
      turnOn();
    }
  }

  // Initialize
  buildTimeline();

  // Event listeners
  document
    .querySelector(SELECTOR_SWITCHER_TV)
    .addEventListener('click', toggleTV);

  // Optional: Keyboard shortcut (Space bar)
  document.addEventListener('keydown', function (e) {
    if (e.code === 'Space') {
      e.preventDefault();
      toggleTV();
    }
  });
})();

Advanced Features

Performance Optimizations

// Optimize for 60fps by using transform3d
function buildOptimizedTimeline() {
  timeline = new TimelineMax({
    paused: true,
    onStart: function () {
      // Enable hardware acceleration
      TweenMax.set(SELECTOR_SCREEN_ELEMENT, {
        transformStyle: 'preserve-3d',
        backfaceVisibility: 'hidden',
      });
    },
  });

  timeline
    .to(SELECTOR_SCREEN_ELEMENT, 0.2, {
      scaleY: 0.001,
      scaleX: 1,
      transformOrigin: 'center center',
      ease: Power2.easeOut,
    })
    .to(SELECTOR_SCREEN_ELEMENT, 0.2, {
      scaleX: 0.001,
      ease: Power2.easeIn,
    })
    .to(SELECTOR_SCREEN_ELEMENT, 0.1, {
      opacity: 0,
      ease: Power2.easeIn,
    });
}

Enhanced Audio Integration

// Add authentic CRT sound effects
class TVSoundManager {
  constructor() {
    this.audioContext = new (window.AudioContext ||
      window.webkitAudioContext)();
    this.createTurnOffSound();
  }

  createTurnOffSound() {
    // Synthesize the classic TV "pop" sound
    this.turnOffSound = this.audioContext.createOscillator();
    this.gainNode = this.audioContext.createGain();

    this.turnOffSound.connect(this.gainNode);
    this.gainNode.connect(this.audioContext.destination);

    // Configure the "pop" frequency and envelope
    this.turnOffSound.frequency.setValueAtTime(
      800,
      this.audioContext.currentTime
    );
    this.turnOffSound.frequency.exponentialRampToValueAtTime(
      100,
      this.audioContext.currentTime + 0.1
    );

    this.gainNode.gain.setValueAtTime(0.3, this.audioContext.currentTime);
    this.gainNode.gain.exponentialRampToValueAtTime(
      0.01,
      this.audioContext.currentTime + 0.3
    );
  }

  playTurnOffSound() {
    this.createTurnOffSound();
    this.turnOffSound.start();
    this.turnOffSound.stop(this.audioContext.currentTime + 0.3);
  }
}

Key Technical Insights

Animation Physics

  1. Authentic CRT Behavior: Real CRT TVs collapsed vertically first (electron beam shuts off) then horizontally
  2. Easing Functions: Power2.easeOut for initial collapse feels natural, Power2.easeIn for final disappearance
  3. Transform vs Position: Using transforms instead of changing width/height provides better performance

Browser Compatibility

// Graceful degradation for older browsers
function checkGSAPSupport() {
  if (typeof TweenMax === 'undefined') {
    // Fallback to CSS transitions
    document.querySelector(SELECTOR_SCREEN_ELEMENT).style.transition =
      'all 0.5s ease';

    function fallbackToggle() {
      const screen = document.querySelector(SELECTOR_SCREEN_ELEMENT);
      screen.style.transform = isTurnedOn ? 'scale(0)' : 'scale(1)';
      isTurnedOn = !isTurnedOn;
    }

    document
      .querySelector(SELECTOR_SWITCHER_TV)
      .addEventListener('click', fallbackToggle);
  }
}

Real-World Applications

Loading Screen Enhancement

// Use as a loading screen transition
class TVLoadingScreen {
  constructor(onComplete) {
    this.onComplete = onComplete;
    this.setupLoadingContent();
  }

  setupLoadingContent() {
    const content = document.querySelector('.content');
    content.innerHTML = `
      <div class="loading-spinner"></div>
      <h1>Loading Experience...</h1>
      <p>Please wait while we prepare your content</p>
    `;
  }

  completeLoading() {
    // Turn off the TV effect, then load new content
    timeline.play().then(() => {
      this.onComplete();
    });
  }
}

Page Transition Effect

// Smooth page transitions with TV effect
function navigateWithTVEffect(newUrl) {
  turnOff().then(() => {
    window.location.href = newUrl;
  });
}

// Usage
document.querySelectorAll('a[data-tv-transition]').forEach((link) => {
  link.addEventListener('click', (e) => {
    e.preventDefault();
    navigateWithTVEffect(link.href);
  });
});

Performance Metrics

Benchmark Results

Mobile Optimization

// Simplified animation for mobile devices
function isMobileDevice() {
  return /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
    navigator.userAgent
  );
}

if (isMobileDevice()) {
  // Simpler animation for mobile performance
  timeline.to(SELECTOR_SCREEN_ELEMENT, 0.3, {
    opacity: 0,
    scale: 0,
    ease: Power2.easeOut,
  });
}

Future Enhancements

WebGL Integration

// Three.js implementation for enhanced visual effects
class WebGLTVEffect {
  constructor() {
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    this.renderer = new THREE.WebGLRenderer();

    this.setupCRTShader();
  }

  setupCRTShader() {
    // Custom shader for authentic CRT distortion
    this.crtMaterial = new THREE.ShaderMaterial({
      uniforms: {
        time: { value: 0 },
        resolution: { value: new THREE.Vector2() },
        scanlineIntensity: { value: 0.04 },
      },
      vertexShader: /* glsl */ `
        void main() {
          gl_Position = vec4(position, 1.0);
        }
      `,
      fragmentShader: /* glsl */ `
        uniform float time;
        uniform vec2 resolution;
        uniform float scanlineIntensity;
        
        void main() {
          vec2 uv = gl_FragCoord.xy / resolution.xy;
          
          // Scanline effect
          float scanline = sin(uv.y * 800.0) * scanlineIntensity;
          
          // CRT curvature
          vec2 curved = uv;
          curved = curved * 2.0 - 1.0;
          curved = curved * (1.0 + length(curved) * 0.1);
          curved = curved * 0.5 + 0.5;
          
          gl_FragColor = vec4(vec3(1.0 - scanline), 1.0);
        }
      `,
    });
  }
}

React Component Implementation

// React hook for TV effect
import { useRef, useEffect } from 'react';
import { gsap } from 'gsap';

export const useTVEffect = () => {
  const screenRef = useRef(null);
  const timelineRef = useRef(null);

  useEffect(() => {
    timelineRef.current = gsap
      .timeline({ paused: true })
      .to(screenRef.current, {
        duration: 0.2,
        scaleY: 0.001,
        ease: 'power2.out',
      })
      .to(screenRef.current, {
        duration: 0.2,
        scaleX: 0.001,
        ease: 'power2.in',
      })
      .to(screenRef.current, {
        duration: 0.1,
        opacity: 0,
        ease: 'power2.in',
      });
  }, []);

  const turnOff = () => timelineRef.current?.play();
  const turnOn = () => timelineRef.current?.reverse();

  return { screenRef, turnOff, turnOn };
};

Conclusion

This experiment demonstrates how modern web technologies can recreate nostalgic physical experiences. The TV turn-off effect serves as:

The implementation showcases advanced animation principles while maintaining broad browser compatibility and smooth performance across devices.


This experiment captures the essence of tactile, physical feedback in digital interfaces, proving that nostalgia and modern web performance can coexist beautifully.