juliangarnier / anime

JavaScript animation engine
https://animejs.com
MIT License
50.19k stars 3.68k forks source link

Smooth transition between keyframes instead of per-step-easing #702

Open famecastle opened 4 years ago

famecastle commented 4 years ago

I am searching for a CSS-like behavior where the whole easing would be applied to the whole animation instead of all the individual keyframes.

Some example code:

anime({
    targets: currentPosition,
    easing: "easeOutExpo",
    duration: 1000,
    round: 1,
    update: function() {
        item.move(currentPosition.progress);
        item.rotate(currentPosition.rotation);
    },
    keyframes: [
        {progress: 100},
        {progress: 100 + 16, rotation: 90},
        {progress: 100 + 16 + 320},
        {progress: 100 + 16 + 320 + 16, rotation: 90 + 90},
        {progress: 100 + 16 + 320 + 16 + 100}
    ]
});

Basically, this will ease out to {progress: 100}. Afterwards it will start a new animation from this point to {progress: 100 + 16, rotation: 90}, easing out again, ... and so on. So the result looks like five actual animations where I expected to get just one assembled smooth animation that eases out over the full duration.

I couldn't find this feature, but I think it's really essential since it resembles a core concept of the keyframe idea (as used in CSS or any 3d animation software).

qqilihq commented 4 years ago

I’m wondering myself about this. Did you solve this?

se-panfilov commented 10 months ago

I'd also like to have this. Question: Should we expect this feature in v4?

Besides:

juliangarnier commented 10 months ago

It was originally in V4, but removed it from the core since I didn't like the way it was implemented.

The idea was to set all keyframes eases to 'linear', and then animate the progress of the animation using the provided easing function. Basically I was doing something like this internally:

animate(animate('.playground div', {
  x: [0, 50, 100, 150, 200, 250],
  y: [40, 50, 75, 100, 75, 50],
  ease: 'linear',
  autoplay: false
}), {
  progress: 1,
  duration: 2000,
  ease: 'inOut(2)',
})

@se-panfilov Is this the default keyframes behaviour in both motion one and gsap?

se-panfilov commented 10 months ago

@juliangarnier tl;dr - looks like yes, it's a default behavior.

I'm not a big expert in both libs (gsap, motion). In fact I know more about animejs, rather than those two. Just looked for some alternatives.

But afaik - yes, it's default for keyframes for gsap:

gsap.to(yourTarget, {
   ...
  ease: "power1.inOut", //will make one smooth animation for all 
  keyframes: [
    { x: 50, y: 30 },
    { x: 100, y: 45 },
    { x: 150, y: 55 },
    { x: 200, y: 45 },
    { x: 250, y: 0 },
  ],
});

(link)

(As I understood, if you want to have a separate easing for every step, you can do timeline().to({ x: 50, y: 30 }).to({ x: 100, y: 45 }), etc)

For motion dev there is no keyframes, just a list of values, but works the same without any additional setup:

animate(
  youTarget,
  { x: [0, 50, 100, 150, 200, 250], y: [40, 50, 75, 100, 75, 50] },
  {
    ...
    easing: circOut, //also smooth for all
  }
);

(link)

To be honest, I expected more people to be wanting that feature.

But could you confirm, that in the current version there is no possibility to achieve that (nor with svg path, keyframes or something else).

P.S. Thanks for the great lib, btw.

different55 commented 10 months ago

One workaround is to use two animations. One animation with autoplay: false and easing set to linear, and the other with the easing you want that controls the progress of the first animation. One downside comes to mind, I'm not sure if this solution would behave itself for easings that have overshoot, like elastic or spring.

se-panfilov commented 10 months ago

@different55 Hmmm, something like this? The animation is smooth now, but it doesn't feels like easing is actually working (still linear).

UPDATE: With help of @juliangarnier this should work now:

const path = [
  { x: 50, y: 30 },
  { x: 100, y: 45 },
  { x: 150, y: 55 },
  { x: 200, y: 45 },
  { x: 250, y: 0 },
];

const duration = 1000;

const baseAnimation = anime({
  targets: complexStateProxy, 
  x: path.map(p => p.x),
  y: path.map(p => p.y),
  easing: 'linear',
  autoplay: false,
  duration 
});

anime({
  targets: baseAnimation,
  progress: [0, 100], 
  easing: 'easeInCubic',
  duration,
  update: (anim) => {
    baseAnimation.seek(baseAnimation.duration * (baseAnimation.progress / 100));
  }
});

(jsfiddle)

P.S. As you said, elastic and spring animations doesn't really works (at leas not as expected)

juliangarnier commented 10 months ago

@different55 is right, that's pretty much how I would do it (and eases with overshoot will just works as expected I guess?)

Thanks for the examples @se-panfilov!

I think this easing behaviour is worth adding to the lib and I'm still trying to figure out the best way to do it without breaking the current way of declaring keyframes.

Being able to declare specific parameters (like easing) per property is at the core of anime.js, and I'm questioning what should append when you apply an easing to the entire chain of keyframes, while also declaring per keyframe easing?

With this behaviour, all easings declared inside the keyframes won't match their actual timing since the time value is modified by the easing function. This also apply to delays and durations that will be affected by the "main" easing.

For example whit the new behaviour, when writing:

animate('div', {
  keyframes: [
    { x: 50, duration: 500 },
    { x: 100, duration: 500 },
  ],
  ease: 'outExpo',
}

the x value will go to 50 in less than 500ms, and I find this a bit confusing.

Apparently gsap doesn't allow setting duration per keyframe and it looks like a delay in a keyframe is stretched to fit the total duration of the animation, but I'm not 100% sure whats going on exactly.

My implementation in V4 looked like this:

animate('div', {
  keyframes: [
    { x: 50, duration: 500 },
    { x: 100, duration: 500 },
  ],
  easeAll: 'outExpo', // explicitly ease everything
}

I still like the API, but the code was complicated and felt out of place. But I changed a lot of things internally that could make this easier to implement now.

@different55, @se-panfilov Am I missing something important here? And what do you think of the easeAll parameter?

Thanks!!

se-panfilov commented 10 months ago

@juliangarnier I guess easeAll sounds logical (maybe could be a better name, but whatever).

The only thing is that this could be quite confusing:

animate('div', {
  keyframes: [
    { x: 50, duration: 500, ease: 'linear' }, // <-- easing
    { x: 100, duration: 500, ease: 'easeInCubic' } // <-- easing
  ],
  easeAll: 'outExpo' // <-- easing again
}

In this definition I would be confused of what to expect. In the worst case I'd throw an exception that you cannot mix top- and per-keyframe easings together (or at least console warning).

But in general - yes, I think that easeAll (or just a top-level ease) is a way to go.

P.S. Could you maybe take a quick look into my attempt to use 2 animations as @different55 suggested? It looks like it's still a linear movement, without easing (but at least smooth). The link to my comment, the link to jsfiddle.

P.P.S. I'd be glad if you notify me/us when that functionality will be available (in v4 I guess).

Thanks

juliangarnier commented 10 months ago

P.S. Could you maybe take a quick look into my attempt to use 2 animations as @different55 suggested? It looks like it's still a linear movement, without easing (but at least smooth). The https://github.com/juliangarnier/anime/issues/702#issuecomment-1865251756, the link to jsfiddle.

You have to use the "eased" progress value:

baseAnimation.seek(baseAnimation.duration * (baseAnimation.progress / 100));

animate('div', { keyframes: [ { x: 50, duration: 500, ease: 'linear' }, // <-- easing { x: 100, duration: 500, ease: 'easeInCubic' } // <-- easing ], easeAll: 'outExpo' // <-- easing again } In this definition I would be confused of what to expect.

Yeah that's why this feature is a complex one, it's not only the easings that will be affected, the duration: 500 too...

I guess easeAll sounds logical (maybe could be a better name, but whatever).

At least easeAll implies that you're applying this ease to everything, all the timings, (durations, delays), even the other eases!

I think that easeAll (or just a top-level ease) is a way to go.

Top level ease parameter is already taken, since you can do :

animate('div', {
  keyframes: [
    { x: 50, duration: 500 },
    { x: 100, duration: 500 }
  ],
  ease: 'outExpo' // Apply 'outExpo' to each keyframes
}

So with easeAll you might even be able to do:

animate('div', {
  keyframes: [
    { x: 50, duration: 500 },
    { x: 100, duration: 500 }
  ],
  ease: 'outExpo' // Apply 'outExpo' to each keyframes
  easeAll: 'inOutExpo' // Apply 'inOutExpo' to smooth out all the keyframes durations / easings
}

But I'm open to suggestions for the name!

juliangarnier commented 9 months ago

OK so I ended up calling this parameter playbackEase, since it applies the easing on the progress of the animation instead of each tweens. I also added WAAPI / CSS style percentage based keyframes syntax. This is currently working on the dev branch of V4 beta. It looks like this:

animate('.waapi-like', {
  keyframes: {
    '0%'  : { left: 100, top: 100, ease: 'out' },
    '20%' : { left: -100, ease: 'in' },
    '50%' : { left: 100, ease: 'out' },
    '70%' : { left: -100, ease: 'in' },
    '100%': { left: 100, top: -100 },
  },
  playbackEase: 'out', // Is applied across all keyframes
  duration: 4000,
});

This is the equivalent of this WAPPI animation:

document.querySelector('.waapi').animate([
  { left: '100px', top: '100px', easing: 'ease-out', offset: 0 },
  { left: '-100px', easing: 'ease-in', offset: .2 },
  { left: '100px', easing: 'ease-out', offset: .5 },
  { left: '-100px', easing: 'ease-in', offset: .7 },
  { left: '100px', top: '-100px', offset: 1 },
], {
  easing: 'ease-out', // Is applied across all keyframes
  duration: 4000,
  fill: 'forwards',
});
different55 commented 9 months ago

playbackEase makes good enough sense to me, and the percent-style keyframes oughta make that a lot easier to use. Excited to get to use this!