Introducción
Para despertar el 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 experimentos y juegos centrados en los dispositivos móviles, centrados en las interacciones táctiles, el audio generativo y la alegría de descubrir. 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 del toque usando este 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 En casa de E/S para que juegues. 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.
Refinamiento
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. Profundicemos en esto.
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 el valor RGBA de cada píxel. 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 JavaScript modernos pueden controlar bucles y actuar sobre esa cantidad de datos lo suficientemente rápido para mantener la velocidad de fotogramas constante. (Sugerencia: no intentes registrar esos datos en la consola de 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 Eightbit Shader
A continuación, quitamos la superposición de Eightbit y vemos la animación original a continuación. La opción "Finalizar pantalla" te mostrará un efecto extraño con el que nos encontramos al muestrear incorrectamente los píxeles de origen. Terminamos usándolo como un huevo de pascua "responsivo" cuando se cambia el tamaño del modo Eightbit a relaciones de aspecto poco probables. ¡Suerte con el accidente!
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 procesamos 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 CSS, animación JS, audio web, etc.) hicieron que el desarrollo del proyecto fuera muy divertido.
Incluso hay mucho más por explorar de 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ébalo 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