Hero image

Basic Animation Library


Animation libraries are great! I have used them a bit and they make micro interactions really simple, but I wanted to make sure I understood what happens underneath so I can keep things performant.

What better way to do this than to make my own animation library?

Core Requirements

To keep things simple I have the following requirements for my Library.

  1. Tweening between two number properties
  2. Simple easing options for the Tween
  3. An option to start, stop or restart a tween

Definitions

What is a tween?

A tween is simply the action of going between one value to another value. For example, here I have a box which I set from rotation 0turn to rotation 1turn over one second.

The CSS for this is transform: rotate(0turn); at the beginning, and transform: rotate(1turn); at the end. The aim of this library is to handle all the between timings and values for us.

What is an Easing Function

An easing function is the magic sauce of animations. They tell us at each point in the animation how close or far from the start or end state we are.

I have added two animations below that both take exactly 1 second to complete, but you will notice that they look very different. This is because one has linear easing and the other has easeInOut easing.

There are many different easing functions out there - some look like bounces, some are smooth curves, and some are completely custom.

A great resource for seeing what different types of easing functions can do is easings.net.

Stopping and Starting

What is the point of an animation library if you cannot control when to display an animation? We will want to provide an option to start the animation, stop the animation, and restart the animation.

  • Restart - starts the animation from the beginning.
  • Start - starts the animation from when it was stopped, or at the beginning if it has yet to run.
  • Stop - stops the animation at the current location.

The Code

For this library I am going to create a single class that will represent an individual animation. For this it will wrap an HTMLElement and then update its styles as required.

The animation library will support any css properties that are number based, but for simplicity in this example I will define only:

  • x - horizontal translations
  • y - vertical translations
  • width - vertical size
  • height - vertical size
  • rotate - rotation around the center of the 2d element

We can define these properties as a type so we can restrict the allowed values later.

// All supported properties that the library knows about
type ValidTweenProperties = {
    x?: string | number;
    y?: string | number;
    width?: string | number;
    height?: string | number;
    rotate?: string | number;
}



The class will need to know the element to control, the duration of the animation, as well as the start and end state. This is where we can use generics to enforce the supported properties. There is a fair bit of template code here which does little other than setup the variables we will use later.

type TweenConstructorParams = {
    element: HTMLElement;
    duration: number;
    startState: T;
    endState: T;
}

class Tween<T extends ValidTweenProperties> {
    // Constructor parameters
    private element: HTMLElement;
    private duration: number;
    private startState: T;
    private endState: T;

    constructor({
        element,
        duration,
        startState,
        endState
    }: TweenConstructorParams<T>) {
        this.element = element;
        this.duration = duration;
        this.startState = startState;
        this.endState = endState;
    }
}

Running a Tick

In order to run a tick there are two things that need to be set up:

  1. An action that lets us start the animation
  2. An onTick handler that will calculate the first ticks state

To start from the beginning I will use the restart option, as it is the easiest to understand. It will always start at the start state and end at the end state. We will define a few useful properties that allow us to know whether the animation is running, and what time it started.

class Tween {

    // Is the animation currently running
    private running: boolean = false;

    // Timestamp of the first tick
    private startTickTime: number = 0;

    // Timestamp of the last tick then complete
    private lastTickTime: number = 0;


    restart() {
        // Record that the animation has been started
        this.running = true;

        // Reset the startTickTime
        this.startTickTime = 0;

        // browser animation callback
        requestAnimationFrame((...args) => this.onTick(...args));
    }
}



A key piece to note is the requestAnimationFrame call. This is the browsers inbuilt tool to handle animating elements on the screen. The provided callback will be called once when the next frame is to be drawn. In this case we have defined an onTick function that now needs to be implemented.

onTick

The basic steps that a tick needs to do is:

  1. Check if the animation is even running, and cancel early if it shouldn’t be
  2. Check if the animation should end, and set the end state if it should
  3. Process the current tick

To do this, there is a bit of state that should be set up at the beginning of each tick. Below is the starter code that does part 1, and sets up state we will want later if the animation is running.

onTick(time: number) {
    // Cancel early if the animation has stopped
    if (!this.running) return;

    // If this is the first frame, set the start time
    if (!this.startTickTime) this.startTickTime = time;

    // Current animation time - used to know when we stopped the animation
    this.lastTickTime = time;

    // ...
}



For ending the animation all that needs to happen is we get the this.endState configuration and apply it to the element. The below will not quite work for what we need, but it will for basic css styles.

Note: We have assumed pixels here - but I will show you how to use any unit further into this article.

// Check if the animation duration is finished
const isFinished = this.startTickTime + this.duration < time;
if (isFinished) {
    Object.entries(this.endState).forEach(([key, value]) => {
        // Assuming pixels for now
        this.element.style[key] = `${value}px`;
    }

    // No more ticks requested
    return;
}



The hard part

The final part to go is setting the position of the current ticks animation while it is in progress. First we need to find the current percentage we are through the animation, then we need to calculate what that means for the value we set.

To get the percentage we are through the animation we can use the time the animation started, minus the current tick time. This can then be divided by the animation duration to get the percentage.

const timeElapsed = time - this.startTickTime
const percentageElapsed = timeElapsed / this.duration;



Now we know the percentage, we can start with a linear easing function. The linear function is great because it is simply the difference between the start and end value, plus the start value.

Object.entries(this.endState).forEach(([key, value]) => {
    const startValue = this.startState[key];
    const endValue = this.endState[key];

    /**
     * Example of a tween between 100 to 200 at 20% through the animation
     * 
     * value = 100 + ((200 - 100) * 0.25)
     * value = 125
     * 
     * This means we can add 125px to the given attribute
     */
    const value = startValue + ((endValue - startValue) * percentageElapsed);
    // Assuming pixels for now
    this.element.style[key] = `${value}px`;
}

// The animation is not finished, so request the next frame
requestAnimationFrame((...args) => this.onTick(...args));

Stopping

To stop the animation, all we need to do is set the running flag to be false. This will already prevent the next tick from running, as well as preventing the next frame from being requested.

stop() {
    this.running = false;
}

Try the below demo with the two actions we have implemented so far.

Starting from where we stopped

To start from where the animation was stopped we need to do a few things:

  1. Set the running flag back to true
  2. Request the next animation frame
  3. Set the start time to be now, minus any elapsed time

This will let the animation think it has already elapsed the same amount of time as was done before it was stopped.

start() {
    this.running = true;

    requestAnimationFrame((time) => {
        // start animation from where it was paused
        const elapsed = this.lastTickTime - this.startTickTime;
        this.startTickTime = time - elapsed;
        this.onTick(time);
    });
    
}

Try the below demo with all the actions we have now implemented. See how start does the animation from where it was stopped? Versus restart which starts it from the beginning?

And with that all the actions that I wanted to make are complete - so lets add support for some easing functions.

Making multiple easing functions

In the example above we used the basic linear easing function as it is the easiest to understand. To make a different easingfunction we need to take the given percentage an animation has complete so far, and convert it to a different value.

A common way of doing this is through the Math.sin or Math.cos functions. They will give us the position of a wave at a given point. For example, the value grows from 0 to 1 when given values between 0 and π.

We can use this by multiplying our percentage by Math.PI to normalise it to this wave. A few example values are:

  • Math.sin(0 * Math.PI) is 0
  • Math.sin(0.1 * Math.PI) is 0.31
  • Math.sin(0.4 * Math.PI) is 0.95
  • Math.sin(0.5 * Math.PI) is 1
  • Math.sin(0.4 * Math.PI) is 0.95
  • Math.sin(0.9 * Math.PI) is 0.31
  • Math.sin(1 * Math.PI) is 0

You can see that it quickly goes away from 0, then slows down before 0.5, then follows that change in reverse back to 0.

I won’t into more depth here about easing as the lovely easings.net gives the functions for all its easing examples.

For instance, their easeInOutSine looks like:

function easeInOutSine(x: number): number {
    return -(Math.cos(Math.PI * x) - 1) / 2;
}

We can use this by changing the percentageElapsed to easeInOutSine(percentageElapsed)

Support multiple unit types

To support multiple unit types, all we need to do is strip the unit off the start/end start values when processing, then add them back on when applying the style. To get the numeric value, I have written a function to remove any non-numeric character (excluding decimal points) that then parses the value to a float.

To get the unit - I have done the opposite with the regex, and stripped off any numeric characters (including decimal points).

// Uses regex to remove any non-numeric part of the value
const stripUnit = (value: string) =>
    parseFloat(value.replace(/[^\d.-]/g, ''));

// Use regex to remove any numeric part of the value
const getUnit = (value: string) => {
    const unit = value.replace(/[\d.-]/g, '');

    // Default to pixels if no unit found
    return unit.length > 0 ? unit : 'px';
}

As the start and end value should have the same unit, we can get the unit once and then strip the values. This unit is then stored and applied any time we update the style.

// When getting the values
const unit = getUnit(this.startState[key].toString());
const startValue = stripUnit(this.startState[key].toString());
const endValue = stripUnit(this.endState[key].toString());

// When setting the values
this.element.style[key] = `${value}${unit}`;

Full Code

You can find the code at Github. There are a few little tweaks and additions I have done but the core code works in exactly the same way. Some additional changes I made are:

  • Added an option for infinite animations
  • Converted some animations to transforms

Here is a showcase of the final product.



What to read more? Check out more posts below!