<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Hallowe'en</title>
  <link rel="stylesheet" href="app.css" />
  <link href="https://fonts.googleapis.com/css?family=Comfortaa" rel="stylesheet">
</head>

<body>
  <header>
    <h1>Happy <span>Ha</span>llowe'en</h1>
  </header>
  <main>
    <video autoplay id="video"></video>
    <div class="controls">
      <button id="button">Start creepy video</button>
      <p>(put your headphones on start the video, then scream at yourself)</p>
    </div>
  </main>
  <footer>
    <p>By <a href="https://twitter.com/philnash">phil nash</a></p>
  </footer>
  <script src="index.js"></script>
</body>

</html>
:root {
  --move: 3px;
}

body {
  background: #111;
  font-family: Comfortaa;
}
header h1 {
  text-align: center;
  border: 0 solid rgba(255, 255, 255, 1);
  color: rgba(255, 255, 255, 1);
  font-size: 64px;
  text-align: center;
  animation: flicker 10s none 0s infinite;
}
header h1 span {
  position: relative;
  z-index: 10;
  text-shadow: 0 0 10px rgba(255, 255, 255, 1), 0 0 20px rgba(255, 255, 255, 1),
    0 0 30px rgba(255, 255, 255, 1), 0 0 40px rgb(255, 69, 0),
    0 0 70px rgb(255, 69, 0), 0 0 80px rgb(255, 69, 0),
    0 0 100px rgb(255, 69, 0);
}
button {
  border: none;
  font-size: 16px;
  padding: 5px 0.5em;
  background: rgb(255, 69, 0);
  color: #fff;
  border-radius: 5px;
}
button:hover {
  background: rgb(200, 50, 0);
}
p { color: #fff; }
footer {
  text-align: center;
  color: #fff;
}
footer a {
  color: rgb(255, 69, 0);
}

@keyframes flicker {
  0% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }
  49% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }
  50% {
    text-shadow: none;
  }
  51% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }

  74% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }
  75% {
    text-shadow: none;
  }
  76% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }
  77% {
    text-shadow: none;
  }
  78% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }

  100% {
    text-shadow: 0 0 10px rgba(255, 255, 255, 1),
      0 0 20px rgba(255, 255, 255, 1), 0 0 30px rgba(255, 255, 255, 1),
      0 0 40px rgb(255, 69, 0), 0 0 70px rgb(255, 69, 0),
      0 0 80px rgb(255, 69, 0), 0 0 100px rgb(255, 69, 0);
  }
}

@keyframes glitch {
  0% {
    transform: translate(0, 0);
  }
  19.9% {
    transform: translate(0, 0);
  }
  20% {
    transform: translate(30px, 0);
  }
  20.9% {
    transform: translate(30px, 0);
  }
  21% {
    transform: translate(0, 0);
  }

  40% {
    transform: translate(var(--move), calc(var(--move) * -1));
  }
  40.3% {
    transform: translate(calc(var(--move) * -1), var(--move));
  }
  40.6% {
    transform: translate(var(--move), calc(var(--move) * -1));
  }
  40.9% {
    transform: translate(var(--move), calc(var(--move) * -1));
  }
  41% {
    transform: translate(0, 0);
  }

  69.9% {
    transform: translate(0, 0);
    box-shadow: none;
  }
  70% {
    transform: translate(calc(var(--move) * 2), calc(var(--move) * -2));
    box-shadow: 3px 3px 0 3px rgba(221, 0, 0, 0.6),
      -3px -3px 0 3px rgba(0, 136, 136, 0.6);
  }
  70.3% {
    transform: translate(calc(var(--move) * -2), calc(var(--move) * 2));
    box-shadow: 3px -3px 0 3px rgba(221, 0, 0, 0.6),
      -3px 3px 0 3px rgba(0, 136, 136, 0.6);
  }
  70.6% {
    transform: translate(calc(var(--move) * 2), calc(var(--move) * -2));
    box-shadow: -3px 3px 0 3px rgba(221, 0, 0, 0.6),
      3px -3px 0 3px rgba(0, 136, 136, 0.6);
  }
  70.9% {
    transform: translate(calc(var(--move) * -2), calc(var(--move) * 2));
    box-shadow: -3px -3px 0 3px rgba(221, 0, 0, 0.6),
      3px 3px 0 3px rgba(0, 136, 136, 0.6);
  }
  71% {
    transform: translate(0, 0);
    box-shadow: none;
  }
  100% {
    transform: translate(0, 0);
  }
}

video {
  margin: 1em auto;
  display: block;
  border-radius: 5px;
}
video.boo {
  animation: glitch 20s none 0s infinite alternate;
}
video.boo::before {
  content: ' ';
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 0, 0, 0.25) 50%),
    linear-gradient(
      90deg,
      rgba(255, 0, 0, 0.06),
      rgba(0, 255, 0, 0.02),
      rgba(0, 0, 255, 0.06)
    );
  z-index: 2;
  background-size: 100% 2px, 3px 100%;
  pointer-events: none;
}
video.boo::after {
  content: ' ';
  display: block;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: rgba(18, 16, 16, 0.1);
  opacity: 0;
  z-index: 2;
  pointer-events: none;
  animation: crt 0.15s infinite;
}
video.booo {
  filter: invert(75%);
  --move: 12px;
}

.controls {
  text-align: center;
}
@keyframes crt {
  0% {
    opacity: 0.27861;
  }
  5% {
    opacity: 0.34769;
  }
  10% {
    opacity: 0.23604;
  }
  15% {
    opacity: 0.90626;
  }
  20% {
    opacity: 0.18128;
  }
  25% {
    opacity: 0.83891;
  }
  30% {
    opacity: 0.65583;
  }
  35% {
    opacity: 0.67807;
  }
  40% {
    opacity: 0.26559;
  }
  45% {
    opacity: 0.84693;
  }
  50% {
    opacity: 0.96019;
  }
  55% {
    opacity: 0.08594;
  }
  60% {
    opacity: 0.20313;
  }
  65% {
    opacity: 0.71988;
  }
  70% {
    opacity: 0.53455;
  }
  75% {
    opacity: 0.37288;
  }
  80% {
    opacity: 0.71428;
  }
  85% {
    opacity: 0.70419;
  }
  90% {
    opacity: 0.7003;
  }
  95% {
    opacity: 0.36108;
  }
  100% {
    opacity: 0.24387;
  }
}
window.addEventListener('load', () => {
  const video = document.getElementById('video');
  const button = document.getElementById('button');
  let currentStream = null;
  let animationFrame = null;
  let dataArray;

  const makeDistortionCurve = amount => {
    var k = typeof amount === 'number' ? amount : 50,
      n_samples = 44100,
      curve = new Float32Array(n_samples),
      deg = Math.PI / 180,
      i = 0,
      x;
    for (; i < n_samples; ++i) {
      x = (i * 2) / n_samples - 1;
      curve[i] = ((3 + k) * x * 20 * deg) / (Math.PI + k * Math.abs(x));
    }
    return curve;
  };

  const getAverageVolume = array => {
    let values = 0;
    let length = array.length;
    for (let i = 0; i < length; i++) {
      values += array[i];
    }
    return values / length;
  };

  const setStream = (videoTrack, audioTrack) => {
    video.srcObject = null;
    const mediaStream = new MediaStream();
    mediaStream.addTrack(videoTrack);
    mediaStream.addTrack(audioTrack);
    currentStream = mediaStream;
    video.srcObject = mediaStream;
  };

  button.addEventListener('click', () => {
    if (currentStream) {
      video.srcObject = null;
      currentStream.getTracks().forEach(track => {
        track.stop();
      });
      currentStream = null;
      button.innerText = 'Start creepy video';
      window.cancelAnimationFrame(animationFrame);
      video.classList.remove('boo');
    } else {
      button.setAttribute('disabled', 'disabled');
      navigator.mediaDevices
        .getUserMedia({ video: true, audio: true })
        .then(stream => {
          currentStream = stream;
          const audioTrack = currentStream.getAudioTracks()[0];
          const videoTrack = currentStream.getVideoTracks()[0];
          const audioStream = new MediaStream();
          audioStream.addTrack(audioTrack);

          const audioCtx = new (window.AudioContext ||
            window.webkitAudioContext)();
          const src = audioCtx.createMediaStreamSource(audioStream);

          const analyser = audioCtx.createAnalyser();
          analyser.fftSize = 2048;
          const bufferLength = analyser.fftSize;
          dataArray = new Uint8Array(bufferLength);

          const processVolume = () => {
            analyser.getByteFrequencyData(dataArray);
            const volume = getAverageVolume(dataArray);
            if (volume > 15) {
              video.classList.add('booo');
            } else {
              video.classList.remove('booo');
            }
            animationFrame = window.requestAnimationFrame(processVolume);
          };
          processVolume();

          const gainNode = audioCtx.createGain();
          const destinationStream = audioCtx.createMediaStreamDestination(
            audioStream
          );

          const distortion = audioCtx.createWaveShaper();
          distortion.curve = makeDistortionCurve(800);
          distortion.oversample = '2x';

          const biquadFilter = audioCtx.createBiquadFilter();
          biquadFilter.type = 'bandpass';

          const compressor = audioCtx.createDynamicsCompressor();
          compressor.threshold.value = -50;
          compressor.knee.value = 40;
          compressor.ratio.value = 12;
          compressor.attack.value = 0.25;
          compressor.release.value = 0.25;

          const delay = audioCtx.createDelay(2.0);

          src.connect(analyser);
          analyser.connect(delay);
          delay.connect(gainNode);
          gainNode.connect(distortion);
          distortion.connect(biquadFilter);
          biquadFilter.connect(compressor);
          compressor.connect(destinationStream);

          setStream(videoTrack, destinationStream.stream.getAudioTracks()[0]);
          button.innerText = 'Stop';
          button.removeAttribute('disabled');
          video.classList.add('boo');
        })
        .catch(err => {
          console.error('Could not get stream', err);
        });
    }
  });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.