jQuery для начинающих. Часть 9. Пишем плагины анимации

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

Материалы данной статьи включены в учебник «jQuery для начинающих». Учебник распространяется бесплатно, и сопровождается интерактивными примерами.

Обучаем animate

Большинство разработчиков уже сталкивалось с функцией animate в jQuery, это достаточно удобный инструмент для анимирования элементов на странице, а я расскажу как вы можете легко изменить поведение данной функции под свои цели.

Для начала затравка – метод animate манипулирует объектом $.fx, в себе он содержит следующие методы:

  • cur – возвращает текущее значение атрибута
  • custom – отвечает за запуск анимации
  • hide/show – простые функции hide/show
  • step – каждый отдельный шаг анимации, т.е. вызывается много-много раз
  • update – данная функция отвечает за применение атрибутов

Из всего приведенного мы будем расширять объект $.fx.step. Возьмем достаточно тривиальную задачу – заставим плавно изменить цвет шрифта для заданного набора элементов:

1
$('p').animate({color:'#ff0000'});

Приведенный выше код не даст никакого эффекта, нам потребуется прокачать $.fx.step:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
(function($) {
// color - свойство с которым мы будем работать
$.fx.step.color = function(fx) {
  
    // настройки анимации
    fx.options;
  
    // свойство которое изменяем
    fx.prop;
  
    // начальные значения
    fx.start;
  
    // результирующие значения
    fx.end;
  
    // коэффициент, изменяется от 0 до 1
    fx.pos;
  
    // еденицы измерения
    fx.unit;
  
};
})(jQuery);

Перед вами основные свойства объекта $.fx – это и есть наши кирпичики, с их помощью будем строить нашу анимацию. Перед работой стоит заглянуть внутрь каждого из приведенных свойств:

01
02
03
04
05
06
07
08
09
10
11
// добавим логирование внутрь нашего плагина
console.log(fx.options, fx.prop, fx.start, fx.end, fx.pos, fx.unit);
 
/* вывод в консоли:
Object duration=5000 old=false curAnim=Object orig=Object / color / 0 / #ff0000 / 0 / px
Object duration=5000 old=false curAnim=Object orig=Object / color / 0 / #ff0000 / 0.0008351307317917 / px
Object duration=5000 old=false curAnim=Object orig=Object / color / 0 / #ff0000 / 0.0009670080649619717 / px
Object duration=5000 old=false curAnim=Object orig=Object / color / 0 / #ff0000 / 0.0010877292712792586 / px
Object duration=5000 old=false curAnim=Object orig=Object / color / 0 / #ff0000 / 0.0012155411253085835 / px
..
*/

Как и предполагалось – метод вызывается N кол-во раз, в зависимости от продолжительности анимации, при этом fx.pos постепенно наращивает своё значение с 0 до 1 (аналогично ведет себя и параметр fx.state, только знаков после запятой у него поменьше, есть ли еще какая разница – я не знаю).

fx.pos, по умолчанию, наращивается линейно, если надо как-то иначе — то стоит посмотреть на easing плагин или дочитать статью до конца (об этом я уже упоминал в статье Эффекты)

Теперь не мешало бы заставить эту систему работать, начнем с инициализации крайних значений (листинг функции parseColor вы сможете найти в самом примере):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// инициализация должна проходить лишь однажды
if (fx.state == 0 ) {
    // получаем текущее значение элемента
    var start = $.curCSS(fx.elem,'color');
  
    // разбираем на составляющие текущее состояние элемента
    // start содержит строку вида rgb(255, 255, 255)
            var res = start.match(/rgb\(([0-9]{1,3}),\s?([0-9]{1,3}),\s?([0-9]{1,3})\)/);
 
            if (!res) {
                // или #ffffff;
                var res = start.match(/^#(\w{2})(\w{2})(\w{2})$/);
                var rgb = [parseInt(res[1], 16), parseInt(res[2], 16), parseInt(res[3], 16)];
            } else {
                var rgb = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])];
            }
 
    // составляющие запихиваем в fx.start
    fx.start = rgb;
  
    // разбираем что нам нужно получить, и закидываем в fx.end
    fx.end   = parseColor(fx.end);
}

Теперь необходимо изменить текущее значение свойств и применить его:

1
2
3
4
5
6
7
// нехитрое вычисление
var R = Math.round(((fx.end[0] - fx.start[0]) * fx.pos) + fx.start[0]);
var G = Math.round(((fx.end[1] - fx.start[1]) * fx.pos) + fx.start[1]);
var B = Math.round(((fx.end[2] - fx.start[2]) * fx.pos) + fx.start[2]);
  
// непосредственно измение свойств элемента
fx.elem.style.color = 'rgb('+R+','+G+','+B+')';

Всё вместе:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$.fx.step.color = function(fx) {
    // инициализация должна проходить лишь однажды
    if (fx.state == 0 ) {
        // получаем текущее значение элемента
        var start = $.curCSS(fx.elem,'color');
  
        // разбираем на составляющие текущее состояние элемента
        // start содержит строку rgb(255, 255, 255)
        var res = start.match(/rgb\(([0-9]{1,3}),\s?([0-9]{1,3}),\s?([0-9]{1,3})\)/);
  
        // составляющие запихиваем в fx.start
        fx.start = [parseInt(res[1]), parseInt(res[2]), parseInt(res[3])];
  
        // разбираем что нам нужно получить, и закидываем в fx.end
        fx.end   = parseColor(fx.end);
    }
    // нехитрое вычисление
    var R = Math.round(((fx.end[0] - fx.start[0]) * fx.pos) + fx.start[0]);
    var G = Math.round(((fx.end[1] - fx.start[1]) * fx.pos) + fx.start[1]);
    var B = Math.round(((fx.end[2] - fx.start[2]) * fx.pos) + fx.start[2]);
  
    // непосредственно измение свойств элемента
    fx.elem.style.color = 'rgb('+R+','+G+','+B+')';
};

Результатом станет плавное перетекание исходного цвета к красному (#F00 == #FF0000 == 255,0,0)

Своя анимация

Теперь, познав основы, можем попробовать анимировать некоторый неизвестный параметр shiver (да пусть дрожат пред нами):

1
$('p').animate({shiver:100});

Идем по натоптанному пути:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
(function($) {
    var left;
    $.fx.step.shiver = function(fx) {
  
        // инициализация должна проходить лишь однажды
        if (fx.state == 0 ) {
  
            // начальное расположение элемента от левого края
            // дрожать будем по горизонтали
            left = $.curCSS(fx.elem,'left');
  
            // устанавливаем относительное расположение элемента
            fx.elem.style.position = 'relative';
        }
    };
})(jQuery);

Так же реализуем саму дрожь элемента:

01
02
03
04
05
06
07
08
09
10
11
12
13
// если это последняя итерация - вернем всё на своё место
if (fx.now == fx.end) {
    fx.elem.style.left = left;
} else if (Math.round(1000 * fx.pos)%2==0) {
    // четная итерация
    // непосредственно измение свойств элемента
    fx.elem.style.left = - Math.round(Math.random()*fx.end) + fx.unit;
  
} else {
    // нечетная итерация
    // непосредственно измение свойств элемента
    fx.elem.style.left = + Math.round(Math.random()*fx.end) + fx.unit;
}

Собираем вместе, и смотрим на результат работы

Пишем свой easing плагин

Возвращаясь к easing’у – мы можем легко написать свою функцию, которой будет следовать анимация. Дабы особо не фантазировать – я взял пример из статьи o MooTools – очень наглядный пример с сердцебиением, которое описывается следующими функциями:

Heart Beat Functions

Теперь напишем соответствующий код:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$.extend($.easing, {
    /**
     * Heart Beat
     *
     * @param x
     * @param t current time
     * @param b begInnIng
     * @param c change In value
     * @param d duration
     *
     */
    heart:function(x, t, b, c, d) {
        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));
    }
});

Чуть-чуть расширю пример дополнительными функциями (развернем и скомбинируем):

01
02
03
04
05
06
07
08
09
10
heartIn:function (x, t, b, c, d) {
    return $.easing.heartIn(x, t, b, c, d);
},
heartOut:function (x, t, b, c, d) {
    return c - $.easing.heartIn(1 - x, t, b, c, d) + b;
},
heartInOut: function (x, t, b, c, d) {
    if (t < d/2) return $.easing.heartIn(1 - x, t, b, c, d) + b;
    return $.easing.heartOut(1 - x, t, b, c, d) + b;
}

Графически это будет выглядеть так:

HeartIn

HeartOut

HeartInOut

Результат лучше не показывать кардиолагам.