Pull to refresh

За кадром — Media Query Mario

Reading time 9 min
Views 13K
Original author: Ashley Nolan

В этом хабратопике пойдет речь о создании демки Media Query Mario, о которой упоминалось в дайджесте Zfort #30.

Я обожаю каждое новое техническое демо, которое добирается до моего браузера. То, что люди порой в них совмещают, может взорвать мозг своей сложностью и креативностью.

После посещения WebDevConf 2012 в середине октября, я чувствовал то самое, возвышенное вдохновение, которое преподносит хорошая конференция. Возвращаясь с конференции в Лондон, я заметил твит о Mozilla Dev Derby, и, все еще вдохновленный, решил внести свой вклад. Результатом стало техническое демо Media Query Mario, которое представляет собой смесь из медиа запросов, CSS3 анимаций и HTML5 audio.

С чего начать?


Идею я начал обдумывать руководствуясь списком технологий, с которыми мне хотелось поэкспериментировать больше всего. Мне показалось вполне логичным углубиться на некоторое время в CSS анимации и смешать их с медиа запросами — главной темой этого Dev Derby. Отдать CSS контроль над анимацией вместо JavaScript — звучит неплохо.

Марио 3 оказался первым, что пришло в голову. Хотелось получить двумерный сайдскроллер, и, так как я помешан на ретро играх, я сразу подумал о Марио. Каждый, кто не просто интересуется играми про Марио, поймет, что Марио 3 был единственным возможным кандидатом на роль (если кто-то не согласен, то у меня всегда найдутся аргументы, чтобы доказать, что это лучший двумерный Марио).

После публикации демки, меня спросили, зачем использовать CSS анимации, когда другие технологии могли бы подойти лучше? Главная причина состоит в том, что я просто хотел посмотреть, на что они способны. Существует много демок, демонстрирующих крутость canvas и SVG, но моя отнюдь не предназначена для пропаганды CSS анимаций. Я всего лишь хотел оценить их текущее состояние, и начать разговор, о том, что у них тоже есть своя область применения.

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

Для начала нажмите на любую кнопку


Одна из первых проблем, с которыми я столкнулся, состояла в том, чтобы понять, при какой ширине экрана пользователь будет все это просматривать. Это было важно не только при проектировании размера анимаций, это было важно чтобы определить, какой кусок уровня будет показан пользователю единовременно. Чем больший кусок просматривается, тем больше нужно анимации за раз.

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

@media screen and (max-width: 320px), (min-width: 440px) {
    .startBtn {
        display:none;
    }
}

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

Закончив с планированием структуры, мне осталось лишь добыть ресурсы. Как и ожидалось, найти в интернете оригинальные картинки, спрайты и звуковые файлы из игры было проще простого. На NESMaps и Mario Mayhem я подобрал карты уровня и спрайты персонажей и объектов, а на The Mushroom Kingdom я нашел подходящие звуки. Картинки пришлось немного подредактировать, но они всеравно позволили мне быстро и хорошо стартовать.

Вот что использовалось в дальнейшем для анимации:


Поехали!


Итак, к этому моменту я имел на руках продуманную идею и подходящие ресурсы, и уже был готов совместить их в коде.

Для начала, я изучил спецификацую CSS анимаций. Несколько ресурсов действительно меня выручили. MDN (Mozilla Developer Network) отличная отправная точка, что и в этом случае не стало исключением. Я бы еще порекомендовал Вам прочитать любую из этих прекрасных статей Питера, Криса или Дэвида — все они являются отличным введением в тему.

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

Небольшое замечание, которое стоит сделать: когда пользуешься новыми фичами CSS3, использование препроцессора, вроде LESS или SASS, может сильно упростить жизнь, и я бы настоятельно рекомендовал Вам посмотреть в их сторону. Использование миксинов прячет вендорные префиксы и убирает их из кода, с которым Вы работаете непосредственно, позволяет избежать беспорядка в коде и экономит кучу времени при глобальной замене CSS свойств.

Прежде чем мы приступим к изучению специфичных техник, нам нужно понять, что анимация состоит из двух главных частей: свойства анимации (animation properties) и связанные с ней ключевые кадры (keyframes).

Свойства анимации


Анимация может быть создана с помощью нескольких связанных свойств. Вот главные свойства, которые использовал я:
//устанавливает имя анимации, которое напрямую относится к набору ключевых кадров
animation-name: mario-jump;
 
//время, за которое анимация будет проиграна, в милисекундах или секундах
animation-duration: 500ms;
 
//функция прогресса анимации в зависимости от времени
animation-timing-function: ease-in-out;
 
//как долго анимация ожидает своего старта, милисекунды или секунды
animation-delay: 0s;
 
//сколько раз анимация должна быть проиграна
animation-iteration-count: 1;
 
//должна ли анимация применить отрисованные стили к анимируемому элементу и когда
animation-fill-mode: forwards;

Использование в демке свойства animation-fill-mode было особенно важным, так как оно говорит анимации применить финальные стили по окончанию воспроизведения. Без этого элемент вернется к тому состоянию, в котором он находился до анимации.

К примеру, если animation-fill-mode не установлен когда мы двигаем левую границу объекта на 30px с изначальной позиции в 0px, то после анимации он вернется в позицию 0px. А если fill-mode имеет значение forwards, то элемент останется в своей конечной позиции.

Ключевые кадры


Правило @keyframes позволяет Вам указать шаги анимации. На самом базовом своем уровне, это правило может быть записано как:
@keyframes mario-move {
    from { left:0px;   }
    to   { left:200px; }
}

Где from и to это ключевые слова для 0% и 100% анимации, соответственно. Чтобы показать более сложный пример, давайте посмотрим на код, который анимирует прыжок Марио между несколькими платформами:
@keyframes mario-jump-sequence {
    0% { bottom:30px; left: 445px; }
    20% { bottom:171px; left: 520px; }
    30% { bottom:138px; left: 544px; }
    32% { bottom:138px; left: 544px; }
    47% { bottom:228px; left: 550px; }
    62% { bottom:138px; left: 550px; }
    64% { bottom:138px; left: 550px; }
    76% { bottom:233px; left: 580px; }
    80% { bottom:253px; left: 590px; }
    84% { bottom:273px; left: 585px; }
    90% { bottom:293px; left: 570px; }
    100% { bottom:293px; left: 570px; }
}


Если указанная выше анимация длится 1 секунду, то Марио передвинется из позиции bottom: 30px; left: 445px; в 0 секунд (0% анимации) в позицию bottom: 138px; left: 520px; за первых 200 мс (или 20%). И так далее, от первого ключевого кадра до последнего.

Анимация действия


Анимации которые из демки можно разбить на три категории по своему типу:
  • Перемещения, такие как прыжки Марио или монетка, выскакивающая из коробки с вопросом.
  • Управление спрайтами, т.е. контроль фоновой картинки персонажей и объектов.
  • Зацикливание любой анимации, которую нужно повторять некоторое количество милисекунд или секунд.


Перемещение


Перемещение составляет примерно 75% всей анимации демки. К примеру, движение персонажа (бег или прыжки), появление пауэр-апов и удары по коробке с вопросом. Различие между анимациями перемещения задается свойствами animation-timing-function, animation-duration и animation-delay.

Свойство animation-timing-function помогает контроллировать скорость анимации на протяжении всей её длительности. Везде, где было возможно, я использовал функции смягчения (easing), такие как ease-in or ease-in-out, потому что иначе пришлось бы описывать каждый шаг анимации слишком подробно. Но когда это не помогало достичь необходимого эффекта, я делал animation-timing-function линейной и использовал ключевые кадры, чтобы достичь того результата, который был мне необходим.

Пример анимации перемещения можно увидеть тут.

Спрайты


Чтобы контроллировать background-position персонажей и объектов, я использовал функцию step-end:

.mario {
    animation-timing-function: step-end;
    ...
}

Изначально я полагал, что мне понадобится использовать JavaScript для контроля над спрайтами, добавляя и удаляя классы у элементов. Тем не менее, после экспериментов с внутренностями step-end, я нашел идеальное сочетание.

Чтобы увидеть это в действии, посмотрите на простую анимацию движения Марио и превращение Марио после съедания грибочка.

Тем не менее, использование step-end таким образом не так уж безболезненно. К своему удивлению, я обнаружил глюк в webkit, который проявлялся при пересечении нескольких медиа запросов и заставлял анимацию отрисовываться иначе, чем предполагалось. Замечу, что такое применение CSS анимаций — это пограничный случай для движка рендера браузера, но это действительно было багом Chromium, и я надеюсь, что в будущем его исправят.

Зацикливание


Каждый раз когда анимация должна продолжаться на протяжении какого-то времени, зацикливание задавалось настройкой animation-iteration-count:

//анимация повторяется 5 раз
animation-iteration-count: 5;
 
//анимация повторяется бесконечно
animation-iteration-count: infinite;

Примером этого из демки может послужить вращение огненного шара.

Таким образом, из трех типов анимации и была сконструирована вся демка. Последним этапом стала озвучка.

Озвучка


Хотя ранее я скачал все нужные мне звуковые файлы в формате .wav, мне пришлось конвертировать их в формат, доступный для HTML5 audio.ogg и .mp3. Я использовал Switch Audio Convertor (на Mac) для этого, но отлично подойдет и любой другой софт.

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

var audio = new Audio(); //объявляем базовый аудио объект
var canPlayOgg = !!audio.canPlayType && audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";
var canPlayMP3 = !!audio.canPlayType && audio.canPlayType('audio/mp3') !== "";

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

//создаем звуковые объекты и подгружаем их
function createAudio (audioFile, loopSet) {
    var tempAudio = new Audio();
    var audioExt;
 
    //решаем, в каком формате проигрывать
    if (canPlayMP3) {
        audioExt = '.mp3';
    } else if (canPlayOgg) {
        audioExt = '.ogg';
    }
 
    tempAudio.setAttribute('src', audioFile + audioExt); //set the source file
    tempAudio.preload = 'auto'; //подгружаем звуковой файл
 
    //говорим, будет ли звук зациклен (нужно зацикливать фоновую музыку)
    tempAudio.loop = (loopSet === true ? true : false);
 
    return tempAudio;
}
var audioMarioJump = createAudio("soundboard/smb3_jump"); //пример вызова

Дальше осталось только проигрывать звуки в нужное время, синхронно с анимацией. Чтобы слушать события animationstart и animationend (или в WebKit, webkitAnimationStart и webkitAnimationEnd), мне понадобился JS. Так я научился ловить моменты начала и конца анимаций, и проигрывать подходящий для ситуации звук.

Когда срабатывает обработчик события, можно получить доступ к свойству animationName, которое мы можем использовать как идентификатор.

mario.addEventListener('animationstart', marioEventListener);
 
function marioEventListener(e) {
    if (e.animationName === 'mario-jump') {
        audioMarioJump.play();
    }
}

Если у Вас есть несколько событий animationstart для одного элемента (например, Марио в демке), Вы можете обрабатывать animationName с помощью конструкции switch.

С тех пор, как я написал это демо, я обнаружил, что используя Keyframe Event JS shim от Joe Lambert можно ориентироваться на каждый отдельный ключевой кадр анимации, что дает еще больше контроля.

Конец игры


Я получил более позитивные отзывы об этой демке, чем вообще когда-либо мог бы себе представить. Как и в любом другом хаке (пост взят с hacks.mozilla.org, прим. переводчика), здесь еще есть над чем поработать, но я думаю, что важнее было бы вложить все, чему я научился, в свой следующий проект. Я думаю, демка показала, что CSS анимации можно использовать для создания впечатляющих эффектов путем написания реально простого кода.

Сложные CSS анимации уже способны показать хорошую производительность, но их создание все еще сильно утомляет. Конечно, существуют инструменты, которые упрощают задачу, такие как Adobe Edge Animate и Sencha Animator, но они производят CSS анимации, обернутые в JS. Это провал, как по мне, так как сила CSS анимаций заключается в том, что они могут работать не опираясь на другие технологии. Я не уверен, что существует какой-нибудь способ обойти необходимость их написания вручную, но если вдруг кто-то знает что-то подобное, я бы с удовольствием узнал об этом в комментариях.

Возвращаясь к моему предыдущему комментарию, в котором я сравнивал CSS анимации с использованием canvas и SVG, я считаю, что все они являются достойными кандидатами при выборе технологии для анимаций. Тем не менее, чем раньше барьер времени, затрачиваемого на создание подобных сложных анимаций, упадет, тем больше в нашей ежедневной работе откроется подходящих и актуальных мест для их применения.

Да, анимация заканчивается там, где Марио улетает в космос по диагонали. Это не баг, а простая недоработка.
Tags:
Hubs:
+26
Comments 13
Comments Comments 13

Articles