SVG Filters in Canvas

tutorial

Earlier I talked about the new (and still experimental) filters that you can apply to the HTML Canvas’s 2d rendering context. One of the more powerful aspects of this is the ability to use external SVG filters in this workflow.

External SVG filters are applied using the url filter. It looks like this:

context.filter = "url(pathtofilter)";

It took quite a bit of frustration to figure out how to actually get these to work though. The documentation says that the path should point to an external XML document that contains an SVG filter. Again, all of this was borrowed from the CSS filter that works the same way. So let’s get an example of that working first.

SVG Filters in CSS

For this example, we’ll create a div and give it a style. The style will load in an external SVG document that has a filter defined in it. We’ll use the turbulence filter, which I believe uses some version of Perlin noise.

First the HTML:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <style>
      #filtered {
        filter: url(./filter.svg#turb);
        width: 600px;
        height: 600px;
      }
    </style>
  </head>
  <body>
    <div id="filtered"></div>
  </body>
</html>

The div is down at the bottom with an id of “filtered”. The style block creates the following filter:

filter: url(./filter.svg#turb);

This points to a file named filter.svg and a specific filter within that document with an id of turb. Next, lets look at that SVG document.

<svg
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
  >
  <defs>
    <filter id="turb">
      <feTurbulence baseFrequency="0.01"/>
    </filter>
  </defs>
</svg>

This is a pretty bare bones SVG document. You see the filter there with the id of turb. This creates a turbulence filter with the line:

<feTurbulence baseFrequency="0.01"/>

That’s all. Run it and you get:

Now let’s see if we can do the same thing in a Canvas.

SVG Filters in Canvas

Per the documentation, we should just be able to do the same thing to load in an SVG filter and apply it to a 2d rendering context:

context.filter = "url(./filter.svg#turb)";

So let’s try it out. We’ll create a canvas right in the HTML doc:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <canvas id="canvas" width="600" height="600"></canvas>
    <script src="./main.js"></script>
  </body>
</html>

And in that script, we’ll get a reference to the canvas and context and try to apply the filter.

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

context.filter = "url(./filter.svg#turb)";
context.fillRect(0, 0, 100, 100);

When I run this, I get the black square that I drew, but no turbulence filter.

After a lot of digging in, I discovered that the context filter for external SVG docs does not seem to work in the same way. There is a workaround though – you add the SVG directly to the HTML of the main page. Our HTML now looks like this:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
  <svg
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    >
  <defs>
    <filter id="turb">
      <feTurbulence baseFrequency="0.01"/>
    </filter>
  </defs>
</svg>
    <canvas id="canvas" width="600" height="600"></canvas>
    <script src="./main.js"></script>
  </body>
</html>

Now the script can load the filter directly by its id:

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

context.filter = "url(#turb)";

context.fillRect(0, 0, 100, 100);

And now we have some turbulence in our context!

But notice how it’s shifted over to the right. That’s because the SVG is now on the page. Even though it has no content, it’s taking up space. You can see it using the dev tools to highlight it:

There are probably a number of ways to handle this with CSS. Here’s how I dealt with it:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
  <svg
    style="width: 0; height: 0; position: absolute;"
    version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink"
    >
  <defs>
    <filter id="turb">
      <feTurbulence baseFrequency="0.01"/>
    </filter>
  </defs>
</svg>
    <canvas id="canvas" width="600" height="600"></canvas>
    <script src="./main.js"></script>
  </body>
</html>

This makes the width and height 0 and the absolute position makes sure it doesn’t affect the layout of anything around it. Problem solved.

More Weirdness

Note that I’m drawing a 100×100 black box into the context, but the turbulence filter just fills the entire canvas with its Perliny swirls. It seems that the black box is kind of useless. But if you remove it, you get … a blank canvas. So it seems the filter is applied only when something is drawn. In fact, it doesn’t matter what you draw. A single pixel will activate the filter and give you full canvas turbulence.

But then it gets more weirder. EVERY TIME you draw something, the filter is applied again. Trying this…

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

context.filter = "url(#turb)";

context.fillRect(0, 0, 100, 100);
context.fillRect(0, 0, 100, 100);
context.fillRect(0, 0, 100, 100);
context.fillRect(0, 0, 100, 100);
context.fillRect(0, 0, 100, 100);

Gives you this…

It applied the filter 5 times, making it a lot darker.

So if you want to apply a filter like this and then draw something on top of it, you have to kill the filter. This would seem pretty easy. But the first few things I tried did not work so well.

context.filter = "";
context.filter = null;
context.filter = "null";   // worth a shot!

None of these did anything. The previous filter remained active. Finally I found that this worked:

const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");

context.filter = "url(#turb)";

context.fillRect(0, 0, 100, 100);

context.filter = "url()";
context.fillRect(0, 0, 100, 100);

Applying an actual filter type with no parameters does the trick. This also works:

context.filter = "blur()";

There might be a better way to do this. I stopped when I found something that worked.

Summary

Applying SVG filters to a context is pretty powerful. I’ll be checking out other available filter types. Here’s a list:

  • <feBlend>
  • <feColorMatrix>
  • <feComponentTransfer>
  • <feComposite>
  • <feConvolveMatrix>
  • <feDiffuseLighting>
  • <feDisplacementMap>
  • <feFlood>
  • <feGaussianBlur>
  • <feImage>
  • <feMerge>
  • <feMorphology>
  • <feOffset>
  • <feSpecularLighting>
  • <feTile>
  • <feTurbulence>
  • <feDistantLight>
  • <fePointLight>
  • <feSpotLight>

And you can combine several of these filters into your own custom filter that you apply to a canvas. It gets really powerful. I’ll probably be playing around with this more in the near future and will share my results.

At the same time, the workflow for creating and applying filters to a canvas rendering context is still pretty janky. I think the CSS flow is pretty solid, but as I said in my last post, the canvas feature is still experimental.

Resources

If you want to find out more about creating and combining SVG filters into neat patterns, here are some links:

https://css-tricks.com/creating-patterns-with-svg-filters/

https://www.linkedin.com/pulse/26-images-you-wont-believe-were-created-only-svg-bence-szab%C3%B3/

https://www.smashingmagazine.com/2015/05/why-the-svg-filter-is-awesome/

All of these techniques should work pretty well in native SVG elements as well as CSS, and theoretically in canvas too, with a bit of effort.

Leave a Reply

Your email address will not be published. Required fields are marked *