Опубликован: 10.04.2013 | Доступ: свободный | Студентов: 440 / 9 | Длительность: 16:52:00
Специальности: Программист
Лекция 8:

CSS-анимации и переходы

< Лекция 7 || Лекция 8: 123

Создание собственных анимаций: советы и секреты

Если вы чем-то похожи на меня, я могу думаю, что первым, что вы сделали, когда начали экспериментировать с анимациями JavaScript, были: настройка начальных условий, создание таймеров с помощью setInterval, выполнение каких-то вычислений в обработчиках и обновление элементов (или рисование в элементе canvas), и вы продолжали все это до тех пор, пока не получали то, что вам нужно. В конце концов, подобные вещи лежат в основе наших любимых развлечений! (В качестве введения, если только вы не попробовали еще это сами, посмотрите материал "Анимация графики полотна" (http://msdn.microsoft.com/library/windows/apps/hh465053.aspx).

Таким образом, сообщество разработчиков накопило немало ценных сведений по этим вопросам, если вы решили заняться этим. Я сказал так, потому что сейчас, посмотрев на библиотеку анимаций WinJS и на возможности CSS, вы вполне можете решить, стоит ли вам вообще этим заниматься. Некоторые люди подсчитали, что подавляющиее большинство анимаций, необходимых большинству приложений, могут быть полностью реализованы посредством CSS: просто задайте стиль и позвольте хост-процессу приложения сделать все остальное. Но если вы решили, что вам все еще нужна низкоуровневая анимация, вот первое, о чем вы должны спросить себя:"Каков подходящий интервал анимации?"

Это очень важный вопрос, так как часто разработчики не знают, какой интервал использовать для анимации. Это не столько вопрос о длинных интервалах, наподобие 500 мс или 1 с, но рабзрабочтики часто просто используют 10 мс, так как результат его применения кажется им "достаточно быстрым".

Честно говоря, 10 мс – это излишество по целому ряду причин. 60 кадров в секунду (frames per second, fps) соответствует интервалу анимации в 16,7 мс – это меньше, чем способен различить человек и лучше, чем способно отобразить множество дисплеев. На самом деле, лучшие результаты получаются, когда кадры вашей анимации синхронизированы с частотой обновления дисплея.

Рассмотрим это подробнее. Вы когда-нибудь смотрели на экран, когда ели что-нибудь хрустящее и замечали, что пиксели выглядят "пляшущими"? Это потому что дисплей – это не просто пассивное средство просмотра того, что находится в видеопамяти. Вместо этого дисплеи (даже LCD и LED-дисплеи) обычно просматривают видеопамять с заданной частотой обновления, которая обычно равняется 60 Гц, или 60 fps (но может равняться и 50 Гц, и 100 Гц).

Это означает, что попытка вывода анимации с интервалом, меньшим, чем частота обновления дисплея – это пустая трата времени, пустая трата энергии (было показано, что это способно сократить время работы от батарей на 25%!), это приводит к выпадению кадров. Последнее заявление проиллюстрировано ниже, где красные точки – это кадры, которые были нарисованы на чем-то вроде элемента canvas, но никогда не появились на экране, так как другой кадр был нарисован до обновления экрана:


  • Обновление элемента canvas с интервалом менее 10 мс (Canvas update (<10ms intervals))
  • Интервалы обновления дисплея(Display Refresh Interfals)
  • Время (Time)

Именно поэтому обычно анимацию выводят с интервалами, кратными 16,7 мс, испольуя setInterval. Однако, использование значения в 16,7 подразумевает наличие дисплея, имеющего частоту обновления в 60 Гц, а это не всегда так. Правильное решение, таким образом, и для приложений Магазина Windows на JavaScript, и для веб-приложений – использовать requestAnimationFrame. Это API просто принимает функцию, которую нужно вызывать для каждого кадра:

requestAnimationFrame(renderLoop);

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


Более того, requestAnimationFrames принимает во внимание и видимость, что означает, что если элемент невидим (и анимация, таким образом, бесполезна), кадры отрисовываться не будут. Это означает, что вам не нужно самостоятельно обрабатывать события изменения видимости страницы для включения или выключения анимации: вы просто можете положиться на requestAnimationFrames.

Совет. Если вы по-настоящему хотите оптимизировать вывод данных, постарайтесь выполнять все задачи рисования (а не только анимации) с помощью обратного вызова requestAnimationFrames. Таким образом, при обработке изменений, например, при реакции на событие ввода, обновляйте данные и вызывайте requestAnimationFrames с вашей функцией отрисовки вместо того, чтобы напрямую вызывать эту функцию. И всегда перерисовывайте что-либо только тогда, когда вам нужно сделать это, как мы скоро увидим, для того, чтобы наилучшим способом использовать процессор и батареи.

Полезно знать, что попытка анимации элемента canvas, который частично перекрыт элементом с display: inline-block, приведет к значительному падению производительности и большим промежуткам между кадрами из-за чрезмерного аннулирования областей. Использование различных моделей отображения, таких, как table-cell, помогает избежать этой проблемы.

Единственный вызов этого метода активирует обратный вызов для одного кадра. Для того, чтобы выполнять длительную анимацию, ваш обработчик должен вызвать requestAnimationFrames снова. Это показано в примере "Использование requestAnimationFrames для энергоэффективной анимации" (http://code.msdn.microsoft.com/windowsapps/Using-requestAnimationFrame-924b039a) (этот пример выигрывает второе место в конкурсе на самое длинное имя!), который выводит и анимирует часы с секундной стрелкой:


Первый вызов requestAnimationFrames происходит в методе страницы ready, затем обратный вызов выполняет запрошенные изменения (js/scenario1.js):

window.requestAnimationFrame(renderLoopRAF);

function renderLoopRAF() {	
drawClock();	
window.requestAnimationFrame(renderLoopRAF);
}	

Здесь функция drawClock принимает текущее время и вычисляет угол для рисования стрелок часов:

function drawClock() {
// ...

// Обратите внимание: пример модифицирован, Date создается лишь однажды, а не каждый раз
var date = new Date();	
var hour = date.getHours();	
var minute = date.getMinutes();	
var second = date.getSeconds();	

// ...

var sDegree = second / 60 * 360 - 180;
var mDegree = minute / 60 * 360 - 180;
var hDegree = ((hour + (minute / 60)) / 12) * 360 - 180;

// Код для использования методов контекста translate, rotate, и drawImage 
// для рисования каждой стрелки
}

Вот вам вопрос: "Что не так с этим кодом?". Запустите пример и посмотрите на секундную стрелку. Затем подумайте о том, как requestAnimationFrames обеспечивает соответствие с циклами обновления экрана с интервалом 16,7 мс. Что не так с изображением?

Проблема здесь в том, что хотя секундная стрелка совершает видимые перемещения раз в секунду, код метода drawClock исполняется примерно 50, 60 или 100 раз в секунду – то есть гораздо чаще, чем нужно!. Таким образом, заголовок "Эффективные и плавные анимации" примера, означает все, что угодно, кроме того, что мы видим! На самом деле, если вы запустите Диспетчер задач, вы можете видеть, что эти простые и "эффективные" часы, по иронии судьбы, потребляют 15-20% ресурсов процессора. Ну ничего ж себе!


Помните, что интервалы ориентированы на частоту обновления экрана в 16,7 мс (для 60 Гц-экрана), что подразумевает рендеринг 60 кадров в секунду. Если вам не нужно так много, вам следует пропускать кадры самостоятельно, ориентируясь на время, и экономить таким образом энергию, а не слепо перерисовывать экран, как сделано в этом примере. На самом деле, все, что нам нужно в подобных часах – это одно перемещение стрелки в секунду, постоянное вызывание requestAnimationFrames – это сущее излишество. Мы можем, вместо этого, использовать конструкцию setInterval(function () { requestAnimationFrame(drawClock) }, 1000) для того, чтобы скоординировать необходимый нам 1-секундный интервал с частотой обновления экрана. Если вы внесете это изменение, например, в метод ready, нагрузка на процессор значительно снизится:


Но допустим, мы, на самом деле хотим выполнять анимацию с частотой 60 Гц и потребление при этом 20% процессорного времени нас устраивает. В подобном случае нам следует, как минимум, сделать движения секундной стрелки более плавными, что можно сделать, просто использовав миллисекунды при вычислении угла:

var second = date.getSeconds() + date.getMilliseconds() / 1000;

Тем не менее, 20% мощности процессора – это слишком много на что-то столь простое, и 60 кадров в секунду, это все еще серьезное излишество. Возможно, что-то около 10 кадров в секунду нам вполне подойдет. В таком случае мы можем вычислять прошедшее время в renderLoopRAF и вызывать drawClock только тогда, когда прошла 0,1 секунды:

var lastTime = 0;

function renderLoopRAF() {	
var fps = 10; // Целевое значение кадров в секунду	
var interval = 1000 / fps;	
var curTime = Math.floor(Date.now() / interval);

if (lastTime != curTime) {
lastTime = curTime;	
drawClock();	
}	

requestAnimationFrame(renderLoopRAF);
}

Движения уже не такие плавные, вывод с частотой 10 кадров в секунду создает впечатление механических движений, но такой подход позволяет значительно снизить нагрузку на процессор:


Предлагаю вам поэкспериментировать со всем этим для того, чтобы увидеть, интервалы какой длины вы способны различить. 10 кадров в секунду и 15 дают ощущение механических движений, на 20 кадрах в секунду я не вижу особой разницы с 60-ю, а загрузка процессора состоавляет 7 – 10%. Вы можете так же попробовать что-то вроде 4 кадров в секунду (интервалы в четверть секунды) для того, чтобы увидеть их эффект. В дополнительных материалах к этой лекции я разместил вариант исходного примера, где вы можете выбирать из различных целевых частот рендеринга.

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

var second = date.getSeconds() + date.getMilliseconds() / 1000;
var minute = date.getMinutes() + second / 60;
var hour  = date.getHours() + minute / 60;

При реальном использовании, например, в игре, обычно избегают выполнения бесконечных циклов анимации, как здесь: если по экрану не перемещается ничего, что должно анимироваться (для чего вы можете использовать setInterval в качестве таймера), и нет событий ввода, на которые нужно реагировать, нет причин вызывать requestAnimationFrames. Кроме того, удостоверьтесь, что когда приложение "поставлено на паузу", вы прекращаете вызывать requestAnimationFrames до тех пор, пока, анимация не будет снова нужна (Так же вы можете испоьзовать cancelAnimationFrames для того, чтобы остановить уже запрошенную анимацию). То же самое справедливо и для setTimeout и setInterval: не делайте ненужные вызовы функции обратного вызова до тех пор, пока вам не нужно выполнять анимацию. Для этой цели используйте событие visibilitychange (http://msdn.microsoft.com/library/windows/apps/hh441213.aspx), чтобы узнать, отображается ли ваше приложение на экране. Так как requestAnimationFrames принимает во внимание видимость (использование примером процессора падает до 0% перед приостановкой), вам нужно это сделать с помощью setTimeout и setInterval.

В итоге, главная цель здесь – по-настоящему понять, какой интервал анимации вам нужен (то есть, какая частота кадров) для того, чтобы наилучшим образом использовать requestAnimationFrames, если это нужно, или использовать setInterval/setTimeOut. И у того и у другого есть собственные варианты использования, которые позволяют предоставить хороший опыт взаимодействия пользователя и приложения при соответствующей нагрузке на систему.

Знаете ли вы? Одно из изменений в Windows 8 и Internen Explorer заключается в том, что setTimeout, setInterval, вместе с setImmediate, поддерживают включение параметров, которые вы можете передать в функцию обратного вызова?

Что мы только что изучили

  • В панели управления на Рабочем столе, пользователи могут задать параметр, отключающий большинство анимаций, не являющихся существенными для работы системы. Приложениям следует принимать это во внимание, как это делает WinJS, проверяя свойство Windows.UI.ViewManagement.UISettings.animationsEnabled.
  • Библиотека анимаций WinJS содержит множество встроенных анимаций, которые воплощают индивидуальность Windows 8. Они рекомендованы для использования в соответствующих им сценариях приложения, в таких, как переходы содержимого и страниц, выделение, манипуляции со списками и в других.
  • Все анимации WinJS построены на основе CSS и, таким образом, используют преимущества аппаратного ускорения. Когда соблюдены верные условия, такие анимации исполняются на GPU, и, таким образом, не подвержены влиянию деятельности в потоке пользовательского интерфейса.
  • Приложения так же могут непосредственно использовать CSS-анимации и переходы в соответствии со спецификациями W3C.
  • Помимо возможностей WinJS и CSS, приложения могут использовать функции наподобие setInterval и requestAnimationFrame для реализации непосредственной покадровой анимации. Метод requestAnimationFrame синхронизирует отрисовку кадров с частотой обновления экрана, что ведет к лучшей общей производительности.
< Лекция 7 || Лекция 8: 123
Вадик Елетин
Вадик Елетин
Россия, г. Санкт-Петербург
Николай Жигульский
Николай Жигульский
Россия, Тверь, Тверской государственный технический университет