Case study - Conversione di Wordico da Flash a HTML5

Adam Mark
Adam Mark
Adrian Gould
Adrian Gould

Introduzione

Quando abbiamo convertito il nostro cruciverba Wordico da Flash in HTML5, la nostra prima attività è stata disimparare tutto ciò che sapevamo sulla creazione di una ricca esperienza utente nel browser. Sebbene Flash offrisse un'unica API completa per tutti gli aspetti dello sviluppo di applicazioni, dal disegno vettoriale al rilevamento degli hit dei poligoni, all'analisi XML, HTML5 offriva un miscuglio di specifiche con diversi tipi di supporto per i browser. Ci siamo anche chiesti se l'HTML, un linguaggio specifico per i documenti, e il CSS, un linguaggio incentrato sulla scatola, fossero adatti alla creazione di un gioco. Il gioco verrebbe visualizzato in modo uniforme su tutti i browser, come in Flash, e avrebbe lo stesso aspetto e comportamento corretto? Per Wordico, la risposta è stata .

Qual è il tuo vettore, Victor?

Abbiamo sviluppato la versione originale di Wordico utilizzando solo grafica vettoriale: linee, curve, riempimenti e gradienti. Il risultato è stato sia estremamente compatto che scalabile all'infinito:

Wireframe Wordico
In Flash, ogni oggetto display era composto da forme vettoriali.

Inoltre, abbiamo sfruttato la sequenza temporale Flash per creare oggetti con più stati. Ad esempio, abbiamo usato nove fotogrammi chiave denominati per l'oggetto Space:

Uno spazio a tre lettere in Flash.
Uno spazio di tre lettere in Flash.

In HTML5, tuttavia, utilizziamo uno sprite bitmap:

Uno sprite PNG che mostra tutti e nove gli spazi.
Uno sprite PNG che mostra tutti e nove gli spazi.

Per creare il tabellone 15x15 dai singoli spazi, ripetiamo l'iterazione su una notazione stringa di 225 caratteri in cui ogni spazio è rappresentato da un carattere diverso (come "t" per lettera tripla e "T" per parola tripla). Si è trattato di un'operazione semplice in Flash: abbiamo semplicemente abbozzato gli spazi e li abbiamo disposti in una griglia:

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

In HTML5, è un po' più complicato. Utilizziamo l'elemento <canvas>, una superficie di disegno bitmap, per dipingere il tabellone un quadrato alla volta. Il primo passaggio consiste nel caricare lo sprite immagine. Dopo averla caricata, eseguiamo l'iterazione della notazione del layout, disegnando una parte diversa dell'immagine con ogni iterazione:

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

Ecco il risultato nel browser web. Tieni presente che la tela stessa ha un'ombra CSS:

In HTML5, il tabellone del gioco è un singolo elemento canvas.
In HTML5, il tabellone di gioco è un singolo elemento canvas.

La conversione dell'oggetto riquadro è stata eseguita in modo simile. In Flash, abbiamo usato campi di testo e forme vettoriali:

Il riquadro Flash era una combinazione di campi di testo e forme vettoriali
Il riquadro Flash era una combinazione di campi di testo e forme vettoriali.

In HTML5, combiniamo tre sprite di immagini su un singolo elemento <canvas> in fase di runtime:

Il riquadro HTML è un elemento composito di tre immagini.
Il riquadro HTML è composto da tre immagini.

Ora abbiamo 100 tele (una per ogni casella) più una tela per il tabellone. Ecco il markup per un riquadro "H":

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

Ecco il CSS corrispondente:

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

Applichiamo gli effetti CSS3 quando il riquadro viene trascinato (ombra, opacità e ridimensionamento) e quando il riquadro è posizionato sul rack (riflesso):

Il riquadro trascinato è leggermente più grande, leggermente trasparente e con un&#39;ombra.
Il riquadro trascinato è leggermente più grande, leggermente trasparente e ha un'ombra.

L'utilizzo di immagini raster presenta alcuni vantaggi evidenti. Il primo risultato è l'accuratezza dei pixel. In secondo luogo, queste immagini possono essere memorizzate nella cache dal browser. In terzo luogo, con un po' di lavoro in più, possiamo sostituire le immagini per creare nuovi modelli di piastrelle, ad esempio una piastrella di metallo, e questo lavoro può essere fatto in Photoshop anziché in Flash.

Lo svantaggio? Utilizzando le immagini, concediamo l'accesso programmatico ai campi di testo. In Flash, era una semplice operazione per modificare il colore o altre proprietà del tipo; in HTML5, queste proprietà sono integrate nelle immagini stesse. (Abbiamo provato il testo HTML, ma richiedeva molto markup e CSS in più. Abbiamo anche provato il testo canvas, ma i risultati non erano coerenti nei vari browser.)

Logica fuzzy

Volevamo sfruttare al massimo la finestra del browser a qualsiasi dimensione, evitando di scorrere. Si tratta di un'operazione relativamente semplice in Flash, poiché l'intero gioco è composto da vettori e può essere ridimensionato senza perdere la fedeltà. Ma era più difficile in HTML. Abbiamo provato a utilizzare la scalabilità CSS, ma il risultato è stato un canvas sfocato:

Scalabilità CSS (sinistra) e ridisegno (destra).
Scalabilità CSS (a sinistra) e ricalcolo (a destra).

La nostra soluzione consiste nel ridisegnare il tabellone, il rack e le schede ogni volta che l'utente ridimensiona il browser:

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

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

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

Otteniamo immagini nitide e layout piacevoli su schermi di qualsiasi dimensione:

Il tabellone riempie lo spazio verticale, circondato da altri elementi della pagina.
Il tabellone riempie lo spazio verticale; intorno a esso scorrono altri elementi della pagina.

Arriva al punto

Dal momento che ogni casella è in posizione assoluta e deve essere allineata con precisione al tabellone e al rack, abbiamo bisogno di un sistema di posizionamento affidabile. Utilizziamo due funzioni, Bounds e Point, per gestire la posizione degli elementi nello spazio globale (la pagina HTML). Bounds descrive un'area rettangolare sulla pagina, mentre Point descrive una coordinata x,y relativa all'angolo superiore sinistro della pagina (0,0), altrimenti nota come punto di registrazione.

Con Bounds, possiamo rilevare l'intersezione di due elementi rettangolari (ad esempio quando un riquadro attraversa il rack) o se un'area rettangolare (ad esempio uno spazio a due lettere) contiene un punto arbitrario (ad esempio il punto centrale di un riquadro). Di seguito è riportata l'implementazione dei limiti:

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

Utilizziamo Point per determinare la coordinata assoluta (angolo in alto a sinistra) di qualsiasi elemento della pagina o di un evento del mouse. Point contiene anche metodi per calcolare la distanza e la direzione, necessari per creare effetti di animazione. Di seguito è riportata l'implementazione di 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);
}

Queste funzioni costituiscono la base delle funzionalità di trascinamento e animazione. Ad esempio, utilizziamo Bounds.intersects() per determinare se una casella si sovrappone a uno spazio del tabellone del gioco; utilizziamo Point.vector() per determinare la direzione di una casella trascinata; usiamo Point.interpolate() in combinazione con un timer per creare un effetto di interpolazione del movimento o di rilassamento.

Seguo il flusso

Mentre i layout con dimensioni fisse sono più facili da produrre in Flash, i layout fluidi sono molto più facili da generare con il codice HTML e il modello a casella CSS. Considera la seguente visualizzazione griglia, con larghezza e altezza variabili:

Questo layout non ha dimensioni fisse: le miniature scorrono da sinistra a destra e dall&#39;alto verso il basso.
Questo layout non ha dimensioni fisse: le miniature scorrono da sinistra a destra e dall'alto verso il basso.

In alternativa, prendi in considerazione il riquadro della chat. La versione Flash richiedeva diversi gestori di eventi per rispondere alle azioni del mouse, una maschera per l'area scorrevole, un calcolo matematico per calcolare la posizione di scorrimento e molto altro codice per incollarlo.

Il riquadro della chat in Flash era piuttosto complesso, ma complesso.
Il riquadro della chat in Flash era piuttosto complesso, ma complesso.

La versione HTML, invece, è solo un <div> con altezza fissa e la proprietà overflow impostata su nascosta. Lo scorrimento non costa nulla.

Il modello di riquadro CSS in azione.
Il modello di box CSS in azione.

In casi come questo (attività di layout ordinarie) HTML e CSS superano Flash.

Mi senti ora?

Abbiamo avuto problemi con il tag <audio>, che semplicemente non era in grado di riprodurre ripetutamente effetti sonori brevi in alcuni browser. Abbiamo provato due soluzioni alternative. Per prima cosa, abbiamo riempito i file audio con l'aria morta per allungarli. Poi abbiamo provato ad alternare la riproduzione su più canali audio. Nessuna delle due tecniche era del tutto efficace né elegante.

Alla fine abbiamo deciso di implementare il nostro lettore audio Flash e di utilizzare l'audio HTML5 come riserva. Ecco il codice di base in 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);

In JavaScript, cerchiamo di rilevare il player Flash incorporato. Se non funziona, viene creato un nodo <audio> per ogni file 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();
}
}

Tieni presente che questa opzione funziona solo per i file MP3: non ci siamo mai preoccupati di supportare OGG. Ci auguriamo che il settore si accontenti di un unico formato nel prossimo futuro.

Posizione sondaggio

In HTML5 viene utilizzata la stessa tecnica utilizzata in Flash per aggiornare lo stato del gioco: ogni 10 secondi il client chiede al server gli aggiornamenti. Se lo stato del gioco è cambiato dall'ultimo sondaggio, il client riceve e gestisce le modifiche, altrimenti non succede nulla. Questa tecnica tradizionale di polling è accettabile, se non proprio elegante. Tuttavia, vorremmo passare ai polling lunghi o ai WebSockets man mano che il gioco matura e gli utenti si aspettano interazioni in tempo reale sulla rete. WebSocket, in particolare, offrirebbe molte opportunità per migliorare il gameplay.

Che strumento!

Abbiamo utilizzato Google Web Toolkit (GWT) per sviluppare sia l'interfaccia utente frontend che la logica di controllo back-end (autenticazione, convalida, persistenza e così via). Il codice JavaScript stesso viene compilato dal codice sorgente Java. Ad esempio, la funzione Punto è adattata da 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));
}
...
}

Alcune classi di UI hanno file modello corrispondenti in cui gli elementi di pagina sono "associati" ai membri della classe. Ad esempio, ChatPanel.ui.xml corrisponde a 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>

I dettagli completi non rientrano nell'ambito di questo articolo, ma ti invitiamo a consultare GWT per il tuo prossimo progetto HTML5.

Perché utilizzare Java? Innanzitutto, per la digitazione rigorosa. Sebbene la digitazione dinamica sia utile in JavaScript, ad esempio la capacità di un array di contenere valori di tipo diverso, può essere un problema nei progetti grandi e complessi. Secondo, per le capacità di refactoring. Considera come cambieresti la firma di un metodo JavaScript su migliaia di righe di codice, il che non è facile. Ma con un buon IDE Java, è un gioco da ragazzi. Infine, a scopo di test. La scrittura di test delle unità per le classi Java supera la tecnica consacrata dal tempo, ossia "salva e aggiorna".

Riepilogo

Fatta eccezione per i problemi audio, HTML5 ha superato di gran lunga le nostre aspettative. Wordico non solo ha un aspetto buono come in Flash, ma è anche fluido e reattivo. Senza Canvas e CSS3 non avremmo potuto farcela. La nostra prossima sfida: adattare Wordico all'uso sui dispositivi mobili.