Animations, Timing, and Sequencing

The benefit of encoding your data using Counterpoint’s Attributes and Marks is that you can immediately animate updates to data properties with very little code change. For example, given the following non-animated code:

mark.setAttr('x', mark.attr('x') + 100)

making this an animation is as easy as changing setAttr to animateTo. The rest is customization and control to fit animation within the needs of your web app, as we describe below.

Tickers

All updates and animations of attributes require a ticker to be loaded somewhere in your code. You can set up a Ticker with a single line of code somewhere in your script such as:

let ticker = new Ticker(advanceables).onChange(draw);

Here, advanceables represents an object or array of objects that support the advance(dt) method, which includes Attribute, Mark, and MarkRenderGroup. Every frame, the ticker will call the advance method on each object. If any of them returns true (which happens when an attribute was updated or is being animated), the render state is considered to have changed, and the onChange callback is called.

WARNING: Ticker Inputs

It’s important to make sure that the argument to the Ticker contains all potentially changing attributes. If not, your draw function might not always get called when your attributes change.

Note that while the advance method is called on every element every frame, conditioning on the advance methods’ return values allows us to reduce the number of times your drawing code is called.

It’s a good idea to call stop() on the Ticker if you no longer need to update the rendering, otherwise the Ticker will continue to tick until the user navigates away from the page.

You can also use the related class LazyTicker, which behaves similarly to Ticker but stops whenever all of its advanceables return false. You opt-in to the ticker on the fly by calling start() whenever there is an animation you need to run, and it will automatically stop when no longer needed. This can be useful when you have a lot of advanceable objects that may be expensive to iterate through when not necessary.

Specified and Momentary States

Counterpoint maintains a conceptual separation between the final value of an attribute (its specified state) and its intermediate value during an animation (the momentary state). This allows you to read attribute values with or without assuming an animation is ongoing.

When drawing marks, for instance, you’ll probably want to get the momentary state of each attribute. To do so, simply use the Mark.attr method (which wraps Attribute.get).

let currentX = mark.attr('x');
// OR
let currentX = attribute.get();

If no animation is present, this momentary state is equal to the specified state.

In other cases in your code, you may want to read the final value of an attribute. For example, let’s say we have a color attribute that animates between red and blue when the user clicks a button. In our click handler, we may want to check whether the attribute is currently set to red or blue. To do so we can use the Mark.data or Attribute.data method:

if (mark.data('color') == 'red') mark.animateTo('color', 'blue');
else mark.animateTo('color', 'red');

Choosing the Specified State Value

Each mark and render group provides two animation functions: animateTo and animate.

To specify the final state value directly, use animateTo, as follows:

mark.animateTo('x', 100);

Using the same function, we can also specify that the mark or attribute should be assigned a new value function and its value animated to the function’s result:

mark.animateTo('x', getX);

Now, if the final state value will be computed implicitly by the attribute’s existing value function, we can use animate without a value argument:

mark.animate('x');

When running animations on an entire render group, we may want to specify a different final value per mark. To do so, we can use MarkRenderGroup.animateTo and pass a function as argument:

renderGroup.animateTo('x', (mark) => mark.attr('x') + 100);

TIP: Chaining Animations

Animation calls on the same object can be chained together. For example, mark.animate('x').animate('y') animates both the x and y attributes simultaneously.

Animation Timing Options

We can add arguments to the call to animate or animateTo to specify the duration, delay, curve, or custom interpolation behavior.

For example, to animate a mark slowly with a delay and an cubic ease-in-out easing function:

mark.animate('color', {
    duration: 2000, // 2 seconds
    delay: 500, // 0.5 seconds
    curve: curveEaseInOut
});

The animation curve can be any function that takes a linear animation progress value between 0 and 1, and returns a new animation progress value. For example, you can use D3’s ease functions directly.

Custom Interpolators

Interpolators in Counterpoint are objects that have a single function, interpolate. This function should take an initial value and a progress value (between 0 and 1), and produce the momentary value for the attribute.

By default, Counterpoint animates numerical attributes using a continuous numerical interpolator, and string attributes using a color interpolator. However, you can also define a custom interpolator to support animations on different data types.

For example, let’s say we want to create a typewriter-style animation for text, in which the existing characters will be deleted one-by-one, followed by adding the new characters one-by-one:

We will store the text value in an attribute:

let textMark = new Mark('mark-id', {
    text: 'I like apples'
});

Then, we define an interpolator object by creating a factory function that creates an interpolator given a final value:

function makeTypewriterInterpolator(finalValue: string): Interpolator<string> {
    return { 
        finalValue, // store the final value in the returned object for internal bookkeeping
        interpolate (initialValue, t) {
            let initialLength = initialValue.length;
            let finalLength = finalValue.length;
            let total = initialLength + finalLength;
            if (t < initialLength / total)
                return initialValue.slice(0, initialLength - Math.floor(t * total));
            else
                return finalValue.slice(0, Math.floor((t - initialLength / total) * total));
        }
    };
}

Finally, we pass the interpolator to the mark in our call to animate (note we do not use animateTo because we are using a custom interpolator):

textMark.animate('text', {
    interpolator: makeTypewriterInterpolator('I like oranges'),
    duration: 1000
});

Waiting for an Animation to Complete

Animations in Counterpoint run asynchronously, so a call to animate or animateTo simply starts the animation. To wait until an animation is completed and then run some code, use the aptly-named wait method. This method returns a Promise that resolves when the animation completes, and rejects if the animation is canceled by the start of another animation or attribute update. (If no animation is currently running, the Promise immediately resolves.)

For example, to create a pulse animation:

async function pulse() {
    mark.animateTo('radius', 1.2)
    await mark.wait('radius');
    mark.animateTo('radius', 1.0);
}

Note that you must specify which attribute name(s) to wait on (either a string or an array of strings). If you specify multiple attributes, the Promise resolves only when all attributes’ animations have completed, and rejects if any of them are canceled.

results matching ""

    No results matching ""