案例研究 - 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 家庭模式。在此实现中,我们还公开了许多其他选项。如果您打开“显示点”,则会看到物理模拟和力的作用单个点。

换肤

在对 Home 模式动作感到满意后,我们希望将同样的效果用于两种复古模式:Eightbit 和 Ascii。

为了实现这种换肤,我们在 Home 模式下使用同一个画布,并使用像素数据来生成两种效果。这种方法让人想起 OpenGL fragment 着色器,在这种着色器中,场景的每个像素都经过检查和操作。我们来深入探讨一下。

画布“着色器”代码示例

您可以使用 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 叠加层,在其下方看到原始动画。“终止屏幕”选项将向您展示一种奇怪的效果,这是我们因对源像素进行错误采样而偶然发现的。当 Eightbit 模式的大小调整为不可能的宽高比时,我们最终将它用作“自适应”复活节彩蛋。祝你好运!

画布合成

通过组合多个渲染步骤和蒙版可以实现的功能非常惊人。我们构建了一个 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);

总结

我们使用的各种技术和实现的技术(例如画布、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

赠金

开发者:

  • 托马斯·雷诺兹
  • 布赖恩·赫夫特
  • 斯蒂凡尼·海切尔
  • 保罗·法宁

设计师:

  • 丹·谢克特
  • 灰棕色
  • 凯尔·贝克

制作人:

  • 艾米·帕斯卡
  • 安德烈·尼尔森