Reverse a CSS animation

前端 未结 3 659
故里飘歌
故里飘歌 2021-01-22 15:15

I did this: http://codepen.io/yayoni/pen/pgXoWY

And I want to reverse the animation when I click on the small button but what I did doesnt work and I don\'t understand w

相关标签:
3条回答
  • If it were a simple color change, I would've also recommended CSS transitions like in Martin's answer but as you've explained that it is something more complex, Osama8DZ's answer is your best solution. But that doesn't give an answer to your question, which states the following (emphasis is mine):

    And I want to reverse the animation when I click on the small button but what I did doesnt work and I don't understand why.

    Reason:

    When you click on the button which causes the reverse animation to trigger, the class name changes and so the original animation is replaced with the reverse animation. This is actually two steps - first is removal of the original animation and second is addition of the reverse.

    Whenever an animation that exists on an element is removed, the element immediately snaps to what was its original state (which is, the one before the animation). Here the element had red background in its original state. Thus the element immediately gets a red background and then the reverse animation has no visual effect because it is animating from a red background to a red background.

    You can see a sample of what I mean in the below snippet. I added a left and top margin to the original state of the element and changed it during the forward animation. As soon as the reverse button click is performed, the shape immediately goes back to original state and then applies the left, top margins provided within the reverse animation.

    var div = document.getElementById('fab');
    
    function anim() {
      div.className = "anim";
    }
    
    function animrev() {
      div.className = "animrev";
    }
    html,
    body {
      height: 100%;
      width: 100%;
    }
    #content {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #fab {
      margin-right: 30px;
      color: white;
      font-size: 45px;
      outline: 0;
      border: 0;
      height: 150px;
      width: 150px;
      border-radius: 75px;
      text-align: center;
      background-color: red;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
      
      /* added these for demo */
      margin-left: 20px;
      margin-top: 10px;
    }
    .anim {
      animation-name: example;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    @keyframes example {
      100% {
        background-color: green;
        left: 0px;
        top: 0px;
        
        /* added these for demo */
        margin-left: 40px;
        margin-top: 20px;
      }
    }
    @keyframes exampleux {
      100% {
        background-color: red;
        left: 0px;
        top: 0px;
    
        /* added these for demo */
        margin-left: 30px;
        margin-top: 15px;    
      }
    }
    .animrev {
      animation-name: exampleux;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    <div id="content">
      <button onclick="anim()" id="fab">+</button>
      <button onclick="animrev()" id="fab2">+</button>
    </div>


    Solution:

    The simple and best solution would be the one provided in Osama8DZ's answer as it makes it look as though the original element never snapped back to its original state. I am not going to detail that further because it would look like I am copying his answer (or) plagiarizing it.

    However, I am going to highlight a few cases where it doesn't work and how to fix them.


    Case 1: When the animation has a delay - When either both the animations (or) only the reverse is delayed, you will still be able to see it snap back to original state before running the animation. Below snippet shows the problem in action.

    var div = document.getElementById('fab');
    
    function anim() {
      div.className = "anim";
    }
    
    function animrev() {
      div.className = "animrev";
    }
    html,
    body {
      height: 100%;
      width: 100%;
    }
    #content {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #fab {
      margin-right: 30px;
      color: white;
      font-size: 45px;
      outline: 0;
      border: 0;
      height: 150px;
      width: 150px;
      border-radius: 75px;
      text-align: center;
      background-color: red;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    }
    .anim {
      animation-name: example;
      animation-duration: 1s;
      animation-fill-mode: forwards;
      animation-delay: 1s;
    }
    @keyframes example {
      100% {
        background-color: green;
        left: 0px;
        top: 0px;
      }
    }
    @keyframes exampleux {
      0% {
        /* You need to add this */
        background-color: green;
        left: 0px;
        top: 0px;  
      }
      100% {
        background-color: red;
        left: 0px;
        top: 0px;
      }
    }
    .animrev {
      animation-name: exampleux;
      animation-duration: 1s;
      animation-fill-mode: forwards;
      animation-delay: 1s;
    }
    <div id="content">
      <button onclick="anim()" id="fab">+</button>
      <button onclick="animrev()" id="fab2">+</button>
    </div>

    How to fix it? The reason for this is because the animation would continue to hold its original state until the delay time is elapsed and so even though the 0% keyframe within the reverse animation is the same as 100% keyframe of forward animation, the element will still snap back and then animate again. Fix for this is to set animation-fill-mode as both (which means, to have the effect of both forwards and backwards) instead of forwards. This makes the element hold the state as at 0% keyframe during the delay period also (due to backwards) and hold the state as at 100% keyframe even after animation has ended (due to forwards).

    var div = document.getElementById('fab');
    
    function anim() {
      div.className = "anim";
    }
    
    function animrev() {
      div.className = "animrev";
    }
    html,
    body {
      height: 100%;
      width: 100%;
    }
    #content {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #fab {
      margin-right: 30px;
      color: white;
      font-size: 45px;
      outline: 0;
      border: 0;
      height: 150px;
      width: 150px;
      border-radius: 75px;
      text-align: center;
      background-color: red;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    }
    .anim {
      animation-name: example;
      animation-duration: 1s;
      animation-fill-mode: both;
      animation-delay: 1s;
    }
    @keyframes example {
      100% {
        background-color: green;
        left: 0px;
        top: 0px;
      }
    }
    @keyframes exampleux {
      0% {
        /* You need to add this */
        background-color: green;
        left: 0px;
        top: 0px;  
      }
      100% {
        background-color: red;
        left: 0px;
        top: 0px;
      }
    }
    .animrev {
      animation-name: exampleux;
      animation-duration: 1s;
      animation-fill-mode: both;
      animation-delay: 1s;
    }
    <div id="content">
      <button onclick="anim()" id="fab">+</button>
      <button onclick="animrev()" id="fab2">+</button>
    </div>


    Case 2: What if the reverse animation is common for more than one element but forward animation is different like in the below snippet. In the snippet below, the forward animation of first element changes background from red to green whereas the second element changes it from red to blue but the reverse animation should set both back to red. In such scenario we cannot set the 0% keyframe of reverse animation as the 100% of the forward animation because the end state is different for the two forward animations.

    var div = document.getElementById('fab');
    var div2 = document.getElementById('fab3');
    
    function anim() {
      div.className = "anim";
    }
    function animAlt() {  
      div2.className = "anim-alt";
    }
    
    function animrev() {
      div.className = "animrev";
      div2.className = "animrev";
    }
    html,
    body {
      height: 100%;
      width: 100%;
    }
    #content {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #fab, #fab3 {
      margin-right: 30px;
      color: white;
      font-size: 45px;
      outline: 0;
      border: 0;
      height: 150px;
      width: 150px;
      border-radius: 75px;
      text-align: center;
      background-color: red;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
    }
    .anim {
      animation-name: example;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    @keyframes example {
      100% {
        background-color: green;
        left: 0px;
        top: 0px;
      }
    }
    .anim-alt {
      animation-name: example2;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    @keyframes example2 {
      100% {
        background-color: blue;
        left: 0px;
        top: 0px;
      }
    }
    
    @keyframes exampleux {
      0% {
        /* You need to add this */
        background-color: green;
        left: 0px;
        top: 0px;  
      }
      100% {
        background-color: red;
        left: 0px;
        top: 0px;
      }
    }
    .animrev {
      animation-name: exampleux;
      animation-duration: 1s;
      animation-fill-mode: forwards;  
    }
    <div id="content">
      <button onclick="anim()" id="fab">+</button>
      <button onclick="animAlt()" id="fab3">+</button>
      <button onclick="animrev()" id="fab2">+</button>
    </div>

    How to fix it? There are no pure CSS solutions for this case (unless you switch to using transitions). However there is a workaround, which is to add the styles from last keyframe of the original animation via inline styles to the element right after it has ended. This means that we are mimicking the effect of the element not having snapped back to its original state.

    var div = document.getElementById('fab');
    var div2 = document.getElementById('fab3');
    
    function anim() {
      div.className = "anim";
    }
    function animAlt() {
      div2.className = "anim-alt";
    }
    
    function animrev() {
      div.className = "animrev";
      div2.className = "animrev";
    }
    
    div.addEventListener('animationend', function(e) {
      if (e.animationName == "example") { /* meaning the forward animation has ended */
        this.style.backgroundColor = 'green';
        this.style.marginLeft = '40px';
        this.style.marginTop = '20px';
      }
      else
        this.style = null;
    })
    div2.addEventListener('animationend', function(e) {
      if (e.animationName == "example2") { /* meaning the forward animation has ended */
        this.style.backgroundColor = 'blue';
        this.style.marginLeft = '40px';
        this.style.marginTop = '20px';
      }  
      else
        this.style = null;
    })
    html,
    body {
      height: 100%;
      width: 100%;
    }
    #content {
      height: 100%;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #fab, #fab3 {
      margin-right: 30px;
      color: white;
      font-size: 45px;
      outline: 0;
      border: 0;
      height: 150px;
      width: 150px;
      border-radius: 75px;
      text-align: center;
      background-color: red;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
      
      margin-left: 20px;
      margin-top: 10px;  
    }
    .anim {
      animation-name: example;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    @keyframes example {
      100% {
        background-color: green;
        left: 0px;
        top: 0px;
        
        margin-left: 40px;
        margin-top: 20px;    
      }
    }
    .anim-alt {
      animation-name: example2;
      animation-duration: 1s;
      animation-fill-mode: forwards;
    }
    @keyframes example2 {
      100% {
        background-color: blue;
        left: 0px;
        top: 0px;
        
        margin-left: 40px;
        margin-top: 20px;
      }
    }
    
    @keyframes exampleux {
      100% {
        background-color: red;
        left: 0px;
        top: 0px;
        
        margin-left: 20px;
        margin-top: 10px;
      }
    }
    .animrev {
      animation-name: exampleux;
      animation-duration: 1s;
      animation-fill-mode: forwards;  
    }
    <div id="content">
      <button onclick="anim()" id="fab">+</button>
      <button onclick="animAlt()" id="fab3">+</button>
      <button onclick="animrev()" id="fab2">+</button>
    </div>

    Note: We should not leave the inline styles as-is because that would cause the original animation to have problems when clicked for a second time. So, we have to listen to the animation's end event and remove the inline styles (if any). The animationend still requires browser prefixing in a few versions but I would leave that part to you.

    I hope all the above have provided you a full explanation of why your code does not (seem to) work, how to fix for various scenarios.

    0 讨论(0)
  • 2021-01-22 15:55

    In fact you don't need to use any CSS3 keyframes nor animation and use just transition: background-color 1s linear; to create the colour transition.

    See you updated demo: http://codepen.io/anon/pen/YwozaQ

    0 讨论(0)
  • 2021-01-22 15:55

    Try:

    @keyframes exampleux {
    0%   
    {
        background-color: green; left:0px; top:0px; /* You need to add this */
    }
    
    100% 
    {
        background-color: red; left:0px; top:0px;
    }
    }
    
    0 讨论(0)
提交回复
热议问题