Exporting SVG to MP4

Tags:

Continued from this post, wherein I animate some SVG

After creating the SVG animation I want to export the SVG file as a video file, preferrably mp4 or webm. lenra-io/svg-recorder works, but only with Chrome. I don't know why the img tag with the SVG URL cannot be fetched as animated bitmap with Firefox, maybe this is to prevent canvas / browser fingerprinting.

The result is OKish:

The original SVG as comparison:

Animated SVG

Quality

The default export quality is low, at 1MB for x frames in 20s.

Bumping the video bitrate in the HTML file improves the quality a lot:

videoBitRate: 25000000

Ugly parts

There still are some bits that I don't like. Exporting a 1080p or 4K mp4 does not work on my machine in realtime (Linux, Nvidia RTX 2080, Nvidia drivers version 550). The animation stutters and when I try to export 20 seconds, I get less than that in the exported frames. The alternative here seems to manually set the time offset for each frame and then capture it, instead of relying on the capture being fast enough. On the other hand, trying this on Windows, RTX 4090, with the default driver version yields better results, so I'll simply do the final exports from there.

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.