那是 1995 年;玩具总动员上映了,孩子们痴迷地收集着 小纸板圆圈,而“来自玫瑰的吻”被每个人唱得乱七八糟。我那时是一个瘦高个的十岁男孩,像任何其他相对高大的孩子一样,我经常被问到“你一定很擅长打篮球!”。所以我练习,练习,在小学的球场上花了好几个小时。最后,我意识到,令阿姨和其他捏脸的人都很沮丧的是,虽然占据垂直空间可能在篮球艺术中给你带来优势,但这并不能保证你会成功。
快进 21 年。现在我成为了一个瘦高个的开发者,仍然不擅长打篮球,我面临着一个项目:为 NBA 的底特律活塞队设计和实施一个 全动态视频网络篮球游戏。扔球是一回事;扔像素——这才是最终我能够胜任的篮球挑战!
在开发游戏时,我使用了许多很棒的东西,比如 canvas、SVG 和 CSS 动画、手势识别以及动态构建的视频流。如今,仅仅使用浏览器,我们就能做到如此惊人的事情。快去 试试看.
在这篇文章中,我想重点介绍一下我是如何使用原生 JS 结合 GSAP 实现超级力量计的动画的。这是我在实现动画时使用的运动参考,它是在 After Effects 中创建的。
在 1 对 1 中,一旦用户成功完成了一个动作,他们就会获得连击分数。计量表位于屏幕的左上角,它的任务是向用户传达他们的连击分数,如红色段的数量所示。在游戏中的某些时刻,超级力量计会变为活跃状态,通知用户他们可以点击它来让他们的游戏内化身执行特殊动作。
超级力量的基本结构是用一个 Canvas 元素和一些简单的几何图形来实现的。
查看笔 活塞超级力量:结构 由 Opher Vishnia (@OpherV) 在 CodePen 上创建。
本质上,这里有两个主要组件——中心图像和计量表段。图像很容易,它只是对 canvas 的 drawImage 的简单应用。计量表段是事情变得有趣的地方。我定义了一个通用的 options
,它包含一些属性,以便稍后进行操作,例如段数、半径、宽度等等。然后,我遍历一个 segment
对象数组,并使用它们的属性(strokeStyle
、lineWidth
)来使用 canvas 弧线函数绘制实际的段。到目前为止一切都很好——但动画在哪里呢?
我一直在考虑是否使用 canvas 动画框架,但最终决定放弃。这是因为我需要在项目中使用几种类型的动画:Canvas、SVG 和 CSS/DOM,而没有一个框架能够做到所有这些。此外,所有动画都必须在播放视频的同时在台式机和移动设备上平滑运行,并且具有不同的功能和网络条件。这意味着性能至关重要,我希望确切地知道哪些代码驱动动画。幸运的是,GSAP(也称为 Greensock、TweenMax、TweenLite)允许我做到这一点。
GSAP 很酷。它使你能够动画化几乎任何东西!技巧在于,动画 API 不仅接受 DOM/SVG 对象,还接受任意的 JS 数据结构,然后你可以对它们的属性进行“动画”。

基本思想是使用 GSAP 随着时间的推移更改这些对象的属性。这些值指定 UI 在任何给定时间点的外观。在每个 requestAnimationFrame
上,你都会对 canvas 进行绘制调用,以根据这些对象绘制 UI 的状态。
function render() {
//draw the animation state
drawComboGui();
//draw the image
ctx.drawImage(...);
//render on the next frame as well
window.requestAnimationFrame(render)
}
render();
以下是实现的不同动画的细分。
计量表填充
查看笔 活塞超级力量:计量表填充 由 Opher Vishnia (@OpherV) 在 CodePen 上创建。
让我们讨论一下此动画的结构。在空闲状态下,所有尚未填充的计量表段都是灰色且细长的,而填充的段是红色且略微更粗。一旦计量表的下一段填充完毕,所有先前的活动段就会变为白色,增大厚度并开始发光。然后填充下一段,最后所有段停止发光并恢复到原始的活动宽度。
还记得那个 segment 对象数组吗?这里就是它们与 GSAP 一起发挥作用的地方。addActiveSegment
函数是魔法的核心,我们在其中使用 TweenMax.fromTo
来动画化 lineWidth
和 anglePercent
等属性。GSAP colorProps 插件允许我们在 strokeStyle
和 activeStrokeStyle
等颜色属性中进行平滑过渡。我使用 delay
属性来计时此动画的各个部分。
TweenMax.fromTo(segments[index], expandAnimLength, {
anglePercent: 0,
colorProps:{strokeStyle: options.activeStrokeStyle},
},
{
anglePercent: 1,
colorProps:{strokeStyle: options.activeStrokeStyle},
ease: Power0.easeIn,
delay: growAnimLength
});
正如我之前提到的,render 函数然后在每个 requestAnimationFrame
上调用 drawComboGui,理想情况下每秒 60 次。在 drawComboGui
中,我们首先清除 canvas 上绘制的任何先前数据,然后再绘制当前状态。
为了创建发光效果,我在每个 segment 之上绘制了两个 segment。底部的那个使用 canvas 路径上的 shadowBlur,而顶部的那个没有 shadowBlur。这使得模糊元素“窥视”非模糊元素,从而产生发光效果。

超级力量计还有几个额外的动画。超级力量启用和超级力量禁用在概念上与这里讨论的计量表填充非常简单。它们是通过动画化图像宽度和活动段来实现的。


超级力量已充能 和 超级力量已放电 动画需要其他技术,例如动画化图像精灵并应用动态模糊过滤器。现在有点超出范围,但在以后的文章中会讨论!
在实现游戏 UI 时,有一个主要的陷阱。游戏是极度有状态的。在任何给定时刻,游戏的状态,以及代理,其 UI 都可能发生变化。在超级力量计的特定情况下,这意味着在任何时刻,计量表都可能填充、启用、放电或禁用。即使在动画进行过程中,也可能发生这种情况!那么,在这种情况下该怎么办呢?
你有两种选择——一种是停止当前正在播放的任何动画,并突然过渡到新的动画。这种方法的问题是,用户的体验非常不连贯,使他们脱离游戏,最终传达更多噪音而不是信息。这与良好界面的作用完全相反。
另一种选择是将动画排队,以便每个动画都在上一个动画开始之前触发。这有点棘手,因为动画可能由更小的子动画组成,但由于 GSAP 的 Timeline 功能,管理所有这些状态和动画的任务变得更加容易。你不需要调用 TweenMax.to
,而是初始化一个 Timeline 实例对象并使用它来进行 to 调用。默认情况下,这些 to 调用定义新的动画,这些动画将在时间轴的末尾开始,形成一个动画队列,但这是 高度可配置的!例如,你可以定义一个动画,使其相对于时间轴结束位置开始,或者在时间轴上的绝对位置开始。这也允许你避免使用 delay 属性来计算动画的排队,因为在处理多个动画时,这往往会变得很麻烦。
以下是如何使用 GSAP 的 Timeline 实现计量表填充动画。尝试在动画已经进行中时单击“添加段”按钮。
查看笔 活塞超级力量:使用 Timeline 填充计量表 由 Opher Vishnia (@OpherV) 在 CodePen 上创建。
我希望这能帮助你解决在你的游戏/网站/项目中遇到的挑战和问题。如果你有任何问题,或者你想知道我是如何处理游戏中的其他 UI 元素的,请随时在 Twitter 上联系我!
结语
虽然我仍然时不时地被低垂的树枝攻击,而且我的投篮技巧还有待提高,但说到快速按下按键组合,安德烈·德拉蒙德就比不了我了。
如果你连续点击按钮,它仍然会卡住。可能是某个属性跳得太远,而其他属性还在赶上来。
发现得不错!
实际上,您描述的问题与 GSAP 的 `from` 函数的一个小陷阱有关。这里有一个很好的解释:这里。
无论如何,我已经修复了有问题的 CodePen。随意点击!:)
不错!
很棒的文章,我很好奇是否会有关于这个游戏的动态视频组件的后续内容?
这是一个好主意!我会看看我能做些什么:)