Анимированные баннеры на Javascript — это просто*

*) На самом деле все равно сложно, но зато проще, чем было раньше.

История началась с постановки задачи: нужно сделать анимированный баннер с примерно тридцатью объектами средствами HTML+javascript за один день. За день, конечно, баннер сделан не был, а был сделан за два усилиями трех человекодней. После выполнения задания осталась библиотека пакетной анимации, которую я назвал Scenario. О её доработанной версии я и хочу рассказать.

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

Запуск сценария из любого места предельно прост:

var newScenario = [...];
$.scenario(newScenario, {
    complete: function(time) {
        alert('Готово!');
    }
});

Осталось только разобраться, что писать вместо трех точек в примере :)

Описание сценариев


Любой сценарий является массивом анимируемых объектов либо один анимируемым объектом. Каждый анимируемый объект может содержать три основных свойства:
Таким образом, весь сценарий представляет из себя дерево анимируемых объектов. Причем структура этого дерева не обязана совпадать со структурой HTML-элементов, анимацию которых описывает сценарий. Мне представляется, что начинать сценарий удобнее всего с построения этого дерева. В конце должно получиться что-то такое:

var newScenario = {
    element: '#scenario',
    child: [{
        element: '.rocket_smile'
        child: [{
            element: '.fire'
        }, {
            element: '.eyes'
        }, {
            element: '.ears'
        }]
    }, {
        element: '.rocket_atack'
        child: {
            element: '.fire'
        }
    }, {
        element: '.cloud',
        child: [{
            element: ['eq', 0]
        }, {
            element: ['eq', 1]
        }
    }]
};


А теперь магия — описание объекта сцены.
Параметры before и after позволяют в большинстве случаев обойтись без функций обратного вызова start и end. Помимо обычных css-свойств, в них можно указать некоторые дополнительные манипуляции над анимируемыми элементами: поддерживаются функции show, hide, attr, removeAttr, addClass, removeClass, toggleClass. Небольшой пример, из которого все станет понятно:

scene: [{
    time: [2500, 4000],
    before: { opacity: 0, show: '' },
    animate: { opacity: 1 }
    after: { opacity: '' }
}, {
    time: [5500, 6500],
    before: { addClass: 'rocket_wink' },
    after: { removeClass: 'rocket_wink' }
}]


Ну и сферический код облаков в вакууме:

{
    element: '.cloud',
    child: [{
        element: ['eq', 0],
        time: [0, 7500, 'linear'],
        before: { top: -80, display: 'block', left: 300 },
        animate: { top: 374 }
    }, {
        element: ['eq', 1],
        scene: [{
            time: [2500, 5500, 'linear'],
            before: { top: -80, display: 'block', left: 500 },
            animate: { top: 374 }
        }, {
            time: [5500, 9000, 'linear'],
            before: { top: -80, display: 'block', left: 150 },
            animate: { top: 374 }
        }]
    }]
}


Обратите внимание, что элемент .cloud вообще не имеет анимации, а элемент eq(0) не содержит свойства scene, но содержит свойства time, before и animate — это возможность еще немного сократить запись сценария.

Для усвоения основных принципов пока хватит.

Почему бы не сделать все на jQuery.animate. Живой пример


На первую часть подзаголовка я отвечу второй. Я подготовил пример анимации, сделанной с помошью Scenario. Пример очень похож на баннер, о котором шла речь в самом начале, правда я вырезал из него всю айдентику и заменил главных персонажей на ниндзя. Кроме того, ради интереса я сделал такой-же баннер на jQuery.animate (тот, что снизу).

Живой пример

И вот почему jQuery.animate не слишком подходит для подобных задач:
  1. Animate вызывает функцию complete после окончания анимации. Но она ничего не вызывает перед стартом. Из-за этой особенности приходится пользоваться очередями:
    .delay(2500)
    .queue(function(next) {
        $(this).css({top: 82, left: 74});
        next();
    })
    .animate({top: 85}, 1500)
    

  2. Animate анимирует все HTML-элементы в текущем jQuery-объекте независимо друг от друга. Для каждого из элементов после окончания анимации вызывается функция complete. Функция step же вызывается для каждого свойства каждого элемента на каждом шаге анимации. Это еще более неудобно.

  3. Через animate нельзя просто прогнать функцию step в течении определенного времени. При пустом наборе анимируемых свойств animate немедленно возвращает результат. Приходится использовать грязный трюк: анимировать какое-либо значение из 0 в 0.

  4. Множество точек выхода из анимации. Если 10 разных объектов одновременно заканчивают анимацию, на событие complete какого из них поставить завершающий код баннера? В каком событии complete искать этот код через 10 дней?
    Scenario, напротив, имеет одну точку выхода, это complete функции $.scenario()
Из-за этих проблем получается такой код:

// сначала сохраним исходный селектор, чтобы иметь к нему доступ
var $rocket_smile_fire = $rocket_smile.find('.fire');
$rocket_smile_fire
  // Первый хак: нам нужно, чтобы калбэки вызывались только один раз
  .eq(0)
  // Второй хак: marginTop и так 0, но без него анимация вовсе не запустится 
  .animate({marginTop: 0}, {
    duration: 9000,
    step: function(x, opt) {
      // Вместо обращения к this, обращаемся к внешней переменной
      $rocket_smile_fire
        .hide()
        // Из информации о времени предоставляется только время начала анимации
        .eq(((new Date() - opt.startTime) / 100) % $rocket_smile_fire.length)
        .show();
    },
    complete: function() {
      $rocket_smile_fire.removeAttr('style');
    }
  });


Это, конечно, печально, но жить можно, если бы не…

Киллер-фича Scenario


Представьте: анимация, которую вы делаете, длится 20 секунд. Вы делаете финальную сцену, которая длится секунды две. Сколько времени тратится впустую, пока вы просматриваете баннер до последних секунд? Как решить проблему в jQuery? Закомментировать кусок кода, делающий большую часть работы. При этом не факт, что все объекты будут на своих местах, ведь код, который их двигал, закомментирован.

Решение от Scenario — просто укажите с какого времени начать сцену и в какое закончить. Ну и скорость воспроизведения можно задать. И частоту кадров до кучи.

Пример с возможностью настраивать время

Отладка — одно удовольствие.

Еще немного о сценариях


Без описания осталось свойство анимируемого объекта element и функции обратного вызова для сцен. Восполняю пробел. Element может принимать следующие значения:

Функции обратного вызова start, end и step выполняются в контексте найденного элемента. Первым аргументом все три принимают время, прошедшее с начала сценария. Последним — объект текущей сцены, расширенный объектом анимации:

{
    element: '.rocket_smile',
    button: $('#start-button'),
    scene: {
        time: [0, 2500],
        fadeDuration: 400,
        start: function(time, scene) {
            // scene.fadeDuration доступно из текущей сцены
            // scene.button наследуется из объекта анимации
            scene.button.fadeOut(scene.fadeDuration);
        },
        animate: { top: 69 }
    },
}


Функция обратного вызова step, помимо этих двух параметров, принимает еще один параметр с подробной информацией о текущем шаге анимации:
time, время с начала сценария;
passed, время с начала сцены;
progress, время с начала сцены, нормированное по отрезку 0..1;
position, это progress после применения функции изинга.

{
    element: '.rocket_atack',
    scene: {
        before: { top: 20 },
        // пользовательское свойство, доступное в функции step
        lastTop: 300,
        step: function(time, opt, scene) {
            // начальное значение параметра
            var startTop = scene.before.top;
            // на сколько изменится параметр
            var changeTop = scene.lastTop - startTop;
            // this — текущий элемент 
            this.css({top: startTop + changeTop * opt.position});
        }
    }
}


Код Scenario на Гитхабе


Планы на будущее

Многие не поняли, что про визуальный редактор — это шутка. Пишу прямым текстом — это шутка.
@homm
15.02.2011 03:40 UTC
Первоисточник