7 Days of Code : July ’21 : 07. Divide

7 days of code, experiments, minicomps

https://7daysofcode.art/

I spent WAAAAAAAY too long on this last weekend. I was possessed. It was fun. The code is hideous. Microcomps? Too bad they don’t do anything. Would be fun to build this out a bit. Clean up the code and modularize things, get them animated and maybe some bit of interactivity. But I doubt I’ll be doing that any time soon.

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.

New HTML Canvas Stuff: Filters

tutorial

For the past couple of years or so, I’ve focused mainly on creating images and animations with Go and my own Go library, blgo, that uses CairoGraphics bindings (my own fork of go-cairo).

When I started creating MiniComps, it was an opportunity to get back into JavaScript-based content creation using HTML’s Canvas and its 2d rendering context. The canvas drawing API is pretty much the same as Cairo, but it’s been nice getting back into interactive, web-based pieces – not so easy in Go/Cairo. I made an updated version of my bitlib library for 2021, bljs, and if you’ve been on this blog or follow me on Twitter, you’ve seen some of the stuff I’ve been doing with it.

But recently I started looking at the documentation for Canvas and the 2d rendering context and found some pretty interesting things that weren’t there last time I looked.

Filters!

This is super powerful. Filters are a cool new way to change the look of your canvas drawings. The context now has a filter property, which is a string and allows you to add one or more defined filters to it. For example, to add a blur:

context.filter = "blur(8px)";

To add multiple filters, just add them to the string:

context.filter = "blur(8px) contrast(50%) sepia(20%)";

Here’s a demo that allows you to play with a handful of the filters available:

Source

Here are the full list of filters available:

  • blur
  • brightness
  • contrast
  • drop-shadow
  • grayscale
  • hue-rotate
  • invert
  • opacity
  • saturate
  • sepia

If you look through these, you’ll realize these are basically the same as already existing CSS filters. In fact, you could already do some of these same affects by adding a filter style to the canvas element itself. For example:

canvas.style = "filter: invert(100%)";

Here’s another demo using this method:

Source

So what’s the difference?

Actually, a pretty big difference. When you apply a filter to a 2d rendering context, it does NOT change the existing content. It will only affect anything drawn after the point when the filter was applied. If you take a look at the code in the first example, I need to clear the canvas and redraw all the content in order to have the filter take place. Whereas applying the filter to the canvas affects the whole element in real time. No need to redraw the content. This also means that using the CSS filter on the canvas element is non-destructive. Using a blur, for example, on the context means the subsequently drawn pixels are permanently blurred.

Also, because the CSS filters apply to the whole element, the blur filter will blur the element beyond its boundaries, whereas the context filter only applies to the pixels inside the canvas. (You can probably change that for the CSS version by applying other styles, such as overflow.)

So far it seems like the CSS version has a lot of the advantages. But the context version does have some plus points. If you want to export the blurred or otherwise filtered image and save it somewhere, you’ll need to have the filters applied to the image in the context, not just the canvas element.

Also, applying filters to the context means you can selectively turn them off and on or change their parameters for each shape you draw. Here’s an example:

Source

Here, the red square is drawn with no filter, the black one with a 10px blur, and the blue with a 0px blur. You couldn’t do that directly with a CSS filter.

Experimental

It’s worth noting that these are experimental features. They may not work in all browsers. Definitely WON’T work as of this writing in Safari or IE, and may change in the future.

SVG Filters

One of the more powerful aspects of being able to use filters is that you are not limited to the fairly basic assortment of built-in filters. You can create extremely complex and powerful filters using SVG and then apply those directly to your canvas context. I’ll discuss how to do that in another article.

Resources

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/filter

https://developer.mozilla.org/en-US/docs/Web/CSS/filter