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

Thomas Reynolds
Thomas Reynolds

Introducción

Para generar interés entre los desarrolladores en el sitio web de Google I/O 2013 antes de que se abriera el registro para la conferencia, desarrollamos una serie de experimentos y juegos que priorizaban los dispositivos móviles y se enfocaban en las interacciones táctiles, el audio generativo y la alegría del descubrimiento. Inspirada en el potencial del código y el poder del juego, esta experiencia interactiva comienza con los sonidos simples de "I" y "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 tambaleante que no se suele ver en las interacciones de HTML5. El ajuste de las opciones para que se sintiera divertido y reactivo llevó un poco de tiempo.

Ejemplo de código de física con rebote

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, todos los puntos se aceleran desde la ubicación de la presión. Se extienden y se alejan antes de volver a entrar.

En la creación de instancias, cada punto obtiene un valor de aceleración aleatorio y un "rebote" para que no se anime de forma 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 la presión 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 vuelve 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

Este es el modo de inicio de E/S para que lo pruebes. También exponemos muchas opciones adicionales en esta implementación. Si activas "Mostrar puntos", verás los puntos individuales sobre los que actúan la simulación de física y las fuerzas.

Redefinición

Una vez que quedamos conformes con el movimiento del modo de inicio, quisimos usar ese mismo efecto para dos modos retro: Eightbit y Ascii.

Para lograr este cambio de diseño, usamos el mismo lienzo del modo de inicio y 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. Analicemos esto con más detalle.

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 los píxeles. Estos píxeles se unen en una estructura masiva similar a un array. Por ejemplo, un lienzo de 2 × 2 tendría 4 píxeles y 16 entradas en su array imageData.

Nuestro lienzo es de pantalla completa, por lo que, si suponemos que la pantalla es de 1024 × 768 (como en un iPad), el array tiene 3,145,728 entradas. Como se trata de una animación, todo este array se actualiza 60 veces por segundo. Los motores de JavaScript modernos pueden manejar bucles y actuar sobre esta gran cantidad de datos con la suficiente rapidez para mantener la velocidad de fotogramas coherente. (Sugerencia: no intentes registrar esos datos en Play Console, ya que ralentizarán tu navegador o lo harán fallar por completo).

A continuación, se muestra cómo nuestro modo Eightbit lee el lienzo del modo de inicio y aumenta los píxeles para tener un efecto más bloqueado:

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 sombreador de ocho bits

A continuación, quitamos la superposición de Eightbit y vemos la animación original debajo. La opción "Pantalla de eliminación" te mostrará un efecto extraño que encontramos cuando tomamos muestras incorrectas de los píxeles de origen. Al final, lo usamos como un huevo de Pascua "responsivo" cuando se cambia el tamaño del modo Eightbit a relaciones de aspecto poco probables. ¡Suerte!

Compaginación de Canvas

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

Para lograrlo, usamos dos lienzos separados. El primer lienzo calcula y dibuja la forma de metaball. Un segundo lienzo dibuja gradientes radiales en cada posición de la bola. Luego, la forma enmascara los gradientes y renderizamos 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 pudimos usar y las tecnologías que implementamos (como Canvas, SVG, animación de CSS, animación de JS, audio web, etc.) hicieron que el desarrollo del proyecto fuera muy divertido.

Hay mucho más para explorar que lo que ves aquí. Sigue presionando el logotipo de I/O y, con las secuencias correctas, se desbloquearán más miniexperimentos, juegos, imágenes alucinantes y, tal vez, incluso algunos alimentos para el desayuno. Te sugerimos que lo 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-I. Pruébala ahora: google.com/io

Código abierto

Convertimos nuestro código en código abierto con la licencia Apache 2.0. Puedes encontrarla en nuestro GitHub: http://github.com/Instrument/google-io-2013.

Créditos

Desarrolladores:

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

Diseñadores:

  • Dan Schechter
  • Verde salvia
  • Kyle Beck

Productores:

  • Amie Pascal
  • Andrea Nelson