SVG Animations and Live Reloading

Tags:

End result

Animated SVG

Why

For my video conference displays, I'd like to have a (subtly) animated background. Not too animated as to be distracting, but if you look close enough, you should be able to see that the background changes over time.

SVG animations

Instead of getting into Blender or ShaderToy, I decided to go with simple animated SVG. SVG has <animate> elements that allow me to specify how a single DOM node should animate over time.

In the end the background is just some Bezier curves morphing into another, and some CPU-gobbling filter effects for blur.

My goal is to create the illusion of waves travelling across the image by animating each path into the next one and having all these animations play in a loop. Here, only the second line is animated to animate into the third:

Static SVG

Workflow

I do the basic SVG editing in Inkscape, and luckily it keeps existing <animate> elements alive even if it does not know about them. Working in Inkscape makes it easy to get the layout of lines and trace existing images without having to guess the coordinates.

Inkscape does not display the animations, but that's fine, I add these later in a post-processing step.

<animate> tag

Static SVG

If all lines slowly animate into their next neighbour, this will create the intended illusion of lines flowing across the screen. The <animate> element is a good fit for my animation goal, with a small snag. There seems to be no easy way to tell SVG "animate from shape with id foo to id bar in 5 seconds". I can only describe "animate the shape of the element from this (explicit) path into another (explicit) path":

<path
 d="M 9.0454762,0 C -7.6337807,60.410349 -84.36489,310.51971 137.97241,386.67508..."
 id="line_009"
 ><animate
   attributeName="d"
   dur="20s"
   repeatCount="indefinite"
   keyTimes="0; 1"
   values="M 9.0454762,0 C -7.6337807,60.410349 -84.36489,310.51971 137.97241,386.67508...
           M parameters-of-next-line />
</path>

This animation between two paths is still workable, but I have to copy the path parameters into the animation parameters of each line and its preceding animation element whenever I change the shape of a line.

Perl

To update the paths in the animation elements, I wrote me a short Perl script that reads the XML, finds all lines that I want to animate, sorts them by X offset and adds an animation to them that animates the line into the next line:

my $dom = XML::LibXML->load_xml( location => 'lines.svg' );
my @lines = $dom->findnodes('//svg:*[@id="layer1"]//svg:path[@d]');

# Sort the lines
my %line_offset = map {
    0+$_ => $_->getAttribute( 'd' ) =~ /^\s*M\s*(-?[\d.]+),/i
} @lines;
@lines = sort {
    $line_offset{ 0+$a }
    <=>
    $line_offset{ 0+$b }
} @lines;

# Add animations
for my $i (0..$#lines-1) {
    my $line = $lines[ $i ];
    my $next = $lines[ $i+1 ];
    # Now, remove all animation nodes and other children:
    $line->removeChildNodes();

    my $ani = $dom->createElement('animate');
    $ani->setAttribute( 'attributeName' => 'd' );
    $ani->setAttribute( 'dur' => '20s' );
    $ani->setAttribute( 'repeatCount' => 'indefinite' );
    $ani->setAttribute( 'keyTimes' => '0; 1' );
    $ani->setAttribute( 'values' =>
        sprintf "%s; %s", $line->getAttribute('d'), $next->getAttribute('d') );
    $line->appendChild($ani);
}

open my $output, '>:raw', 'lines-animated.svg';
print $output $dom->toString;

Live reloading

I really prefer interactive tools when doing graphics, so to get away from manually reloading the SVG animation in the browser, I wanted live reloading of the browser tab whenever the file changed. Adding SVG support was a fairly simple addition to App::Mojo::AssetReloader, but work on that is not finished, as the animation resets on each reload. I need to think about how to preserve the current animation time. Likely this is "easily" done by saving the time from .getCurrentTime() and restoring it after reload via .setCurrentTime(). But then I also need to figure out how/when I want to reset the animation time to the start instead of keeping it.

Further ideas: WebMIDI integration / knob integration

Integrating the whole thing with WebMIDI would be fun for an all-in-one thing. Twisting a knob is so much more fun for adjusting animation parameters than manually editing a number via the keypad. I have MIDI and non-MIDI hardware though, so going a custom route is also interesting.