Jump to content
GreenSock

Over the years Jack has "whipped together" various GSAP-related helper functions for community members, so we gathered them into one place (below) for convenience. We'll keep adding relevant helper functions here as we craft them. Enjoy!

Blend two eases

If you need one ease at the start of your animation, and a different one at the end, you can use this function to blend them!

//just feed in the starting ease and the ending ease (and optionally an ease to do the blending), and it'll return a new Ease that's...blended!
function blendEases(startEase, endEase, blender) {
    var parse = function(ease) {
            return typeof(ease) === "function" ? ease : gsap.parseEase("Power4.easeInOut");
        },
        s = gsap.parseEase(startEase),
        e = gsap.parseEase(endEase),
        blender = parse(blender);
    return function(v) {
      var b = blender(v);
      return s(v) * (1 - b) + e(v) * b;
    };
}
//example usage:
gsap.to("#target", {duration: 2, x: 100, ease: blendEases("Back.easeIn.config(1.2)", "Bounce.easeOut")});

DEMO

If you need to invert an ease instead, see this demo for a different helper function.

Pluck random elements from an array until it's empty...then start over

Randomly pluck values from an array one-by-one until they've all been plucked (almost as if when you pluck one, it's no longer available to be plucked again until ALL of them have been uniquely plucked):

function pluckRandomFrom(array) {
  if (!array.eligible || array.eligible.length === 0) {
    array.eligible = array.slice(0); //make a copy, attach it as "eligible" property
    array.eligible.sort(function() { return 0.5 - Math.random(); }); //shuffle
  }
  return array.eligible.pop();
}

All you've gotta do is feed the array in each time and it keeps track of things for you!

Alternatively, if you just want to pull a random element from an array that's not the PREVIOUS one that was pulled (so not emptying the array, just pulling randomly from it while ensuring the same element isn't pulled twice in a row), you can use this:

function getRandomFrom(array) {
  var selected = array.selected;
  while (selected === (array.selected = Math.floor(Math.random() * array.length))) {};
  return array[array.selected];
}

FLIP

If you're shifting things around in the DOM and then you want elements to animate to their new positions, you can use this helper function (see the comments at the top to learn how to use it):

/* 
This is the function that does all the magic. Copy this to your project. Pass in the elements (selector text 
or NodeList or array), then a function/callback that actually makes your DOM changes, and optionally a vars 
object that contains any of the following properties to customize the transition:

 - duration [Number] - duration (in seconds) of each animation
 - stagger [Number | Object | Function] - amount to stagger the starting time of each animation. You may use advanced staggers too (see https://codepen.io/GreenSock/pen/jdawKx)
 - ease [Ease] - controls the easing of the animation. Like Power2.easeInOut, or Elastic.easeOut, etc.
 - onComplete [Function] - a callback function that should be called when all the animation has completed.
 - delay [Number] - time (in seconds) that should elapse before any of the animations begin. 

This function will return a TimelineLite containing all the animations. 
*/
function flip(elements, changeFunc, vars) {
  if (typeof(elements) === "string") {
    elements = document.querySelectorAll(elements);
  }
  vars = vars || {};
  var bounds = [],
      tl = gsap.timeline({onComplete: vars.onComplete, delay: vars.delay || 0}),
      copy = {},
      i, b, p;
  for (i = 0; i < elements.length; i++) {
    bounds[i] = elements[i].getBoundingClientRect();
  }
  changeFunc();
  for (p in vars) {
    if (p !== "onComplete" && p !== "delay") {
      copy[p] = vars[p];
    }
  }
  copy.x = function(i, element) {
    return "-=" + (element.getBoundingClientRect().left - bounds[i].left);
  };
  copy.y = function(i, element) {
    return "-=" + (element.getBoundingClientRect().top - bounds[i].top);
  }
  tl.from(elements, copy);
  return tl;
}

DEMO

Animating backgroundSize:"cover" or "contain"

I was asked about animating to or from a backgroundSize of "cover" or "contain" with GSAP. The problem: GSAP interpolates between numbers, but how is it supposed to interpolate between something like "300px 250px" and "contain" (not a number)? So I whipped together a function that basically translates "contain" or "cover" into their px-based equivalents for that particular element at whatever size it is then. Once we've got it converted, it's easy to animate.

//this function converts the backgroundSize of an element from "cover" or "contain" or "auto" into px-based dimensions. To set it immediately, pass true as the 2nd parameter.
function getBGSize(element, setInPx) {
    var e = (typeof(element) === "string") ? document.querySelector(element) : element,
            cs = window.getComputedStyle(e),
            imageUrl = cs.backgroundImage,
            size = cs.backgroundSize,
            image, w, h, iw, ih, ew, eh, ratio;
    if (imageUrl && !/\d/g.test(size)) {
        image = new Image();
        image.setAttribute("src", imageUrl.replace(/(^url\("|^url\('|^url\(|"\)$|'\)$|\)$)/gi, "")); //remove any url() wrapper. Note: some browsers include quotes, some don't.
        iw = image.naturalWidth;
        ih = image.naturalHeight;
        ratio = iw / ih;
        ew = e.offsetWidth;
        eh = e.offsetHeight;
        if (!iw || !ih) {
            console.log("getBGSize() failed; image hasn't loaded yet.");
        }
        if (size === "cover" || size === "contain") {
            if ((size === "cover") === (iw / ew > ih / eh)) {
                h = eh;
                w = eh * ratio;
            } else {
                w = ew;
                h = ew / ratio;    
            }
        } else { //"auto"
            w = iw;
            h = ih;
        }
        size = Math.ceil(w) + "px " + Math.ceil(h) + "px";
        if (setInPx) {
            e.style.backgroundSize = size;
        }
    }
    return size;
}

DEMO

SplitText lines in nested elements

SplitText doesn't natively support splitting nested elements by "lines", but if you really need that we've put together a helper function for it.

function nestedLinesSplit(target, vars) {
    var split = new SplitText(target, vars),
        words = (vars.type.indexOf("words") !== -1),
        chars = (vars.type.indexOf("chars") !== -1),
        insertAt = function(a, b, i) { //insert the elements of array "b" into array "a" at index "i"
            var l = b.length,
                j;
            for (j = 0; j < l; j++) {
                a.splice(i++, 0, b[j]);
            }
            return l;
        },
        children, child, i;

    if (typeof(target) === "string") {
        target = document.querySelectorAll(target);
    }
    if (target.length > 1) {
        for (i = 0; i < target.length; i++) {
            split.lines = split.lines.concat(nestedLinesSplit(target[i], vars).lines);
        }
        return split;
    }

    //mark all the words and character 
elements as _protected so that we can identify the non-split stuff. children = (words ? split.words : []).concat(chars ? split.chars : []); for (i = 0; i < children.length; i++) { children[i]._protect = true; } children = split.lines; for (i = 0; i < children.length; i++) { child = children[i].firstChild; //if the first child isn't protected and it's not a text node, we found a nested element that we must bust up into lines. if (!child._protect && child.nodeType !== 3) { children[i].parentNode.insertBefore(child, children[i]); children[i].parentNode.removeChild(children[i]); children.splice(i, 1); i += insertAt(children, nestedLinesSplit(child, vars).lines, i) - 1; } } return split; } //used like var mySplitText = nestedLinesSplit(assetTexts, {type: "lines"});

DEMO

ThreePlugin (for THREE.js)

var _gsScope = (typeof module !== "undefined" && module.exports && typeof global !== "undefined") ? global : this || window;
(_gsScope._gsQueue || (_gsScope._gsQueue = [])).push(function () {
    "use strict";
    var _xyzContexts = "position,scale,rotation".split(","),
        _contexts = {x:"position", y:"position", z:"position"},
        _DEG2RAD = Math.PI / 180,
        _visibleSetter = function(target, start, end) {
            var time = end ? 0 : 0.999999999;
            return function(ratio) {
                var value = (ratio > time) ? end : start;
                if (target.visible !== value) {
                    target.visible = value;
                    target.traverse(function (child) {
                        child.visible = value;
                    });
                }
            };
        },
        _addFuncPropTween = function(tween, func) {
            var proxy = {setRatio: func},
                backward = !!tween.vars.runBackwards,
                pt = {_next:tween._firstPT, t:proxy, p:"setRatio", s:backward ? 1 : 0, f:1, pg:0, n:"setRatio", m:0, pr:0, c:backward ? 0 : 1};
            tween._firstPT = pt;
            if (pt._next) {
                pt._next._prev = pt;
            }
            return pt;
        },
        _degreesToRadians = function(value) {
            return (typeof(value) === "string" && value.charAt(1) === "=") ? value.substr(0, 2) + (parseFloat(value.substr(2)) * _DEG2RAD) : value * _DEG2RAD;
        }, i, p;
    for (i = 0; i < _xyzContexts.length; i++) {
        p = _xyzContexts[i];
        _contexts[p + "X"] = p;
        _contexts[p + "Y"] = p;
        _contexts[p + "Z"] = p;
    }
    var ThreePlugin = _gsScope._gsDefine.plugin({
        propName: "three",
        priority: 0,
        API: 2,
        version: "0.0.2",
        init: function (target, values, tween, index) {
            var context, axis, value, p, i, m;
            for (p in values) {
                context = _contexts[p];
                value = values[p];
                if (typeof(value) === "function") {
                    value = value(index || 0, target);
                }
                if (context) {
                    i = p.charAt(p.length-1).toLowerCase();
                    axis = (i.indexOf("x") !== -1) ? "x" : (i.indexOf("z") !== -1) ? "z" : "y";
                    this._addTween(target[context], axis, target[context][axis], (p.indexOf("rotation") !== -1) ? _degreesToRadians(value) : value, p);
                } else if (p === "scale") {
                    this._addTween(target[p], "x", target[p].x, value, p + "X");
                    this._addTween(target[p], "y", target[p].y, value, p + "Y");
                    this._addTween(target[p], "z", target[p].z, value, p + "Z");
                } else if (p === "opacity") {
                    m = (target.material.length) ? target.material : [target.material];
                    i = m.length;
                    while (--i > -1) {
                        m[i].transparent = true;
                        this._addTween(m[i], p, m[i][p], value, p);
                    }
                } else if (p === "visible") {
                    if (target.visible !== value) {
                        _addFuncPropTween(tween, _visibleSetter(target, target.visible, value));
                    }
                } else {
                    this._addTween(target, p, target[p], value, p);
                }
                this._overwriteProps.push(p);
            }
            return true;
        }
    });

}); if (_gsScope._gsDefine) { _gsScope._gsQueue.pop()(); }

Just load the plugin above once and then you'll be able to put everything in a "three: {}" object, like:

gsap.to(sprite, {duration: 2, three: {scaleX: 2, scaleY: 1.5, x: 200, y: 100, opacity: 0.5}, ease: "Power2.easeInOut"});

It supports:

  • x, y, z (like position.x, position.y, position.z)
  • scaleX, scaleY, scaleZ, or just "scale" to do all 3 at once (like scale.x, scale.y, scale.z)
  • rotationX, rotationY, rotationZ (like rotation.x, rotation.y, rotation.z) - IMPORTANT: as a convenience, you define them in degrees, not radians!
  • opacity
  • supports function-based values.

Change transformOrigin without a jump

If you want to change transformOrigin dynamically without a jump, you'd need to compensate its translation (x/y). Here's a function I whipped together for that purpose:

function smoothOriginChange(element, transformOrigin) {
  if (typeof(element) === "string") {
    element = document.querySelector(element);
  }
  var before = element.getBoundingClientRect();
  element.style.transformOrigin = transformOrigin;
  var after = element.getBoundingClientRect();
  gsap.set(element, {x:"+=" + (before.left - after.left), y:"+=" + (before.top - after.top)});
}

DEMO

That's it for now. Happy tweening!

Copyright 2017, GreenSock. All rights reserved. This work is subject to theterms of useor for Club GreenSock members, the software agreement that was issued with the membership.
×