Search code examples
csscss-animationseasing

Why is my CSS keyframe animation slightly pausing between loops?


I'm trying to animate an element in a 'rocking' motion as if it were a boat on waves.

JSBIN here: https://jsbin.com/bakugifulo/edit?html,css,output

I have four colored example divs in the JSBIN.

(1) Pink = this is the motion I am after. Very smooth rocking with easing. The animation keyframes:

@keyframes rocking1 {
    0% {
        transform: rotate(-5deg);
        animation-timing-function: ease-in-out;
    }
    50% {
        transform: rotate(5deg);
        animation-timing-function: ease-in-out;
    }
    100% {
        transform: rotate(-5deg);
        animation-timing-function: ease-in-out;
    }
}

(2) Blue = The catch: due to how this animation is being incorporated into the larger picture, I need it to do the same, but not initially start rotated. Meaning, I want it to start at '0' rotation.

Shifting the keyframe % spots and tweaking easing should, in theory, give me the same result:

@keyframes rocking2 {
    0% {
        transform: rotate(0deg);
    }
    25% {
        transform: rotate(-5deg);
        animation-timing-function: ease-out;
    }
    75% {
        transform: rotate(5deg);
        animation-timing-function: ease-in-out;
    }
    100% {
        transform: rotate(0deg);
        animation-timing-function: ease-in;
    }
}

Note that there is a very slight pause as the animation loops from 100% back to 0%. This is what I am trying to prevent.

(3 & 4) Brown & Green = duplicates of the above two, but using linear instead of easing. Note that there isn't a pause on the green one.

So it appears the problem is my easing logic in the blue div. However, I can not figure this out. I've tried a variety of shuffling and swapping easing options. Removing the 0% and 100% marks altogether. And they all produces the same problem...an ever-so-slight pause when it loops.

Is there a way to achieve what I am after without that pause? Or is this just inevitable when using easing (I'm guessing the first example is pausing, but you just don't notice it due to it pausing at the end of the direction it's moving in).

But I'm hoping I'm just missing something obvious in my easing logic.


Solution

  • It can be done in single animation starting at "0 rotation" without stacking and without negative delay, and you were pretty close to that. (Welcome to SO, by the way!)

    You just had the easing functions set one frame later, but the progression (ease-out - ease-in-out - ease-in) was correct.

    For the POC demo I've changed the "thing" to resemble a pendulum, because I think it is slightly more illustrative for this purpose:

    @keyframes swing {
     /* Starting at the bottom. */
     0% {
      transform: rotate(0turn); color: red;
      animation-timing-function: ease-out;
     }
     /* From the bottom to the right cusp:
     start full speed, end slow (ease-out). */
     25% {
      transform: rotate(-0.2turn); color: blue;
      animation-timing-function: ease-in-out;
     }
     /* From the right cusp to the left cusp:
     start slow, end slow (ease-in-out).
     It will effectively cross the bottom
     `0turn` point at 50% in full speed.
     */
     75% {
      transform: rotate(0.2turn); color: green;
      animation-timing-function: ease-in;
     }
     /* From the left cusp to the bottom:
     start slow, end full speed (ease-in). */
     100% {
      transform: rotate(0turn); color: yellow;
      animation-timing-function: step-end;
     }
     /* Back at the bottom.
     Arrived here at the full speed.
     Animation timing function has no effect here. */
    }
    
    div {
     animation: swing;
     animation-duration: 3s;
     /* `animation-timing-function` is set explicitly 
     (overridden) in concrete keyframes. */
     animation-iteration-count: infinite;
     animation-direction: normal;
     /* `reverse` still obeys "reversed" timing functions from *previous* frames. */
     animation-play-state: running;
     transform-origin: center top;
     margin: auto;
     width: 100px;
     display: flex;
     flex-direction: column;
     align-items: center;
     pointer-events: none;
     &::before,
     &::after {
      content: '';
      background-color: currentcolor;
     }
     &::before {
      width: 1px;
      height: 100px;
     }
     &::after{
      width: 50px;
      height: 50px;
     }
    }
    
    #reset:checked ~ div {
     animation: none;
    }
    #pause:checked ~ div {
     animation-play-state: paused;
    }
    <meta name="color-scheme" content="dark light">
    
    <input type="checkbox" id="pause"><label for="pause">Pause animation</label>, 
    <input type="checkbox" id="reset"><label for="reset">Remove animation</label>.
    
    <div></div>

    I must admit it never occurred to me that we can set different timing functions for each key frame, so such naturally looking multi-step animation with "bound" easing types is in fact achievable. Big takeaway for me is also information that easing function of the last (to / 100%) key frame logically doesn't have any effect.


    Personally I'd most probably go with terser "back-and-forth" animation-direction: alternate between the two cusp points, ease-in-out timing and negative half-duration delay shifting its initial state to the "bottom" mid-point (similar to that proposed in other answer here) but I definitely see the benefits of this more straightforward approach without delay.