Pull to refresh

Анимация в MooTools. Основы и не только.

Reading time 18 min
Views 7.3K
В данном топике я собираюсь свести все свои знания об анимации в MooTools воедино и рассмотреть темы, более углубленные, чем просто примеры использования плагинов. Теоретическая информация справедлива не только для MooTools, но и для других фреймворков. Начинающим будет интересно ознакомиться с возможностями фреймворка, а продолжающим — понять, как все это работает :). В статье приведено много примеров, есть довольно оригинальные, вот некоторые из них: 1, 2, 3. Приятного вам чтения.

Анимация в MooTools.


Основные принципы. Класс Fx.
Итак, сегодня мы займемся анимацией. Для начала необходимо понять, как реализованы принципы анимации в MooTools. Анимация в MooTools — это изменение какого-либо свойства какого-либо элемента во времени (переход), например, изменение CSS-свойства left — это движение по горизонтали, а background-color — плавное изменение цвета заливки.

Для создания анимационных эффектов в MooTools используется класс Fx. Все классы, реализующие какую-либо анимацию наследуются от Fx. В этом классе определены вспомогательные методы для анимации:
  • start — этот метод запускает переход.
  • set — изменяет значение анимируемого параметра. Вызывается на каждом шаге перехода. По смыслу это абстрактный метод, если его можно так назвать, т.е. каждая реализация анимационного плагина должна переопределить set по-своему.
  • cancel — отменяет текущую анимацию.
  • pause — приостанавливает текущую анимацию.
  • resume — возобновляет приостановленную анимацию.
Конструктор принимает следующие опции:
  • fps — количество кадров в секунду для анимации. Характеризует, если можно так сказать, «плавность» анимации — чем он больше, тем «плавнее».
  • unit — единица измерения, например «em», «px», «%». Используется для преобразований CSS-свойств.
  • link — опция, характеризующая поведение анимации, когда во время перехода был вызван start. Может принимать следующие значения (пример с разными значениями link):
    • 'ignore' — все вызовы start во время анимации игнорируются. Используется по умолчанию.
    • 'cancel' — текущая анимация будет остановлена и начата новая с параметрами, пришедшими в start.
    • 'chain' — все вызовы start во время анимации будут добавлены в цепь и их анимация начнется сразу после того, как закончится текущая.
  • duration — длительность анимации.
  • transition — уравнение перехода. Подробнее уравнения переходов будут рассмотрены ниже.
Работает Fx следующим образом. В вызов метода start передаются начальное и конечное значения изменяемого параметра, затем запускается таймер, который периодически (период равен 1 / fps) вызывает метод step, где на каждом шаге анимации вычисляется значение анимируемого параметра. А вычисляется оно так:
  1. Время, прошедшее со старта перехода до текущего момента делится на duration, таким образом получается число от 0 до 1 характеризующее прогресс анимации. Назовем его progress.
  2. Это число передается в функцию перехода (будет рассмотрено ниже). Результат вычисления функции перехода назовем delta.
  3. Значение же анимируемого параметра вычисляется так: param = (to − from) × delta + from, где from — начальное значение параметра, to — конечное значение параметра. Очевидно, что при delta равном 0 мы получаем начальное значение параметра, а при 1 — конечное.
После вычисления значения параметра, в методе set происходит уже непосредственная визуализация анимации (например, изменение CSS-свойства left).

Стоит подчеркнуть, что сам по себе класс Fx является абстрактным, то есть ничего сам не анимирует, он оперирует только общими данными для реализации принципов анимации. Непосредственной анимацией занимаются наследники Fx. В основном для анимации элементов на HTML-страницах используются классы, которые анимируют различные CSS-свойства элементов. Об этих классах мы поговорим, а также рассмотрим случай с созданием своего класса, который реализует анимацию не-CSS-свойств.

Уравнения переходов. Написание собственного уравнения.
Согласитесь, не очень красиво бы смотрелась анимация перемещения элемента на странице, если бы он резко стартовал и так же резко останавливался в конце пути. Так происходит, если приращение delta (которое упоминалось выше) на протяжении всего времени остается постоянным. Это называется — линейный переход, его уравнение delta = progress. Если взять последовательно несколько значений progress от 0 до 1 и подставить в уравнение, то получим обычную прямую, которая резко начинается в значении 0 и резко заканчивается в 1.

Надеюсь, из предыдущего абзаца стало примерно понятно, что такое уравнение перехода — это закон, по которому следует вычислять приращение delta. Благо, в MooTools есть целый набор таких законов, которые позволят сделать анимацию плавной, резкой, затухающей, прыгающей и т.д., но нам этого окажется мало — мы напишем еще и свое уравнение. Но об этом позже. Сейчас же посмотрим, какие бывают стандартные уравнения:
  • Linear — прямая (delta = progress).
  • Quad — квадратичная функция (delta = progress2).
  • Cubic — кубическая функция (delta = progress3).
  • Quart — delta = progress4.
  • Quint — delta = progress5.
  • Pow — общая степенная функция delta = progressx (по умолчанию степень равна 6).
  • Expo — экспоненциальная функция (delta = 2(progress − 1) × 8).
  • Circ — четверть окружности (delta = 1 − sin(acos(progress))).
  • Sine — кусок синусоиды (delta = 1 − sin((1 − progress) × π / 2)).
  • Back — сначала оттягивает delta в минус, а потом плавно доводит до 1.
  • Bounce — прыгающий переход.
  • Elastic — эластичный переход (единственная ассоциация — резинка :).
Все эти уравнения доступны в классе Fx.Transitions. Они используются в качестве значения опции transition конструктора Fx. Помимо прямого использования этих уравнений можно еще применять к ним модификаторы:
  • easeIn — используется по умолчанию и ничего не меняет.
  • easeOut — вычисляет значение delta по формуле 1 − transition(1 − progress), таким образом разворачивая кривую перехода.
  • easeInOut — до середины перехода вычисляет delta по формуле transtition(2 × progress), а после — по (2 − transition(2 × progress)) / 2, совмещая в одном переходе две кривых: прямую и развернутую.
Для наглядности привожу кривые некоторых наиболее интересных уравнений.



А вот и та самая прыгающая анимация.



Четверть окружности.



Резинка.



Экспоненциальная функция.



Пятая степень.



Кусок синусоиды.



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

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

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



Как видно, на первом промежутке определена «квадро-функция» для плавного подъема из нуля, далее набор из прямых с разными наклонами и длинами, и в конце четверть окружности для плавного подъема в единицу. Вот как это выглядит в виде кривых:



Добавим эту функцию в объект Fx.Transitions следующим образом:

    Fx.Transitions.extend({
      Heartbeat: function(x){
        if (x < 0.3)
          return Math.pow(x, 4) * 49.4;
          
        if (x < 0.4)
          return 9 * x - 2.3;

        if (x < 0.5)
          return -13 * x + 6.5;

        if (x < 0.6)
          return 4 * x - 2;
          
        if (x < 0.7)
          return 0.4;

        if (x < 0.75)
          return 4 * x - 2.4;

        if (x < 0.8)
          return -4 * x + 3.6;

        if (x >= 0.8)
          return 1 - Math.sin(Math.acos(x));
      }
    });

Теперь можно им воспользоваться, передав в качестве опции transition (замечание: в примере используется плагин Fx.Morph, который будет рассмотрен ниже, а пока его можно просто считать наследником Fx, понимающем в конструкторе все опции, что понимает Fx):

      var fx = new Fx.Morph($('Heart'), { transition: Fx.Transitions.Heartbeat, duration: 900, link: 'chain' });
      
      // Производим анимацию 10 раз.
      for (var i = 0; i < 10; i++){
        // Увеличиваем сердце.
        fx.start({
          'width': 265,
          'height': 238,
          'margin-left': -20,
          'margin-top': -20
        });

        // Уменьшаем сердце.
        fx.start({
          'width': 225,
          'height': 198,
          'margin-left': 0,
          'margin-top': 0
        });
      }

В HTML-коде присутствует всего лишь один элемент:

      <img id="Heart" src="./images/heart.png" />

Живой пример лежит тут. Интересный эффект: во время тестирования скрипта я слушал Drum’n’Bass и казалось, что биение этого сердечка происходит очень в такт музыке (попробуйте сами).

Ok, может мы и не добились идеальной человеческой кардиограммы, но свою функцию все же написали. Как оказалось, это не так сложно :).

Цепочки. Сцепление анимационных преобразований.
Очень часто бывает нужно задать несколько последовательных преобразований так, чтобы каждое следующее осуществлялось после окончания предыдущего. Если просто вызвать их последовательно, то по умолчанию каждый следующий вызов будет игнорироваться (смотрим опцию link класса Fx). Если же указать link равным 'chain', то все последующие вызовы добавятся в цепочку и будут выполнены последовательно.

Однако, существует и специальный синтаксис для создания цепочек. Реализуются они в классе Chain. Он содержит аж целых три метода:
  • chain — дописывает функцию в конец цепочки.
  • callChain — вызывает следующую в цепочке функцию и удаляет ее из цепочки.
  • clearChain — очищает цепочку.
На основании Chain напишем класс, который будет упралять зеленой гусеницей и будет поддерживать цепочки вызовов. С помощью цепочки вызовов мы заставим гусеницу проползти по восьмерке.

Сама гусеница представляет собой обычный div, у которого изменяются CSS-свойства left, top, width и height создавая эффект движения (ах, да, еще изменяется его цвет для реалистичности). Шаг гусеницы будет состоять из двух этапов: первый — передвижение ее головы в нужное место, а второй — подтягивание хвоста к голове. В примере использован Fx.Morph, позволяющий анимировать одновременно несколько CSS-свойств, который будет рассмотрен подробнее ниже.

    var CaterpillarController = new Class({
      // Реализуем методы Chain в классе.
      Implements: Chain,
      
      // Максимальный и минимальный размеры гусеницы.
      largeSize: 200,
      smallSize: 10,

      // Конструктор.
      initialize: function(caterpillar){
        this.caterpillar = $(caterpillar);
        
        // Создаем для элемента гусеницы экземпляр Fx.Morph со сцеплением последующих вызовов start.
        this.fx = new Fx.Morph(this.caterpillar, { duration: 900, transition: Fx.Transitions.Expo.easeOut, link: 'chain' });
        
        // Как только завершается анимация второго этапа шага гусеницы вызываем следующую функцию в нашей цепи.
        this.fx.addEvent('chainComplete', this.callChain.bind(this));
        
        return this;
      },

      // Этот метод отвечает за один шаг гусеницы.
      // На вход передается измерение (dimension: горизонтальное — true, вертикальное — false)
      // и направление (direction: положительное — true, отрицательное — false).
      move: function(dimension, direction){
        var dimensions = this.caterpillar.getCoordinates();
        var options1, options2;
        
        // Формируем объекты с опциями для двух этапов шага гусеницы.
        if (dimension){
          // Движение по горизонтали.
          if (direction){
            options1 = { 'width': dimensions.width + this.largeSize };
            options2 = { 'left': dimensions.left + this.largeSize, 'width': this.smallSize };
          } else {
            options1 = { 'left': dimensions.left - this.largeSize, 'width': this.largeSize + this.smallSize };
            options2 = { 'width': this.smallSize };
          }
        } else {
          // Движение по вертикали.
          if (direction){
            options1 = { 'height': dimensions.height + this.largeSize };
            options2 = { 'top': dimensions.top + this.largeSize, 'height': this.smallSize };
          } else {
            options1 = { 'top': dimensions.top - this.largeSize, 'height': this.largeSize + this.smallSize };
            options2 = { 'height': this.smallSize };
          }
        }
        
        // Расширим объекты добавив в них свойство для изменения цвета (для пущей реалистичности).
        $extend(options1, { 'background-color': '#7CCB26' });
        $extend(options2, { 'background-color': '#4C9004' });
        
        // Стартуем два этапа шага
        // (второй этап начнется после окончания первого, вспоминаем link равный 'chain').
        this.fx.start(options1);
        this.fx.start(options2);
        
        return this;
      }
    });

    window.addEvent('domready', function(){
      // Следующая последовательность вызовов move опишет восьмерку.
      new CaterpillarController('Caterpillar').move(true, true).
        chain(function() { this.move(false, true); }).
        chain(function() { this.move(true, false); }).
        chain(function() { this.move(false, true); }).
        chain(function() { this.move(true, true); }).
        chain(function() { this.move(false, false); }).
        chain(function() { this.move(true, false); }).
        chain(function() { this.move(false, false); });
    });

Живой пример тут. А тут уже пример никак к цепочкам не относящийся — просто полет фантазии, но прикольный.

Анимация собственных переменных.
До сих пор мы использовали анимацию различных CSS-свойств. Но, как я указывал в начале статьи, класс Fx можно использовать для анимации чего угодно. Поэтому было бы неплохо попробовать написать своего наследника Fx. За основу возьмем улучшеный вариант скрипта реостата, о котором я писал в этом топике. Что же в нем можно анимировать? Например, угол поворота индикатора. Вспомним, что если сделать одиночный клик на реостате, то индикатор сразу же оказывается в месте нажатия. А что если анимировать этот переход? Было бы забавно, да и написать-то надо всего несколько строк :). Итак, что же нужно для создания полноценного наследника Fx:
  • добавить в описание класса Extends: Fx для наследования.
  • передать опции в базовый конструктор.
  • переопределить метод set для применения значения анимируемого параметра к «физике» параметра, в данном случае повернуть индикатор на значение угла.
  • вызвать start для начала анимации.
Код метода set:

  set: function(now){
    // now — текущее значение угла во время анимации.
    this.oldMouseAngle = this.mouseAngle = this.angle = now;
    this.updateIndicatorPosition();
  }

Метод, который реагировал на клик мыши вместо мгновенного изменения угла теперь вызывает метод start и передает в него текущее значение угла поворота индикатора и значение угла, на который он должен быть повернуть после окончания анимации:

  captureMouse: function(e){
    this.captured = true;
    
    var mouseAngle = this.getMouseAngle(e);
    if ((mouseAngle >= this.options.minAngle) && (mouseAngle <= this.options.maxAngle))
      // Стартуем анимацию перехода индикатора в место клика.
      this.start(this.angle, mouseAngle);
  }

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

Стандартные анимационные плагины.


Fx.Tween.
Этот класс является простейшим классом, анимирующим любое CSS-свойство элемента. Для работы ему нужна ссылка на анимируемый элемент, имя анимируемого CSS-свойства и диапазон значений. Пример создания:

  var myFx = new Fx.Tween(element, [, options]);

На этом классе основан пример с применением различных уравнений переходов:

      // Здесь мы будем менять CSS-свойство top у элемента с идентификатором 'Ball'.
      var fx = new Fx.Tween('Ball', { property: 'top', duration: 900, link: 'cancel' });
      var transitionSelect = $('Transition');
      var modifierSelect = $('Modifier');
      var test = $('Test');
      
      // При возникновении одного из следующих событий запустить анимацию.
      modifierSelect.addEvent('change', startAnimation);
      transitionSelect.addEvent('change', startAnimation);
      test.addEvent('click', startAnimation);
      
      function startAnimation(){
        // Получаем имя перехода в виде 'sine:in'.
        fx.options.transition = transitionSelect.value + modifierSelect.value;
        
        // Двигаем шарик с выбранным переходом.
        fx.start(60, 400);
      }

В HTML-коде примера два выпадающих списка, анимируемый шарик и кнопка на всякий пожарный.

Fx.Morph.
Используется чаще, чем Fx.Tween потому что может одновременно анимировать несколько CSS-свойств элемента. Конструктор почти такой же как и у Fx.Tween за исключением отсутствия опции property, вместо нее в метод start передается объект, описывающий, какие CSS-свойства необходимо анимировать и в каких диапазонах. Например:

  morph.start({
    'margin-left': [0, 10],
    'background-color': ['#EEE', '#555']
  });

означает, что анимироваться будут два CSS-свойства: margin-left (от 0 до 10) и background-color (от '#EEE' до '#555'). Диапазоны значений можно и не указывать, а только указать конечное значение, тогда в качестве начального будет взято текущее значение, указанное в стиле элемента. В качестве примера можно привести использование Fx.Morph для создания анимированного меню:

      var menuHeadItems = $$('#Menu .MenuHeadItem');
      
      menuHeadItems.each(function(item, i){
        var morph = new Fx.Morph(item, { duration: 900, transition: Fx.Transitions.Elastic.easeOut, link: 'cancel' });
        
        // Для каждого пункта меню стартуем анимацию с прогрессивной задержкой 100i ms.
        // Анимируется два параметра: opacity и margin-left. Таким образом элемент "выезжает"
        // слева одновременно с изменением прозрачности до 1.
        morph.start.delay(i * 100, morph, {
          'opacity': [0, 1],
          'margin-left': 0
        });
        
        item.addEvents({
          // При наведении курсора мыши отодвигаем элемент и делаем его темнее.
          'mouseenter': function(){
            morph.start({
              'margin-left': 8,
              'background-color': '#DDD'
            });
          },
          // При уведении курсора мыши возвращаем элемент в исходное состояние.
          'mouseleave': function(){
            morph.start({
              'margin-left': 0,
              'background-color': '#EEE'
            });
          }
        });
      });

А также менюшку можно посмотреть.
Официальный пример.

Fx.Slide.
Довольно полезный класс, когда нужно чтобы что-то откуда-то выехало или заехало. Конструктор очень похож на все предыдущие, поэтому особо останавливаться на нем не буду. Скажу только, что он понимает еще несколько опций, самая важная их которых — это mode, определяющая направление «выезжания»: вертикальное или горизонтальное. На основе этого класса сделаны еще два примера с анимированными меню: первый и второй.
Официальный пример.

Fx.Elements.
Позволяет удобно анимировать любое количество CSS-свойств для любого количества элементов одновременно. В его конструктор передается массив элементов, над которыми будут осуществляться преобразования. А вся прелесть заложена в методе start — туда передается массив объектов для преобразования. Получается, каждому элементу из массива, переданного в конструкторе соответствует элемент объект из массива, переданного в start. Таким образом осуществляется массовая анимация элементов.

В примере ниже изменяется прозрачность массива элементов при наведении на них курсора, причем чем ближе элемент к тому, что в данный момент под курсором, тем меньше его прозрачность, а чем дальше — тем больше:

      var elements = $$('#ElementsContainer .Element');
      var elementsFx = new Fx.Elements(elements, { duration: 500, link: 'cancel' });
      
      elements.each(function(element, i){
        // При наведении курсора на элемент пересчитываем прозрачность соседей.
        element.addEvent('mouseenter', function(){
          var arg = {};
          
          // Для всех соседних элементов вычисляем значение их прозрачности
          // на основании их удаленности от текущего.
          elements.each(function(element, j){
            arg[j] = { opacity: 1 - Math.min(Math.abs(i - j), 5) / 8 };
          });
          
          // Стартуем изменение прозрачности.
          elementsFx.start(arg);
        });
      });

Живой пример.
Интересный вариант меню от MooTools.

Надеюсь, что читателям было интересно закопаться поглубже в реализацию анимации в MooTools. Большинство из описанного здесь должно быть применимо и к другим фреймворкам.
Спасибо за внимание.
Tags:
Hubs:
+55
Comments 39
Comments Comments 39

Articles