More Articles
RSS

Mar 2023

GifTV

Here I explore making a simulated retro tv with some animated gifs from a few of my favorite movies.

<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <link rel="preload" href="righteous.ttf" as="font" type="font/ttf">
    <link rel="preload" href="permanent-marker.ttf" as="font" type="font/ttf">
    <style media="screen">
      @font-face { font-family: "Righteous"; src: url(righteous.ttf); }
      @font-face { font-family: "Permanent Marker"; src: url(permanent-marker.ttf); }
      html,body { margin: 0; padding: 0; font-family: system-ui; font-size: 14px; line-height: 26px; height: 100%; width: 100%; }
      body { background: #1a1a1a; }
      #root { cursor: pointer; display: block; height: 350px; }
      #logo { z-index: 6; position: absolute; bottom: 30px; right: 60px; line-height: 1; font-size: 70px; color: #fff; }
      .gif { font-family: "Righteous"; position: relative; z-index: 1; -webkit-text-fill-color: transparent; -webkit-text-stroke: 0.1px #f1f1f1; -webkit-background-clip: text; background-image: -webkit-linear-gradient(#C3BFB4 0%, #FDFCFA 50%, #E8E7E5 51%, #757172 52%, #E8E9DB 100%); -webkit-filter: drop-shadow(2px 2px 15px #3F59F4); }
      .tv { font-family: "Permanent Marker"; font-size: 40px; width: 52px; position: relative; z-index: 1; display: inline-block; -webkit-transform: skew(-15deg,-15deg); background-image: -webkit-linear-gradient(#FF0FF8 0%,  #F9F9F7 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; -webkit-filter: drop-shadow(2px 2px 20px #f008b7); }
      #root img { display: block; width: 100vw; height: 100vh; object-fit: cover; }
      .image-container { z-index: 1; position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
      .filter {  transition: opacity 0.5s ease-out; background-position: left top; position: absolute; top: 0; right: 0; bottom: 0; left: 0; }
      .filter.noise { z-index: 2; background-image: url(noise.gif); background-size: cover; mix-blend-mode: hard-light; opacity: 1; }
      .filter.scanlines-a { z-index: 3; background-image: url(rgb-scanlines.png); background-size: auto; mix-blend-mode: overlay; opacity: 0.2 }
      .filter.scanlines-b { z-index: 4; background-image: url(rgb-scanlines-black.png); background-size: auto; mix-blend-mode: overlay; opacity: 1 }
      .filter.scanlines-c { z-index: 5; background-image: url(rgb-scanlines-black.png); background-size: auto; mix-blend-mode: darken; opacity: 1 }
    </style>
    <script type="text/javascript">
      const createNode = (options) => {
        const node = document.createElement(options.tag);
        if(options.className) node.setAttribute('class',options.className);
        if(options.innerHTML) node.innerHTML = options.innerHTML;
        if(options.attributes) Object.keys(options.attributes).forEach(key => node.setAttribute(key,options.attributes[key]) );
        if(options.style) Object.keys(options.style).forEach(key => node.style[key] = options.style[key]);
        if(options.event_listeners) Object.keys(options.event_listeners).forEach(key => node.addEventListener(key,options.event_listeners[key]) )
        options.root.appendChild(node);
        return node;
      }
      class App {
        constructor(){
          document.addEventListener('DOMContentLoaded',this.init.bind(this));
          document.addEventListener('click',this.handleDocumentClick.bind(this));
          this.images = [
            { url: 'terminator.gif', duration: 1500 },
            { url: 'bladerunner.gif', duration: 3500 },
            { url: 'fifth-element.gif', duration: 2300 },
            { url: 'abyss.gif', duration: 2400 },
            { url: 'blind-fury.gif', duration: 1800 },
            { url: 'predator.gif', duration: 1600 },
            { url: 'indiana-jones.gif', duration: 1300 },
            { url: 'jurassic-park.gif', duration: 4000 },
          ]
          this.current_image = null;
          this.timeout_next = null;
          this.timeout_noise = null;
          this.els = {};
        }
        init(){
          this.root = document.getElementById('root');
          this.els.logo = createNode({ root: this.root, tag: 'div', attributes: { id: 'logo' }, innerHTML: `<span class="gif">GIF</span><span class="tv">TV</span>` })
          this.els.image_container = createNode({ root: this.root, tag: 'div', attributes: { class: 'image-container' } });
          this.els.noise = createNode({ root: this.root, tag: 'div', attributes: { class: 'filter noise' } });
          this.els.scanlines_a = createNode({ root: this.root, tag: 'div', attributes: { class: 'filter scanlines-a' } });
          this.els.scanlines_b = createNode({ root: this.root, tag: 'div', attributes: { class: 'filter scanlines-b' } });
          this.els.scanlines_c = createNode({ root: this.root, tag: 'div', attributes: { class: 'filter scanlines-c' } });
          this.loadNext();
        }
        loadNext(){
          if(this.timeout_next) clearTimeout(this.timeout_next);
          if(this.timeout_noise) clearTimeout(this.timeout_noise);
          this.current_image = this.current_image === null || this.current_image + 1 === this.images.length ? 0 : this.current_image + 1;
          this.els.noise.style.opacity = 1;
          let image = new Image();
          image.onload = () => {
            this.timeout_noise = setTimeout(() => {
              this.els.noise.style.opacity = 0;
              this.timeout_next = setTimeout(() => { this.loadNext() }, this.images[this.current_image].duration - 500);
            }, 500)
            this.els.image_container.innerHTML = ``;
            this.els.image_container.appendChild(image);
          };
          // image.onerror = this.handleDocumentClick.bind(this);
          image.src = this.images[this.current_image].url;
        }
        handleDocumentClick(){
          this.loadNext();
        }
      }
      const app = new App();
    </script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

I first went looking for some animated gifs of cool scenes that I want to use on Google Images. I store the filenames for each and asynchronously load them for a specified duration, and a noise effect overlay is applied using mix-blend-mode while each image is waiting to load. I then added two extra overlays of a repeating background image for the CRT effect.

The logo uses two fonts from Google Fonts which are styled with CSS to achieve the retro logo effect.

Project Assets

Asset File Description
noise.gif Download Noise image overlay
rgb-scanlines-black.png Download Black scanlines
rgb-scanlines.png Download RGB scanlines
permanent-marker.ttf Source Permanent Marker font
righteous.ttf Source Righteous font