skip to Main Content

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.

3

Answers


  1. You can specify a negative animation-delay to start the animation from the desired frame.
    Here is a simple example based on your rocking1 animation, but with the start at trasform: rotate(0) – if the total animation will be 5s, then animation-delay = -1 * 5 * 25 / 100 = -1.25s:

    div {
      width: 200px;
      height: 100px;
      margin: 50px;
    }
    
    #div2 {
      --animation-duration: 5s;
      background: lightblue;
      animation: rocking2 var(--animation-duration) calc(-1 * var(--animation-duration) * .25) infinite ease-in-out;
    }
    
    @keyframes rocking2 {
      0%, 100% {
        transform: rotate(-5deg);
      }
      50% {
        transform: rotate(5deg);
      }
    }
    <div id="div2">easing - start at zero</div>

    P.S. You can combine same frames of animation with a comma:
    0%, 100% { transform: rotate(-5deg); }

    Login or Signup to reply.
  2. I found the solution. Just use 2 animations as shown here Play multiple CSS animations at the same time.
    1 animation to get to starting position and then play the original animation.

    There we set 1 animation with the duration of 25% (1250ms) of original animation to play once and original animation with the delay of 1250ms.

    animation: rocking2start 1250ms 1, rocking1 2500ms infinite 1250ms alternate;
    
    @keyframes rocking2start {
      0% {
        transform: rotate(0deg);
        animation-timing-function: ease-in-out;
      }
      100% {
        transform: rotate(-5deg);
        animation-timing-function: ease-in-out;
      }
    }
    
    div {
      width: 200px;
      height: 100px;
      margin: 50px;
    }
    
    @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;
      }
    }
    
    #div1 {
      background: pink;
      animation: rocking1 2500ms infinite 1250ms alternate;
    }
    
    #div2 {
      background: lightblue;
      animation: rocking2start 1250ms 1, rocking1 2500ms infinite 1250ms alternate;
    }
    
    @keyframes rocking2start {
      0% {
        transform: rotate(0deg);
        animation-timing-function: ease-in-out;
      }
      100% {
        transform: rotate(-5deg);
        animation-timing-function: ease-in-out;
      }
    }
    <div id="div1">easing</div>
    <div id="div2">easing - start at zero</div>
    Login or Signup to reply.
  3. 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-outease-in-outease-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 {
     00% { /* bottom -> right cusp: start full speed, end slow: */
      animation-timing-function: ease-out;
      transform: rotate(-.0turn); color: red;
     }
     25% { /* right cusp -> left cusp: start slow, end slow: */
      animation-timing-function: ease-in-out;
      transform: rotate(-.2turn); color: blue;
     }
     75% { /* left cusp -> bottom: start slow, end full speed */
      animation-timing-function: ease-in;
      transform: rotate(+.2turn); color: green;
     }
     100% { /* bottom, timing function has no effect here */
      animation-timing-function: step-end;
      transform: rotate(+.0turn); color: yellow;
     }
    }
    
    div {
     animation: swing;
     animation-duration: 3s;
     animation-timing-function: ease-in-out;
     animation-iteration-count: infinite;
     animation-direction: normal;
     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 go most probably with terser animation with only two key frames, ..direction: alternate starting paused, and having negative delay shifting it’s initial state to the middle, similar to that proposed in other answer here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search