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