Turn an animated svg into a video file

lenra_io

Alienor

Posted on May 31, 2022

Turn an animated svg into a video file

Check out the video here to see the how-to with Thomas !

Local SVG recorder

Record an animated SVG into a video file without uploading anything.

Let's use the modern browser JavaScript APIs to copy the SVG frames to a canvas and record the result into a WebM video file.

This will be done in a static website, without server side.

There is no real SVG upload nor video download.

schema ? SVG preview -> canvas -> MediaRecorder

Preview the SVG

Let's start with a form to let the users select the SVG and manage the record configurations.

In this article we will not manage record configuration to make it easier.

We also won't set CSS rules since it's not what you are looking for here.

Here is the basic HTML page:


<!DOCTYPE html>

<html lang="en">

    <head>

        <meta charset='utf-8'>

        <meta http-equiv='X-UA-Compatible' content='IE=edge'>

        <title>SVG Recorder</title>

        <meta name="description" content="Record an animated SVG and export it as WebM video">

        <meta name='viewport' content='width=device-width, initial-scale=1'>

    </head>

    <body>

        <h1>SVG Recorder</h1>

        <form>

            <label for="svg"><input id="svg" name="svg" type="file" accept="image/svg+xml" placeholder="Upload the SVG" required/></label>

            <button>Record</button>

        </form>

        <script src='main.js' defer></script>

    </body>

</html>

Enter fullscreen mode Exit fullscreen mode

You can see the input of type file to select the SVG and the Record button to start recording.

Here is the basic main.js JavaScript file:


"use strict";

const form = document.querySelector('form');

form.onsubmit = function (event) {

    /**

     * @type {File}

     */

    const file = form.svg.files[0];

    // start recording

    startRecord(URL.createObjectURL(file))

        .then(blob => saveResult(file.name.replace(/\.svgz?$/i, '.webm'), blob));

    // prevent form submit

    event.preventDefault();

    event.stopPropagation();

    return;

}

/**

 * Start recording a SVG from it URL

 * @param {URL} url The SVG URL

 * @returns {Promise<Blob>} The recoreded video blob

 */

function startRecord(url) {

    return new Promise(function(resolve, reject) {

        // TODO: implement

    });

}

/**

 * Creates a download button auto-clicked if the browser manage it

 * @param {string} name The recoreded video blob

 * @param {Blob} video The recoreded video blob

 */

function saveResult(name, video) {

    // TODO: implement

}

/**

 * Render the canvas with the given background and SVG

 * @param {CanvasRenderingContext2D} ctx The target canvas context

 * @param {HTMLImageElement} image The image component to render

 */

function render(ctx, image) {

    // TODO: implement

}

Enter fullscreen mode Exit fullscreen mode

We will see in the next steps how to implement the startRecord promise content, the saveResult and render functions.

Copy the SVG to a canvas

In order to record the SVG we have to copy each of it frames into a canvas.

We will do it with a simple img tag and a JavaScript loop.

Play the SVG

Here we start implementing the startRecord promise content by creating a img tag and add it to the body element to play the selected SVG file:


const image = document.createElement('img');

body.appendChild(image);

image.src = url;

Enter fullscreen mode Exit fullscreen mode

Copy SVG to a canvas

We now need to create a canvas in startRecord and use the render function to render the image in the canvas.

We also need to resize the canvas with the image natural size (note that the SVG file needs the width and height attributs in it svg tag), like this:


const canvas = document.createElement('canvas'),

    ctx = canvas.getContext('2d');

image.onload = function() {

    canvas.width = image.naturalWidth;

    canvas.height = image.naturalHeight;

}

body.appendChild(canvas);

render(ctx, image);

Enter fullscreen mode Exit fullscreen mode

Here is the content of the render function that set the SVG a white rectangle of it full size and copy the image to it:


function render(ctx, image) {

    ctx.fillStyle = 'white';

    ctx.rect(0, 0, image.naturalWidth, image.naturalWidth);

    ctx.fill();

    ctx.drawImage(image, 0, 0, image.naturalWidth, image.naturalWidth);

}

Enter fullscreen mode Exit fullscreen mode

Set copy loop over a duration

We now need to call the render function in a loop to have all the SVG frames.

We will create a five seconds loop in startRecord and replace the render call by a call of requestAnimationFrame:


let initTime = null;

requestAnimationFrame(renderLoop);

/**

 * Loop rendering the canvas

 * @param {number} time The loop time

 */

function renderLoop(time) {

    render(ctx, image);

    if (initTime==null) {

        // First call

        initTime = time; 

    }

    else if (time - initTime > 5000) { // 5 seconds

        return;

    }

    requestAnimationFrame(renderLoop);

}

Enter fullscreen mode Exit fullscreen mode

Record the canvas

From the canvas we can use the captureStream method to get a stream of the animation.

We will next create a MediaRecorder of this stream and an array to save the record datas.

We resolve the startRecord promise and remove the img and canvas tags when the recorder is stopped:


const chunks = [],

    stream = canvas.captureStream(30),

    recorder = new MediaRecorder(stream, { mimeType: "video/webm" });

recorder.ondataavailable = function(event) {

    const blob = event.data;

    if (blob && blob.size) { 

        chunks.push(blob); 

    }

};

recorder.onstop = function(event) {

    // remove temp components

    body.removeChild(image);

    body.removeChild(canvas);

    resolve(new Blob(chunks, { type: "video/webm" }));

};

Enter fullscreen mode Exit fullscreen mode

We also add the MediaRecorder start and stop methods calls in the renderLoop function:


function renderLoop(time) {

    render(ctx, image);

    if (initTime==null) {

        // First call

        recorder.start();

        initTime = time; 

    }

    else if (time - initTime > 5000) { // 5 seconds

        // stop recording after defined duration

        recorder.stop();

        return;

    }

    requestAnimationFrame(renderLoop);

}

Enter fullscreen mode Exit fullscreen mode

The final startRecord function

Here is the complete startRecord function:


function startRecord(url) {

    return new Promise(function(resolve, reject) {

        // create image, canvas and recorder

        const image = document.createElement('img'),

            canvas = document.createElement('canvas'),

            ctx = canvas.getContext('2d'),

            chunks = [],

            stream = canvas.captureStream(30),

            recorder = new MediaRecorder(stream, { mimeType: "video/webm" });

        let initTime = null;

        image.onload = function() {

            canvas.width = image.naturalWidth;

            canvas.height = image.naturalHeight;

        }

        body.appendChild(image);

        body.appendChild(canvas);

        recorder.ondataavailable = function(event) {

            const blob = event.data;

            if (blob && blob.size) { 

                chunks.push(blob); 

            }

        };

        recorder.onstop = function(event) {

            // remove temp components

            body.removeChild(image);

            body.removeChild(canvas);

            resolve(new Blob(chunks, { type: "video/webm" }));

        };

        image.src = url;

        requestAnimationFrame(renderLoop);

        /**

         * Loop rendering the canvas

         * @param {number} time The loop time

         */

        function renderLoop(time) {

            render(ctx, image);

            if (initTime==null) {

                // First call

                recorder.start();

                initTime = time; 

            }

            else if (time - initTime > 5000) { // 5 seconds

                // stop recording after defined duration

                recorder.stop();

                return;

            }

            requestAnimationFrame(renderLoop);

        }

    });

}

Enter fullscreen mode Exit fullscreen mode

Save the file with the saveResult function

Now that recorded the SVG as a WebM video, we can save it into a file by implementing the saveResult function:


function saveResult(name, video) {

    const generatedFile = new File([new Blob([blob], {type: 'application/octet-stream'})], name);

    const a = document.createElement('a');

    a.download = generatedFile.name;

    a.href = URL.createObjectURL(generatedFile);

    a.dataset.downloadurl = [generatedFile.type, a.download, a.href].join(':');

    const mouseEvent = document.createEvent('MouseEvents');

    mouseEvent.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);

    a.dispatchEvent(mouseEvent);

}

Enter fullscreen mode Exit fullscreen mode

Go further

To go further we can add some features:

  • define the video width and height since it's from a vectorial image.

The inputs can be pre-filled by the image natural sizes.

We can link them to keep the initial ratio.

  • define the recording framerate.

  • define the recording duration.

It could be pre-filled by a calculated duration from SVG animations.

  • define the canvas background color.

  • define the video codec by detecting the ones managed by the browser.

  • validate form inputs before recording

  • add some CSS

To see the current version of the project you can see it online: https://lenra-io.github.io/svg-recorder/

Help us to improve this project on Github

💖 💪 🙅 🚩
lenra_io
Alienor

Posted on May 31, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related