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
- Pluck random elements from an array until it's empty...then start over
- FLIP
- Animating backgroundSize:"cover" or "contain"
- SplitText lines in nested elements
- ThreePlugin (for THREE.js)
- Change transformOrigin without a jump
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 characterelements 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!