【问题标题】:How To Sync CSS Animations Across Multiple Elements?如何跨多个元素同步 CSS 动画?
【发布时间】:2011-06-17 20:39:25
【问题描述】:

我想通过 CSS(特别是 -webkit-animation)对页面上的两个元素进行动画处理。动画本身只是向上和向下反弹一个元素。一个元素始终显示并弹跳,另一个元素在鼠标悬停(悬停)之前没有动画。

我的问题是:无论何时开始第二个元素的动画,是否可以同步(让两个元素同时到达它们的顶点等)两个元素的动画?

这是我的 HTML:

<div id="bouncy01">Drip</div>
<div id="bouncy02">droP</div>

还有我的 CSS:

@-webkit-keyframes bounce {
    0% {-webkit-transform: translateY(0px);}
    25% {-webkit-transform: translateY(-2px);}
    50% {-webkit-transform: translateY(-4px);}
    75% {-webkit-transform: translateY(-2px);}
    100% {-webkit-transform: translateY(0px);}
}
#bouncy01,
#bouncy02 {
    margin:10px;
    float: left;
    background: #ff0000;
    padding: 10px;
    border: 1px solid #777;
}
#bouncy01 {
    -webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}
#bouncy02 {
    background: #ffff00;
}
#bouncy02:hover {
    -webkit-animation:bounce 0.25s ease-in-out infinite alternate;
}

最后是该问题的工作演示:http://jsfiddle.net/7ZLmq/2/

(要查看问题,请将鼠标悬停在黄色块上)

【问题讨论】:

    标签: animation webkit css


    【解决方案1】:

    我认为它本身不可能,但您实际上可以通过使用弹跳包装器和一些位置改变来破解类似的功能

    html:

    <div id="bouncywrap">
        <div id="bouncy01">Drip</div>
        <div id="bouncy02">droP</div>
    <div>
    

    CSS:

    @-webkit-keyframes bounce {
        0% { padding-top:1px;}
    /* using padding as it does not affect position:relative of sublinks
     * using 0 instead of 0 b/c of a box-model issue,
     * on kids wiht margin, but parents without margin, just try out
     */
        50% { padding-top:5px;} /*desired value +1*/
        100% { padding-top:1px;}
    }
    
    #bouncy01,
    #bouncy02 {
        margin:10px;
        background: #ff0000;
        padding: 10px;
        border: 1px solid #777;
        width:30px;
           position:absolute;
    }
    #bouncywrap {
        -webkit-animation:bounce 0.125s ease-in-out infinite;
        position:relative;
        width:140px;
        height:50px;
    /*    background:grey; /*debug*/
    }
    #bouncy02 {
        background: #ffff00;
        left:60px;
        top:2px; /*half of desired value, just a fix for the optic*/
    }
    #bouncy02:hover {
        position:relative; /*here happens the magic*/
        top:0px;
    }
    

    演示http://jsfiddle.net/A92pU/1/

    【讨论】:

    • 哦,是的...我忘了提到这种方法在我的情况下不起作用(我的元素位于页面的不同侧)。不过谢谢!
    • 为什么不呢?一个 0px 高、空、透明的 div 不会影响设计和可用性。
    • 就像这里jsfiddle.net/A92pU/2,如果您提供更多详细信息/告诉我实际设计,我可能会提供更多帮助
    • 看起来很有趣 =)
    【解决方案2】:

    我一直在寻找此处提出的替代解决方案,因为:

    1. 我正在为背景颜色设置动画 - 无法在接受的答案中使用定位魔法。
    2. 我想避免在我的应用中对非常简单的动画进行计算。

    经过进一步研究,我通过bealearts 发现了this module

    它公开了一个非常简洁的 API,让您可以通过引用动画名称在应用中保持动画同步:

    import sync from 'css-animation-sync';
    sync('spinner');
    

    由于这似乎有点好得令人难以置信,我在这个小提琴中测试了库(这是一个单一的短文件),很高兴报告 it works(将鼠标悬停在第三张图片上,看到我快速同步到第二张图片的动画):)。

    致谢:我使用 Simurai 的 this fiddle 中的动画作为我的小提琴的基础。

    如果有人想复制这种同步背后的机制,代码是开放的,但本质上,它使用动画本身的事件侦听器作为同步点:

    window.addEventListener('animationstart', animationStart, true);
    window.addEventListener('animationiteration', animationIteration, true);
    

    希望这有助于下一个人寻找解决此问题的方法。

    【讨论】:

    • 不错的库,唯一找到真正解决问题的方法的库!但是当动画停止(例如显示:无)然后再次运行时,它会出现问题。它也应该使用animationcancel,用于观察停止动画(例如display:none
    • 听起来像是一个有用的拉取请求;)
    【解决方案3】:

    Web Animations API 现在允许非常精确和轻松地控制动画。

    有多种方法可以声明一个 Web 动画,但由于我们从 CSS 开始,这里是如何挂钩它:

    // when the animation starts
    document.querySelector("#bouncy02")
      .addEventListener("animationstart", (evt) => {
      // double check it the animation we wanted
      if (evt.animationName === "bounce") {
        // retrieve both Animation objects
        const myAnim = findAnimByName(evt.target, "bounce");
        const otherAnim = findAnimByName(document.querySelector("#bouncy01"), "bounce");
        // update mine to act as if it started
        // at the same time as the first one
        myAnim.startTime = otherAnim.startTime;
      }
    });
    // simple helper to find an Animation by animationName
    function findAnimByName(elem, name) {
      // get all the active animations on this element
      const anims = elem.getAnimations();
      // return the first one with the expected animationName
      return anims.find((anim) => anim.animationName === name);
    }
    @keyframes bounce {
        0% {transform: translateY(0px);}
        25% {transform: translateY(-2px);}
        50% {transform: translateY(-4px);}
        75% {transform: translateY(-2px);}
        100% {transform: translateY(0px);}
    }
    #bouncy01,
    #bouncy02 {
        margin:10px;
        float: left;
        background: #ff0000;
        padding: 10px;
        border: 1px solid #777;
    }
    #bouncy01 {
        animation:bounce 0.25s ease-in-out infinite alternate;
    }
    #bouncy02 {
        background: #ffff00;
    }
    #bouncy02:hover {
        animation:bounce 0.25s ease-in-out infinite alternate;
    }
    <div id="bouncy01">Drip</div>
    <div id="bouncy02">droP</div>

    请注意,虽然令人惊讶的是它还没有流行,但这个 API 实际上已经存在了一段时间,而且它的 browser support(除 IE 之外的所有)都非常好。

    【讨论】:

      【解决方案4】:

      看起来你可以只堆叠两个黄色的,然后在 :hover 上通过父元素切换可见性。

      您需要动画始终在运行,否则您会遇到同步问题。

      我稍微修改了你的代码以获得this.

      【讨论】:

      • 这与@Valerij 解决方案非常相似,但我认为前者更简洁,因为它使用父级来触发悬停时的弹跳变化位置,而不是使用重复的内容。看一下:jsfiddle.net/A92pU/2(可以在OP里找到)
      【解决方案5】:

      您可以使用setInterval 来维持第一个动画的动画状态,并给另一个动画一个负延迟以在鼠标悬停时寻找其匹配的关键帧。

      在“操作 CSS 动画”部分了解 state-maintaining-interval-thing here;阅读有关寻求 here 的负面延迟。

      【讨论】:

        【解决方案6】:

        在添加类之前计算并添加延迟:

        function getTime (seconds) {
          const msDuration = (seconds * 1000).toFixed(0);
          const currentTime = (new Date()).getTime();
          const msDelay = msDuration - (currentTime % msDuration);
          return (msDelay / 1000).toFixed(4);
        }
        
        $('div').css({animationDelay: getTime(0.6) + "s"}).addClass('animating');
        

        https://codepen.io/s-flhti/pen/GRoVXZw

        【讨论】:

          【解决方案7】:

          鼠标悬停时:

          1. 从两个元素中删除动画类
          2. 使用requestAnimationFrame(() =&gt; { ... add here "bounce" class to both elements })

          应该很好地同步。

          【讨论】:

            【解决方案8】:

            这是我为不同元素和pseudo-elements同步动画的小任务,多亏了上面的想法,解决方案变得非常简单。我希望这个小代码可以帮助某人。

            window.addEventListener('animationstart', e =>
                e.animationName == 'rgb' && e.target.getAnimations({subtree: true}).forEach(e => e.startTime = 0), true)
            

            【讨论】:

            • 这非常有用。从技术上讲,它仍然是一个实验性 API,但多年来似乎已在所有主要浏览器中得到支持。
            【解决方案9】:

            您可以在设置交替状态的根元素上设置一个类,然后使用计时器交替该类

            CSS

            .alt .bouncy {
               padding-top:5px !important;
            }
            .bouncy {
               padding-top: 1px;
               transition: padding-top ease 500ms;
            }
            

            HTML

            <div class="container">
                <div class="bouncy">Drip</div>
                <div class="bouncy">droP</div>
            <div>
            

            Javascript

            $(function () {
              setInterval(() => $(".container").toggleClass("alt"), 1000)
            })
            

            通过这种方式,transition 和 timer 完成 css 动画的工作,但由单个主开关(容器)控制。

            【讨论】:

              【解决方案10】:

              使用 bealearts 的 css animation sync lib,您可以轻松同步动画。但是在 0.4.1 版本(今天最新)中,它有错误:

              1. 同步丢失,当所有同步动画元素停止时,它们会动画(例如display:none
              2. 第一个动画在重新启动后从非零帧开始,这可能很关键。
              3. 在第一个动画开始后一段时间,它会闪烁。
              4. 不适用于伪元素:before:after

              要修复所有这些错误(但 4 个),您可以修复库代码:

              -添加动画取消回调

              function animationCancel(event) {
                  if (shouldSync(event)) {
                    elements.delete(event.target);
                  }
              }
              window.addEventListener('animationcancel', animationCancel, true);
              

              -修改animation-start回调处理第一个动画

              function animationStart(event) {
                  if (shouldSync(event)) {
                    const { target: element, timeStamp } = event;
                    elements.add(element);
              
                    let diff;
                    if (elements.size == 1){
                      diff = 0;
                      lastIterationTimestamp = timeStamp;
                    }else diff = timeStamp - lastIterationTimestamp;
              
                    element.style.setProperty('animation-delay', `-${diff}ms`);
                  }
              }
              

              -清空init()方法的主体。

              这里是固定使用示例:

              //Sample demo code
              jQuery(function($){
                window.cssAnimationSync('pulse-visible');
                let animateGroup = function(selector){
                  let hideNext = function(){
                    let next = $(selector + ':visible:first');
                    if (next.length){
                        next.fadeOut();
                        setTimeout(hideNext, 200 + Math.random()*200);
                     }else setTimeout(showNext, 200 + Math.random()*200);
                  }
                  let showNext = function(){
                     let next = $(selector + ':hidden:first');
                     if (next.length){
                        next.fadeIn();
                        setTimeout(showNext, 200 + Math.random()*200);
                     }else setTimeout(hideNext, 200 + Math.random()*200);
                  };
                  showNext();
                };
                animateGroup('.pulsar_sync');
                animateGroup('.pulsar');
              });
              
              
              //Fixed library code
              /** @see https://github.com/bealearts/css-animation-sync */
              window.cssAnimationSync = function(animationNameOrNames) {
                const animationNames = new Set(
                  Array.isArray(animationNameOrNames) ? animationNameOrNames : [animationNameOrNames]
                );
                const elements = new Set();
                let animationDuration;
                let isPaused = false;
                let lastIterationTimestamp = 0;
              
                const api = {
                  getElements() {
                    return elements;
                  },
              
                  free() {
                    window.removeEventListener('animationiteration', animationIteration, true);
                    window.removeEventListener('animationstart', animationStart, true);
              
                    this.start();
                    elements.clear();
                  },
              
                  start() {
                    elements.forEach((el) => {
                      if (validate(el)) {
                        if (isPaused) {
                          el.style.removeProperty('animation-play-state');
                        } else {
                          el.style.removeProperty('animation');
                        }
                      }
                    });
                    isPaused = false;
                  },
              
                  stop() {
                    isPaused = false;
                    elements.forEach((el) => {
                      if (validate(el)) {
                        el.style.setProperty('animation', 'none');
                      }
                    });
                  },
              
                  pause() {
                    isPaused = true;
                    elements.forEach((el) => {
                      if (validate(el)) {
                        el.style.setProperty('animation-play-state', 'paused');
                      }
                    });
                  }
                };
              
                function shouldSync(event) {
                  return animationNames.has(event.animationName);
                }
              
                function validate(el) {
                  const isValid = document.body.contains(el);
                  if (!isValid) {
                    elements.delete(el);
                  }
                  return isValid;
                }
              
                function init() {
                  //setTimeout(restart, animationDuration);
                }
              
                function restart() {
                  api.stop();
                  setTimeout(api.start, 50);
                }
              
                function animationStart(event) {
                  if (shouldSync(event)) {
                    const { target: element, timeStamp } = event;
                    elements.add(element);
              
                    let diff;
                    if (elements.size == 1){
                      diff = 0;
                      lastIterationTimestamp = timeStamp;
                    }else diff = timeStamp - lastIterationTimestamp;
              
                    element.style.setProperty('animation-delay', `-${diff}ms`);
                  }
                }
                
                function cssToMs(time) {
                    const num = parseFloat(time);
                    let unit = time.match(/m?s/);
              
                    if (!unit) return 0;
              
                    [unit] = unit;
              
                    switch (unit) {
                      case 's':
                        return num * 1000;
                      case 'ms':
                        return num;
                      default:
                        return 0;
                    }
                 }
              
              
                function animationIteration(event) {
                  if (shouldSync(event)) {
                    const { target: element, timeStamp } = event;
                    elements.add(element);
              
                    lastIterationTimestamp = timeStamp;
                    if (!animationDuration) {
                      animationDuration = cssToMs(window.getComputedStyle(element).animationDuration);
                      init();
                    }
                  }
                }
              
                function animationCancel(event) {
                  if (shouldSync(event)) {
                    elements.delete(event.target);
                  }
                }
              
                window.addEventListener('animationiteration', animationIteration, true);
                window.addEventListener('animationstart', animationStart, true);
                window.addEventListener('animationcancel', animationCancel, true);
              
                return api;
              };
              @keyframes pulse-visible {
                  0% { opacity: 0.85;}
                  30% { opacity: 0.85;}
                  40% { opacity: 0.55;}
                  45% { opacity: 0;}
                  85% { opacity: 0;}
                  90% { opacity: 0.55;}
                  100% { opacity: 0.85;}
              }
              
              @keyframes pulse-visible-copy {
                  0% { opacity: 0.85;}
                  30% { opacity: 0.85;}
                  40% { opacity: 0.55;}
                  45% { opacity: 0;}
                  85% { opacity: 0;}
                  90% { opacity: 0.55;}
                  100% { opacity: 0.85;}
              }
              .pulsar
              {
               animation-name: pulse-visible-copy;
              }
              .pulsar_sync
              {
               animation-name: pulse-visible;
              }
              .pulsar, .pulsar_sync
              {
               animation-duration: 0.7s;
               animation-iteration-count: infinite;
               animation-timing-function: linear;
               
               /*styles not depending on animation*/
               display: inline-block;
               width: 30px;
               height: 30px;
               margin: 5px;
               border: 3px solid red;
               border-radius: 25%;
              }
              <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
              With cssAnimationSync
              <div style='min-height:50px;'>
              <div class="pulsar_sync" style="display: none;"></div>
              <div class="pulsar_sync" style="display: none;"></div>
              <div class="pulsar_sync" style="display: none;"></div>
              <div class="pulsar_sync" style="display: none;"></div>
              <div class="pulsar_sync" style="display: none;"></div>
              </div>
              Without
              <div>
              <div class="pulsar" style="display: none;"></div>
              <div class="pulsar" style="display: none;"></div>
              <div class="pulsar" style="display: none;"></div>
              <div class="pulsar" style="display: none;"></div>
              <div class="pulsar" style="display: none;"></div>
              </div>

              【讨论】:

                猜你喜欢
                • 2019-11-01
                • 2014-12-15
                • 2023-03-13
                • 2017-07-04
                • 1970-01-01
                • 1970-01-01
                • 2021-04-22
                • 2015-03-31
                • 1970-01-01
                相关资源
                最近更新 更多