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 |