Фронтенд Средиземья, часть вторая

Обзор разработки под разные устройства

В нашей первой части статьи о разработке эксперимента для Google Chrome Хоббит: Пустошь Смауга - Путешествие по Средиземью мы сконцентрировались на работе с WebGL для мобильных устройств. В этой статье мы рассмотрим все остальные задачи, описание которых не вошло в первую часть, основные проблемы и их решения, к которым мы пришли в ходе работы над остальной частью HTML5 фронтенда нашего проекта.

Три версии одного и того же сайта

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

Весь проект основан на так называемом «кинематографическом» стиле, где мы хотели бы побольше использовать ландшафтную (горизонтальную) ориентацию экрана, чтобы сохранить сходство с фильмом. Основываясь на том, что большая часть сайта состоит из мини-игр, они так же должны использовать ландшафтную ориентацию.

Можно привести посадочную страницу сайта в качестве примера адаптации дизайна под разные размеры экранов.

Крылья орлов только что принесли нас на посадочную страницу.

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

Мы использовали данные из user-agent для определения мобильных устройств и размера окна браузера, с последующим вычленением из этих данных группы планшетов, с размером окна от 645 px и выше. В принципе, каждый из основных режимов может отображать все доступные разрешения, потому что наша верстка основана на медиа запросах или относительном позиционировании в процентах при помощи JavaScript.

Так как в нашем случае дизайн не привязан к жёсткой сетке и контент очень меняется от модуля к модулю, и контрольные точки (breakpoints) в стилях сильно зависят от специфического поведения конкретных элементов и сценариев их использования. Неоднократно случалась ситуация, когда наш идеально свёрстаный с помощью миксинов Sass и медиа запросов интерфейс необходимо обновить, добавив эффект, который привязан к положению курсора или других динамических объектов. В итоге, всё закончилось тем, что переписали большую часть фронтенда под JavaScript.

Кроме этого, мы добавили класс с текущим режимом в родительском тэге для использования в нашем стилевом оформлении, как в этом примере на SCSS:

.loc-hobbit-logo {

    // Значения по умолчанию.

    .desktop & {
        // Стилевое оформление применяется 
        // только для режима «декстоп».
    }

    .tablet &, .mobile & {
        
        // Другие стили для планшетов и мобильных.
            @media screen and (max-height: 760px), (max-width: 760px) {
            // Стили определяются для данных контрольных точек.
        }
            @media screen and (max-height: 570px), (max-width: 400px) {
            // Стили определяются для данных контрольных точек.
        }
    }
}

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

Примечание: События DeviceOrientation Вёрстка контента управляется контрольными точками и CSS, но, также нам нужно знать, когда происходит изменения ориентации устройства, и отслеживать это событие в JavaScript, чтобы приостанавливать игру и анимацию, и сохранять при этом текущее состояние приложения. Оказывается, что нельзя так уж полагаться на значение window.orientation, потому что оно не стандартизовано и неодинаково для разных устройств. Вместо этого лучше следить за состоянием window.innerWidth и window.innerHeight, чтобы правильно определить текущую ориентацию.*

Хотелось бы еще раз напомнить вам, что не стоит смешивать вёрстку с определением возможностей приложения, таких как способ ввода данных пользователем, ориентация устройства, наличие сенсоров и прочее. Всё это должно присутствовать в разных режимах и корректно работать в них. Например, одновременная поддержка мыши и тач-событий. Или, например, поддержка ретина-дисплеев для улучшения качества изображения. Однако, приоритетным для нас является улучшение производительности — ведь чаще всего производительность важнее просто четкой картинки. Так, canvas-элементы генерируются в половину доступного на ретина-дисплеях разрешения, иначе пришлось бы отрендерить в четыре раза больше пикселей.

Можно легко испытать все размеры приложения прямо в браузере, эмулируя различные типы устройств в Chrome DevTools. При переключении режима между мобильным, планшетом и настольным компьютером нужно перезагружать сайт для получения правильных зависимостей и настроек.

Мы часто пользовались этим инструментом во время разработке, особенно в Chrome Canary, в котором предлагаются улучшенные возможности эмуляции и большее количество различных предустановок. Это хороший способ проверить наш дизайн. При этом мы всё еще тестировали сайт и на реальных устройствах. В первую очередь потому, что сайт рассчитан на полноэкранный режим. На страницах с вертикальной прокруткой мы скрываем браузерный скролл в большинстве случаев (разве что в Safari на iOS7 с этим некоторые проблемы), но, нам нужно было уместить весь контент на страницу, независимо от этого. Мы использовали одну из предустановок в эмуляторе и изменяли размер экрана, для симуляции изменения доступного места на странице. Тестирование на реальных устройствах также очень важно для отслеживания затрат памяти в приложении и измерения его производительности в полевых условиях.

Управление состоянием

Сразу за посадочной страницей мы попадаем на карту Средиземья. Вы наверное обратили внимание на то, что адрес поменялся? Сайт — это одностраничное приложение, которое использует возможности History API для управления маршрутизацией.

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

Так как пользователь может нажать кнопку «Назад» в браузере, или передвигаться с помощью меню, то нам необходимо следить и за очищением старых элементов. Таймауты и анимация должны быть остановлены и отменены, иначе это может вызвать различные нежелательные последствия, ошибки и утечку памяти. Это не всегда является простой задачей, особенно накануне дедлайна, когда нужно, чтобы всё заработало, и желательно, как можно скорее.

Сохраняйте спокойствие и добавляйте слушателей событий. Хорошей практикой является добавление функции для очистки каждого объекта. Важно так же не забывать про таймеры и циклы анимации. При наличии подобных циклов в вашем проекте пользуйтесь эквивалентом TweenMax.killTweensOf(foo), или сохраняйте ссылки на них в переменные и останавливайте вызов колбэков. Удаляйте временно созданные элементы DOM. Крайне полезно часто обращаться к инструменту профилирования в Chrome DevTool для контроля над потреблением и утечкой памяти.

Локации

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

Примечание: Полоса прокрутки предполагает определённый тип поведения для пользователей. Если сайт неожиданно для пользователя перехватывает управление этим элементом, то это может оставить у него неприятное впечатление. Используйте этот трюк осторожно. Einar Öberg

Таймлайн для Thranduil’s Hall

Таймлайны

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

Модули и компоненты поведения

Мы создали отдельные модули для клипов из нескольких картинок, статичных изображений, сцен с параллакс-эффектом, сцен с изменением фокуса и текста. Сцены с параллакс-эффектом имеют непрозрачный фон и некоторое количество слоёв, позиционирование которых связано с продвижением пользователя по странице. Сцены с изменением фокуса — это, по сути, подвид предыдущего модуля с поправкой на то, что мы используем в нем две картинки для каждого слоя, которые плавно исчезают, симулируя изменение фокуса. Мы пытались использовать CSS-фильтр blur, но он всё ещё слишком требователен к ресурсам устройства, поэтому стоит подождать официального выхода CSS-шейдеров. Текст в текстовых модулях можно перетаскивать с помощью плагина Draggable от TweenMax. Кроме этого, можно использовать колесо мыши или свайп двумя пальцами, чтобы прокручивать текст по вертикали. Обратите также внимание на плагин throw-props, который добавляет физику «броска» при окончании свайпа. У модулей есть разные сценарии поведения, которые подключаются как наборы компонентов. Они привязываются к определённым селекторам и имеют собственные настройки. Для движения элементов используется translate, для масштабирования scale, hotspots для всплывающих подсказок, debug metrics для визуального тестирования, start-title overlay для отображения названия перед запуском, flare layer — слой для эффектов, и некоторые другие вещи. Всё это добавляется в структуру документа и управляется элементом-родителем внутри модуля. С таким подходом мы можем показывать разные истории с помощью всего лишь файла настроек, которые определяет, какие именно ресурсы загружать в данный момент и как настраивать разные типы модулей и компонентов.

Клипы из нескольких картинок

Самым сложным моментом с точки зрения производительности и объёма загружаемых данных оказалась разработка модуля для клипов из картинок. По этой теме уже немало написано. На мобильных и планшетах мы заменяем их статичным изображением. Но, это всё ещё слишком большой объём данных для обработки и хранения в памяти, если мы хотим добиться высокого качества на мобильных устройствах. Мы попробовали несколько альтернативных подходов. Сначала мы пробовали пользоваться фоновыми изображениями и спрайтами, но это привело к проблемам с памятью и задержкой в тот момент, когда процессору нужно было переключаться между разными спрайтами. Потом мы попробовали заменять сами элементы img, но это тоже оказалось очень медленным способом. Отрисовка кадра из спрайта с помощью canvas дало прирост скорости в этом модуле, и мы решили остановится на этом способе. Для экономии времени на расчёт каждого кадра изображение предварительно обрабатывается во временном элементе canvas и сохраняется с помощью putImageData() в специальный массив, уже готовое к дальнейшему использованию. После этого оригинальный спрайт может быть удалён из памяти, так как в ней мы храним только минимально необходимую информацию. Возможно, необработанные изображения занимали бы меньше памяти, но в нашем случае производительность была выше. Кадры клипа довольно маленькие, всего 640x400, но это можно заметить только в процессе их прокрутки. Когда анимация останавливается, последний кадр клипа сразу же заменяется картинкой с большего разрешением.

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Сохраняем изображения для каждого кадра в массив
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

Спрайты создаются с помощью Imagemagick. Здесь есть простой пример на GitHub, который показывает, как создать спрайт из картинок в папке.

Анимация модулей

Когда готовые модули размещены в таймлайне, в специальном скрытом блоке создаётся их условное представление, которое связано с позицией и размерами этих модулей. Мы сделали визуальное отображение для этого инструмента, чтобы легче проводить отладку. Однако, на работающем проекте он, скрыт и в нём обновляются только размеры и положение элементов. Из-за того, что некоторые модули заполняют экран полностью, а некоторые имеют свои фиксированные соотношения сторон, правильно размещать и масштабировать все элементы так, чтобы они оставались видимыми и не слишком обрезаны. Это тоже оказалось довольно непростой задачей. Каждый модуль имеет два индикатора прогресса — первый, для видимой на экране части, и второй, для измерения длительности самого модуля. При создании движения в параллакс-сцене часто бывает сложно рассчитать начальную и конечную позицию объектов для того, чтобы в последствии синхронизировать их с позицией, когда они появляются в зоне видимости. Гораздо проще работать, когда точно знаешь, что сейчас происходит с модулем — находится ли он на экране, на каком этапе сейчас его внутренняя анимация и когда он исчезнет.

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

Производительность

Прогресс от функционального прототипа к плавно работающей релизной версии означает переход от угадывания к уверенности в том, что происходит сейчас в браузере. Это как раз тот момент, когда инструмент DevTools в Chrome становится вашим лучшим другом.

Мы потратили довольно много времени на оптимизацию сайта. Использование аппаратного ускорения — один из самых важных приемов, который поможет обеспечить вам плавную анимацию и переходы. Кроме того, нужно по-настоящему охотиться за цветными колонками и красными прямоугольниками в DevTools. На эту тему существует достаточное количество материала, и вам следует прочитать его целиком. Устранение пропущенных кадров в анимации мгновенно приносит облегчение, но при их повторном появлении это всё так же неприятно. И они будут снова появляться. Это, по сути, непрерывный процесс, требующий нескольких итераций.

Примечание: Следите за панелью layers (только в Chrome Canary) и paint rectangles в DevTools. Например, если дочерние элементы нужно обновлять на каждом кадре, стоит проверить, не будет произойдет ли прирост в скорости при изменении порядка элементов, чтобы отрисовывать их на данном этапе как можно меньше.

Я люблю TweenMax от Greensock из-за простоты в работе с зацикливанием свойств, трансформацией и CSS. Визуализируйте структуру при добавлении новых слоёв. Имейте в виду, что существующие трансформации могут быть перезаписаны новыми. Свойство translateZ(0), которое приводит к принудительному использованию аппаратного ускорения для CSS, заменяется на двумерную матрицу, если нужно зациклить только двумерные (2D) значения. Поэтому, если для нужного вас слоя всё же необходимо использовать аппаратное ускорение, можно установить значение force3D:true в цикле для превращение двумерной матрицы в трёхмерную. Легко упустить эти мелочи в стилях, когда смешиваешь CSS и JavaScript при анимации.

Не используйте принудительное ускорение, если в нем нет необходимости. Память графического процессора может быстро закончиться, что может вызвать нежелательные последствия, если вы хотите применять ускорение для многих контейнеров, особенно это актуально для iOS, где память имеет более жёсткие ограничения. Загрузка менее «тяжёлых» ресурсов и их масштабирование с помощью CSS, а также отключение некоторых эффектов для мобильных привело к отличным улучшениям в работе нашего приложения.

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

Выход из радела с плохо работающей функцией очистки.

Намного лучше!

Для нахождения утечек мы использовали инструмент профилирования в DevTools и сохраняли отчёты. Гораздо легче, когда есть конкретные объекты, которые легко отследить, например, трёхмерная геометрия или определённая библиотека. В примере, приведённом выше, оказалось, что трёхмерная сцена и массив с данными всё ещё находились в памяти. Если всё же возникают сложности с поиском нужного объекта, из за которого падает приложение, мы рекомендуем вам прибегнуть к любопытной функции retaining paths. Достаточно нажать на интересующий элемент в отчёте профайлера и можно получить более детальную информацию о нём в панели снизу. Хорошая структура, построенная на небольших объектах, помогает при поиске этих связей.

Сцена была вызвана EffectComposer.

В целом, стоит подумать дважды прежде, чем манипулировать элементами в DOM. Если вы всё-таки делаете это, не забывайте об эффективности этих манипуляций. По возможности, не двигайте элементы во время игрового цикла. Если нужно найти некоторый элемент — используйте самый короткий путь, сохраняя ссылки к главным контейнерам и ищите внутри ближайшего родительского блока.

Добавляйте задержку при считывании размеров добавленных или обновленных элементов и классов, если вы ожидаете, что их появление может вызвать баги в интерфейсе. Также убедитесь, что ваша разметка инициализирована. Иногда браузер собирает изменения для стилей в «пакет» и не применяет их до следующей инициализации. Это может быть серьёзной проблемой для вас, но это сделано неспроста, поэтому стоит понять природу такого поведения и научиться работать с ним.

Полноэкранный режим

Некоторые модули позволяют перевести сайт в полноэкранный режим с помощью Fullscreen API. Но в браузерах на мобильных устройствах существует ещё один порог ограничений для этой возможности. Safari на iOS раньше позволял управлять переходом в полноэкранный режим, но сейчас это недоступно, поэтому стоит подготовить свой дизайн для работы в обычном режиме для страниц без прокрутки. Мы полагаем, что стоит ожидать изменений в будущих обновлениях, так как запрет на использование этой технологии нарушил корректную работу многих веб-приложений.

Ресурсы

Анимированные инструкции для экспериментов.

На сайте используются разные типы ресурсов: изображения (PNG и JPEG), SVG (в контенте и в фоне), спрайты (PNG), нестандартные иконочные шрифты и анимации из Adobe Edge. Мы используем PNG для основных ресурсов и анимации (в виде спрайтов), когда элемент не может быть реализован векторно, но для всех других случаев мы стараемся использовать SVG.

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

Иконочные шрифты обладают теми же преимуществами, что и SVG, когда речь идёт о масштабировании и используется вместо SVG для небольших элементов, для которых нужно изменять только цвет, например, состояние кнопок — активна, ховер и т.д. Иконки также очень удобны для повторного использования, достаточно изменить CSS свойство content.

Анимация

В некоторых случаях анимация SVG-элементов с помощью скриптов может быть довольно трудозатратной, особенно, если анимация меняется во время правок дизайна. Для улучшения взаимодействия дизайнеров и разработчиков мы иногда используем пакет Adobe Edge для разработки некоторых типов анимации, например, в анимации инструкций перед играми. Процесс работы над анимацией очень похож на работу с Flash и это помогло нашей команде, но всё ещё есть недочёты, что связано с внедрением Edge-анимаций в загрузку наших ресурсов, так как Adobe использует собственные загрузчики и логику внедрения.

Я думаю, что нам предстоит ещё долгий путь по оттачиванию процесса работы с ресурсами и анимацией, сделанной «вручную». Мы с интересом наблюдаем за развитием инструментов вроде Edge. И с удовольствием выслушаем ваши мысли на эту тему в комментариях.

Выводы

Сейчас, когда весь проект запущен, и я смотрю на наш конечный результат, я могу с уверенностью сказать, что впечатлён уровнем современных мобильных браузеров. В начале работы над этим сайтом наши ожидания насчёт того, насколько плавно и красиво всё будет работать, были намного ниже. Это был отличный опыт для всех нас, и всё время, потраченное на разработку и тестирование (особенно на него) улучшили наше понимание того, как работают современные браузеры. И, конечно, это сократит время на разработку подобных проектов в будущем, в частности, столь долгий путь от угадывания к уверенности.