Estudo de caso: Google I/O 2013 Experiment

Thomas reynolds
Thomas Reynolds

Introdução

Para aumentar o interesse dos desenvolvedores no site do Google I/O 2013 antes do início das inscrições para a conferência, desenvolvemos uma série de experimentos e jogos para dispositivos móveis com foco em interações por toque, áudio generativo e a alegria da descoberta. Inspirada no potencial do código e no poder da diversão, essa experiência interativa começa com os sons simples de "I" e "O" ao tocar no novo logotipo do I/O.

Movimento orgânico

Decidimos implementar as animações de E e O em um efeito orgânico e instável que nem sempre é visto em interações do HTML5. Ligar para as opções para tornar o jogo divertido e reativo demorou um pouco.

Exemplo de código de física do Bouncy

Para conseguir esse efeito, usamos uma simulação física simples em uma série de pontos representando as bordas das duas formas. Quando um usuário toca em uma das formas, todos os pontos são acelerados a partir do local do toque. Eles se esticam e se afastam antes de serem puxados de volta.

Na instanciação, cada ponto recebe uma quantidade de aceleração aleatória e um "rebote" de rebote para que não sejam animados de maneira uniforme, como você pode conferir neste 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);
}

Depois, quando tocados, eles são acelerados para fora da posição do 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 fim, cada partícula é desacelerada em cada frame e lentamente retorna ao equilíbrio com esta abordagem no 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']);
}

Demonstração de movimento orgânico

Aqui está o modo casa de E/S para você brincar. Também mostramos várias opções adicionais nesta implementação. Se você ativar "mostrar pontos", verá cada ponto em que a simulação física e as forças estão agindo.

Reskning

Quando ficamos satisfeitos com o movimento do modo casa, queríamos usar esse mesmo efeito para dois modos retrô: Eightbit e Ascii.

Para isso, usamos a mesma tela do modo inicial e os dados de pixel para gerar cada um dos dois efeitos. Essa abordagem se lembra de um sombreador de fragmento do OpenGL em que cada pixel da cena é inspecionado e manipulado. Vamos nos aprofundar nisso.

Exemplo de código para "Shader" da tela

Os pixels em uma tela podem ser lidos usando o método getImageData. A matriz retornada contém 4 valores por pixel, representando cada valor RGBA de pixels. Esses pixels são unidos em uma enorme estrutura semelhante a uma matriz. Por exemplo, uma tela 2x2 teria 4 pixels e 16 entradas em sua matriz imageData.

Nossa tela está em tela cheia, então se fingirmos que ela tem 1024 x 768 (como em um iPad), a matriz tem 3.145.728 entradas. Como esta é uma animação, a matriz inteira é atualizada 60 vezes por segundo. Mecanismos JavaScript modernos podem lidar com repetições e agir com base nesse volume de dados com rapidez suficiente para manter o frame rate consistente. Dica: não tente registrar esses dados no console para desenvolvedores, porque isso atrasará o rastreamento do seu navegador ou travará totalmente.

Veja como nosso modo de 8bits lê a tela do modo home e aumenta os pixels para criar um efeito de bloco.

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

Demonstração do sombreador de oito bits

A seguir, removemos a sobreposição de 8 bits e vemos a animação original nela. A opção "kill screen" mostrará um efeito estranho que encontramos realizando a amostragem incorreta dos pixels de origem. Acabamos usando-o como um easter egg "responsivo" quando o modo Eightbit é redimensionado para proporções improváveis. Feliz acidente!

Criação de tela

É incrível o que você pode realizar combinando várias etapas e máscaras de renderização. Criamos um metaball 2D que exige que cada bola tenha seu próprio gradiente radial, e esses gradientes devem ser misturados onde as bolas se sobrepõem. Isso pode ser visto na demonstração abaixo.

Para fazer isso, usamos duas telas separadas. O primeiro canvas calcula e desenha a forma do metaball. Uma segunda tela desenha gradientes radiais em cada posição da bola. Em seguida, a forma mascara os gradientes e renderizamos a saída final.

Exemplo de código de composição

Veja o código que faz tudo acontecer:

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

Depois configure a tela para mascarar e desenhar:

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

Conclusão

A variedade de técnicas que usamos e as tecnologias que implementamos (como Canvas, SVG, animação CSS, animação JS, Web Audio etc.) tornou o projeto incrivelmente divertido de desenvolver.

Há muito mais para explorar do que o que você vê aqui. Continue tocando no logotipo da I/O e as sequências corretas desbloqueiam mais miniexperimentos, jogos, visuais divertidos e talvez até alguns pratos para o café da manhã. Sugerimos que você teste no seu smartphone ou tablet para ter a melhor experiência.

Aqui está uma combinação para começar: O-I-I-I-I-I-I. Faça um teste: google.com/io

Código aberto

Abrimos o código da licença Apache 2.0. Você pode encontrá-la em nosso GitHub em: http://github.com/Instrument/google-io-2013.

Créditos

Desenvolvedores:

  • Thomas reynolds
  • Brian Hefter
  • Stefanie hatcher
  • Paul farning

Designers:

  • Dan schechter
  • Eucalipto
  • Kyle beck

Produtores:

  • Amie pascal
  • Andrea nelson