DEV Community

loading...
Cover image for Dynamically masking video using canvas and SVG

Dynamically masking video using canvas and SVG

benjaminblack profile image Benjamin Black ・37 min read

TL;DR what we're building:


Part 1: Masking a static image on a canvas

Let's start with a canvas, an image to use as a matte background, and an SVG snowflake to use as a mask, plus some simple CSS to keep it clean.

<canvas width="960" height="540"></canvas>

<img src="https://images.unsplash.com/photo-1607714724990-8f5495b75883?q=30" width="1875" height="2502">

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 432 432">
  <path fill="#fff" d="M364 318l35 4-21-29-24-2-4-2 19-5a4 4 0 00-2-9l-27 7-33-19 7-2 24 14a4 4 0 104-7l-16-10 4-1 24 4 28-22-35-6-20 16-3 1 9-17a4 4 0 10-7-4l-14 24-16 4-64-37h33l20 20a4 4 0 106-6l-14-14h4l23 10 32-14-32-14-23 10h-4l14-14a4 4 0 10-6-6l-20 20h-33l64-37 16 4 14 24a4 4 0 107-4l-9-17 3 1 20 16 35-6-28-22-24 4-4-1 16-10a4 4 0 00-4-7l-24 13-7-1 33-19 27 7a4 4 0 102-9l-19-5 4-2 24-2 21-29-35 4-15 20-3 2 5-18a4 4 0 10-8-3l-8 27-32 19 1-7 24-14a4 4 0 00-4-8l-16 10 1-4 15-19-5-35-23 28 4 24-1 4-9-17a4 4 0 10-8 5l14 24-4 15-64 37 16-28 27-7a4 4 0 00-2-9l-19 5 2-3 20-15 4-35-29 21-2 24-2 4-5-19a4 4 0 10-8 3l7 26-17 29v-74l12-12h27a4 4 0 000-8h-19l3-3 23-9 13-33-33 13-9 23-3 3V81a4 4 0 10-8 0v27l-6 6V76l20-20a4 4 0 10-6-6l-14 13v-4l10-22-14-33-14 33 10 22v4l-14-13a4 4 0 10-6 6l20 20v38l-6-6V81a4 4 0 00-8 0v19l-3-3-9-23-33-13 13 33 23 9 3 3h-19a4 4 0 000 8h27l12 12v74l-17-29 7-26a4 4 0 00-8-3l-5 19-2-4-2-24-29-21 4 35 20 15 2 3-19-5a4 4 0 10-2 9l27 7 16 28-64-37-4-15 14-24a4 4 0 00-8-5l-9 17-1-4 4-24-23-28-5 35 15 19 1 4-16-10a4 4 0 10-4 8l24 14 1 7-32-19-8-27a4 4 0 10-8 3l5 18-3-2-15-20-35-4 21 29 24 2 4 2-19 5a4 4 0 002 9l27-7 33 19-7 1-24-13a4 4 0 10-4 7l16 10-4 1-24-4-28 22 35 6 20-16 3-1-9 17a4 4 0 107 4l14-24 16-4 64 37h-33l-20-20a4 4 0 10-6 6l14 14h-4l-23-10-32 14 32 14 23-10h4l-14 14a4 4 0 106 6l20-20h33l-64 37-16-4-14-24a4 4 0 00-7 4l9 17-3-1-20-16-35 6 28 22 24-4 4 1-16 10a4 4 0 004 7l24-14 7 2-33 19-27-7a4 4 0 00-2 9l19 5-4 2-24 2-21 29 35-4 15-20 3-2-5 18a4 4 0 008 3l8-27 32-19-1 7-24 14a4 4 0 004 8l16-10-1 4-15 19 5 35 23-28-4-24 1-4 9 17a4 4 0 008-5l-14-24 4-15 64-37-16 28-27 7a4 4 0 102 9l19-5-2 3-20 15-4 35 29-21 2-24 2-4 5 19a4 4 0 108-3l-7-26 17-29v74l-12 12h-27a4 4 0 100 8h19l-3 3-23 9-13 33 33-13 9-23 3-3v19a4 4 0 008 0v-27l6-6v38l-20 20a4 4 0 006 6l14-13v4l-10 22 14 33 14-33-10-22v-4l14 13a4 4 0 006-6l-20-20v-38l6 6v27a4 4 0 008 0v-19l3 3 9 23 33 13-13-33-23-9-3-3h19a4 4 0 000-8h-27l-12-12v-74l17 29-7 26a4 4 0 008 3l5-19 2 4 2 24 29 21-4-35-20-15-2-3 19 5a4 4 0 102-9l-27-7-16-28 64 37 4 15-14 24a4 4 0 108 5l9-17 1 4-4 24 23 28 5-35-15-19-1-4 16 10a4 4 0 004-8l-24-14-1-7 32 19 8 27a4 4 0 108-3l-5-18 3 2 15 20z"/>
</svg>
Enter fullscreen mode Exit fullscreen mode
* { box-sizing: inherit; }

html, body {
  margin: 0;
}

html {
  box-sizing: border-box;
  padding: 0;
  color: white;
  background-color: black;
}

body {
  padding: 1rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1em;
}

canvas {
  width: 100%;
  height: auto;
}

img, svg {
  width: 75vmin;
  height: auto;
}
Enter fullscreen mode Exit fullscreen mode

Since we're waiting for the DOM (and until we get top-level await), we'll wrap our JS in an async main function and call it after DOMContentLoaded is fired on the window.

async function main() {

}

self.addEventListener('DOMContentLoaded', () => main().catch(console.error), {once: true});
Enter fullscreen mode Exit fullscreen mode

For now, we'll just dump our code in main.

Let's begin by declaring some refs that we'll need.

const canvas = document.querySelector('canvas');
const canvasContext = canvas.getContext('2d');
const canvasAspectRatio = canvas.width / canvas.height;

const matte = document.querySelector('img');
Enter fullscreen mode Exit fullscreen mode

We can't draw an inline <svg> directly on the canvas, so we'll convert it to an Image, and then wait for both the matte and mask images to load before proceeding.

const mask = document.createElement('img');

mask.src = URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(document.querySelector('svg'))], {type: 'image/svg+xml'}));

await Promise.all([
  new Promise((resolve) => mask.addEventListener('load', resolve, {once: true})),
  matte.complete || new Promise((resolve) => matte.addEventListener('load', resolve, {once: true})),
]);
Enter fullscreen mode Exit fullscreen mode

Once both images are loaded, we can calculate their aspect ratios and declare some variables we'll need to position them on the canvas.

const matteAspectRatio = matte.naturalWidth / matte.naturalHeight, maskAspectRatio = mask.naturalWidth / mask.naturalHeight;

let width, height, top, left;
Enter fullscreen mode Exit fullscreen mode

Next, we'll draw the matte background on the canvas, scaling and positioning it so that it covers the canvas.

[width, height] =
  canvasAspectRatio <= matteAspectRatio
  ? [canvas.height * matteAspectRatio, canvas.height]
  : [canvas.width, canvas.width / matteAspectRatio];

[top, left] = [(canvas.height - height) / 2, (canvas.width - width) / 2];

canvasContext.drawImage(matte, 0, 0, matte.naturalWidth, matte.naturalHeight, left, top, width, height);
Enter fullscreen mode Exit fullscreen mode

Now let's mask the image on the canvas using destination-in compositing. We'll draw the mask to fit inside the canvas, instead of filling it (simply by switching the cases of the ternary expression to calculate width and height).

[width, height] = canvasAspectRatio <= maskAspectRatio
  ? [canvas.width, canvas.width / maskAspectRatio]
  : [canvas.height * maskAspectRatio, canvas.height];

[top, left] = [(canvas.height - height) / 2, (canvas.width - width) / 2];

canvasContext.globalCompositeOperation = 'destination-in';

canvasContext.drawImage(mask, left, top, width, height);
Enter fullscreen mode Exit fullscreen mode

Part 1 Pen


Part 2: Using a video instead of a matte background

Let's replace the matte background image with a video. Instead of adding a <video> element to the DOM, we'll keep the document clean and create the video programmatically.

We'll use requestAnimationFrame to redraw every frame of the video on the canvas.

Let's wipe the main function and start over.

We'll remember the dimensions and position of the video and mask, instead of recalculating them on each frame, so they'll need their own variables instead of reusing width, height, left, and top.

async function main() {
  const canvas = document.querySelector('canvas');
  const canvasContext = canvas.getContext('2d');
  const canvasAspectRatio = canvas.width / canvas.height;

  const video = document.createElement('video');

  video.muted = true;
  video.autoplay = true;
  video.loop = true;
  video.playsinline = true;
  video.src = "https://giant.gfycat.com/CheapBitterCaterpillar.webm";

  await video.play();

  const videoAspectRatio = video.videoWidth / video.videoHeight;

  const [frameWidth, frameHeight] =
    canvasAspectRatio <= videoAspectRatio
    ? [canvas.height * videoAspectRatio, canvas.height]
  : [canvas.width, canvas.width / videoAspectRatio];

  const [frameTop, frameLeft] = [(canvas.height - frameHeight) / 2, (canvas.width - frameWidth) / 2];

  requestAnimationFrame(function frame() {
    canvasContext.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, frameLeft, frameTop, frameWidth, frameHeight);
    requestAnimationFrame(frame);
  });
}
Enter fullscreen mode Exit fullscreen mode

Now the video is being drawn on the canvas on every frame. Let's write the code to load the mask. We'll scale the mask to be centered and contained within the canvas.

Let's also get rid of the mask <svg> from the DOM, and create it dynamically in Javascript, skipping an intensive serialization step.

We also need to update the requestAnimationFrame, since it overwrites the canvas on each frame. We'll switch the drawing order so that the mask is drawn first, using destination-over compositing (to overwrite the previous frame), and then draw the video frame using source-in compositing. (This is to prepare for using multiple masks instead of just one.)

async function main() {
  const canvas = document.querySelector('canvas');
  const canvasContext = canvas.getContext('2d');
  const canvasAspectRatio = canvas.width / canvas.height;

  const video = document.createElement('video');
  video.muted = true;
  video.autoplay = true;
  video.loop = true;
  video.playsinline = true;
  video.src = "https://giant.gfycat.com/CheapBitterCaterpillar.webm";

  const maskImage = document.createElement('img');
  maskImage.src = URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" width="432" height="432"><path fill="#fff" d="M364 318l35 4-21-29-24-2-4-2 19-5a4 4 0 00-2-9l-27 7-33-19 7-2 24 14a4 4 0 104-7l-16-10 4-1 24 4 28-22-35-6-20 16-3 1 9-17a4 4 0 10-7-4l-14 24-16 4-64-37h33l20 20a4 4 0 106-6l-14-14h4l23 10 32-14-32-14-23 10h-4l14-14a4 4 0 10-6-6l-20 20h-33l64-37 16 4 14 24a4 4 0 107-4l-9-17 3 1 20 16 35-6-28-22-24 4-4-1 16-10a4 4 0 00-4-7l-24 13-7-1 33-19 27 7a4 4 0 102-9l-19-5 4-2 24-2 21-29-35 4-15 20-3 2 5-18a4 4 0 10-8-3l-8 27-32 19 1-7 24-14a4 4 0 00-4-8l-16 10 1-4 15-19-5-35-23 28 4 24-1 4-9-17a4 4 0 10-8 5l14 24-4 15-64 37 16-28 27-7a4 4 0 00-2-9l-19 5 2-3 20-15 4-35-29 21-2 24-2 4-5-19a4 4 0 10-8 3l7 26-17 29v-74l12-12h27a4 4 0 000-8h-19l3-3 23-9 13-33-33 13-9 23-3 3V81a4 4 0 10-8 0v27l-6 6V76l20-20a4 4 0 10-6-6l-14 13v-4l10-22-14-33-14 33 10 22v4l-14-13a4 4 0 10-6 6l20 20v38l-6-6V81a4 4 0 00-8 0v19l-3-3-9-23-33-13 13 33 23 9 3 3h-19a4 4 0 000 8h27l12 12v74l-17-29 7-26a4 4 0 00-8-3l-5 19-2-4-2-24-29-21 4 35 20 15 2 3-19-5a4 4 0 10-2 9l27 7 16 28-64-37-4-15 14-24a4 4 0 00-8-5l-9 17-1-4 4-24-23-28-5 35 15 19 1 4-16-10a4 4 0 10-4 8l24 14 1 7-32-19-8-27a4 4 0 10-8 3l5 18-3-2-15-20-35-4 21 29 24 2 4 2-19 5a4 4 0 002 9l27-7 33 19-7 1-24-13a4 4 0 10-4 7l16 10-4 1-24-4-28 22 35 6 20-16 3-1-9 17a4 4 0 107 4l14-24 16-4 64 37h-33l-20-20a4 4 0 10-6 6l14 14h-4l-23-10-32 14 32 14 23-10h4l-14 14a4 4 0 106 6l20-20h33l-64 37-16-4-14-24a4 4 0 00-7 4l9 17-3-1-20-16-35 6 28 22 24-4 4 1-16 10a4 4 0 004 7l24-14 7 2-33 19-27-7a4 4 0 00-2 9l19 5-4 2-24 2-21 29 35-4 15-20 3-2-5 18a4 4 0 008 3l8-27 32-19-1 7-24 14a4 4 0 004 8l16-10-1 4-15 19 5 35 23-28-4-24 1-4 9 17a4 4 0 008-5l-14-24 4-15 64-37-16 28-27 7a4 4 0 102 9l19-5-2 3-20 15-4 35 29-21 2-24 2-4 5 19a4 4 0 108-3l-7-26 17-29v74l-12 12h-27a4 4 0 100 8h19l-3 3-23 9-13 33 33-13 9-23 3-3v19a4 4 0 008 0v-27l6-6v38l-20 20a4 4 0 006 6l14-13v4l-10 22 14 33 14-33-10-22v-4l14 13a4 4 0 006-6l-20-20v-38l6 6v27a4 4 0 008 0v-19l3 3 9 23 33 13-13-33-23-9-3-3h19a4 4 0 000-8h-27l-12-12v-74l17 29-7 26a4 4 0 008 3l5-19 2 4 2 24 29 21-4-35-20-15-2-3 19 5a4 4 0 102-9l-27-7-16-28 64 37 4 15-14 24a4 4 0 108 5l9-17 1 4-4 24 23 28 5-35-15-19-1-4 16 10a4 4 0 004-8l-24-14-1-7 32 19 8 27a4 4 0 108-3l-5-18 3 2 15 20z"/></svg>`], {type: 'image/svg+xml'}));

  await Promise.all([
    video.play(),
    new Promise((resolve) => maskImage.addEventListener('load', resolve, {once: true})),
  ]);

  const videoAspectRatio = video.videoWidth / video.videoHeight;

  const [frameWidth, frameHeight] =
    canvasAspectRatio <= videoAspectRatio
    ? [canvas.height * videoAspectRatio, canvas.height]
  : [canvas.width, canvas.width / videoAspectRatio];

  const [frameTop, frameLeft] = [(canvas.height - frameHeight) / 2, (canvas.width - frameWidth) / 2];

  const maskAspectRatio = maskImage.naturalWidth / maskImage.naturalHeight;

  const [maskWidth, maskHeight] =
        canvasAspectRatio <= maskAspectRatio
  ? [canvas.width, canvas.width / maskAspectRatio]
  : [canvas.height * maskAspectRatio, canvas.height];

  const [maskTop, maskLeft] = [(canvas.height - maskHeight) / 2, (canvas.width - maskWidth) / 2];

  requestAnimationFrame(function frame() {
    canvasContext.globalCompositeOperation = 'destination-over';
    canvasContext.drawImage(maskImage, maskLeft, maskTop, maskWidth, maskHeight);

    canvasContext.globalCompositeOperation = 'source-in';
    canvasContext.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, frameLeft, frameTop, frameWidth, frameHeight);

    requestAnimationFrame(frame);
  });
}
Enter fullscreen mode Exit fullscreen mode

Now we have a video that is being dynamically masked on every frame.

Since the HTML only contains a single <canvas> element, let's tweak the CSS a little bit.

* { box-sizing: inherit; }

html, body {
  margin: 0;
}

html {
  box-sizing: border-box;
  padding: 0;
  color: white;
  background-color: black;
  height: 100%;
}

body {
  padding: 1rem;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

canvas {
  width: 100%;
  height: auto;
}
Enter fullscreen mode Exit fullscreen mode

Part 2 Pen


Part 3: Define multiple masks and use one at random

Instead of a single big snowflake, let's cache a bunch of different snowflake patterns in an array. We'll create an array of promises that will resolve with a loaded mask image, and which will replace themselves in the array with the resulting Image object, so that we can use Promise.all on the entire array to wait for all images to load.

We'll declare it at the top-level scope, so that the images can begin loading immediately instead of waiting for main() to be executed.

const masks = Array.from([
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" width="432" height="432"><path fill="#fff" d="M364 318l35 4-21-29-24-2-4-2 19-5a4 4 0 00-2-9l-27 7-33-19 7-2 24 14a4 4 0 104-7l-16-10 4-1 24 4 28-22-35-6-20 16-3 1 9-17a4 4 0 10-7-4l-14 24-16 4-64-37h33l20 20a4 4 0 106-6l-14-14h4l23 10 32-14-32-14-23 10h-4l14-14a4 4 0 10-6-6l-20 20h-33l64-37 16 4 14 24a4 4 0 107-4l-9-17 3 1 20 16 35-6-28-22-24 4-4-1 16-10a4 4 0 00-4-7l-24 13-7-1 33-19 27 7a4 4 0 102-9l-19-5 4-2 24-2 21-29-35 4-15 20-3 2 5-18a4 4 0 10-8-3l-8 27-32 19 1-7 24-14a4 4 0 00-4-8l-16 10 1-4 15-19-5-35-23 28 4 24-1 4-9-17a4 4 0 10-8 5l14 24-4 15-64 37 16-28 27-7a4 4 0 00-2-9l-19 5 2-3 20-15 4-35-29 21-2 24-2 4-5-19a4 4 0 10-8 3l7 26-17 29v-74l12-12h27a4 4 0 000-8h-19l3-3 23-9 13-33-33 13-9 23-3 3V81a4 4 0 10-8 0v27l-6 6V76l20-20a4 4 0 10-6-6l-14 13v-4l10-22-14-33-14 33 10 22v4l-14-13a4 4 0 10-6 6l20 20v38l-6-6V81a4 4 0 00-8 0v19l-3-3-9-23-33-13 13 33 23 9 3 3h-19a4 4 0 000 8h27l12 12v74l-17-29 7-26a4 4 0 00-8-3l-5 19-2-4-2-24-29-21 4 35 20 15 2 3-19-5a4 4 0 10-2 9l27 7 16 28-64-37-4-15 14-24a4 4 0 00-8-5l-9 17-1-4 4-24-23-28-5 35 15 19 1 4-16-10a4 4 0 10-4 8l24 14 1 7-32-19-8-27a4 4 0 10-8 3l5 18-3-2-15-20-35-4 21 29 24 2 4 2-19 5a4 4 0 002 9l27-7 33 19-7 1-24-13a4 4 0 10-4 7l16 10-4 1-24-4-28 22 35 6 20-16 3-1-9 17a4 4 0 107 4l14-24 16-4 64 37h-33l-20-20a4 4 0 10-6 6l14 14h-4l-23-10-32 14 32 14 23-10h4l-14 14a4 4 0 106 6l20-20h33l-64 37-16-4-14-24a4 4 0 00-7 4l9 17-3-1-20-16-35 6 28 22 24-4 4 1-16 10a4 4 0 004 7l24-14 7 2-33 19-27-7a4 4 0 00-2 9l19 5-4 2-24 2-21 29 35-4 15-20 3-2-5 18a4 4 0 008 3l8-27 32-19-1 7-24 14a4 4 0 004 8l16-10-1 4-15 19 5 35 23-28-4-24 1-4 9 17a4 4 0 008-5l-14-24 4-15 64-37-16 28-27 7a4 4 0 102 9l19-5-2 3-20 15-4 35 29-21 2-24 2-4 5 19a4 4 0 108-3l-7-26 17-29v74l-12 12h-27a4 4 0 100 8h19l-3 3-23 9-13 33 33-13 9-23 3-3v19a4 4 0 008 0v-27l6-6v38l-20 20a4 4 0 006 6l14-13v4l-10 22 14 33 14-33-10-22v-4l14 13a4 4 0 006-6l-20-20v-38l6 6v27a4 4 0 008 0v-19l3 3 9 23 33 13-13-33-23-9-3-3h19a4 4 0 000-8h-27l-12-12v-74l17 29-7 26a4 4 0 008 3l5-19 2 4 2 24 29 21-4-35-20-15-2-3 19 5a4 4 0 102-9l-27-7-16-28 64 37 4 15-14 24a4 4 0 108 5l9-17 1 4-4 24 23 28 5-35-15-19-1-4 16 10a4 4 0 004-8l-24-14-1-7 32 19 8 27a4 4 0 108-3l-5-18 3 2 15 20z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 296 296"><path d="M250 185a7 7 0 00-12 1l-2 4-13-8 19-12a7 7 0 10-8-13l-25 16-19-25 19-26 25 17a7 7 0 0010-2c3-3 2-8-2-10l-19-14 13-8 2 5a7 7 0 0012 1l26-29a7 7 0 00-8-12l-37 8a7 7 0 00-5 11l2 4-13 7-1-23a8 8 0 00-15 1l2 29-32 4-13-29 27-14c3-2 5-6 3-10s-7-5-11-3l-20 11V51h5a7 7 0 006-10L155 5c-1-3-4-5-7-5-4 0-7 2-8 5l-12 36a7 7 0 007 10h5v15l-22-10c-3-2-8-1-10 3-1 4 0 8 4 10l26 13-12 29-31-4 1-29a7 7 0 10-15-1l-1 23-12-7 2-4a8 8 0 00-5-11l-37-8a7 7 0 00-8 13l26 28a7 7 0 0012-1l2-4 13 7-20 14a8 8 0 009 12l24-16 18 25-18 25-25-16a8 8 0 00-8 13l20 12-13 8-2-4a7 7 0 00-12-1l-25 28a8 8 0 007 12l37-7a8 8 0 005-11l-2-5 13-7 1 24a7 7 0 1015-1l-2-29 31-4 12 29-26 13c-4 1-5 6-3 10 1 3 6 5 9 3l22-10v15h-5a7 7 0 00-7 10l12 36c1 3 4 5 8 5 3 0 6-2 7-5l12-36a8 8 0 00-7-10h-5v-15l20 11 4 1c3 0 5-2 7-4 2-4 0-9-3-11l-27-13 13-29 31 4-2 29a8 8 0 0015 1l2-24 13 8-2 4a7 7 0 005 11l37 8a7 7 0 008-4c2-3 2-6-1-9l-25-28z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297"><path d="M290 142h-12l9-10a7 7 0 10-11-10l-19 20h-14l9-10a7 7 0 10-11-10l-19 20h-13l17-19 2-5c0-4-3-7-7-7h-23l8-8h27a7 7 0 100-15h-12l10-10h27a7 7 0 100-15h-12l8-9a8 8 0 00-11-10l-8 8V39c0-4-4-8-8-8s-7 4-7 8v28l-10 10V64a8 8 0 00-15 0l1 27-9 9V78c0-3 0-5-2-6-3-3-8-4-11-1l-19 17V74l20-18c3-3 3-8 0-11-2-3-7-3-10 0l-10 9V40l20-19c3-3 3-8 0-11s-7-3-10 0l-10 9V8a7 7 0 10-15 0v10l-8-8c-3-3-7-3-10 0s-3 8-1 10l19 19v14l-8-8c-3-3-7-3-10 0s-3 8-1 10l19 19v13l-17-16a7 7 0 00-12 6l-1 22-8-8V65a7 7 0 10-15 0v11L77 66V40a7 7 0 10-15 0v11l-8-8c-3-3-7-3-10 0s-3 8 0 11l8 8H39c-4 0-7 4-7 8s3 7 7 7h28l10 10H64a7 7 0 100 15l27-1 10 10H78l-7 2c-3 3-3 8 0 11l1 1 16 17H74l-18-20a7 7 0 10-11 10l9 9H40l-19-19a7 7 0 10-11 10l9 9H8a7 7 0 100 15h10l-8 9a7 7 0 1010 10l19-18h14l-8 8a7 7 0 1010 10l19-18h13l-16 16a7 7 0 005 13l24 1-9 9-27-1a8 8 0 000 15l12 1-10 10-27-1c-4 0-7 4-8 8 0 4 4 7 8 7h13l-8 8a8 8 0 005 13l5-2 7-7v12a7 7 0 1015 0v-27l10-10v12a7 7 0 1015 0v-27l10-10v23a8 8 0 008 8c2 1 4-1 6-2l15-16v12l-18 19c-3 3-3 8 0 11l5 2 5-2 8-9v14l-18 19c-3 3-3 8 0 11l5 2 5-2 8-9v12a7 7 0 1015 0v-12l10 9a8 8 0 0010 0c3-3 3-8 0-11l-20-19v-14l10 9a8 8 0 0010 0c3-3 3-8 0-11l-20-19v-13l17 17 7 2c4 0 7-3 7-7v-25l10 10-1 27a7 7 0 1015 1l1-13 10 9-1 28c0 4 3 7 7 7l1 1c4 0 7-4 7-8v-13l8 8a7 7 0 0011 0c3-3 3-7 0-10l-7-7h11a7 7 0 100-15h-26l-10-10h11a7 7 0 100-15h-26l-9-10 23-1 5-2c3-3 3-7 0-10l-16-16h12l19 18a7 7 0 0011 0c3-3 3-7 0-10l-9-8h14l19 18a7 7 0 0011 0c3-3 3-7 0-10l-9-8h12a7 7 0 100-15zm-175-17l17 17h-24l-14-16 21-1zm0 47H94l14-16h23l-16 16zm25 17l-14 14-1-20 15-15v21zm0-60l-15-15V94l15 14v21zm15-21l17-15v22l-17 17v-24zm16 96l-16-15v-24l16 17v22zm12-32l-15-15h21l14 14-20 1zm6-30h-22l16-17h21l-15 17z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M291 142h-12v-11a7 7 0 10-15 0v11h-9v-17a7 7 0 10-15 0v17h-69l-2-3 50-49 12 12a7 7 0 0011 0c3-3 3-8 0-11l-12-12 6-6 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 7-8a8 8 0 00-10-10l-8 7-8-7a8 8 0 00-10 10l7 8-6 6-12-12a8 8 0 00-11 11l12 12-50 50-2-1V58h18a7 7 0 100-15h-18v-9h12a7 7 0 100-15h-12V8a7 7 0 10-15 0v11h-10a7 7 0 100 15h10v9h-16a7 7 0 100 15h16v70l-1 1-50-50 12-12a7 7 0 10-11-11L79 68l-6-6 7-8a8 8 0 00-10-10l-8 7-8-7a8 8 0 00-10 10l7 8-7 8a8 8 0 1010 10l8-7 6 6-12 12a7 7 0 1011 11l12-12 50 50-1 1H57v-16a7 7 0 10-15 0v17h-9v-11a7 7 0 10-15 0v11H8a7 7 0 100 15h10v11a7 7 0 1015 0v-11h9v17a7 7 0 1015 0v-17h71l1 2-50 49-12-12a8 8 0 00-11 11l12 12-6 6-8-7a7 7 0 10-10 10l7 8-7 8a8 8 0 1010 10l8-7 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 6-6 12 12a7 7 0 0011 0c3-3 3-8 0-11l-12-12 49-50 2 2v70h-16a7 7 0 100 15h16v9h-10a7 7 0 100 15h10v11a7 7 0 1015 0v-11h12a7 7 0 100-15h-12v-9h18a7 7 0 100-15h-18v-70l3-1 49 49-12 12a8 8 0 0011 11l12-12 6 6-7 8a8 8 0 0010 10l8-7 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 7-8a8 8 0 00-10-10l-8 7-6-6 12-12a8 8 0 00-11-11l-12 12-49-49 1-3h69v17a7 7 0 1015 0v-17h9v11a7 7 0 1015 0v-11h12a7 7 0 100-14z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297"><path d="M290 133c-4-5-10-7-16-7a22 22 0 00-21 15h-20l8-8a8 8 0 00-10-11l-19 19h-22l-8-16 17-16h27a7 7 0 100-15h-12l14-14a22 22 0 0025-5 23 23 0 00-16-38 22 22 0 00-22 22c0 4 0 7 2 10l-14 14V72a7 7 0 10-15 0v26l-16 16-16-6V86l19-20c3-3 3-7 0-10s-8-3-11 0l-8 8V44c9-3 15-12 15-21 0-13-10-23-23-23-12 0-22 10-22 23 0 9 7 18 15 21v20l-9-8c-3-3-8-3-11 0-2 3-3 7 0 10l20 20v21l-18 6-14-15V71a7 7 0 10-15 0v12L80 69a22 22 0 00-21-32 22 22 0 00-22 22 22 22 0 0022 23l10-2 14 14H72a7 7 0 100 15h26l15 14-8 18H85l-19-19a8 8 0 00-11 10l9 9H44a23 23 0 100 15h20l-8 8a7 7 0 1010 11l19-19h21l6 18-14 14H71a7 7 0 100 15h12l-14 14-10-2a22 22 0 00-22 22 22 22 0 0022 23 22 22 0 0021-32l14-14v11a7 7 0 1015 0v-26l14-15 18 7v21l-20 19c-2 3-2 8 1 11l5 2 5-2 9-9v20c-8 3-15 12-15 21a23 23 0 0045 0c0-9-6-18-15-21v-20l8 8a8 8 0 0011 0c3-3 2-8-1-11l-18-18v-22l16-7 16 15v28a7 7 0 1015 0v-13l14 15c-2 3-2 6-2 9a22 22 0 0022 23 22 22 0 0023-23 22 22 0 00-23-22l-9 2-14-14h11a7 7 0 100-15h-26l-16-16 7-16h22l19 19a7 7 0 0011 0c3-3 3-7 0-10l-9-9h20a23 23 0 1037-23zm-122 36l-21 9-20-9-8-21 8-20 20-8 20 9 10 19-9 21z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M290 141h-25l11-12a7 7 0 10-11-10l-21 22h-37l16-18a8 8 0 00-4-12h-22l24-24h31a7 7 0 100-15h-16l18-18c3-3 3-8 0-11s-8-3-11 0l-18 18V46a7 7 0 10-15 0v30l-25 25V78l-2-5c-3-3-8-3-11 0l-16 15V53l22-21c3-3 3-8 0-11s-8-3-11 0l-11 11V8a7 7 0 10-15 0v24l-10-11c-3-3-8-3-11 0s-3 8 0 11l21 21v36l-16-16a7 7 0 00-12 4l-1 2v21L87 75V46a7 7 0 10-15 0v14L55 43c-3-3-7-3-10 0s-3 8 0 11l17 18H47a7 7 0 100 15h30l25 24-23 1c-4 0-7 4-7 7l-1 1h1c0 2 0 4 2 5l16 16H55l-22-22a8 8 0 00-10 10l11 11H9a7 7 0 100 15h25l-11 11a8 8 0 1010 11l22-21h35l-15 16a8 8 0 005 13v-1 1l22-1-25 26H47a7 7 0 100 15h15l-17 17a8 8 0 005 13l5-2 17-18v16a7 7 0 1015 0v-31l26-25 1 24c0 4 3 8 7 8l4-2 16-16v36l-21 21c-3 3-3 7 0 10l6 3c2 0 3-1 5-3l10-10v25a7 7 0 1015 0v-26l12 11a8 8 0 0010 0c3-3 3-7 0-10l-22-22v-36a5625 5625 0 0023 19c4 0 7-4 7-8v-24l24 25v31a7 7 0 1015 0v-16l18 18a8 8 0 0011 0c3-3 3-8 0-11l-17-17h15a7 7 0 100-15h-30l-26-26 23-1a7 7 0 004-14l-14-13h35l21 21a7 7 0 0011 0c3-3 3-8 0-11l-11-10h25a7 7 0 100-15zm-89-16l-14 16h-20l15-16h19zm-31-29l1 20-15 14v-21l14-13zm-43 0l14 13v20l-14-14V96zm-30 30h20l14 15h-20l-14-15zm0 43l13-13h21l-14 13H97zm31 33l-1-21 14-15v22l-13 14zm43-1l-15-13v-22l15 14v21zm30-32h-20l-13-14h20l13 14z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297"><path d="M295 140a17 17 0 00-30 2h-16l-39-20 14-8 9 7a7 7 0 0010-1c3-3 2-8-1-11l-4-3 11-6 6 2a8 8 0 008-4l9-13a8 8 0 00-7-11h-15c-4 1-6 3-7 6l-2 7-11 6v-4a7 7 0 10-15 2l1 10-14 8 3-43 9-15a16 16 0 009-3 17 17 0 10-22-5l-9 16-35 23V67l10-4c4-2 6-6 4-10-1-4-6-6-10-4l-4 2V38l4-5c3-2 3-6 2-9l-7-13a7 7 0 00-14 0l-7 13c-1 3-1 7 1 9l6 6v11l-5-1c-3-2-8 0-9 4-2 4 0 8 4 10l10 4v16l-37-24-9-15a17 17 0 00-15-26 17 17 0 102 34l10 15 2 42-13-8 2-11a7 7 0 10-15-2l-1 5-11-6-2-7c0-3-3-5-6-5l-16-1a8 8 0 00-7 11l9 14a8 8 0 008 3l7-2 10 6-3 3a7 7 0 109 11l8-6 14 8-40 20H32a17 17 0 00-15-10 17 17 0 00-16 21 17 17 0 0016 12c6 0 11-3 14-8h20l37 18-13 8-9-7a7 7 0 10-10 12l5 3-11 6-7-1c-3-1-6 0-8 3l-9 13a8 8 0 007 11h16c3 0 6-3 6-6l2-7 11-6v4a8 8 0 1015-2l-1-10 13-8-2 43-9 16a17 17 0 1014 7l8-16 36-24v16l-11 5c-4 1-6 5-4 9 1 3 4 5 7 5l2-1 6-2v13l-5 5c-3 2-3 6-2 9l7 14c2 2 4 4 7 4s6-2 7-4l7-14c2-3 1-7-1-9l-5-5v-12l3 1c4 2 9 0 10-4 2-3 0-8-4-9l-9-4v-16l35 24 12 17c-2 2-3 5-3 9 0 9 8 17 17 17a17 17 0 10-2-34l-11-17-3-43 14 8-1 11a7 7 0 1014 2l1-5 11 6 2 7c1 3 3 5 7 6h16a7 7 0 006-11l-8-13c-2-3-6-4-9-4l-7 2-10-6 3-2a7 7 0 10-9-12l-8 6-14-8 37-18h19c3 5 8 8 14 8a17 17 0 0015-25zM189 79l-2 39-30 17v-35l32-21zm-81 0l34 22v35l-31-18-3-39zm-40 70l36-18 30 18-30 17-36-17zm40 69l3-39 31-18v35l-34 22zm81 1l-32-22v-35l30 17 2 40zm5-53l-30-17 30-18 36 18-36 17z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 296 296"><path d="M270 201a15 15 0 00-14 0l-12-6 24-11a7 7 0 00-7-14l-32 16-11-7 23-11a7 7 0 10-7-13l-32 15-24-13v-18l23-14 32 17a7 7 0 0010-4c2-3 1-8-3-10l-23-12 11-6 32 16a7 7 0 0010-3c2-4 1-8-3-10l-23-12 11-7a15 15 0 10-7-13l-11 7 2-26a7 7 0 10-15-1l-3 36-11 6 2-26a7 7 0 10-15-1l-3 36-23 14-15-10V90l29-19a7 7 0 10-8-13l-21 14V59l29-19a7 7 0 10-8-13l-21 14V28c4-3 7-7 7-13a15 15 0 00-30 0c0 6 3 10 8 13v13l-22-15a8 8 0 00-9 13l31 20v13l-22-15a8 8 0 00-9 13l31 20v26l-17 9-22-13-2-35a7 7 0 10-15 1l1 25-11-6-2-36c0-4-4-7-8-7-4 1-7 4-7 8l1 26-11-7a15 15 0 10-8 13l12 7-24 11a7 7 0 107 14l32-16 11 6-23 12a7 7 0 106 13l33-16 23 13v19l-23 13-32-16a7 7 0 10-6 13l22 12-11 7-31-17a7 7 0 10-7 14l23 12-12 6a15 15 0 108 13l11-6-2 26a7 7 0 1015 1l3-36 11-7-2 26a7 7 0 1015 1l2-36 23-13 17 9v26l-31 20a8 8 0 009 12l22-14v13l-31 20a7 7 0 109 12l21-14v13c-5 3-7 8-7 14a15 15 0 1022-13v-13l22 14a7 7 0 0010-2c3-3 2-8-2-10l-30-20v-13l22 14a7 7 0 0010-2c3-3 2-8-2-10l-30-20v-27l16-9 23 13 2 36a7 7 0 1015-1l-2-26 12 6 2 36a7 7 0 1015-1l-2-26 12 7a15 15 0 1022-13z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297"><path d="M275 213l-21-12 21-14a8 8 0 00-8-12l-28 17-11-6 11-9a8 8 0 00-9-12l-16 13-25-15v-29l25-15 18 11a7 7 0 0010-2c2-4 1-8-2-10l-11-7 10-6 28 17a7 7 0 0010-3c2-3 1-8-2-10l-21-13 20-11a7 7 0 00-7-13l-21 12-1-26c-1-4-4-7-8-7-4 1-7 4-7 8l1 33-11 7-2-14a7 7 0 10-14 1l2 21-25 14-26-16V78l19-9c4-1 6-6 4-9-2-4-6-6-10-4l-13 5V48l29-15c4-2 6-6 4-10s-7-5-10-3l-23 11V8a7 7 0 10-15 0v23l-21-12c-4-2-8-1-10 3-2 3-1 8 3 10l28 16v12l-11-6c-4-2-8-1-10 3s-1 8 3 10l18 10v29l-25 14-24-13 2-21a7 7 0 10-14-1l-2 14-11-7 1-33a7 7 0 10-15-1l-1 26-21-12a7 7 0 10-7 13l20 11-21 13a8 8 0 008 13l28-17 10 6-11 7a7 7 0 108 12l18-11 24 14v31l-24 14-16-13a8 8 0 00-9 12l11 9-11 6-28-17a8 8 0 00-8 12l21 14-21 12a7 7 0 008 13l20-12v24c0 4 4 7 8 7s7-3 7-7v-32l10-7 1 13a8 8 0 0015 0l-1-21 25-15 25 15v29l-18 10c-4 2-5 6-3 10 1 3 4 4 7 4l3-1 11-6v12l-28 16c-4 2-5 7-3 10 1 3 4 4 7 4l3-1 21-12v24a7 7 0 1015 0v-24l23 11c3 2 8 1 10-3s0-8-4-10l-29-15v-13l13 5c4 2 8 0 10-4 2-3 0-8-4-9l-19-9v-27l27-15 25 14-1 21a8 8 0 0015 0l1-13 10 7v32c0 4 3 7 7 7s8-3 8-7v-24l20 12a7 7 0 0010-3c2-4 1-8-2-10z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M275 213l-17-10 16-7a7 7 0 10-6-14l-27 11-47-27 28-11a8 8 0 000-14l-29-10 48-28 27 11a7 7 0 105-14l-16-6 18-11a7 7 0 00-7-13l-18 10 2-17a7 7 0 10-15-2l-3 29-48 27 5-29c1-3-1-7-3-8-3-2-7-1-9 1l-24 19V48l24-18c3-2 4-7 1-10-2-4-7-4-10-2l-15 11V8a7 7 0 10-15 0v21l-13-10c-3-3-7-2-10 1s-2 8 1 10l22 18v52l-23-19c-2-3-6-3-8-1-3 1-4 4-4 8l5 30-45-27-4-29a7 7 0 10-15 2l3 18-19-11a7 7 0 10-7 13l19 11-16 6a8 8 0 005 14l27-10 45 25-29 11a8 8 0 000 14l29 10-45 27-27-12a7 7 0 10-6 14l16 7-18 11a7 7 0 007 13l19-11-3 16a8 8 0 1015 3l5-29 44-25-5 29c0 3 1 7 4 8 2 2 6 1 8-1l23-19v55l-22 18c-3 2-4 7-1 10s7 4 10 1l13-10v21a7 7 0 1015 0v-21l15 11a8 8 0 0010-2c3-3 2-8-1-10l-24-18v-55l24 19a8 8 0 008 1c3-1 5-4 4-8l-5-30 47 28 5 28a8 8 0 0014-2l-2-17 18 11a7 7 0 0010-3c2-4 1-8-3-10zm-77-65l-21 8-14-8 14-8 21 8zm-25-43l-4 22-14 8v-15l18-15zm-50 0l17 14v16l-13-8-4-22zm-24 43l21-8 13 8-13 8-21-8zm24 43l4-22 13-8v15l-17 15zm50 0l-18-14v-16l14 8 4 22z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M291 142h-12v-11a7 7 0 10-15 0v11h-9v-17a7 7 0 10-15 0v17h-69l-2-3 50-49 12 12a7 7 0 0011 0c3-3 3-8 0-11l-12-12 6-6 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 7-8a8 8 0 00-10-10l-8 7-8-7a8 8 0 00-10 10l7 8-6 6-12-12a8 8 0 00-11 11l12 12-50 50-2-1V58h18a7 7 0 100-15h-18v-9h12a7 7 0 100-15h-12V8a7 7 0 10-15 0v11h-10a7 7 0 100 15h10v9h-16a7 7 0 100 15h16v70l-1 1-50-50 12-12a7 7 0 10-11-11L79 68l-6-6 7-8a8 8 0 00-10-10l-8 7-8-7a8 8 0 00-10 10l7 8-7 8a8 8 0 1010 10l8-7 6 6-12 12a7 7 0 1011 11l12-12 50 50-1 1H57v-16a7 7 0 10-15 0v17h-9v-11a7 7 0 10-15 0v11H8a7 7 0 100 15h10v11a7 7 0 1015 0v-11h9v17a7 7 0 1015 0v-17h71l1 2-50 49-12-12a8 8 0 00-11 11l12 12-6 6-8-7a7 7 0 10-10 10l7 8-7 8a8 8 0 1010 10l8-7 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 6-6 12 12a7 7 0 0011 0c3-3 3-8 0-11l-12-12 49-50 2 2v70h-16a7 7 0 100 15h16v9h-10a7 7 0 100 15h10v11a7 7 0 1015 0v-11h12a7 7 0 100-15h-12v-9h18a7 7 0 100-15h-18v-70l3-1 49 49-12 12a8 8 0 0011 11l12-12 6 6-7 8a8 8 0 0010 10l8-7 8 7a7 7 0 0010 0c3-3 3-7 0-10l-7-8 7-8a8 8 0 00-10-10l-8 7-6-6 12-12a8 8 0 00-11-11l-12 12-49-49 1-3h69v17a7 7 0 1015 0v-17h9v11a7 7 0 1015 0v-11h12a7 7 0 100-14z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M291 143h-23l11-11a7 7 0 10-11-10l-19 19-43-15 20-42 1-1h25a7 7 0 100-14h-10l13-14c3-3 3-8 0-11s-8-3-11 0l-16 16V46a7 7 0 10-15 0v28l-40 19-16-44 19-19c3-2 3-7 0-10s-8-3-10 0l-10 9V8a7 7 0 10-15 0v20l-8-8c-3-3-7-3-10 0s-3 8 0 10l18 19-15 44-42-20V47a7 7 0 10-15 0v12L54 44c-3-3-7-3-10 0s-3 8 0 11l15 15H47a7 7 0 100 15h26l20 41-43 15-19-19a8 8 0 00-11 11l10 9H8a7 7 0 100 15h21l-9 9a8 8 0 1011 10l19-18 43 15-19 41H47a7 7 0 100 15h13l-16 15a8 8 0 006 13c2 0 3 0 5-2l14-14v11a7 7 0 1015 0v-26l42-20 15 43-18 18a7 7 0 005 13l5-2 8-8v21a7 7 0 1015 0v-22l10 9a8 8 0 0010 0c3-3 3-8 0-11l-18-18 15-43 41 20v26a7 7 0 1015 0v-12l15 15a8 8 0 0010 0c3-3 3-8 0-11l-15-15h14a7 7 0 100-15h-28l-19-41 43-16h1l18 19a7 7 0 0011 0c2-3 2-8 0-11l-8-7h20a7 7 0 100-15zm-87 61l-40-19-14 42-15-42-19 9-21 10 19-40-42-14 42-15-19-40 40 19 14-43 15 43 40-19-19 40 42 15-42 14 19 40z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 297 297"><path d="M295 140a17 17 0 00-30 2h-16l-39-20 14-8 9 7a7 7 0 0010-1c3-3 2-8-1-11l-4-3 11-6 6 2a8 8 0 008-4l9-13a8 8 0 00-7-11h-15c-4 1-6 3-7 6l-2 7-11 6v-4a7 7 0 10-15 2l1 10-14 8 3-43 9-15a16 16 0 009-3 17 17 0 10-22-5l-9 16-35 23V67l10-4c4-2 6-6 4-10-1-4-6-6-10-4l-4 2V38l4-5c3-2 3-6 2-9l-7-13a7 7 0 00-14 0l-7 13c-1 3-1 7 1 9l6 6v11l-5-1c-3-2-8 0-9 4-2 4 0 8 4 10l10 4v16l-37-24-9-15a17 17 0 00-15-26 17 17 0 102 34l10 15 2 42-13-8 2-11a7 7 0 10-15-2l-1 5-11-6-2-7c0-3-3-5-6-5l-16-1a8 8 0 00-7 11l9 14a8 8 0 008 3l7-2 10 6-3 3a7 7 0 109 11l8-6 14 8-40 20H32a17 17 0 00-15-10 17 17 0 00-16 21 17 17 0 0016 12c6 0 11-3 14-8h20l37 18-13 8-9-7a7 7 0 10-10 12l5 3-11 6-7-1c-3-1-6 0-8 3l-9 13a8 8 0 007 11h16c3 0 6-3 6-6l2-7 11-6v4a8 8 0 1015-2l-1-10 13-8-2 43-9 16a17 17 0 1014 7l8-16 36-24v16l-11 5c-4 1-6 5-4 9 1 3 4 5 7 5l2-1 6-2v13l-5 5c-3 2-3 6-2 9l7 14c2 2 4 4 7 4s6-2 7-4l7-14c2-3 1-7-1-9l-5-5v-12l3 1c4 2 9 0 10-4 2-3 0-8-4-9l-9-4v-16l35 24 12 17c-2 2-3 5-3 9 0 9 8 17 17 17a17 17 0 10-2-34l-11-17-3-43 14 8-1 11a7 7 0 1014 2l1-5 11 6 2 7c1 3 3 5 7 6h16a7 7 0 006-11l-8-13c-2-3-6-4-9-4l-7 2-10-6 3-2a7 7 0 10-9-12l-8 6-14-8 37-18h19c3 5 8 8 14 8a17 17 0 0015-25zM189 79l-2 39-30 17v-35l32-21zm-81 0l34 22v35l-31-18-3-39zm-40 70l36-18 30 18-30 17-36-17zm40 69l3-39 31-18v35l-34 22zm81 1l-32-22v-35l30 17 2 40zm5-53l-30-17 30-18 36 18-36 17z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 298 298"><path d="M268 148l27-27a8 8 0 00-10-11l-30 30h-44l16-16a7 7 0 00-5-13l-25-1 31-31h42a7 7 0 100-14h-38V27a7 7 0 10-15 0v42l-30 30-1-24c0-3-1-5-4-7-3-1-6 0-8 2l-18 16V42l30-29c3-3 3-8 0-11s-8-3-11 0l-27 27-27-27c-2-3-7-3-10 0s-3 8 0 11l30 30v44l-18-17c-2-2-5-3-8-2-2 2-4 4-4 7l-1 24-32-32V25a7 7 0 10-15 0v39H25a7 7 0 100 15h44l31 31-25 1a8 8 0 00-5 12l17 18H43l-30-30a8 8 0 00-11 10l27 27-27 28a8 8 0 1011 10l29-30h44l-16 17a8 8 0 005 13l23 1-32 31H25a7 7 0 100 15h38v38a7 7 0 1015 0v-43l32-32 1 26c0 3 1 5 4 7 3 1 6 0 8-2l17-16v44l-30 30c-3 3-3 8 0 10a8 8 0 0011 0l27-27 27 27a7 7 0 0010 0c3-2 3-7 0-10l-30-30v-45l18 17a8 8 0 009 2c2-2 4-4 4-7l1-24 30 31v42a7 7 0 1015 0v-38h39a7 7 0 100-15h-43l-31-31 25-1a8 8 0 005-12l-17-19h45l30 30a7 7 0 0010 0c3-2 3-7 0-10l-27-27zm-112-41l16-14v21l-14 14-2-1v-20zm-31-14l16 15v19l-2 1-14-14V93zm-10 32l13 13-1 3h-19l-15-16h22zm-2 47H93l14-16h20l1 1-15 15zm27 18l-15 14v-23l13-13 2 2v20zm31 14l-16-15v-19l2-1 15 15-1 20zm11-32l-14-13 2-4h19l15 17h-22zm8-32h-20l-1-1 14-14h21l-14 15z"/></svg>`], {type: 'image/svg+xml'})),
  URL.createObjectURL(new Blob([`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 296 296"><path d="M250 185a7 7 0 00-12 1l-2 4-13-8 19-12a7 7 0 10-8-13l-25 16-19-25 19-26 25 17a7 7 0 0010-2c3-3 2-8-2-10l-19-14 13-8 2 5a7 7 0 0012 1l26-29a7 7 0 00-8-12l-37 8a7 7 0 00-5 11l2 4-13 7-1-23a8 8 0 00-15 1l2 29-32 4-13-29 27-14c3-2 5-6 3-10s-7-5-11-3l-20 11V51h5a7 7 0 006-10L155 5c-1-3-4-5-7-5-4 0-7 2-8 5l-12 36a7 7 0 007 10h5v15l-22-10c-3-2-8-1-10 3-1 4 0 8 4 10l26 13-12 29-31-4 1-29a7 7 0 10-15-1l-1 23-12-7 2-4a8 8 0 00-5-11l-37-8a7 7 0 00-8 13l26 28a7 7 0 0012-1l2-4 13 7-20 14a8 8 0 009 12l24-16 18 25-18 25-25-16a8 8 0 00-8 13l20 12-13 8-2-4a7 7 0 00-12-1l-25 28a8 8 0 007 12l37-7a8 8 0 005-11l-2-5 13-7 1 24a7 7 0 1015-1l-2-29 31-4 12 29-26 13c-4 1-5 6-3 10 1 3 6 5 9 3l22-10v15h-5a7 7 0 00-7 10l12 36c1 3 4 5 8 5 3 0 6-2 7-5l12-36a8 8 0 00-7-10h-5v-15l20 11 4 1c3 0 5-2 7-4 2-4 0-9-3-11l-27-13 13-29 31 4-2 29a8 8 0 0015 1l2-24 13 8-2 4a7 7 0 005 11l37 8a7 7 0 008-4c2-3 2-6-1-9l-25-28z"/></svg>`], {type: 'image/svg+xml'})),
], (url, i) => {
  const image = document.createElement('img');
  image.src = url;

  return new Promise((resolve) => image.addEventListener('load', resolve, {once: true})).then(() => masks[i] = image);
});
Enter fullscreen mode Exit fullscreen mode

We'll test that the masks are working by picking one at random and reloading the page several times.

async function main() {
  const canvas = document.querySelector('canvas');
  const canvasContext = canvas.getContext('2d');
  const canvasAspectRatio = canvas.width / canvas.height;

  const video = document.createElement('video');
  video.muted = true;
  video.autoplay = true;
  video.loop = true;
  video.playsinline = true;
  video.src = "https://giant.gfycat.com/CheapBitterCaterpillar.webm";

  await Promise.all([...masks, video.play()]);

  const videoAspectRatio = video.videoWidth / video.videoHeight;

  const [frameWidth, frameHeight] =
        canvasAspectRatio <= videoAspectRatio
  ? [canvas.height * videoAspectRatio, canvas.height]
  : [canvas.width, canvas.width / videoAspectRatio];

  const [frameTop, frameLeft] = [(canvas.height - frameHeight) / 2, (canvas.width - frameWidth) / 2];

  const maskImage = masks[Math.floor(Math.random() * masks.length)];

  const maskAspectRatio = maskImage.naturalWidth / maskImage.naturalHeight;

  const [maskWidth, maskHeight] =
        canvasAspectRatio <= maskAspectRatio
  ? [canvas.width, canvas.width / maskAspectRatio]
  : [canvas.height * maskAspectRatio, canvas.height];

  const [maskTop, maskLeft] = [(canvas.height - maskHeight) / 2, (canvas.width - maskWidth) / 2];

  requestAnimationFrame(function frame() {
    canvasContext.globalCompositeOperation = 'destination-over';
    canvasContext.drawImage(maskImage, maskLeft, maskTop, maskWidth, maskHeight);

    canvasContext.globalCompositeOperation = 'source-in';
    canvasContext.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, frameLeft, frameTop, frameWidth, frameHeight);

    requestAnimationFrame(frame);
  });
}
Enter fullscreen mode Exit fullscreen mode

Part 3 Pen


Part 4: Use multiple random masks on the same image

Next, instead of a single random snowflake, let's generate lots of flakes of varying size and draw them randomly.

First, let's define a class Flake to create a random snowflake. Its constructor will take the <canvas> it is drawn on as its only argument. We'll also define two static class variables, minProportion and maxProportion to determine the min and max size of the snowflake, expressed as a fraction of the minimum dimension (width or height) of the canvas, with some sane defaults.

We'll use a truncated exponential distribution with lambda = 1 to randomize the size of the snowflake, so that small flakes are more likely than big ones. We'll crop the distribution at the 99th percentile and normalize it.

We'll also give the class a draw method to draw the flake on the canvas.

class Flake {
  static minProportion = 0.01;
  static maxProportion = 0.1;

  constructor(canvas) {
    this.context = canvas.getContext('2d');

    this.image = masks[Math.floor(Math.random() * masks.length)];

    // https://www.wolframalpha.com/input/?i=exponential+distribution
    const lambda = 2, pct = 4.60517 / lambda, r = -1 * Math.log(Math.random()) * 1 / lambda, p = Math.min(pct, r) / pct;

    const size = Flake.minProportion + (p * (Flake.maxProportion - Flake.minProportion));

    this.width = Math.max(canvas.width, canvas.height) * size;
    this.height = this.width * this.image.naturalWidth / this.image.naturalHeight;
    this.left = Math.floor(Math.random() * canvas.width) - this.width / 2;
    this.top = Math.floor(Math.random() * canvas.height) - this.height / 2;
  }

  draw() {
    this.context.drawImage(this.image, this.left, this.top, this.width, this.height);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, instead of a single mask, we'll create an array of Flakes.

  const flakes = Array.from({length: 300}, () => new Flake(canvas));
Enter fullscreen mode Exit fullscreen mode

Next, within the frame function, loop through flakes and call the draw method.

  requestAnimationFrame(function frame() {
    canvasContext.globalCompositeOperation = 'destination-over';
    flakes.forEach((flake) => flake.draw());

    // ...
  });
Enter fullscreen mode Exit fullscreen mode

Part 4 Pen


Part 5: Animating masks

Let's animate the snowflakes.

Since we'll be doing things over time, we'll need a way to measure time. We'll use the performance.now() API to calculate the elapsed time between each frame.

let t0 = performance.now();

requestAnimationFrame(function frame() {
  const t1 = performance.now(), elapsed = t1 - t0;
  t0 = t1;

  // ...
});
Enter fullscreen mode Exit fullscreen mode

For simplicity, let's also define a "period" for the amount of time a snowflake falling vertically takes to travel the full length of the canvas. We'll define it as a static variable on the Flake class.

class Flake {
  static period = 1000; // 1 second period, in ms to match performance.now() units

  // ...
}
Enter fullscreen mode Exit fullscreen mode

In the Flake constructor, let's define a velocity vector using the period and the height of the canvas. For initial development, the vector will have only a vertical y component and no horizontal x. To emulate the perception of depth, smaller snowflakes, since they are perceived to be further away, will have their speed scaled in proportion to their size, with a reasonable minimum.

constructor(canvas) {
  // ...

  const speed = Math.max(0.1, p) * canvas.height / period; // (px / ms)
  this.velocity = [0, speed];
}
Enter fullscreen mode Exit fullscreen mode

Next, let's define a method to update the position of a Flake, passing it the amount of time that has elapsed. Call it tick. When a snowflake moves off of either edge of the canvas, we'll just wrap it around.

tick(t = 16.667) {
  const [x, y] = this.velocity;
  let left = this.left + x * t, top = this.top + y * t;

  if (top >= this.canvas.height) {
    top = -this.height;
  }

  if (left >= this.canvas.width) {
    left = -this.width;
  }

  this.left = left;
  this.top = top;
}
Enter fullscreen mode Exit fullscreen mode

To make everything move, we'll update the frame function to call the tick method on each Flake. Now that the masks are moving, we also need to to clear the canvas on each frame.

requestAnimationFrame(function frame() {
  const t1 = performance.now(), elapsed = t1 - t0;
  t0 = t1;

  canvasContext.clearRect(0, 0, canvas.width, canvas.height);

  canvasContext.globalCompositeOperation = 'source-over';
  canvasContext.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, frameLeft, frameTop, frameWidth, frameHeight);

  canvasContext.globalCompositeOperation = 'destination-in';

  flakes.forEach((flake) => {
    flake.tick(elapsed);
    flake.draw();
  });

  requestAnimationFrame(frame);
});
Enter fullscreen mode Exit fullscreen mode

The rest of the code is just aesthetic tweaks.

Let's move a random percentage of each flake's velocity, from 0% - 33%, into the horizontal component.

constructor(canvas) {
  // ...
  const speed = Math.max(0.1, p) * canvas.height / period;
  const xFrac = 0.15 * Math.random();
  this.velocity = [speed * xFrac, speed * (1 - xFrac)];
}
Enter fullscreen mode Exit fullscreen mode

Finally, let's add some more randomness into the scene. When a snowflake runs off of the canvas, let's randomize it, except that it will always be positioned just above the top of the canvas (because snowflakes appearing in the middle of the canvas would be weird). To do that, we'll move most of the code out of the constructor into a method that can be called independently.

  constructor(canvas) {
    this.canvas = canvas;
    this.context = canvas.getContext('2d');
    this.randomize();
  }

  randomize() {
    this.image = masks[Math.floor(Math.random() * masks.length)];

    // exponential distribution (lambda = 2, percentile = 99%)
    const lambda = 2, pct = 4.60517 / lambda, r = -1 * Math.log(Math.random()) * 1 / lambda, p = Math.min(pct, r) / pct;

    const size = Flake.minProportion + (p * (Flake.maxProportion - Flake.minProportion));

    this.width = Math.min(this.canvas.width, this.canvas.height) * size;
    this.height = this.width * this.image.naturalWidth / this.image.naturalHeight;
    this.left = Math.floor(Math.random() * this.canvas.width) - this.width / 2;
    this.top = -this.height;

    const speed = Math.max(0.1, p) * this.canvas.height / Flake.period;

    const xFrac = 0.333 * Math.random();
    this.velocity = [xFrac * speed, (1 - xFrac) * speed];
  }

  tick(t = 16.667) {
    // ...

    if (top >= this.canvas.height || left >= this.canvas.width) {
      return this.randomize();
    }

    this.left = left;
    this.top = top;      
  }
Enter fullscreen mode Exit fullscreen mode

And the very last thing: let's make the canvas cover the entire viewport.

<canvas></canvas>
Enter fullscreen mode Exit fullscreen mode
/* ... */

body {
  height: 100%;
}

canvas {
  display: block;
  width: 100%;
  height: 100%;
}
Enter fullscreen mode Exit fullscreen mode
async function main() {
  const canvas = document.querySelector('canvas');
  const canvasContext = canvas.getContext('2d');

  let canvasAspectRatio;

  const resize = () => {
    const { width, height } = canvas.getBoundingClientRect();  
    canvas.width = width;
    canvas.height = height;
    canvasAspectRatio = width / height;
  };

  resize();

  self.addEventListener('resize', resize);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

And we're done!

Part 5 Pen


Conclusion

This is just a proof-of-concept. There are many other possibilities with this technique. The masks could themselves be programmatically animated. Other compositing modes and/or filters can be combined and used to remarkable effect. For example, snowflakes closer to the "camera" could have an increasing amount of blur applied to them using a filter.

Discussion (0)

pic
Editor guide