Fallstudie – Experiment der Google I/O 2013

Reynolds
Thomas Reynolds

Einleitung

Um vor Beginn der Konferenzregistrierung das Entwicklerinteresse an der Google I/O 2013-Website zu wecken, haben wir eine Reihe von Tests und Spielen für Mobilgeräte entwickelt, bei denen es um Touch-Interaktionen, generatives Audio und die Freude am Entdecken ging. Inspiriert vom Potenzial von Code und Spielen beginnt dieses interaktive Erlebnis mit den einfachen Klängen von „I“ und „O“, wenn man auf das neue I/O-Logo tippt.

Organische Bewegung

Wir haben beschlossen, die I- und O-Animationen in einem wackeligen, organischen Effekt zu implementieren, der bei HTML5-Interaktionen nicht oft zu beobachten ist. Es hat etwas Zeit in Anspruch genommen, die Optionen auszuwählen, damit es Spaß machen und reaktiv sein kann.

Codebeispiel für Bouncy Physics

Um diesen Effekt zu erzielen, haben wir eine einfache physikalische Simulation an einer Reihe von Punkten verwendet, die die Kanten der beiden Formen darstellen. Beim Antippen einer der Punkte werden alle Punkte aus der Stelle des Tippens herausbeschleunigt. Sie dehnen sie aus und weg, bevor sie wieder hineingezogen werden.

Bei der Instanziierung erhält jeder Punkt einen zufälligen Beschleunigungsbetrag und eine Rebound-"Hülle", sodass sie nicht gleichmäßig animiert werden, wie Sie in diesem Code sehen können:

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

Wenn sie dann angetippt werden, werden sie mit dem folgenden Code aus der Position des Tippens herausbeschleunigt:

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

Schließlich wird jedes Partikel in jedem Frame verzögert und kehrt langsam mit diesem Codeansatz ins Gleichgewicht zurück:

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

Demo für organische Bewegung

Hier ist der I/O-Zuhausemodus, mit dem du spielen kannst. Bei dieser Implementierung werden auch eine Reihe zusätzlicher Optionen vorgestellt. Wenn Sie „Punkte anzeigen“ aktivieren, werden die einzelnen Punkte angezeigt, auf die sich die physikalische Simulation und Kräfte auswirken.

Umhauen

Sobald wir mit der Bewegung im Home-Modus zufrieden waren, wollten wir denselben Effekt für zwei Retro-Modi verwenden: den 8-Bit- und den Ascii-Modus.

Für diese Umgestaltung haben wir denselben Canvas aus dem Startmodus verwendet und die Pixeldaten verwendet, um jeden der beiden Effekte zu generieren. Dieser Ansatz ähnelt einem OpenGL-Fragment-Shader, bei dem jedes Pixel der Szene untersucht und manipuliert wird. Sehen wir uns das einmal genauer an.

Canvas-Codebeispiel für „Shader“

Pixel auf einem Canvas können mit der Methode getImageData gelesen werden. Das zurückgegebene Array enthält vier Werte pro Pixel, die für jeden RGBA-Wert jedes Pixels stehen. Diese Pixel sind in einer riesigenarray-ähnlichen Struktur aneinandergereiht. Ein 2 x 2-Canvas hätte beispielsweise 4 Pixel und 16 Einträge in seinem imageData-Array.

Unser Canvas ist im Vollbildmodus. Wenn wir also von einer Größe von 1024 x 768 ausgehen (wie bei einem iPad), hat das Array 3.145.728 Einträge. Da es sich um eine Animation handelt, wird das gesamte Array 60-mal pro Sekunde aktualisiert. Moderne JavaScript-Engines können Schleifen verarbeiten und diese vielen Daten schnell genug verarbeiten, um die Framerate einheitlich zu halten. Tipp: Versuchen Sie nicht, diese Daten in der Entwicklerkonsole zu protokollieren, da Ihr Browser sonst langsamer gecrawlt wird oder vollständig zum Absturz führt.

Im 8-Bit-Modus wird der Canvas für den Startmodus wie folgt vorgelesen und die Pixel optimiert, um einen Blockiereffekt zu erzielen:

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

Eightbit-Shader-Demo

Unten entfernen wir das Eightbit-Overlay und sehen die Originalanimation darunter. Die Option "Bildschirm löschen" zeigt einen seltsamen Effekt an, der uns durch das falsche Sampling der Quellpixel aufgefallen ist. Wir haben es als „responsives“ Easter Egg verwendet, als der Eightbit-Modus auf ungewöhnliche Seitenverhältnisse angepasst wurde. Viel Glück beim Unfall!

Canvas-Zusammenstellung

Es ist ziemlich erstaunlich, was Sie durch die Kombination mehrerer Renderingschritte und Masken erreichen können. Wir haben einen 2D-Metaball entwickelt, bei dem jede Kugel einen eigenen radialen Farbverlauf hat und diese Farbverläufe dort überlappen, wo sich die Bälle überschneiden. (Sie können dies in der Demo unten sehen.)

Dazu haben wir zwei separate Canvases verwendet. Im ersten Canvas wird die Metaball-Form berechnet und gezeichnet. Auf einer zweiten Canvas werden an jeder Ballposition radiale Farbverläufe dargestellt. Dann maskiert die Form die Farbverläufe und wir rendern die endgültige Ausgabe.

Codebeispiel für Zusammensetzung

Hier ist der Code, mit dem alles möglich ist:

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

Richten Sie dann den Canvas zum Maskieren und Zeichnen ein:

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

Fazit

Durch die Vielzahl von Techniken, die wir verwenden, und die von uns implementierten Technologien (wie Canvas, SVG, CSS Animation, JS Animation, Web Audio usw.) macht die Entwicklung des Projekts unglaublich viel Spaß.

Selbst wenn du hier bist, gibt es noch viel mehr zu entdecken. Wenn du weiter auf das I/O-Logo tippst, werden durch die richtigen Reihenfolgen weitere Miniexperimente, Spiele, skurrile Bilder und vielleicht sogar Frühstücksspeisen freigeschaltet. Am besten probierst du es auf deinem Smartphone oder Tablet aus.

Hier ist eine Kombination für den Einstieg: O-I-I-I-I-I-I-I. Jetzt ausprobieren: google.com/io

Open Source

Unser Code Apache 2.0 ist als Open Source verfügbar. Sie finden es auf unserem GitHub unter http://github.com/Instrument/google-io-2013.

Guthaben

Entwickler:

  • Reynolds
  • Benjamin Hefter
  • Stefanie-Früchte
  • Paul Farning

Designschaffende:

  • Logo: Dan Schechter
  • Salbeibraun
  • Beck

Produzenten:

  • Amie Pascal
  • Andrea Wagner