• Stars
    star
    179
  • Rank 214,039 (Top 5 %)
  • Language
    HTML
  • Created almost 9 years ago
  • Updated about 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Testing various ways of generating gifs and videos off data-driven JS animations.

Animation Tests

Miscellaneous experiments with automatically rendering gifs and videos from data-driven JavaScript animations.

Why?

Interactive visualizations are cool, but sometimes a gif or a video tells the story pretty well by itself. Gifs have the advantage of being easy to save, easy to share, and easy to view on pretty much any device.

Even if you're going to publish a whole interactive whatever, odds are you'll want something like a gif for social sharing or for demonstrating particular aspects of the data.

The current state-of-the-art for rendering a gif from your in-browser animation still mostly seems to be screen capture, which is easy but not very repeatable or configurable. I was curious to explore other options.

How?

Here's an overview of some different techniques for creating a gif of a cool JavaScript animation you wrote, with pros and cons of each.

QuickTime + FFmpeg

As outlined by Mike Bostock, you can create a gif of your animation by taking a screen recording of your browser with QuickTime, converting the movie to frames with FFmpeg, and combining them with gifsicle.

Pros

  • Relatively straightforward process
  • Works on any kind of animation
  • Can show user interactions (like a button being clicked)

Cons

  • Manual recording means you have to redo the whole process for any change
  • Hard to precisely crop the screen area and start/end times
  • Lack of fast visual feedback for tweaking
  • Requires command-line tools and comfort with them

QuickTime + Photoshop

As outlined by Lena Groeger you can create a gif by taking a screen recording of your browser with QuickTime, converting the movie to frames with Photoshop's "Video Frames To Layers" function, and saving the result as an animated gif.

Pros

  • Relatively straightforward process
  • Works on any kind of animation
  • Can show user interactions (like a button being clicked)
  • Easy to tweak individual frames

Cons

  • Manual recording means you have to redo the whole process for any change
  • Requires Photoshop license
  • Requires comfort with Photoshop, especially for cropping or tweaking frames
  • Rendering can be a drag with a long animation or a slow computer

LICEcap

You can create a gif using LICEcap by taking a screen recording of your browser and saving it directly as a gif.

Pros

  • Straightforward process with very few steps
  • Works on any kind of animation
  • Can show user interactions (like a button being clicked)
  • You can move the window while it's recording
  • Easy control over framerate

Cons

  • Manual recording means you have to redo the whole process for any change
  • Hard to precisely crop the screen area and start/end times
  • Tweaking requires adding in other tools

Node + canvas

As described by Tom MacWright, you can use the canvas module to write JavaScript for an animation the same way you would for a browser and then run a Node script to spit out all the frames as images.

To compile the frames into a gif, you can either use command line tools or do it within your script using modules like node-gif or gifencoder.

Pros

  • All code, not manual - can make this part of an automated pipeline, or quickly get a new gif when you tweak something in your code
  • Detailed control over size, start/end, and framerate

Cons

  • All code, not manual - higher barrier to entry
  • Only works with code that generates frames from scratch one at a time, not something declarative like $.animate, d3.transition, or a CSS animation
  • Only works with a canvas-based animation. That means no styling with CSS, no SVG-specific features, etc. without extra stuff.
  • No access to browser APIs or features that aren't supported by the canvas module (e.g. Path2D).
  • Lots of image library dependencies
  • Requires Node.js and some comfort with it
  • Requires command line tools and some comfort with them

Web workers + canvas

Demo: http://bl.ocks.org/veltman/03edaa335f93b5a9ee57

The basic idea here is to write in-browser code similar to the "Node + canvas" approach above, where you update a canvas element frame-by-frame. You combine the frames into a gif in the background using gif.js web workers. This is relatively slow, but you get visual feedback and there are no dependencies besides the gif.js library. You only need some JavaScript and a web browser.

Pros

  • Code-based - easy to get a new gif when you tweak something in your code
  • Quick visual feedback
  • Detailed control over size, start/end, and framerate
  • Can access browser APIs and non-canvas features. For example, this demo creates a dummy SVG to calculate the length of a path before adding it to a canvas.
  • No Node scripts or command line required
  • I assume you could use this with WebGL to make a 3D gif? That would be pretty cool.

Cons

  • You still need to execute the code in a browser and save the result manually; it's better for an internal tool than for any sort of automated pipeline
  • Only works with code that generates frames one at a time, not something declarative like $.animate, d3.transition, or a CSS animation
  • The animation itself ultimately has to be done with a <canvas> element; you can't really manipulate things as DOM elements or style them with CSS without some insane hacks
  • Rendering takes a non-trivial amount of time, something like 5 or 10 seconds of render time per 1 second of animation
  • Doesn't work in every browser

Web workers + SVG

Demo: http://bl.ocks.org/veltman/1071413ad6b5b542a1a3

The basic idea here is similar to the "Web workers + canvas" approach above, but using an SVG instead. With a little trickery involving a Blob and a dummy image element, you can render the contents of an SVG as an image and turn that into an animation frame. Unlike the canvas approach, this gives you full access to SVG elements, attributes, and techniques. The one catch is that you have to make sure any stylesheets that affect the SVG are copied into the SVG element itself.

Pros

  • Code-based - easy to get a new gif when you tweak something in your code
  • Detailed control over size, start/end, and framerate
  • Can make full use of SVG goodness and style things with CSS
  • No Node scripts or command line required

Cons

  • You still need to execute the code in a browser and save the result manually; it's better for an internal tool than for any sort of automated pipeline.
  • Only works with code that generates frames one at a time, not something declarative like $.animate, d3.transition, or a CSS animation
  • Rendering is even slower than the <canvas> version
  • No 3D :(
  • Doesn't work in every browser

PhantomJS

One of the big downsides to the all the frame-by-frame approaches above is that you have to structure your code in a particular way. It has to explicitly set the current frame based on how far along it is, like:

// Do a bunch of initialization

// Loop through the number of frames you want
for (var i = 0; i < numFrames; i++) {

  bar.attr("width",maxBarWidth * i / numFrames);

  // save the frame

}

For a simple animation this is feasible, but once it gets more complex it really sucks. Your normal code is more likely to look like:

// d3
allTheBars.transition()
  .duration(2000)
  .ease("some-weird-easing-function")
  .attr("width",someComplicatedFunction);

or maybe

// jQuery
$(".bar").animate({
  width: 100
},2000);

One way to capture that programmatically while still leveraging browser goodness is with PhantomJS, a headless web browser. Using a variation of this technique, you can screen capture your page every few milliseconds and get your frames that way.

For example, if you have the following page, where the function ready() kicks off the animation:

<!DOCTYPE html>
<meta charset="utf-8">
<style>

body {
  margin: 0;
  padding: 0;
}

rect {
    fill: #e51133;
}

</style>
<body>
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.5.10/d3.min.js"></script>
<script>

  var width = 600,
      height = 600;

  var svg = d3.select("body").append("svg")
      .attr("width",width)
      .attr("height",height);

  var y = d3.scale.ordinal()
      .domain(d3.range(6))
      .rangeRoundBands([0,height],0.1);

  var bars = svg.selectAll("rect")
      .data(y.domain())
      .enter()
      .append("rect")
      .attr("y",y)
      .attr("width",0)
      .attr("height",y.rangeBand());

  function ready() {

    bars.transition()
        .duration(2000)
        .attr("width",function(d){
          return width / (d+1);
        });

  }

</script>

You could capture it and pipe it to stdout with this script:

var page = require("webpage").create();

// Page dimensions
var url = "http://localhost:8888/", // URL of your page
    duration = 2000,
    numFrames = 50,
    currentFrame = 0;

page.open(url,function(status) {

  // Update page dimensions
  var wh = page.evaluate(function(){ return [width,height]; });
  page.clipRect = { top: 0, left: 0, width: wh[0], height: wh[1] };
  page.viewportSize = { width: wh[0], height: wh[1] };

  // Start animation
  page.evaluate(function(){ ready(); });

  // Get the first frame right away
  getFrame();

  function getFrame() {
    page.render("/dev/stdout", { format: "png" });

    if (currentFrame > numFrames) {
      return phantom.exit();
    }

    // Get a new frame every ___ milliseconds
    setTimeout(getFrame,duration/numFrames);

  }

});

And save that output as an mp4 using FFmpeg:

$ phantomjs render.js | ffmpeg -y -c:v png -f image2pipe -r 25 -t 2  -i - -an -c:v libx264 -pix_fmt yuv420p -movflags +faststart movie.mp4

Then you've still got to convert your movie into a gif, but you're most of the way there.

The main problem with this approach in my experience is that the page.render() call is synchronous and takes enough time to complete that you can't actually sample the animation at a consistent interval. Each frame ends up being recorded later than it's supposed to be, and the longer the animation is, the more out of sync the end results will be.

As a compromise, you could instead use PhantomJS to step through the frames by calling a stepper function repeatedly and rendering that way, but then we're basically back to where we were with the other approaches.

Pros

  • Can use anything that works in a vanilla Webkit browser

Cons

  • Syncing things between your browser code and your PhantomJS script is pretty annoying and is hard to do without lots of exposed global variables
  • Timing is unreliable
  • Still need several steps to actually end up with a gif
  • PhantomJS, FFmpeg, gifsicle dependencies
  • PhantomJS gets weird about certain things like webfonts sometimes
  • Probably the slowest option
  • Seems like a very convoluted hack!

Web workers + PhantomJS

Demo: https://github.com/veltman/headless-gif

One slight variation on the above is using the technique from one of the web worker examples to generate your entire gif, but encoding the result as a Base64 string so that you can grab it with PhantomJS. This is a nice compromise because you can create/debug your gifmaker in a browser and then use PhantomJS to crank out gifs server-side, potentially supplying custom options for each.

  1. PhantomJS requests gifmaker.html.
  2. gifmaker.html creates a gif with web workers. When it's done, it encodes the result as a Base64 string and saves it as a global variable.
  3. Meanwhile, PhantomJS is checking periodically to see whether the gif is done. When it's done, it gets the content of that global variable.
  4. Decode the Base64 string and pipe the results into a .gif file.

Pros

  • Can automate server-side gifs but still use a lot of browser-specific goodness

Cons

  • PhantomJS dependency
  • PhantomJS gets weird about certain things like webfonts sometimes
  • Only works with code that generates frames one at a time, not something declarative like $.animate, d3.transition, or a CSS animation
  • Still very convoluted (but in a good way maybe?)

The next frontier: hacking d3 transitions

Demo: https://bl.ocks.org/veltman/23460413ea085c024bf8

As noted in the "PhantomJS" section above, there's a frustrating tradeoff between the sort of animation code you might naturally write and the way you have to write it to save out a gif. Wouldn't it be nice if you could still make use of abstractions like d3.transition but also say "while you're doing all that transitioning, stop every few milliseconds and save a frame for a gif"?

allTheBars.transition()
  .duration(2000)
  .ease("some-weird-easing-function")
  .attr("width",someComplicatedFunction);
  .frames(50)
  .on("frame",function(f){
    // f is image data for the next frame, add it to a gif
  });

Because these transitions are asynchronous and use requestAnimationFrame, they're hard to tap into, and they don't even step through the animation at totally predictable intervals. If your browser got distracted by a bird, it might be a while before the next frame happens and the animation will be jumpy. So we need a way to tell d3: "Here's the transition I want; now you know all the math to calculate any arbitrary point along the way; let me have the remote and press play/pause/stop/fast-forward and jump to arbitrary time t."

// The normal transition
var transition = allTheBars.transition()
  .duration(2000)
  .ease("some-weird-easing-function")
  .attr("width",someComplicatedFunction);

// A ghost transition that internalizes all the transition math
// Knows what value to set for each element at any time t but
// doesn't actually run
var ghostTransition = transition.ghost();

saveAllTheFrames(ghostTransition,50);

function saveAllTheFrames(ghost,numFrames) {

  for (var i = 0; i < numFrames; i++) {
    // synchronously update attributes of elements in the transition
    // to match a given percentage progress in time
    ghost.progress(i / numFrames);

    //save the frame here
  }

}

This seems mildly insane but I gave it a shot. d3-record.js in this demo is a proof of concept that mostly works. It takes every element in a selection, and gets the easing function and list of tweens that d3 is storing internally. Then, when it's called, it loops through each element, and applies each tween with the appropriate adjusted time value.

TODO: write some sort of d3-animator module that has the same API as d3.transition but is for synchronous updates instead of a background transition.

Pros

  • Can use pretty much anything that works in a browser
  • Can use declarative syntax for your animations (i.e. a lot less math)
  • Detailed control over size, start/end, and framerate

Cons

  • Still doesn't work with things like d3 axes, which have specific behavior around transitions
  • Seems like an even more convoluted hack!

Stopping time

Demo: http://bl.ocks.org/veltman/5de325668417b1d504dc

One idea that is somehow both beautiful and grotesque is to manually control an otherwise uncontrollable animation by messing with time itself. This approach has the benefit of working with any sort of declarative animation library, and not requiring a ton of code. It has the downside of being crazy and breaking almost everything.

D3, jQuery et al. rely on representations of the current time to "tick" the animation forward by running a render loop. They get the current time when the animation starts, with something like Date.now() or the timestamp supplied by requestAnimationFrame. That becomes t=0 and then the library repeatedly asks what time it is and updates the animation accordingly, until it reaches the end.

You can short-circuit this process by lying to the browser (or Node) about what time it is with an overwrite of Date.now and requestAnimationFrame. When you declare an animation, call the current time t=0. Then, whenever D3 asks what time it is, just lie and say it's t + __ milliseconds, where the blank is how many milliseconds into the animation you want to be. In this way you can step through an animation at any speed you want (and save frames along the way).

This still does not solve the "teardown" problem, where animations clean themselves up behind the scenes after they pass key times, so if your animation is complicated you can probably only advance it in one direction, but that's probably fine for something like rendering a gif.

Note: although this is intriguing, it feels like malpractice to recommend it. Overwriting global time functions is probably not a good idea. Be careful.

Automate QuickTime screen recording

Same as the "Quicktime + FFmpeg" or "Quicktime + Photoshop" approaches, but automate the actual screen recording. Automatically get the screen x, y, width, and height of the <body> element in an open browser and capture a screen recording of it. You'd still need a way to specify the duration and start it at the right time. This could probably be done with AppleScript but I wouldn't wish that task on my worst enemy.

Pros

  • Slightly more automated

Cons

  • Mac-only
  • Completely insane

Summary

If you just want a quick demo of your animated thing to post on Twitter, screen recording probably makes more sense and you should ignore all of this. But these techniques can be pretty useful in other situations.

If you want to automatically produce a bunch of animated gifs or videos on a server and your animation is pretty simple, "Node + canvas" is a good option. If your animation is complicated and needs browser stuff, but you still want an automated generator, "Web workers + PhantomJS" could suit you well.

If you want to produce on-demand gifs without any server-side code, the web workers techniques listed above all work pretty well. They're not instantaneous and they're a bit clunky but on the other hand you can let users generate totally custom gifs and do all the work in their browsers.

Another nice thing about these techniques is that they can all be put towards videos, not just gifs. Any mechanism that generates a stack of images, one frame at a time, can be put through ffmpeg or something similar to create a video instead, so you could potentially create nice data-driven animations in videos without mastering After Effects or whatever.

What did I miss? Do you know a better way? Get in touch!

Other resources

More Repositories

1

flubber

Tools for smoother shape animations.
JavaScript
6,378
star
2

clmystery

A command-line murder mystery
5,317
star
3

principles

Things to keep in mind when making stuff for the web
1,265
star
4

learninglunches

Materials for a series of learning lunches on news development topics.
237
star
5

csvgeocode

Node module for bulk geocoding addresses in a CSV.
JavaScript
163
star
6

wherewolf

A server-less boundary service. Find what geographic feature (e.g. an election district) a given point lies in.
HTML
160
star
7

mapstarter

A tool for generating starter SVG maps from a geographic data source
JavaScript
129
star
8

ca-license-plates

Vanity license plate applications from the California DMV.
122
star
9

pancakejs

A mini-library for easily flattening SVG and Canvas elements into images on the fly.
JavaScript
86
star
10

d3-stateplane

D3-friendly State Plane projections
86
star
11

openvis

Links and bibliography for OpenVisConf 2017
84
star
12

fourscore

An open-source version of the WNYC sentiment tracker.
JavaScript
46
star
13

nflplays

Cleaned-up NFL play-by-play data from 2002-2012
42
star
14

stakeout

For watching a set of URLs and notifying someone when something has changed.
JavaScript
31
star
15

xyz-affair

Generate list of x/y/z Spherical Mercator tiles based on a bounding box and a zoom level.
JavaScript
31
star
16

endorsements

Data on newspaper presidential endorsements
28
star
17

markdowneyjr

A hacky Markdown-to-JSON parser for easier copy editing.
JavaScript
25
star
18

loopify

Seamless looping with the WebAudioAPI.
JavaScript
22
star
19

maps-nicar14

Notes for map session with Tom MacWright (@tmcw) at NICAR14.
21
star
20

d3-unconf

HTML
17
star
21

presidential-nouns

Nouns of assemblage for US presidents.
13
star
22

flipbookjs

For automatically flip-booking progress while developing something for the web.
JavaScript
13
star
23

snd3

D3.js resources for SND/NYC 2018
HTML
13
star
24

mahalanobis

Calculate Mahalanobis distances for multivariate data.
JavaScript
13
star
25

ire2014

A list of resources for getting started with interactive news projects.
12
star
26

notsimple

Chrome extension that puts quotes around any instance of the words 'simple,' 'simply,' 'easy,' and 'easily' in GitHub and RTD docs.
JavaScript
11
star
27

gobblefunk

Rename your JS variables with silly words derived from Dr. Seuss, Roald Dahl, and Lewis Carroll.
JavaScript
9
star
28

hashnav

Simple JS hash-based navigation library
JavaScript
9
star
29

oh-snap

Snap points to nearest point in a different set.
JavaScript
7
star
30

hhbaworkshop

Mapping History - A Leaflet.js workshop for the Hacks/Hackers Buenos Aires Media Party
CSS
6
star
31

headless-gif

Saving web worker'd gif via PhantomJS.
JavaScript
6
star
32

dotmapper

Auto-generate a dotmap from a GeoJSON file
JavaScript
5
star
33

node-geosupport

Node.js wrapper for fast geocoding with NYC's Geosupport system
JavaScript
5
star
34

streets

A bunch of convoluted processing for turning OSM data into Leaflet-mappable street objects with names.
Python
5
star
35

fresh-start

A full bootstrap script for a new installation of Ubuntu Desktop
Shell
5
star
36

presidential-election-results

1976-2020 presidential election popular vote totals by state and party
4
star
37

chopped-and-viewed

Node module for chopping fixed-width text files.
JavaScript
4
star
38

congressional-acronyms

Raw data on congressional acronyms, 1972-2013
3
star
39

insecurity

Analyze HTML, CSS, JS, etc. files for insecure URLs.
JavaScript
3
star
40

nicar15-scrapeoff

Gathering notes/suggestions for a Scrape-off at NICAR 15.
2
star
41

knickout

For simulating the rest of the Knicks season.
JavaScript
2
star
42

point-on-line

Test whether a point is on a line or LineString
JavaScript
2
star
43

postal-abbreviations

A no-fuss US postal abbreviations module.
JavaScript
2
star
44

BriefMemorableSlug

Generate random adjective-adjective-animal slugs
JavaScript
1
star
45

state-population-by-age

State population by age, 1960-2040
1
star
46

dst

Collecting data to visualize effects of DST
Python
1
star
47

lazy-vector-tiles

A lazy vector tile system. Not robust or high-performance, but rather straightforward.
JavaScript
1
star
48

bracket

CSS
1
star
49

rosetta

TK
1
star
50

euler

Solving Project Euler problems in JS, Python, and maybe Ruby
JavaScript
1
star
51

gulp-insecurity

Gulp plugin for detecting insecure URLs that could cause mixed content errors.
JavaScript
1
star
52

statelympics

Python
1
star
53

webpack-babel-bug-repro

JavaScript
1
star
54

fuel-prices

Misc. fuel price data
JavaScript
1
star
55

recs

Travel recommendations in Markdown
CSS
1
star
56

ticker-tape

A list of ticker tape parades in New York City
1
star
57

jsv

1
star
58

emojify

Replace PHP variable names with emojis.
PHP
1
star
59

supreme-court-transcripts

Attempting to parse supreme court oral argument transcripts.
JavaScript
1
star
60

split-multipart-features

A script for selectively splitting up multipart features in QGIS.
Python
1
star
61

nyc-css

NYC in CSS (Chrome only)
HTML
1
star