В некоторых кругах анимации и переходам уделяется слишком мало внимания, но то, как мы относимся к ним, очень важно для того, чтобы отличить хорошие веб-сайты от плохих.

Анимация происходит от латинского слова «animātiō», которое определяется как акт оживления чего-либо. Анимация в интерфейсных приложениях помогает сделать приложение более естественным и органичным. При правильном использовании они повышают удобство использования приложения пользователями. Простые переходы помогают передать отношения между различными компонентами, а хорошо спланированная оркестровка перенаправляет внимание пользователей на основной контент.

Подумайте, что в реальной жизни сразу ничего не происходит. Вы нажимаете кнопку, чтобы открыть дверь лифта, и дверь медленно перемещается, пока не откроется полностью. Вы нажимаете на одну из педалей своего автомобиля, и он либо ускоряется, либо замедляется. Но ничего из этого не происходит мгновенно. Итак, кто будет наслаждаться пользовательским интерфейсом, в котором вы нажимаете кнопку, и вдруг все содержимое меняется?

Работать с анимацией в Интернете всегда было сложно, но с появлением таких библиотек, как React, Vue, Angular, Svelte… писать их теперь намного проще. Поскольку Vue является выбранной библиотекой для создания интерфейса на Empathy.co, давайте сосредоточимся на ней и на том, как она предоставляет API-интерфейсы и компоненты для управления анимацией .

Как работают переходы Vue

Vue - еще одна популярная библиотека для создания многофункциональных и динамических интерфейсов веб-сайтов. Как и многие из его братьев и сестер, Vue - это библиотека, управляемая данными, которая работает, имея виртуальную DOM, которая затем транслируется в реальную DOM, избегая ненужных обновлений. Библиотека, управляемая данными, означает, что жизненный цикл компонентов зависит от изменений состояния приложения. Вы не обязательно добавляете или удаляете элементы в DOM. Вы изменяете данные, хранящиеся в ваших компонентах, и Vue выполняет необходимые обновления DOM за вас.

Итак, предположим, что мы хотим анимировать первый раз, когда появляется компонент. Как мы можем сделать это? Во Vue есть специальный компонент под названием переход. Этот компонент позволяет нам писать входящие / выходящие переходы для наших компонентов с помощью JavaScript или CSS.

<template>
  <transition name="slide-from-bottom-" appear>
    <h1>Hello world!</h1>
  </transition>
</template>
<script>
  export default {
    name: "HelloWorld"
  }
</script>
<style>
.slide-from-bottom--enter {
  opacity: 0;
  transform: translate3d(0, 25px, 0);
}
.slide-from-bottom--enter-active {
  transition: 0.25s ease-out;
  transition-property: opacity, transform;
}
</style>

Приведенный выше код заставит компонент HelloWorld плавно скользить снизу, когда он появится. В этом примере Vue добавит определенные классы CSS, в которых вы можете настроить стили для своей анимации. До сих пор мы использовали только классы enter и enter-active, но их больше.

export default {
  name: "CustomTransition",
  props: ["name", "appear"],
  render(createElement) {
    const child = this.$slots.default.?[0];
    if(child) {
      if (!child.data) {
        child.data = {};
      }
      child.data.transition = { ...this.$listeners, ...this.$props };
      return child;
    }
    return createElement();
  },
};

Замена transition в предыдущем примере этим новым компонентом CustomTransition будет делать то же самое. Конечно, компонент transition Vue делает гораздо больше, потому что ему приходится обрабатывать больше вариантов использования (например, JS-переходы, неанимируемые узлы, режим анимации). Дело в том, что официальные компоненты Vue используют те же API, которые Vue предоставляет для создания компонентов. Мы здесь ничего противозаконного не делаем.

Список переходов

Мы обсудили, как работают переходы входа и выхода, но есть третий вид переходов, который помогает сгладить впечатление, которое мы испытываем при посещении веб-сайта, и это подвижные переходы. Самый простой пример - это список, который сортируется. Обычно, когда это происходит, элементы мгновенно перемещаются в новое положение, но не лучше ли было бы для пользователей, если бы мы анимировали это движение?

В Vue есть специальный компонент для таких переходов списка - transition-group. Этот компонент очень похож на компонент transition: вы обертываете с ним компоненты, которые хотите анимировать, и готово. Он поддерживает переходы входа и выхода, а также новые перемещения.

Основное отличие состоит в том, что transition-group может обернуть более одного узла одновременно, а transition может обернуть только один. Из-за этого transition-group имеет новую опору с именем tag, которая представляет собой имя элемента оболочки, который он должен отображать в DOM. Это сделано потому, что в Vue 2 фрагменты не поддерживаются; к счастью, это изменилось в Vue 3.

Другое отличие состоит в том, что теперь виртуальные узлы слотов также должны содержать уникальный атрибут с именем key. Этот атрибут используется Vue для идентификации их между разными итерациями рендеринга. Вы скоро узнаете почему. Давайте напишем компонент для отображения списка чисел и кнопку для случайного перемешивания этого списка:

<template>
  <div>
    <button @click="shuffle">Shuffle</button>
    <transition-group tag="ul" name="list-" class="list">
      <li v-for="number in numbers" :key="number" class="item">{{ number }}</li>
    </transition-group>
  </div>
</template>
<script>
import CustomTransitionGroup from "./CustomTransitionGroup";
export default {
  name: "ListTransition",
  components: {
    CustomTransitionGroup,
  },
  data() {
    return {
      numbers: [0, 1, 2, 3, 4],
    };
  },
  methods: {
    shuffle() {
      this.numbers.sort(() => Math.random() - 0.5);
    },
  },
};
</script>
<style>
.list--enter {
  opacity: 0;
  transform: translate3d(0, 25px, 0);
}
.list--enter-active {
  transition: 0.25s ease-out;
  transition-property: transform, opacity;
}
.list--move {
  transition: transform 0.25s ease-in-out;
}
</style>

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

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

  • Запишите Первую позицию, также известную как исходная позиция.
  • Переместите элемент в место назначения и запишите позицию Last или новую позицию, которую он сейчас занимает.
  • Инвертировать новое положение в исходное, при этом просто вычитаются координаты для вычисления смещения и устанавливается это различие с помощью свойства transform. Теперь у вас есть элемент в новой позиции DOM, но похоже, что он расположен так же, как и раньше.
  • Воспроизвести анимацию. Удалите стили и позвольте браузеру интерполировать позиции.

Для его реализации Vue просто использует функцию рендеринга и одну из своих ловушек жизненного цикла, обновленную ловушку:

  • В функции render, помимо установки данных перехода виртуальных узлов (например, компонента transition), она также записывает исходное положение узлов.
  • В ловушке updated DOM синхронизирована, поэтому узлы находятся в новой позиции. Затем Vue записывает новое положение этих узлов. Наконец, он сравнивает исходное положение с новым и, если они отличаются, применяет CSS transform к перемещенным элементам. Это возможно благодаря атрибуту key, который теперь должен иметь узлы слотов.
export default {
  name: "CustomTransitionGroup",
  props: ["tag", "name", "appear"],
  render(createElement) {
    this.previousChildren = this.newChildren || [];
    this.newChildren = this.$slots.default || [];
    this.previousPositions = {};
this.previousChildren.forEach(this.syncTransitionData);
    // Record the *First* position of the nodes
    this.previousChildren.forEach(this.recordPreviousPosition);
    this.newChildren.forEach(this.syncTransitionData);
return createElement(this.tag, this.newChildren);
  },
  updated() {
    // The DOM has been updated with the new nodes.
    this.newPositions = {};
    // Record the *Last* position of the nodes
    this.newChildren.forEach(this.recordNewPosition);
    // *Invert* the positions so that the new one looks like the previous one.
    const movingNodes = this.newChildren.filter(this.applyTranslationIfNeeded);
    // Force a browser re-flow to put everything in place.
    document.body.getBoundingClientRect();
    // *Play* the animation
    movingNodes.forEach(this.playAnimation);
  },
  computed: {
    moveClassName() {
      return `${this.name}-move`;
    },
  },
  methods: {
    syncTransitionData(vNode) {
      if (!vNode.data) {
        vNode.data = {};
      }
      vNode.data.transition = {
        ...this.$props,
        ...this.$listeners,
      };
    },
    recordPreviousPosition(vNode) {
      this.previousPositions[vNode.key] = vNode.elm.getBoundingClientRect();
    },
    recordNewPosition(vNode) {
      this.newPositions[vNode.key] = vNode.elm.getBoundingClientRect();
    },
    applyTranslationIfNeeded(vNode) {
      const newPosition = this.newPositions[vNode.key];
      const previousPosition = this.previousPositions[vNode.key] || newPosition;
      // Calculate the deviation between the positions.
      const dx = previousPosition.left - newPosition.left;
      const dy = previousPosition.top - newPosition.top;
      if (dx || dy) {
        const style = vNode.elm.style;
        style.transform = `translate3d(${dx}px,${dy}px,0)`;
        style.transitionDuration = "0s";
        return true;
      }
      return false;
    },
    playAnimation(vNode) {
      const element = vNode.elm;
      element.classList.add(this.moveClassName);
      element.style.transform = element.style.transitionDuration = "";
      element.addEventListener(
        "transitionend",
        () => {
          element.classList.remove(this.moveClassName);
        },
        { once: true }
      );
    },
  },
};

Объяснить словами намного проще, чем кодом, но идея та же. Если вы сосредоточите свое внимание на функции render и ловушке updated, вы узнаете технику FLIP.

Выводы

К анимациям и переходам в сети следует относиться как к первоклассному гражданину. Такой подход - одно из отличий плохих сайтов от хороших. К счастью, самые известные библиотеки для создания веб-сайтов позволяют легко их добавлять. В случае Vue очень интересно посмотреть, как они решили проблему, экспортируя компоненты для переходов, которые повторно используют тот же API, который они предлагают своим пользователям.

Благодаря такому подходу здесь, в Empathy, мы смогли написать собственный transition-group компонент, который управляет различными типами переходов. Сначала он запускает оставленные, затем перемещаемые и, наконец, входящие. Как мы это сделали? Это для другого поста;)