Анимацията и преходът получават твърде малко уважение в някои среди, но как се отнасяме към тях е от съществено значение за разграничаването на добрите уебсайтове от лошите.

Анимацията произлиза от латинската дума „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. Тази техника е ефективен начин за преход на елемент, който се е преместил. Състои се от четири стъпки, които определят всяка една от буквите на акронима:

  • Запишете Първа позиция, известна още като първоначалната позиция.
  • Преместете елемента до местоназначението му и запишете Последната позиция или новата позиция, която има сега.
  • Инвертирайте новата позиция към оригиналната, която просто изважда координатите за изчисляване на изместването и задава тази разлика с помощта на свойството 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 компонент, който организира различните типове преходи. Първо изпълнява тези за напускане, след това тези за преместване и накрая тези за влизане. Как го направихме? Това е за друг пост ;)