中土世界前端

多设备开发演示

Daniel Isaksson
Daniel Isaksson
Einar Öberg
Einar Öberg

在介绍 Chrome 实验性项目中土世界之旅开发的第一篇文章中,我们着重介绍了适用于移动设备的 WebGL 开发工作。在本文中,我们将讨论在创建 HTML5 前端的其余部分时所遇到的挑战、问题及解决方案。

同一网站的三个版本

首先,我们从屏幕尺寸和设备功能的角度探讨如何调整此实验,使其同时适用于桌面设备和移动设备。

整个项目基于一种非常“电影式”的风格,我们在设计层面上希望将体验保持在面向横向的固定帧内,以保持电影的魔力。由于该项目的大部分内容都由互动式迷你“游戏”组成,因此让它们溢出帧也不合理。

我们可以用着陆页为例,说明我们如何针对不同尺寸调整设计。

老鹰刚刚把我们丢到了着陆页。
小老鹰刚刚在着陆页中留下了我们。

该网站有三种不同的模式:桌面设备、平板电脑和移动设备。不仅是为了处理布局,还因为我们需要处理运行时加载的资源并添加各种性能优化措施。由于设备的分辨率比台式机和笔记本电脑要高,但性能却不如手机,因此定义一套最终的规则并非易事。

我们使用用户代理数据来检测移动设备,并使用视口大小测试来定位这些设备中的平板电脑(645px 及以上)。实际上,每种不同的模式都可以渲染所有分辨率,因为布局是基于媒体查询或基于 JavaScript 的相对/百分比定位。

由于在这种情况下的设计并非基于网格或规则,并且在不同部分之间非常独特,因此实际上取决于具体的元素和场景以及要使用的断点或样式。我们曾经多次使用出色的混音器和媒体查询来设置完美布局,然后需要根据鼠标位置或动态对象添加效果,最后用 JavaScript 重写了所有内容。

我们还在 head 标记中添加一个包含当前模式的类,以便在样式中使用该信息,如下例所示(在 SCSS 中):

.loc-hobbit-logo {

  // Default values here.

  .desktop & {
     // Applies only in desktop mode.
  }

 .tablet &, .mobile & {
   
   // Different asset for mobile and tablets perhaps.

   @media screen and (max-height: 760px), (max-width: 760px) {
     // Breakpoint-specific styles.
   }

   @media screen and (max-height: 570px), (max-width: 400px) {
     // Breakpoint-specific styles.
   }
 }
}

我们支持低至约 360x320 的所有尺寸,这在打造沉浸式网络体验方面一直相当具有挑战性。在桌面设备上,我们规定了最小尺寸,才能显示滚动条,因为我们希望尽可能让您在更大的视口中体验网站。在移动设备上,我们决定一直支持横屏模式和竖屏模式,一直到互动体验(也就是要求您将设备转为横屏模式)为止。与此相反的一点是,纵向模式不像横向模式一样有沉浸感;但是网站的扩展非常好,因此我们保留了它。

请务必注意,布局不应与功能检测(例如输入类型、设备屏幕方向、传感器等)混合在一起。这些功能可以存在于所有这些模式中,并且应该覆盖所有模式。同时支持鼠标和触摸就是一个示例。Retina 可针对质量进行补偿,但最重要的是性能是另一个,有时质量越低越好。例如,画布的分辨率是 Retina 显示屏上 WebGL 体验的一半,否则需要渲染 4 倍像素数

在开发过程中,我们经常会使用开发者工具中的模拟器工具,尤其是在 Chrome Canary 版中,该工具具有经过改进的全新功能和大量预设。这是快速验证设计的好方法。我们仍然需要定期在真实设备上进行测试。其中一个原因是网站正在调整以全屏显示。大多数情况下,支持垂直滚动功能的页面会在滚动时隐藏浏览器界面(iOS7 上的 Safari 目前在这方面存在问题),但我们必须适应此以外的所有内容。我们还在模拟器中使用了预设值,并更改了屏幕尺寸设置,以模拟可用空间丢失的情况。在真实设备上进行测试对于监控内存消耗和性能也很重要

处理状态

到达着陆页后,我们将着陆到中土世界的地图。您是否注意到网址发生了变化?该网站是一款使用 History API 处理路由的单页应用。

网站的每个部分都是其自己的对象,继承了功能样板,例如 DOM 元素、转换、资源加载、处理等。当您浏览网站的不同部分时,系统会启动各个部分,在 DOM 中添加和删除元素,并加载当前部分的资源。

由于用户可以随时点击浏览器的后退按钮或通过菜单进行导航,因此创建的所有内容都需要在某个时间点处置完毕。超时和动画必须停止并舍弃,否则会导致意外行为、错误和内存泄漏。这并非易事,尤其是在截止日期临近且您需要尽快完成所有事项时。

显示营业地点

为了展示精美的画面和中土世界的精彩角色,我们打造了由图片和文字组件组成的模块化系统,你可以横向拖动或横向滑动这些组件。我们没有在此处启用滚动条,因为我们希望在不同的范围中具有不同的速度,例如在图像序列中,您横向停止移动,直到剪辑播放完毕。

瑟兰迪尔大厅
瑟兰迪尔大厅时间表

时间轴

开发之初,我们并不知道各个位置的模块内容。我们知道,我们想要一种模板化方式,以横向时间线显示不同类型的媒体和信息。这样,我们就可以自由地展示 6 个不同的地点,而不必重复 6 次重建。为了对此进行管理,我们创建了一个时间轴控制器,它能够根据设置和模块行为来处理其模块的平移。

模块和行为组件

我们添加了支持的不同模块包括:图片序列、静态图片、视差场景、焦点转移场景和文本。

视差场景模块具有不透明背景,该背景具有自定义数量的层,用于监听视口中确切位置的进度。

焦点移动场景是视差存储分区的一个变体,除此之外,我们还为每层使用两张图片,分别淡入和淡出以模拟焦点变化。我们尝试使用模糊处理滤镜,但成本仍然很高,因此我们将等待 CSS 着色器实现这一点。

文本模块中的内容通过 TweenMax 插件 Draggable 启用拖动。您也可以使用滚轮或双指滑动进行垂直滚动。请注意,throw-props-plugin 会在您滑动和松开时添加快速滑动样式的物理特性。

这些模块还可以具有作为一组组件添加的不同行为。它们都有自己的目标选择器和设置。翻译以移动元素、缩放以进行缩放、使用信息叠加层的热点、用于视觉测试的调试指标、起始标题叠加层、光晕图层等。这些元素将附加到 DOM 中,或在模块内控制其目标元素。

有了这些信息,我们只需使用配置文件即可创建不同的位置,配置文件用于定义要加载哪些资源,并设置不同类型的模块和组件。

图片序列

从性能和下载大小方面来看,这些模块中最具挑战性的是图像序列。有很多关于此主题的报道。在移动设备和平板电脑上,我们将其替换为静态图片。如果我们希望在移动设备上提供出色的品质,那么解码和存储在内存中的数据就太多了。我们尝试了多种替代解决方案;先使用了背景图片和雪碧图,但会导致内存问题,以及 GPU 需要在精灵表之间切换时出现延迟的问题。然后,我们尝试了切换 img 元素,但速度也太慢了。将帧从雪碧图绘制到画布的效果最佳,因此我们开始对其进行优化。为了节省每一帧的计算时间,系统会通过临时画布对要写入画布的图片数据进行预处理,并使用 putImageData() 保存到数组中并解码后以供使用。原始精灵表随后可能会被垃圾回收,因此我们只会在内存中存储所需的最少数据。或许,存储未解码的图片实际上要少一些,但通过这种方式梳理序列可以获得更好的性能。这些帧非常小,只有 640x400,但是在拖动操作期间仅可以看到这些帧。当您停止播放时,高分辨率图片会加载并快速淡入。

var canvas = document.createElement('canvas');
canvas.width = imageWidth;
canvas.height = imageHeight;

var ctx = canvas.getContext('2d');
ctx.drawImage(sheet, 0, 0);

var tilesX = imageWidth / tileWidth;
var tilesY = imageHeight / tileHeight;

var canvasPaste = canvas.cloneNode(false);
canvasPaste.width = tileWidth;
canvasPaste.height = tileHeight;

var i, j, canvasPasteTemp, imgData, 
var currentIndex = 0;
var startIndex = index * 16;
for (i = 0; i < tilesY; i++) {
  for (j = 0; j < tilesX; j++) {
    // Store the image data of each tile in the array.
    canvasPasteTemp = canvasPaste.cloneNode(false);
    imgData = ctx.getImageData(j * tileWidth, i * tileHeight, tileWidth, tileHeight);
    canvasPasteTemp.getContext('2d').putImageData(imgData, 0, 0);

    list[ startIndex + currentIndex ] = imgData;

    currentIndex++;
  }
}

精灵表是使用 Imagemagick 生成的。下面是 GitHub 上的一个简单示例,展示了如何创建文件夹内所有图片的雪碧图。

为模块添加动画效果

为了将模块放置在时间轴上,时间轴的隐藏表示形式(显示在屏幕外)会持续跟踪“进度条指针”和时间轴的宽度。这只需使用代码即可完成,但在开发和调试时,如果采用可视化表示形式,效果会很好。实际运行时,它只会在调整大小时更新,以设置尺寸。有些模块会填充视口,而有些模块则有自己的宽高比,因此要在所有分辨率下缩放和定位所有内容会有些棘手,因为这样一来,所有内容都可见且不会过度剪裁。每个模块有两个进度指示器,一个表示屏幕上的可见位置,另一个表示模块本身的时长。在进行视差移动时,通常很难计算物体的开始和结束位置,以便与可见的预期位置同步。最好知道模块何时进入视图、播放其内部时间轴,以及模块何时再次以动画方式离开视图。

每个模块顶部都有一个细微的黑色层,用于调整其不透明度,使其在处于中心位置时完全透明。这有助于您一次只专注学习一个模块,从而提升体验。

网页性能

从正常运行的原型迁移到无卡顿的发布版本意味着从猜测到了解浏览器会发生什么。这时,Chrome DevTools 就是您的得力助手。

我们花了大量时间来优化网站。当然,强制硬件加速是获得流畅动画的最重要的工具之一。但也要在 Chrome 开发者工具中寻找彩色列和红色矩形。有关这些主题的优质文章有很多,您应该阅读全部。移除跳帧后会获得立竿见影的奖励,但当它们再次回来时,也会感到沮丧。而且他们会这样做。这是一个持续不断的过程,需要不断迭代。

我喜欢使用 Greensock 的 TweenMax 进行补间属性、转换和 CSS。考虑容器,在添加新图层时直观呈现结构。请注意,现有转换可能会被新转换覆盖。如果您仅添加 2D 值,则系统会将 CSS 类中强制硬件加速的 TranslateZ(0) 会替换为 2D 矩阵。要在这些情况下使层保持加速模式,请在补间中使用“force3D:true”属性来创建 3D 矩阵,而不是 2D 矩阵。当您结合使用 CSS 和 JavaScript 补间动画来设置样式时,很容易忘记。

请勿在不需要硬件加速的地方强制使用硬件加速。当您要对许多容器进行硬件加速时,GPU 内存可能会快速填满并产生意外的结果,尤其是在内存具有较多限制的 iOS 上。要加载较小的资源,并使用 CSS 将其放大,并停用移动模式下的部分效果,我们做出了巨大的改进。

内存泄漏是我们需要提高技能的另一个领域。在不同的 WebGL 体验之间导航时,系统会创建大量对象、材质、纹理和几何图形。如果您在离开该部分并移除该部分时尚未准备好进行垃圾回收,则可能会导致设备在内存耗尽一段时间后崩溃。

退出包含失败处理函数的部分。
退出含有失败处理函数的部分。
好多了!
好多了!

查找泄漏问题是在开发者工具中完成的非常直接的工作流程,可以记录时间轴和捕获堆快照。如果有特定的对象(例如 3D 几何图形或特定库),您可以将其滤除,这样会更轻松。在上面的示例中,结果显示 3D 场景依然存在,并且存储几何图形的数组未被清除。如果您发现很难找到对象所在的位置,可以通过一项很棒的功能来查看称为“保留路径”。只需点击堆快照中要检查的对象,即可在下面的面板中获得相关信息。使用结构较好的结构来处理较小的对象有助于查找引用。

该场景是在 EffectComposer 中引用的。
EffectComposer 中引用了场景。

一般来说,在操作 DOM 之前应三思而后行。制作时,要考虑效率。切勿在游戏循环中对 DOM 有所帮助。将引用存储在变量中以便重复使用。如果您需要搜索元素,请使用最短路线,方法是存储对战略容器的引用,并在最近的祖先元素中进行搜索。

如果您遇到布局 bug,则需要延迟读取新添加元素的尺寸,或者在移除/添加类时延迟读取。或者确保触发了布局。有时,浏览器会批量更改样式,并且在下一个布局触发器之后不会更新。这有时确实是一个大问题,但它是有原因的,因此尝试了解它在幕后的工作原理,您将收获很多。

全屏

您可以选择通过 Fullscreen API 将网站设置为全屏模式下的菜单(如果有)。但在设备上,浏览器也可以决定全屏显示。iOS 版 Safari 之前有一项允许您控制此功能的黑客手段,但现在该功能不再可用,因此您必须准备好您的设计,使其能够在制作非滚动网页时在没有该功能的情况下正常运行。我们很可能会在未来的更新中更新有关此问题,因为它破坏了很多网络应用。

资产

实验的动画说明。
动画说明。

在整个网站中,我们使用了许多不同类型的资源,包括图片(PNG 和 JPEG)、SVG(内嵌和背景)、精灵表 (PNG)、自定义图标字体和 Adobe Edge 动画。对于元素不能基于矢量的资源和动画(精灵表),我们会使用 PNG。否则,我们会尽可能多地使用 SVG。

矢量格式意味着不会损失质量,即使进行缩放也是如此。1 个文件适用于所有设备。

  • 文件较小。
  • 我们可以分别为每个部分添加动画效果(非常适合高级动画)。例如,我们在霍比特人徽标(Smaug 的荒漠)缩小时隐藏了其“副标题”。
  • 此类图片可作为 SVG HTML 标记嵌入,或用作无需额外加载的背景图片(与 HTML 网页同时加载)。

在可伸缩性方面,图标字体与 SVG 的优势相同,对于图标等小元素,我们使用图标字体代替 SVG,因为我们只需更改颜色(悬停、活动等)。这些图标也非常易于重复使用,您只需设置元素的 CSS“content”属性即可。

动画

在某些情况下,使用代码为 SVG 元素添加动画效果会非常耗时,尤其是在设计过程中需要大幅更改动画的情况下。为了改善设计人员和开发者之间的工作流程,我们使用 Adobe Edge 制作一些动画(游戏开始前的说明)。动画工作流非常接近 Flash,对团队有所帮助,但也存在一些缺点,特别是当将 Edge 动画集成到我们的资源加载过程中时,因为它自带加载器和实现逻辑。

我仍然认为,我们还有很长的路要走,要打造一个完美的工作流程,以便在网络上处理资源和手动制作动画。我们期待 Edge 等工具的发展。欢迎在注释中添加有关其他动画工具和工作流的建议。

总结

现在,当项目的所有部分都发布时,我们看看最终成果,肯定是我们对现代移动浏览器的印象很深了。最初这个项目开始时,我们对能够实现它的无缝、集成和性能抱有低得多的期望。这对我们来说是一次很棒的学习体验,我们花费大量的时间(大量)进行迭代和测试,加深了我们对现代浏览器工作原理的理解。这正是我们想要缩短这类项目的制作时间(从猜测到掌握)的正确方法。