JT's Scribblings

a.k.a. a blog of some description.

Shiny stuff with SVG, part 4: Primitives II

Sunday November 11th 2012, at 5:08 pm

Firstly, apologies to anyone who read the first four posts in this series expecting (as I did) that they would continue on a daily basis. This one is somewhat delayed as I've been kept busy with many other things. Also I had to figure out how to get equations onto my blog automatically without breaking everything.

Anyway, this post will cover a few more of the basic filter primitives, and then move onto how to combine everything from this and the last four posts to create some effects.

I apologise in advance for the fact that the description of feColorMatrix has a load of equations in it. It's not strictly necessary to know the exact technicality of how the various matrices are defined, but I've nonetheless included them for information's sake.

feColorMatrix

The feColorMatrix primitive modifies the input image on a pixel-by-pixel basis according to a matrix transformation, which may seem a little daunting if you've never done anything vector-algebra related:

What this means, if you've never come across matrix multiplication before, is this:

The values for each pixel are measured between 0 and 1 for each of R, G, B and A (instead of 0 to 255 as is often used for colours).

There are few options available for either specifying the matrix entirely manually, component-by-component, or applying some simpler parameterized transforms. These are specified using the type attribute, and the various parameters are specified with the values parameter. The avaialable values for type are: matrix, saturate, hueRotate and luminanceToAlpha

matrix

When using type="matrix" (the default if type isn't specified), the values attribute should just be a space-delimited list of the components of the matrix in left-to-right, top-to-botom order (a00, a01, a02, … a34). The transformation is then performed as simple matrix multiplication.

So, for example, to apply a very simple 240-degree hue rotation to the (rather disturbing) smiley face from the last post by mapping RGB to BRG:

<svg>
  <defs>
    <filter id="mat">
      <feImage xlink:href="smiley.png"
               x="0" y="0" width="100" height="100" />
      <feColorMatrix values="0 1 0 0 0
                             0 0 1 0 0
                             1 0 0 0 0
                             0 0 0 1 0" />
    </filter>
  </defs>
  <rect x="0" y="0" width="100" height="100" filter="url(#mat)" />
</svg>

If you compare this to the equations above, you'll see that what the above example is doing effectively reduces down to this:

Note that this is not the same result as you'd get from doing a 240-degree hueRotate. More on that in a minute.

Or, for example, we could invert all the colours:

<svg>
  <defs>
    <filter id="inv">
      <feImage xlink:href="smiley.png"
               x="0" y="0" width="100" height="100" />
      <feColorMatrix values="-1  0  0 0 1
                              0 -1  0 0 1
                              0  0 -1 0 1
                              0  0  0 1 0" />
    </filter>
  </defs>
  <rect x="0" y="0" width="100" height="100" filter="url(#inv)" />
</svg>

And this boils down to the following operation:

There are many more interesting colour transformations you can do with an explicitly-specified feColorMatrix; I have only covered the most simple possible examples here.

saturate

With type="saturate", the values attribute should be a single real number between 0 and 1. If we call this number s, the matrix operation performed is the following:

This transformation will completely desaturate the image if values is 0, and leave it untouched if values is 1. Although the SVG specification says values should lie between 0 and 1, most browsers allow you to set a value greater than 1 to over-saturate the input.

The significance of the various numbers in this matrix is to conform to the luminance calculation from the sRGB colour standard (see equation 1.8 near the end if you're interested), so the input data can be desaturated while preserving its luminance.

For example, to desaturate the smiley by 75%:

<svg>
  <defs>
    <filter id="sat">
      <feImage xlink:href="smiley.png"
               x="0" y="0" width="100" height="100" />
      <feColorMatrix type="saturate" values="0.25" />
    </filter>
  </defs>
  <rect x="0" y="0" width="100" height="100" filter="url(#sat)" />
</svg>

hueRotate

The hueRotate operation is a little more complicated to represent exactly. The idea of it is to take the values attribute, which should be a single numeric value, and rotate the hue of every pixel by that number of degrees. However, the transformation it applies is a luminance-preserving transformation according to sRGB luminance, so (for example) the colour #123456 will not be transformed to #561234 by a 120-degree rotation.

For example, to apply a 100-degree hue rotation to the smiley:


<svg>
  <defs>
    <filter id="hr">
      <feImage xlink:href="smiley.png"
               x="0" y="0" width="100" height="100" />
      <feColorMatrix type="hueRotate" values="100" />
    </filter>
  </defs>
  <rect x="0" y="0" width="100" height="100" filter="url(#hr)" />
</svg>

The exact definition is as follows:

Where, if θ is the numeric value of values,

luminanceToAlpha

The luminanceToAlpha type takes the luminance of the red, green and blue channels and translates that to the value of the alpha channel (and puts 0 in all the RGB channels). This is particularly useful if you have one image that you want to use as an alpha mask for another (using the feComposite primitive with operator="in"). When used with this type, feColorMatrix pays no attention to the values attribute.

So, for example:


<svg>
  <defs>
    <filter id="ltoa">
      <feImage xlink:href="smiley.png"
               x="0" y="0" width="100" height="100" />
      <feColorMatrix type="luminanceToAlpha" />
    </filter>
  </defs>
  <rect x="0" y="0" width="100" height="100" filter="url(#ltoa)" />
</svg>

You can see that the transparent pixels of the smiley (which have R, G, B and A values all 0) become transparent, the bright yellow area is mostly-opaque, and the black area is completely transparent.

One thing that is worth noting is that the alpha channel of the original image is completely ignored, so if you have transparent pixels in your image you will get different results depending on the RGB values of those pixels (i.e. RGBA(0, 0, 0, 0) and RGBA(255, 255, 255, 0) look the same, but the first will be converted to 100% transparent and the second will be converted to 100% opaque).

The exact definition of the matrix operation used is this:

feGaussianBlur

The feGaussianBlur does exactly what you'd expect it to if you've ever come across Gaussian blurs in image editors or the like. It applies a Gaussian blur (if you haven't come across Gaussian blurs before, this is just a particularly-attractive looking blur calculation) with a standard deviation (blur amount) specified by the stdDeviation attribute, measured in pixels. For example:

<svg>
  <defs>
    <filter id="blur" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur stdDeviation="10" />
    </filter>
  </defs>
  <circle cx="50" cy="50" r="30" fill="#28d" filter="url(#blur)" />
</svg>

It's worth noting that as a Gaussian blur extends beyond the boundaries of its input, you will most likely need to use the x, y, width and height attributes of the filter to extend its clipping rectangle.

The stdDeviation attribute can alternatively contain two components, in which case these are the deviations in the X and Y directions respectively. Using this you can achieve some interesting effects by only blurring in one direction:

<svg>
  <defs>
    <filter id="hblur" x="-50%" y="-50%" width="200%" height="200%">
      <feGaussianBlur stdDeviation="10 0" />
    </filter>
  </defs>
  <rect x="25" y="25" width="50" height="50"
        fill="#28d" filter="url(#hblur)" />
</svg>

feMorphology

The feMorphology primitive has the effect of either 'thinning' or 'thickening' the input. You can specify to either dilate or erode the input using the operator attribute, and supply an amount using the radius attribute.

For example, to dilate (expand) by 2 pixels:

Dilate
<svg>
  <defs>
    <filter id="morph1">
      <feMorphology operator="dilate" radius="2" />
    </filter>
  </defs>
  <text x="10" y="50" filter="url(#morph1)"
        font-size="30">Dilate</text>
</svg>

Or, to erode (contract) by 1 pixel:

Erode
<svg>
  <defs>
    <filter id="morph2">
      <feMorphology operator="erode" radius="1" />
    </filter>
  </defs>
  <text x="10" y="50" filter="url(#morph2)" font-size="30">Erode</text>
</svg>

It's worth noting that the shape that feMorphology uses to calculate the resulting image data is a rectangle rather than an ellipse, so for example if you apply a dilation to a circle the result will not be a larger circle, but will in fact be a square with rounded corners, thus:

<svg>
  <defs>
    <filter id="morph3" x="-50%" y="-50%" width="200%" height="200%">
      <feMorphology operator="dilate" radius="20" />
    </filter>
  </defs>
  <circle cx="50" cy="50" r="20" fill="#38d" filter="url(#morph3)" />
</svg>

I may well write a further post with a little more about precisely how feMorphology calculates the output data. If I do, I'll update this post to include it.

Putting it all together

Now that we've covered enough of the basic filter primitives, it's now possible to combine several of these to create a useful effect.

Box Shadow

The first example is a box shadow. This example will be a bit exaggerated to demonstrate the method, but it's easily adapted to whatever parameters you need. The basic steps are thus:

  1. Offset the input object (using feOffset)
  • Create a fill of the desired colour and opacity (using feFlood)
  • Clip the fill to the offset object (using feComposite with operator="in")
  • Apply a blur to the shadow (using feGaussianBlur)
  • Compose the shadow behind the original image (using feMerge)

So here we go:

<svg>
  <defs>
    <filter id="shadow" x="-50%" y="-50%" width="200%" height="300%">
      <feOffset dx="10" dy="10" in="SourceAlpha" result="offset" />
      <feFlood flood-color="#000" flood-opacity="0.5" />
      <feComposite in="offset" operator="in" />
      <feGaussianBlur stdDeviation="5" result="blur" />
      <feMerge>
        <feMergeNode in="blur" />
        <feMergeNode in="SourceGraphic" />
      </feMerge>
    </filter>
  </defs>
  <rect x="10" y="10" width="60" height="40" fill="#ace"
        rx="4" ry="4" stroke="#58a"
        stroke-width="2" filter="url(#shadow)" />
</svg>

Inner Shadow

Creating an inner shadow is a little more complicated if the shape has a border, as you'll want the inner shadow to appear only inside that border. To achieve this you can use a feMorphology to erode the shape to just the area within the border bounds. This has its drawbacks though — if you have a large stroke width, the curvature of any part of the shape will be distorted slightly (due to the rectangular erosion kernel). For smallish borders though, this won't be noticeable.

One method for creating an inner shadow is thus:

  1. Create an eroded version of the input, to represent only the area inside the srtoke (using feMorphology)
  • Create an offset version of it (using feOffset)
  • Blur the offset version (using feGaussianBlur)
  • Compose the blurred offset image with the original eroded one using feComposite and operator="out" to get only those pixels outside the blurred version. This will be used as an alpha mask for the shadow
  • Create a flood of the desired colour and opacity (using feFlood)
  • Compose the flood with the mask (another feComposite)
  • Blend the shadow with the original (feBlend with mode="multiply")
<svg>
  <defs>
    <filter id="shadow2">
      <feMorphology operator="erode" radius="2"
                    result="eroded" in="SourceAlpha"/>
      <feOffset dx="0" dy="5" in="eroded" />
      <feGaussianBlur stdDeviation="3" />
      <feComposite operator="out" in="eroded" result="mask" />
      <feFlood flood-color="#222" flood-opacity="0.7"/>
      <feComposite operator="in" in2="mask" />
      <feBlend mode="multiply" in2="SourceGraphic" />
    </filter>
  </defs>
  <rect x="10" y="10" width="60" height="40" fill="#ace"
        rx="4" ry="4" stroke="#49f"
        stroke-width="2" filter="url(#shadow2)" />
</svg>

As you will no doubt have figured out by now, there are many ways of doing the same thing with filters. The method described above is just my particular way of creating an inner shadow in the simplest way I can think of, while demonstrating as many of the primitives covered so far as possible. An equivalent effect could be achieved, for example, by substituting the feFlood and second feComposite with <feColorMatrix values="0 0 0 0 0.125 0 0 0 0 0.125 0 0 0 0 0.125 0 0 0 0.5 0" />. There may well be an even simpler way than that. Go experiment, and see what you come up with!

Next Time

The next post will be a little shorter than this one. I'll explain feTurbulence and how you can use it to create fractal noise and grain effects, plus a few more examples of combining pretty much everything covered so far to create some actually-useful effects, rather than very basic primitives.

Other Posts in This Series

Useful Links