Stop CSS rotate animation smoothly on unknown keyframe

2.3k views Asked by At

I have an image that swings using CSS rotate animation. I would like to stop it smoothly (and put it back to its original position when clicking on another element but without getting the jumpy stop feeling). It seems that the expected behaviour only happens on the first iteration of the animation but not on the upcoming ones (this is when clicking on the "slow" button during the first 2 seconds)

Here is a sample code https://jsfiddle.net/pbarrientos/5v3xwak6/

I have already tried adding animation-iteration-count: 1; and adding/removing classes.

var css = { 
        '-webkit-animation-iteration-count': "1",
        '-moz-animation-iteration-count': "1",
        'animation-iteration-count': "1"
    }

Clues anyone?

3

There are 3 answers

1
AudioBubble On BEST ANSWER

I would use manual animation here. The browser is in charge of its CSS animation and it will be challenging to intervene with that with perfectly synced position and speed.

As the animation is not very complex we can simply set up our own matrix, or use the helper methods, as well as using a sine function where radius is reduced when stopped.

When hitting the stop button we reduce the radius to make it appear to be stopping. We can do the opposite to start again. The benefit is that we can stop at any point and have a natural rest. If you want to offset the angle you could interpolate to that angle at the same time as reducing radius.

By using requestAnimationFrame and transforms we will obtain smooth animation just like with CSS.

The main function would be:

angle = Math.sin(time) * radius;  // sin=[-1,1] radius => angle

Then when stopping, reduce radius which will end up as angle:

radius *= 0.99;      

Example

var img = $("img"), btn = $("button"),
    angle, maxRadius = 10, radius = maxRadius,
    playing = true, time= 0;

(function loop() {
  angle = Math.sin(time) * radius;            // calc current angle
  setTransform(img, angle);

  if (playing) {
    if (radius < maxRadius) radius *= 1.03;   // increase 3% each frame upto max
  } else {
    radius *= 0.99;                           // reduce 1% each frame
  }
  
  time += 0.1;
  requestAnimationFrame(loop)                 // loop, can be stopped when radius < n
})();

function setTransform(img, angle) {
  img.css("transform", "rotate(" + angle + "deg)");
  img.css("-webkit-transform", "rotate(" + angle + "deg)");
}

btn.on("click", function() {
  playing = !playing;
  if (playing && radius < 0.1) radius = 0.1;  // give some meat in case =0
});
img {
  transform-origin: top center; -webkit-transform-origin: top center;
  }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<img src="https://i.stack.imgur.com/Vma8v.png"><br>
<button>Start / Stop smoothly</button>

You may want to implement a mechanism to abort the loop as well when radius is lower than n, then start the loop when needed. Use a separate flag for that so the risk of starting several rAF's is eliminated.

2
guest271314 On

Try saving matrix position of #element before animation-play-state set to paused , adding saved matrix position of #element to css applied to #element after animation paused ; reset animation-iteration-count to infinite at #play click event - for instance where #slow previously clicked , setting animation-iteration-count to 1

$(document).ready(function() {
  var el = $("#element");
  
  // return `matrix` position of `el` `#element`
  function tfx(el) {
    return el.css(["-webkit-transform", "-moz-transform"]);
  }

  $("#pause").click(function() {
    // save `matrix` position of `el`
    var pos = tfx(el);
    var css = {
      "-webkit-animation-play-state": "paused",
      "-moz-animation-play-state": "paused",
      "animation-play-state": "paused"
    }
    // extend `css` with `pos`
    var _css = $.extend(pos, css);
    el.css(_css);
  });

  $("#play").click(function() {
    // save `matrix` position of `el` 
    var pos = tfx(el);
    // reset `animation-iteration-count` to `infinite`
    var css = {
      "-webkit-animation-iteration-count": "infinite",
      "-moz-animation-iteration-count": "infinite",
      "animation-iteration-count": "infinite",
      "-webkit-animation-play-state": "running",
      "-moz-animation-play-state": "running",
      "animation-play-state": "running"
    }
    // extend `css` with `pos`
    var _css = $.extend(pos, css);
    el.removeClass("stopit").addClass("swing").css(_css);
  });

  $("#slow").click(function() {
    el.removeClass("swing").addClass("stopit");
    var css = {
      "-webkit-transition": "all 4000ms ease-out",
      "-webkit-animation-iteration-count": "1",
      "-moz-animation-iteration-count": "1",
      "animation-iteration-count": "1"
    }
    el.css(css);
     // `stopit` class added above ?
     // el
     // .one("webkitAnimationEnd oanimationend msAnimationEnd animationend", function (e) {
      // el.addClass("stopit");
    // });
  });
});
.swing {
  transform-origin: top center;
  -webkit-transform-origin: top center;
  animation: badge-swing 2s infinite;
  -webkit-animation: badge-swing 2s infinite;
  -webkit-animation-fill-mode: both;
  -webkit-animation-timing-function: ease-in-out;
  -moz-animation: badge-swing 2s infinite;
  -moz-animation-fill-mode: forwards;
  animation-fill-mode: forwards;
}
.stopit {
  -webkit-animation-duration: 2s;
  -webkit-animation-name: stopit;
  -webkit-animation-fill-mode: forwards;
  -moz-animation-duration: 2s;
  -moz-animation-name: stopit;
  -moz-animation-fill-mode: forwards;
  animation-name: stopit;
}
@-webkit-keyframes badge-swing {
  0% {
    -webkit-transform: rotate(-5deg);
    -webkit-animation-timing-function: ease-in;
  }
  25% {
    -webkit-transform: rotate(0deg);
    -webkit-animation-timing-function: ease-out;
  }
  50% {
    -webkit-transform: rotate(5deg);
    -webkit-animation-timing-function: ease-in;
  }
  75% {
    -webkit-transform: rotate(0deg);
    -webkit-animation-timing-function: ease-out;
  }
  100% {
    -webkit-transform: rotate(-5deg);
    -webkit-animation-timing-function: ease-in;
  }
}
@-moz-keyframes badge-swing {
  0% {
    -moz-transform: rotate(-5deg);
    -moz-animation-timing-function: ease-in;
  }
  25% {
    -moz-transform: rotate(0deg);
    -moz-animation-timing-function: ease-out;
  }
  50% {
    -moz-transform: rotate(5deg);
    -moz-animation-timing-function: ease-in;
  }
  75% {
    -moz-transform: rotate(0deg);
    -moz-animation-timing-function: ease-out;
  }
  100% {
    -moz-transform: rotate(-5deg);
    -moz-animation-timing-function: ease-in;
  }
}
@-webkit-keyframes stopit {
  0% {
    -webkit-transform: rotate(-5deg);
    -webkit-animation-timing-function: ease-out;
  }
  100% {
    -webkit-transform: rotate(0deg);
    -webkit-animation-timing-function: ease-out;
  }
}
@-moz-keyframes stopit {
  0% {
    -moz-transform: rotate(-5deg);
    -moz-animation-timing-function: ease-out;
  }
  100% {
    -moz-transform: rotate(0deg);
    -moz-animation-timing-function: ease-out;
  }
}
#pause,
#play,
#slow {
  display: inline-block;
  padding: 5px 30px;
  background: lightgrey;
  border-radius: 5px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<img id="element" src="https://dl.dropboxusercontent.com/u/39131788/palmed.png" class="swing">
<br>
<br>
<div id="pause">pause</div>
<div id="play">play</div>
<div id="slow">slow</div>

jsfiddle https://jsfiddle.net/5v3xwak6/5/

2
Tahir Ahmed On

Would've been fun if you were using TweenMax.

jsFiddle.

Snippet:

var element=document.getElementById('element');
var playButton=document.getElementById('play');
var pauseButton=document.getElementById('pause');
var slowButton=document.getElementById('slow');
var maxDegree=-10;
var minDegree=10;
var duration=.8;
var easeFunc=Power2;
var timeline=new TimelineMax({paused:true,repeat:-1});
TweenMax.set(element,{transformOrigin:'top center'});
timeline.to(element,duration,{rotation:maxDegree,ease:easeFunc.easeOut});
timeline.to(element,duration,{rotation:0,ease:easeFunc.easeIn});
timeline.to(element,duration,{rotation:minDegree,ease:easeFunc.easeOut});
timeline.to(element,duration,{rotation:0,ease:easeFunc.easeIn});

playButton.addEventListener('click',onPlayClick,false);
pauseButton.addEventListener('click',onPauseClick,false);
slowButton.addEventListener('click',onSlowClick,false);

function onPlayClick(){timeline.timeScale(1).play();}
function onPauseClick(){timeline.timeScale(1).pause();}
function onSlowClick(){
    timeline.pause().timeScale(.5);
    if(timeline.progress()<.25){
        timeline.tweenTo(0);
    }else if(timeline.progress()>=.25&&timeline.progress()<.75){
        timeline.tweenTo(timeline.duration()*.5);
    }else{
        timeline.tweenTo(timeline.duration());
    }
}
#pause, #play, #slow {
    display: inline-block;
    padding: 5px 30px;
    background: lightgrey;
    border-radius: 5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.17.0/TweenMax.min.js"></script>
<img id="element" src="https://dl.dropboxusercontent.com/u/39131788/palmed.png" class="swing">
<br>
<br>
<div id="pause">pause</div>
<div id="play">play</div>
<div id="slow">slow</div>

The intention is to provide you with an alternative, if you are interested. Hope it helps.