Technical Write-up: Scroll Linked Animations

(This post is about my thesis project. You can see it at letsfreecongress.org)

Let’s Free Congress uses D3.js, Underscore.js, PubSub, JQuery and a keyframing routine I wrote to achieve the scoll linked animation effects. The basic idea is as follows.

Part One: Where is the User?

All animation on the page are triggered by scrolling, using JQuery’s scroll event to figure out where the user is relative to the document’s height. I quickly learned to keep the code modular, and started using PubSub to broadcast the scroll position to animation components. All the animating components subscribe to the scroll channel, and calculates properties like position, size, etc based on the scroll position broadcasted.

$w = $(window);
$w.scroll(function() {
    PubSub.publish("scrollTop", $w.scrollTop());
});

Something like that.

Part Two: Keyframes

With PubSub we have a scroll position and an event to trigger change. The next question becomes how we transform scroll position data to parameters values like x and y positions or sizes.

I figured I would try to work backwards. Ideally, I would want to be able to specify “keyframes” (which in this case would be scroll positions) and the numeric value that should be associated with it. Imagine I want a bar that grows to 240px tall as the page scrolls down 500px, and then shrinks down to 10px tall as the page scrolls further. I want to specify that animation like this:

[
    {"scroll":0,"value":0},
    {"scroll":500,"value":240},
    {"scroll":800,"value":10},
]

I want to feed an array like that into a function, and get a magical function back. That magical function should take any positive integer (i.e. scroll position) and return what the value should be at that given position.

I figured I needed two parts. One set of functions that calculated scaled values between each keyframe (i.e. tweening functions), and a function that determines which tweening function to use.

A tweening function is basically a scale function in D3.js. Something like:

var tween = [];
tween[0] = d3.scale.linear()
    .domain([0,500]).range([0,240]);
tween[1] = d3.scale.linear()
    .domain([501,800]).range([240,10]);

Of course, the actual functions were generated by looping through scroll-to-value pairs. The next thing I needed was another function that determined which tweening function should be used given the scroll position. Since I am not a clever programmer, I just did it with a for loop.

var index = function(frame) {
    // Loop thru each keyframe
    for(var i = 0; i < keyframes.length; i++) {
       // If the current keyframe is > scroll position,
    we've found the range we are in.
        if(keyframes[i].pixel > frame) { break; }
    }
    return i-1;
}

To recap: We use the index function to determine which range we are in, then use the tween function to get the exact value we should have. The whole thing comes together like this.

return function(scrollTop) {
    return tween[index(scrollTop)](scrollTop);
}

Part 3: All Together Now

With PubSub broadcasting scroll position, and a way to create keyframe tweening functions, I can now animate an object precisely based on scroll position, by doing something like:

keyframes = [
    {"scroll":0,"value":0},
    {"scroll":500,"value":240},
    {"scroll":800,"value":10},
]

var barHeight = frameMapFactory(keyframes);

Which then can be used to animate graphical elements on mouse scroll, like this:

PubSub.subscribe("scrollTop", function(msg, data) {
    var height = barHeight(data);
    $("#bar").css({"height":height+"px"});
}

You can imagine how a similar process can be applied to affect position, opacity and sizes of SVG object using D3.js. I made use of this technique extensively in the Let’s Free Congress website.

Once I do some cleaning up, I’ll put the open source the code on GitHub. Do ping me @tonyhschu if you have questions or comments!