如何创建纯 CSS 插图并为其添加动画效果 - 第三部分
下一步
实用工具和资源
这是关于 CSS 插图和动画的三篇系列文章的第三篇。在最后一部分中,我们将构建一个动画灯塔场景。为此,我们将学习一些新技术,例如 SASS 循环和 CSS 3D 变换。
第一部分:学习 CSS 笑脸的基础知识和工作流程技巧;
第二部分:CSS 宝丽来动画入门;
第三部分:CSS 灯塔场景的高级技巧。
我们将要建造的东西如下:
由于这幅图比之前的图稍微复杂一些,我们会快速回顾一下已经学过的内容。不过,我会介绍一些新技术,例如 CSS 3D 变换、重复渐变和 SASS 循环。
在开始之前,我们先来观察一下这幅图的组成元素以及它们之间的重叠情况。我们的场景由两个主要部分组成:背景和灯塔。我们需要将背景设置在 z 轴堆叠的底部,将灯塔设置在顶部。这两个部分都包含许多重叠的元素,这很容易造成混淆。让我们使用 z-index 属性来确保每个元素都位于堆叠的正确位置。
这是我们基本的HTML/Pug树状结构。稍后我们会添加更多元素。
.scene
.background
.stars
.moon
.mountains
.sea
.waves
.boat
.lighthouse-group
.land
.lighthouse-holder
.shadow
.lighthouse
.top
.windows
.door
.stairs
在 SCSS 中,我们来定义颜色变量和全局属性:
$x-dark: #29284c;
$dark: #4c4b82;
$medium: #717ae1;
$light: #b9befa;
$x-light: #d6d9f6;
$aqua: #75e2fa;
$grey: #9e9ebe;
$yellow: #f7f2b4;
html,
body {
height: 100%;
width: 100%;
overflow: hidden;
padding: 0;
margin: 0;
}
html {
box-sizing: border-box;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
* {
position: absolute;
}
*:before,
*:after {
content: "";
position: absolute;
}
我们先从背景入手:
.scene {
width: 100vw;
height: 100vh;
}
.background {
background: $x-dark;
background-image: linear-gradient(
$x-dark 0%,
$dark 10%,
$medium 60%,
$aqua 90%
);
width: 100%;
height: 100%;
overflow: hidden;
z-index: 1;
}
.sea {
background: $x-dark;
background-image: linear-gradient(
to top,
$x-dark 0%,
$dark 30%,
$medium 60%,
$aqua 90%
);
width: 100%;
height: 170px;
bottom: 0;
left: 0;
z-index: 2;
}
该.scene元素将作为我们的主容器,我们希望它的宽度和高度与屏幕相同。这.background是我们的第一层,而.sea位于其上的元素则位于顶部。我们使用 `gradient` 属性为这两个元素应用渐变background-image效果。同样,在实际应用中,我们需要为该属性添加厂商前缀,但为了简洁起见,我们省略它们。
使用循环生成和随机化内容
接下来是星星部分。我们需要创建大约 60 颗星星,所以需要 60 个 HTML 元素。我们还需要为每个.star元素生成一个唯一的定位。这意味着需要 60 个不同的 CSS 类,或者说 60 次使用nth-child选择器。我们并不想这样做,因为这听起来既冗长又重复。所以,让我们使用循环吧。
在 Pug 和 SCSS 中,就像在 JavaScript 中一样,循环是一种强大的工具。它们可以让你轻松自动地生成内容等等。以下是它的工作原理。
首先,我们需要.star使用 Pug 创建 60 个元素。Pug 循环本质上就是语法略有不同的JavaScript 循环:
- for (var x = 0; x < 60; x++)
.star
就这么简单!只需要两行代码!
现在我们需要为这些元素设置样式。首先,我们创建一个.stars容器来容纳所有.star元素。然后,我们可以定义所有元素的通用样式。(闪烁动画我们稍后再做。)
.stars {
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
.star {
border-radius: 50%;
background-color: $light;
animation: twinkle 5s ease-in-out infinite;
}
}
我们图中仍然看不到任何星星,因为我们还没有设置它们的位置和大小。我们需要让每颗星星的这些属性都不同。就像我们之前提到的,我们可以逐个选中这些元素,并手动为每个元素编写不同的值,但我们已经决定不这样做。我们将使用 SASS 循环来随机生成这些值。以下是一个基本循环的语法:
@for $i from 1 through (60) {
// do something
}
为了针对每个元素设置不同的值,我们可以使用 nth-child 选择器:
@for $i from 1 through (60) {
.star:nth-child(#{$i}) {
// do something
}
}
在 CSS 中,这将编译成:
.star-nth-child(1) {
// do something
}
.star-nth-child(2) {
// do something
}
...
.star-nth-child(60) {
// do something
}
为了使这些值随机化,我们使用了 SASS 的 random 函数:
@for $i from 1 through (60) {
.star:nth-of-type(#{$i}) {
top: random(100)+vh;
left: random(100)+vw;
width: random(4)+px;
height: random(4)+px;
animation-delay: random(5)+s;
}
}
随机函数接受一个整数作为参数,并返回一个介于 1 和该整数之间的随机值。因此,random(100)它会返回一个介于 1 和 100 之间的随机数。然后,我们添加所需的单位作为后缀。我们希望星星分布在整个屏幕上,所以我为 ` topand`left属性使用了 vw/vh 单位。它会将星星放置在屏幕上的随机位置,并且由于随机函数定义在循环中,因此它会为每颗星星调用一次,每次都会生成一个新的位置。
设置起来很简单,但我们遇到了一个问题。仔细观察这些星星,你会发现它们不是圆形的。这是因为高度和宽度属性分别被赋予了不同的值,而这些值是由随机函数的不同实例生成的。为了解决这个问题,我们需要将随机函数的结果存储在一个变量中:
@for $i from 1 through (60) {
$size: random(4)+px;
.star:nth-of-type(#{$i}) {
top: random(100) + vh;
left: random(100) + vw;
width: $size;
height: $size;
animation-delay: random(5) + s;
}
}
好了。我们的星星目前还没有动画效果,但我们稍后会解决这个问题。
让我们补充一些背景细节:
月亮是通过简单的半径边框创建的,并结合了box-shadow使其周围发光的属性:
.moon {
width: 80px;
height: 80px;
top: 25%;
right: 10%;
border-radius: 50%;
z-index: 2;
background-color: $x-light;
box-shadow: 0 0 10px $x-light, 0 0 20px $x-light, 0 0 30px $x-light, 0 0 40px $x-light, 0 0 50px $aqua, 0 0 100px $x-light;
}
对于山脉,我们再次使用 Pug 环来创建四个元素。
.mountains
- for (var i = 0; i < 4; i++)
.mountain
每座山本质上都是一个旋转了 45 度的正方形,并且一半被.sea元素遮挡。然后,我们使用:after选择器为每座山添加渐变效果。我们还可以使用nth-child选择器为每座山设置不同的位置和大小。
.mountains {
width: 100%;
height: 250px;
bottom: 65px;
z-index: 3;
.mountain {
width: 250px;
height: 250px;
background-color: $medium;
right: 50px;
bottom: -40px;
transform: rotate(45deg);
border-radius: 3px;
&:after {
width: 100%;
height: 100%;
opacity: 0.7;
background-image: linear-gradient(135deg, $dark 0%, $medium 20%, $aqua 40%);
}
}
.mountain:nth-child(2) {
right: 220px;
width: 240px;
height: 240px;
z-index: 2;
}
.mountain:nth-child(3) {
right: 350px;
width: 260px;
height: 260px;
}
.mountain:nth-child(4) {
right: 130px;
width: 200px;
height: 200px;
z-index: 3;
bottom: -70px;
&:after {
background-image: linear-gradient(135deg, $dark 0%, $medium 6%, $aqua 20%);
}
}
}
为了添加海浪效果,我再次使用了 Pug 和 SASS 循环:
.sea
- for (var i = 0; i < 30; i++)
.wave
.wave {
background-color: $x-light;
height:3px;
border-radius: 100%;
opacity: 0.2;
animation: wave 5s linear infinite;
}
@for $i from 1 through (30) {
$size: random(100) + 50px;
.wave:nth-of-type(#{$i}) {
bottom: random(170) + px;
left: random(100) + vw;
width: $size;
opacity: random (5) * 0.1;
animation-delay: random(3) + s;
}
}
背景中的最后一个元素是船。它由三个主要部分组成:船底和两张帆。为了创建船底和帆的三角形形状,我们使用裁剪路径属性。这个工具非常适合快速创建裁剪路径形状。
.boat {
width: 90px;
height: 90px;
bottom: 90px;
.base {
width: 110px;
height: 25px;
bottom: -20px;
clip-path: polygon(0 0, 100% 0, 100% 100%, 20% 100%);
background-color: $dark;
}
.sail:nth-child(1) {
width: 90px;
height: 80px;
left: 5px;
clip-path: polygon(50% 0%, 0% 100%, 50% 100%);
background: linear-gradient($light 0%, $dark 60%);
}
.sail:nth-child(2) {
width: 80px;
height: 70px;
left: 15px;
bottom: 10px;
transform: scaleX(-1);
clip-path: polygon(50% 0%, 0% 100%, 50% 100%);
background: linear-gradient($light 0%, $dark 60%);
}
}
对第二个帆使用 transform: scaleX(-1) 进行水平翻转。
借助选择:before器:after,我们可以添加阴影和拖影。该z-index:-1属性确保这些元素位于其同级元素的后面:
.boat {
width: 90px;
height: 90px;
bottom: 90px;
&:after {
height: 8px;
width: 200px;
background: linear-gradient(90deg, transparentize($x-light, 0.3) 30%, rgba(255, 255, 255, 0) 100%);
border-radius: 40%;
top: 105px;
left: 20px;
z-index: -1;
}
&:before {
width: 92px;
height: 50px;
left: 25px;
bottom: -70px;
background: linear-gradient(to bottom, transparentize($x-dark, 0.2) 30%, transparentize($x-dark, 1) 100%);
z-index: -1;
}
//...
}
使用 CSS 变换
插图的背景部分已经完成。接下来我们来画灯塔。
我们将使用一个.lighthouse-groupdiv 元素作为这部分插图的主要容器。它包含两个主要部分:陆地和灯塔本身。让我们.lighthouse-group通过设置大小和位置将其放置在文档中:
.lighthouse-group {
width: 50%;
height: 100%;
bottom: 0;
left: 0;
z-index: 2;
}
我们先来加上那块地:
.land {
width: 400px;
height: 60px;
left: -30px;
bottom: 0;
background-image: linear-gradient(to top, $x-dark 80%, $medium 100%);
transform: skewX(35deg);
border-radius: 10px;
}
利用 transform 属性,我们可以倾斜元素,然后将其向左移动并隐藏左侧。
现在来说说灯塔本身,我们可以先给灯塔组赋予大小和位置:
.lighthouse-holder {
height: 480px;
width: 100px;
bottom: 80px;
left: 180px;
}
然后,为了使灯塔的顶部变窄,我们将使用两个变换属性,perspective和rotateX。
该perspective属性为灯塔元素设置了一个三维空间,然后该rotateX属性使灯塔元素在该三维空间中绕 X 轴旋转。从技术上讲,灯塔的顶部离我们更远,而底部离我们更近,但这可以产生我们想要的梯形效果。
.lighthouse {
width: 100%;
height: 100%;
transform: perspective(600px) rotateX(20deg);
background-color: $x-light;
}
如果你想了解更多关于 CSS 3D 变换的信息,这里有一个很棒的链接。
接下来,为了创建条纹,我们使用重复渐变:
.lighthouse {
width: 100%;
height: 100%;
transform: perspective(600px) rotateX(20deg);
background-color: $x-light;
background-image: repeating-linear-gradient(
-40deg,
transparent,
transparent 60px,
$dark 60px,
$dark 120px
);
}
我还想在上面叠加一层渐变来添加一些阴影。我们将使用:after伪选择器来实现这一点:
.lighthouse {
width: 100%;
height: 100%;
transform-style: preserve-3d;
transform: perspective(600px) rotateX(20deg);
background-color: $x-light;
background-image: repeating-linear-gradient(
-40deg,
transparent,
transparent 60px,
$dark 60px,
$dark 120px
);
&:after {
width: 100%;
height: 100%;
opacity: 1;
background-image: linear-gradient(
90deg,
transparentize($x-light, 0.4) 0%,
$x-dark 8%,
transparent 70%,
transparentize($x-light, 0.6) 100%
);
}
}
我们还需要在灯塔底部添加一个额外的阴影。我们可以skewX再次使用这个属性来实现:
.shadow {
width: 117px;
height: 50px;
left: -32px;
bottom: -70px;
background: $x-dark;
transform: skewX(-45deg);
}
要创建窗口,我们首先需要.window在 HTML 中添加四个元素。为了使它们之间保持间距,我们可以像之前一样使用 SASS 循环。不过这次,我们不用随机函数,而是使用简单的加法运算。
首先,我们将底部属性的初始值设置为 0。90px然后,在每次循环中,我们将该值加 90px。最终结果是四个相同的.window元素,它们在垂直方向上彼此间隔 90px。
.windows
- for (var i = 0; i < 4; i++)
.window
.windows {
height: 100%;
width: 100%;
.window {
background-color: $x-dark;
height: 25px;
width: 15px;
left: 43px;
border-bottom: 2px solid rgba($light, 0.7);
border-radius: 25px 25px 0 0;
}
$bottom: 90px;
@for $i from 1 through (4) {
.window:nth-of-type(#{$i}) {
bottom: $bottom;
}
$bottom: $bottom + 90px;
}
}
门的设计很简单。至于楼梯,我们再次使用渐变色组合perspective来rotateX得到一个梯形。然后,使用重复的渐变色来创建台阶。
.door {
background-color: $x-dark;
height: 40px;
width: 25px;
left: 38px;
bottom: -2px;
border-radius: 2px 2px 0 0;
.stairs {
width: 27px;
height: 28px;
background-color: $x-dark;
top: 34px;
left: -1px;
transform: perspective(100px) rotateX(45deg);
background-image: repeating-linear-gradient(
to bottom,
$x-dark,
$x-dark 4px,
$light 4px,
rgba(white, 0.1) 5px
);
}
}
好了,灯塔的下半部分完成了。我们开始做上半部分吧。
首先我们需要添加一些HTML元素:
.top
.light-container
.light
.rail
.middle
.roof
.roof-light
.glow
我们首先设置容器的大小和位置.top,然后就可以开始构建.base结构部分了。我们再次使用perspective“+”rotateX符号来赋予它形状,并使用重复渐变来创建边框。
.top {
width: 94px;
height: 60px;
left: 3px;
top: -15px;
.rail {
width: 100%;
height: 17px;
bottom: 1px;
border: 3px solid $x-dark;
border-radius: 1px;
transform: perspective(1000px) rotateX(-35deg);
background-image: repeating-linear-gradient(
90deg,
$x-dark,
$x-dark 3px,
$grey 3px,
$yellow 10px
);
background-position: -2px 0;
}
}
这.middle部分非常相似,只是我们不需要变换。我们还使用:before选择器在其后添加漂亮的发光效果:
.middle {
width: 88px;
height: 35px;
left: 3px;
bottom: 14px;
border: 2px solid $x-dark;
border-radius: 3px;
background-image: repeating-linear-gradient(
90deg,
$x-dark,
$x-dark 4px,
$grey 4px,
rgba(255, 255, 255, 0) 21px
);
background-position: -2px 0;
&:before {
width: 100%;
height: 100%;
z-index: -1;
background-color: $yellow;
box-shadow: 0 0 10px $x-light, 0 0 20px $yellow, 0 0 30px $yellow,
0 0 40px $yellow, 0 0 70px $yellow;
}
}
对于屋顶,我们可以使用边框技术来创建一个三角形形状。
在 CSS 中创建三角形的方法有很多种。之前我们用 `clip-path` 方法创建了船帆的三角形部分。但这次我想用 `<div>`:before和:after`<div>` 来创建屋顶的顶部,而 ` clip-path` 方法clip-path无法实现这一点。
.roof {
width: 0px;
height: 0px;
left: -3px;
bottom: 45px;
border-left: 50px solid rgba(255, 255, 255, 0);
border-right: 50px solid rgba(255, 255, 255, 0);
border-bottom: 40px solid $x-dark;
&:before {
width: 14px;
height: 14px;
left: -7px;
bottom: -7px;
background-color: $x-dark;
border-radius: 50%;
}
&:after {
width: 4px;
height: 14px;
left: -2px;
bottom: 5px;
background-color: $x-dark;
border-radius: 3px;
}
}
最后,我们需要给屋顶添加一些灯光。伪类选择器已经用完了,所以我们需要创建一个新的 HTML.roof-light元素。这次我使用clip-path属性来确保渐变效果包含在屋顶范围内:
.roof-light {
width: 100px;
height: 40px;
left: -50px;
clip-path: polygon(50% 0, 0% 100%, 100% 100%);
background-image: linear-gradient(
135deg,
$x-dark 40%,
rgba($yellow, 0.5) 100%
);
}
我们插图的最后一个元素是光。因为我们马上要在三维空间中为其添加动画效果,所以我们需要两个元素。第一个.light-container元素用于使光从后向前旋转。第二个.light元素用于赋予光梯形形状:
.light-container {
height: 40px;
width: 35vw;
bottom: 4px;
left: 40px;
transform-style: preserve-3d;
transform-origin: left bottom;
transform: perspective(500px) rotateY(0deg);
.light {
width: 100%;
height: 100%;
transform-style: preserve-3d;
transform-origin: left center;
transform: perspective(500px) rotateY(-35deg);
background: linear-gradient(90deg, $yellow 40%, rgba(255, 255, 255, 0) 100%);
}
}
最后一步是添加当光线朝向我们旋转时的光晕效果。目前我们将其不透明度设置为 0:
.glow {
width: 100px;
height: 60px;
top: 0;
left: 0;
background-color: $yellow;
opacity: 0;
border-radius: 50%;
box-shadow: 0 0 10px $yellow, 0 0 20px $yellow, 0 0 30px $yellow, 0 0 40px $yellow, 0 0 50px $yellow, 0 0 60px $yellow, 0 0 70px $yellow, 0 0 80px $yellow;
}
我们的插画完成了!这可真是一项浩大的工程,不过还没完工。接下来该制作动画了!
以下是我们要制作动画的元素:
- 星星会闪烁
- 波浪将从左向右移动
- 船会穿过屏幕。
- 当船远离月亮时,它的影子会发生变化。
- 灯光将从后向前旋转
- 当光线照射到我们身上时,就会出现光晕。
星星的动画很简单,因为我们只对透明度属性进行了动画处理。之前我们已经为星星元素添加了随机动画延迟,以确保动画不会同时在所有星星上播放。
@keyframes twinkle {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
.star {
//...
animation: twinkle 5s linear infinite;
}
波浪动画也很简单:
@keyframes wave {
0%,
100% {
transform: translateX(0);
}
50% {
transform: translateX(-10px);
}
}
.wave {
//...
animation: wave 5s linear infinite;
}
我们来制作船的动画。初始状态下,船需要位于屏幕外。我们可以利用translateX属性将船移动到屏幕最右侧。动画过程中,船会先出现在右侧,然后缓慢向左移动,最后再次消失在屏幕外。我还添加了一个scale(0.8)值来稍微缩小船的尺寸。
@keyframes boat {
0% {
transform: translateX(120vw) scale(0.8);
}
80%,
100% {
transform: translateX(-25vw) scale(0.8);
}
}
我们还需要制作它的影子在经过月亮前面,然后远离月亮时的动画:
@keyframes boatShadow {
0% {
transform: skewX(35deg) translateX(15px);
}
50%,
100% {
transform: skewX(-55deg) translateX(-40px);
}
}
$boatSpeed: 100s;
.boat {
//...
animation: boat $boatSpeed linear infinite;
&:after {
//...
animation: boatShadow $boatSpeed linear infinite;
}
}
灯光动画是最复杂的。它需要先旋转到背面,然后旋转到正面,最后在动画结束时回到初始状态。
初始状态是我们已经在选择器中定义的状态.light-container。要将其旋转到后方,我们绕其 Y 轴旋转 35 度。然后,要将其旋转到前方,我们执行相同的操作,但使用负值:
@keyframes lightRotate {
0%,
100% {
// initial and final state
transform: perspective(500px) rotateY(0deg);
}
25% {
//rotates to the back
transform: perspective(500px) rotateY(35deg);
}
75% {
// rotates to the front
transform: perspective(500px) rotateY(-35deg);
}
}
$lightSpeed: 40s;
.light-container {
//...
animation: lightRotate $lightSpeed linear infinite;
}
最后,我们还需要让光晕在光源朝向我们旋转时显现出来:
@keyframes lightGlow {
0%, 50%, 100% {
opacity: 0;
}
75% {
opacity: 1;
}
}
.glow {
//...
animation: lightGlow $lightSpeed linear infinite;
}
好啦,我们完成了!
3D变换可能有点难以理解,如果您遇到一些困难,这里有个小技巧:给所有应用了3D变换的元素添加边框。这样就能显示所有元素的形状和位置,包括隐藏的元素。
例如:
.light-container {
//...
border: 1px solid white;
.light {
border: 1px solid pink;
}
}
这是 Codepen 上的最终项目。
下一步
你已经按照教程完成了第一个 CSS 图片的创建和动画制作。接下来该做什么呢?如果你不知道从何入手,我建议你先从临摹现有的插图开始。在Dribbble上找一张你喜欢的简单插图作为参考(只要注明插图作者并链接到原图,完全没问题!)。从范例入手可以让你专注于代码和逻辑,而无需过多发挥创意。当你对整个流程足够熟悉后,你会发现从零开始创建自己的 CSS 图片会更容易。
一般来说,最好采用“扁平化设计”风格,即使用几何图形、无纹理和纯色,因为这种风格更容易用 CSS 实现。当然,只要发挥一点想象力并进行一些尝试,你也能获得令人惊艳的效果。以下是一些令人印象深刻的纯 CSS 插图示例:
- CSS Francine,作者:Diana Adrianne
- 大卫·库尔希德的《荡绳猫》
- Julia Muzafarova 的纯 CSS Moustached Nanny
- 只有中屋祐介的CSS Paper Bird
- CSS Camera作者:Cassidy Williams
- 玛乔·索布雷卡雷的《发光的流星》
实用工具和资源
好了,各位!希望你们喜欢这个系列,并且从中有所收获。欢迎在CodePen上关注我,看看我的最新动态,或者在Twitter上联系我,保持联系。
文章来源:https://dev.to/agathacco/how-to-create-pure-css-illustrations-and-animate-them---part-3-3e8a

















