en.javascript.info/5-animation/3-js-animation/article.md
2015-02-27 13:21:58 +03:00

29 KiB
Raw Blame History

JS-Анимация

В этой главе мы рассмотрим устройство браузерной анимации. Она примерно одинаково реализована во всех фреймворках.

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

Анимация при помощи JavaScript и современная CSS-анимация дополняют друг друга. [cut]

Основы анимации

С точки зрения HTML/CSS, анимация -- это постепенное изменение стиля DOM-элемента. Например, увеличение координаты style.left от 0px до 100px сдвигает элемент.

Код, который производит изменение, вызывается таймером. Интервал таймера очень мал и поэтому анимация выглядит плавной. Это тот же принцип, что и в кино: для непрерывной анимации достаточно 24 или больше вызовов таймера в секунду.

Псевдо-код для анимации выглядит так:

var timer = setInterval(function() {
    показать новый кадр 
    if (время вышло) clearInterval(timer);     
}, 10)

Задержка между кадрами в данном случае составляет 10 ms, что означает 100 кадров в секунду.

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

Если анимация требует большого количества вычислений, то нагрузка процессора может доходить до 100% и вызывать ощутимые "тормоза" в работе браузера. В таком случае, задержку можно увеличить. Например, 40мс дадут нам 25 кадров в секунду, что очень близко к кинематографическому стандарту в 24 кадра.

[smart header="setInterval вместо setTimeout"] Мы используем setInterval, а не рекурсивный setTimeout, потому что нам нужен один кадр за промежуток времени, а не фиксированная задержка между кадрами. В статье описана разница между setInterval и рекурсивным setTimeout. [/smart]

Пример

Например, передвинем элемент путём изменения element.style.left от 0 до 100px. Изменение происходит на 1px каждые 10мс.

<script>
function move(elem) {

  var left = 0; // начальное значение

  function frame() { // функция для отрисовки
    left++;
    elem.style.left = left + 'px' 

    if (left == 100) { 
      clearInterval(timer); // завершить анимацию
    }
  }

  var timer = setInterval(frame, 10) // рисовать каждые 10мс
}
</script>

<div onclick="move(this.children[0])" class="example_path">
  <div class="example_block"></div>
</div>

Кликните для демонстрации: [iframe height=60 src="move100" link]

Структура анимации

У анимации есть три основных параметра:

`delay`
Время между кадрами (в миллисекундах, т.е. 1/1000 секунды). Например, 10мс.
`duration`
Общее время, которое должна длиться анимация, в мс. Например, 1000мс.
`step(progress)`
Функция **`step(progress)`** занимается отрисовкой состояния анимации, соответствующего времени `progress`.

Каждый кадр выполняется, сколько времени прошло: progress = (now-start)/duration. Значение progress меняется от 0 в начале анимации до 1 в конце. Так как вычисления с дробными числами не всегда точны, то в конце оно может быть даже немного больше 1. В этом случае мы уменьшаем его до 1 и завершаем анимацию.

Создадим функцию animate, которая получает объект со свойствами delay, duration, step и выполняет анимацию.

function animate(opts) {
  
  var start = new Date; // сохранить время начала 

  var timer = setInterval(function() {

    // вычислить сколько времени прошло
    var progress = (new Date - start) / opts.duration;
    if (progress > 1) progress = 1;

    // отрисовать анимацию
    opts.step(progress);
    
    if (progress == 1) clearInterval(timer); // конец :)
     
  }, opts.delay || 10); // по умолчанию кадр каждые 10мс

}

Пример

Анимируем ширину элемента width от 0 до 100%, используя нашу функцию:

function stretch(elem) {
  animate({ 
    duration: 1000, // время на анимацию 1000 мс
    step: function(progress) {
      elem.style.width = progress*100 + '%';
    }
  });
}

Кликните для демонстрации: [iframe height=60 src="width" link]

Функция step может получать дополнительные параметры анимации из opts (через this) или через замыкание.

Следующий пример использует параметр to из замыкания для анимации бегунка:

function move(elem) {
  var to = 500;

  animate({
    duration: 1000,
    step: function(progress) {
      // progress меняется от 0 до 1, left от 0px до 500px
      elem.style.left = to*progress + "px";
    }
  });

}

Кликните для демонстрации: [iframe height=60 src="move" link]

Временная функция delta

В сложных анимациях свойства изменяются по определённому закону. Зачастую, он гораздо сложнее, чем простое равномерное возрастание/убывание.

Для того, чтобы можно было задать более хитрые виды анимации, в алгоритм добавляется дополнительная функция delta(progress), которая вычисляет текущее состояние анимации от 0 до 1, а step использует её значение вместо progress.

В animate изменится всего одна строчка. Было:

...
opts.step(progress);
...

Станет:

...
opts.step( opts.delta(progress) );
...
//+ hide="Раскрыть код animate с delta"
function animate(opts) {

  var start = new Date; 

  var timer = setInterval(function() {

    var progress = (new Date - start) / opts.duration;
    if (progress > 1) progress = 1;

    opts.step( opts.delta(progress) );

    if (progress == 1) clearInterval(timer);

  }, opts.delay || 10);

}

Такое небольшое изменение добавляет много гибкости. Функция step занимается всего лишь отрисовкой текущего состояния анимации, а само состояние по времени определяется в delta.

Разные значения delta заставляют скорость анимации, ускорение и другие параметры вести себя абсолютно по-разному.

Рассмотрим примеры анимации движения с использованием различных delta.

Линейная delta

Самая простая функция delta -- это та, которая просто возвращает progress.

function linear(progress) {
  return progress;
}

То есть, как будто никакой delta нет. Состояние анимации (которое при передвижении отображается как координата left) зависит от времени линейно.

График:

По горизонтали - progress, а по вертикали - delta(progress).

Пример:

<div onclick="move(this.children[0], linear)" class="example_path">
	<div class="example_block"></div>
</div>

Здесь и далее функция move будет такой:

function move(elem, delta, duration) {
  var to = 500;
  
  animate({
    delay: 10,
    duration: duration || 1000, 
    delta: delta,
    step: function(delta) {
      elem.style.left = to*delta + "px"    
    }
  });
  
}

То есть, она будет перемещать бегунок, изменяя left по закону delta, за duration мс (по умолчанию 1000мс).

Кликните для демонстрации линейной delta:

В степени n

Вот еще один простой случай. delta - это progress в n-й степени . Частные случаи - квадратичная, кубическая функции и т.д.

Для квадратичной функции:

function quad(progress) {
  return Math.pow(progress, 2)
}

График квадратичной функции:

Пример для квадратичной функции (клик для просмотра):

Увеличение степени влияет на ускорение. Например, график для 5-й степени:

И пример:

Функция delta описывает развитие анимации в зависимости от времени.

В примере выше -- сначала медленно: время идёт (ось X), а состояние анимации почти не меняется (ось Y), а потом всё быстрее и быстрее. Другие графики зададут иное поведение.

Дуга

Функция:

function circ(progress) {
    return 1 - Math.sin(Math.acos(progress))
}

График:

Пример:

Back: стреляем из лука

Эта функция работает по принципу лука: сначала мы "натягиваем тетиву", а затем "стреляем".

В отличие от предыдущих функций, эта зависит от дополнительного параметра x, который является "коэффициентом упругости". Он определяет расстояние, на которое "оттягивается тетива".

Её код:

function back(progress, x) {
    return Math.pow(progress, 2) * ((x + 1) * progress - x)
}

График для x = 1.5:

Пример для x = 1.5:

Отскок

Представьте, что мы отпускаем мяч, он падает на пол, несколько раз отскакивает и останавливается.

Функция bounce делает то же самое, только наоборот: "подпрыгивание" начинается сразу.

Эта функция немного сложнее предыдущих и использует специальные коэффициенты:

function bounce(progress) {
  for (var a = 0, b = 1, result; 1; a += b, b /= 2) {
    if (progress >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2)
    }
  }
}

Код взят из MooTools.FX.Transitions. Конечно же, есть и другие реализации bounce.

Пример:

Упругая анимация

Эта функция зависит от дополнительного параметра x, который определяет начальный диапазон.

function elastic(progress, x) {
  return Math.pow(2, 10 * (progress-1)) * Math.cos(20*Math.PI*x/3*progress)
}

График для x=1.5:

Пример для x=1.5:

Реверсивные функции (easeIn, easeOut, easeInOut)

Обычно, JavaScript-фреймворк предоставляет несколько delta-функций. Их прямое использование называется "easeIn".

Иногда нужно показать анимацию в обратном режиме. Преобразование функции, которое даёт такой эффект, называется "easeOut".

easeOut

В режиме "easeOut", значение delta вычисляется так: deltaEaseOut(progress) = 1 - delta(1 - progress)

Например, функция bounce в режиме "easeOut":

function bounce(progress) {
  for (var a = 0, b = 1, result; 1; a += b, b /= 2) {
    if (progress >= (7 - 4 * a) / 11) {
      return -Math.pow((11 - 6 * a - 11 * progress) / 4, 2) + Math.pow(b, 2);
    }
  }
}

*!*
function makeEaseOut(delta) {  // преобразовать delta 
  return function(progress) {
    return 1 - delta(1 - progress);
  }
}
*/!*

*!*var bounceEaseOut = makeEaseOut(bounce);*/!*

Кликните для демонстрации:

Давайте посмотрим, как преобразование easeOut изменяет поведение функции:

Красным цветом обозначен easeIn, а зеленым - easeOut.

  • Обычно анимируемый объект сначала медленно скачет внизу, а затем, в конце, резко достигает верха..
  • А после easeOut он сначала прыгает наверх, а затем медленно скачет внизу.

При easeOut анимация развивается в обратном временном порядке.

Если есть анимационный эффект, такой как подпрыгивание -- он будет показан в конце, а не в начале (или наоборот, в начале, а не в конце).

easeInOut

А еще можно сделать так, чтобы показать эффект и в начале и в конце анимации. Соответствующее преобразование называется "easeInOut".

Его код выглядит так:

if (progress <= 0.5) { // первая половина анимации)
  return delta(2 * progress) / 2;
} else { // вторая половина
  return (2 - delta(2 * (1 - progress))) / 2;
}

Например, easeInOut для bounce:

У этого примера длительность составляет 3 секунды для того, что бы хватило времени для обоих эффектов(начального и конечного).

Код, который трансформирует delta:

function makeEaseInOut(delta) {  
  return function(progress) {
    if (progress < .5)
      return delta(2*progress) / 2;
    else
      return (2 - delta(2*(1-progress))) / 2;
  }
}

bounceEaseInOut = makeEaseInOut(bounce);

Трансформация "easeInOut" объединяет в себе два графика в один: easeIn для первой половины анимации и easeOut -- для второй.

Например, давайте посмотрим эффект easeOut/easeInOut на примере функции circ:

Как видно, график первой половины анимации представляет собой уменьшенный "easeIn", а второй -- уменьшенный "easeOut". В результате, анимация начинается и заканчивается одинаковым эффектом.

Графопостроитель

Для наглядной демонстрации в действии различных delta, как нормальных(easeIn), так и измененных(easeOut,easeInOut), я подготовил графопостроитель.

Открыть в новом окне.

Выберите функцию и нажмите Рисовать!

  • easeIn - базовое поведение: медленная анимация в начале, с постепенным ускорением.
  • easeOut - поведение, обратное easeIn: быстрая анимация на старте, а затем все медленней и медленней.
  • easeInOut - слияние обоих поведений. Анимация разделяется на две части. Первая часть - это `easeIn`, а вторая - `easeOut`.

Для примера, попробуйте "bounce".

[summary] Процесс анимации полностью в ваших руках благодаря delta. Вы можете сделать ее настолько реалистичной, насколько захотите.

И кстати. Если вы когда-нибудь изучали математику... Некоторые вещи все же бывают полезны в жизни :) Можно продумать и сделать красиво.

Впрочем, исходя из практики, можно сказать, что варианты delta, описанные выше, покрывают 95% потребностей в анимации. [/summary]

Сложные варианты step

Анимировать можно все, что угодно. Вместо движения, как во всех предыдущих примерах, вы можете изменять прозрачность, ширину, высоту, цвет... Все, о чем вы можете подумать!

Достаточно лишь написать соответствующий step.

Подсветка цветом

Функция highlight, представленная ниже, анимирует изменение цвета.

function highlight(elem) {
  var from = [255,0,0], to = [255,255,255]
  animate({
    delay: 10,
    duration: 1000,
    delta: linear,
    step: function(delta) {
      elem.style.backgroundColor = 'rgb(' +
        Math.max(Math.min(parseInt((delta * (to[0]-from[0])) + from[0], 10), 255), 0) + ',' +
        Math.max(Math.min(parseInt((delta * (to[1]-from[1])) + from[1], 10), 255), 0) + ',' +
        Math.max(Math.min(parseInt((delta * (to[2]-from[2])) + from[2], 10), 255), 0) + ')'
    }
  })  
}
Кликните по этой надписи, чтобы увидеть функцию в действии

А теперь тоже самое, но delta = makeEaseOut(bounce):

Кликните по этой надписи, чтобы увидеть функцию в действии

Набор текста

Вы можете создавать интересные анимации, как, например, набор текста в "скачущем" режиме:

[pre]

Он стал под дерево и ждет. И вдруг граахнул гром — Летит ужасный Бармаглот И пылкает огнем!

[/pre] Исходный код:

function animateText(textArea) {
  var text = textArea.value
  var to = text.length, from = 0
  
  animate({
    delay: 20,
    duration: 5000,
    delta: bounce,
    step: function(delta) {
      var result = (to-from) * delta + from
      textArea.value = text.substr(0, Math.ceil(result))
    }
  })
}

Итого [#animate]

Анимация выполняется путём использования setInterval с маленькой задержкой, порядка 10-50мс. При каждом запуске происходит отрисовка очередного кадра.

Анимационная функция, немного расширенная:

function animate(opts) {

  var start = new Date;
  var delta = opts.delta || linear;

  var timer = setInterval(function() {

    var progress = (new Date - start) / opts.duration;
    if (progress > 1) progress = 1;

    opts.step( delta(progress) );

    if (progress == 1) {
      clearInterval(timer);
      opts.complete && opts.complete();
    }
  }, opts.delay || 13);

  return timer;
}

Основные параметры:

  • `delay` - задержка между кадрами, по умолчанию 13мс.
  • `duration` - длительность анимации в мс.
  • `delta` - функция, которая определяет состояние анимации каждый кадр. Получает часть времени от 0 до 1, возвращает завершенность анимации от 0 до 1. По умолчанию `linear`.
  • `step` - функция, которая отрисовывает состояние анимации от 0 до 1.
  • `complete` - функция для вызова после завершенности анимации.
  • Вызов `animate` возвращает таймер, чтобы анимацию можно было отменить.

Функцию delta можно модифицировать, используя трансформации easeOut/easeInOut:

function makeEaseInOut(delta) {
  return function(progress) {
    if (progress < .5) return delta(2*progress) / 2;
    else return (2 - delta(2*(1-progress))); / 2;
  }
}

function makeEaseOut(delta) {
  return function(progress) {
    return 1 - delta(1 - progress);
  }
}

На основе этой общей анимационной функции можно делать и более специализированные, например animateProp, которая анимирует свойство opts.elem[opts.prop] от opts.start px до opts.end px :

//+ run
function animateProp(opts) {
  var start = opts.start, end = opts.end, prop = opts.prop;

  opts.step = function(delta) {
    opts.elem.style[prop] = Math.round(start + (end - start)*delta) + 'px';
  }
  return animate(opts);
}

// Использование:
animateProp({ 
  elem: document.body,
  prop: "width",
  start: 0,
  duration: 2000,
  end: document.body.clientWidth
});

Можно добавить еще варианты delta, step, создать общий фреймворк для анимации с единым таймером и т.п. Собственно, это и делают библиотеки типа jQuery.

Советы по оптимизации

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

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

Фреймворки обычно используют один setInterval и запускают все кадры в заданном интервале.

Помогайте браузеру в отрисовке
Браузер управляет отрисовкой дерева и элементы зависят друг от друга.

Если анимируемый элемент лежит глубоко в DOM, то другие элементы зависят от его размеров и позиции. Даже если анимация не касается их, браузер все равно делает лишние расчёты.

Для того, чтобы анимация меньше расходовала ресурсы процессора(и была плавнее), не анимируйте элемент, находящийся глубоко в DOM.

Вместо этого:

  1. Для начала, удалите анимируемый элемент из DOM и прикрепите его непосредственно к `BODY`. Вам, возможно придется использовать `position: absolute` и выставить координаты.
  2. Анимируйте элемент.
  3. Верните его обратно в DOM.

Эта хитрость поможет выполнять сложные анимации и при этом экономить ресурсы процессора.

Там, где это возможно, стоит использовать CSS-анимацию, особенно на смартфонах и планшетах, где процессор слабоват и JavaScript работает не быстро.

[head]

[/head]