Однако, иногда стоят действительно творческие задачи, и обычный копипаст не спасает демократию (честно говоря, он почти никогда не спасает). Об одном из таких случаев я и хочу рассказать уважаемой публике.
Предыстория такая. Мы в компании разрабатывали продукт, клиентская часть которого должна была быть написана на чистом 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. Сразу выполняем необходимые проверки:
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.
Теперь действия, которые необходимо совершить с элементом, вносим в массив:
// заносим в массив, что будем выполнять
var instructions = [];
for (var key in props) {
if (!props.hasOwnProperty(key)) continue;
instructions.push([key, props[key]]);
}
// если выполнять нечего, выдаём ошибку
if (instructions.length === 0) {
doFail("Не сказано, что делать с элементом");
return;
}
Дефолтные опции перезаписываем клиентскими там, где это указано (естественно, с проверками):
// перезаписываем опции клиентскими значениями
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 = {};
Здесь, наконец, до нас доходит, что нужна будет ещё какая-то дополнительная функция, с помощью которой мы опишем анимируемый элемент (о ней чуть дальше). А пока сделаем ещё одну важную проверку.
Количество и очередь анимаций
Да, задача становится всё сложнее. Дело в том, что анимация может вызываться на элементе, который уже находится в состоянии выполнения предыдущей анимации (и ещё не закончил её). Наша функция должна быть универсальна, поэтому она должна уметь принимать и корректно обрабатывать цепь независимых вызовов.
Ниже дан код. К нему есть построчные пояснение, поэтому просто привожу целиком этот блок:
// если вторая или более анимация на этом объекте
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(). Вот тут-то, в самом начале, мы и описываем наш элемент через его свойства (делать это раньше нельзя, т.к. элемент, пока анимация находилась в очереди, мог быть изменён):
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 (да-да, мы пишем настоящий коммерческий код, который продаётся за деньги в разные страны, поэтому аргумент «это не модно» не принимается!), сжимаем всё волю в кулак и идём забарывать ослиные заморочки:
/**
* Получение всех стилей элемента (если подан только 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) {};
В первой её части, которая приведена ниже, самое интересное — расшифровка инструкций анимации («что делать с объектом?»). Надеюсь, все помнят, что некоторые свойства элемента (размеры, положение, отступы) могут задаваться не только пикселями, но и процентами:
// значение параметра
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);
Вот это есть наш «движок» анимации.
Обратите внимание: мы устанавливаем интервал как свойство элемента, ведь нам в последующем нужен будет доступ извне, чтобы прекратить выполнять анимацию (то есть обнулить интервал).
Наконец, функция отрисовки элемента, которая выполняется через заданные промежутки времени всё время выполнения анимации:
// 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, это интерфейс вызова остановки анимации:
// интерфейс остановки анимации
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 = [];
}
}
Вызов остановки анимации прост: мы просто указываем элемент, говорим, нужно ли в момент остановки перейти в конечную точку анимации, и указываем, если нужно, новый коллбэк.
Для прекращения анимации у нас есть отдельная глобальная функция:
/*
* Остановкка анимации: элемент, переход в конечную точку (true/false) и остановить ли выполнение коллбэка (true/false), два последних необязательно
*/
$.w.ltAnimateStop = function(el, jumpToEnd, callback) {
// останавливаем анимацию элемента, если она уже есть
if (!el.ltAnimateInterval) return false;
el.stopAnimation(jumpToEnd, callback);
};
Ну и самое последнее — обработчик ошибок. Он пишется в $.w.ltAnimate и вызывается, если требуется, из неё же:
// обработка ошибок
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. Поскольку код писался полтора года назад, подробности некоторых моментов, почему было сделано именно так, уже начали стираться из памяти. Однако, если вы зададите вопрос, я постараюсь максимально точно вспомнить, что имел ввиду, когда писал код.
Безусловно, представленный код не является идеальным. Буду рад конструктивным замечанием и указанием на ошибки и дополнения.
Также было бы интересно обсудить, как можно подойти к задаче разработке анимации элементов в браузере, используя другой подход.
Комментарии (25)
bromzh
23.11.2016 14:361) CSS-анимации
2) https://github.com/web-animations/web-animations-js
3) https://greensock.com/get-started-js
botyaslonim
23.11.2016 14:42+1Знаете, я ждал подобных комментариев.
Первый говорит об альтернативном способе воспроизведения анимации (без JS), второй предлагает готовое решение.
Между тем, кмк, в описании задачи (я подчеркнул это слово, потому что решение было в рамках бизнес-задачи, а не свободного творчества) недвусмысленно сказано, что:
а) Это всё должно успешно работать на IE8
б) Сторонний код принципиально недопустим
Предлагаю данную тему рассматривать в разрезе решения конкретно поставленной задачи, а не как попытку осмыслить, как бы не решать её, а заняться какой-то другой.bromzh
23.11.2016 15:17+1А зачем обсуждать странную полутрогодовалую задачу? Особенно в сфере веба, где технологии меняются очень быстро. Поддерживать ie8 в 2016 не надо. Доля восьмёрки чуть меньше нуля. Даже MS теперь довольно агрессивно обновляет свои продукты и внедряет стандарты в браузеры.
Не, я понимаю, что статья дала вам инвайт на хабр, но давайте лучше обсуждать свежие идеи и стандарты, а не ковыряться в старом г-не.
botyaslonim
23.11.2016 15:24Не, я понимаю
— нет же, вы не понимаете :)
Я прямо словами написал, что продукт используется в разных странах. В том числе, таких, где доля IE8 до сих пор значительна.
Здесь, возможно, может последовать вполне предсказуемый холивар на тему, а зачем сотрудничать с такими компаниями, зачем продавать в такие страны, а зачем программисту горбатится в компаниях, которые разрабатывают такое «старое г-но». Но я, право, не имею никакого желания заниматься праведной борьбой в пучине этого платоновского моря чистой мысли :)
bromzh
23.11.2016 16:28+1Нет, я не буду спорить с кем и чем вам стоит работать, это сугубо ваше дело.
У меня такой вопрос: если вы так жаждете обсуждения иных подходов при такой поставленной задаче, вы смотрели готовые решения? Не с целью их использовать, а с целью изучить подход. Если да, то какие и почему они не подошли?
Когда мне понадобилилась библиотека для анимации я быстро нашёл GSAP, ссылку на который я уже кидал. Полтора года назад он уже точно был. Библиотека имеет довольно маленькое ядро, и при этом очень быстрая и поддерживает всё вплоть до ie6.
i360u
23.11.2016 15:38Это "а" и "б" вы сейчас добавили, а в статье было так:
1) Анимируется любой элемент на странице, количество элементов не ограничено
2) Список анимируемых свойств задан (линейные размеры, положение, отступы, прозрачность)
3) Анимация должна управляться через параметры (время исполнения, функция скорости исполнения)
4) По завершению анимации вызывается любой произвольный коллбэк
5) Анимация в любой момент может быть прерванаТ. е. вполне резонные вопросы в комментариях. IE8 — уже экзотика, не всем понятно зачем его поддерживать по умолчанию, у такого решения должно быть очень веское обоснование, о котором Вы умолчали.
creater777
23.11.2016 15:15Почему в коде одни свитчи. И зачем огородить огород? На твоем маке тормозов не наблюдается?
botyaslonim
23.11.2016 15:16Предложите, как сделать лучше, я с удовольствием ознакомлюсь.
Тормозит в разумных пределах не только на Маке, но даже на стареньких андроидах. Тестирование продукта у нас поставлено достаточно хорошо, он продаётся в разные страныi360u
23.11.2016 15:45Лучше — реализовать анимацию через CSS и "изящно деградировать". Это избавит от тормозов на современных системах и на старых с IE8 тем более.
botyaslonim
23.11.2016 15:47Я всё-таки проявлю упорство и переспрошу: как лучше организовать JS-код?
creater777
23.11.2016 16:56Извини, может это дело вкуса, но считаю что каждый свитч есть отдельная функция. Просто так кажется лучше
qbz
23.11.2016 21:30function 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; } }
можно изменить на
const getProgress = (progress) => { const easeFn = { swing: progress => (0.5 - Math.cos(progress * Math.PI ) / 2), cubic: progress => (Math.pow(progress, 3)), quad: progress => (Math.pow(progress, 2)) }[options.easing]; return easeFn ? easeFn(progress) : progress; }
kahi4
23.11.2016 16:21+3Сразу создаём var $ = {d: window.document, w: window}, чтобы не лезть прямо в window.
И тут же
$.w.ltAnimate = function (el, props, opts, cb) {});
Судя по всему, чтобы лезть в window через определенное отверстие?
И да, Люк, не гадь в глобальный скоуп.
setInterval
Есть аж, наверное, 3 причины, почему нельзя использовать setInterval, даже если оооочень хочется. Это даже типовой вопрос на middle frontend developer.
Вопрос на senior: почему следует избегать try-catch в js? (Вдобавок, зачем он используется тут вместо проверки существования функции?).
Ну и да, таймауты, поджидающие конец интервалов — у вас никогда более-менее сложная анимация не сойдется. (Подсказка, js не гарантирует, что setTimeout(..., 1000) выполнится через секунду. Более того, даже примерно это не обещает).
botyaslonim
23.11.2016 16:25Судя по всему, чтобы лезть в window через определенное отверстие?
Совершенно верно. Но вообще, код выдернут из некой библиотеки, так что обвязка весьма условна. Впрочем, и не она суть темы.
Вдобавок, зачем он используется тут вместо проверки существования функции?
Всё просто: что делать, если во время исполнения коллбэка (мы не знаем, что туда напхают) вылезет exception?
А вот как избежать try-catch, я бы сам заслушал. Пока ещё не senior, есть, чему учитьсяkahi4
23.11.2016 17:36+1Подобным образом пишут чтобы как раз не писать в глобальный scope в js. Почитайте umd, лучше сделать так. В window желательно вообще ничего лишнего, тем более в 2016 веке с поголовным es6 import, require.js и подобными технологиями.
Если же беспокоитесь, что кто-то там странно и криво вызовет вашу библиотеку в неожиданных местах — это уже скорее его проблемы, покуда там либо будут глобально доступны setTimeout и иже с ним, либо подобным вариантом уже не спасти.
setInterval никогда нельзя использовать в js, покуда движок будет ставить, допустим, каждые 50 мс событие в очередь независимо от того, успела ли очередь выполнения рассосаться или нет. Как не сложно догадаться — если время, через которое по интервалу будут добавляться события меньше, чем время рассасывания этих сообщений, у вас все застрянет наглухо и в какой-то момент попросту повиснет. Ну и что уж говорить про то, что в таком случае желаемые 50 мс будут даже близко не равны реальным, что будет выглядеть дерганно в конечной анимации. И это прям совсем не редкая ситуация.
- а: в данном случае в вашей библиотеке достаточно написать
if (options.sucess) options.success();
(ну илиoptions.success && options.success();
если вы js-ninja, хотя этот вариант не читаемый, так что не стоит).
Если оно упадет в этой функции — заботы того, кто использует библиотеку. Более того, своей оберткой над этим вы только навредите, покуда найти источник ошибки будет сложнее.
б: есть 3 условия, которые должны быть выполнены для того, чтобы движок webkit мог скомпилировать функцию в нативный байт-код. Так получилось, что отсутствие блока try-catch одно из них. Обычно это не столь важно (в вашем примере так же не важно), однако в целом когда речь про какую-либо анимацию на js — позволять скомпилировать webkit-y код в нативный идея хорошая. (Кто не знает, два других условия: вроде как функция должна быть вызвана не менее 100 раз и у функции не должны меняться типы входных значений. И это справедливо только для webkit, про другие движки не скажу. Впрочем, это тоже вероятно поменялось, документации по этому особо нет).
botyaslonim
23.11.2016 17:571. Нет, здесь только чистый JS, никаких require и прочих библиотек. До ES6 тоже ещё очень далеко. Но это так, заметка, по сути это вопрос на отдельную тему.
2. Вот тут интереснее. Interval всё-таки существует в JS. Значит, это инструмент, который нужно использовать по назначению.
Для функции я сделал очередь входящих вызовов. Конечно, её можно забить, вызвав, допустим, 1000 раз с с разницей в 1мс (думаю, при таких условиях любой браузер просто повиснет). Однако, ровно так же повиснет и jQuery-анимация, и любая другая, сделанная именно на JS. Тут мы упираемся в возможности связки язык + браузер. Для практического же применения (несколько анимируемых элементов на странице, не тысяча) моё решение, безусловно, подходит, и неплохо работает. Поэтому утверждение "setInterval никогда нельзя использовать в js" мне не кажется разумным. Каждый раз надо разбираться.
3. «Если оно упадет в этой функции — заботы того, кто использует библиотеку» — такой подход тоже является спорным. Точнее, так: можно сделать обработку ошибок, можно не делать. В данном случае, как я вижу, «призом» за отказ от обработки является некоторые призрачные шансы, что webkit скомпилирует нативный байт-код. Не очень железно. Хотя за подсказку идеи Вам спасибо, раньше об этом не слышал.Nadoedalo
23.11.2016 20:48Скрытый текстХотел написать комментарий о том что в интернете кто-то не прав, но я полностью согласен с предыдущими комментариями, а после этого ответа автора вообще отпала необходимость что-либо комментировать.
viktornaymayer
24.11.2016 10:46Весьма нерентабельно, но интересно и познавательно в плане «сделай сам». Автору спасибо :)
i360u
А полтора года назад CSS-анимаций еще не было? Может я чего-то не понимаю, но зачем вообще было огород городить, если через CSS все это делается проще и щадит ресурсы при отрисовке?
stas404
i360u
Никто не мешает CSS-анимацию использовать из JS, они же все равно обращаются к style в коде...
Aingis
Вероятно, были сложности с пунктом
Хотя, если извратиться (переопределятьtransition
на отсутствие анимации), наверное, можно сделать.Плюс с коллбеком я встречался с багом, что в Хроме не выполнялся колбэк
transitionend
(возможно, и в других браузерах, не проверял). Не очень понял причину, но возможно из-за того, что элемент был уже в конечном состоянии, поэтому анимации не было. Проблема в том, что эту ситуацию через JS в принципе не определить. Максимум, что можно сделать, фолбек черезsetTimeout
.Но вообще, по-хорошему, надо конечно использовать Web Animation API и полифил.