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) { blender = blender || Power4.easeInOut; return new Ease(function(v) { var b = blender.getRatio(v); return startEase.getRatio(v) * (1 - b) + endEase.getRatio(v) * b; }); } //example usage: TweenMax.to("#target", 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 = new TimelineLite({onComplete:vars.onComplete, delay:vars.delay || 0}), duration = vars.duration || 1, copy = {cycle:vars.cycle || {}}, i, b, p; for (i = 0; i < elements.length; i++) { bounds[i] = elements[i].getBoundingClientRect(); } changeFunc(); for (p in vars) { if (p !== "duration" && p !== "onComplete" && p !== "delay") { copy[p] = vars[p]; } } copy.cycle.x = function(i, element) { return "-=" + (element.getBoundingClientRect().left - bounds[i].left); }; copy.cycle.y = function(i, element) { return "-=" + (element.getBoundingClientRect().top - bounds[i].top); } tl.staggerFrom(elements, vars.duration || 1, copy); return tl; }
DEMO
Find child animation by id
Use the function below to search through all of a timeline's children (including nested ones) to find an element with a particular id:
function getChildById(timeline, id) { var tween = timeline._first, sub; while (tween) { if (tween.vars.id === id) { return tween; } else if (!(tween instanceof TweenLite)) { sub = getChildById(tween, id); if (sub) { return sub; } } tween = tween._next; } }
Animating backgroundSize:"cover" or "contain"
I was asked about animating to/from backgroundSize: "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. The forums post says it all (and contains the helper function): https://greensock.com/forums/topic/18243-split-text-confused/?tab=comments#comment-84134
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:
TweenMax.to(sprite, 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(); TweenLite.set(element, {x:"+=" + (before.left - after.left), y:"+=" + (before.top - after.top)}); }
DEMO
That's it for now. Happy tweening!