案例研究 - 2013 年 Google I/O 大会实验

Thomas Reynolds
Thomas Reynolds

简介

为了在 Google I/O 2013 大会注册开始之前吸引开发者访问 Google I/O 2013 网站,我们开发了一系列以移动设备为先的实验和游戏,重点关注触控互动、生成式音频和探索的乐趣。这款互动式体验的灵感来源于代码的潜力和游戏的力量,首先会播放简单的“I”和“O”声音(当您点按新的 I/O 大会徽标时)。

自然运动

我们决定以 HTML5 互动中不常见的摇摆自然效果来实现 I 和 O 动画。拨号加入选项,让游戏更有趣味且反应迅速,这需要花点时间。

弹跳物理代码示例

为了实现此效果,我们对表示这两个形状边缘的一系列点使用了简单的物理模拟。点按任一形状后,所有点都会从点按位置加速飞出。它们会先伸展开来,然后再收回。

在实例化时,每个点都会获得随机的加速度量和回弹“弹跳”,因此它们的动画效果不会一致,如以下代码所示:

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 主屏幕模式。我们还在此实现中公开了许多其他选项。如果开启“显示点”,您将看到物理模拟和力作用于的各个点。

换肤

对“在家”模式动作感到满意后,我们希望将同样的效果用于两种怀旧模式:Eightbit 和 Ascii。

为了实现这种换肤效果,我们使用了与主屏幕模式相同的画布,并使用像素数据来生成这两种效果。这种方法让人联想到 OpenGL fragment 着色器,其中对场景的每个像素都进行了检查和操作。我们来深入了解一下。

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

八位着色器演示

在下面,我们将删除 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);

总结

我们使用的各种技术和实现的技术(例如画布、SVG、CSS 动画、JS 动画、Web 音频等)让该项目的开发变得非常有趣。

不仅如此,这里还有许多内容值得探索。不断点按 I/O 徽标,正确的顺序可解锁更多迷你实验、游戏、迷幻视觉效果,甚至可能还能解锁一些早餐食品。建议您在智能手机或平板电脑上试用,以获得最佳体验。

下面这些组合可以助你入门:O-I-I-I-I-I-I-I。立即试用:google.com/io

开源

我们已开放 Apache 代码 Apache 2.0 许可的源代码。您可以在我们的 GitHub 上找到该演讲:http://github.com/Instrument/google-io-2013

赠金

开发者:

  • 托马斯·雷诺兹
  • Brian Hefter
  • Stefanie Hatcher
  • 保罗·法宁 (Paul Farning)

设计师:

  • Dan Schechter
  • 鼠尾草棕色
  • Kyle Beck

生产者:

  • 阿米·帕斯卡 (Amie Pascal)
  • Andrea Nelson