Skip to content

Instantly share code, notes, and snippets.

@veltman
Created September 2, 2017 12:31
Show Gist options
  • Star 30 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save veltman/ff864215009174bc5d164ec3533125c2 to your computer and use it in GitHub Desktop.
Save veltman/ff864215009174bc5d164ec3533125c2 to your computer and use it in GitHub Desktop.
SVG animation to video

Converting an SVG animation to a video with the MediaRecorder API and a hidden canvas.

Drawing frames from img elements can introduce an extra delay, so this version generates all the frames upfront and then renders them in a loop with requestAnimationFrame().

See also: Canvas animation to video

<!DOCTYPE html>
<meta charset="utf-8">
<body>
<svg width="960" height="500"></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script>
var svg = d3.select("svg"),
canvas = document.createElement("canvas"),
width = canvas.width = +svg.attr("width"),
height = canvas.height = +svg.attr("height"),
context = canvas.getContext("2d");
var projection = d3.geoOrthographic()
.scale(195)
.translate([width / 2, height / 2])
.precision(0.1);
var path = d3.geoPath().projection(projection);
d3.json("/mbostock/raw/4090846/world-110m.json", function(err, world) {
var data = [],
stream = canvas.captureStream(),
recorder = new MediaRecorder(stream, { mimeType: "video/webm" });
recorder.ondataavailable = function(event) {
if (event.data && event.data.size) {
data.push(event.data);
}
};
recorder.onstop = () => {
var url = URL.createObjectURL(new Blob(data, { type: "video/webm" }));
d3.selectAll("canvas, svg").remove();
d3.select("body")
.append("video")
.attr("src", url)
.attr("controls", true)
.attr("autoplay", true);
};
var background = svg.append("rect")
.attr("width", width)
.attr("height", height)
.attr("fill", "#fff");
svg.append("path")
.datum({ type: "Sphere" })
.attr("stroke", "#222")
.attr("fill", "none");
svg.append("path")
.datum(topojson.feature(world, world.objects.land))
.attr("fill", "#222")
.attr("stroke", "none");
svg.append("path")
.datum(topojson.mesh(world, world.objects.countries, function(a, b) {
return a !== b;
}))
.attr("fill", "none")
.attr("stroke", "#fff");
var queue = d3.queue(1);
d3.range(120).forEach(function(frame){
queue.defer(drawFrame, frame / 120);
});
queue.awaitAll(function(err, frames){
recorder.start();
drawFrame();
function drawFrame() {
if (frames.length) {
context.drawImage(frames.shift(), 0, 0, width, height);
requestAnimationFrame(drawFrame);
} else {
recorder.stop();
}
}
});
function drawFrame(t, cb) {
projection.rotate([360 * t]);
svg.selectAll("path").attr("d", path);
var img = new Image(),
serialized = new XMLSerializer().serializeToString(svg.node()),
url = URL.createObjectURL(new Blob([serialized], {type: "image/svg+xml"}));
img.onload = function(){
cb(null, img);
};
img.src = url;
}
});
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment