個案研究 - 2013 年 Google I/O 大會實驗

湯瑪斯雷諾茲
Thomas Reynolds

引言

為協助開發人員在 2013 年 Google I/O 大會網站正式開幕之前引起開發人員的興趣,我們開發了一系列以行動裝置為主的實驗和遊戲,著重於觸控互動、生成式音訊,以及享受探索樂趣。這項互動體驗的靈感來自於程式碼的潛力,且強大的遊戲力量,會在輕觸新的 I/O 標誌時顯示「I」和「O」這類簡單的音效。

自然動作

我們決定以流暢自然的方式導入 I 和 O 動畫,而這在 HTML5 互動中通常不常見。多一點時間,

Bouncy 物理程式碼範例

為了達成這個效果,我們使用簡單的物理模擬來模擬代表兩個形狀的邊緣。輕觸任一形狀時,所有點都會從輕觸位置加速。他們再度伸展四肢之前,

在每個點執行個體化時,每個點都會獲得隨機的加速度金額,並且會重新繫結「獎勵」,因此不會統一套用動畫效果,如以下程式碼所示:

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

然後在輕觸時,使用以下程式碼,系統就會從輕觸的位置向外加速:

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

最後,每個影格的每顆粒子都會減少,並在程式碼中慢慢回到平衡:

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

自然動作示範

現在就播放 I/O 大會的主畫面模式吧!另外,我們在這項實作項目中也提供了很多其他選項。如果開啟「顯示點數」,你會看到物理模擬和力隊正在執行的個別點數。

再生

假如我們已對在家模式的運動做出調整,我們要對八位元和 Ascii 兩種懷舊模式使用相同的特效。

為了達到這種畫面外觀,我們使用居家模式中的相同畫布,並使用像素資料產生兩種效果。這種做法會回收一個 OpenGL 片段著色器,其中每個像素的像素都會經過檢查和操作。讓我們來深入探討這個問題

Canvas「著色器」程式碼範例

您可以使用 getImageData 方法讀取畫布上的像素。傳回的陣列包含 4 個每個像素的值,代表每個像素 RGBA 值。這些像素會聚集在類似陣列的架構中。舉例來說,2x2 畫布的 imageData 陣列中會有 4 像素,而 16 個項目。

我們的畫布是全螢幕,因此如果我們假設螢幕是 1024x768 (例如在 iPad 上),則陣列包含 3,145,728 個項目。由於這是動畫,因此整個陣列每秒更新 60 次。新型 JavaScript 引擎可以快速處理重複資料,並對這些資料採取行動,藉此維持影格速率的一致性。(提示:請勿將資料記錄到開發人員控制台,否則會導致瀏覽器無法檢索或完全當機)。

以下為八位元模式如何讀取居家模式畫布,然後高舉像素來提高方塊效果:

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 著色器示範

下方,我們會去除 8 位元疊加層,然後在下方查看原始動畫。「殺害畫面」選項會錯誤地對來源像素進行取樣,從而顯示令人不解的效果。當八位元模式重新調整為不可能的顯示比例時,我們最後就會把它當做「回應式」彩蛋使用。事故愉快!

畫布合成

這個功能結合了多個轉譯步驟和遮罩,可達到的效果。我們打造了 2D 元球,因此每個球體都有各自的放射漸層,而且這些漸層在球發生重疊的位置必須混合。(您可在下方的示範中看見這一點)。

為此,我們使用兩個獨立的畫布。第一個畫布會計算並繪製中繼球形狀。第二張畫布會在每個球體位置繪製放射漸層。接著,形狀會遮蓋漸層,並轉譯最終輸出內容。

撰寫程式碼範例

以下是完成上述所有動作的程式碼:

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

接著,將畫布設定遮蓋及繪圖:

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

結語

我們得以運用各式各樣的技術,以及我們導入的技術 (例如 Canvas、SVG、CSS 動畫、JS 動畫、網路音訊等),讓這個專案在開發過程中變得無比樂趣。

除了在這裡,你也能探索更多豐富功能。只要持續輕觸 I/O 標誌,正確的步驟就能解鎖更多迷你實驗、遊戲、三星視覺畫面,甚至一些早餐美食。建議你在智慧型手機或平板電腦上試用此版本,以獲得最佳體驗。

以下組合有助您踏出第一步:O-I-I-I-I-I-I-I。立即試用:google.com/io

開放原始碼

我們已開放程式碼 Apache 2.0 授權。您可以在 GitHub 中找到這項工具:http://github.com/Instrument/google-io-2013

抵免額

開發人員:

  • 湯瑪斯雷諾茲
  • 布萊恩海夫特 (Brian Hefter)
  • 史蒂芬妮哈特爾
  • 保羅法寧

設計師:

  • 施徹特 (Dan Schechter)
  • 鼠尾草布朗
  • 凱爾貝克

製作人:

  • 阿米帕斯卡
  • 尼爾森 (Andrea Nelson)