bugfix> javascript > 投稿

ナビゲーションバーのCSSキーフレームアニメーションを開発しようとしています。

コードスニペットでアニメーションのしくみを確認できます。ユーザーがナビゲーションバーの要素をクリックすると、赤い線がアニメーション化されます。ナビゲーションバーの最初の要素はデフォルトでアクティブです(赤い線はこの要素の下にあります)。要素がクリックされると、JSはアニメーション要素のプロパティとクリックされた要素のプロパティを取得します。これらのプロパティは、単一のキーフレームルールに挿入される新しいキーフレームに組み込まれます。

2番目の要素をクリックすると、アニメーションは要素1->2から正常に実行されます。アニメーションも要素1->3から正常に実行されます。

しかし、アニメーションが要素1->2から再生された後、要素2->3からは再生されません。animationendイベントはトリガーされません(これをチェックしました)。今のところ、私は今後のアニメーションのみに関心があります。

調査した後、これを修正するためにいくつかの方法を試しました。 DOMリフローがトリガーされても、アニメーションクラスの削除と再接続は機能しません。アニメーションの再生状態を「実行中」から「一時停止」に変更しても機能しません。アニメーション名を「なし」に変更してから戻すなど、他の解決策では、アニメーションの終了時にアニメーション要素の位置がリセットされるなど、さらに問題が発生するだけです。私はこれを修正する方法を本当に知りません。

力ずくで強制するのではなく、このような柔軟なキーフレームアニメーションを作成したいと思います。ブルートフォースシナリオには、6つの異なるキーフレームルールを作成することが含まれます。ナビゲーションバーの任意の数の要素にコードを適用できるようにしたいと考えています。要素の追加ごとにキーフレームルールを追加するには、追加ごとに指数関数的に多くのコードが必要になります。

ありがとう。

〜デモ用のコード〜

var keyframes = findKeyframesRule('movey');
$(document).ready(() => {
    $('div.one').click(() => {
        if (!($('div.one').hasClass('active'))) {
            /* unfinished */
        }
    
    })
    $('div.two').click(() => {
        if (!($('div.two').hasClass('active'))) {
            
            /* transfer active class */
            $('div.active').removeClass('active');
            $('div.two').addClass('active');
            
            var left = ( parseInt($('div.absolute').css('left')) / $(window).width() ) * 100;
            
            /* reset keyframes before animation */
            clearKeyframes();
            /* add new keyframes for when div.two is clicked */
            keyframes.appendRule("0% { width: 15%; left: " + left + "%;}");
            keyframes.appendRule("49.99% { width: 30%; left: " + left + "%; right: 70%;}");
            keyframes.appendRule("50% { width: 30%; left: unset; right: 70%;}");
            keyframes.appendRule("100% { width: 15%; right: 70%;}");
            
            /* first animation - add animation class */
            if (!($('div.absolute').hasClass('animateMovey'))) {
                $('div.absolute').addClass('animateMovey');
            /* animations after first - remove and reattach animation class with new keyframes */
            } else {
                $('div.absolute').removeClass('animateMovey');
                $('div.absolute').addClass('animateMovey');
            }
            /* ensure animation occurs */
            $('div.animateMovey').on('animationend', () => {
                console.log('Animation ended');
            })
        }
    })
    $('div.three').click(() => {
        
        if (!($('div.three').hasClass('active'))) {
            $('div.active').removeClass('active');
            $('div.three').addClass('active');
            var left = ( parseInt($('div.absolute').css('left')) / $(window).width() ) * 100;
            var width = 45 - left;
            clearKeyframes();
            keyframes.appendRule("0% { width: 15%; left: " + left + "%;}");
            keyframes.appendRule("49.99% { width: " + width + "%; left: " + left + "%; right: 55%;}");
            keyframes.appendRule("50% { width: " + width + "%; left: unset; right: 55%;}");
            keyframes.appendRule("100% { width: 15%; right: 55%;")
            
            if (!($('div.absolute').hasClass('animateMovey'))) {
                $('div.absolute').addClass('animateMovey');
            } else {
                $('div.absolute').removeClass('animateMovey');
                $('div.absolute').addClass('animateMovey');
            }
            $('div.animateMovey').on('animationend', () => {
                console.log('Animation ended');
            })
        }
    })
})
function findKeyframesRule(rule) {
    var ss = document.styleSheets;
    for (var i = 0; i < ss.length; ++i) {
        for (var j = 0; j < ss[i].cssRules.length; ++j) {
            if (ss[i].cssRules[j].type == window.CSSRule.KEYFRAMES_RULE && ss[i].cssRules[j].name == rule)
                return ss[i].cssRules[j];
        }
    }
    return null;
}
function clearKeyframes() {
    for (var i = 0; i <= 3; ++i) {
        if (keyframes[0]) {
            var keyToRemove = keyframes[0].keyText;
            keyframes.deleteRule(keyToRemove);
        }
    }
}

body {
    margin: 0;
}
div.nav {
    position: relative;
    display: block;
    overflow: hidden;
    width: 100%;
}
div.nav div {
    float: left;
    width: 15%;
    height: 75px;
}
div.nav div:hover {
    opacity: 0.5;
}
div.one {
    background-color: #7a7a7a;
}
div.two {
    background-color: #9e9e9e;
}
div.three {
    background-color: #bdbdbd;
}
.active {
    box-shadow: inset 3px 5px 6px #000;
}
div.animateMovey {
    animation-name: movey;
    animation-duration: 0.6s;
    animation-fill-mode: forwards;
    animation-timing-function: ease-in-out;
}
div.relative {
    position: relative;
    width: 100%;
    height: 20px;
}
div.absolute {
    position: absolute;
    background-color: #ff8c69;
    width: 15%;
    height: 100%;
}
@keyframes movey {
    100% { }
}

<div>
    <div class="nav">
        <div class="one active"></div>
        <div class="two"></div>
        <div class="three"></div>
    </div>
    <div class="relative">
        <div class="absolute"></div>
    </div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>

回答 1 件
  • 興味深い質問です。この場合にイベントが再トリガーされない理由はわかりませんが、アプローチにいくつかの変更を提案します。

    アニメートを目指す transform そして opacity の代わりに width そして leftright (https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance)

    これを行う1つの方法は、各ボックスの下に個別の赤い要素を使用し、それを使用して左または右にスライドさせることです。 transform

    使用する animation-delay 延長効果と短縮効果を作成する

    アイテムの数に関係なく動作するように、アニメーションロジックを再利用してみてください。

    このエフェクトの難しい部分は、各線の不透明度を管理することです。私は使いました animationEnd それを助けるために、それは正常に動作するように見えます。

    コード例の追加コメント。アニメーションがアクティブなときにクリックを処理したり、アニメーション機能を統合したりすることで改善できます。また、アイテムの数に応じてアニメーションの長さを変えることもできます。

    let boxes = null;
    let lines = null;
    let fromIndex = 0;
    let toIndex = 0;
    const ANIMATION_DURATION = 0.1; // seconds
    const animation = {
      animating: false,
      lines: [],
      direction: "right",
      inOrOut: "in"
    };
    function getEls() {
      boxes = [...document.querySelectorAll(".box")];
      lines = [...document.querySelectorAll(".line")];
    }
    function setAnimationDuration() {
      lines.forEach((line) => {
        line.style.animationDuration = `${ANIMATION_DURATION}s`;
      });
    }
    function addEvents() {
      boxes.forEach((box, index) => {
        box.addEventListener("click", () => {
          // User has clicked the currently active box
          if (fromIndex === index) return;
          // Line is currently animating
          if (animation.animating) return;
          toIndex = index;
          updateActiveBox();
          handleLineAnimation();
        });
      });
      document.addEventListener("animationend", (e) => {
        // Maintain opacity on lines that animate in
        if (animation.inOrOut === "in") {
          e.target.style.opacity = 1;
        }
      });
    }
    function updateActiveBox() {
      boxes[fromIndex].classList.remove("active");
      boxes[toIndex].classList.add("active");
    }
    function updateActiveLine(line) {
      lines[fromIndex].classList.remove("active");
      line.classList.add("active");
    }
    function handleLineAnimation() {
      animation.animating = true;
      animation.lines = [];
      if (toIndex > fromIndex) {
        animation.direction = "right";
        for (let i = fromIndex; i <= toIndex; i++) {
          animation.lines.push(lines[i]);
        }
      } else {
        animation.direction = "left";
        for (let i = fromIndex; i >= toIndex; i--) {
          animation.lines.push(lines[i]);
        }
      }
      animate();
    }
    function animate() {
      const wait = (animation.lines.length - 1) * ANIMATION_DURATION * 1000;
      animation.inOrOut = "in";
      animateIn();
      setTimeout(() => {
        resetLine();
        updateActiveLine(lines[toIndex]);
        animation.inOrOut = "out";
        animateOut();
        setTimeout(() => {
          resetLine();
          onAnimationComplete();
        }, wait);
      }, wait);
    }
    function animateIn() {
      const {
        direction,
        lines
      } = animation;
      lines.forEach((line, index) => {
        // index = 0 is currently active, no need to animate in
        if (index > 0) {
          line.classList.add(`animate-in-${direction}`);
          line.style.animationDelay = `${(index - 1) * ANIMATION_DURATION}s`;
        }
      });
    }
    function animateOut() {
      const {
        direction,
        lines
      } = animation;
      lines.forEach((line, index) => {
        // lines.length - 1 is new active, don't animate out
        if (index < lines.length - 1) {
          line.classList.remove(`animate-in-${direction}`);
          line.classList.add(`animate-out-${direction}`);
          line.style.animationDelay = `${index * ANIMATION_DURATION}s`;
        }
      });
    }
    function resetLine() {
      const {
        direction,
        lines,
        inOrOut
      } = animation;
      lines.forEach((line) => {
        line.classList.remove(`animate-${inOrOut}-${direction}`);
        line.style.animationDelay = null;
        // After animating out, remove inline opacity
        if (inOrOut === "out") {
          if (!line.classList.contains("active")) {
            line.style.opacity = "";
          }
        }
      });
    }
    function onAnimationComplete() {
      animation.animating = false;
      fromIndex = toIndex;
    }
    function init() {
      getEls();
      setAnimationDuration();
      addEvents();
    }
    function reset() {
      fromIndex = 0;
      init();
      lines.forEach((line, index) => {
        line.classList.remove('active');
        line.style.opacity = "";
        boxes[index].classList.remove('active');
      });
      boxes[0].classList.add("active");
      lines[0].classList.add("active");
    }
    init();
    // DEBUG
    document.getElementById("debug").addEventListener("change", (e) => {
      document.querySelector("nav").classList.toggle("debug-on");
    });
    document.getElementById("add").addEventListener("click", (e) => {
      const div = document.createElement("div");
      div.classList.add("box");
      div.innerHTML = '<div class="new"></div><span class="line"></span>';
      document.querySelector("nav").appendChild(div);
      reset();
    });
    document.getElementById("remove").addEventListener("click", (e) => {
      const indexToRemove = boxes.length - 1;
      if (indexToRemove > 0) {
        const box = boxes[indexToRemove];
        box.parentNode.removeChild(box);
        reset();
      }
    });
    
    
    nav {
      display: flex;
      flex-wrap: wrap;
      overflow: hidden;
    }
    .debug-on .line {
      border: 1px solid;
      box-sizing: border-box;
      opacity: 0.2;
    }
    .box {
      display: flex;
      flex-direction: column;
      position: relative;
      float: left;
      flex: 0 0 15%;
      /* Allows the line to slide left or right with opacity: 1 */
      overflow: hidden;
    }
    .box>div {
      cursor: pointer;
      height: 75px;
    }
    .one {
      background-color: #7a7a7a;
    }
    .two {
      background-color: #9e9e9e;
    }
    .three {
      background-color: #bdbdbd;
    }
    .new {
      background-color: pink;
      border: 1px solid;
      box-sizing: border-box;
    }
    .line {
      background-color: #ff8c69;
      height: 20px;
      opacity: 0;
      pointer-events: none;
      width: 100%;
      animation-fill-mode: forwards;
      animation-timing-function: linear;
    }
    .active>div {
      box-shadow: inset 3px 5px 6px #000;
    }
    .box:hover div {
      opacity: 0.5;
    }
    .line.active {
      opacity: 1;
    }
    .line.show {
      opacity: 1;
    }
    .animate-in-right {
      animation-name: SLIDE_IN_RIGHT;
    }
    .animate-out-right {
      animation-name: SLIDE_OUT_RIGHT;
    }
    .animate-in-left {
      animation-name: SLIDE_IN_LEFT;
    }
    .animate-out-left {
      animation-name: SLIDE_OUT_LEFT;
    }
    @keyframes SLIDE_IN_RIGHT {
      from {
        opacity: 1;
        transform: translateX(-100%);
      }
      to {
        opacity: 1;
        transform: translateX(0);
      }
    }
    @keyframes SLIDE_OUT_RIGHT {
      from {
        opacity: 1;
        transform: translateX(0);
      }
      to {
        opacity: 1;
        transform: translateX(100%);
      }
    }
    @keyframes SLIDE_IN_LEFT {
      from {
        opacity: 1;
        transform: translateX(100%);
      }
      to {
        opacity: 1;
        transform: translateX(0);
      }
    }
    @keyframes SLIDE_OUT_LEFT {
      from {
        opacity: 1;
        transform: translateX(0);
      }
      to {
        opacity: 1;
        transform: translateX(-100%);
      }
    }
    
    /* for demo only */
    .debug {
      background: #eee;
      padding: 1rem;
      display: inline-flex;
      flex-direction: column;
      font: 14px/1 sans-serif;
      position: fixed;
      bottom: 0;
      right: 0;
    }
    .debug button {
      margin-top: 1rem;
      padding: .25rem;
    }
    
    
    <nav>
      <div class="box active">
        <div class="one"></div>
        <span class="line active"></span>
      </div>
      <div class="box">
        <div class="two"></div>
        <span class="line"></span>
      </div>
      <div class="box">
        <div class="three"></div>
        <span class="line"></span>
      </div>
    </nav>
    <br><br>
    <div class="debug">
      <label for="debug">Debug Lines <input type="checkbox" id="debug">
      </label>
      <button id="add">Add cell</button>
      <button id="remove">Delete cell</button>
    </div>
    
    

あなたの答え