Turn an animated svg into a video file
Tags: tutorial, webdev, svg
Published: 2022-05-31 07:23:20

_Check out the video here to see the how-to with Thomas !_
{% youtube Lj2PvU1rOs4 %}

# 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

- [Local SVG recorder](#local-svg-recorder)

  - [Preview the SVG](#preview-the-svg)

  - [Copy the SVG to a canvas](#copy-the-svg-to-a-canvas)

    - [Play the SVG](#play-the-svg)

    - [Copy SVG to a canvas](#copy-svg-to-a-canvas)

    - [Set copy loop over a duration](#set-copy-loop-over-a-duration)

  - [Record the canvas](#record-the-canvas)

  - [The final `startRecord` function](#the-final-startrecord-function)

  - [Save the file with the `saveResult` function](#save-the-file-with-the-saveresult-function)

  - [Go further](#go-further)

## 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:

```html

<!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>

```

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:

```javascript

"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

}

```

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:

```javascript

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

body.appendChild(image);

image.src = url;

```

### 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:

```javascript

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);

```

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:

```javascript

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);

}

```

### 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`](doc-mdn):

```javascript

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);

}

```

## Record the canvas

From the canvas we can use the [`captureStream`](doc-mdn) method to get a stream of the animation.

We will next create a [MediaRecorder](doc-mdn) 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:

```javascript

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" }));

};

```

We also add the MediaRecorder [`start`](doc-mdn) and [`stop`](doc-mdn) methods calls in the `renderLoop` function:

```javascript

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);

}

```

## The final `startRecord` function

Here is the complete `startRecord` function:

```javascript

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);

        }

    });

}

```

## 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: 

```javascript

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);

}

```

## 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]([lien-projet](https://github.com/lenra-io/svg-recorder))