Masking Out Video Tags with HTML5 Canvas

Nate Hunzaker, Former Development Director

Article Category: #Code

Posted on

For Halloween we built Haunted Hills, a neat video chat app that places users' web cameras into a spooky, ParallaxJS powered graveyard. One of our more curious challenges was to cut out video feeds so that users' faces fit perfectly into place.

Haunted Hills

This feature went through several iterations, including experiments with CSS masks, overflow: hidden, and overlayed graphics. However the best solution I could come up with was to capitalize on the <canvas> element's globalCompositeOperation option, which tells context how to draw on top of existing content.

Video tags are drawable canvas elements

2d canvas context has the ability to draw <img> elements using context.drawImage(), however this also supports the use of <video> and <canvas> tags. With a basic requestAnimationFrame loop, producing a masked video only takes a few steps. Once the images have been selected, all that is required to produce the intended effect is to draw the video feed, set the context's globalCompositeOperation to destination-in, and the mask will eat away the unwanted pixels when drawn.

There is an inverse to destination-in: destination-out. It's possible to use this setting, however the mask would need to be inverted as well. With a mask such as:

A mask

The desired effect can be achieved with the following code:

// Get our mask image
var canvas = document.querySelector('.canvas')
var mask = document.querySelector('.mask')
var video = document.querySelector('.video')

function drawMaskedVideo() {
  ctx.save()

  // Draw the video feed
  ctx.drawImage(video, 0, 0)

  // Set the composite operation, which is responsible for masking
  // see https://developer.mozilla.org/samples/canvas-tutorial/6_1_canvas_composite.html
  ctx.globalCompositeOperation = 'destination-in'

  // Apply the mask
  ctx.drawImage(mask, 0, 0)

  ctx.restore()
}

requestAnimationFrame(function loop() {
  requestAnimationFrame(loop.bind(this))
  drawMaskedVideo()
})

As for the original <video> tag, there are a couple of ways to hide it. However on haunted-hills.com we actually don't even attach the <video> tag to the DOM. I've demonstrated this in the following prototype. Enjoy!

Related Articles