Практический пример — преобразование Wordico из Flash в HTML5

Введение

Когда мы конвертировали нашу кроссворд Wordico из Flash в HTML5, нашей первой задачей было забыть все, что мы знали о создании удобного пользовательского интерфейса в браузере. В то время как Flash предлагал единый комплексный API для всех аспектов разработки приложений — от векторного рисования до обнаружения попаданий в полигоны и анализа XML, — HTML5 предлагал множество спецификаций с различной поддержкой браузеров. Мы также задались вопросом, подходят ли HTML, язык, ориентированный на работу с документами, и CSS, ориентированный на блоки язык, для создания игры. Будет ли игра одинаково отображаться во всех браузерах, как во Flash, и будет ли она выглядеть и вести себя так же хорошо? Для Wordico ответ был утвердительным.

Какой у тебя вектор, Виктор?

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

Wordico Каркас
Во Flash каждый объект отображения состоял из векторных фигур.

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

Трехбуквенный пробел во Flash.
Трехбуквенный пробел во Flash.

Однако в HTML5 мы используем растровый спрайт:

Спрайт PNG, показывающий все девять пробелов.
Спрайт PNG, показывающий все девять пробелов.

Чтобы создать игровое поле 15x15 из отдельных пробелов, мы перебираем строковую нотацию длиной 225 символов, в которой каждый пробел представлен отдельным символом (например, «t» для тройной буквы и «T» для тройного слова). Во Flash это была простая операция; мы просто вырезали пробелы и располагали их в сетке:

var spaces:Array = new Array();

for (var i:int = 0; i < 225; i++) {
  var space:Space = new Space(i, layout.charAt(i));
  ...
  spaces.push(addChild(space));
}

LayoutUtil.grid(spaces, 15);

В HTML5 все немного сложнее. Мы используем элемент <canvas> , поверхность рисования растрового изображения, для рисования игрового поля по одному квадрату за раз. Первый шаг — загрузить спрайт изображения. После загрузки мы перебираем нотацию макета, рисуя на каждой итерации новую часть изображения:

var x = 0;  // x coordinate
var y = 0;  // y coordinate
var w = 35; // width and height of a space

for (var i = 0; i < 225; i++) {
  if (i && i % 15 == 0) {
    x = 0;
    y += w;
  }

  var imageX = "_dDFtTqQxm".indexOf(layout.charAt(i)) * 70;

  canvas.drawImage("spaces.png", imageX, 0, 70, 70, x, y, w, w);

  x += w;
}

Вот результат в веб-браузере. Обратите внимание, что сам холст имеет тень CSS:

В HTML5 игровая доска представляет собой один элемент холста.
В HTML5 игровая доска представляет собой один элемент холста.

Преобразование объекта-плитки было аналогичным упражнением. Во Flash мы использовали текстовые поля и векторные фигуры:

Плитка Flash представляла собой комбинацию текстовых полей и векторных фигур.
Плитка Flash представляла собой комбинацию текстовых полей и векторных фигур.

В HTML5 мы объединяем три спрайта изображения в одном элементе <canvas> во время выполнения:

Плитка HTML представляет собой композицию трех изображений.
Плитка HTML представляет собой композицию трех изображений.

Теперь у нас есть 100 холстов (по одному на каждую плитку) плюс холст для игрового поля. Вот разметка для плитки «H»:

<canvas width="35" height="35" class="tile tile-racked" title="H-2"/>

Вот соответствующий CSS:

.tile {
  width: 35px;
  height: 35px;
  position: absolute;
  cursor: pointer;
  z-index: 1000;
}

.tile-drag {
  -moz-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -webkit-box-shadow: 1px 1px 7px rgba(0,0,0,0.8);
  -moz-transform: scale(1.10);
  -webkit-transform: scale(1.10);
  -webkit-box-reflect: 0px;
  opacity: 0.85;
}

.tile-locked {
  cursor: default;
}

.tile-racked {
  -webkit-box-reflect: below 0px -webkit-gradient(linear, 0% 0%, 0% 100%,  
    from(transparent), color-stop(0.70, transparent), to(white));
}

Мы применяем эффекты CSS3 при перетаскивании плитки (тень, непрозрачность и масштабирование) и когда плитка находится на стойке (отражение):

Перетаскиваемая плитка немного больше, слегка прозрачна и имеет тень.
Перетаскиваемая плитка немного больше, слегка прозрачна и имеет тень.

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

Обратная сторона? Используя изображения, мы отказываемся от программного доступа к текстовым полям. Во Flash это была простая операция по изменению цвета или других свойств шрифта; в HTML5 эти свойства встроены в сами изображения. (Мы попробовали текст HTML, но для этого потребовалось много дополнительной разметки и CSS. Мы также попробовали текст холста, но результаты были непостоянными в разных браузерах.)

Нечеткая логика

Мы хотели в полной мере использовать окно браузера любого размера и избежать прокрутки. Во Flash это была относительно простая операция, поскольку вся игра была нарисована в векторах и ее можно было увеличивать или уменьшать без потери точности. Но в HTML все было сложнее. Мы попробовали использовать масштабирование CSS, но в итоге получили размытый холст:

Масштабирование CSS (слева) и перерисовка (справа).
Масштабирование CSS (слева) и перерисовка (справа).

Наше решение — перерисовывать игровое поле, стойку и плитки всякий раз, когда пользователь меняет размер браузера:

window.onresize = function (evt) {
...
gameboard.setConstraints(boardWidth, boardWidth);

...
rack.setConstraints(rackWidth, rackHeight);

...
tileManager.resizeTiles(tileSize);
});

В итоге мы получаем четкие изображения и приятный макет на экране любого размера:

Игровое поле заполняет вертикальное пространство; другие элементы страницы обтекают его.
Игровое поле заполняет вертикальное пространство; другие элементы страницы обтекают его.

Перейдем к делу

Поскольку каждая плитка абсолютно позиционирована и должна точно совпадать с игровой доской и стойкой, нам нужна надежная система позиционирования. Мы используем две функции, Bounds и Point , чтобы управлять расположением элементов в глобальном пространстве (странице HTML). Bounds описывает прямоугольную область на странице, а Point описывает координату x,y относительно верхнего левого угла страницы (0,0), также известную как точка регистрации.

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

// bounds.js
function Bounds(element) {
var x = element.offsetLeft;
var y = element.offsetTop;
var w = element.offsetWidth;
var h = element.offsetHeight;

this.left = x;
this.right = x + w;
this.top = y;
this.bottom = y + h;
this.width = w;
this.height = h;
this.x = x;
this.y = y;
this.midx = x + (w / 2);
this.midy = y + (h / 2);
this.topleft = new Point(x, y);
this.topright = new Point(x + w, y);
this.bottomleft = new Point(x, y + h);
this.bottomright = new Point(x + w, y + h);
this.middle = new Point(x + (w / 2), y + (h / 2));
}

Bounds.prototype.contains = function (point) {
return point.x > this.left &amp;&amp;
point.x < this.right &amp;&amp;
point.y > this.top &amp;&amp;
point.y < this.bottom;
}

Bounds.prototype.intersects = function (bounds) {
return this.contains(bounds.topleft) ||
this.contains(bounds.topright) ||
this.contains(bounds.bottomleft) ||
this.contains(bounds.bottomright) ||
bounds.contains(this.topleft) ||
bounds.contains(this.topright) ||
bounds.contains(this.bottomleft) ||
bounds.contains(this.bottomright);
}

Bounds.prototype.toString = function () {
return [this.x, this.y, this.width, this.height].join(",");
}

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

// point.js

function Point(x, y) {
this.x = x;
this.y = y;
}

Point.prototype.distance = function (point) {
var a = point.x - this.x;
var b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}

Point.prototype.distanceX = function (point) {
return Math.abs(this.x - point.x);
}

Point.prototype.distanceY = function (point) {
return Math.abs(this.y - point.y);
}

Point.prototype.interpolate = function (point, pct) {
var x = this.x + ((point.x - this.x) * pct);
var y = this.y + ((point.y - this.y) * pct);

return new Point(x, y);
}

Point.prototype.offset = function (x, y) {
return new Point(this.x + x, this.y + y);
}

Point.prototype.vector = function (point) {
return new Point(point.x - this.x, point.y - this.y);
}

Point.prototype.toString = function () {
return this.x + "," + this.y;
}

// static
Point.fromElement = function (element) {
return new Point(element.offsetLeft, element.offsetTop);
}

// static
Point.fromEvent = function (evt) {
return new Point(evt.x || evt.clientX, evt.y || evt.clientY);
}

Эти функции составляют основу возможностей перетаскивания и анимации. Например, мы используем Bounds.intersects() , чтобы определить, перекрывает ли плитка пространство на игровом поле; мы используем Point.vector() для определения направления перетаскиваемой плитки; и мы используем Point.interpolate() в сочетании с таймером для создания анимации движения или эффекта замедления.

Плывите по течению

Хотя макеты фиксированного размера легче создавать во Flash, гибкие макеты гораздо проще создавать с помощью HTML и блочной модели CSS. Рассмотрим следующий вид сетки с переменной шириной и высотой:

Этот макет не имеет фиксированных размеров: миниатюры располагаются слева направо и сверху вниз.
Этот макет не имеет фиксированных размеров: миниатюры располагаются слева направо и сверху вниз.

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

Панель чата во Flash была красивой, но сложной.
Панель чата во Flash была красивой, но сложной.

Для сравнения, HTML-версия представляет собой просто <div> с фиксированной высотой и скрытым свойством переполнения. Прокрутка нам ничего не стоит.

Блочная модель CSS в действии.
Блочная модель CSS в действии.

В подобных случаях (обычные задачи верстки) HTML и CSS затмевают Flash.

Сейчас ты меня слышишь?

У нас были проблемы с тегом <audio> — он просто не мог повторно воспроизводить короткие звуковые эффекты в некоторых браузерах. Мы попробовали два обходных пути. Во-первых, мы добавили в звуковые файлы мертвый воздух, чтобы сделать их длиннее. Затем мы попробовали попеременное воспроизведение по нескольким аудиоканалам. Ни одна из техник не была полностью эффективной или элегантной.

В конечном итоге мы решили создать собственный Flash-аудиоплеер и использовать HTML5-аудио в качестве запасного варианта. Вот основной код во Flash:

var sounds = new Array();

function playSound(path:String):void {
var sound:Sound = sounds[path];

if (sound == null) {
sound = new Sound();
sound.addEventListener(Event.COMPLETE, function (evt:Event) {
    sound.play();
});
sound.load(new URLRequest(path));
sounds[path] = sound;
}
else {
sound.play();
}
}

ExternalInterface.addCallback("playSound", playSound);

В JavaScript мы пытаемся обнаружить встроенный Flash-плеер. Если это не помогло, мы создаем узел <audio> для каждого звукового файла:

function play(String soundId) {
var src = "/audio/" + soundId + ".mp3";

// Flash
try {
var swf = window["swfplayer"] || document["swfplayer"];
swf.playSound(src);
}
// or HTML5 audio
catch (e) {
var sound = document.getElementById(soundId);
if (sound == null || sound == undefined) {
    var sound = document.createElement("audio");
    sound.id = soundId;
    sound.src = src;
    document.body.appendChild(sound);
}
sound.play();
}
}

Обратите внимание, что это работает только для файлов MP3 — мы никогда не удосужились поддерживать OGG. Мы надеемся, что в ближайшем будущем индустрия остановится на едином формате.

Позиция в опросе

В HTML5 мы используем тот же метод, что и во Flash, для обновления состояния игры: каждые 10 секунд клиент запрашивает обновления у сервера. Если состояние игры изменилось с момента последнего опроса, клиент получает и обрабатывает изменения; в противном случае ничего не происходит. Эта традиционная методика опроса приемлема, хотя и не совсем элегантна. Однако мы хотели бы переключиться на длинный опрос или WebSockets по мере того, как игра становится более зрелой и пользователи начинают ожидать взаимодействия по сети в реальном времени. В частности, WebSockets предоставит множество возможностей для улучшения игрового процесса.

Какой инструмент!

Мы использовали Google Web Toolkit (GWT) для разработки как внешнего пользовательского интерфейса, так и внутренней логики управления (аутентификация, проверка, сохранение и т. д.). Сам JavaScript компилируется из исходного кода Java. Например, функция Point адаптирована из Point.java :

package com.wordico.client.view.layout;

import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.DomEvent;

public class Point {
public double x;
public double y;

public Point(double x, double y) {
this.x = x;
this.y = y;
}

public double distance(Point point) {
double a = point.x - this.x;
double b = point.y - this.y;

return Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
}
...
}

Некоторые классы пользовательского интерфейса имеют соответствующие файлы шаблонов, в которых элементы страницы «привязаны» к членам класса. Например, ChatPanel.ui.xml соответствует ChatPanel.java :

<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">

<ui:UiBinder
xmlns:ui="urn:ui:com.google.gwt.uibinder"
xmlns:g="urn:import:com.google.gwt.user.client.ui"
xmlns:w="urn:import:com.wordico.client.view.widget">

<g:HTMLPanel>
<div class="palette">
<g:ScrollPanel ui:field="messagesScroll">
    <g:FlowPanel ui:field="messagesFlow"></g:FlowPanel>
</g:ScrollPanel>
<g:TextBox ui:field="chatInput"></g:TextBox>
</div>
</g:HTMLPanel>

</ui:UiBinder>

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

Зачем использовать Java? Во-первых, для строгой типизации. Хотя динамическая типизация полезна в JavaScript (например, способность массива хранить значения разных типов), она может стать головной болью в больших и сложных проектах. Во-вторых, для возможностей рефакторинга. Подумайте, как бы вы изменили сигнатуру метода JavaScript в тысячах строк кода — это непросто! Но с хорошей Java IDE это совсем несложно. Наконец, в целях тестирования. Написание модульных тестов для классов Java превосходит проверенный временем метод «сохранения и обновления».

Краткое содержание

Если не считать проблем со звуком, HTML5 значительно превзошел наши ожидания. Wordico не только выглядит так же хорошо, как и во Flash, но и столь же плавный и отзывчивый. Мы бы не справились без Canvas и CSS3. Наша следующая задача: адаптация Wordico для мобильного использования.