Case study - Esperimento Google I/O 2013

Thomas Reynolds
Thomas Reynolds

Introduzione

Per suscitare l'interesse degli sviluppatori sul sito web di Google I/O 2013 prima dell'apertura delle registrazioni alla conferenza, abbiamo sviluppato una serie di esperimenti e giochi mobile-first incentrati sulle interazioni touch, sull'audio generativo e sul piacere della scoperta. Ispirata al potenziale del codice e alla potenza del gioco, questa esperienza interattiva inizia con i semplici suoni di "I" e "O" quando tocchi il nuovo logo di I/O.

Movimento organico

Abbiamo deciso di implementare le animazioni I e O con un effetto organico traballante che non si vede spesso nelle interazioni HTML5. Impostare le opzioni per renderlo divertente e reattivo ha richiesto un po' di tempo.

Esempio di codice di fisica rimbalzante

Per ottenere questo effetto, abbiamo utilizzato una semplice simulazione fisica su una serie di punti che rappresentano i bordi delle due forme. Quando viene toccata una delle forme, tutti i punti vengono accelerati dalla posizione del tocco. Si allungano e si allontanano prima di essere tirati indietro.

All'inizializzazione, ogni punto riceve un'accelerazione casuale e un "elasticità" di rimbalzo, in modo che l'animazione non sia uniforme, come puoi vedere in questo codice:

this.paperO_['vectors'] = [];

// Add an array of vector points and properties to the object.
for (var i = 0; i < this.paperO_['segments'].length; i++) {
 
var point = this.paperO_['segments'][i]['point']['clone']();
  point
= point['subtract'](this.oCenter);

  point
['velocity'] = 0;
  point
['acceleration'] = Math.random() * 5 + 10;
  point
['bounce'] = Math.random() * 0.1 + 1.05;

 
this.paperO_['vectors'].push(point);
}

Quando vengono toccati, vengono accelerati verso l'esterno dalla posizione del tocco utilizzando il codice riportato di seguito:

for (var i = 0; i < path['vectors'].length; i++) {
 
var point = path['vectors'][i];
 
var vector;
 
var distance;

 
if (path === this.paperO_) {
    vector
= point['add'](this.oCenter);
    vector
= vector['subtract'](clickPoint);
    distance
= Math.max(0, this.oRad - vector['length']);
 
} else {
    vector
= point['add'](this.iCenter);
    vector
= vector['subtract'](clickPoint);
    distance
= Math.max(0, this.iWidth - vector['length']);
 
}

  point
['length'] += Math.max(distance, 20);
  point
['velocity'] += speed;
}

Infine, ogni particella viene decelerata in ogni frame e torna lentamente in equilibrio con questo approccio nel codice:

for (var i = 0; i < path['segments'].length; i++) {
 
var point = path['vectors'][i];
 
var tempPoint = new paper['Point'](this.iX, this.iY);

 
if (path === this.paperO_) {
    point
['velocity'] = ((this.oRad - point['length']) /
      point
['acceleration'] + point['velocity']) / point['bounce'];
 
} else {
    point
['velocity'] = ((tempPoint['getDistance'](this.iCenter) -
      point
['length']) / point['acceleration'] + point['velocity']) /
      point
['bounce'];
 
}

  point
['length'] = Math.max(0, point['length'] + point['velocity']);
}

Demo di movimenti organici

Ecco la modalità A casa di I/O con cui sperimentare. Abbiamo anche esposto una serie di opzioni aggiuntive in questa implementazione. Se attivi "Mostra punti", vedrai i singoli punti su cui agiscono la simulazione fisica e le forze.

Nuova skin

Una volta ottenuto il risultato desiderato con il movimento in modalità Casa, abbiamo voluto utilizzare lo stesso effetto per due modalità retrò: 8 bit e ASCII.

Per eseguire questa modifica dell'aspetto, abbiamo utilizzato lo stesso canvas della modalità A casa e abbiamo utilizzato i dati dei pixel per generare ciascuno dei due effetti. Questo approccio ricorda uno shader di frammenti OpenGL in cui ogni pixel della scena viene ispezionato e manipolato. Analizziamo questo aspetto.

Esempio di codice "Shader" di Canvas

I pixel in una tela possono essere letti utilizzando il metodo getImageData. L'array restituito contiene 4 valori per pixel che rappresentano il valore RGBA di ciascun pixel. Questi pixel sono combinati insieme in una massiccia struttura simile a un array. Ad esempio, un canvas 2x2 avrebbe 4 pixel e 16 voci nell'array imageData.

La nostra tela è a schermo intero, quindi se fingiamo che lo schermo sia 1024x768 (come su un iPad), l'array ha 3.145.728 voci. Poiché questa è un'animazione, l'intero array viene aggiornato 60 volte al secondo. I motori JavaScript moderni possono gestire il looping e l'applicazione di questi dati abbastanza rapidamente da mantenere costante la frequenza dei fotogrammi. (Suggerimento: non provare a registrare questi dati nella console per sviluppatori, perché rallenterebbe la scansione del browser o lo farebbe arrestare in modo anomalo.)

Ecco come la nostra modalità 8 bit legge la tela della modalità Casa e ingrandisce i pixel per ottenere un effetto più blocchi:

var pixelData = pctx.getImageData(0, 0, sourceCanvas.width, sourceCanvas.height);

// tctx is the Target Context for the output Canvas element
tctx
.clearRect(0, 0, targetCanvas.width + 1, targetCanvas.height + 1);

var size = ~~(this.width_ * 0.0625);

if (this.height_ * 6 < this.width_) {
 size
/= 8;
}

var increment = Math.min(Math.round(size * 80) / 4, 980);

for (i = 0; i < pixelData.data.length; i += increment) {
 
if (pixelData.data[i + 3] !== 0) {
   
var r = pixelData.data[i];
   
var g = pixelData.data[i + 1];
   
var b = pixelData.data[i + 2];
   
var pixel = Math.ceil(i / 4);
   
var x = pixel % this.width_;
   
var y = Math.floor(pixel / this.width_);

   
var color = 'rgba(' + r + ', ' + g + ', ' + b + ', 1)';

    tctx
.fillStyle = color;

   
/**
     * The ~~ operator is a micro-optimization to round a number down
     * without using Math.floor. Math.floor has to look up the prototype
     * tree on every invocation, but ~~ is a direct bitwise operation.
     */

    tctx
.fillRect(x - ~~(size / 2), y - ~~(size / 2), size, size);
 
}
}

Demo shader a 8 bit

Di seguito, rimuoviamo l'overlay di Eightbit e vediamo l'animazione originale sotto. L'opzione "kill screen" mostra uno strano effetto che abbiamo scoperto campionando in modo errato i pixel di origine. Alla fine l'abbiamo utilizzato come easter egg "responsive" quando la modalità 8 bit viene ridimensionata in proporzioni improbabili. Incidente felice.

Compositing di Canvas

È sorprendente cosa puoi ottenere combinando più passaggi di rendering e maschere. Abbiamo creato una metaball 2D che richiede che ogni sfera abbia il proprio gradiente radiale e che questi gradienti vengano miscelati dove le sfere si sovrappongono. Puoi vedere questo nella demo di seguito.

Per farlo, abbiamo utilizzato due canvas distinti. La prima tela calcola e disegna la forma della metasfera. Un secondo canvas disegna gradienti radiali in ogni posizione della palla. Poi la forma maschera i gradienti e viene visualizzato l'output finale.

Esempio di composizione di codice

Ecco il codice che fa funzionare tutto:

// Loop through every ball and draw it and its gradient.
for (var i = 0; i < this.ballCount_; i++) {
 
var target = this.world_.particles[i];

 
// Set the size of the ball radial gradients.
 
this.gradSize_ = target.radius * 4;

 
this.gctx_.translate(target.pos.x - this.gradSize_,
    target
.pos.y - this.gradSize_);

 
var radGrad = this.gctx_.createRadialGradient(this.gradSize_,
   
this.gradSize_, 0, this.gradSize_, this.gradSize_, this.gradSize_);

  radGrad
.addColorStop(0, target['color'] + '1)');
  radGrad
.addColorStop(1, target['color'] + '0)');

 
this.gctx_.fillStyle = radGrad;
 
this.gctx_.fillRect(0, 0, this.gradSize_ * 4, this.gradSize_ * 4);
};

Poi, configura la tela per il mascheramento e disegna:

// Make the ball canvas the source of the mask.
this.pctx_.globalCompositeOperation = 'source-atop';

// Draw the ball canvas onto the gradient canvas to complete the mask.
this.pctx_.drawImage(this.gcanvas_, 0, 0);
this.ctx_.drawImage(this.paperCanvas_, 0, 0);

Conclusione

La varietà di tecniche e tecnologie che abbiamo potuto utilizzare (come Canvas, SVG, animazione CSS, animazione JS, Web Audio e così via) ha reso lo sviluppo del progetto incredibilmente divertente.

C'è molto di più da esplorare rispetto a quanto vedi qui. Continua a toccare il logo I/O e, se le sequenze sono corrette, sbloccherai altri mini-esperimenti, giochi, immagini psichedeliche e forse anche alcuni alimenti per la colazione. Ti consigliamo di provarlo sullo smartphone o sul tablet per un'esperienza ottimale.

Ecco una combinazione per iniziare: O-I-I-I-I-I-I-I. Prova subito: google.com/io

Open source

Abbiamo reso open source il nostro codice con licenza Apache 2.0. Puoi trovarlo sul nostro canale GitHub all'indirizzo http://github.com/Instrument/google-io-2013.

Crediti

Sviluppatori:

  • Thomas Reynolds
  • Brian Hefter
  • Stefanie Hatcher
  • Paul Farning

Designer:

  • Dan Schechter
  • Salvia
  • Andrea Rossi

Producer:

  • Amie Pascal
  • Andrea Nelson