SVGs - Super Versatile Graphics

posted on 2023-10-13


Images are an important part of modern websites. Scalable Vector Graphics (SVGs) are recipes for computers to create graphics. Unlike other static graphics formats like .jpg’s, SVGs can scale to different sizes because the recipes can be scaled. You can double the ingredients in a recipe, right? There is plenty of documentation on the web covering how they work, like this tutorial. This post explains SVGs used on the website, and as such, my tinkering with them. Let’s look at some of our website components (as of posting).

Mmmmm Hamburger

The simple hamburger. A staple of healthy website navigation menus, though usually more abstractly minimalist. This one comes from here. I’ve modified it animate on click (try it).

Paths

One of the basic ways to draw an image in SVGs is with paths. Given a canvas size, this tells the computer where to put down and move its pen to create a stroke. A system of x-, y-coordinates are used together with codes for types of lines. The command

M 0 0
means “Move to position x=0 y=0”. Recognised commands are:

  • M: Move to
  • L: Line to
  • H: Horizontal line to
  • V: Vertical line to
  • C: Curve to
  • S: Smooth curve to
  • Q: Quadratic Bézier curve to
  • T: smooth quadratic Bézier curve to
  • A: ellipical Arc
  • Z: close path

Most of these are straightforward to understand, though some of the curves (Smooth, Bézier, smooth Bézier) may need further explanation. Bézier curves in particular are interesting, and ubiquitous amongst graphics software. Each command has an equivalent lowercase version which takes relative coordinates, rather than absolute. I.e.

m 3 2
means “move by dx=3 dy=2”. In the hamburger image, this snippet draws the patty:

<path d="M464 256H48a48 48 0 0 0 0 96h416a48 48 0 0 0 0-96z"></path>

Having the elements of an image defined in the HTML like this means you can apply CSS styles and animations to them. In the hamburger icon, there are two identically drawn patties, in group tags <g>, which have different animate to the strokes of the “X” when the menu button is clicked. Neato.

Note that paths are not the only way to draw basic shapes in SVGs, and other commands exist to draw standard shapes,

  • rect: rectangle
  • circle
  • ellipse
  • line
  • polyline: a group of connected straight lines
  • polygon

If it Fits

On the home page, there is this puzzle icon, borrowed shamelessly from here. Try clicking it :).

Animations

You can do more than just draw paths and shapes in SVGs. You can animate different parts of your SVG with the animate, animateMotion and animateTransform tags. It seems like there is some overlap in functionality with animations in SVG HTML code and animations in CSS. Purists would probably argue that all styling and animations should be in the CSS. However, having self-contained SVGs, with image content as well as styling and animations, means that they are more readily portable and reusable.

The rotation animation in the puzzle piece looks like:


            <animateTransform 
                attributeName="transform" 
                attributeType="XML" 
                type="rotate" 
                from="0 256 256" 
                to="360 256 256" 
                dur="10s" 
                repeatCount="indefinite"
            />
            

The transform types for this tag are similar to those available in CSS: translate, scale, rotate, skewX, skewY. See reference. The “from” and “to” fields are formatted as (degrees, centre x, centre y) which defines a starting and ending rotation in degrees around a certain point. The “dur” tag defines the duration between the “from” and “to” states and the “repeatCount” specifies how many times the transformation happens. Nifty.

Let there be Light

What else can SVGs do? Turns out, a lot. Filters have entered the chat. These modify the whole, or parts of the image. Witness my lightbulb, remorselessly thieved from here. The glow is a filter effect with an animation to get the pulsing.

Effect Filters

Let’s look at this filter.


            <filter id="lightbulb-glow-filter" x="-50%" y="-50%" width="200%" height="200%">
                <feGaussianBlur result="blurOut" stdDeviation="20">
                    <animate 
                        attributeName="stdDeviation" 
                        calcMode="paced" 
                        begin="0s" 
                        dur="2s" 
                        values="0;50;0;" 
                        repeatCount="indefinite"
                    />
                </feGaussianBlur>
                <feBlend in="SourceGraphic" in2="blurOut"/>
            </filter>
            

We first define the filter’s size in the opening tag. The “-50%” values for x and y just re-centre to the top left (I think). The first inner tag is “feGaussianBlur” which blurs the filter’s input. The amount of blur is controlled by “stdDeviation”. Within this, we can define an “animate” tag to slide the “stdDeviation” value between values 0 to 50 and back to 0 again to get the pulsing effect. The final inner tag in the filter, “feBlend”, controls how the filter is applied: it blends the “SourceGraphic”, i.e. the input image, with the Gaussian blur effect we labelled as “blurOut”. To apply this filter to a part of the SVG, we simply add

filter=“url(#lightbulb-glow-filter)”
to the path tag which draws the outline of the lightbulb. Et voilà.

There are many more filters, and most can be animated and stacked to create complex effects. This is a very useful tool for experimenting with filters.

To the Moon

We will go to the moon, not because it is easy, but because it is hard. A quote from Abraham Lincoln. This rocket is assembled here and the propulsion was engineered from here. Let’s break it down.

Stacking Filters and Effects

In the defs tag, we define a fill effect, filter effect and masking effect to apply to a simple rectangle positioned at the base of the rocket. This creates our flames.


            <radialGradient id="fade" cx="0.5" cy="0" r="1" color-interpolation="sRGB">
                <stop offset="0.5" stop-color="#fff"/>
                <stop offset="0.7" stop-color="#000"/>
            </radialGradient>
            <mask id="flame-shape" maskContentUnits = "objectBoundingBox">
                <circle cx="0.5" cy="0.28" r="0.28" fill="white"/>
                <polygon points="0.22,0.3, 0.26,1, 0.74,1, 0.78,0.3" fill="url(#fade)"/>
            </mask>
            

Let’s start with the simplest def, the mask. This will define the shape of the flame. We draw some shapes and fill with white and everything outside that shape is hidden. An extra sophistication is the “radialGradient” which is applied to a polygon and used to get the transparent fall-off at the bottom of the flames.

This is what the flame mask looks like.

Next let’s look at the flame filter effect. There are a lot of layers here.
The idea here is a kind of treadmill effect, where the surface has some squashed noise. If you stare at it for a bit, you’ll notice the pattern repeats. Here it is in gory detail.


            <filter id="flames" filterUnits="objectBoundingBox" x="0%" y="-100%" width="100%" height="300%">
                <feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="1" result="noise" stitchTiles="stitch"/>
                <feOffset dy="0" result="off1">
                    <animate attributeType="XML" attributeName="dy" from="-300" to="0" dur="3s" repeatCount="indefinite" /> 
                </feOffset>
                <feOffset in="noise" dy="60" result="off2">
                    <animate attributeType="XML" attributeName="dy" from="0" to="300" dur="3s" repeatCount="indefinite" /> 
                </feOffset>
                <feMerge result="scrolling-noise">
                    <feMergeNode in="off1"/>
                    <feMergeNode in="off2"/>
                </feMerge>
                <feComponentTransfer result="brighter-noise">
                    <feFuncA type="gamma" amplitude="1" exponent="1"/>
                </feComponentTransfer>
                <feComposite in="SourceGraphic" in2="brighter-noise" operator="in" result="gradient-noise"/>
                <feComponentTransfer result="threshhold">
                    <feFuncA type="discrete" tableValues="0 1"/>
                </feComponentTransfer>
                <feFlood flood-color="var(--accent)" result="flood1"/>
                <feComposite in2="threshhold" in="flood1" operator="in" result="flood1-threshhold"/>
                <feFlood flood-color="var(--primary)" result="flood2"/>
                <feComponentTransfer in="SourceGraphic" result="exponent-gradient">
                    <feFuncA type="gamma" exponent="3"/>
                </feComponentTransfer>
                <feComposite in="flood2" in2="exponent-gradient" operator="in" result="flood2-gradient"/>
                <feComposite in2="threshhold" in="flood2-gradient" operator="in" result="flood2-gradient-threshhold"/>
                <feMerge>
                    <feMergeNode in="flood1-threshhold"/>
                    <feMergeNode in="flood2-gradient-threshhold"/>
                </feMerge>
            </filter>
            

Step by step:

  1. feTurbulence: This defines the noise pattern.
  2. feOffset: Two copies of this noise pattern are created, labelled “off1” and “off2”, which are shifted and animated to move downwards. This creates the treadmill effect, and any jerkiness is caused by these copies not lining up with each other.
  3. feMerge: Joins the two noise pattern copies.
  4. feComponentTransfer and feComposite: Define and apply a gamma function to the noise to create some “hot spots”.
  5. feComponentTransfer: Flatten the noise map by binning the alpha channel values to 0 and 1. This creates the holes in the flames.
  6. feFlood and feComposite: Fills in the base colour of the flames.
  7. feFlood, feComponentTransfer, feComposites: Make another copy of the flames from step 5 and add a gradient fill.
  8. feMerge: Merge the solid base colour flame with the gradient filled copy.

Here’s the flame with the mask and filter. If the flame is one colour, try switching the colour theme (lightbulb at the top of the page).

Boom, lift off. SVGs are neat.