Étude de cas : expérience Google I/O 2013

Thomas Reynolds
Thomas Reynolds

Introduction

Pour susciter l'intérêt des développeurs sur le site Web de Google I/O 2013 avant l'ouverture des inscriptions à la conférence, nous avons développé une série d'expériences et de jeux axés sur les interactions tactiles, l'audio génératif et le plaisir de la découverte. Inspirée du potentiel du code et de la puissance du jeu, cette expérience interactive commence par les sons simples des lettres "I" et "O" lorsque vous appuyez sur le nouveau logo I/O.

Organic Motion

Nous avons décidé d'implémenter les animations I et O avec un effet organique et bancal qui n'est pas souvent vu dans les interactions HTML5. Il a fallu un certain temps pour régler les options afin de rendre l'expérience amusante et réactive.

Exemple de code de physique rebondissante

Pour obtenir cet effet, nous avons utilisé une simulation physique simple sur une série de points représentant les bords des deux formes. Lorsque vous appuyez sur l'une des formes, tous les points s'éloignent de l'emplacement de l'appui. Ils s'étirent et s'éloignent avant d'être ramenés à l'intérieur.

Lors de l'instanciation, chaque point reçoit une accélération aléatoire et un "rebondissement" afin qu'ils ne s'animent pas de manière uniforme, comme vous pouvez le voir dans ce code :

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

Ensuite, lorsqu'ils sont enfoncés, ils sont accélérés vers l'extérieur à partir de la position de l'enfoncement à l'aide du code suivant :

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

Enfin, chaque particule est décélérée à chaque frame et revient lentement à l'équilibre avec cette approche en code :

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

Démo du mouvement naturel

Voici le mode d'accueil d'entrée/sortie que vous pouvez tester. Nous avons également exposé un certain nombre d'options supplémentaires dans cette implémentation. Si vous activez l'option "Afficher les points", vous verrez les points individuels sur lesquels agissent la simulation et les forces physiques.

Refonte

Une fois que nous avons été satisfaits du mouvement du mode Accueil, nous avons voulu utiliser le même effet pour deux modes rétro : Eightbit et Ascii.

Pour effectuer ce changement de couleur, nous avons utilisé le même canevas que celui du mode d'accueil et les données de pixel pour générer chacun des deux effets. Cette approche rappelle un nuanceur de fragment OpenGL, où chaque pixel de la scène est inspecté et manipulé. Examinons cela plus en détail.

Exemple de code "Nuanceur" du canevas

Les pixels sur un canevas peuvent être lus à l'aide de la méthode getImageData. Le tableau renvoyé contient quatre valeurs par pixel, représentant la valeur RGBA de chaque pixel. Ces pixels sont assemblés dans une structure massive semblable à un tableau. Par exemple, un canevas de 2 x 2 comporte 4 pixels et 16 entrées dans son tableau imageData.

Notre canevas est en plein écran. Par conséquent, si nous prétendons que l'écran est en 1 024 x 768 (comme sur un iPad), le tableau contient 3 145 728 entrées. Comme il s'agit d'une animation, l'ensemble de ce tableau est mis à jour 60 fois par seconde. Les moteurs JavaScript modernes peuvent gérer les boucles et agir sur une telle quantité de données assez rapidement pour maintenir la fréquence d'images. Conseil: n'essayez pas de consigner ces données dans la Play Console, car cela pourrait ralentir l'exploration de votre navigateur ou le faire planter.

Voici comment notre mode Eightbit lit le canevas du mode Chez moi et fait exploser les pixels pour créer un effet de blocs:

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

Démo du nuanceur 8 bits

En dessous, nous supprimons la superposition Eightbit et voyons l'animation d'origine en dessous. L'option "Écran de fermeture" vous montre un effet étrange que nous avons découvert en échantillonnant de manière incorrecte les pixels sources. Nous l'avons finalement utilisé comme œuf de Pâques "adaptatif" lorsque le mode Eightbit est redimensionné pour des formats inhabituels. Bon accident !

Compositing du canevas

Les possibilités offertes par la combinaison de plusieurs étapes de rendu et de masques sont assez incroyables. Nous avons créé une métaballe 2D qui nécessite que chaque balle ait son propre dégradé radial et que ces dégradés soient mélangés là où les balles se chevauchent. (Vous pouvez le voir dans la démonstration ci-dessous.)

Pour ce faire, nous avons utilisé deux canevas distincts. Le premier canevas calcule et dessine la forme de la métaballe. Un deuxième canevas dessine des dégradés radiaux à chaque position de la balle. La forme masque ensuite les dégradés, et nous affichons le résultat final.

Exemple de code de composition

Voici le code qui permet de tout faire :

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

Configurez ensuite le canevas pour le masquage et dessinez :

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

Conclusion

La variété des techniques que nous avons pu utiliser et les technologies que nous avons implémentées (telles que Canvas, SVG, CSS Animation, JS Animation, Web Audio, etc.) ont rendu le projet incroyablement amusant à développer.

Il y a bien plus à explorer que ce que vous voyez ici, même. Appuyez de manière répétée sur le logo I/O. Si vous saisissez la bonne séquence, vous débloquerez d'autres mini-expériences, jeux, visuels psychédéliques et peut-être même des aliments pour le petit-déjeuner. Nous vous suggérons de l'essayer sur votre smartphone ou votre tablette pour une expérience optimale.

Voici une combinaison pour vous lancer : O-I-I-I-I-I-I-I. Essayez-la dès maintenant : google.com/io

Open Source

Nous avons rendu notre code Open Source sous licence Apache 2.0. Vous pouvez le trouver sur notre site GitHub à l'adresse http://github.com/Instrument/google-io-2013.

Crédits

Développeurs :

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

Concepteurs :

  • Dan Schechter
  • Marron taupe
  • Kyle Beck

Producteurs :

  • Amie Pascal
  • Andrea Nelson