Étude de cas : Conversion de Wordico de Flash à HTML5

Adrian Gould
Adrian Gould

Introduction

Lorsque nous avons fait passer notre jeu de mots croisés Wordico de Flash au format HTML5, notre première mission consistait à éradiquer tout ce que nous savions sur la création d'une expérience utilisateur enrichie dans le navigateur. Si Flash proposait une API unique et complète pour tous les aspects du développement d'applications, du dessin vectoriel à la détection des correspondances de polygones en passant par l'analyse XML, HTML5 proposait un tas de spécifications avec des navigateurs compatibles. Nous nous sommes également demandé si le HTML, un langage propre à un document, et le CSS, un langage centré sur les zones, se prêtaient à la création d'un jeu. Le jeu s'afficherait-il de manière uniforme dans les différents navigateurs, comme dans Flash, et aurait-il l'air et le comportement ? Pour Wordico, la réponse était oui.

Quel est votre vecteur, Victor ?

Nous avons développé la version originale de Wordico en utilisant uniquement des graphiques vectoriels: lignes, courbes, remplissages et dégradés. Résultat : un format compact et une évolutivité illimitée :

Maquette fonctionnelle Wordico
Dans Flash, chaque objet d'affichage était composé de formes vectorielles.

Nous avons également utilisé la timeline Flash pour créer des objets ayant plusieurs états. Par exemple, nous avons utilisé neuf images clés nommées pour l'objet Space:

Espace à trois lettres dans Flash.
Espace à trois lettres dans Flash

En HTML5, nous utilisons un sprite bitmap:

Lutin PNG montrant les neuf espaces.
Lutte PNG montrant les neuf espaces.

Pour créer le jeu 15 x 15 à partir d'espaces individuels, nous itérons une chaîne de 225 caractères dans laquelle chaque espace est représenté par un caractère différent (comme "t" pour un triple mot et "T" pour un triple mot). Cette opération dans Flash a été simple : il nous a simplement fallu estimer les espaces et les organiser sous forme de grille :

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);

En HTML5, c'est un peu plus compliqué. Nous utilisons l'élément <canvas>, une surface de dessin bitmap, pour peindre la carte de jeu un carré à la fois. La première étape consiste à charger le lutin d'image. Une fois le chargement terminé, nous parcourons la notation de mise en page, en dessinant une partie différente de l'image à chaque itération:

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;
}

Voici le résultat dans le navigateur Web. Notez que le canevas lui-même présente une ombre projetée CSS:

En HTML5, le jeu est un élément de canevas unique.
En HTML5, une partie de jeu est un élément de canevas unique.

La conversion de l'objet Tile représentait un exercice similaire. Dans Flash, nous avons utilisé des champs de texte et des formes vectorielles:

La vignette Flash était une combinaison de champs de texte et de formes vectorielles.
La carte Flash était une combinaison de champs de texte et de formes vectorielles.

En HTML5, nous combinons trois sprites d'image dans un seul élément <canvas> lors de l'exécution:

La vignette HTML est un composite de trois images.
La carte HTML est un composite de trois images.

Nous avons maintenant 100 toiles (une pour chaque carte) ainsi qu'une toile pour le plateau de jeu. Voici le balisage d'une vignette « H » :

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

Voici le CSS correspondant:

.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));
}

Nous appliquons des effets CSS3 lorsque la tuile est déplacée (ombre, opacité et mise à l'échelle) et lorsqu'elle est placée sur le rack (réflexion):

La vignette déplacée est légèrement plus grande, légèrement transparente et présente une ombre projetée.
La tuile déplacée est légèrement plus grande, légèrement transparente et présente une ombre projetée.

L'utilisation d'images matricielles présente certains avantages évidents. Tout d'abord, le résultat est précis, au pixel près. Deuxièmement, ces images peuvent être mises en cache par le navigateur. Troisièmement, avec un peu de travail supplémentaire, nous pouvons permuter les images pour créer de nouvelles tuiles, telles qu'une tuile en métal. Ce travail de conception peut être effectué dans Photoshop plutôt que dans Flash.

L'inconvénient ? En utilisant des images, nous renonçons à l'accès programmatique aux champs de texte. Dans Flash, la modification de la couleur ou d'autres propriétés du type était une opération simple. Dans HTML5, ces propriétés sont intégrées aux images elles-mêmes. (Nous avons essayé le texte HTML, mais cela nécessitait beaucoup de balisage et de code CSS supplémentaires. Nous avons également essayé le texte canevas, mais les résultats étaient incohérents d'un navigateur à l'autre.)

Logique floue

Nous voulions exploiter au mieux la fenêtre du navigateur, quelle que soit sa taille, et éviter de devoir faire défiler l'écran. Cette opération dans Flash était relativement simple, car l'intégralité du jeu était dessinée sous forme de vecteurs et pouvait être ajustée à la hausse ou à la baisse sans perdre la fidélité. Mais c'était plus délicat en HTML. Nous avons essayé d'utiliser la mise à l'échelle CSS, mais le canevas a bien été flouté:

Ajustement CSS (à gauche) ou redessin (à droite).
Mise à l'échelle CSS (à gauche) ou redessinage (à droite).

Notre solution consiste à redessiner le jeu, le rack et les tuiles chaque fois que l'utilisateur redimensionne le navigateur:

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

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

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

Nous obtenons des images nettes et des mises en page agréables, quelle que soit la taille de l'écran:

La plate-forme de jeu remplit l&#39;espace vertical, tandis que d&#39;autres éléments de la page s&#39;y trouvent.
La carte de jeu remplit l'espace vertical, tandis que d'autres éléments de la page s'entourent.

Aller à l'essentiel

Étant donné que chaque carte est positionnée de manière précise et doit s'aligner avec précision sur le plateau de jeu et le support, nous avons besoin d'un système de positionnement fiable. Nous utilisons deux fonctions, Bounds et Point, pour vous aider à gérer l'emplacement des éléments dans l'espace global (la page HTML). Bounds décrit une zone rectangulaire sur la page, tandis que Point décrit une coordonnée x,y relative à l'angle supérieur gauche de la page (0,0), également appelée point d'enregistrement.

Avec Bounds, nous pouvons détecter l'intersection de deux éléments rectangulaires (par exemple, lorsqu'une carte traverse le rack) ou si une zone rectangulaire (comme un espace à deux lettres) contient un point arbitraire (comme le point central d'une carte). Voici l'implémentation des limites:

// 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(",");
}

Nous utilisons Point pour déterminer la coordonnée absolue (coin supérieur gauche) de tout élément sur la page ou d'un événement de souris. Point contient également des méthodes de calcul de la distance et de la direction, qui sont nécessaires pour créer des effets d'animation. Voici l'implémentation de 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);
}

Ces fonctions constituent la base des fonctionnalités de glisser-déposer et d'animation. Par exemple, nous utilisons Bounds.intersects() pour déterminer si une tuile chevauche un espace sur le plateau de jeu ; nous utilisons Point.vector() pour déterminer la direction d'une tuile déplacée ; et nous utilisons Point.interpolate() en combinaison avec un minuteur pour créer un effet d'interpolation de mouvement ou de lissage de vitesse.

Je suis le mouvement, en toute fluidité

Si les mises en page à taille fixe sont plus faciles à produire dans Flash, les mises en page fluides sont beaucoup plus faciles à générer avec le code HTML et le modèle "box" CSS. Prenons l'exemple suivant en mode Grille, avec une largeur et une hauteur variables:

Cette mise en page n&#39;a pas de dimensions fixes: les miniatures s&#39;affichent de gauche à droite et de haut en bas.
Cette mise en page n'a pas de dimensions fixes: les miniatures s'affichent de gauche à droite et de haut en bas.

Ou bien consultez le panneau de chat. La version Flash nécessitait plusieurs gestionnaires d'événements pour répondre aux actions de la souris, un masque pour la zone déroulante, des calculs mathématiques pour calculer la position de défilement et beaucoup d'autres codes pour le coller ensemble.

Le panneau de chat dans Flash était assez mais complexe.
Le panneau de chat dans Flash était assez, mais complexe.

La version HTML, en comparaison, n'est qu'un <div> avec une hauteur fixe et la propriété overflow définie sur "hidden". Le défilement ne nous coûte rien.

Le modèle de zone CSS est utilisé.
Le modèle "box CSS" en action

Dans ce cas, il s'agit de tâches de mise en page ordinaires. Les langages HTML et CSS dépassent la technologie Flash.

Vous m'entendez ?

Nous avons rencontré des difficultés avec la balise <audio>, car elle ne pouvait tout simplement pas diffuser de courts effets sonores de façon répétée dans certains navigateurs. Nous avons essayé deux solutions. Tout d'abord, nous avons rempli d'air vide pour les fichiers audio afin de les rallonger. Nous avons ensuite essayé d'alterner la lecture sur plusieurs canaux audio. Aucune de ces deux techniques n'était totalement efficace ni élégante.

Nous avons finalement décidé de déployer notre propre lecteur audio Flash et d'utiliser l'audio HTML5 en remplacement. Voici le code de base dans 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);

En JavaScript, nous essayons de détecter le lecteur Flash intégré. En cas d'échec, nous créons un nœud <audio> pour chaque fichier 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();
}
}

Notez que cela ne fonctionne que pour les fichiers MP3. Nous n'avons jamais pris en charge le format OGG. Nous espérons que tous les acteurs du secteur trouveront bientôt un format unique.

Position du sondage

Nous utilisons la même technique en HTML5 que dans Flash pour actualiser l'état du jeu: toutes les 10 secondes, le client envoie une demande de mise à jour au serveur. Si l'état du jeu a changé depuis le dernier sondage, le client reçoit et gère les modifications. Sinon, rien ne se passe. Cette technique de sondage traditionnelle est acceptable, si ce n'est pas tout à fait élégante. Nous souhaitons toutefois passer à la fonctionnalité de sondage long ou à WebSockets à mesure que le jeu gagne en maturité et que les utilisateurs s'attendent à une interaction en temps réel sur le réseau. WebSockets, en particulier, offrirait de nombreuses possibilités pour améliorer le jeu.

Quel outil !

Nous avons utilisé Google Web Toolkit (GWT) pour développer à la fois l'interface utilisateur et la logique de contrôle du backend (authentification, validation, persistance, etc.). Le JavaScript lui-même est compilé à partir du code source Java. Par exemple, la fonction Point est adaptée de 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));
}
...
}

Certaines classes d'interface utilisateur ont des fichiers de modèle correspondants dans lesquels les éléments de page sont "liés" aux membres de la classe. Par exemple, ChatPanel.ui.xml correspond à 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>

Les détails complets dépassent le cadre de cet article, mais nous vous encourageons à essayer GWT pour votre prochain projet HTML5.

Pourquoi utiliser Java ? Commençons par la saisie stricte. Bien que la saisie dynamique soit utile en JavaScript (par exemple, la capacité d'un tableau à contenir des valeurs de différents types), elle peut s'avérer un casse-tête dans les projets volumineux et complexes. Deuxièmement, pour les capacités de refactorisation. Réfléchissez à la façon dont vous modifieriez la signature d'une méthode JavaScript sur des milliers de lignes de code, mais ce n'est pas très simple. Mais avec un bon IDE Java, c'est un jeu d'enfant. Enfin, à des fins de test. L'écriture de tests unitaires pour les classes Java surpasse la technique traditionnellement utilisée pour enregistrer et actualiser la page.

Résumé

Hormis nos problèmes audio, le format HTML5 a largement dépassé nos attentes. En plus d'être aussi fluide que dans Flash, Wordico est aussi fluide et réactif. C'est impossible sans Canvas et CSS3. Notre prochain défi: adapter Wordico aux appareils mobiles