Search code examples
javascripthtmlcsscanvaswebcam

SVG Filter on HTML Canvas Video


So I wanna create glitch effect on canvas, that get every frame from webcam. Code below. Here the code, which works only if width: 200px height: 200px if change this value to width: 600px height: 400px(container size), glitch effect stops working. Here's code where you can change value canvas.width, canvas.height, and see it by yourself CodePen. Does someone know, what a reason for that and how fix it? Or it's some kinda limitations on feTurbulence?

const addEffect = (element, filter) => {
  element.style.webkitFilter = `url(#${filter})`;
  element.style.mozFilter = `url(#${filter})`;
  element.style.filter = `url(#${filter})`;
};

const runEffect = () => {
  const turb = document.querySelector('#displacement feTurbulence');
  const turbVal = { val: 0.000001 };

  const btTl = new TimelineLite({
    paused: true,
    onUpdate() {
      turb.setAttribute('baseFrequency', `0 ${turbVal.val}`);
    },
  });
  btTl.to(turbVal, 0.2, { val: 0.3 });
  btTl.to(turbVal, 0.2, { val: 0.000001 });

  btTl.restart();
};

const URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
const video = document.createElement('video');

navigator.mediaDevices.getUserMedia({
  video: true,
}).then((stream) => {
  video.src = URL.createObjectURL(stream);
});

const canvas = document.querySelector('#canvas');

const ctx = canvas.getContext('2d');
addEffect(canvas, 'displacement');
setInterval(() => runEffect(), 1000);

const loop = () => {
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
  requestAnimationFrame(loop);
};
loop();
.container {
  margin: 0 auto;
  width: 600px;
  height: 400px;
  margin-top: 100px;
  box-shadow: 1px 1px 20px #666;
  position: relative;
}
.container > canvas {
  position: absolute;
  background-color: red;
  mix-blend-mode: darken;
}
svg {
  position: absolute;
  width: 0;
  height: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js"></script>
<div class="container">
      <canvas id="canvas" width="200px" height="200px"></canvas>
      <svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-filters">
        <defs>
          <filter id="displacement">
              <feTurbulence type="fractalNoise" baseFrequency="0.0" numOctaves="1" result="warp"></feTurbulence>
              <feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="80" in="SourceGraphic" in2="warp" />
          </filter>
        </defs>
      </svg>
    </div>


Solution

  • It seems that there is some bug causing the canvas to not be redrawn fast enough when it is larger in size. You can force it to redraw by tweaking a CSS property, such as opacity in the example below.

    const addEffect = (element, filter) => {
      element.style.webkitFilter = `url(#${filter})`;
      element.style.mozFilter = `url(#${filter})`;
      element.style.filter = `url(#${filter})`;
    };
    
    const runEffect = () => {
      const turb = document.querySelector('#displacement feTurbulence');
      const turbVal = { val: 0.000001 };
    
      const btTl = new TimelineLite({
        paused: true,
        onUpdate() {
          turb.setAttribute('baseFrequency', `0 ${turbVal.val}`);
        },
      });
      btTl.to(turbVal, 0.2, { val: 0.3 });
      btTl.to(turbVal, 0.2, { val: 0.000001 });
    
      btTl.restart();
    };
    
    const URL = window.URL || window.webkitURL || window.mozURL || window.msURL;
    const video = document.createElement('video');
    
    navigator.mediaDevices.getUserMedia({
      video: true,
    }).then((stream) => {
      video.src = URL.createObjectURL(stream);
    });
    
    const canvas = document.querySelector('#canvas');
    
    const ctx = canvas.getContext('2d');
    addEffect(canvas, 'displacement');
    setInterval(() => runEffect(), 1000);
    
    /**
     * Use a hack to force the canvas to redraw
     */
    const refreshCanvas = function(){
      let on = true;
      return () => {
        on = !on;
        canvas.style.opacity = on ? 1 : 0.999;
      }
    }();
    
    
    const loop = (timestamp) => {
        ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
        refreshCanvas(); // call our hack
        requestAnimationFrame(loop);
    };
    loop();
    .container {
      margin: 0 auto;
      width: 600px;
      height: 400px;
      margin-top: 100px;
      box-shadow: 1px 1px 20px #666;
      position: relative;
    }
    .container > canvas {
      position: absolute;
      background-color: red;
      mix-blend-mode: darken;
    }
    svg {
      position: absolute;
      width: 0;
      height: 0;
    }
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js"></script>
    <div class="container">
          <canvas id="canvas" width="600" height="400"></canvas>
          <svg xmlns="http://www.w3.org/2000/svg" version="1.1" class="svg-filters">
            <defs>
              <filter id="displacement">
                  <feTurbulence type="fractalNoise" baseFrequency="0.0" numOctaves="1" result="warp"></feTurbulence>
                  <feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="80" in="SourceGraphic" in2="warp" />
              </filter>
            </defs>
          </svg>
        </div>