Часто при поиске ответа на вопрос, как сделать ту или иную до этого незнакомую вещь, программист знакомится с опытом коллег. И довольно часто у нас, во фронтенд-разработке, можно увидеть советы следующего содержания: мол, просто подключи ту или иную библиотеку, просто поставь тот или иной плагин, просто перепиши проект на Ангуляре (просто_на_Ангуляре ))) ), и не надо забивать себе голову посторонними вещами.
Однако, иногда стоят действительно творческие задачи, и обычный копипаст не спасает демократию (честно говоря, он почти никогда не спасает). Об одном из таких случаев я и хочу рассказать уважаемой публике.
Предыстория такая. Мы в компании разрабатывали продукт, клиентская часть которого должна была быть написана на чистом Javascipt. В какой-то момент для реализации всех возможностей продукта мы поняли, что не можем обойтись без анимации элементов (раскрытие элементов, растворение, плавное перемещение по экрану и т.п.)
Самым логичным шагом представлялось просто пойти и посмотреть, как анимация устроена в том же jQuery, и по возможности повторить решение в своём коде. Однако, даже при беглом взгляде стало понятно, что код jQuery не так прост для понимания всех зависимостей. И потом, руководство проекта выделило достаточно времени, чтобы, не оглядываясь на опыт коллег, сесть и сделать решение самому. Сейчас, оглядываясь назад, понимаю, что это был прекрасный опыт решения действительно сложной задачи в клиентском программировании.
Итак, рассмотрим задачу на верхнем уровне
У нас есть следующие условия:
1) Анимируется любой элемент на странице, количество элементов не ограничено
2) Список анимируемых свойств задан (линейные размеры, положение, отступы, прозрачность)
3) Анимация должна управляться через параметры (время исполнения, функция скорости исполнения)
4) По завершению анимации вызывается любой произвольный коллбэк
5) Анимация в любой момент может быть прервана
Скорее всего, за пункты 1-4 будет отвечать одна глобальная функция, вызываемая со списком параметров, за пункт 5 будет отвечать отдельная специальная функция. Всего две (на самом деле оказалось, что три :) ). Далее я расскажу ход написания функций с краткими пояснениями, зачем существует тот или иной блок и как он работает, и в конце дам ссылку на полный код, а также пример.
Начнём
Сразу создаём var $ = {d: window.document, w: window}, чтобы не лезть прямо в window.
Создаём основную функцию анимации:
Здесь el — анимируемый элемент, props — сущность анимации, opts — характеристики, cb — коллбэк.
Начинаем её наполнять. Для сохранения контекста используем self, для однозначного определения анимации id. Сразу выполняем необходимые проверки:
Обратите внимание: обработка ошибок удобна через специальную функцию doFail.
Теперь действия, которые необходимо совершить с элементом, вносим в массив:
Дефолтные опции перезаписываем клиентскими там, где это указано (естественно, с проверками):
Теперь немного вернёмся в реальный мир.
Дело в том, что анимация вызывается в любой момент на любом элементе. Соответственно, сначала нам нужно принять параметры этого элемента в этой точке времени, чтобы понять, как его видоизменить.
Здесь, наконец, до нас доходит, что нужна будет ещё какая-то дополнительная функция, с помощью которой мы опишем анимируемый элемент (о ней чуть дальше). А пока сделаем ещё одну важную проверку.
Количество и очередь анимаций
Да, задача становится всё сложнее. Дело в том, что анимация может вызываться на элементе, который уже находится в состоянии выполнения предыдущей анимации (и ещё не закончил её). Наша функция должна быть универсальна, поэтому она должна уметь принимать и корректно обрабатывать цепь независимых вызовов.
Ниже дан код. К нему есть построчные пояснение, поэтому просто привожу целиком этот блок:
Мы готовы стартовать выполнение анимации, её запуск производится функцией startAnimation(). Вот тут-то, в самом начале, мы и описываем наш элемент через его свойства (делать это раньше нельзя, т.к. элемент, пока анимация находилась в очереди, мог быть изменён):
Как видно, тут мы обращаемся к специальной функции, которая даст нам описание стилей элемента. Памятуя о том, что наш код должен быть на чистом JS, а проект должен адекватно работать на браузере IE8 (да-да, мы пишем настоящий коммерческий код, который продаётся за деньги в разные страны, поэтому аргумент «это не модно» не принимается!), сжимаем всё волю в кулак и идём забарывать ослиные заморочки:
Вот, казалось бы, всего-то делов: получить стили элемента! Ан-нет, и здесь есть поле для творчества.
Наконец, элемент полностью описан, все параметры анимации проверены и приведены в нормативный вид. Теперь пишем код исполнения. Он целиком заключён в функции doAnimation(params) {};
В первой её части, которая приведена ниже, самое интересное — расшифровка инструкций анимации («что делать с объектом?»). Надеюсь, все помнят, что некоторые свойства элемента (размеры, положение, отступы) могут задаваться не только пикселями, но и процентами:
Наконец, мы подобрались к самому сердцу механизма
Давайте подумаем, что вообще такое анимация? В реальном мире любое движение суть замысел, который разворачивается во времени.
В мире программирования мы должны перевести любую поэзию в цифру. То есть, ближе к делу, описать элемент в любой момент времени через понятные величины.
Теперь задумаемся — нужен ли нам «любой момент»? Нам доступно управление поведением элемента в промежутки времени от миллисекунды. На деле, конечно, требуемый интервал в описании элемента есть компромисс между производительностью браузера и способностью человеческого мозга складывать отдельные дискретные картинки в одну сцену. Опытным путём я установил, что 30 миллисекунд будет в самый раз.
Другими словами, анимация — это последовательное, через равные промежутки времени, изменение состояние элемента. А за равные промежутки времени у нас отвечает setInterval:
Вот это есть наш «движок» анимации.
Обратите внимание: мы устанавливаем интервал как свойство элемента, ведь нам в последующем нужен будет доступ извне, чтобы прекратить выполнять анимацию (то есть обнулить интервал).
Наконец, функция отрисовки элемента, которая выполняется через заданные промежутки времени всё время выполнения анимации:
Как видите, первые два блока кода функции — это механизм выключения анимации (думаю, особо разбирать не нужно), а сама отрисовка делается функцией _step(getProgress(progress)):
Тут разберём всё максимально подробно:
Теперь о factor, с которым вызывается эта функция. Это вычисляемый параметр, говорящий нам, насколько следует изменить параметр элемента (по пути от начального значения к конечному) в данный конкретный момент времени, который понимается как точка на линии времени в отрезке от 0 до 1. Это уже было выше:
Идти по отрезку можно с равной скоростью, либо следуя поведению одной из стандартных функций анимации:
По-русски всё это звучит сложновато, да (либо я просто не умею применяй русского языка довольно). Но на картинке всё нагляднее, ось абсцисс — время, ось ординат — значение изменяемого параметра:
Таким образом, суть механизма следующая: через равные промежутки времени мы спрашиваем getProgress, на какой стадии анимации (по пути от начальной точке к конечной) мы находимся, а потом идём с этим знанием в _step и выполняем функции изменения параметров из списка.
И последнее, что мы прописываем в doAnimation, это интерфейс вызова остановки анимации:
Вызов остановки анимации прост: мы просто указываем элемент, говорим, нужно ли в момент остановки перейти в конечную точку анимации, и указываем, если нужно, новый коллбэк.
Для прекращения анимации у нас есть отдельная глобальная функция:
Ну и самое последнее — обработчик ошибок. Он пишется в $.w.ltAnimate и вызывается, если требуется, из неё же:
→ Попробовать погонять прямоугольнички самому можно здесь
Там же можно взять полный исходный код всех трёх функций.
_____________________________________
ps. Поскольку код писался полтора года назад, подробности некоторых моментов, почему было сделано именно так, уже начали стираться из памяти. Однако, если вы зададите вопрос, я постараюсь максимально точно вспомнить, что имел ввиду, когда писал код.
Безусловно, представленный код не является идеальным. Буду рад конструктивным замечанием и указанием на ошибки и дополнения.
Также было бы интересно обсудить, как можно подойти к задаче разработке анимации элементов в браузере, используя другой подход.
Однако, иногда стоят действительно творческие задачи, и обычный копипаст не спасает демократию (честно говоря, он почти никогда не спасает). Об одном из таких случаев я и хочу рассказать уважаемой публике.
Предыстория такая. Мы в компании разрабатывали продукт, клиентская часть которого должна была быть написана на чистом Javascipt. В какой-то момент для реализации всех возможностей продукта мы поняли, что не можем обойтись без анимации элементов (раскрытие элементов, растворение, плавное перемещение по экрану и т.п.)
Самым логичным шагом представлялось просто пойти и посмотреть, как анимация устроена в том же jQuery, и по возможности повторить решение в своём коде. Однако, даже при беглом взгляде стало понятно, что код jQuery не так прост для понимания всех зависимостей. И потом, руководство проекта выделило достаточно времени, чтобы, не оглядываясь на опыт коллег, сесть и сделать решение самому. Сейчас, оглядываясь назад, понимаю, что это был прекрасный опыт решения действительно сложной задачи в клиентском программировании.
Итак, рассмотрим задачу на верхнем уровне
У нас есть следующие условия:
1) Анимируется любой элемент на странице, количество элементов не ограничено
2) Список анимируемых свойств задан (линейные размеры, положение, отступы, прозрачность)
3) Анимация должна управляться через параметры (время исполнения, функция скорости исполнения)
4) По завершению анимации вызывается любой произвольный коллбэк
5) Анимация в любой момент может быть прервана
Скорее всего, за пункты 1-4 будет отвечать одна глобальная функция, вызываемая со списком параметров, за пункт 5 будет отвечать отдельная специальная функция. Всего две (на самом деле оказалось, что три :) ). Далее я расскажу ход написания функций с краткими пояснениями, зачем существует тот или иной блок и как он работает, и в конце дам ссылку на полный код, а также пример.
Начнём
Сразу создаём var $ = {d: window.document, w: window}, чтобы не лезть прямо в window.
Создаём основную функцию анимации:
$.w.ltAnimate = function (el, props, opts, cb) {});
Здесь el — анимируемый элемент, props — сущность анимации, opts — характеристики, cb — коллбэк.
Начинаем её наполнять. Для сохранения контекста используем self, для однозначного определения анимации id. Сразу выполняем необходимые проверки:
Код 1
var self = this,
id = new Date().getTime(); // id анимации
self._debug = true;
// проверяем элемент
if ((typeof el == "string") && el) el = this.ltElem(el);
if ((typeof el != "object") || !el || (typeof el.nodeType != "number") || (el.nodeType > 1)) {
doFail("Нет анимируемого элемента");
return;
}
// проверяем аргумент opts
switch (typeof opts) {
case "number":
opts = {duration: opts};
break;
case "function":
opts = {cbDone: opts};
break;
case "object":
if (!opts) opts = {};
break;
default:
opts = {};
}
if (typeof cb == "function") opts.cbDone = cb;
// устанавливаем умолчания
var defaultOptions = {
tick : 30, // период отрисовки нового кадра в миллисекундах, задаётся только здесь
duration : 1000, // длительность выполнения анимации
easing : 'linear', // функция расчёта параметров
cbDone : function() { // коллбэк после удачного выполнения
if (self._debug) $.w.console.log("Анимация [id: " + id + "] прошла удачно");
},
cbFail : function() { // коллбэк после неудачного выполнения
if (self._debug) $.w.console.log("Анимация [id: " + id + "] прошла неудачно");
}
}
Обратите внимание: обработка ошибок удобна через специальную функцию doFail.
Теперь действия, которые необходимо совершить с элементом, вносим в массив:
Код 2
// заносим в массив, что будем выполнять
var instructions = [];
for (var key in props) {
if (!props.hasOwnProperty(key)) continue;
instructions.push([key, props[key]]);
}
// если выполнять нечего, выдаём ошибку
if (instructions.length === 0) {
doFail("Не сказано, что делать с элементом");
return;
}
Дефолтные опции перезаписываем клиентскими там, где это указано (естественно, с проверками):
Код 3
// перезаписываем опции клиентскими значениями
var optionsList = [],
easing = {linear: 1, swing:1, quad:1, cubic:1};
for (var key in opts) {
if (!opts.hasOwnProperty(key)) continue;
switch (key) {
case "duration":
if (typeof opts[key] != "number") {
$.w.console.log("ltAnimate(): Внимание! Длительность анимации задаётся числом. Будет применена стандартная длительность");
continue;
}
break;
case "easing":
if (typeof easing[opts[key]] == "undefined") {
$.w.console.log("ltAnimate(): Внимание! Неизвестное значение easing. Будет применена стандартная функция");
continue;
}
break;
case "cbDone":
case "cbFail":
if (typeof opts[key] != "function") {
$.w.console.log("ltAnimate(): Внимание! Коллбэк должен быть функцией!");
continue;
}
break;
default:
$.w.console.log("ltAnimate(): Внимание! Неизвестный параметр в списке опций!");
continue;
}
optionsList.push([key, opts[key]])
}
// формируем options на основе defaultOptions
var options = defaultOptions;
if (optionsList.length) {
for (var i=0; i < optionsList.length; i++) {
if (optionsList[i][0] == 'duration') options.duration = optionsList[i][1];
if (optionsList[i][0] == 'easing') options.easing = optionsList[i][1];
if (optionsList[i][0] == 'cbDone') options.cbDone = optionsList[i][1];
if (optionsList[i][0] == 'cbFail') options.cbFail = optionsList[i][1];
}
}
Теперь немного вернёмся в реальный мир.
Дело в том, что анимация вызывается в любой момент на любом элементе. Соответственно, сначала нам нужно принять параметры этого элемента в этой точке времени, чтобы понять, как его видоизменить.
// объект, куда будут записываться параметры элемента при старте анимации
var startParams = {};
Здесь, наконец, до нас доходит, что нужна будет ещё какая-то дополнительная функция, с помощью которой мы опишем анимируемый элемент (о ней чуть дальше). А пока сделаем ещё одну важную проверку.
Количество и очередь анимаций
Да, задача становится всё сложнее. Дело в том, что анимация может вызываться на элементе, который уже находится в состоянии выполнения предыдущей анимации (и ещё не закончил её). Наша функция должна быть универсальна, поэтому она должна уметь принимать и корректно обрабатывать цепь независимых вызовов.
Ниже дан код. К нему есть построчные пояснение, поэтому просто привожу целиком этот блок:
Код 4
// если вторая или более анимация на этом объекте
if (el.ltAnimateQueue && el.ltAnimateQueue.length > 0) {
// смотрим, через сколько её нужно будет попытаться выполнить (точно предугадать нельзя, т.к. несколько мс уходит на исполнение кода)
var animateEnds = 1,
timeNow = new Date().getTime();
for (var i=0; i < el.ltAnimateQueue.length; i++) {
if (i == 0) {
animateEnds = el.ltAnimateQueue[i][1] - timeNow + el.ltAnimateQueue[i][0];
} else {
animateEnds += el.ltAnimateQueue[i][1];
}
}
// заносим анимацию в очередь анимаций
el.ltAnimateQueue.push([timeNow + animateEnds, options.duration]);
// через посчитанное время смотрим, действительно ли все предыдущие анимации завершились и можно ли выполнять эту
var thisTimeout = $.w.setTimeout(function(){
checkAnimation();
}, animateEnds);
// массив таймаутов, которые поставлены для активации анимации, нужен при вызове ltAnimateStop
if (!el.ltAnimateTimeouts) {
el.ltAnimateTimeouts = [];
}
el.ltAnimateTimeouts.push(thisTimeout);
// первая анимация на объекте
} else {
// создаём очередь выполнения анимаций, если первая анимация на элементе
el.ltAnimateQueue = [[new Date().getTime(), options.duration]];
startAnimation();
}
// проверяем, действительно ли никакие анимации не выполняются и можно запускать эту
function checkAnimation() {
// если никаких анимаций не выполняется, то сразу запускаем
if (!el.ltAnimateIsDoing) {
startAnimation();
} else {
// периодически опрашиваем, действительно ли анимации закончились
function _check() {
if (!el.ltAnimateIsDoing) {
$.w.clearInterval(_checking);
startAnimation();
}
}
var _checking = $.w.setInterval(_check, 30);
}
}
Мы готовы стартовать выполнение анимации, её запуск производится функцией startAnimation(). Вот тут-то, в самом начале, мы и описываем наш элемент через его свойства (делать это раньше нельзя, т.к. элемент, пока анимация находилась в очереди, мог быть изменён):
Код 5
function startAnimation() {
// флаг выполнения анимации
el.ltAnimateIsDoing = true;
// размеры элемента
var startStyles = self.ltStyle(el);
// запоминаем стартовые значения свойств элемента
startParams.left = parseInt(startStyles.left);
startParams.right = parseInt(startStyles.right);
startParams.top = parseInt(startStyles.top) + 0.01;
startParams.bottom = parseInt(startStyles.bottom) - 0.01;
startParams.width = parseInt(startStyles.width);
startParams.height = parseInt(startStyles.height);
startParams.opacity = parseFloat(startStyles.opacity);
startParams.marginTop = parseInt(startStyles.marginTop);
startParams.marginBottom = parseInt(startStyles.marginBottom);
startParams.marginLeft = parseInt(startStyles.marginLeft);
startParams.marginRight = parseInt(startStyles.marginRight);
startParams.parentWidth = parseInt(self.ltStyle(el.parentNode).width);
startParams.parentHeight = parseInt(self.ltStyle(el.parentNode).height);
// проверки и подстановки для Chrome и IE
for (key in startParams) {
if (key == 'left' && !startParams[key]) {
startParams.left = startParams.parentWidth - startParams.right - startParams.width || 0;
}
if (key == 'right' && !startParams[key]) {
startParams.right = startParams.parentWidth - startParams.left - startParams.width || 0;
}
if (key == 'bottom' && !startParams[key]) {
startParams.bottom = startParams.parentHeight - startParams.top - startParams.height || 0;
}
if (key == 'top' && !startParams[key]) {
startParams.top = startParams.parentHeight - startParams.bottom - startParams.height || 0;
}
}
// выполнение анимации
el.currentAnimation = new doAnimation({
element : el,
delay : defaultOptions.delay
});
}
Как видно, тут мы обращаемся к специальной функции, которая даст нам описание стилей элемента. Памятуя о том, что наш код должен быть на чистом JS, а проект должен адекватно работать на браузере IE8 (да-да, мы пишем настоящий коммерческий код, который продаётся за деньги в разные страны, поэтому аргумент «это не модно» не принимается!), сжимаем всё волю в кулак и идём забарывать ослиные заморочки:
Код 6
/**
* Получение всех стилей элемента (если подан только el) либо значения конкретного стиля (в styleName передаётся строка).
* opts - объект, свойство computed по умолчанию равно true. Если да, возвращает конечный стиль элемента, если false - инлайновый.
* Если элемент подан неявно (например, тег div) и при поиске выясняется, что подобных элементов на странице несколько, возвращается пустая строка.
* Для IE8 выполняется преобразование %, auto, thin/medium/thick в нормальный вид.
* Opacity для IE8 возвращается в нормальном виде (от 0 до 1)
*
* @param {DOM} el - обрабатываемый элемент
* @param {string} style - название стиля, значение которого нужно получить
* @param {Object} opts - дополнительные опции операции
*
* @returns {(number|string)} value - вычисленное значение
*/
$.w.ltStyle = function(el, styleName, opts) {
if (!opts || typeof opts != 'object' || typeof opts.computed != 'boolean') opts = {computed : true};
if (typeof el == 'string') el = this.ltElem(el);
// если возвращается массив (NodeList), то возвращаем пустую строку
if (!el || !el.nodeType || (el.nodeType != 1)) return '';
var _style;
// в IE8 вместо getComputedStyle есть currentStyle
if (!$.w.getComputedStyle) {
var __style = el.currentStyle,
_style = {};
for (var i in __style) {
_style[i] = __style[i];
}
// стили, для которых в IE8 существуют родные стили: pixelLeft, pixelRight и так далее - их можно взять напрямую, не считая
var pixel = {
left: 1,
right: 1,
width: 1,
height: 1,
top: 1,
bottom: 1
};
// для этих стилей используем хак http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
var other = {
paddingLeft: 1,
paddingRight: 1,
paddingTop: 1,
paddingBottom: 1,
marginLeft: 1,
marginRight: 1,
marginTop: 1,
marginBottom: 1
};
var leftCopy = el.style.left;
var runtimeLeftCopy = el.runtimeStyle.left;
// для всех стилей сразу
if (!styleName) {
// толщина границ в IE8 приходит в виде прилагательных, приводим в нормальный вид
for (c in _style) {
if (!_style.hasOwnProperty(c)) continue;
if (c.indexOf("border") !== 0) continue;
switch (_style[c]) {
case "thin":
_style[c] = 2;
break;
case "medium":
_style[c] = 4;
break;
case "thick":
_style[c] = 6;
break;
default:
_style[c] = 0;
}
}
//pixel
for (var key in pixel) {
_style[key] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")];
}
// вариант замены getComputedStyle для некоторых параметров
for (var key in other) {
el.runtimeStyle.left = el.currentStyle.left;
el.style.left = _style[key];
_style[key] = el.style.pixelLeft;
el.style.left = leftCopy;
el.runtimeStyle.left = runtimeLeftCopy;
}
// для одного выбранного стиля
} else {
if (_style[styleName]) {
if (style.indexOf("border") === 0)
switch (_style[styleName]) {
case "thin":
_style[styleName] = 2;
break;
case "medium":
_style[styleName] = 4;
break;
case "thick":
_style[styleName] = 6;
break;
default:
_style[styleName] = 0;
}
} else {
if (pixel[styleName]) {
_style[styleName] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")];
} else {
el.runtimeStyle.left = el.currentStyle.left;
el.style.left = _style[styleName];
_style[styleName] = el.style.pixelLeft;
el.style.left = leftCopy;
el.runtimeStyle.left = runtimeLeftCopy;
}
}
}
// костыль для opacity IE8
if (_style.filter.match('alpha')) {
_style.opacity = _style.filter.substr(14);
_style.opacity = parseInt(_style.opacity.substring(0, _style.opacity.length - 1)) / 100;
} else {
_style.opacity = 1;
}
// нормальные браузеры
} else {
if (opts.computed) {
_style = $.w.getComputedStyle(el, null);
} else {
_style = el.style.styleName;
}
}
if (!styleName) {
return _style || '';
} else {
return _style[styleName] || '';
}
};
Вот, казалось бы, всего-то делов: получить стили элемента! Ан-нет, и здесь есть поле для творчества.
Наконец, элемент полностью описан, все параметры анимации проверены и приведены в нормативный вид. Теперь пишем код исполнения. Он целиком заключён в функции doAnimation(params) {};
В первой её части, которая приведена ниже, самое интересное — расшифровка инструкций анимации («что делать с объектом?»). Надеюсь, все помнят, что некоторые свойства элемента (размеры, положение, отступы) могут задаваться не только пикселями, но и процентами:
Код 7
// значение параметра
var val = instructions[i][1].toString();
// смотрим, задан ли параметр в процентах
val.match(/\%/) ? percent = true : percent = false;
val = parseFloat(val);
var x;
switch (instructions[i][0]) {
case 'top' :
x = function(factor, val, percent) {
element.style.bottom = '';
element.style.top = startParams.top - (startParams.top - (percent ? startParams.parentHeight * val / 100 : val))*factor + 'px';
};
break;
case 'bottom' :
x = function(factor, val, percent) {
element.style.top = '';
element.style.bottom = startParams.bottom - (startParams.bottom - (percent ? (startParams.parentHeight * val / 100) : val))*factor + 'px';
};
break;
case 'left' :
x = function(factor, val, percent) {
element.style.right = '';
element.style.left = startParams.left - (startParams.left - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px';
};
break;
case 'right' :
x = function(factor, val, percent) {
element.style.left = '';
element.style.right = startParams.right - (startParams.right - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px';
};
break;
case 'width' :
x = function(factor, val, percent) {
element.style.width = startParams.width - (startParams.width - (percent ? (startParams.width * val / 100) : val))*factor + 'px';
};
break;
case 'height' :
x = function(factor, val, percent) {
element.style.height = startParams.height - (startParams.height - (percent ? (startParams.height * val / 100) : val))*factor + 'px';
};
break;
case 'opacity' :
x = function(factor, val, percent) {
// IE8
if (!$.w.getComputedStyle) {
element.style.filter = 'alpha(opacity=' + (startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor) * 100 + ')';
} else {
element.style.opacity = startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor;
}
}
break;
case 'marginTop' :
x = function(factor, val, percent) {
element.style.marginBottom = 'auto';
element.style.marginTop = startParams.marginTop - (startParams.marginTop - (percent ? (startParams.height * val / 100) : val))*factor + 'px';
};
break;
case 'marginBottom' :
x = function(factor, val, percent) {
element.style.marginTop = 'auto';
element.style.marginBottom = startParams.marginBottom - (startParams.marginBottom - (percent ? (startParams.height * val / 100) : val))*factor + 'px';
};
break;
case 'marginLeft' :
x = function(factor, val, percent) {
element.style.marginRight = 'auto';
element.style.marginLeft = startParams.marginLeft - (startParams.marginLeft - (percent ? (startParams.width * val / 100) : val))*factor + 'px';
};
break;
case 'marginRight' :
x = function(factor, val, percent) {
element.style.marginLeft = 'auto';
element.style.marginRight = startParams.marginRight - (startParams.marginRight - (percent ? (startParams.width * val / 100) : val))*factor + 'px';
}
break;
// если попытка анимировать неподдерживаемое свойство, просто ничего не делаем
default : x = function(){};
}
// заносим выполняемые функции в массив
exec.push([x, val, percent]);
}
var eLength = exec.length;
Наконец, мы подобрались к самому сердцу механизма
Давайте подумаем, что вообще такое анимация? В реальном мире любое движение суть замысел, который разворачивается во времени.
Самурай, взмахнувший мечом
Подобен самураю, не взмахнувшему мечом,
Но только взмахнувший мечом
В мире программирования мы должны перевести любую поэзию в цифру. То есть, ближе к делу, описать элемент в любой момент времени через понятные величины.
Теперь задумаемся — нужен ли нам «любой момент»? Нам доступно управление поведением элемента в промежутки времени от миллисекунды. На деле, конечно, требуемый интервал в описании элемента есть компромисс между производительностью браузера и способностью человеческого мозга складывать отдельные дискретные картинки в одну сцену. Опытным путём я установил, что 30 миллисекунд будет в самый раз.
Другими словами, анимация — это последовательное, через равные промежутки времени, изменение состояние элемента. А за равные промежутки времени у нас отвечает setInterval:
el.ltAnimateInterval = $.w.setInterval(function(){
_animating();
}, options.tick);
Вот это есть наш «движок» анимации.
Обратите внимание: мы устанавливаем интервал как свойство элемента, ведь нам в последующем нужен будет доступ извне, чтобы прекратить выполнять анимацию (то есть обнулить интервал).
Наконец, функция отрисовки элемента, которая выполняется через заданные промежутки времени всё время выполнения анимации:
Код 8
// jumpToEnd - true/false - говорит о том, следует ли прекратить анимацию в конечной точке, сразу перейдя в конечную точку
// отрисовка
function _animating(param, jumpToEnd, callback) {
counter++;
// переменная принимает значения от 0 до 1
var progress = counter / animationLength;
// выключаем анимацию при помощи stopAnimation
if (param == animationLength) {
$.w.clearInterval(el.ltAnimateInterval);
// если нужно завершить в конечной точке
if (jumpToEnd) _step(getProgress(1));
// удаляем анимацию из очереди анимаций
el.ltAnimateQueue.splice(0, 1);
// выключаем флаг выполнения анимации
el.ltAnimateIsDoing = false;
// остановка, если явно не указано по поводу коллбэка
if (!callback) {
try {
options.cbDone();
} catch(e) {
doFail(e);
}
} else {
try {
callback();
} catch(e) {
doFail(e);
}
}
return false;
}
// выключаем анимацию, если пройдены все шаги
if (progress > 1) {
// делаем заключительный шаг, без него анимация чуть не доезжает до финальной точки (progress меняется дискретно, последнее значение 0.99...)
_step(getProgress(1));
$.w.clearInterval(el.ltAnimateInterval);
// удаляем анимацию из очереди анимаций
el.ltAnimateQueue.splice(0, 1);
// выключаем флаг выполнения анимации
el.ltAnimateIsDoing = false;
try {
options.cbDone();
} catch(e) {
doFail(e);
}
return false;
}
_step(getProgress(progress));
}
Как видите, первые два блока кода функции — это механизм выключения анимации (думаю, особо разбирать не нужно), а сама отрисовка делается функцией _step(getProgress(progress)):
function _step(factor) {
for (var i=0; i < eLength; i++) {
var s = exec[i][0],
val = exec[i][1],
percent = exec[i][2];
s(factor, val, percent);
}
}
Тут разберём всё максимально подробно:
- eLength мы уже вычислил ранее — длина списка директив анимации («что делать с элементом?»)
- s — функция, которая изменяет параметр элемента (см. switch (instructions...) в doAnimation)
- val — конечное значение параметра, к которому придёт анимация
- percent — параметр задан в процентах или нет
Теперь о factor, с которым вызывается эта функция. Это вычисляемый параметр, говорящий нам, насколько следует изменить параметр элемента (по пути от начального значения к конечному) в данный конкретный момент времени, который понимается как точка на линии времени в отрезке от 0 до 1. Это уже было выше:
// переменная принимает значения от 0 до 1
var progress = counter / animationLength;
Идти по отрезку можно с равной скоростью, либо следуя поведению одной из стандартных функций анимации:
// переменная для счёта, согласно заданным при вызове параметрам
function getProgress(p) {
switch (options.easing) {
case 'linear' : return p; break;
case 'swing' : return 0.5 - Math.cos(p * Math.PI ) / 2; break
case 'quad' : return Math.pow(p, 2); break;
case 'cubic' : return Math.pow(p, 3); break;
default : return p;
}
}
По-русски всё это звучит сложновато, да (либо я просто не умею применяй русского языка довольно). Но на картинке всё нагляднее, ось абсцисс — время, ось ординат — значение изменяемого параметра:
Таким образом, суть механизма следующая: через равные промежутки времени мы спрашиваем getProgress, на какой стадии анимации (по пути от начальной точке к конечной) мы находимся, а потом идём с этим знанием в _step и выполняем функции изменения параметров из списка.
И последнее, что мы прописываем в doAnimation, это интерфейс вызова остановки анимации:
Код 9
// интерфейс остановки анимации
el.stopAnimation = function(jumpToEnd, callback) {
_animating(animationLength, jumpToEnd, callback);
// очищаем таймауты очереди ожидания анимации
if (el.ltAnimateTimeouts) {
for (var i=0; i < el.ltAnimateTimeouts.length; i++) {
$.w.clearTimeout(el.ltAnimateTimeouts[i])
}
el.ltAnimateTimeouts = [];
}
}
Вызов остановки анимации прост: мы просто указываем элемент, говорим, нужно ли в момент остановки перейти в конечную точку анимации, и указываем, если нужно, новый коллбэк.
Для прекращения анимации у нас есть отдельная глобальная функция:
Код 10
/*
* Остановкка анимации: элемент, переход в конечную точку (true/false) и остановить ли выполнение коллбэка (true/false), два последних необязательно
*/
$.w.ltAnimateStop = function(el, jumpToEnd, callback) {
// останавливаем анимацию элемента, если она уже есть
if (!el.ltAnimateInterval) return false;
el.stopAnimation(jumpToEnd, callback);
};
Ну и самое последнее — обработчик ошибок. Он пишется в $.w.ltAnimate и вызывается, если требуется, из неё же:
Код 11
// обработка ошибок
function doFail(text) {
if (self._debug._enabled) {
if ((typeof text != "string") || !text) text = "С анимацией [id: " + id + "] что-то не так.";
$.w.console.log("ltAnimate(): Внимание! " + text);
}
if (opts.cbFail) {
try {
opts.cbFail();
} catch (e) {
$.w.console.log("ltAnimate(): Внимание! Ошибка выполнения коллбэка анимации [id: " + id + ", " + e.name + ": " + e.message + "]");
}
}
}
→ Попробовать погонять прямоугольнички самому можно здесь
Там же можно взять полный исходный код всех трёх функций.
_____________________________________
ps. Поскольку код писался полтора года назад, подробности некоторых моментов, почему было сделано именно так, уже начали стираться из памяти. Однако, если вы зададите вопрос, я постараюсь максимально точно вспомнить, что имел ввиду, когда писал код.
Безусловно, представленный код не является идеальным. Буду рад конструктивным замечанием и указанием на ошибки и дополнения.
Также было бы интересно обсудить, как можно подойти к задаче разработке анимации элементов в браузере, используя другой подход.