Case study - Esperimento Google I/O 2013

[Nome di persona]
Thomas Reynolds

Introduzione

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

Movimenti organici

Abbiamo deciso di implementare le animazioni I e O in un effetto organico traballante che non si verifica spesso nelle interazioni HTML5. La scelta delle opzioni per farla sembrare divertente e reattiva 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 una delle due forme viene toccata, tutti i punti vengono accelerati in uscita dalla posizione del tocco. Si allungano e si allontanano prima di essere tirati dentro.

Al momento dell'istanza, ogni punto riceve una quantità di accelerazione casuale e "rimbalzi" del rimbalzo in modo che non si animano in modo 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);
}

Successivamente, quando vengono toccati, vengono accelerati verso l'esterno rispetto alla posizione del tocco utilizzando il seguente codice:

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 lentamente torna all'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 movimento organico

Ecco la modalità Home I/O con cui puoi giocare. Inoltre, abbiamo proposto una serie di opzioni aggiuntive in questa implementazione. Se attivi "Mostra punti", vedrai i singoli punti su cui agiscono le forze e la simulazione fisica.

Rinnovamento dell'aspetto

Una volta soddisfatti del movimento in modalità Home, volevamo utilizzare lo stesso effetto per due modalità retrò: Eightbit e Ascii.

Per ottenere il rinnovamento dell'aspetto, abbiamo utilizzato la stessa tela della modalità Home e i dati dei pixel per generare ciascuno dei due effetti. Questo approccio è simile a uno Shader di frammenti OpenGL, in cui ogni pixel della scena viene ispezionato e manipolato. Esaminiamo questo aspetto più nel dettaglio.

Esempio di codice "Shader" della tela

I pixel su Canvas possono essere letti utilizzando il metodo getImageData. L'array restituito contiene 4 valori per pixel che rappresentano ciascun valore RGBA in pixel. Questi pixel sono uniti in un'enorme struttura simile a un array. Ad esempio, un canvas 2 x 2 avrà 4 pixel e 16 voci nell'array imageData.

La nostra tela è a schermo intero, quindi se facciamo finta che lo schermo sia 1024 x 768 (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 moderni motori JavaScript sono in grado di gestire il loop e agire su questa quantità di dati abbastanza rapidamente da mantenere coerente la frequenza fotogrammi. Suggerimento: non provare a registrare questi dati nella console per gli sviluppatori, perché potrebbe rallentare la scansione del browser o arrestarsi del tutto.

Ecco come la nostra modalità a 8 bit legge il canvas con la modalità Home e fa esplodere i pixel per ottenere un effetto più effetto:

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 a otto Shader

Di seguito rimuoviamo l'overlay Eightbit e al di sotto viene visualizzata l'animazione originale. L'opzione "kill screen" mostra uno strano effetto in cui ci siamo imbattuti a causa del campionamento errato dei pixel di origine. Abbiamo finito per utilizzarlo come un easter egg "adattabile" quando la modalità a otto bit viene ridimensionata a proporzioni improbabili. Buon incidente!

Composizione di canvas

È sorprendente ciò che puoi ottenere combinando più passaggi di rendering e maschere. Abbiamo creato una metaball 2D che richiede che ogni pallina abbia un proprio gradiente radiale che deve essere miscelato insieme nel punto in cui le due sfere si sovrappongono. (Puoi verificarlo nella demo riportata di seguito).

Per farlo, abbiamo usato due tele separate. Il primo canvas calcola e disegna la forma metaball. Un secondo canvas disegna gradienti radiali in ciascuna posizione della sfera. Quindi la forma maschera i gradienti e viene eseguito il rendering dell'output finale.

Esempio di codice di compositing

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

Quindi, 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 che abbiamo dovuto utilizzare e le tecnologie che abbiamo implementato (come Canvas, SVG, CSS Animation, JS Animation, Web Audio ecc.) hanno reso il progetto estremamente divertente da sviluppare.

C'è molto altro da esplorare di quello che si vede qui, persino. Continua a toccare il logo dell'I/O e le sequenze corrette sbloccheranno altri mini-esperimenti, giochi, immagini accattivanti e forse anche qualche spuntino per la colazione. Ti consigliamo di provarla sul tuo smartphone o tablet per un'esperienza migliore.

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

Open source

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

Crediti

Sviluppatori:

  • [Nome di persona]
  • Brian Hefter
  • Stefanie Hatcher
  • Paul Farning

Designer:

  • Dan Schechter
  • Salvia
  • Kyle Beck

Produttori:

  • Amie Pascal
  • Andrea Rossi