Studium przypadku – eksperyment Google I/O 2013

Thomas Reynolds
Thomas Reynolds

Wstęp

Aby wzbudzić zainteresowanie deweloperów witryną Google I/O 2013 jeszcze przed otwarciem rejestracji na konferencję, opracowaliśmy serię eksperymentów i gier dostosowanych do urządzeń mobilnych, skupiających się na interakcjach dotykowych, dźwięku generatywnym i radości z odkrywania treści. Ta interaktywna gra inspirowana potencjałem kodu i siły gry rozpoczyna się od prostych dźwięków „I” i „O”, gdy klikasz nowe logo I/O.

Ruch bezpłatny

Postanowiliśmy wdrożyć animacje I i O w chwiejącym, naturalnym efektie, który nie jest często spotykany w interakcjach HTML5. Wybieranie różnych opcji w celu urozmaicenia rozmowy i reagowania na nią zajęło trochę czasu.

Przykład kodu fizyki sprężystości

W tym celu przeprowadziliśmy prostą symulację fizyczną na serii punktów reprezentujących krawędzie obu kształtów. Po dotknięciu dowolnego kształtu wszystkie punkty zostaną przyspieszone od lokalizacji dotknięcia. Rozciągają się i rozciągają, zanim zostaną wciągnięte z powrotem.

Podczas tworzenia wystąpienia każdy punkt otrzymuje losową wartość przyspieszenia i rezygnuje z „odchylenia”, dzięki czemu nie są animowane równomiernie, jak widać w tym kodzie:

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

Następnie, po dotknięciu, następuje przyspieszenie od miejsca dotknięcia na zewnątrz, zgodnie z tym kodem:

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

Na koniec każda cząstka jest zwalniana w każdej klatce i powoli wraca do równowagi, stosując w kodzie takie podejście:

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

Wersja demonstracyjna bezpłatnego ruchu

Oto tryb domowy I/O, którym możesz się pobawić. Udostępniamy też kilka dodatkowych opcji w ramach tego wdrożenia. Jeśli włączysz funkcję „pokazuj punkty”, zobaczysz poszczególne punkty, na które działają symulacje i siły fizyczne.

Zmiana wyglądu

Kiedy wszyscy byliśmy zadowoleni z ruchu w trybie domowym, chcieliśmy zastosować ten sam efekt w dwóch trybach retro: Eightbit i Ascii.

W tym celu użyliśmy tego samego obszaru roboczego z trybu głównego i wykorzystaliśmy dane pikseli do wygenerowania każdego z 2 efektów. To podejście przypomina cieniowanie fragmentów OpenGL, w którym każdy piksel sceny jest badany i manipulowany. Przyjrzyjmy się bliżej tej kwestii.

Przykład kodu Canvas „Shader”

Piksele na płótnie można odczytywać za pomocą metody getImageData. Zwrócona tablica zawiera 4 wartości na piksel, które odpowiadają każdej wartości RGBA tego piksela. Te piksele są rozmieszczone w masywnej strukturze tablicy. Na przykład obiekt canvas o wymiarach 2 x 2 będzie mieć w tablicy imageData 4 piksele i 16 wpisów.

Odbitka na płótnie jest wyświetlana na pełnym ekranie, więc jeśli użyliśmy ekranu w rozdzielczości 1024 x 768 (jak na iPadzie), to tablica ma 3 145 728 wpisów. Ponieważ jest to animacja, cała tablica jest aktualizowana 60 razy na sekundę. Nowoczesne mechanizmy JavaScriptu są w stanie zapętlać i przetwarzać duże ilości danych na tyle szybko, aby utrzymać stałą liczbę klatek na sekundę. Wskazówka: nie loguj tych danych w konsoli programisty, ponieważ spowolni to indeksowanie Twojej przeglądarki lub jej całkowite awarię.

Oto jak nasz tryb ośmiobitowy odczytuje obiekt canvas i nadmuchuje piksele, by uzyskać efekt blokowy:

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

Wersja demonstracyjna ośmiobitowego Shadera

Poniżej usuwamy nakładkę ośmiobitową i widzimy oryginalną animację. Opcja „Zamknij ekran” pokazuje dziwny efekt, na który natknęliśmy się przez nieprawidłowe próbkowanie pikseli źródłowych. Wykorzystaliśmy ją jako „elastyczną” niespodziankę, po zmianie rozmiaru trybu 8bitowego na nieprawdopodobne proporcje obrazu. Szczęśliwego wypadku!

Komponowanie Canvas

Łącząc różne kroki renderowania i maski, możesz osiągnąć niesamowite, co możesz osiągnąć. Zbudowaliśmy metaball 2D, w którym każda kula musi mieć własny gradient promieniowy, a te gradienty muszą się ze sobą łączyć w miejscach, w których nakładają się kulki. Możesz to zobaczyć w poniższej prezentacji.

Wprowadziliśmy w tym celu 2 osobne obszary robocze. W pierwszym z nich obliczamy i rysujemy kształt metaballa. Drugie płótno rysuje promieniowe gradienty w każdym położeniu piłki. Następnie kształt maskuje gradienty i wyrenderuje się wynik końcowy.

Przykład kodu kompilacji

Oto kod, dzięki któremu wszystko działa:

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

Następnie skonfiguruj obszar roboczy pod kątem maskowania i rysowania:

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

Podsumowanie

Różnorodność technik i wdrożonych technologii (takich jak Canvas, SVG, CSS Animation, JS Animation, Web Audio itp.) sprawiła, że projekt był niezwykle przyjemny.

Do odkrycia jest o wiele więcej niż to, co tu widać. Klikaj logo I/O, a odpowiednie sekwencje odblokowują więcej minieksperymentów, gier, odważnych grafik, a nawet niektórych śniadaniowych potraw. Aby wygodniej korzystać z tej funkcji, zalecamy wypróbowanie jej na smartfonie lub tablecie.

Oto kombinacja na początek: O-I-I-I-I-I-I. Wypróbuj: google.com/io

Oprogramowanie typu open source

Udostępniliśmy kod licencji Apache 2.0 na licencji open source. Znajdziesz ją w naszym GitHubie: http://github.com/Instrument/google-io-2013.

Środki

Programiści:

  • Thomas Reynolds
  • Brian Hefter
  • Stefanie Hatcher
  • Paweł Farning

Projektanci:

  • Dan Schechter
  • Szarobrązowy
  • Kyle'a Becka

Producenci:

  • Amie Pascal
  • Andrea Nelson