<div><canvas width="640" height="480" /></div>

<!--

WebGL CRT monitor effect demo with a tiny surprise. Uses offscreen 2D canvas as buffer source.

Libraries and sources:

- ReGL (WebGL helper): http://regl.party/
- glMatrix (math): http://glmatrix.net/
- onecolor (RGB conversion): https://github.com/One-com/one-color
- Google Fonts Inconsolata
- Renminbi by Fengquan Li from the Noun Project
- Skull by Jordan Alfarishy from the Noun Project
- bunny by Smalllike from the Noun Project
- dollar by Oksana Latysheva from the Noun Project

-->
html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  background: #000;
}

body {
  min-height: 640px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: radial-gradient(circle at 50% 20%, #002a2a, #000);
}

div {
  padding: 0px 10px 20px;
  border-radius: 40px;
  box-shadow: 0 0 50px 50px #000;
  background: #000;
}
View Compiled
const onecolor = one.color;

function hex2vector(cssHex) {
    const pc = onecolor(cssHex);

    return vec3.fromValues(
        pc.red(),
        pc.green(),
        pc.blue()
    );
}

const charW = 6;
const charH = 10;
const bufferCW = 80;
const bufferCH = 24;
const bufferW = bufferCW * charW;
const bufferH = bufferCH * charH;
const textureW = 512;
const textureH = 256;

const consolePad = 8; // in texels
const consoleW = bufferW + consolePad * 2;
const consoleH = bufferH + consolePad * 2;

const bufferCanvas = document.createElement('canvas');
bufferCanvas.width = bufferW;
bufferCanvas.height = bufferH;
// document.body.appendChild(bufferCanvas);

const bufferContext = bufferCanvas.getContext('2d');

bufferContext.fillStyle = '#000';
bufferContext.fillRect(0, 0, bufferW, bufferH);

function charRange(start, end) {
  return Array.apply(null, new Array(end - start)).map((_, index) => {
    return String.fromCharCode(start + index);
  });
}

const characterSet = ([]
  .concat(charRange(0x30, 0x3a)) // ASCII digits
  .concat(charRange(0x40, 0x5b)) // ASCII uppercase and @
  .concat(charRange(0x30a0, 0x30ff)) // kanji
);

// const bannerSet = [
//   '❤', '☠', '☣', '☻', '⚇', '⚿', '⛯'
// ];

const bannerImgSet = [
  new Image(),
  new Image(),
  new Image(),
  new Image()
];

bannerImgSet[0].src = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 80" x="0px" y="0px"><g><path d="M26,44H38a7.00818,7.00818,0,0,0,7-7V26H19V37A7.00818,7.00818,0,0,0,26,44Zm8.419-9.81348,7-5a.99968.99968,0,1,1,1.1621,1.627l-7,5a.99968.99968,0,1,1-1.1621-1.627ZM21.186,29.419a.99814.99814,0,0,1,1.395-.23243l7,5a.99968.99968,0,1,1-1.1621,1.627l-7-5A.9994.9994,0,0,1,21.186,29.419Z"/><path d="M13,53v3h6V50H16A3.00328,3.00328,0,0,0,13,53Z"/><rect x="23" y="10" width="2" height="14"/><path d="M45,24V9A5,5,0,0,0,35,9V24h2V9a.99974.99974,0,0,1,1-1h4a.99974.99974,0,0,1,1,1V24Z"/><path d="M29,24V9A5,5,0,0,0,19,9V24h2V9a.99974.99974,0,0,1,1-1h4a.99974.99974,0,0,1,1,1V24Z"/><rect x="39" y="10" width="2" height="14"/><rect x="13" y="58" width="6" height="2"/><rect x="45" y="58" width="6" height="2"/><path d="M40,46H37v5a.99974.99974,0,0,1-1,1H28a.99974.99974,0,0,1-1-1V46H24a3.00328,3.00328,0,0,0-3,3V60h2V57a.99974.99974,0,0,1,1-1H40a.99974.99974,0,0,1,1,1v3h2V49A3.00328,3.00328,0,0,0,40,46Z"/><rect x="33" y="58" width="6" height="2"/><path d="M48,50H45v6h6V53A3.00328,3.00328,0,0,0,48,50Z"/><rect x="29" y="46" width="2" height="4"/><rect x="33" y="46" width="2" height="4"/><rect x="25" y="58" width="6" height="2"/></g></svg>');

bannerImgSet[1].src = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 80" x="0px" y="0px"><path d="M20.67,28.71c6.92,0,7.67-4.23,7.67-6a7.67,7.67,0,0,0-15.33,0C13,24.48,13.75,28.71,20.67,28.71Zm0-9.71a3.67,3.67,0,0,1,3.67,3.67c0,.77,0,2-3.67,2S17,23.43,17,22.67A3.67,3.67,0,0,1,20.67,19Z"/><path d="M42.67,28.71c6.92,0,7.67-4.23,7.67-6a7.67,7.67,0,1,0-15.33,0C35,24.48,35.75,28.71,42.67,28.71Zm0-9.71a3.67,3.67,0,0,1,3.67,3.67c0,.77,0,2-3.67,2S39,23.43,39,22.67A3.67,3.67,0,0,1,42.67,19Z"/><path d="M25.14,34.5c0,2.49,4,3.21,6.5,3.21s6.5-.73,6.5-3.21-4-4.5-6.5-4.5S25.14,32,25.14,34.5Z"/><path d="M42,2H22A16,16,0,0,0,6,18V47a9,9,0,0,0,7.2,8.82A9,9,0,0,0,22,63H42a9,9,0,0,0,8.8-7.18A9,9,0,0,0,58,47V18A16,16,0,0,0,42,2ZM10,18A12,12,0,0,1,22,6H42A12,12,0,0,1,54,18V37.5A1.5,1.5,0,0,1,52.5,39,5.52,5.52,0,0,0,47,44.5V47H44V45a2,2,0,0,0-4,0v2H34V45a2,2,0,0,0-4,0v2H24V45a2,2,0,0,0-4,0v2H17V44.5A5.51,5.51,0,0,0,11.5,39a1.47,1.47,0,0,1-1.07-.46,1.45,1.45,0,0,1-.43-1V18Zm0,29V42.77a5.47,5.47,0,0,0,1.5.23A1.5,1.5,0,0,1,13,44.5v7.08A5,5,0,0,1,10,47Zm36.94,7.72A5,5,0,0,1,42,59H22a5,5,0,0,1-5-4.37A3.51,3.51,0,0,1,17,54V51h3v2a2,2,0,0,0,4,0V51h6v2a2,2,0,0,0,4,0V51h6v2a2,2,0,0,0,4,0V51h3v3A3.85,3.85,0,0,1,46.94,54.72ZM51,51.58V44.5a1.47,1.47,0,0,1,.46-1.07,1.45,1.45,0,0,1,1-.43,5.47,5.47,0,0,0,1.5-.21V47A5,5,0,0,1,51,51.58Z"/></svg>');

bannerImgSet[2].src = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 125"><symbol viewBox="-27.299 -39.024 54.598 78.047"><path fill="none" d="M11.157,34.02c-1.275-1.85-3.408-3.026-5.733-3.026c-2.13,0-4.118,0.985-5.424,2.607   c-1.306-1.622-3.294-2.607-5.423-2.607c-2.325,0-4.457,1.176-5.734,3.026c-1.275-1.85-3.408-3.026-5.733-3.026   c-1.913,0-3.648,0.776-4.908,2.029v-66.474c1.26,1.253,2.995,2.029,4.908,2.029c2.325,0,4.458-1.176,5.733-3.026   c1.277,1.85,3.409,3.026,5.734,3.026c2.129,0,4.117-0.986,5.423-2.607c1.306,1.622,3.294,2.607,5.424,2.607   c2.325,0,4.458-1.176,5.733-3.026c1.277,1.85,3.409,3.026,5.734,3.026c1.913,0,3.647-0.776,4.907-2.028v66.473   c-1.26-1.253-2.994-2.028-4.907-2.028C14.567,30.995,12.435,32.17,11.157,34.02z"/><path fill="#000000" stroke="#000000" stroke-width="3" stroke-miterlimit="10" d="M-20.463-37.524   c0.391,1.615,1.839,2.822,3.573,2.822c1.733,0,3.181-1.207,3.572-2.822h4.323c0.391,1.615,1.839,2.822,3.573,2.822   s3.182-1.207,3.573-2.822h3.701c0.391,1.615,1.839,2.822,3.573,2.822s3.182-1.207,3.573-2.822h4.322   c0.391,1.615,1.839,2.822,3.573,2.822s3.182-1.207,3.573-2.822h5.335v75.047h-5.266c-0.219-1.825-1.759-3.25-3.641-3.25   s-3.423,1.425-3.641,3.25H9.065c-0.219-1.825-1.759-3.25-3.641-3.25s-3.423,1.425-3.641,3.25h-3.564   c-0.219-1.825-1.759-3.25-3.641-3.25s-3.423,1.425-3.641,3.25h-4.186c-0.219-1.825-1.758-3.25-3.641-3.25   c-1.883,0-3.423,1.425-3.641,3.25h-5.267v-75.047H-20.463z"/></symbol><symbol viewBox="-30.282 -48.056 60.563 94.113"><path fill="#000000" stroke="#000000" stroke-width="6" stroke-miterlimit="10" d="M16.006,43.056   c-3.233-4.883-8.77-8.108-15.065-8.108s-11.832,3.225-15.065,8.108h-10.584v-88.113h10.584c3.233,4.883,8.77,8.108,15.065,8.108   s11.832-3.224,15.065-8.108H26.59v88.113H16.006z"/><line fill="none" stroke="#000000" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-dasharray="7,8" x1="27.781" y1="-20.755" x2="-27.782" y2="-20.755"/></symbol><symbol viewBox="-24.712 -42.834 54.356 85.667"><path fill="#000000" stroke="#000000" stroke-width="5" stroke-miterlimit="10" d="M17.181,40.333h-4.475   c-0.466-2.355-2.539-4.131-5.031-4.131c-2.491,0-4.565,1.776-5.03,4.131h-4.883c-0.466-2.355-2.539-4.131-5.031-4.131   c-2.491,0-4.565,1.776-5.03,4.131h-4.883c-0.466-2.355-2.539-4.131-5.03-4.131v-72.023c2.622,0,4.76-1.974,5.069-4.512h4.806   c0.309,2.539,2.447,4.512,5.069,4.512c2.622,0,4.76-1.974,5.069-4.512h4.806c0.309,2.539,2.447,4.512,5.069,4.512   c2.622,0,4.76-1.974,5.069-4.512h4.398c0.309,2.539,2.447,4.512,5.069,4.512v72.023C19.72,36.202,17.647,37.978,17.181,40.333z"/><line fill="none" stroke="#000000" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" stroke-dasharray="7,6" x1="-22.644" y1="-17.395" x2="27.644" y2="-17.395"/></symbol><symbol viewBox="-23.971 -42.094 47.944 84.188"><g><path fill="#000000" stroke="#000000" stroke-width="3.521" stroke-miterlimit="10" d="M17.182,40.333h-4.475    c-0.466-2.355-2.539-4.131-5.031-4.131c-2.491,0-4.565,1.776-5.03,4.131h-4.883c-0.466-2.355-2.539-4.131-5.031-4.131    c-2.491,0-4.565,1.776-5.03,4.131h-4.883c-0.466-2.355-2.539-4.131-5.03-4.131v-72.023c2.622,0,4.76-1.974,5.069-4.512h4.806    c0.309,2.539,2.447,4.512,5.069,4.512c2.622,0,4.76-1.974,5.069-4.512h4.806c0.309,2.539,2.447,4.512,5.069,4.512    c2.622,0,4.76-1.974,5.069-4.512h4.398c0.309,2.539,2.447,4.512,5.069,4.512v72.023C19.721,36.202,17.648,37.978,17.182,40.333z"/></g><polygon fill="#000000" stroke="#000000" stroke-width="3.8113" stroke-miterlimit="10" points="5.14,17.02 11.952,13.853    8.431,7.217 7.524,-0.241 0.124,1.057 -7.248,-0.385 -8.301,7.053 -11.951,13.62 -5.201,16.919 -0.084,22.42  "/></symbol><symbol id="a" viewBox="-21.087 -31.266 42.17 72.87"><path fill="#000000" stroke="#000000" stroke-width="5" stroke-miterlimit="10" d="M6.373,39.104h5.21c0-3.87,3.13-7,7-7v-53.87   c-3.87,0-7-3.14-7-7h-5.21"/><path fill="#000000" stroke="#000000" stroke-width="5" stroke-miterlimit="10" d="M-6.377-28.766h-5.21c0,3.86-3.13,7-7,7v53.87   c3.87,0,7,3.13,7,7h5.21"/><line fill="none" stroke="#000000" stroke-width="5" stroke-miterlimit="10" x1="2.373" y1="39.104" x2="-2.377" y2="39.104"/><line fill="none" stroke="#000000" stroke-width="5" stroke-miterlimit="10" x1="2.373" y1="-28.766" x2="-2.377" y2="-28.766"/><g><path stroke="#000000" stroke-width="0.9" stroke-miterlimit="10" d="M-9.901-10.118c0-0.994-0.806-1.8-1.8-1.8    s-1.8,0.806-1.8,1.8s0.806,1.8,1.8,1.8S-9.901-9.124-9.901-10.118z"/><path stroke="#000000" stroke-width="0.9" stroke-miterlimit="10" d="M-2.101-10.118c0-0.994-0.806-1.8-1.8-1.8    s-1.8,0.806-1.8,1.8s0.806,1.8,1.8,1.8S-2.101-9.124-2.101-10.118z"/><path stroke="#000000" stroke-width="0.9" stroke-miterlimit="10" d="M5.699-10.118c0-0.994-0.806-1.8-1.8-1.8s-1.8,0.806-1.8,1.8    s0.806,1.8,1.8,1.8S5.699-9.124,5.699-10.118z"/><path stroke="#000000" stroke-width="0.9" stroke-miterlimit="10" d="M13.499-10.118c0-0.994-0.806-1.8-1.8-1.8    s-1.8,0.806-1.8,1.8s0.806,1.8,1.8,1.8S13.499-9.124,13.499-10.118z"/></g></symbol><symbol viewBox="-21.086 -31.266 42.17 72.87"><use xlink:href="#a" width="42.17" height="72.87" x="-21.087" y="-31.266" transform="matrix(1 0 0 1 6.996482e-004 -2.671033e-004)" overflow="visible"/><line fill="none" stroke="#000000" stroke-width="4" stroke-miterlimit="10" x1="9.224" y1="13.75" x2="-9.223" y2="13.75"/></symbol><g display="none"><path display="inline" fill="#000000" d="M828.792,474.743H-409.006c-2.084,0-3.789-1.705-3.789-3.789V-716.211   c0-2.084,1.705-3.789,3.789-3.789H828.792c2.084,0,3.789,1.705,3.789,3.789V470.954   C832.582,473.038,830.877,474.743,828.792,474.743z"/></g><g><g><path d="M50,91.998c23.195,0,41.998-18.803,41.998-41.998C91.998,26.805,73.195,8.002,50,8.002    C26.805,8.002,8.002,26.805,8.002,50C8.002,73.195,26.805,91.998,50,91.998z M31.914,67.307l2.067-4.598    c0.255-0.569,0.734-1.003,1.318-1.213c0.24-0.075,0.479-0.12,0.719-0.12c0.374,0,0.734,0.09,1.063,0.27    c3.145,1.677,7.369,2.681,11.293,2.681c4.613,0,7.968-1.887,7.968-4.478c0-1.678-0.974-3.7-8.148-6.036    c-7.773-2.441-15.697-6.006-15.697-14.004c0-6.066,4.418-10.814,11.817-12.686l1.123-0.285v-5.018    c0-1.228,1.018-2.232,2.247-2.232h5.317c1.243,0,2.247,1.003,2.247,2.232v4.508l1.318,0.165c3.16,0.374,5.961,1.153,8.537,2.367    c0.539,0.255,0.959,0.704,1.153,1.273c0.21,0.554,0.18,1.183-0.075,1.707l-2.142,4.538c-0.374,0.779-1.168,1.288-2.037,1.288    c-0.315,0-0.629-0.075-0.929-0.21c-1.393-0.644-4.658-2.127-9.87-2.127c-4.733,0-6.845,1.827-6.845,3.655    c0,2.037,2.202,3.415,9.091,5.692c7.339,2.396,14.828,6.006,14.828,14.693c0,4.688-3.25,10.994-12.417,13.3l-1.123,0.299v5.197    c0,1.243-1.018,2.247-2.247,2.247h-5.407c-1.243,0-2.247-1.003-2.247-2.247v-4.763l-1.333-0.135    c-4.134-0.464-8.028-1.588-10.649-3.1C31.839,69.598,31.45,68.37,31.914,67.307z"/></g></g></svg>');

bannerImgSet[3].src = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 55" x="0px" y="0px"><circle cx="22" cy="22" r="15.25" transform="translate(-9.11 22) rotate(-45)" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/><line x1="18.41" y1="14.99" x2="19.9" y2="18.82" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/><line x1="25.92" y1="14.99" x2="24.44" y2="18.82" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/><line x1="16.29" y1="18.84" x2="27.71" y2="18.84" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/><line x1="16.29" y1="24.41" x2="27.71" y2="24.41" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/><line x1="22" y1="18.84" x2="22" y2="29.01" style="" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="3px"/></svg>');

// pseudo-random
// credit: https://gist.github.com/blixt/f17b47c62508be59987b
const SEED_OFFSET = new Date().getTime();

function randomize(seed) {
    const intSeed = seed % 2147483647;
    const safeSeed = intSeed > 0 ? intSeed : intSeed + 2147483646;
    return safeSeed * 16807 % 2147483647;
}

function getRandomizedFraction(seed) {
    return (seed - 1) / 2147483646;
}

// main character trail state
function createTrail() {
  const cx = Math.floor(Math.random() * bufferCW);
  const cy = 0;
  const cvy = 5 + Math.random() * 15;

  return [ cx, cy, cvy ];
}

const trails = Array.apply(null, new Array(30)).map((_, index) => {
  return createTrail();
});

function updateWorld(delta) {
  trails.forEach((trail, index) => {
    trail[1] += trail[2] * delta;

    if (trail[1] > bufferCH) {
      trails[index] = createTrail();
    }
  });
}

// "warm up" the state by simulating the world for a bit
Array.apply(null, new Array(100)).forEach(() => {
  updateWorld(0.1);
});

let fadeCountdown = 0;
let bannerCountdown = 8.0;
let bannerChoice = 0;

function renderWorld(delta) {
  // fade screen every few frames
  // (not every frame, for long trails without rounding artifacts)
  fadeCountdown -= delta;
  
  if (fadeCountdown < 0) {
    bufferContext.fillStyle = 'rgba(0, 0, 0, 0.5)';  
    bufferContext.fillRect(0, 0, bufferW, bufferH);
    
    fadeCountdown += 0.2;
  }

  // redraw
  bufferContext.textAlign = 'center';
  bufferContext.font = '12px "Inconsolata"';

  trails.forEach((trail, index) => {
    const k = index / trails.length;
    const charY = Math.floor(trail[1]);
    
    // randomize based on character position
    const charSeed = index + (trail[0] + charY * bufferCW) * 50;
    const outSeed = randomize(charSeed * 1500 + SEED_OFFSET);

    const char = characterSet[Math.floor(getRandomizedFraction(outSeed) * characterSet.length)];

    bufferContext.fillStyle = `hsl(${180 + k * 120}, 100%, 60%)`;
    bufferContext.fillText(
      char,
      (trail[0] + 0.5) * charW, // center inside character box
      charY * charH + charH,
      charW // restrict width, but allow a tiny bit of spillover
    );
  }); 
  
  // fade screen every few frames
  // (not every frame, for long trails without rounding artifacts)
  bannerCountdown -= delta;
  
  if (bannerCountdown < 1.5) {
    bufferContext.fillStyle = `hsla(${180 + Math.random() * 220}, 100%, 30%, 1)`;
    bufferContext.fillRect(0, 0, bufferW, bufferH);

    if (bannerCountdown > 0.8) {
      bannerChoice = Math.floor(Math.random() * bannerImgSet.length);
    }

    const bannerImg = bannerImgSet[bannerChoice];
    bufferContext.drawImage(bannerImg, 0, 10, bufferW, bufferH + 40);

    // bufferContext.fillStyle = `hsla(${100 + Math.random() * 220}, 100%, 10%, 1)`;
    // bufferContext.font = '250px sans-serif';
    
    // bufferContext.save();
    // bufferContext.scale(2, 1);
    // bufferContext.fillText(
    //   bannerSet[Math.floor(Math.random() * bannerSet.length)],
    //   240 / 2, 200
    // );
    // bufferContext.restore();
  }
  
  if (bannerCountdown < 0) {
      bannerCountdown += 10 + Math.random() * 10;
  }
}

// init WebGL
const regl = createREGL({
    canvas: document.body.querySelector('canvas'),
    attributes: { antialias: true, alpha: false, preserveDrawingBuffer: true }
});

const spriteTexture = regl.texture({
    width: 512,
    height: 256,
    mag: 'linear'
});

const termFgColor = hex2vector('#fee');
const termBgColor = hex2vector('#002a2a');

const quadCommand = regl({
    vert: `
        precision mediump float;

        attribute vec3 position;

        varying vec2 uvPosition;

        void main() {
            uvPosition = position.xy * vec2(0.5, -0.5) + vec2(0.5);

            gl_Position = vec4(
                vec2(-1.0, 1.0) + (position.xy - vec2(-1.0, 1.0)) * 1.0,
                0.0,
                1.0
            );
        }
    `,

    frag: `
        precision mediump float;

        varying vec2 uvPosition;
        uniform sampler2D sprite;
        uniform float time;
        uniform vec3 bgColor;
        uniform vec3 fgColor;

        #define textureW ${textureW + '.0'}
        #define textureH ${textureH + '.0'}
        #define consoleW ${consoleW + '.0'}
        #define consoleH ${consoleH + '.0'}
        #define consolePadUVW ${consolePad / consoleW}
        #define consolePadUVH ${consolePad / consoleH}
        #define charUVW ${charW / consoleW}
        #define charUVH ${charH / consoleH}

        void main() {
            // @todo use uniform
            vec2 consoleWH = vec2(consoleW, consoleH);

            // @todo use uniforms
            float glitchLine = mod(0.8 + time * 0.07, 1.0);
            float glitchFlutter = mod(time * 40.0, 1.0); // timed to be slightly out of sync from main frame rate
            float glitchAmount = 0.06 + glitchFlutter * 0.01;
            float glitchDistance = 0.04 + glitchFlutter * 0.15;

            vec2 center = uvPosition - vec2(0.5);
            float factor = dot(center, center) * 0.2;
            vec2 distortedUVPosition = uvPosition + center * (1.0 - factor) * factor;

            vec2 fromEdge = vec2(0.5, 0.5) - abs(distortedUVPosition - vec2(0.5, 0.5));

            if (fromEdge.x > 0.0 && fromEdge.y > 0.0) {
                vec2 fromEdgePixel = min(0.2 * consoleWH * fromEdge, vec2(1.0, 1.0));

                // simulate 2x virtual pixel size, for crisp display on low-res
                vec2 inTexel = mod(distortedUVPosition * consoleWH * 0.5, vec2(1.0));

                float distToGlitch = glitchLine - (distortedUVPosition.y - inTexel.y / consoleH);
                float glitchOffsetLinear = step(0.0, distToGlitch) * max(0.0, glitchDistance - distToGlitch) / glitchDistance;
                float glitchOffset = glitchOffsetLinear * glitchOffsetLinear;

                vec2 inTexelOffset = inTexel - 0.5;
                float scanlineAmount = inTexelOffset.y * inTexelOffset.y / 0.25;
                float intensity = 8.0 - scanlineAmount * 5.0 + glitchOffset * 2.0; // ray intensity is over-amped by default
                vec2 uvAdjustment = inTexelOffset * vec2(0.0, .5 / consoleH); // remove vertical texel interpolation

                distortedUVPosition.x -= glitchOffset * glitchAmount + 0.011 * (glitchFlutter * glitchFlutter * glitchFlutter);

                vec4 sourcePixel = texture2D(
                    sprite,
                    (distortedUVPosition - uvAdjustment) * consoleWH / vec2(textureW, textureH)
                );

                vec3 pixelRGB = sourcePixel.rgb * sourcePixel.a;

                // multiply by source alpha as well
                float screenFade = 1.0 - dot(center, center) * 1.8;
                float edgeFade = fromEdgePixel.x * fromEdgePixel.y;
                gl_FragColor = vec4(edgeFade * screenFade * mix(
                    bgColor,
                    fgColor,
                    intensity * pixelRGB + glitchOffset * 1.5
                ) * (1.0 - 0.2 * scanlineAmount), 0.2);
            } else {
                gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
            }
        }
    `,

    attributes: {
        position: regl.buffer([
            [ -1, -1, 0 ],
            [ 1, -1, 0 ],
            [ -1, 1, 0 ],
            [ 1, 1, 0 ]
        ])
    },

    uniforms: {
        time: regl.context('time'),
        camera: regl.prop('camera'),
        sprite: spriteTexture,
        bgColor: regl.prop('bgColor'),
        fgColor: regl.prop('fgColor')
    },

    primitive: 'triangle strip',
    count: 4,

    depth: {
        enable: false
    },

    blend: {
        enable: true,
        func: {
            src: 'src alpha',
            dst: 'one minus src alpha'
        }
    }
});

regl.clear({
    depth: 1,
    color: [ 0, 0, 0, 1 ]
});

// main loop
let currentTime = performance.now();

function rafBody() {
  // measure time
  const newTime = performance.now();
  const delta = Math.min(0.05, (newTime - currentTime) / 1000); // apply limiter to avoid frame skips
  currentTime = newTime;
  
  updateWorld(delta);
  renderWorld(delta);  

  regl.poll();
  spriteTexture.subimage(bufferContext, consolePad, consolePad);
  quadCommand({
      bgColor: termBgColor,
      fgColor: termFgColor
  });

  requestAnimationFrame(rafBody);
}

// kickstart the loop
rafBody();

Run Pen

External CSS

  1. https://fonts.googleapis.com/css?family=Inconsolata

External JavaScript

  1. https://npmcdn.com/regl@1.3.9/dist/regl.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.7.1/gl-matrix-min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/onecolor/3.1.0/one-color-all.js