Caso de éxito: Experimento de Google I/O 2013

Tomás Reynolds
Thomas Reynolds

Introducción

Para generar interés de los desarrolladores en el sitio web de Google I/O 2013 antes de que se inscribiera en la conferencia, desarrollamos una serie de juegos y experimentos para dispositivos móviles que se centran en las interacciones táctiles, el audio generativo y la alegría del descubrimiento. Esta experiencia interactiva, inspirada en el potencial del código y el poder del juego, comienza con sonidos simples de la "yo" y la "o" cuando presionas el nuevo logotipo de I/O.

Movimiento orgánico

Decidimos implementar las animaciones de I y O en un efecto orgánico y inestable que no se ve con frecuencia en las interacciones HTML5. Marcar las opciones para hacerla parecer divertida y reactiva llevó un poco de tiempo.

Ejemplo de código de física Bouncy

Para lograr este efecto, usamos una simulación física simple en una serie de puntos que representan los bordes de las dos formas. Cuando se presiona cualquiera de las formas, se aceleran todos los puntos desde la ubicación en la que se realizó el toque. Se estiran y se estiran antes de volver a retraerlos.

En la creación de la instancia, cada punto obtiene un importe de aceleración aleatorio y rebote un "rebote" para que no se animen de manera uniforme, como puedes ver en este código:

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

Luego, cuando se presionan, se aceleran hacia afuera desde la posición de toque con el siguiente código:

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

Por último, cada partícula se desacelera en cada fotograma y regresa lentamente al equilibrio con este enfoque en el código:

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

Demostración de movimiento orgánico

Aquí está el modo En casa de I/O para que juegues. También expusimos muchas opciones adicionales en esta implementación. Si activas "mostrar puntos", verás los puntos individuales sobre los que actúan las fuerzas y la simulación física.

Redefinición

Una vez que estábamos contentos con el movimiento del modo En casa, quisimos usar ese mismo efecto para dos modos retro: Eightbit y Ascii.

Para lograr esto, usamos el mismo lienzo del modo En casa y usamos los datos de píxeles para generar cada uno de los dos efectos. Este enfoque recuerda a un sombreador de fragmentos de OpenGL en el que se inspecciona y manipula cada píxel de la escena. Analicémoslo un poco más.

Ejemplo de código de "Shader" de Canvas

Los píxeles de un lienzo se pueden leer con el método getImageData. El array que se muestra contiene 4 valores por píxel que representan cada valor RGBA de cada píxel. Estos píxeles se encadenan en una gran estructura similar a un array. Por ejemplo, un lienzo de 2x2 tendría 4 píxeles y 16 entradas en su array imageData.

Nuestro lienzo es en pantalla completa, por lo que si suponemos que la pantalla es de 1024 x 768 (como en un iPad), entonces el array tiene 3,145,728 entradas. Debido a que se trata de una animación, todo el array se actualiza 60 veces por segundo. Los motores JavaScript modernos pueden controlar los bucles y utilizar estos datos con la rapidez suficiente para mantener la coherencia de la velocidad de fotogramas. (Sugerencia: No intentes registrar esos datos en la consola para desarrolladores, ya que ralentizará el navegador o lo bloqueará por completo).

Este es el modo en que el modo Eightbit lee el lienzo del modo de inicio y aumenta los píxeles para lograr un efecto más bloqueador:

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

Demostración de Eightbit Shader

A continuación, quitamos la superposición de Eightbit y vemos la animación original debajo. La opción "kill screen" te mostrará un efecto extraño con el que nos topamos al muestrear incorrectamente los píxeles de origen. Lo usamos como un huevo de pascua "responsivo" cuando se cambia el tamaño del modo Eightbit a relaciones de aspecto improbables. ¡Feliz accidente!

Composición de lienzo

Es sorprendente lo que puedes lograr combinando varios pasos y máscaras de renderización. Creamos una metabola 2D que requiere que cada bola tenga su propio gradiente radial y esos gradientes se combinen donde se superpongan. (Puedes ver esto en la demostración que aparece a continuación).

Para lograrlo, usamos dos lienzos independientes. El primer lienzo calcula y dibuja la forma de la metabola. Un segundo lienzo dibuja gradientes radiales en cada posición de la bola. Luego, la forma enmascara los gradientes y se renderiza el resultado final.

Ejemplo de código de composición

Este es el código que hace que todo suceda:

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

Luego, configura el lienzo para enmascarar y dibujar:

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

Conclusión

La variedad de técnicas que usamos y las tecnologías que implementamos (como lienzo, SVG, animación CSS, JS Animation, Web Audio, etc.) hicieron que desarrollar el proyecto fuera increíblemente divertido.

Incluso hay mucho más para explorar de lo que ves aquí. Sigue presionando el logotipo de I/O y las secuencias correctas desbloquearán más miniexperimentos, juegos, imágenes alucinantes y, tal vez, algunos alimentos para el desayuno. Te recomendamos que las pruebes en tu smartphone o tablet para obtener la mejor experiencia.

Esta es una combinación para comenzar: O-I-I-I-I-I-I. Pruébalo ahora: google.com/io

Código abierto

La licencia de código Apache 2.0 se creó con código abierto. Puedes encontrarlo en nuestro GitHub en: http://github.com/Instrument/google-io-2013.

Créditos

Desarrolladores:

  • Tomás Reynolds
  • Brian Hefter
  • Stefanie Habcher
  • Pablo Farning

Diseñadores:

  • Dan Schechter
  • Marrón salvia
  • Kyle Beck

Los productores:

  • Amie Pascal
  • Andrea Nelson