Jump to content
Search Community

How to set animation properties for a specific point in the progress of a tween?

invisibleEase test
Moderator Tag

Go to solution Solved by GreenSock,

Recommended Posts

Coming from CSS, I am used to being able to control what happens at a specific point during an animation with keyframe percentages. This is particularly helpful when the change I want to animate doesn't follow a linear pattern from state A to state B, but rather has some variation in between.

 

So, for example, I can do something like this:

 

@keyframes irregular-animation {
  0% { 
    transform: scale(0);
    opacity: 0;
  }
  75% { 
    transform: scale(1.25);
  }
  100% { 
    transform: scale(1);
    opacity: 1;
  }
}

However, I haven't managed to figure out how to do the same with GSAP.

In GSAP, it seems I only have control of the start and end states (like with toFrom), but nothing in between.

 

I know I can use custom ease functions to achieve somewhat of a similar effect to the one in the snippet above (whereby I design the easing curve to overextend a little), but this is far from ideal since I often only want to change a single property in this irregular way, not the entire tween.

 

Notice how, in the snippet above, the opacity follows a linear transformation, and it would not make sense to overextend it (even though, for this example, an opacity of, let's say, 1.5 at 75% wouldn't break the animation). Also, it's hard to manipulate a custom curve such that a property reaches a specific value exactly after 75% of an animation is done (or an even less intuitive percentage, like, say, 27%).

 

Timelines are also not ideal for this (from what I've tried, at least), because I want the entire animation to follow along the same easing curve (which is not the same as reusing the same ease in each tween) and, again, it's tricky to calculate when exactly 75% or some other percentage of the overall animation will be done. But maybe I'm missing something...

 

If you had to recreate the simple animation in the CodePen I linked using GSAP, how would you do it? Is it possible to control the values for properties at a specific point during an animation that resembles what we can accomplish with @keyframes in CSS?

 

Thank you for reading. I appreciate any help.

 

 

 

See the Pen zYzXEXB by markpiero (@markpiero) on CodePen

Link to comment
Share on other sites

Welcome to the forums, @invisibleEase!

 

Here are a few options: 

See the Pen GRELQBQ?editors=0010 by GreenSock (@GreenSock) on CodePen

 

Also keep in mind that if you want something to overshoot the destination value and come back, you can actually configure a "back" ease to any amount you want (of overshoot), like "back(5)", so you could skip the tween to the intermediate value. But if you need it to hit a very particular number (scale: 1.25 in this case), it's totally fine to just create a tween that goes to that intermediate value, and then another tween to go to the final value like in the demo. 

 

I really think that if you stick with it through the learning curve, you'll see why so many people who have switched to GSAP from CSS say they'll never go back. GSAP just gives you soooo much more control and makes your code more modular/maintainable for anything remotely complex. And of course there are a ton of things GSAP can do that are simply impossible with CSS. I don't mean to say CSS is "bad" or always inferior at all - sometimes for very simple things CSS is perfectly adequate. But there are compelling reasons why so many of the top animators in the world (and over half of awwward-winning sites) use GSAP. :)

 

Enjoy!

  • Like 1
Link to comment
Share on other sites

Thank you for your detailed response! I really appreciate that you even shared several ways of achieving a similar effect.

 

I love GSAP and writing animations with it feels very natural.

 

For animations like the one in this post, though, it seems very impractical. From what I can see, you now basically have to juggle with the duration of each tween to try to achieve a similar effect. Updating the point at which something happens would mess up the entire animation and you'd have to recalculate the durations of the other tweens to accommodate the change. I imagine you could write some helper functions to set the durations of related tweens automatically for you, but this can quickly get overly obscure for someone unfamiliar with the animation to understand at a glance.

 

I am not sure if this is the right place to suggest an improvement, but it would be phenomenal if you could write tweens like this:

 

timeline.to(".element", {
  opacity: 1,
  scale: {
    "0%": 0.5,
    "75%": 1.25,
    "100%": 1
  }
});

 

That's so much cleaner and easier to understand, isn't it?

 

Anyway, I really like GSAP so I'll definitely continue to use it. I'll just have to keep using CSS keyframes for animations like this, though.

 

Please let me know if I should move my suggestion somewhere else so it can get to the attention of the right people.

 

Thank you again.

Link to comment
Share on other sites

Yeah, crafting APIs can be really tricky because it's super tempting to just have one particular use case in mind and code for that, but then you run into a bunch of other use cases where it gets very sticky. The CSS keyframes API is an example of that - most animators find it incredibly cumbersome and limiting EXCEPT for the exact scenario you happen to be facing here. Building out full-blown sequences with any complexity can be super annoying. 

 

A few example questions/request we'd likely get with your proposed API: 

  1. What if I want to control the ease between each keyframe? 
  2. What if I want multiple properties that use the same percentages? Do I have to define them all over and over again (once for each property)? 
  3. What if I want to call a function (or pause) at a certain percentage? 
  4. How can I control the ease of the overall progress? If we let them set an "ease" on the tween itself (which would be intuitive), what would happen if they use an ease that exceeds the "legal" progress values of 0 - 1 (like elastic or back)? 

It can just get messy. 

 

That being said, here's a plugin that'd let you do exactly what you requested, but just tucking it into a "keys" object:

See the Pen GREaRZB?editors=0010 by GreenSock (@GreenSock) on CodePen

 

The plugin code: 

gsap.registerPlugin({
	name: "keys",
	init(target, vars, tween) {
		let tl = this.tl = gsap.timeline(),
			add = (prop, obj) => {
				let a = [],
					p;
				for (p in obj) {
					p === "ease" || a.push({t: parseFloat(p), v: obj[p]});
				}
				a.sort((a, b) => a.t - b.t).forEach((kf, i) => {
					let v = {ease: obj.ease || "none", duration: kf.t - (i ? a[i - 1].t : 0)};
					v[prop] = kf.v;
					tl.to(target, v);
				});
				tl.duration() < 100 && tl.to({}, {duration: 100 - tl.duration()});
			},
			p
		this.tween = tween;
		this.ease = gsap.parseEase(vars.ease || "none");
		for (p in vars) {
			p === "ease" || add(p, vars[p]);
		}
	},
	render(ratio, {tl, tween, ease}) {
		tl.progress(ease(tween.progress()));
	}
});

It uses a linear ease by default between each keyframe, but you can set an "ease" in each property and/or you can set an ease on the main object to control the overall ease across all the keyframes: 

gsap.to(".gsap", {
	opacity: 1,
	keys: {
		scale: {
			"0%": 0.5,
			"75%": 1.25,
			"100%": 1,
			ease: "power1.inOut" // between each keyframe
		},
		ease: "none" // overall playhead ease across entire sequence of keyframes
	},
	duration: 3,
	repeat: -1,
	yoyo: true
});

I used "keys" instead of "keyframes" just because GSAP already has a "keyframes" feature built-in with a different syntax that allows a greater level of customization including gaps and overlapping. 

 

If enough people show interest in this, we could consider either releasing it as an official plugin or **MAYBE** put it into the core as an alternate behavior for "keyframes" (it could sense if it's an Array or Object and respond accordingly), but I'm always VERY reluctant to add anything to the core that eats up kb and might only be used by a tiny fraction of our users. 

 

Curious to hear feedback from anyone. 

  • Like 4
Link to comment
Share on other sites

Enhancement: I added the ability to define values in an Array for a more simplistic way to just go to certain values, split equally across the whole animation, like:

keys: {
  scale: [1, 0.5, 1.5, 1]
}

To be clear, that first value in the Array isn't a "from" value - it's the first destination. So, for example, if scale is 0 when that tween renders for the first time, it'd go 0 > 1 > 0.5 > 1.5 > 1 over the course of the whole tween. 

 

So with this syntax, it gives you the flexibility to use the percent-based values when you need them spaced non-evenly or to use a non-linear ease, -OR- use the shorter Array-based syntax for evenly-spaced linear values. 👍

 

I'm not set on the "keys" name, so if anyone has a better idea, I'm all ears.

 

New plugin: 

gsap.registerPlugin({
	name: "keys",
	init(target, vars, tween) {
		let tl = this.tl = gsap.timeline(),
			add = (prop, obj) => {
				let a = [],
					p;
				if (Array.isArray(obj)) {
					obj.forEach((value, i) => a.push({t: (i + 1) / obj.length * 100, v: value}));
				} else {
					for (p in obj) {
						p === "ease" || a.push({t: parseFloat(p), v: obj[p]});
					}
					a.sort((a, b) => a.t - b.t);
				}
				a.forEach((kf, i) => {
					let v = {ease: obj.ease || "none", duration: kf.t - (i ? a[i - 1].t : 0)};
					v[prop] = kf.v;
					tl.to(target, v);
				});
				tl.duration() < 100 && tl.to({}, {duration: 100 - tl.duration()});
			},
			p
		this.tween = tween;
		this.ease = gsap.parseEase(vars.ease || "none");
		for (p in vars) {
			p === "ease" || add(p, vars[p]);
		}
	},
	render(ratio, {tl, tween, ease}) {
		tl.progress(ease(tween.progress()));
	}
});

 

  • Like 6
Link to comment
Share on other sites

Pretty neat Jack. I can see this being a pretty handy little plugin. I did a little test run comparing some of the usual ways I write an overshoot. ease:back is my usual quick choice, multiple tweeens when I need a specific overshoot point and a custom ease when I want to really slingshot it. Having another option in the toolbox would be good. It's nice and quick to change values too. 👍

 

See the Pen XWgwjjZ by PointC (@PointC) on CodePen

 

My vote would be for a plugin rather than in the core. No idea on the "keys" name. 🤷‍♂️

 

Cool stuff.

  • Like 4
Link to comment
Share on other sites

It would be really handy for people that are used to CSS animations.

I remember having to swop over some banner ads that were written with CSS keyframes back in the day, it was a nightmare...

Saying that though it did help me learn GSAP, and the GSAP code was a lot easier to adjust afterwards than the keyframes.

  • Like 1
Link to comment
Share on other sites

Thank you for sharing that keys plugin, Jack.

 

I understand the goal should be to keep APIs lean and simple, so ultimately it's up to how many people would use something like this to determine if it's worth adding it to the core.

 

Anyway, I've thought about the issues you anticipated could arise with my suggested implementation, and have made some corrections to my idea.

 

We can think of regular gsap.to tweens as having an implicit 100% keyframe assignment (since all the animated properties inside will reach their written values when the tween's playhead reaches 100%). Building on that idea, we could add support for "keyframe" properties like this:

gsap.to(".element", {
  "0%": {
    scale: 0.5,
    opacity: 0
    // ...if the "0%" key doesn't exist in the tween, the default behavior of
    // deriving the starting values from the current property values of the
    // target element persists
  },
  
  "75%": {
    scale: 1.25
    // ...if there are no 'in-between' n% keys denoting keyframe steps, the
    // tween eases to the end state as usual
  },
  
  scale: 1,
  opacity: 1,
  // ...for values that should be reached at 100% of the tween's progress, they
  // don't have to be nested inside an "n%" key if we're using a gsap.to
  // tween. 
  // The reverse is true for gsap.from. In that case, what happens at
  // 0% of the tween's progress doesn't need to be nested inside any key.
  // This behavior would make current GSAP animations compatible with the new
  // API by default.
  
  duration: 3,
  // only the tween can have a duration, but keyframes cannot since their starts
  // are relative to the progress in the duration of their containing tween
  // (that's why they're defined with percentages)
  
  ease: "power1.inOut",
  // if the tween has an ease, any ease a user may have defined inside a child
  // keyframe (n% property) is ignored because they're basically overwriting each
  // other. But, if the parent tween doesn't have its ease property defined,
  // then each keyframe could have an ease property with a custom easing curve
  // which would then be animated with an implied duration derived from the
  // end of the previous keyframe and the duration of the parent tween.
  // (Check out the next code snippet for an example of this)
});

 

On 10/3/2021 at 3:23 AM, GreenSock said:

What if I want multiple properties that use the same percentages? Do I have to define them all over and over again (once for each property)? 

This new syntax addresses this problem because now you just define the percentage you want to add custom values for once, and then you can nest as many properties as you want to animate inside.

 

On 10/3/2021 at 3:23 AM, GreenSock said:

What if I want to call a function (or pause) at a certain percentage? 

Keyframe properties (n% keys) could have an onReached() callback function that lets you run any logic you want whenever that percentage of the animation is reached. You could also use that hook to run something like this.pause()   to pause the animation at that point and, if you stored the tween in a variable when you defined it, you could then resume it as usual with animation.resume() . 

 

On 10/3/2021 at 3:23 AM, GreenSock said:

How can I control the ease of the overall progress? If we let them set an "ease" on the tween itself (which would be intuitive), what would happen if they use an ease that exceeds the "legal" progress values of 0 - 1 (like elastic or back)? 

If the tween has an ease, it will affect all transitions between each keyframe percentage, just like setting an ease on a CSS animation does, so eases defined inside the keyframes in that tween would be ignored.

If a tween with keyframes has a linear ease and a duration of 10 seconds, then the values defined for the "70%" keyframe will be reached exactly after 7 seconds. But, if it has a different ease (like power.inOut or even the more fancy ones like elastic), then 70% of the animation could be reached much faster or slower than 7 seconds. This is how CSS animations already work.

 

On 10/3/2021 at 3:23 AM, GreenSock said:

What if I want to control the ease between each keyframe? 

If you don't assign an ease to the entire tween, you could do it like this:

 

gsap.to(".element", {
  duration: 10,
  
  "0%": {
    scale: 0.5,
    opacity: 0
    ease: "power1.in"
    // ...this ease would have no effect becase the inferred duration
    // between the start of the tween (0s) and the start of this keyframe
    // (0% of 10s = 0s) is 0, so it's basically the same as a tween with
    // no duration.
    // Adding an ease here probably wouldn't break the animation, but
    // it'd be best not to add it
  },
  
  "70%": {
    scale: 1.25
    ease: "power3.out"
    // ...this ease would have an inferred duration of 7s, which is the difference between
    // the start of the previous keyframe (0% = 0s) and this one (70% of 10s = 7s)
  },
  
  "100%": {
    scale: 1,
    opacity: 1,
    ease: "elastic"
    // ...to add an ease to the last fragment of the animation, we could nest it inside
    // a keyframe instead of the tween's root. This would help ensure we don't add
    // an ease to the entire tween that overrides those inside each keyframe we have
    // defined. Also, now this ease will only affect the transition from the previous
    // keyframe (70% in this case) to this one.
    // Its inferred duration is 3s (10s - 7s).
    // The elastic ease will work in the same way as if we had a gsap.set() for whatever
    // has been already animated by the preceding tweens, and this one was the first
    // gsap.to() after that.
  }
});

 

Removing my comments to offer a cleaner view of the API, the animation from the original post I made would look like this:

gsap.to(".circle", {
  duration: 3,
  ease: "power1.inOut",
  yoyo: true,
  repeat: -1,
  
  "0%": {
    scale: 0.5,
    opacity: 0
  },
  "75%": {
    scale: 1.25
  },
  
  scale: 1,
  opacity: 1
});

 

Link to comment
Share on other sites

  • Solution

I totally see why you'd make those suggestions, but:

  1. It's actually much more expensive to parse internally because it'd have to analyze every property first looking for keyframe percentages. This is a deal-breaker because it would affect ALL animations. One way to improve it is to tuck it into a dedicated property like "keys", as my proposed plugin does. 
  2. From a readability standpoint, I think this is less desirable. You solved the "repeating keyframe percentages" problem, but you just shifted it to repeating property values instead which, again, I think is less intuitive/readable. Imagine if we want "y" to go down and back up, but "x" just goes down: 
    // grouped keyframes (awkward): 
    keys: {
      "0%": {
        x: 100,
        y: 100
      },
      "50%": {
        y: -100
      },
      "100%": {
        x: 0,
        y: 0
      }
    }
    
    // grouped properties (easier to see what's happening)
    keys: {
      y: {
        "0%": 100,
        "50%": -100,
        "100%": 0
      },
      x: {
        "0%": 100,
        "100%": 0
      }
    }

    If I'm just scanning this, I clearly see what's going on when the properties are grouped but it's more of a struggle when I glance at the grouped keyframes. 

  3. Although from a logic standpoint, it makes complete sense to treat values outside the "keys" as being inherently "100%" but it feels a bit awkward to me when it's not grouped together with the property. 
  4. In terms of easing, I'm not sure it's better to affix them to keyframe percentages rather than the properties. I can definitely imagine scenarios when I want only a specific property to get a special ease (like y: "bounce" but x: "linear") and if you want per-keyframe easing (for ALL properties) you can switch to the normal/current keyframes syntax. In other words, grouping by property gives us a unique behavior that isn't currently possible without multiple tweens or awkward/complex keyframes

All that being said, maybe it's worth having the plugin allow BOTH of those syntax options:

gsap.registerPlugin({
	name: "keys",
	init(target, vars, tween) {
		let tl = this.tl = gsap.timeline(),
			props = {},
			addProp = (prop, obj) => {
				let ease = obj.ease || "none",
					p, a;
				if (~prop.indexOf("%")) {
					for (p in obj) {
						a = props[p] || (props[p] = []);
						p === "ease" || a.push({t: parseFloat(prop), v: obj[p], e: ease});
					}
				} else {
					a = props[prop] || (props[prop] = []);
					if (Array.isArray(obj)) {
						obj.forEach((value, i) => a.push({t: (i + 1) / obj.length * 100, v: value, e: ease}));
					} else {
						for (p in obj) {
							p === "ease" || a.push({t: parseFloat(p), v: obj[p], e: ease});
						}
					}
				}
			},
			p, a, time, i, kf, v;
		this.tween = tween;
		this.ease = gsap.parseEase(vars.ease || "none");
		for (p in vars) {
			p === "ease" || addProp(p, vars[p]);
		}
		for (p in props) {
			a = props[p].sort((a, b) => a.t - b.t);
			time = 0;
			for (i = 0; i < a.length; i++) {
				kf = a[i];
				v = {ease: kf.e, duration: kf.t - (i ? a[i - 1].t : 0)};
				v[p] = kf.v;
				tl.to(target, v, time);
				time += v.duration;
			}
		}
		tl.duration() < 100 && tl.to({}, {duration: 100 - tl.duration()});
	},
	render(ratio, {tl, tween, ease}) {
		tl.progress(ease(tween.progress()));
	}
});

Thoughts? I don't want it to be too confusing for end users but I can see how each syntax option could be useful in various situations. 

 

See the Pen abwgzYj?editors=0010 by GreenSock (@GreenSock) on CodePen

 

So it works with any of these: 

keys: {
  scale: [0.5, 1.25, 1]
}

keys: {
  scale: {
    "0%": 0.5,
    "75%": 1.25,
    "100%": 1
  }
}

keys: {
  "0%": { scale: 0.5 },
  "75%": { scale: 1.25 },
  "100%": { scale: 1 }
}

 

  • Like 3
Link to comment
Share on other sites

  • 1 month later...

I wanted to return here with a few more enhancements:

 

1) Use seconds instead of percentages like: 

keys: {
  "0s": { x: 100, y: 100},
  "2s": { x: 0, y: 0 },
  "5s": { x: 50, y: 50 }
}

2) Define an "easeEach" for controlling the default ease between EACH of the keyframes. The default is "power1.inOut":

keys: {
  "0s": { x: 100, y: 100},
  "2s": { x: 0, y: 0 },
  "5s": { x: 50, y: 50 },
  easeEach: "none"
}

This works for pretty much any of the syntaxes (not just the seconds-based one). 

 

3) In the Array-based syntax, the first value will be placed at 0% instead of using the current value as the initial value, but you can get that behavior by just adding "+=0" in the first spot, like x: ["+=0", 100, -200]

 

Here's the new plugin code: 

gsap.registerPlugin({
	name: "keys",
	init(target, vars, tween) {
		let tl = this.tl = gsap.timeline(),
			props = {},
			duration = tween.duration(),
			addProp = (prop, obj, easeEach, maxSeconds) => {
				let ease = obj.ease || easeEach || "power1.inOut",
					p, a;
				if (maxSeconds || ~prop.indexOf("%")) {
					for (p in obj) {
						a = props[p] || (props[p] = []);
						p === "ease" || a.push({t: parseFloat(prop) / (maxSeconds / 100 || 1), v: obj[p], e: ease});
					}
				} else {
					a = props[prop] || (props[prop] = []);
					if (Array.isArray(obj)) {
						obj.forEach((value, i) => a.push({t: i / (obj.length - 1) * 100, v: value, e: ease}));
					} else {
						for (p in obj) {
							p === "ease" || a.push({t: parseFloat(p), v: obj[p], e: ease});
						}
					}
				}
			},
			p, a, time, i, kf, v;

		// if keyframes use seconds like {"2s": {...}}, assign the maximum to v so we can calculate percentages. Reusing the "v" and "a" local variables to improve file size.
		v = 0;
		for (p in vars) {
			a = p.substr(-1) === "s" ? parseFloat(p) : tween;
			isNaN(a) || a < v || (v = a);
		}
		v && duration === gsap.defaults().duration && duration < v && tween.duration(v);

		this.tween = tween;
		this.ease = gsap.parseEase(vars.ease || "none");
		for (p in vars) {
			p === "ease" || p === "easeEach" || addProp(p, vars[p], vars.easeEach, v);
		}
		for (p in props) {
			a = props[p].sort((a, b) => a.t - b.t);
			time = 0;
			for (i = 0; i < a.length; i++) {
				kf = a[i];
				v = {ease: kf.e, duration: kf.t - (i ? a[i - 1].t : 0)};
				v[p] = kf.v;
				tl.to(target, v, time);
				time += v.duration;
			}
		}
		tl.duration() < 100 && tl.to({}, {duration: 100 - tl.duration()});
	},
	render(ratio, {tl, tween, ease}) {
		tl.progress(ease(tween.progress()));
	}
});

Enjoy!

  • Like 2
Link to comment
Share on other sites

  • 1 month later...

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...