Пример использования: создание дудла Станислава Лема для Google

Привет, (странный) мир

Домашняя страница Google — это увлекательная среда для написания кода. Он имеет множество жестких ограничений: особое внимание к скорости и задержке, необходимость поддержки всех типов браузеров и работы в различных обстоятельствах и… да, удивление и восторг.

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

Каждый интерактивный рисунок, который я написал ( Pac-Man , Жюль Верн , Всемирная выставка ) – и многие из них, с которыми я помогал – были в равной степени футуристическими и анахроничными: большие возможности для масштабных приложений передовых веб-функций… и суровый прагматизм кроссбраузерной совместимости.

Мы многому учимся из каждого интерактивного дудла, и недавняя мини-игра Станислава Лема не стала исключением: в ее 17 000 строках кода JavaScript впервые в истории дудлов пробуется множество вещей. Сегодня я хочу поделиться с вами этим кодом — возможно, вы найдете там что-то интересное или укажете на мои ошибки — и немного о нем поговорим.

Посмотреть код каракулей Станислава Лема »

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

Итак, давайте пройдемся по некоторым современным веб-технологиям, которые нашли свое место (а некоторые нет) в дудле Станислава Лема.

Графика через DOM и холст

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

Я собрал графический движок, который абстрагирует графические примитивы, называемые «прямоугольниками», а затем отображает их с использованием либо холста, либо DOM, если холст недоступен.

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

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

Я уже абстрагировал z-индекс с помощью понятия «плоскости». Рисунок определял ряд плоскостей – от неба далеко позади до указателя мыши впереди всего – и каждый актер в рисунке должен был решить, к какому из них он принадлежит (небольшие поправки плюс/минус внутри плоскости были возможны с помощью planeCorrection ).

При рендеринге через DOM плоскости просто переводятся в z-index. Но если мы рендерим через холст, нам нужно отсортировать прямоугольники по их плоскостям, прежде чем рисовать их. Поскольку каждый раз делать это затратно, порядок пересчитывается только при добавлении актера или при его переходе в другую плоскость.

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

Одной из вещей, которую мы хотели попробовать с помощью этого рисунка, было разрушение четвертой стены. Вышеупомянутый движок позволил нам объединить актеров на основе холста с актерами на основе DOM. Например, взрывы в финале присутствуют как на холсте для объектов вселенной, так и в DOM для остальной части главной страницы Google. Птица, обычно летающая вокруг и подрезанная нашей зазубренной маской, как и любой другой актер, решает держаться подальше от неприятностей во время уровня стрельбы и садится на кнопку «Мне повезет». Это делается так: птица покидает холст и становится элементом DOM (и наоборот позже), что, как я надеялся, будет полностью прозрачным для наших посетителей.

Частота кадров

Знание текущей частоты кадров и реагирование на то, что она слишком медленная (или слишком быстрая!), было важной частью нашего движка. Поскольку браузеры не сообщают частоту кадров, нам приходится рассчитывать ее самостоятельно.

Я начал с использования requestAnimationFrame , возвращаясь к старомодному setTimeout , если первый был недоступен. requestAnimationFrame в некоторых ситуациях ловко экономит ресурсы ЦП — хотя некоторые из этих действий мы делаем сами, как будет объяснено ниже — но также просто позволяет нам получить более высокую частоту кадров, чем setTimeout .

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

Какие решения?

  • Если частота кадров превышает 60 кадров в секунду, мы ее ограничиваем. В настоящее время requestAnimationFrame в некоторых версиях Firefox не имеет верхнего предела частоты кадров, и нет смысла тратить ресурсы ЦП. Обратите внимание, что на самом деле мы ограничиваемся 65 кадрами в секунду из-за ошибок округления, из-за которых частота кадров в других браузерах немного превышает 60 кадров в секунду — мы не хотим по ошибке начинать ее регулировать.

  • Если частота кадров ниже 10 кадров в секунду, мы просто замедляем движок вместо пропуска кадров. Это проигрышное предложение, но я чувствовал, что чрезмерный пропуск кадров будет более запутанным, чем просто более медленная (но все же связная) игра. Есть еще один приятный побочный эффект: если система временно замедлится, пользователь не испытает странного скачка вперед, поскольку движок отчаянно догоняет его. (Я сделал это немного по-другому для Pac-Man, но минимальная частота кадров — лучший подход.)

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

У нас также есть понятие физического тика и логического тика. Первый происходит из requestAnimationFrame / setTimeout . Соотношение в обычном игровом процессе 1:1, но для быстрой перемотки мы просто добавляем больше логических тиков на физический такт (до 1:5). Это позволяет нам выполнять все необходимые вычисления для каждого логического тика, но назначать только последний из них, который будет обновлять информацию на экране.

Бенчмаркинг

Можно предположить (и на ранних этапах это действительно было), что Canvas будет работать быстрее, чем DOM, когда бы он ни был доступен. Это не всегда так. В ходе тестирования мы обнаружили, что Opera 10.0–10.1 на Mac и Firefox на Linux на самом деле быстрее перемещают элементы DOM.

В идеальном мире дудл мог бы незаметно сравнивать различные графические методы — элементы DOM перемещались с помощью style.left и style.top , рисовались на холсте и, возможно, даже элементы DOM перемещались с помощью преобразований CSS3.

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

В конце концов, веб-разработка иногда сводится к необходимости делать то, что нужно. Я оглянулся через плечо, чтобы убедиться, что никто не смотрит, а затем просто жестко запрограммировал Opera 10 и Firefox из холста. В следующей жизни я вернусь как тег <marquee> .

Экономия процессора

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

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

Когда?

  • через 18 секунд на главной странице (в аркадных играх это называется режимом привлечения )
  • через 180 секунд, если вкладка находится в фокусе
  • через 30 секунд, если вкладка не имеет фокуса (например, пользователь переключился на другое окно, но, возможно, все еще смотрит рисунок на неактивной вкладке)
  • немедленно, если вкладка становится невидимой (например, пользователь переключился на другую вкладку в том же окне — нет смысла тратить циклы, если нас не видно)

Как мы узнаем, что вкладка в данный момент находится в фокусе? Мы прикрепляемся к window.focus и window.blur Как мы узнаем, что вкладка видна? Мы используем новый API видимости страниц и реагируем на соответствующее событие.

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

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

Переходы, трансформации, события

Одной из сильных сторон HTML всегда было то, что вы можете сделать его лучше самостоятельно: если что-то в обычном наборе HTML и CSS не достаточно хорошо, вы можете уговорить JavaScript расширить его. К сожалению, зачастую это означает необходимость начинать с нуля. Переходы CSS3 великолепны, но вы не можете добавить новый тип перехода или использовать переходы для чего-либо, кроме стилизации элементов. Другой пример: преобразования CSS3 отлично подходят для DOM, но когда вы переходите на холст, вы внезапно оказываетесь сами по себе.

Эти и многие другие проблемы являются причиной того, что у Lem doodle есть собственный движок переходов и преобразований. Да, я знаю, звонили 2000-е и т. д. — возможности, которые я встроил, далеко не такие мощные, как CSS3, но что бы ни делал движок, он делает это стабильно и дает нам гораздо больше контроля.

Я начал с простой системы действий (событий) — временной шкалы, которая запускает события в будущем без использования setTimeout , поскольку в любой момент время рисования может отделиться от физического времени, поскольку оно становится быстрее (перемотка вперед), медленнее (низкая частота кадров). или засыпает для экономии процессора) или вообще останавливается (ожидая завершения загрузки изображений).

Переходы — это просто еще один тип действий. В дополнение к базовым движениям и вращению мы также поддерживаем относительные движения (например, перемещение чего-либо на 10 пикселей вправо), пользовательские вещи, такие как дрожание, а также анимацию изображений по ключевым кадрам.

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

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

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

Работа с изображениями и спрайтами

Движок предназначен не только для запуска рисунков, но и для работы над ними. Выше я поделился некоторыми параметрами отладки: остальные вы можете найти в engine.readDebugParams .

Спрайтинг — хорошо известная техника, которую мы тоже используем для рисования. Это позволяет нам экономить байты и сокращать время загрузки, а также упрощает предварительную загрузку. Однако это также усложняет разработку: каждое изменение изображений потребует повторного спрайта (в значительной степени автоматизированного, но все же громоздкого). Таким образом, движок поддерживает работу с необработанными изображениями для разработки, а также со спрайтами для производства через engine.useSprites — оба включены в исходный код.

Пакман каракули
Спрайты, используемые в дудле Pac-Man .

Мы также поддерживаем предварительную загрузку изображений по ходу работы и остановку рисования, если изображения не загрузились вовремя — в комплекте с искусственным индикатором выполнения! (Фальшь, потому что, к сожалению, даже HTML5 не может сказать нам, какая часть файла изображения уже загружена.)

Снимок экрана загрузки графики с настроенным индикатором выполнения.
Снимок экрана загрузки графики с настроенным индикатором выполнения.

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

Какое место во всем этом занимает HTML5? Выше об этом не так уж и много, но инструмент, который я написал для спрайтов/обрезки, был совершенно новой веб-технологией: Canvas, Blobs , a[download] . Одна из замечательных особенностей HTML заключается в том, что он постепенно включает в себя то, что раньше приходилось делать вне браузера; единственное, что нам нужно было сделать, это оптимизировать PNG-файлы.

Сохранение состояния между играми

Миры Лема всегда казались большими, живыми и реалистичными. Его рассказы обычно начинались без особых объяснений: первая страница начиналась в medias res, и читателю приходилось сориентироваться.

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

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

К счастью, HTML5 предоставляет нам веб-хранилище , простое в использовании, позволяющее нам сохранять и вызывать общее количество воспроизведений и последнюю сцену, воспроизведенную пользователем – с гораздо большей грацией, чем когда-либо позволяли бы файлы cookie.

Что нам делать с этой информацией?

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

Существует ряд параметров отладки, контролирующих это:

  • ?doodle-debug&doodle-first-run – притвориться, что это первый запуск
  • ?doodle-debug&doodle-second-run – представьте, что это второй запуск
  • ?doodle-debug&doodle-old-run – притворитесь, что это старый запуск

Сенсорные устройства

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

Необходимо было внести некоторые предварительные изменения в пользовательский интерфейс. Первоначально указатель мыши был единственным местом, которое сообщало о том, что происходит кат-сцена/неинтерактивная часть. Позже мы добавили небольшой индикатор в правом нижнем углу, чтобы нам не приходилось полагаться только на указатель мыши (учитывая, что его нет на сенсорных устройствах).

Нормальный Занятый Кликабельно Щелкнул
Работа в процессе
Обычный указатель незавершенной работы
Указатель занятости незавершенной работы
Кликабельный указатель незавершенной работы
Нажат указатель незавершенной работы
Финал
Конечный нормальный указатель в
Последний указатель занятости
Последний кликабельный указатель
Последний щелчок указателя
Указатели мыши во время разработки и окончательные эквиваленты.

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

В этом мне очень помогли отдельные кликабельные прозрачные элементы DOM, поскольку я мог изменять их размер независимо от визуальных элементов. Я ввел дополнительные 15-пиксельные отступы для сенсорных устройств и использовал их всякий раз, когда создавались кликабельные элементы. (Я также добавил 5-пиксельные отступы для мыши, просто чтобы порадовать мистера Фиттса.)

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

Мы также используем более современные свойства стиля, чтобы удалить некоторые сенсорные функции, которые браузеры WebKit добавляют по умолчанию (нажмите выделение, нажмите выноску).

И как нам определить, поддерживает ли данное устройство, на котором запущен дудл, сенсорный ввод? Лениво. Вместо того, чтобы выяснить это априори, мы просто использовали наши объединенные IQ, чтобы сделать вывод о том, что устройство поддерживает сенсорное управление… после того, как мы получили первое событие запуска касания.

Настройка указателя мыши

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

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

Если не это, то что? Хорошо, почему бы не сделать указатель мыши просто еще одним актером в каракулях? Это работает, но имеет ряд оговорок, в основном:

  • вам нужно иметь возможность удалить собственный указатель мыши
  • вам нужно уметь синхронизировать указатель мыши с «настоящим»

Первое сложно. CSS3 позволяет использовать cursor: none , но он также не поддерживается в некоторых браузерах. Нам пришлось прибегнуть к некоторой гимнастике: использовать пустой файл .cur в качестве запасного варианта, указать конкретное поведение для некоторых браузеров и даже жестко запрограммировать другие, чтобы вообще исключить этот опыт.

Другой на первый взгляд относительно тривиален, но поскольку указатель мыши является всего лишь еще одной частью вселенной каракулей, он тоже унаследует все его проблемы. Самый большой? Если частота кадров каракулей низкая, частота кадров указателя мыши также будет низкой – и это имеет ужасные последствия, поскольку указатель мыши, являясь естественным продолжением вашей руки, должен реагировать, несмотря ни на что. (Люди, которые раньше использовали Commodore Amiga, теперь энергично кивают.)

Одним из довольно сложных решений этой проблемы является отделение указателя мыши от обычного цикла обновления. Мы так и сделали – в альтернативной вселенной, где мне не нужно спать. Более простое решение для этого? Просто возвращаемся к исходному указателю мыши, если частота кадров падает ниже 20 кадров в секунду. (Здесь пригодится изменяющаяся частота кадров. Если бы мы отреагировали на текущую частоту кадров и если бы она колебалась около 20 кадров в секунду, пользователь увидел бы, что пользовательский указатель мыши постоянно скрывается и отображается.) Это подводит нас к :

Диапазон частоты кадров Поведение
>10 кадров в секунду Замедлите игру, чтобы не пропадало больше кадров.
10–20 кадров в секунду Используйте собственный указатель мыши вместо пользовательского.
20–60 кадров в секунду Нормальная операция.
>60 кадров в секунду Уменьшите скорость, чтобы частота кадров не превышала это значение.
Краткое изложение поведения, зависящего от частоты кадров.

Да, и указатель мыши на Mac темный, а на ПК — белый. Почему? Потому что войнам платформ нужно топливо даже в вымышленных вселенных.

Заключение

Это не идеальный двигатель, но он и не пытается им быть. Он был разработан вместе с дудлом Лема и очень специфичен для него. Это нормально. «Преждевременная оптимизация — корень всех зол», как знаменито сказал Дон Кнут, и я не верю, что сначала нужно написать движок изолированно, а затем применять его позже, имеет смысл — практика информирует теорию так же, как теория информирует практику. В моем случае код был выброшен, некоторые части переписаны снова и снова, и многие общие части были замечены постфактум, а не анте фактум. Но, в конце концов, то, что мы здесь имеем, позволило нам сделать то, что мы хотели – прославить карьеру Станислава Лема и рисунки Даниэля Мроза наилучшим образом, который мы только могли придумать.

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

Я сделал это сам — это ниже было в прямом эфире в последние дни, отсчитывая время до раннего утра 23 ноября 2011 года в России, которая была первым часовым поясом, где увидел каракули Лема. Возможно, это глупая вещь, но, как и в случае с рисунками, вещи, которые кажутся незначительными, иногда имеют более глубокий смысл — этот счетчик действительно был хорошим «стресс-тестом» для движка.

Скриншот Лема, рисующего часы обратного отсчета во вселенной.
Скриншот Лема, рисующего часы обратного отсчета во вселенной.

И это один из способов взглянуть на жизнь дудла Google – месяцы работы, недели тестирования, 48 часов его запекания, и все это ради того, во что люди играют по пять минут. Каждая из этих тысяч строк JavaScript надеется, что эти 5 минут будут потрачены с пользой. Наслаждаться.