像任何开发者一样,我热衷于任何出现在我浏览器中的闪亮的新技术演示;人们正在创造的一些东西,凭借其展示的创造力和技术水平,绝对让我叹为观止。
参加完 10 月中旬的 WebDevConf 2012 之后,我感受到了那种任何优秀的会议都会带给我们的灵感倍增的感觉。在返回伦敦的路上,我偶然在 Twitter 信息流中看到了一条关于当前 Mozilla Dev Derby 的 推文,依然沉浸在灵感之中,我开始思考自己要创作什么东西来参赛。这个东西最终变成了一个名为 媒体查询马里奥 的技术演示;将媒体查询、CSS3 动画和 HTML5 音频融合在一起。
从哪里开始呢?
构思这个创意源于当时我最想尝试的哪些新技术。我一直在想要深入研究 CSS 动画,将它与媒体查询(那个月 Dev Derby 的焦点)结合起来,似乎非常合理。让 CSS 触发动画,而不是需要 JavaScript 来完成,似乎很自然。
选择马里奥 3 作为动画,仅仅是因为它是我脑海中第一个冒出来的想法。我想要一个横向滚动的 2D 动画,作为一名复古游戏迷,马里奥立刻浮现在脑海。任何对 2D 马里奥游戏略有了解的人都会发现,马里奥 3 是我动画的唯一选择(虽然我随时可以反驳任何关于“最佳” 2D 马里奥游戏的不同意见!)。
自从发布了这个演示之后,我被问过一个问题:为什么选择 CSS 动画,而其他技术可能更适合呢?主要原因是我只是想看看它们能做到什么。有很多演示展示了画布和 SVG 到底有多棒;我的演示绝不意味着提倡使用 CSS 动画来替代那些技术。我只是想为现在 CSS 动画的水平设立一个不错的基准,至少在人们选择最适合自己项目的技术时,加入到讨论中。
在开始制作这个演示时,我给自己制定了一个规则 - 我只想尽可能地坚持使用 CSS 来进行动画。如果可以用 CSS 做到,无论性能如何,也不管实现起来有多麻烦,我都想用它。我将在后面回顾一下我认为它的表现如何。
按任意键开始
我遇到的最早的问题之一是,不知道用户会以什么宽度来观看动画。这不仅在设计动画尺寸方面很重要,在决定任何时候显示多少关卡内容方面尤其重要。显示的关卡内容越多,我就需要在任何时候进行更多的动画。
经过一番思考,如何呈现马里奥 3 本身,使用原始菜单屏幕来帮助控制这一点似乎很有道理。它不仅可以作为动画资源加载时的保持屏幕,还可以确保用户将浏览器窗口调整到我可以指定的尺寸,然后再开始动画。这通过添加一个条件媒体查询来隐藏动画开始按钮来控制。
@media screen and (max-width: 320px), (min-width: 440px) {
.startBtn {
display:none;
}
}
在规划动画本身时,我想尽可能地模仿原始游戏的玩法。为了帮助实现这一点,我找到了一段 视频剪辑,它以我可以复制的速度遍历了关卡。这帮助我规划了所需的图像和声音资源,动画的速度,并开始思考如何动画化关卡中不同的敌人和道具。
演示结构规划完毕,我只需要资源。正如你可能预期的那样,你不需要在网上搜索太久就能找到原始的游戏图像、精灵和声音文件。对于我的演示,我使用 NESmaps 和 Mario Mayhem 获取关卡地图和角色/物体精灵,使用 The Mushroom Kingdom 获取声音文件。我自己需要做少量图像编辑,但这些给了我一个非常好的开始。
你可以查看下面我用来制作动画的最终精灵表。
Let's-a Go!
因此,我规划了一个想法,并找到了我的资源;我准备开始用代码把它们组合在一起。
首先,我开始学习 CSS3 动画的细节。一些资源对我有很大的帮助;MDN 始终是一个很好的起点,对于 CSS 动画 也不例外。我还建议阅读 Peter、Chris 或 David 撰写的任何一篇优秀的文章 - 它们都提供了关于 CSS3 动画入门的一个很好的介绍。
我不会试图复制那些文章所涵盖的深度,但会突出我在演示中使用过的关键属性。为了简洁起见,我将使用未添加前缀的 CSS3 语法,但如果你想自己尝试一下,应该在代码中包含前缀,以确保动画在不同的浏览器中都能正常工作。
使用 CSS 动画等较新的 CSS3 特性时,值得一提的一个快速开发技巧是,使用预处理器,如 LESS 或 SASS,是拯救生命的利器,我强烈推荐使用它。创建抽象出供应商前缀的混合宏,可以减少直接在代码中工作时的视觉混乱,并节省大量时间,因为以后更改 CSS 属性值时,无需重复输入前缀。
在我们深入研究演示中使用的具体技术之前,我们需要了解,动画包含两个主要部分:**动画的属性**和与其相关的**关键帧**。
动画属性
动画可以使用多个相关属性构建。我使用过的关键属性是
//set the name of the animation, which directly relates to a set of keyframes
animation-name: mario-jump;
//the amount of time the animation will run for, in milliseconds or seconds
animation-duration: 500ms;
//how the animation progresses over the specified duration (i.e. ease or linear)
animation-timing-function: ease-in-out;
//how long the animation should wait before starting, in milliseconds or seconds
animation-delay: 0s;
//how many times the animation should execute
animation-iteration-count: 1;
//if and when the animation should apply the rendered styles to the element being animated
animation-fill-mode: forwards;
animation-fill-mode
属性的使用在演示中尤为重要,因为它用来告诉动画在动画执行完成后将最终呈现的样式应用于元素。如果没有它,元素将恢复到动画之前的状态。
例如,当将元素的左边距从初始位置 0px 动画到 30px 时,如果没有设置 animation-fill-mode
,元素将在动画完成后返回到 0px。如果将 fill-mode 设置为 forwards
,元素将保持在最终位置 left: 30px
。
关键帧
关键帧 at-规则允许你指定 CSS 动画中的步骤。在最基本的层面上,它可以定义为
@keyframes mario-move {
from { left:0px; }
to { left:200px; }
}
其中 from
和 to
是 0%
和 100%
的动画持续时间分别对应的关键字。为了展示一个更复杂的例子,我们还可以编写类似下面的代码,它关联到演示,使用多个关键帧来动画化马里奥在几个平台之间跳跃。
@keyframes mario-jump-sequence {
0% { bottom:30px; left: 445px; }
20% { bottom:171px; left: 520px; }
30% { bottom:138px; left: 544px; }
32% { bottom:138px; left: 544px; }
47% { bottom:228px; left: 550px; }
62% { bottom:138px; left: 550px; }
64% { bottom:138px; left: 550px; }
76% { bottom:233px; left: 580px; }
80% { bottom:253px; left: 590px; }
84% { bottom:273px; left: 585px; }
90% { bottom:293px; left: 570px; }
100% { bottom:293px; left: 570px; }
}
因此,如果上面的动画持续 1 秒,马里奥将在 0 秒(动画的 0%)时从 bottom: 30px; left: 445px;
位置移动到动画的前 200 毫秒(或 20%)期间的 bottom: 138px; left: 520px;
位置。在整个动画中,这个过程会一直持续下去。
动画化动作
考虑到以上内容,我在演示中创建的动画类型可以大致分为 3 类。
- **运动**,例如马里奥跳跃或硬币从问号块中出现。
- **精灵**控制动画中角色和物体的背景图像位置。
- **循环**任何要重复执行 x 毫秒或 x 秒的动画。
运动
运动涵盖了演示中大约 75% 的所有动画。例如,这包括角色运动(即马里奥奔跑和跳跃)、道具出现和问号块被击中。每个运动动画的不同之处在于 animation-timing-function
、animation-duration
和 animation-delay
属性。
animation-timing-function
属性可以控制动画在整个持续时间内的速度。在可能的情况下,我使用了缓动函数,比如 ease-in
或 ease-in-out
,以避免在定义动画关键帧时过于精确。如果这些函数无法达到我想要的效果,我会将 animation-timing-function
设置为线性,并使用关键帧来指定我需要的精确运动。
您可以通过这个 跳跃序列 查看一个运动动画的例子。
雪碧图
为了控制动画中角色和物体图像的 background-position
,我使用了 step-end
计时函数。
.mario {
animation-timing-function: step-end;
...
}
最初,我认为可能需要使用 JavaScript 通过向元素添加和移除类来控制图像雪碧图。但是,在尝试了 step-end
计时关键字的实现方式后,我发现它可以完美地按我定义的关键帧一步一步地进行,一次一个关键帧。
为了展示它的实际效果,请查看以下示例,其中展示了简单的 马里奥行走动画 和 马里奥获得道具后的变形动画。
但是,这种使用 step-end
的方式并不完全没有问题。令我沮丧的是,当这些雪碧动画在多个媒体查询中叠加时,我发现 WebKit 中存在一个故障,导致动画的渲染方式与我定义的关键帧不同。诚然,这种使用 CSS 动画的方式是浏览器渲染的边缘情况,但我已经在 Chromium 中将其作为 bug 提交,并希望将来能够得到解决。
循环
当动画需要在一段时间内重复播放时,可以通过调整 animation-iteration-count
来定义循环。
//the animation repeats 5 times
animation-iteration-count: 5;
//the animation repeats infinitely
animation-iteration-count: infinite;
演示中有一个例子,即 火球的旋转。
通过这三种类型的动画,整个演示得以构建。最后一层是添加音频。
添加音频
虽然之前我已经下载了所有需要的 .wav
格式的音频文件,但我必须将其转换为 HTML5 音频可用的格式:.ogg
和 .mp3
。我使用了 Switch Audio Convertor(在 Mac 上)来完成这项工作,但任何好的音频转换软件都可以完成这项任务。
一旦有了转换后的文件,我需要检测要为浏览器提供哪种文件类型。这需要几行 JavaScript 代码来检测支持情况。
var audio = new Audio(); //define generic audio object for testing
var canPlayOgg = !!audio.canPlayType && audio.canPlayType('audio/ogg; codecs="vorbis"') !== "";
var canPlayMP3 = !!audio.canPlayType && audio.canPlayType('audio/mp3') !== "";
然后我创建了一个函数,为每个声音设置一些默认音频参数,并根据之前检测到浏览器支持的格式设置源文件。
//generic function to create all new audio elements, with preload
function createAudio (audioFile, loopSet) {
var tempAudio = new Audio();
var audioExt;
//based on the previous detection set our supported format extension
if (canPlayMP3) {
audioExt = '.mp3';
} else if (canPlayOgg) {
audioExt = '.ogg';
}
tempAudio.setAttribute('src', audioFile + audioExt); //set the source file
tempAudio.preload = 'auto'; //preload the sound file so it is ready to play
//set whether the sound file would loop or not
//looping was used for the animations background music
tempAudio.loop = (loopSet === true ? true : false);
return tempAudio;
}
var audioMarioJump = createAudio("soundboard/smb3_jump"); //an example call to the above function
接下来,只需在与动画同步的正确时间播放声音即可。为此,我需要使用 JavaScript 监听动画事件 animationstart
和 animationend
,或者在 WebKit 中,使用 webkitAnimationStart
和 webkitAnimationEnd
。这使我能够监听定义的动画何时开始或结束,并触发相关的音频播放。
当事件监听器被触发时,事件会返回 animationName
属性,我们可以将其用作标识符来播放相关的音频。
mario.addEventListener('animationstart', marioEventListener);
function marioEventListener(e) {
if (e.animationName === 'mario-jump') {
audioMarioJump.play();
}
}
如果一个元素有多个 animationstart
事件,比如演示中的马里奥,可以使用 switch
语句来处理触发事件监听器的 animationName
。
在编写演示之后,我发现也可以通过 Keyframe Event JS shim(由 Joe Lambert 开发)来定位动画中的单个关键帧,这样就能更加灵活地控制何时可以插入动画。
游戏完成
演示发布后,它收到的反馈比我预期的要积极得多。就像任何黑客作品一样,如果我有更多时间,我希望能回去改进一些地方,但我认为将我学到的东西应用到下一个项目中更有价值。我认为演示表明 CSS 动画可以用来从非常简单的代码中创建一些惊人的效果,但也让我想起了在制作过程中遇到的一个更大的问题。
虽然复杂的 CSS 动画实际上性能很好,但创建这样的动画相当繁琐。当然,有一些工具可以帮助我们完成这项工作,比如 Adobe Edge Animate 和 Sencha Animator,但它们都会将 CSS 动画包装在 JavaScript 中输出。这对我来说似乎是一个巨大的遗憾,因为 CSS 动画的强大之处在于,它不应该依赖于其他技术来执行。我不确定是否有可能绕过这个问题,除了手动编码之外,但如果有人知道其他方法,请在评论中告诉我。
回到我之前关于将 CSS 动画与使用 canvas 和 SVG 进行比较的评论,我认为在讨论使用哪种技术进行动画时,所有这些方法都有一定的优势。但是,如果能够降低制作像这样的复杂动画所花费的时间成本,那么 CSS 动画在我们的项目中将会有更多的相关性,以及更多潜在的用例。
关于 Ashley Nolan
Ashley 在 TMW 公司担任创意技术专家。他热衷于所有与视觉相关的事物,并对新技术在该领域为网络带来的可能性感到兴奋。在空闲时间,他会用 CSS 和 JavaScript 编写代码,并在 dragongraphics.co.uk 上写博客,你也可以在 Twitter 上关注他 @dragongraphics。
7 条评论