Useful GSAP utilities

I covered in a recent article about GSAP's function based properties, and gave some simple use cases for what that can do.

GSAP also provides a collection of utility functions that save you from writing a bunch of math and allow you to skip right to the fun, creative stuff.

Keyframes

Sometimes in an animation, you'll be animating a single element through a bunch of different values. The normal way to do it is to string a bunch of .to tweens together, and you're all good

gsap.to('.my-element', {
  y: 10
})
.to('.my-element', {
  y: 20
})
// and so on...

And this approach can work if what you're doing is relatively simple. But can get a bit difficult if you're working with staggers and more advanced GSAP concepts.

To bypass that, keyframes are used.

Here's how the basic keyframe setup works:

The basic setup is as follows:

  1. Create a gsap tween
gsap.to('.my-element', {})
  1. Pass it the keyframe property + config object
gsap.to('.my-element', {
  keyframes: {
    // This is where you put your animation values
  }
})
  1. Add the property + an array of values you want to animate through
gsap.to('.my-element', {
  keyframes: {
    x: ['200', '10', '-20'],
    y: ['10', '-10', '200'],
    color: ['red', 'blue', '#fff']
  }
})

That would get you the same result as chaining a bunch of .to tweens together.

But what's the point of having/knowing a fancy GSAP utility if it can be done with basic tweens?

Staggers.

Let's take this example. For a recent client project, I had to do a scrolling text effect where the text fills with a gradient as you scroll

When I tried to chain the .to tweens together, I got some really buggy interactions. Because on reverse (or scroll up), the tweens didn't cycle through in the exact same order. If a tween was half finished it would go back and then get stuck on a colour.

Here's a snippet from the code so you can get an idea of why this was useful:

// create our scroll trigger timeline
let tl = gsap.timeline({
  scrollTrigger: {
    trigger: textContain,
    start: 'center center',
    // here I wanted it to be a consistent speed regardless of
    // paragraph length so I tweaked the value here till it felt natural
    end: `+=${split.lines.length * 65}`,
    scrub: true,
    pin: true,
    pinSpacing: true,
  },
});

// Animate our characters
tl.to(wrapSelector('.char'), {
  // Create the keyframe property and its config object
  keyframes: {
    // pass through a series of colours we will animate through
    color: [
      'rgba(10, 19, 25, 0.2)',
      'rgba(109, 145, 211, 1)',
      'rgba(5, 22, 64, 1)'
    ],
  },
  // stagger it so it's got that gradient look
  stagger: {
    each: 0.01,
  },
});

So here, the animation was all one tween. I included the initial value so we could scroll backwards and everything worked a dream.

Pipe

I couldn't decide whether to put this one first or last.

Pipe is a utility that lets you connect as many other functions as you want, as long as you're passing the values from 1 to the next.

When you call it, it will return a function that will take 1 parameter. The parameter will get passed to the first function in the pipeline. Then whatever value is returned from function 1 gets passed as a parameter to function 2 and so on.

let { pipe } = gsap.utils;

let example = pipe(
  function1(param1, param2),
  function2(param3, param4)
);

example(1);

In this example, function 1 will have param1, param2 and also have the value of '1' passed through it also.

Then function2 will have param3, param4 and the value of function1 passed to it.

The reason it's so powerful is because it lets you create patterns for your animation. Converting or manipulating values or progress.

Here's an example that connects mouse move to the background color of the body. It uses some utilities we'll dive into later.

// first lets make our utils easier to get a hold of
const { pipe, normalize, clamp, interpolate } = gsap.utils;

// lets create a function that converts a number (our mouse position) into a color between orange and green.
// We start with pipe, as this is the glue that holds the other functions together and passes their values down the chain.
let yToColor = pipe(
  // We clamp the value between 0 and the windows height (this is a good practise to save us from abnormalities that could break the code)
  clamp(0, window.innerHeight),
  // We then use normalize to convert the clamped number into a progress value between 0 and 1
  normalize(0, window.innerHeight),
  // we then use that progress value to give us our color between orange and green.
  interpolate(['orange', 'green'])
);

// Update on mouse move
window.addEventListener('mousemove', (e) => {
  const color = yToColor(e.clientY);
  gsap.set('body', {
    backgroundColor: color
  });
});

// Update on resize to keep height accurate
window.addEventListener('resize', () => {
  yToColor = pipe(
    clamp(0, window.innerHeight),
    normalize(0, window.innerHeight),
    interpolate(['orange', 'green'])
  );
});

Don't worry if it doesn't make a tonne of sense right now. We'll use it a lot so you'll learn through exposure

Clamp

Ah... now onto something familiar if you use CSS. The gsap clamp utility is exactly the same as the css equivalent.

let { clamp } = gsap.utils;

// The syntax is as follows
// clamp(lowestValue, highestValue, valueToClamp).

clamp(1, 5, 6); // = 5
clamp(1, 5, 0); // = 1
clamp(1, 5, 2); // = 2

Don't need to write much more than that.

Normalise

Normalise lets you convert a value between 2 points to a percent. Lets say you've got an element that you're tracking mouseX position of, the far left is 0%, the far right is 100%. How would you convert that to a percentage?

Like this?

// Get your element
const el = document.querySelector('.element');

// Add your event listener
el.addEventListener('mousemove', (e) => {
  // Get the element's rect, get the x and convert it to a percentage
  const rect = el.getBoundingClientRect();
  const x = e.clientX - rect.left;
  const percentage = Math.min(Math.max(x / rect.width, 0), 1);
});

Which is perfectly fine. But why not make it just a bit cleaner?

The normalise syntax works as follows:

let { normalize } = gsap.utils;

// syntax as follows: normalize(lowestNumber, highestNumber, numberToConvert)
normalize(0, 10, 5);    // = 0.5
normalize(0, 100, 50);  // = 0.5

// If you leave out the final value it returns a function that lets you reuse your normalize values.
// For example
let converter = normalize(12, 36);
converter(24); // = 0.5

So you could achieve the same effect with:

let { normalize } = gsap.utils;

let el = document.querySelector('.element');

el.addEventListener('mousemove', (e) => {
  let rect = el.getBoundingClientRect();
  let toPercent = normalize(rect.left, rect.right, e.clientX);
});

Interpolate

now THIS. This is the one that matters most if I'm being honest.

Interpolate will return a value, inbetween 2 values that you pass to it + a progress (see how useful normalise becomes now?). Those values can be anything (within reason).

Lets do some examples, ranging in usefulness and complexity.

let { interpolate } = gsap.utils;

// from 0 to 100, with a progress of 0.5 (50%)
interpolate(0, 100, 0.5); // returns 50

// Pretty simple stuff. Not all that useful yet.

// from red to blue with a progress of 0.5
interpolate('red', 'blue', 0.5); // "rgb(128, 0, 128)" (purple)

interpolate('2rem', '20rem', 0.5); // "11rem"

Now if we revisit the simple mouse animation from earlier. It should make more sense.

We pipe -> clamp -> normalise -> interpolate

// first lets make our utils easier to get a hold of
const { pipe, normalize, clamp, interpolate } = gsap.utils;

// lets create a function that converts a number (our mouse position) into a color between orange and green.
// We start with pipe, as this is the glue that holds the other functions together and passes their values down the chain.
let yToColor = pipe(
  // We clamp the value between 0 and the window's height (this is a good practise to save us from abnormalities that could break the code)
  clamp(0, window.innerHeight),
  // We then use normalize to convert the clamped number into a progress value between 0 and 1
  normalize(0, window.innerHeight),
  // We then use that progress value to give us our color between orange and green.
  interpolate(['orange', 'green'])
);

// Update on mouse move
window.addEventListener('mousemove', (e) => {
  const color = yToColor(e.clientY);
  gsap.set('body', {
    backgroundColor: color
  });
});

// Update on resize to keep height accurate
window.addEventListener('resize', () => {
  yToColor = pipe(
    clamp(0, window.innerHeight),
    normalize(0, window.innerHeight),
    interpolate(['orange', 'green'])
  );
});

Thanks for coming, folks 👋