【译】叶子——可互动的 Web 玩具

@JChehe 2018-08-28 16:50:59发表于 JChehe/blog

原文:Leaf Notes – An Interactive Web Toy

Art, story & experience
在浏览器上体验 https://tendril.ca/

我最近为多伦多的设计动画工作室 Tendril 推出了一个可互动的 Web 小玩具。你可以在其 官网首页 亲自体验。该网站会轮流展示数个不同的 Web 玩具,所以可能需要刷新一到两次才能看到它。

<iframe src="https://player.vimeo.com/video/261147357" width="640" height="367" frameborder="0" allowfullscreen></iframe>

玩法非常简单:用鼠标划过植物就能使它们开花,并发出相应音调。

该项目十分有趣,我对目前结果也非常满意。TwitterInstagram 上的热烈反应使我备受鼓舞,其中最让我暖心的是一位年仅四岁的小孩在平板上进行了体验。

本文将阐述我与优秀团队 Tendril 如何创造这个 Web 玩具,并讨论期间遇到的一些技术挑战。

概念

在前一段时间,Tendril 已在其官网推出了独具一格的交互动画(案例:12)。他们想让我创造一种全新的体验,且要体现生殖生长和程序化几何。

译者注:
生殖生长:当植物生长到一定时期以后,便开始分化形成花芽,以后开花、授粉、受精、结果(实),形成种子。——百度百科
程序化几何:通过程序生成的几何图形。

One of Tendril's previous web toys
Tendril 先前的一个 Web 玩具

这个想法十分开放:为 Tendril 官网开发一个可互动的有趣玩具。它与已有的 Web 玩具共存,所以设计要适中、使用要简单、加载速度要快。总的来说:交互方式要轻易上手,整体体验要与 Tendril 的网站一致。

一个充满创作自由的想法对我来说可是一个挑战。在过去几个月里,我一直逼自己在开发前进行更多的搜索、头脑风暴、艺术指导和设计思考和构思。我发现铅笔和笔记本确实是最好的工具,不过像 Pinterest 和 Behance 这类平台则有助于管理参考文献和寻找灵感来源。

在讨论了几个不同想法后,我们选定了“与热带植物交互”的这个方向。

Early mood board
早期 情绪板

我早期的情绪板更倾向于单色而鲜明的视觉方向。这些信息反映出了项目在迭代开发中的变化区间。

💡相关说明:我希望有一个开源工具能将一组图片生成砌体结构风格的情绪板。虽然 InVision Boards 的用户体验很棒,但它是一个付费服务。

植物的生殖生长

生殖生长植物
早期程序化生化的植物几何体 Canvas2D 原型

最初,我使用 Canvas2D 的线来进行植物结构的原型设计。这无疑是快速验证想法和几何实现的好方法,因为这无需关心 WebGL 和 GPU 的复杂性。

我使用了简单的线段和二次贝塞尔曲线建立了植物的程序结构。二次贝塞尔曲线如下图所示,它由起点、控制点和终点构成。

二次贝塞尔曲线的构成

使用简单的基本图形和参数函数(如线、曲线)能让事情变得更可控,如动画、GPU 的快速渲染、鼠标的碰撞检测、甚至是声音设计等。例如:定义变量 t,它是 [0, 1] 区间的数字,然后使用参数函数高效地计算出该值所代表的 2D 点。

结构

为每棵植物定义一个起点(如屏幕边界)和一个终点(如接近屏幕中点的某个位置)。然后,再放置一个稍微偏离两端点间中点的控制点,以形成一种弯曲植物茎的感觉。

弯曲的植物茎

为生成叶子,需按固定间隔遍历曲线,确定每个位置上的垂直法向量,并使用一些函数对法向量进行缩放 & 旋转操作,最终形成像“羽毛”一样的叶子。在最终案例中,我并未使用垂直法向量,而是使用了斜接的法向量(mitered normal)【译者注:mitered normal 翻译有误】。

像羽毛一样的叶子

我提取部分代码到以下 Canvas2D 案例中,你可以在 这里 查看/修改。点击以下案例可修改曲线结构。

Edit Procedural Leaf

学习到的错误及经验教训

在 2D 原型设计阶段,我犯了两个错误。在后续原型设计中应尽量避免:

  • 维数:如果能将这种体验转化到三维空间就更好了,因为拥有深度和更好的互动效果。大多数算法均能转换到 三维空间上,但代码和数据结构是以二维空间作为假设前提而设计的。
  • 单位:在最初 2D 原型制作时,使用了像素单位进行缩放和定位。这使得在适配屏幕分辨率时变得困难。如果在生成植物的代码中使用相对坐标会让上述情况变得更好处理,如 (0, 0) 代表屏幕左上位置,(1, 1) 代表屏幕右下位置,就像上面的 CodeSandbox 案例那样。

动画 & 交互

在几何植物的顶点上使用简单的弹簧效果,而不是使用复杂且 CPU 密集型的物理系统。这使得植物看起来有点像果冻,但不失为一个有趣好玩的互动。

对植物茎和叶子上的顶点,我都指定了 target(即顶点应该弹向的目标位置)、position(即顶点的实时位置)和 velocity(速度和运动方向)。基础物理系统的伪代码如下:

// 1. 为速度 velocity 添加鼠标力
if (鼠标足够靠近顶点) {
  velocity += mouseVelocity * mouseStrength;
}

// 2. 弹向目标位置
const delta = target - position;
velocity += delta * spring;

// 一直存在的且不变的“空气阻力”
velocity *= friction;

// 累加得出顶点位置
position += velocity;

以下是顶点弹簧效果的交互案例,它展示了如何让二次贝塞尔曲线与鼠标产生弹簧的效果。尝试一下案例吧,也可以 点击这里 阅读完整代码:

Edit Springing Curves

对于碰撞,我使用了 point within radius 模块判断顶点是否与鼠标发生碰撞。该碰撞检测的运算速度很快,但并非完美:叶子上存在部分“盲点”,即不会产于交互。为了在划动叶子时产生更精确的声音效果,我在小树叶上使用 point to line segment distance 进行碰撞检测。使用后者能产生更佳的互动体验,但在最终案例中,较大的鼠标半径和较多的植物数量使得难以发现两者差异。

渲染

尽管 Canvas2D 能很好地完成原型阶段的处理,但却不能胜任诸如逐像素着色的工作。

感谢 ThreeJs 及其 OrthographicCamera,它们使得所有 canvas 代码迁移至 WebGL 变得不会太难。每根植物茎由一个 PlaneGeometry(可复用)和一个自定义顶点着色器(vertex shader)组成。顶点着色器将平面几何段(plane segments)沿曲线(或线段,即植物茎或叶子)放置。

译者注:OrthographicCamera:正交相机,即镜头下所有东西均不会产生近大远小的透视效果,尺寸保持一致。

可通过我之前编写的笔记 《2D Quadratic Curves on the GPU》 了解更多该技术相关的知识点。通过该方法能生成每棵植物所需的曲线和线段。最终效果如下:

2D Quadratic Curves on the GPU

在顶点着色器中,我添加了参数函数,以实现沿 t 弧长变化的线宽。例如:thickness = sin(t * PI) 会压缩曲线的开始和结束部分。有了这些函数,平面几何的轮廓开始变得更像锥形叶子。

锥形叶子

最后,添加颜色和外观细节——每片叶子在亮度、色相、饱和度、叶脉密度和旋转角度等方面均有了细微变化。所有这些计算均在片段着色器(fragment shader)完成。例如:每片叶子的叶脉和中线是基于纹理坐标计算得到的,并使用 fwidth() 计算出抗齿锯 2~3 像素的平滑曲线。

片段早色起

在开发期间,我使用 dat.gui 作为可视化滑块,并使用 surge.sh 与团队的其他成员共享迭代。这些工具使得我们能够尝试许多不同的想法和方向。而这种开发迭代的方式也让我们能想出一些有趣的特性:直到项目后期我们才引入了在黑色“手绘”状态下添加动画的想法(译者注:此句原文为:it wasn’t until later in the project that we introduced the idea of animating plants in from a black “hand-drawn” state)。

dat.gui

小细节

为了让项目更加生意盎然,我在小细节上花费了许多时间。实际上,叶子的核心结构和弹簧般的交互效果是最简单的部分,而大部分时间则花在了提升视觉效果、制作动画和修复各种跨浏览器问题上。

部分细节如下:

  • 随机性在案例的几乎所有部分(即视觉上的细微变化、动效和音效)均有应用。例如,随机长度、曲率、密度、时间、风速、色调、线宽、亮度、音量等。在最终效果中,我使用固定的随机系数,以保证所有用户体验到一致的效果。
  • 声音被节流(通过时间和最大同时播放数),从而避免破音和杂音。
  • 声音的音量是基于鼠标的划动速率进行动态修改。鼠标的快速移动能产生更加戏剧性的声音效果。
  • 根据鼠标的交互位置,声音会在往左/右声道靠拢,以呈现空间立体感。
  • 多处性能优化:整个场景仅有一个着色器(译者注:顶点着色器和片段着色器组成一个着色器程序)和 3 种不同的几何形状;花费大量时间查看分析器(Profilers)和优化函数,直至能在所有浏览器和设备上流畅运行。屏幕像素密度、叶子密度、植物组织和其他变量均基于用户浏览器和分辨率进行适配。

最后的障碍

和一般交互式 Web 项目一样,项目的最后阶段通常需要小调整,以确保能在各类浏览器和设备上顺利运行。

对此,我使用了几个过去用于处理常见跨浏览器问题的模块,如用于统一鼠标和触摸事件的 touches 和兼容 iOS WebAudio 的 web-audio-player

还有其他一些浏览器问题,以下是我处理的方案:

  • 在 FireFox 和 MS Edge 中,当 JavaScript 有大量 CPU 运算时,setTimeout 不能及时触发回调函数。因此,我认为它是不精确的,而且我也不可能为此等待 2~3 秒之久。于是选择 timeout-raf 修复它。
  • 为不支持 WebAudio 的浏览器(如 Safari)进行 polyfill 兼容处理——stereo panner node。而对于声道有偏移问题的浏览器(如移动端 iOS safari)则采取在移动端禁用该效果的处理。
  • Safari 同时还存在其他一些问题:我不得不控制最大同时播放数,以避免破音/咔嚓声;避免因浏览器偶尔中断 audio 上下文,而需在播放前调用 audioContext.resume()
  • 与其他浏览器相比,JIT/JavaScript 引擎在 MS Edge 上表现太差。除了降低植物细节外,我无法修复该问题。
  • iOS Safari 中嵌入 iFrame,有时会获取到不正确的 window.innerWidth 值。为了修复该问题,我最终为 canvas 设置 position:fixed 且宽高 100% 的样式。

作者

感谢 Tendril 团队,名单如下:

本文和交互案例的源码均可在以下链接找到:

https://github.com/mattdesl/tendril-webtoy-blog-post