中土世界前端

多设备开发演示

Daniel Isaksson
Daniel Isaksson
Einar Öberg
Einar Öberg

在介绍 Chrome 实验“穿越中土世界”第一篇文章中,我们重点介绍了面向移动设备的 WebGL 开发。在本文中,我们将讨论在创建其余 HTML5 前端时遇到的挑战、问题和解决方案。

同一网站的三个版本

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

整个项目采用的是极具“电影感”的风格,在设计方面,我们希望将体验保持在横向固定框架内,以保留电影的魔力。由于该项目的大部分内容都由互动式迷你“游戏”组成,因此也不适合让这些游戏溢出帧。

我们可以以着陆页为例,介绍如何针对不同的尺寸调整设计。

老鹰刚刚把我们放到了着陆页。
老鹰刚刚把我们放到了着陆页上。

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

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

由于本例中的设计并非基于网格或规则,并且不同部分之间非常独特,因此具体要使用哪些断点或样式,实际上取决于具体元素和场景。我们不止一次遇到过这样的情况:使用漂亮的 Sass-mixin 和媒体查询设置了完美的布局,然后需要根据鼠标位置或动态对象添加效果,最后不得不使用 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,这在打造身临其境的 Web 体验时非常具有挑战性。在桌面设备上,我们会先达到最小尺寸,然后再显示滚动条,因为我们希望您能尽可能在更大的视口中浏览网站。在移动设备上,我们决定在互动体验之前允许用户在横向和纵向模式之间切换,在互动体验中,我们会要求用户将设备切换到横向模式。反对者认为,纵向模式的沉浸感不如横向模式;但该网站的缩放效果非常出色,因此我们保留了该功能。

请务必注意,布局不应与输入类型、设备屏幕方向、传感器等功能检测混淆。这些功能可以在所有这些模式中存在,并且应涵盖所有这些模式。同时支持鼠标和触控就是一个例子。虽然 Retina 可补偿质量,但最重要的是效果,有时质量越低越好。例如,在视网膜显示屏上,画布在 WebGL 体验中的分辨率只有原来的一半,否则就必须渲染 4 倍的像素数

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

处理状态

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

网站的每个部分都是自己的对象,继承了 DOM 元素、转场效果、加载资源、处置等样板功能。当您浏览网站的不同部分时,系统会启动各个部分,将元素添加到 DOM 中并从中移除,并加载当前部分的资源。

由于用户可以随时点击浏览器的返回按钮或通过菜单进行导航,因此创建的所有内容都需要在某个时间点被处理掉。超时和动画需要停止并舍弃,否则会导致不必要的行为、错误和内存泄漏。这并不总是一件容易的事,尤其是在截止日期临近时,您需要尽快完成所有工作。

展示地点

为了展示中土世界的美丽场景和人物,我们构建了一个由图片和文本组件组成的模块化系统,您可以水平拖动或滑动这些组件。我们在这里没有启用滚动条,因为我们希望在不同范围内采用不同的速度,例如在图片序列中,您可以停止横向移动,直到剪辑播放完毕。

Thranduil's Hall
Thranduil's Hall 时间表

时间轴

在开发之初,我们并不知道每个位置的模块内容。我们知道,我们需要一种模板化的方式,以便在横向时间轴中显示不同类型的媒体和信息,这样我们就可以自由地展示六个不同的位置,而无需重建所有内容六次。为了管理这一点,我们创建了一个时间轴控制器,用于根据设置和模块的行为处理其模块的平移。

模块和行为组件

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

Parallax 场景模块具有不透明的背景,其中包含自定义数量的图层,这些图层会监听视口进度以确定确切位置。

焦点转移场景是视差分桶的变体,此外,我们为每个图层使用两张图片,这些图片会逐渐淡入和淡出以模拟焦点变化。我们尝试过使用模糊滤镜,但它仍然太耗资源,因此我们将等待 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 DevTools 中的彩色列和红色矩形。关于这些主题有很多优质文章,您应该全部阅读。去除跳帧带来的好处是立竿见影的,但当跳帧再次出现时,带来的挫败感也是立竿见影的。他们会这么做的。这是一个需要迭代的过程。

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

请勿在不需要硬件加速的情况下强制启用硬件加速。当您想要对多个容器进行硬件加速时,GPU 显存可能会快速填满并导致不必要的结果,尤其是在内存受更多限制的 iOS 上。通过加载较小的资源并使用 CSS 对其进行放大,以及在移动模式下停用某些效果,取得了巨大的改进。

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

退出包含失败的 dispose 函数的部分。
退出包含失败的 dispose 函数的部分。
现在好多了!
效果好多了!

在 DevTools 中,查找内存泄漏的过程非常简单,只需记录时间轴并捕获堆快照即可。如果您可以滤除特定对象(例如 3D 几何图形或特定库),则更容易进行处理。在上述示例中,3D 场景仍然存在,并且存储几何图形的数组未被清除。如果您很难找到对象所在的位置,可以使用一个名为保留路径的实用功能来查看。只需点击堆快照中要检查的对象,即可在下方的面板中获取相关信息。使用结构良好且较小的对象有助于定位引用。

该场景在 EffectComposer 中被引用。
EffectComposer 中引用了场景。

一般来说,在操控 DOM 之前,最好先三思。在执行此操作时,请考虑效率。请尽量不要在游戏循环中操控 DOM。将引用存储在变量中以供重复使用。如果您需要搜索某个元素,请存储对战略性容器的引用,并在最近的祖先元素内进行搜索,以使用最短的路线。

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

全屏

您可以通过 Fullscreen API 在菜单中选择将网站设为全屏模式(如果可用)。但在设备上,浏览器也可以决定将其设为全屏。以前,iOS 上的 Safari 有一个小技巧可供您控制此行为,但该技巧已不再可用,因此在制作不可滚动页面时,您必须准备好在没有该技巧的情况下使设计正常运行。由于此问题导致许多 Web 应用无法正常运行,因此我们可能会在未来的更新中对其进行更新。

素材资源

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

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

矢量格式意味着即使缩放,也不会降低画质。1 个文件,适用于所有设备。

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

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

动画

在某些情况下,使用代码为 SVG 元素添加动画可能会非常耗时,尤其是在设计过程中需要对动画进行大量更改时。为了改善设计师和开发者之间的工作流,我们使用 Adobe Edge 制作了一些动画(游戏前的说明)。动画工作流与 Flash 非常相似,这对团队很有帮助,但也存在一些缺点,尤其是在将 Edge 动画集成到资源加载过程中,因为它自带加载器和实现逻辑。

我仍然认为,我们还有很长的路要走,才能拥有一个完善的工作流来处理 Web 上的资源和手绘动画。我们期待看到 Edge 等工具的演变。欢迎在评论中添加有关其他动画工具和工作流的建议。

总结

现在,项目的所有部分都已发布,我们在查看最终结果时,不得不说我们对现代移动浏览器的现状印象深刻。在开始这项项目时,我们对其顺畅程度、集成度和性能的预期要低得多。这对我们来说是一次难得的学习经历,我们花了大量时间进行迭代和测试,这让我们对现代浏览器的运作方式有了更深入的了解。如果我们想缩短这类项目的制作时间,从猜测转为确知,就需要这样做。