Come possono due tecnologie tanto diverse finire nello stesso articolo? Nelle prossime righe scopriremo alcuni spunti interessanti di cooperazione tra queste due a prima vista così distanti features introdotte dall'HTML5. Ma prima una veloce panoramica: stiamo ovviamente parlando di sviluppo orientato a device mobile, nella fattispecie le peculiarità delle API qui trattate rendono il tutto abbastanza sperimentale e, di conseguenza, ne limitano il supporto.
Stando al comodissimo caniuse i Device Orientation Event sono disponibili su iOs dalla versione 4.2 e su Android dalla 3.0, mentre i WebSocket sono ad oggi appannaggio solo del sistema operativo mobile di casa Apple. Se a questo aggiungiamo che durante questo articolo ci avvarremo anche dei CSS3D riduciamo ulteriormente il supporto ai soli device iOS.
Ovviamente la speranza è che in un prossimo futuro anche altri browser includano queste interessanti features, ma per il momento dobbiamo accontentarci del carattere limitato dell'iniziativa.
Cosa sono le Device Orientation API?
Fatte le dovute premesse cominciamo esplorando le Device Orientation API. Attraverso l'evento deviceorientation, possiamo accedere ad alcune informazioni legate alla posizione spaziale del nostro device, il tutto è contenuto in 3 variabili agganciate all'oggetto event
passato al listener:
alpha
: contiene informazioni sull'orientamento del device, è comparabile al valore di una bussola, espresso in gradi (0 - 360) con lo 0 al device che punta a sud;beta
: pubblica invece informazioni sull'inclinazione del device rispetto ad una posizione parallela al piano terrestre. Anche questo valore è espresso in gradi con 0 al device in piano e +/- 90 alle due verticalità;gamma
: esprime l'inclinazione del device rispetto all'asse che lo attraversa per il lato più lungo, anche qui lo 0 viene assegnato al device parallelo al piano terrestre e con il display verso l'alto
mentre il valore varia da -180 a +180 gradi.
Il sistema di misurazione è simile a quello utilizzato per definire la posizione degli aerei, dove alpha corrisponde all'imbardata, beta al beccheggio e gamma al rollio. Ecco un semplicissimo esempio nell'uso di questa tecnologia, costruiamo una pagina web con il seguente codice sorgente:
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Level One</title>
<script>
window.addEventListener ("deviceorientation", traccia, false);
function traccia(evento){
document.querySelector('div.alpha span').innerHTML =
Math.round(evento.alpha);
document.querySelector('div.beta span').innerHTML =
Math.round(evento.beta);
document.querySelector('div.gamma span').innerHTML =
Math.round(evento.gamma);
}
</script>
<style>
html,body{ height: 100%; }
body{
display: box;
box-align: center;
box-pack: center;
font-size: 5em;
}
</style>
<!-- scaricalo da http://leaverou.github.com/prefixfree/ -->
<script src="../prefixfree.js"></script>
</head>
<body>
<div>
<div class="alpha">Alpha<span></span></div>
<div class="beta" >Beta<span></span></div>
<div class="gamma">Gamma<span></span></div>
</div>
</body>
</html>
Provando il codice appena sviluppato in un browser mobile dotato di supporto alle Device Orientation API potremo osservare dal vivo il comportamento di questi tre valori.
Ruotiamo le immagini
Anche solo focalizzandoci su di una sola variabile, alpha
, possiamo ottenere risultati di tutto rispetto. Ad esempio possiamo utilizzare la potenza dei CSS3 per applicare una rotazione ad una immagine opposta alla rotazione del device dell'utente.
Per fare questo dobbiamo avvalerci di una proprietà chiamata transform: rotateY(deg), che, come il nome suggerisce, applica una rotazione pari al valore di deg sull'asse delle y che attraversa il centro dell'elemento.
Ecco come possiamo agganciare l'evento deviceorientation alla rotazione sulla nostra immagine:
window.addEventListener ("deviceorientation", traccia, false);
function traccia(evento) {
document.querySelector('img').
setAttribute('style','-webkit-transform: scale(4) rotateY(' + (360 - evento.alpha) + 'deg);' +
'transform: scale(4) rotateY(' + (360 - evento.alpha) + 'deg);');
}
Notiamo come al momento non ci preoccupiamo di aggiungere prefissi sperimentali se non quello dedicato Webkit, layout engine che sottende sia il browser di casa Apple che quello di casa Google. Quando si estenderà il supporto per le features in oggetto dovremo ricordarci di aggiungere anche i prefissi sperimentali per eventuali nuovi browsers.
Prima di poter apprezzare il risultato di questo nostro esperimento dobbiamo necessariamente affrontare un altro tema; con le istruzioni fin qui svolte l'immagine ruota in opposizione alla rotazione del device, ma su se stessa e non attorno alla telecamera virtuale dalla quale l'utente sta visualizzando la pagina. Nessun problema, grazie alla proprietà transform-origin possiamo cambiare l'origine della rotazione facendola coincidere con il punto di osservazione dell'utente, ecco quindi il listato HTML di questo nostro secondo esperimento:
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Level Three</title>
<script>
window.addEventListener ("deviceorientation", traccia, false);
function traccia(evento){
document.querySelector('img').
setAttribute('style',
'-webkit-transform: scale(4) rotateY(' + (360 - evento.alpha) + 'deg);' +
'transform: scale(4) rotateY(' + (360 - evento.alpha) + 'deg);'
);
}
</script>
<style>
html,body { height: 100%; }
body {
display: box;
box-align: center;
box-pack: center;
perspective: 400px;
background-image: radial-gradient(center center, white, gray);
}
img {
display: block;
transform-style: preserve-3d;
transform-origin: 225px 150px 400px;
}
</style>
<!-- scaricalo da http://leaverou.github.com/prefixfree/ -->
<script src="../prefixfree.js"></script>
</head>
<body>
<img src="img/panorama.jpg">
</body>
</html>
Notiamo come la componente z
della proprietà perspective-origin
sia uguale alla distanza imposta dalla direttiva prospective; in questo modo la rotazione avverrà attorno al punto di osservazione dell'utente.
Ecco uno screenshot della pagina in azione:
Creare un cubo con i CSS3D
Utilizzando in modo avanzato le proprietà di rotazione e traslazione nello spazio tridimensionale possiamo costruire un cubo: cominciamo col definire la sua struttura HTML composta dai sei div
dedicati alle facce e dal div
contenitore:
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Un cubo</title>
</head>
<body>
<div id="container">
<div class="square bottom"></div>
<div class="square left"></div>
<div class="square right"></div>
<div class="square front"></div>
<div class="square top"></div>
<div class="square back"></div>
</div>
</body>
</html>
Segue la definizione del foglio di stile, per prima cose impostiamo alcune proprietà base, centrando il contenuto rispetto ai due assi, definendo la profondità prospettica ed abilitando la modalità di visualizzazione in 3D:
html,body{
height: 100%;
}
body{
display: box;
box-align: center;
box-pack: center;
perspective: 800px;
background-image: radial-gradient(center center, white, gray);
}
div#container{
position: relative;
width: 800px;
height: 800px;
transform-style: preserve-3d;
transform: translateZ(-800px) rotateY(0deg);
}
Quindi impostiamo la classe .square
, comune a tutte le facce del cubo:
div.square {
position: absolute;
top: 0px;
left: 0px;
width: 800px;
height: 800px;
}
Ora dobbiamo applicare una trasformazione diversa ad ogni singola faccia, considerando che il centro della trasformazione corrisponde al centro di ogni elemento possiamo pensare di applicare una rotazione seguita da una traslazione di metà della dimensione della faccia, ad esempio:
div.square.left {
background: green;
transform: rotateY(90deg) translateZ(-400px);
}
allo stesso modo risolviamo la disposizione delle altre facce:
div.square.bottom {
background: red;
transform: rotateX(90deg) translateZ(-400px);
}
div.square.right{
background: blue;
transform: rotateY(270deg) translateZ(-400px);
}
div.square.front{
background: yellow;
transform: rotateY(0deg) translateZ(-400px);
}
div.square.top{
background: purple;
transform: rotateX(270deg) translateZ(-400px);
}
div.square.back{
background: orange;
transform: rotateY(180deg) translateZ(-400px);
}
Aggiungiamo il comodissimo Prefix Free di Lea Verou per l'aggiunta automatica dei prefissi sperimentali
<!-- scaricalo da http://leaverou.github.com/prefixfree/ -->
<script src="../prefixfree.js"></script>
e testiamo quanto sviluppato finora:
Ovviamente la faccia più vicina alla telecamere oscura le altre e non abbiamo ancora sviluppato nessun legame tra l'oggetto che abbiamo costruito e le Device Orientation API
. Ma rimediamo subito.
Per complicare la situazione, e rendere più interessante l'esperimento, in questo caso attingiamo dalle informazioni prelevate da tutte e tre le variabili; in questa condizione è importante mantenere l'ordine della trasformazione inverso rispetto a alle variabili ricevute, quindi gamma
, beta
e alpha
:
window.addEventListener ("deviceorientation", traccia, false);
function traccia(evento) {
document.getElementById('container').style['-webkit-transform'] =
"translateZ(-800px)" +
"rotateY(" + ( -evento.gamma ) + "deg) " +
"rotateX(" + evento.beta + "deg) " +
"rotateZ(" + ( evento.alpha ) + "deg) ";
document.getElementById('container').style['transform'] =
document.getElementById('container').style['-webkit-transform'];
}
Aggiorniamo la pagina sul nostro device per apprezzare il risultato:
Dentro al cubo
Ma cosa succede se spostiamo la telecamera all'interno del cubo? Per farlo e sufficiente modificare una sola riga nel progetto precedente, nella fattispecie la proprietà transform del #container
traslando il cubo lungo l'asse z fino a portare la telecamera all'interno di esso:
transform: translateZ(800px) rotateY(0deg);
Aggiorniamo la nostra demo e potremo compiacerci di aver creato un semplicissimo sistema per gestire un tour vrtuale a 360° dove l'utente può indirizzare la telecamera orientando il proprio device.
Ma non è finita, costruendo una cubemap, o utilizzandone una dalla rete, e associando ad ogni faccia del nostro cubo la giusta porzione della cubemap è possibile ottenere un vero e proprio panorama esplorabile a 360 gradi, ecco uno screenshot dimostrativo:
Veicolare le informazioni con i WebSocket
Il funzionamento dei WebSocket è abbastanza semplice, fondamentalmente queste API consentono di stabilire un canale di comunicazione bidirezionale tra un client ed un server, abilitando in questo modo la possibilità che sia il server ad inviare informazioni senza che il client le richieda.
Questo meccanismo è alla base di molte applicazioni asincrone, chat in primis, ma può essere utilizzato
anche in altri ed interessanti modi.
Ad esempio cosa succedesse se decidessimo di trasmettere attraverso WebSocket le informazioni recuperate delle Device Orientation API? Lo scenario potrebbe essere il seguente: un device viene manipolato, le sue informazioni trasmesse ad un server e poi ad un altro client, all'interno del quale vengono utilizzate per agire su quanto visualizzato. Lo stesso comportamento che sperimentiamo ogniqualvolta ci cimentiamo con la console Wii.
Operiamo sull'esempio precedente facendo in modo che la rotazione del cubo possa essere pilotata da un device remoto. In questo caso quindi il cubo verrà proiettato su di uno schermo desktop mentre il device avrà il ruolo di joystick.
La comunicazione attraverso i WebSocket è sufficientemente semplice, almeno nei limiti di quanto serve per questa demo. Ecco come cambia il javascript dell'esempio precedente:
/* ricordarsi di inserire il corretto indirizzo del server */
var socket = new WebSocket("ws://0.0.0.0:8080");
socket.addEventListener("open", registratiMaster, false);
socket.addEventListener("message", stampaMessaggio, false);
function registratiMaster(evento){
socket.send('register master');
}
function stampaMessaggio(evento){
var angoli = JSON.parse(evento.data);
document.getElementById('container').style['-webkit-transform'] =
"translateZ(-400px) " +
"rotateZ(" + ( -angoli.gamma ) + "deg) " +
"rotateX(" + angoli.beta + "deg) " +
"rotateY(" + angoli.alpha + "deg)";
document.getElementById('container').style['transform'] =
document.getElementById('container').style['-webkit-transform'];
}
Come possiamo notare in questo caso le valorizzazioni delle tre rotazioni non provengono più dall'evento deviceorientation ma da message, ovverossia dall'evento che viene generato ogniqualvolta un nuovo messaggio arrivi dal server presso il quale ci siamo registrati istanziando un nuovo oggetto WebSocket.
Ora dobbiamo costruire una pagina HTML progettata per essere eseguita nel device e per inviare le informazioni alpha
beta
e gamma
allo stesso server, che dovrà poi provvedere ad instradarle verso il client contente il cubo che abbiamo visto poche righe fa:
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Pilota il cubo</title>
<meta name="viewport"
content="width=device-width,
initial-scale=1.0,
maximum-scale=1.0,
user-scalable=no"/>
<script>
/* ricordarsi di specificare l'ip del server WebSocket */
var socket = new WebSocket("ws://0.0.0.0:8080");
socket.addEventListener("open", prontoPerMuovere, false);
socket.addEventListener("message", nuovoMessaggio, false);
function prontoPerMuovere(){
window.addEventListener ("deviceorientation", muovi, false);
}
function muovi(evento){
socket.send(JSON.stringify({
alpha: evento.alpha,
beta: evento.beta,
gamma: evento.gamma
}));
}
</script>
</head>
<body>
</body>
</html>
Anche in questo caso il codice è molto facile da interpretare, all'apertura del socket (evento open
) ci mettiamo in ascolto del solito evento deviceorientation, questa volta però, al posto che utilizzare le informazioni direttamente, le serializziamo in JSON e le inviamo al WebSocket, che dovrà inviarle a sua volta al client desktop che abbiamo sviluppato poco fa.
Non resta che sviluppare il server, il cui unico compito consiste nell'instradare correttamente i messaggi tra i due client. Utilizzeremo Ruby che riesce a mantenere un'alta leggibilità pur garantendo un codice succinto.
Ecco le poche righe che compongono il server (server.rb):
require 'em-websocket'
EventMachine.run do
@channel = EM::Channel.new
EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080) do |ws|
ws.onmessage do |msg|
case msg
when 'register master'
puts "the master is ready"
@channel.subscribe { |m| ws.send m }
else
puts "sending #{msg} to master"
@channel.push msg
end
end
end
end
Come possiamo notare il server identifica il master
, ovverosia il client che contiene il cubo, da un messaggio 'register master'; una volta avvenuta questa identificazione ogni successivo messaggio proveniente da qualsiasi altro client verrà semplicemente instradato verso il master.
Ovviamente l'implementazione è molto naif, consente l'esecuzione di un solo master per volta e non tiene d'acconto di nessuna tematica relativa alla sicurezza della comunicazione, però rimane funzionante ed efficiente.
Per provare questa demo è necessario premunirsi di Ruby on Rails e della gemma 'em-websocket' che può essere installata eseguendo da un terminale:
gem install em-websocket
Ora è sufficiente eseguire il server (ruby server.rb
) e successivamente accedere alla pagina master da un qualsiasi desktop ed aprire con un device mobile l'altra pagina sviluppata. Se abbiamo impostato correttamente gli ip e le due istanze condividono la medesima rete dovremmo poter pilotare l'orientamento del cubo su desktop semplicemente inclinando il device mobile che teniamo in mano.
Conclusioni
WebSocket e Device Orientation API si prestano a tutta una serie di sviluppi molto interessanti sia per l'ambito ludico che no. Al momento purtroppo il supporto è ancora troppo scarso per poter considerare questa tecnologia production ready ma tenendo conto dell'attuale evoluzione da parte dei browser mobile sono confidente che questo problema possa risolversi nel breve periodo.