Alienor
Posted on May 31, 2022
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>
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
}
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;
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);
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);
}
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);
}
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" }));
};
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);
}
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);
}
});
}
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);
}
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
Posted on May 31, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.