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)
});
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.
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() .
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.
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
});