Можно ли ждать нескольких CSS-анимаций с использованием JS? - PullRequest
2 голосов
/ 26 сентября 2019

У нас есть способ определить, когда анимация заканчивается, используя JS:

const element = $('#animatable');
element.addClass('being-animated').on("animationend", (event) => {
  console.log('Animation ended!');
});
@keyframes animateOpacity {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes animatePosition {
  0% {
    transform: translate3d(0, 0, 0);
  }
  100% {
    transform: translate3d(0, 15px, 0);
  }
}

#animatable.being-animated {
  animation: animateOpacity 1s ease 0s forwards, animatePosition 2s ease 0s forwards;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="animatable">I'm probably being animated.</div>

И, как вы можете видеть, JS по праву, потому что я подключен к событию animationend, говорит мне: «Да, анимацияготово ", но не знает, что будет после, и я пропускаю второй.

Нет очереди анимации?Конечно, CSS должен зарегистрировать эти вещи где-то в системе до того, как они будут запущены, и я могу войти внутрь.

Ответы [ 3 ]

3 голосов
/ 26 сентября 2019

Отказ от ответственности : Я не думаю, что jQuery важен для ответа на этот вопрос, и это ухудшило бы как нагрузку, так и производительность во время выполнения, если бы другие увидели этот код после просмотра этого ответа.Итак, я отвечу с помощью ванильного JavaScript, чтобы помочь как можно большему количеству людей с этим, но если вы хотите использовать jQuery, вы все равно можете применять те же понятия.

Ответ: Нетt очередь анимации, но вы можете создать свою собственную.

Например, вы можете связать данные об анимации с целевым элементом, используя замыкание, и / или Map (В приведенном ниже фрагменте кода я на самом деле использовал WeakMap в попытке помочь в сборке мусора).Если вы сохраняете состояния анимации как true, когда они завершены, вы можете проверить и в конечном итоге запустить другой обратный вызов, когда все они true, или отправить свое собственное событие.Я использовал пользовательский подход к событиям, потому что он более гибкий (способен добавлять несколько обратных вызовов).

Следующий код должен дополнительно помочь вам избежать ожидания ВСЕХ анимаций в тех случаях, когда вы на самом деле заботитесь только о паре конкретных,Он также должен позволять обрабатывать события анимации несколько раз и для нескольких отдельных элементов (попробуйте запустить фрагмент и щелкнуть несколько раз по полям)

const addAnimationEndAllEvent = (() => {
  const weakMap = new WeakMap()

  const initAnimationsObject = (element, expectedAnimations, eventName) => {
    const events = weakMap.get(element)
    const animationsCompleted = {}
    for (const animation of expectedAnimations) {
      animationsCompleted[animation] = false
    }
    events[eventName] = animationsCompleted
  }

  return (element, expectedAnimations, eventName = 'animationendall') => {
    if (!weakMap.has(element)) weakMap.set(element, {})

    if (expectedAnimations) {
      initAnimationsObject(element, expectedAnimations, eventName)
    }

    // When any animation completes...
    element.addEventListener('animationend', ({ target, animationName }) => {
      const events = weakMap.get(target)
      
      // Use all animations, if there were none provided earlier
      if (!events[eventName]) {
        initAnimationsObject(target, window.getComputedStyle(target).animationName.split(', '), eventName)
      }
      
      const animationsCompleted = events[eventName]
      
      // Ensure this animation should be tracked
      if (!(animationName in animationsCompleted)) return

      // Mark the current animation as complete (true)
      animationsCompleted[animationName] = true

      // If every animation is now completed...
      if (Object.values(animationsCompleted).every(
        isCompleted => isCompleted === true
      )) {
        const animations = Object.keys(animationsCompleted)

        // Fire the event
        target.dispatchEvent(new CustomEvent(eventName, {
          detail: { target, animations },
        }))

        // Reset for next time - set all animations to not complete (false)
        initAnimationsObject(target, animations, eventName)
      }
    })
  }
})()

const toggleAnimation = ({ target }) => {
  target.classList.toggle('being-animated')
}

document.querySelectorAll('.animatable').forEach(element => {
  // Wait for all animations before firing the default event "animationendall"
  addAnimationEndAllEvent(element)

  // Wait for the provided animations before firing the event "animationend2"
  addAnimationEndAllEvent(element, [
    'animateOpacity',
    'animatePosition'
  ], 'animationend2')

  // Listen for our added "animationendall" event
  element.addEventListener('animationendall', ({detail: { target, animations }}) => {
    console.log(`Animations: ${animations.join(', ')} - Complete`)
  })

  // Listen for our added "animationend2" event
  element.addEventListener('animationend2', ({detail: { target, animations }}) => {
    console.log(`Animations: ${animations.join(', ')} - Complete`)
  })

  // Just updated this to function on click, so we can test animation multiple times
  element.addEventListener('click', toggleAnimation)
})
.animatable {
  margin: 5px;
  width: 100px;
  height: 100px;
  background: black;
}

@keyframes animateOpacity {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes animatePosition {
  0% {
    transform: translate3d(0, 0, 0);
  }
  100% {
    transform: translate3d(0, 15px, 0);
  }
}

@keyframes animateRotation {
  100% {
    transform: rotate(360deg);
  }
}

.animatable.being-animated {
  animation:
    animateOpacity 1s ease 0s forwards,
    animatePosition 1.5s ease 0s forwards,
    animateRotation 2s ease 0s forwards;
}
<div class="animatable"></div>
<div class="animatable"></div>
1 голос
/ 26 сентября 2019

@ Удивительный фрагмент BDawg более гибкий и тщательный, он, безусловно, заслуживает того, чтобы быть принятым ответом.Тем не менее, я был вдохновлен, чтобы увидеть, возможен ли менее многословный подход.Вот что я придумал.

Это довольно очевидно, но в основном концепция заключается в том, что индексы всех свойств анимации коррелируют, и мы можем использовать это, чтобы найти имя анимации, которая заканчивается последней.

const getFinalAnimationName = el => {
  const style = window.getComputedStyle(el)
  
  // get the combined duration of all timing properties
  const [durations, iterations, delays] = ['Duration', 'IterationCount', 'Delay']
    .map(prop => style[`animation${prop}`].split(', ')
      .map(val => Number(val.replace(/[^0-9\.]/g, ''))))
  const combinedDurations = durations.map((duration, idx) =>
    duration * iterations[idx] + delays[idx])
  
  // use the index of the longest duration to select the animation name
  const finalAnimationIdx = combinedDurations
    .findIndex(d => d === Math.max(...combinedDurations))
  return style.animationName.split(', ')[finalAnimationIdx]
}

// pipe your element through this function to give it the ability to dispatch the 'animationendall' event
const addAnimationEndAllEvent = el => {
  const animationendall = new CustomEvent('animationendall')
  el.addEventListener('animationend', ({animationName}) =>
    animationName === getFinalAnimationName(el) &&
      el.dispatchEvent(animationendall))
  return el
}

// example usage
const animatable = document.querySelector('.animatable')
addAnimationEndAllEvent(animatable)
  .addEventListener('animationendall', () => console.log('All animations have finished'))
.animatable {
  width: 50px;
  height: 50px;
  background-color: red;
  position: relative;
  left: 0;
  animation: 1.5s slidein, 1s fadein;
}

@keyframes slidein {
  0% { left: 100vw; }
  100% { left: 0; }
}

@keyframes fadein {
  0% { opacity: 0; }
  100% { opacity: 1; }
}
<div class="animatable"></div>
0 голосов
/ 26 сентября 2019

Первая техника:

Добавьте класс к элементу, затем обработайте каждую анимацию и дождитесь ее окончания, несмотря ни на что.Это наиболее распространенный способ выполнения анимации по классам.

Согласно комментарию Кайидо и указанию, он ожидает каждую отдельную анимацию независимо от ее продолжительности.Это было мотивом всего этого: создайте хорошую анимацию и сделайте так, чтобы JS осознал ее (независимо от того, насколько она сложная / длинная), заканчивая ее, чтобы потом можно было связать другие вещи.

Если вы этого не сделаете, у вас может быть хорошая анимация, запущенная и внезапно обрезанная чем-то другим, и ... это плохо.

const triggerAnimationWithClass = (classToAdd, toWhat) => {
    const element = document.querySelector(toWhat);
    /**
     * Initialize the count with 1, because you'll always have at least one animation no matter what.
     */
    let animationCount = 1;
    return new Promise((resolve, reject) => {
        element.addEventListener('animationend', (event) => {
            if((window.getComputedStyle(element).animationName).split(',').length - animationCount === 0) {
                /**
                 * Remove the current function being hooked once we're done. When a class gets added that contains any N number of animations,
                 * we're running in a synchronous environment. There is virtually no way for another animation to happen at this point, so, we're
                 * surgically looking at animations that only happen when our classToAdd gets applied then hooking off to not create conflicts.
                 */
                element.removeEventListener('animationend', this);

                const animationsDonePackage = {
                    'animatedWithClass': classToAdd,
                    'animatedElement': toWhat,
                    'animatedDoneTime': new Date().getTime()
                };

                resolve(animationsDonePackage);
            } else {
                animationCount++;
            }
        });
        element.classList.add(classToAdd);
    });
}

Это обрабатывает добавление нескольких классов.Давайте предположим, что извне кто-то добавляет еще один класс в то же время (странно, но, скажем так), вы добавили свой.Все анимации на этом элементе затем обрабатываются как одна, и функция будет ожидать завершения всех из них.

Второй метод:

Основано на ответе @ B-Dawg.Обработайте набор анимаций, основываясь на имени (имена CSS-анимации), а не на классе, пожалуйста, прочитайте послесловие:

const onAnimationsComplete = ({element, animationsToLookFor}) => {
    const animationsMap = new WeakMap();

    if(!animationsMap.has(element)) {
        const animationsCompleted = {};

        for(const animation of animationsToLookFor) {
            animationsCompleted[animation] = false;
        }

        animationsMap.set(element, animationsCompleted);
    }

    return new Promise((resolve, reject) => {
        // When any animation completes...
        element.addEventListener('animationend', ({target, animationName, elapsedTime}) => {
            const animationsCompleted = animationsMap.get(target);

            animationsCompleted[animationName] = true;

            // If every animation is now completed...
            if(Object.values(animationsCompleted).every(isCompleted => isCompleted === true)) {
                const animations = Object.keys(animationsCompleted);

                // Reset for next time - set all animations to not complete (false)
                animations.forEach(animation => animationsCompleted[animation] = false);

                //Remove the listener once we're done.
                element.removeEventListener('animationend', this);

                resolve({
                    'animationsDone': animationsToLookFor
                });
            }
        });
    });
};

В этом есть ошибка.Предполагая, что новая анимация происходит, скажем, может быть, из нового класса, если она не помещена в список animationsToLookFor, это никогда не разрешается.

Попытка исправить это, но если мы говорим о точном спискеанимаций, которые вы ищете, это переход.

...