从六十天手写 CSS 重生僵尸中学到的经验

Avatar of John Rhea
John Rhea

DigitalOcean 提供适合旅程每个阶段的云产品。立即开始使用 $200 免费积分!

注意:前方存在极度糟糕的幽默感。我们会讨论一些实际的东西,但示例几乎都涉及僵尸和愚蠢的笑话。我已经警告过你了。

我会在讨论我学到的经验时链接到各个 Pens,但如果你想了解整个项目,请查看 Undead Institute 上的 60 天动画。我开始这个项目是为了在 2020 年 8 月 1 日结束,与我写的一本关于 CSS 动画、幽默和僵尸的书的出版日期一致——因为,很明显,如果你不挥舞你的网络技能并阻止末日,僵尸会毁灭世界。没有什么比移动的 HTML 元素更能伤害到僵尸群了!

我在整个项目中为自己制定了一些规则。

  1. 我会手写所有 CSS。(我是一个受虐狂。)
  2. 用户会启动所有动画。(我讨厌遇到已经进行到一半的动画。)
  3. 我会尽可能少地使用 JavaScript,并且绝不用于动画。(我只使用了一次 JavaScript,那就是用它来启动最终动画的音频。我并不反对 JavaScript,只是它不是我在这里想做的。)

经验 1:八十天很长。

嗯,标题不是说“六十”天吗?是的,但我最初的目标是做八十天,当第一天临近时,我只有不到二十个动画准备好了,每个动画的制作平均需要三天,我吓坏了,就改成了六十天。这给了我从开始日期起二十天的时间,以及少做二十个作品。

经验 1A:六十天仍然很长。

在有限的时间、想法和更有限的艺术技能下,要完成这么多动画,这确实很困难。虽然我想过缩短到三十天,但我很高兴我没有这样做。六十天让我成长,并迫使我更深入地了解 CSS 动画——以及扩展,CSS 本身——是如何工作的。我对自己后期创作的许多作品感到最自豪,因为我的技能提高了,我必须更加创新,并更努力地思考如何让作品变得有趣。当你用完所有简单选项后,真正的工作和最佳成果就开始了。(是的,最后变成了六十二天,因为我从 6 月 1 日开始,并想在 8 月 1 日做一个最终的动画。从 6 月 3 日开始感觉很恶心,也不对。)

所以,真正的经验 1:挑战自我。

经验 2:交互式动画很难,而且更难制作响应式。

如果你想要一个物体在屏幕上飞过并与另一个物体连接,或者看起来是触发另一个物体的移动,你必须使用所有标准的、不可灵活的单位,或者所有灵活的单位。

三个变量决定了动画元素在任何动画期间的时间和位置:持续时间、速度和距离。动画的持续时间在动画属性中设置,并且不能根据屏幕尺寸进行更改。动画时间函数决定了速度;屏幕尺寸也不能改变这一点。因此,如果距离随屏幕尺寸变化,则除了特定屏幕宽度和高度之外,时间将不一致。

看看 坦克!在宽屏和窄屏尺寸下运行动画。虽然我让时间很接近,但如果你比较这两个,你会发现坦克在最后僵尸掉落时,相对于僵尸的位置不同。

Showing the same brown take, side by side, where the tank on the left is further along than the tank on the right.

为了避免这些时间问题,你可以使用固定单位和一个较大的数字,例如 2000 或 5000 像素或更多,这样动画将覆盖除了最大显示器之外的所有屏幕的宽度(或高度)。

经验 3:如果你想要一个响应式动画,请将所有内容放在(其中一个)视口单位中。

在单位比例上做一半(例如,将宽度和高度设置为像素,但将位置和移动设置为视口单位)会导致不可预测的结果。也不要同时使用 vw 和 vh,而要使用其中一个;哪个是占主导地位的方向。混合使用 vh 和 vw 单位会导致你的动画变得“古怪”,我认为这是技术术语。

超级僵尸化 为例。它混合了像素、vw 和 vh 单位。前提是超级僵尸向上飞,而“摄像机”跟随。超级僵尸撞到一个壁架上,并随着摄像机继续移动而落下,但如果你的屏幕足够高,你就不会理解这一点。

Two animation frames, side by side where the left shows the flying green zombie hitting a building ceiling and the right shows the zombie leaving the frame after impact.

这也意味着,如果你需要一个物体从顶部进入——就像我在 这里只有我们人类 中做的那样——你必须将 vw 高度设置得足够高,以确保忍者僵尸在大多数纵横比下不可见。

经验 3A:在 SVG 元素内部使用像素单位进行移动。

所有这些说的是,在 SVG 元素内部变换元素不应该使用视口单位。SVG 标签有它们自己的比例宇宙。SVG“像素”将在 SVG 元素内部保持与所有其他 SVG 元素子元素的比例,而视口单位则不会。因此,在 SVG 元素内部使用像素单位进行变换,但在其他地方使用视口单位。

经验 4:SVG 在运行时缩放得很糟糕。

对于动画,例如 糟糕…,我将僵尸的 SVG 图片放大到原来的五倍,但这使得边缘变得模糊。[对着“可缩放”矢量图形挥舞拳头。]

/* Original code resulting in fuzzy edges */
.zombie {
  transform: scale(1);
  width: 15vw;
}

.toggle-checkbox:checked ~ .zombie {
  animation: 5s ease-in-out 0s reverseshrinkydink forwards;
}

@keyframes reverseshrinkydink {
  0% {
    transform: scale(1);
  }
  100% {
    transform: scale(5);
  }
}

我学会了将它们的尺寸设置为在动画结束时生效的最终尺寸,然后使用缩放变换将它们缩小到动画开始时的尺寸。

/* Revised code */
.zombie {
  transform: scale(0.2);
  width: 75vw;
}

.toggle-checkbox:checked ~ .zombie {
  animation: 5s ease-in-out 0s reverseshrinkydink forwards;
}

@keyframes reverseshrinkydink {
  0% {
    transform: scale(0.2);
  }
  100% {
    transform: scale(1);
  }
}

简而言之,修改后的代码将从缩小版本的图像移动到全宽度和高度。浏览器始终以 1 的比例渲染,在 1 的比例下,边缘清晰锐利。因此,我将比例从 1 到 5 更改为从 0.2 到 1。

The same animation frame of a scientist holding a coffee mug standing to the left of a growing zombie where the frame on the left shows the zombie with blurry edges and the frame on the right is clear.

经验 5:坐标轴不是一个普遍的真理。

元素的坐标轴与其本身保持同步,而不是页面。在translateX之前进行 90 度旋转会将translateX的方向从水平变为垂直。在 这里只有我们人类… 2,我使用 180 度旋转来翻转僵尸。但是正 Y 值将忍者移动到顶部,负 Y 值将忍者移动到底部(与正常情况相反)。注意旋转可能会如何影响后续的变换。

Showing the main character facing us in the foreground with 7 ninja characters hanging upside down from the ceiling against a light pink background.

经验 6. 将复杂的动画分解成同心元素,以方便调整。

在创建多个方向移动的复杂动画时,添加包装 div,或者更确切地说,父元素,并单独为每个元素设置动画,将减少冲突的变换,并防止你变成一团糟。

例如,在 Space Cadet中,我使用了三种不同的变换。第一个是僵尸宇航员的上下运动。第二个是横跨屏幕的移动。第三个是旋转。与其尝试在一个变换中完成所有操作,我添加了两个包裹元素,并在每个元素上进行一个动画(我也保住了我的头发……至少一部分)。这有助于避免上一课中讨论的轴问题,因为我在最内层的元素上执行了旋转,使它的父级和祖父母级轴保持原位。

第七课:SVG 和 CSS 变换相同。

某些路径、组和其他 SVG 元素可能已经定义了变换。它可能来自优化算法,或者可能是插图软件生成代码的方式。如果 SVG 中的路径、组或任何元素已经具有 SVG 变换,则移除该变换将重置元素,通常与图形的其余部分相比,将元素重置到奇怪的位置或大小。

由于 SVG 和 CSS 变换相同,因此你所做的任何 CSS 变换都会替换 SVG 变换,这意味着你的 CSS 变换将从那个奇怪的位置或大小开始,而不是在 SVG 中设置的位置或大小。

你可以将 SVG 元素中的变换复制到你的 CSS 中,并将其设置为 CSS 中的起始位置(当然要先将其更新为 CSS 语法)。然后,你可以在 CSS 动画中修改它。

例如,在 Uhhh, Yeah… 中,我对Office Space的致敬,不死族 Lumbergh 的右上臂(#arm2 元素)在原始 SVG 代码中有一个变换。

<path id="arm2" fill="#91c1a3" fill-rule="nonzero" d="M0 171h9v9H0z" transform="translate(0 -343) scale(4 3.55)"/>
A side by side comparison of a zombie dressed in a blue button-up shirt and black suspenders while holding a coffee cup. On the left, the arm holding the coffee mugs the the correct position but the right shows the arm detached from the body.

将该变换移动到 CSS 中,像这样

<path id="arm2" fill="#91c1a3" fill-rule="nonzero" d="M0 171h9v9H0z"/>
#arm2 {
  transform: translate(0, -343px) scale(4, 3.55);
}

… 然后我可以创建一个动画,不会意外地重置位置和比例

.toggle-checkbox:checked ~ .z #arm2 { 
  animation: 6s ease-in-out 0.15s arm2move forwards;
}

@keyframes arm2move {
  0%, 100% {
    transform: translate(0, -343px) scale(4, 3.55);
  }
  40%, 60% {
    transform: translate(0, -403px) scale(4, 3.55);
  }
  50% {
    transform: translate(0, -408px) scale(4, 3.55);
  }
} 

当生成 SVG 代码的工具尝试将变换“简化”为矩阵时,此过程会更难。虽然你可以通过将矩阵变换复制到 CSS 中来重新创建它,但这是一项艰巨的任务。如果你能够取一个矩阵变换并操作它以按你想要的方式进行缩放、旋转或平移,那么你比我更优秀——这可能总是正确的。

或者,你可以使用平移、旋转和缩放来重新创建矩阵变换,但如果路径很复杂,你及时重新创建它的可能性很小,而且你不会发现自己身处困境。

最后一个也是最简单的选择是将元素包裹在一个组(<g>)标签中。为其添加一个类或 ID 以便于 CSS 访问,并变换组本身,从而像上一课中讨论的那样分离变换。

第八课:在变换 SVG 的一部分时,通过使用 transform-origin 保持理智

CSS transform-origin 属性移动变换发生的点。如果你尝试旋转一只手臂——就像我在 Clubbin’ It 中做的那样——你的动画如果从肩膀的中心旋转手臂,看起来会更自然,但该路径的自然变换原点位于左上角。使用 transform-origin 来修复此问题,以获得更流畅、更自然的感觉……你知道那种非常自然的像素艺术风格……

Four sequential frames of an animation showing a caveman character facing left, holding a large wooden club, and raising it up from the bottom to behind his head.

变换原点在缩放时也很有用,就像我在 Mustachioed Oops 中做的那样,或者在旋转嘴巴动作时,比如 Super Tasty 中的恐龙的下巴。如果你不改变原点,变换将使用 SVG 元素左上角的原点。

第九课:精灵动画可以响应式

我最终为这个项目做了很多精灵动画(即,使用多个增量帧并在它们之间快速切换,使角色看起来在移动)。我在一个宽文件中创建了图像,将它们作为背景图像添加到一个与单个帧大小相同的元素中,使用 background-size 将背景图像设置为图像的宽度,并隐藏了溢出。然后,我使用 background-position 和动画时序函数 step() 来遍历图像;例如:Post-Apocalyptic Celebrations

在项目开始之前,我一直使用不可变的图像。我会将它们缩小一点,以便至少有一点响应式空间,但我认为你无法使其完全灵活的宽度。但是,如果你使用 SVG 作为背景图像,你就可以使用视窗单位来随着屏幕大小的变化来缩放元素。唯一的问题是背景位置。但是,如果你对它使用视窗单位,它将保持同步。在 Finally, Alone with my Sandwich… 中查看它

第九课 A:在创建响应式精灵动画时,使用视窗单位来设置图像的背景大小

正如我在整个项目中所学到的那样,使用单一类型的单位几乎总是最好的方法。最初,我使用百分比来设置精灵的背景大小。数学很简单(100% * (步骤数量 + 1)),并且在大多数情况下都很好用。但是,在较长的动画中,精确的帧跟踪可能会出错,并且可能会显示错误的精灵帧的一部分。随着精灵中添加的帧数量增加,问题也会加剧。

我不确定这到底是什么原因,但我认为这是由于在精灵表长度上累积的舍入误差造成的(偏移量随着帧数的增加而增加)。

对于我的最终动画,It Ain’t Over Till the Zombie Sings,我让一只恐龙张开嘴,露出一个唱着歌的僵尸维京人(同时背景中有激光发射,当然还有跳舞、手风琴演奏和从大炮中发射的僵尸)。是的,我知道怎么开派对……一场极客派对。

恐龙和维京人是该项目中最长的精灵动画之一。但是,当我使用百分比来设置背景大小时,跟踪在 Safari 中的某些尺寸下会出错。在动画结束时,来自不同帧的恐龙鼻子的一部分会出现在右边,而左边也会缺失类似的部分。

A large green dinosaur behind a crowd of people, all facing and looking forward.
左边的恐龙缺少左脸颊的一部分,并在右脸颊旁边长出了新的脸颊。

这非常令人沮丧,因为在 Chrome 中它似乎运行良好,我认为我在 Safari 中修复了它,结果查看稍微不同的屏幕尺寸时,又发现帧不匹配了。但是,如果我使用一致的单位——即,background-size、帧宽度和 background-position 使用 vw——一切都运行良好。再次强调,这归结于使用一致的单位!

第十课:邀请人们参与项目。

A crowd of 32 pixel-art characters from the previous demos facing the screen.

虽然我在这个过程中学到了很多东西,但我大部分时间都在撞墙(通常是直到墙破了或者我的头破了……我分不清)。虽然这是一种方法,但即使你头很硬,最终也会头疼。邀请其他人参与你的项目,无论是为了获得建议、指出你忽略的明显盲点、提供反馈、帮助完成项目,还是仅仅是为了鼓励你在范围愚蠢而任意地大的时候继续下去。

所以,让我将这节课付诸实践。你的想法是什么?你将如何用 CSS 动画来阻止僵尸群?你将承担哪些愚蠢而任意地大的项目来挑战自己?