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

<!--

WebGL CRT monitor effect loop. 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

-->
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% 30%, #fff, #444);
}

div {
  padding: 10px 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 @
);

// 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;
}

let cursorX = 0, cursorY = bufferCH - 1;

const chunkList = [ '-\n' ];
let chunkIndex = 0, chunkPos = 0;

fetch('https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.7.1/gl-matrix.js').then(response => {
  if (!response.ok) {
      throw new Error('oops');
  }

  const reader = response.body.getReader();

  const nextChunk = () => {
      reader.read().then(({ done, value }) => {
          if (done) {
              return;
          }

          chunkList.push(new TextDecoder('utf-8').decode(value));
          nextChunk();
      });
  };

  nextChunk();
});

function updateWorld(delta) {
  // redraw
  bufferContext.textAlign = 'center';
  bufferContext.font = '12px "Inconsolata"';

  const advance = 2 + Math.floor(Math.random() * 10);

  for (let i = 0; i < advance; i++) {
    const chunk = chunkList[chunkIndex];
    const char = chunk[chunkPos];
    
    // advance read head
    chunkPos += 1;
    if (chunkPos >= chunk.length) {
      chunkIndex = (chunkIndex + 1) % chunkList.length;
      chunkPos = 0;
    }

    const charCode = char.charCodeAt(0);
    if (charCode >= 32) {
      bufferContext.fillStyle = `hsl(${160 + (charCode / 256) * 60}, 100%, 60%)`;
      bufferContext.fillText(
        char,
        (cursorX + 0.5) * charW, // center inside character box
        cursorY * charH + charH,
        charW // restrict width, but allow a tiny bit of spillover
      );
      cursorX += 1;
    }

    if (charCode === 10 || cursorX >= bufferCW) {
      cursorX = 0;

      bufferContext.drawImage(
        bufferCanvas,
        0, charH, bufferW, bufferH - charH,
        0, 0, bufferW, bufferH - charH
      );

      bufferContext.fillStyle = `#000`;
      bufferContext.fillRect(0, bufferH - charH, bufferW, charH);
    }
  }
}

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

// let fadeCountdown = 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;
//   }

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

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

//   }); 
}

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

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

const termFgColor = hex2vector('#efe');
const termBgColor = hex2vector('#202520');

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 float glitchLine;
        uniform float glitchFlutter;
        uniform float glitchAmount;
        uniform float glitchDistance;
        uniform vec3 bgColor;
        uniform vec3 fgColor;

        #define curvature 1.0
        #define textureW ${textureW + '.0'}
        #define textureH ${textureH + '.0'}
        #define consoleW ${consoleW + '.0'}
        #define consoleH ${consoleH + '.0'}

        vec3 renderFacet(vec2 facetOrigin, vec2 facetSize, vec2 facetWH, vec2 facetTexelUV, float facetGlitchLine, vec2 textureLookupRatio) {
            float facetH = facetWH.y;

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

            float facetGlitchDistance = glitchDistance / facetSize.y;
            float distToGlitch = facetGlitchLine - (facetTexelUV.y - inTexel.y / facetH);
            float glitchOffsetLinear = step(0.0, distToGlitch) * max(0.0, facetGlitchDistance - distToGlitch) / facetGlitchDistance;
            float glitchOffset = glitchOffsetLinear * glitchOffsetLinear;

            facetTexelUV.x -= glitchOffset * glitchAmount + 0.081 * (glitchFlutter * glitchFlutter * glitchFlutter);

            vec2 inTexelOffset = inTexel - 0.5;
            vec2 uvAdjustment = inTexelOffset * vec2(0.0, .5 / facetH); // remove vertical texel interpolation
            vec2 distortedUVPosition = facetOrigin + (facetTexelUV - uvAdjustment) * facetSize;

            vec4 sourcePixel = texture2D(
                sprite,
                distortedUVPosition * textureLookupRatio
            );

            // multiply by source alpha as well
            vec3 pixelRGB = sourcePixel.rgb * sourcePixel.a;

            float scanlineAmount = inTexelOffset.y * inTexelOffset.y / 0.25;
            float intensity = 12.0 - scanlineAmount * 3.0; // ray intensity is over-amped by default
            vec3 glitchLineAmp = vec3(0.7, 0.15, 0.1) * glitchOffset * 20.0;

            return mix(
                bgColor,
                fgColor,
                intensity * pixelRGB
            ) * (1.0 - 0.5 * scanlineAmount) + glitchLineAmp;
        }

        void main() {
            // @todo use uniform
            vec2 consoleWH = vec2(consoleW, consoleH);
            float maxMixFactor = 8.0;
            float mixFactor = max(0.0, mod(time * 0.5, maxMixFactor + 2.0) - 2.0);
            float globalLoopMix = (mixFactor / maxMixFactor);
            globalLoopMix *= globalLoopMix; // slow at first
            //float mixFactor = maxMixFactor - abs(mod(time * 0.5, maxMixFactor * 2.0) - maxMixFactor);
            vec2 textureLookupRatio = consoleWH / vec2(textureW, textureH);

            vec2 globalCenterOffset = uvPosition - vec2(0.5);
            float globalDistortionFactor = dot(globalCenterOffset, globalCenterOffset) * curvature * globalLoopMix;
            vec2 globalTexelUV = uvPosition + globalCenterOffset * (1.0 - globalDistortionFactor) * globalDistortionFactor; // pixel position in parent-relative UV
            vec2 fromGlobalEdge = vec2(0.5) - abs(globalTexelUV - vec2(0.5));

            vec2 parentOrigin = vec2(0); // parent origin in global UV
            vec2 parentSize = vec2(1.0); // parent size in global UV
            vec2 parentWH = consoleWH; // parent size in texels
            vec2 parentUV = globalTexelUV; // pixel position in parent-relative UV
            float parentEdgeSize = 0.1;

            int maxLevels = int(mixFactor);
            for(int level = 0; level < 7; level++) {
              if (level >= maxLevels) {
                break;
              }

              parentSize *= 0.5;
              parentOrigin += floor(parentUV / 0.5) * parentSize;
              parentUV = mod(parentUV, 0.5) / 0.5;
              parentWH *= 0.5;
              parentEdgeSize *= 1.75; // tighten up edge feathering
              mixFactor -= 1.0;
            }

            vec2 parentCenterOffset = parentUV - vec2(0.5);
            float parentDistortionFactor = dot(parentCenterOffset, parentCenterOffset) * curvature;
            vec2 parentTexelUV = parentUV + parentCenterOffset * (1.0 - parentDistortionFactor) * parentDistortionFactor; // intended texture coordinates inside parent UV

            vec2 facetOriginInParent = floor(parentUV / 0.5) * 0.5;
            vec2 facetOrigin = parentOrigin + parentSize * facetOriginInParent; // facet origin in global UV
            vec2 facetWH = parentWH * 0.5; // facet size in texels
            vec2 facetUV = (parentUV - facetOriginInParent) / vec2(0.5); // pixel position inside facet

            vec2 facetCenterOffset = facetUV - vec2(0.5);
            float facetDistortionFactor = dot(facetCenterOffset, facetCenterOffset) * curvature;
            vec2 facetTexelUV = facetUV + facetCenterOffset * (1.0 - facetDistortionFactor) * facetDistortionFactor; // intended texture coordinates inside facet UV

            vec2 parentFacetTexelUV = (parentTexelUV - facetOriginInParent) / 0.5; // parent texture coordinates inside facet UV

            float edgeFadeMixFactor = (clamp(mixFactor, 0.8, 0.95) - 0.8) / 0.15;
            float distortionMixFactor = (max(mixFactor, 0.95) - 0.95) / 0.05;

            // blended target texture coordinates inside facet UV
            vec2 blendedTexelUV = mix(parentFacetTexelUV, facetTexelUV, distortionMixFactor);

            vec2 fromFacetEdge = vec2(0.5) - abs(blendedTexelUV - vec2(0.5)); // use blended position
            vec2 fromParentEdge = vec2(0.5) - abs(parentTexelUV - vec2(0.5));

            if (fromFacetEdge.x > 0.0 && fromFacetEdge.y > 0.0 && fromGlobalEdge.x > 0.0 && fromGlobalEdge.y > 0.0) {
                vec2 fromParentEdgePixel = min(parentEdgeSize * parentWH * fromParentEdge, vec2(1.0, 1.0));
                vec2 fromEdgePixel = min(parentEdgeSize * facetWH * fromFacetEdge, vec2(1.0, 1.0));
                vec2 fromGlobalEdgePixel = min(0.1 * consoleWH * fromGlobalEdge, vec2(1.0, 1.0));

                // fade faster near the parent's center
                float distanceAmount = 4.0 * dot(parentCenterOffset, parentCenterOffset);
                float edgeMixCurve = 2.0 * edgeFadeMixFactor * (1.0 - edgeFadeMixFactor);
                float edgeFade = mix(
                    fromParentEdgePixel.x * fromParentEdgePixel.y,
                    fromEdgePixel.x * fromEdgePixel.y,
                    edgeFadeMixFactor - distanceAmount * edgeMixCurve
                );

                float loopedEdgeFade = edgeFade * mix(
                    1.0,
                    fromGlobalEdgePixel.x * fromGlobalEdgePixel.y,
                    globalLoopMix
                );

                float screenFade = mix(
                    1.0 - dot(parentCenterOffset, parentCenterOffset) * 1.8,
                    1.0 - dot(facetCenterOffset, facetCenterOffset) * 1.8,
                    edgeFadeMixFactor
                );

                float loopedScreenFade = screenFade * mix(
                    1.0,
                    1.0 - dot(globalCenterOffset, globalCenterOffset) * 1.8,
                    globalLoopMix
                );

                vec2 facetSize = parentSize * 0.5;
                float facetGlitchLine = (glitchLine - facetOrigin.y) / facetSize.y;

                gl_FragColor = vec4(
                    loopedEdgeFade * loopedScreenFade * renderFacet(
                        facetOrigin,
                        facetSize,
                        facetWH,
                        blendedTexelUV,
                        facetGlitchLine,
                        textureLookupRatio
                    ),
                    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'),
        glitchLine: regl.prop('glitchLine'),
        glitchFlutter: regl.prop('glitchFlutter'),
        glitchAmount: regl.prop('glitchAmount'),
        glitchDistance: regl.prop('glitchDistance'),
        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();
let elapsedTime = 0;

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;
  elapsedTime += delta;

  // glitch settings
  const glitchLine = (0.8 + elapsedTime * 0.27) % 1.0;
  const glitchFlutter = (elapsedTime * 40.0) % 1.0; // timed to be slightly out of sync from main frame rate
  const glitchAmount = 0.06 + glitchFlutter * 0.01;
  const glitchDistance = 0.04 + glitchFlutter * 0.35;

  updateWorld(delta);
  renderWorld(delta);

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

  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