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

Thomas Reynolds
Thomas Reynolds

Introduction

Afin de susciter l'intérêt des développeurs pour le site Web Google I/O 2013 avant l'ouverture des inscriptions, nous avons développé une série d'expériences et de jeux axés sur le mobile 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 du pouvoir du jeu, cette expérience interactive commence par les sons simples de "I" et "O" lorsque vous appuyez sur le nouveau logo I/O.

Mouvement naturel

Nous avons décidé d'intégrer les animations E/S et O Appeler les options pour qu'il se sente amusant et réactif a pris un peu de temps.

Exemple de code de physique rebondissante

Pour obtenir cet effet, nous avons utilisé une simple simulation physique sur une série de points représentant les arêtes des deux formes. Lorsque l'utilisateur appuie sur l'une des formes, tous les points sont accélérés en dehors de l'endroit où l'utilisateur appuie dessus. Ils s'étirent et s'éloignent avant d'être retirés.

Lors d'une instanciation, chaque point reçoit une valeur d'accélération aléatoire et un rebond ("rebond" ou "rebond") de sorte 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);
}

Lorsqu'un utilisateur appuie dessus, il est accéléré vers l'extérieur à partir de la position de l'appui à l'aide du code ci-dessous:

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 ralentie sur chaque frame et retrouve lentement son équilibre avec cette approche dans le 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 Chez moi d'E/S. Nous avons également présenté un certain nombre d'options supplémentaires pour cette implémentation. Si vous activez l'option "Afficher les points", les points individuels sur lesquels agissent la simulation physique et les forces s'affichent.

Repeindre

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

Pour effectuer ce recadrage, nous avons utilisé le même canevas que celui du mode Chez moi et nous avons généré les deux effets à l'aide des données de pixels. Cette approche rappelle un nuanceur de fragments OpenGL dans lequel chaque pixel de la scène est inspecté et manipulé. Examinons cela plus en détail.

Exemple de code pour le nuanceur de canevas

Les pixels d'un canevas peuvent être lus à l'aide de la méthode getImageData. Le tableau renvoyé contient 4 valeurs par pixel représentant la valeur RVBA de chaque pixel. Ces pixels sont reliés entre eux dans une immense structure semblable à un tableau. Par exemple, un canevas 2x2 possède 4 pixels et 16 entrées dans son tableau imageData.

Notre canevas est en plein écran. Par conséquent, si nous imaginons que l'écran fait 1 024 x 768 pixels (comme sur un iPad), le tableau comporte 3 145 728 entrées. Comme il s'agit d'une animation, l'ensemble du tableau est mis à jour 60 fois par seconde. Les moteurs JavaScript modernes peuvent gérer les boucles et agir sur autant de données suffisamment rapidement pour maintenir la cohérence de la fréquence d'images. Conseil: n'essayez pas d'enregistrer ces données dans la Play Console, car cela ralentit l'exploration de votre navigateur ou le fait planter.

Voici comment le mode Eightbit lit le canevas du mode maison et fait gonfler les pixels pour créer un effet bloquant:

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émonstration du nuanceur Eightbit

En dessous, nous supprimons la superposition Eightbit et voyons l'animation d'origine en dessous. L'option "Fermer l'écran" vous permet d'afficher un effet étrange sur lequel nous avons découvert un échantillonnage incorrect des pixels sources. Nous avons fini par l'utiliser comme un easter egg "réactif" lorsque le mode Eightbit a été redimensionné pour des formats improbables. Bon accident !

Composition de canevas

C'est assez incroyable ce que vous pouvez accomplir en combinant plusieurs étapes et masques de rendu. Nous avons créé un métaball en 2D qui exige 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 voir cela dans la démo ci-dessous.)

Pour ce faire, nous avons utilisé deux canevas distincts. Le premier canevas calcule et dessine la forme du métaball. Un second canevas dessine des dégradés radiaux à chaque position de balle. Ensuite, la forme masque les dégradés et le résultat final est rendu.

Exemple de code de composition

Voici le code qui fait tout se produire:

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

Ensuite, configurez le canevas pour le masquage et le dessin:

// 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 utilisées et les technologies que nous avons mises en œuvre (comme Canvas, SVG, l'animation CSS, l'animation JS, 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. Continuez d'appuyer sur le logo de l'I/O : les séquences correctes vous permettront de débloquer plus de mini-expériences, de jeux, d'éléments visuels délirants et peut-être même de plats pour le petit-déjeuner. Nous vous conseillons de l'essayer sur votre smartphone ou votre tablette pour une expérience optimale.

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

Open Source

Nous avons publié notre licence de code Apache 2.0 en open source. Vous le trouverez sur notre 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 sauge
  • Kyle beck

Les producteurs :

  • Amie Pascal
  • Andrea Nelson