使用 CSS 渐变绘制图像

Avatar of Jon Kantner
Jon Kantner

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 价值 200 美元的免费积分!

我所说的“CSS 图像”是指仅使用 HTML 元素和 CSS 创建的图像。它们看起来像是用 Adobe Illustrator 绘制的 SVG,但它们是在浏览器中直接制作的。我看到的一些技术包括调整边框半径、阴影以及有时使用 clip-path。如果您在 CodePen 上 搜索“daily css images”,可以找到许多很棒的示例。我自己也画了一些,包括这个 无限手套,但我只用一个元素,仅使用背景和最少的其他属性。

让我们看看如何自己创建这种 CSS 图像。

方法

理解 background 简写语法以及 CSS 渐变 的工作原理,实际上就是您用一个元素绘制任何内容所需的全部知识。作为回顾,参数如下

background: <'background-color'> || <image> || <position> [ / <size> ]? || <repeat> || <attachment> || <origin> || <clip>;

它们可以以任何顺序出现,但 positionsize 之间必须有一个 /。我们也必须按照这种顺序保持这两个参数,否则我们将得到意外的结果。并非所有这些参数都需要包含,并且出于此目的,我们不会使用 colorrepeatattachmentoriginclip 参数。这将使我们剩下 imagesizeposition。但是,由于背景默认情况下会重复,因此我们必须在 background 中的所有内容下方放置 background-repeat: no-repeat;(如果某些背景应该重复,我们可以使用 repeating-linear-gradient()repeating-radial-gradient())。在这种情况下,骨架 CSS 将是这个

.image {
  background: <image> <position> / <size>;
  background-repeat: no-repeat;
}

我们甚至可以使用多组背景参数!因此,我们可以像这样用逗号将它们堆叠和分隔开

.image {
  background:
    <image> <position> / <size>,
    <image> <position> / <size>,
    <image> <position> / <size>;
  background-repeat: no-repeat;
}

上面的结构是我们绘制图像的基础——每种形状一行。请记住,渲染顺序与绝对定位或固定定位元素的顺序相反。第一个将显示在顶部,而不是在底部。换句话说,下面的圆圈(径向渐变)将从底部到顶部渲染(蓝色在底部,红色在顶部)。

.circles {
	background:
    radial-gradient(7em 7em at 35% 35%, red 50%, transparent 50%),
    radial-gradient(7em 7em at 50% 50%, gold 50%, transparent 50%),
    radial-gradient(7em 7em at 65% 65%, blue 50%, transparent 50%);
  background-repeat: no-repeat;
  width: 240px;
  height: 240px;
}

绘制

我们将使用 Sass (SCSS) 来绘制这些图像,以便我们可以使用变量来创建调色板。这样可以使代码更短,更易于阅读和更改深色或浅色变体。我们可以使用 普通 CSS 中的变量 并忘记 Sass,但由于 Internet Explorer 不支持,让我们坚持使用 Sass。为了解释它的工作原理,我们将使用线性渐变和径向渐变来尝试使用形状。

设置调色板

我们的调色板将由 RGB 或 HSL 颜色组成。我稍后会解释为什么要将颜色保持在这些格式中。在此示例中,我们将使用 RGB。

$r: rgb(255,0,0); // hsl(0,100%,50%)
$o: rgb(255,128,0); // hsl(32,100%,50%)

我喜欢做的是使用最少的字母来表示每种颜色(例如,$r 代表红色)。如果使用一种颜色的深色或浅色色调,我在基本字母或字母前添加一个 d 代表深色,或者添加一个 l 代表浅色。我将使用 $dr 代表深红色,$lr 代表浅红色。如果需要超过两种其他色调,则我在末尾添加一个数字来表示色调级别。例如,$dr1 代表深红色,$dr2 代表更深的红色,$lr1 代表浅红色,$lr2 代表更浅的红色。这样的调色板将如下所示(深色在前,正常色在中间,浅色在后)

$dr1: rgb(224,0,0);
$dr2: rgb(192,0,0);
$r: rgb(255,0,0);
$lr1: rgb(255,48,48);
$lr2: rgb(255,92,92);

设置比例和画布

我们将使用 em 单位作为图像尺寸,以便图像可以轻松地按比例调整大小。由于 1em 等于元素的字体大小,如果更改图像,图像的每个单位也会相应调整。让我们设置 10px 的字体大小,并将宽度和高度都设置为 24em。10px 的单位最容易想到,因为如果您在脑子里计算,您会立即得到 240 × 240px。然后,为了查看画布边缘的位置,我们将使用 1px 的灰色轮廓。

$r: rgb(255,0,0); // hsl(0,100%,50%)
$o: rgb(255,128,0); // hsl(32,100%,50%)

.image {
  background-repeat: no-repeat;
  font-size: 10px;
  outline: 1px solid #aaa;
  width: 24em;
  height: 24em;
}

但是请注意,使用较小的字体大小;浏览器有一个最小的字体大小设置(出于可访问性原因)。如果您设置 4px 的字体大小,而最小值为 6px,它将被强制设置为 6px。

此外,您可以通过使用 calc() 和视窗单位来启用响应能力。也许我们可以使用类似 calc(10px + 2vmin) 的东西,如果需要,但现在让我们坚持使用 10px。

绘制形状

有趣的部分从这里开始。要绘制一个位于中心的 8 × 8em 的正方形,我们使用一个具有两个相同颜色停止点的 linear-gradient()

.image {
  background:
    linear-gradient($r, $r) 50% 50% / 8em 8em;
  ...
}

要将其塑造成更像梯形的东西,请将角度设置为 60deg。同时,让我们在调色板中添加 $T 代表 transparent,然后将 $r$T 的两个停止点都放在 63%(在右下角被切掉之前)。

$T: transparent;

.image {
  background:
    linear-gradient(60deg,$r 63%, $T 63%) 50% 50% / 8em 8em;
  ...
}

将两个停止点都设置为相同的值会使倾斜边与其他边一样清晰。但如果您仔细观察,您会发现它似乎是像素化的。

要更正此问题,我们稍微调整一下其中一个停止点(大约 1% 或左右),以便边缘足够平滑。因此,让我们将 $r 的 63% 更改为 62%。

使用径向渐变处理圆形边缘时,这也是一个问题,我们将在后面看到。如果您在 Safari 以外的浏览器中查看,即使过渡到非透明颜色(例如橙色),一切看起来都很棒。在 Safari 中过渡到 transparent 的问题是,您会在倾斜边看到一些黑色线条。

这是因为 Safari 中的 transparent 关键字始终是黑色透明度,因此我们会看到一些黑色。我真的希望苹果能解决这个问题,但他们永远不会。暂时,让我们在 $r 下添加一个新的 $redT 变量来表示红色透明度。放弃我们用于 transparent$T,因为我们不再使用它了。

$rT: rgba(255,0,0,0); // hsla(0,100%,50%,0)

然后让我们用 $redT 替换 transparent。这样就解决了我们的 Safari 问题!

.image {
  background:
    linear-gradient(60deg,$r 62%, $rT 63%) 50% 50% / 8em 8em;
  ...
}

如果您一直在想为什么我们没有使用十六进制颜色,因为 Internet Explorer 和 Edge 不支持 #rgba 和 #rrggbbaa 符号(是的,十六进制颜色从 2016 年末开始就有了 alpha 通道,如果您不知道的话),而且我们希望它尽可能跨浏览器工作。我们还希望保持我们对颜色格式的选择的一致性。

现在让我们将形状垂直移动到 20%,并绘制一个相同尺寸的橙色圆圈。另外,为它的透明版本添加另一个变量。为了使边缘平滑,在实心橙色和透明橙色之间插入 1% 的间隙。

$oT: rgba(255,128,0,0); // hsla(32,100%,50%,0)

.image {
  background:
    linear-gradient(60deg,$r 62%, $rT 63%) 50% 20% / 8em 8em,
    radial-gradient(8em 8em at 50% 80%, $o 49%, $oT 50%);
  ...
}

为了保持我们尺寸的一致性,第二个颜色停止点应该在 50% 而不是 100%。

定位形状

渐变的定位方式取决于单位是固定的还是百分比。假设我们将两个渐变都变成正方形,并尝试将它们全部横向放置在 div 上。

.image {
  background:
    linear-gradient($r, $r) 24em 20% / 8em 8em,
    linear-gradient($o, $o) 100% 80% / 8em 8em;
  ...
}

红色正方形最终完全超出画布(带轮廓),橙色正方形的右侧接触另一侧。使用固定单位就像放置绝对定位元素或在 HTML5 画布中绘制形状一样。从这个意义上说,原点确实位于左上角。当使用百分比和设置背景大小时,div 会获得背景大小一半的“假填充”。同时,背景的原点是居中的(不要与 background-origin 混淆,background-origin 与盒子角相关)。

现在,如果我们将这些渐变变成径向渐变作为圆圈,并应用相同的 x 坐标 24em 和 100%,两者都将最终在另一侧被切成两半。这是因为如果我们像这样编写背景,原点始终位于中心

.image {
	background:
  	radial-gradient(8em 8em at 24em 20%, $r 49%, $rT 50%),
  	radial-gradient(8em 8em at 100% 80%, $o 49%, $oT 50%);
	...
}

如果我们重新编写背景,使位置和大小在渐变之后,并使用 100% 100% at center,它们将像线性渐变一样定位。红色最终会在外面,橙色会接触到右边缘。“假填充”再次出现在橙色上。

.image {
  background:
    radial-gradient(100% 100% at center, $r 49%, $rT 50%) 24em 20% / 8em 8em,
    radial-gradient(100% 100% at center, $o 49%, $oT 50%) 100% 80% / 8em 8em;
  ...
}

没有一种单一正确的方式来定位形状,但是要像绝对定位或固定定位的 HTML 元素一样放置它,请使用固定单位。如果需要快速定位形状(使用 position / size 参数)在正中央,50% 是最佳选择,因为形状的原点将是它的中心。如果它应该接触最右侧,请使用 100%。

调整形状大小

CSS 背景中的大小调整按预期工作,但仍受用于定位的单位类型影响——固定或百分比。如果我们再次使用我们的正方形并将它们的宽度更改为 10em,红色正方形会向右扩展,橙色正方形会向两侧扩展。

.image {
  background:
    linear-gradient($r, $r) 12em 20% / 10em 8em,
    linear-gradient($o, $o) 50% 80% / 10em 8em;
  ...
}

如果我们对 y 轴位置使用 em 单位,则在更改高度后,形状将向下增长或向上收缩。如果我们使用百分比,则它将在两个垂直方向上扩展。

刚才,我们研究了使用径向渐变绘制圆圈的两种方法。第一种方法是在 (at 之间指定宽度和高度,然后指定位置。

.image {
  background:
    radial-gradient(8em 8em at 50% 50%, $r 49%, $rT 50%);
  ...
}

第二种方法是在中心使用 100% 100%,然后给出位置和大小。

.image {
  background:
    radial-gradient(100% 100% at 50% 50%, $r 49%, $rT 50%) 50% 50% / 8em 8em;
  ...
}

这两种方法都绘制圆圈,但会产生不同的输出,因为

  1. 第一种方法占用了整个 div,因为没有真正的背景位置或大小。
  2. 对第二种方法给出一个真实的位置和大小会为它设置一个边界框。因此,它的行为就像线性渐变形状一样。

假设我们将 $rT 替换为 $o。您会看到橙色会覆盖原来是白色或它下面的形状(如果我们添加了任何形状)的第一种方法。使用第二种方法,您会很容易注意到橙色揭示的边界框。

此外,使用 100% 100% 而不是使用 circleellipse 的目的是让圆圈占据整个边界框。它甚至可以让我们完全控制它的尺寸。这样一来,即使您将 50% 50% 位置更改为其他位置,它也会保持不变。如果使用这两个关键字中的一个,圆圈的边缘在居中时只停留在大约 71% 的位置,并且如果调整其位置,它会变得更加扭曲。例如,以下是在将 circleellipse 的 x 位置更改为 0 时会发生的情况。

从长远来看,您可以将语法重新想象为 radial-gradient(width height at x y)radial-gradient(100% 100% at x-in-bounding-box y-in-bounding-box) x y / width height。如果您只是绘制一个圆形或椭圆形,可以使用第一种方法简化代码。如果绘制圆形的一部分或环的一部分,则第二种方法会派上用场。在接下来创建的示例中,我们将有许多这种方法的应用。

示例

准备好现在真实地绘制一些东西了吗?我们将逐步介绍三个示例。前两个将是静态的——一个有很多半圆,另一个处理一些圆角矩形。最后一个示例将更小,但重点是动画。

静态图像

这把遮阳伞将是我们第一个静态图像

我们将使用包含红色 ($r$rT)、白色 ($w$wT)、橙色 ($o$oT) 以及深橙色 ($do$doT) 的调色板。

$r: rgb(255,40,40);
$rT: rgba(255,40,40,0);

$w: rgb(240,240,240);
$wT: rgba(240,240,240,0);

$o: rgb(255,180,70);
$oT: rgba(255,180,70,0);

$do: rgb(232,144,0);
$doT: rgba(232,144,0,0);

让我们设置 30 × 29em 的绘图区域。

.parasol {
  // background to go here
  background-repeat: no-repeat;
  font-size: 10px;
  outline: 1px solid #aaa;
  width: 30em;
  height: 29em;
}

background-repeat 上方,我们将开始绘制遮阳伞的各个部分。首先,添加构成杆子的渐变(由于它们彼此不重叠,因此从下到上的顺序无关紧要)。

.parasol {
   background:
     // 1
     radial-gradient(200% 200% at 100% 100%, $do 49%, $doT 50%) 14em 0 / 1em 1em,
     radial-gradient(200% 200% at 0% 100%, $o 49%, $oT 50%) 15em 0 / 1em 1em,
     // 2
     linear-gradient(90deg, $do 50%, $o 50%) 14em 1em / 2em 25em,
     // 3
     radial-gradient(100% 200% at 50% 0, $oT 0.95em, $o 1em, $o 1.95em, $do 2em, $do 2.95em, $doT 3em) 14em 26em / 6em 3em,
     // 4
     radial-gradient(200% 200% at 100% 100%, $o 49%, $oT 50%) 18em 25em / 1em 1em,
     radial-gradient(200% 200% at 0% 100%, $do 49%, $doT 50%) 19em 25em / 1em 1em;
   ...
}
  1. 为了绘制杆子顶部的每一侧,我们使用了 1 × 1em 的四分之一圆。为了让它们占据它们的边界框,我们使用了大小是它们两倍的圆 (200% 200%),位于右下角和左下角。我们也可以使用像 right bottomleft bottom 这样的关键字对,但使用百分比等效项更短。请注意,停止点之间有 1% 的间隙,以确保平滑过渡。
  2. 对于长部分,我们使用了一个长方形,它有一个突兀的深橙色到橙色的渐变。由于我们没有处理曲线或斜线,因此不需要小数分隔的小间隙。
  3. 这部分杆子比其他部分更难绘制,因为我们必须保持 2em 的直径。为了绘制这个圆弧,我们使用了一个 6 × 3em 的盒子,这样在两个也是 2em 的端点之间有一个 2em 的空间。然后,我们在中心顶部使用一个径向渐变,其中每个停止点都发生 1em(并通过 0.05em 平滑地扩展)。
  4. 最后两个与第一个相同,只是它们位于圆弧的右侧末端,以便它们无缝地匹配。此外,颜色互换位置。

然后,在之前的渐变上方,从下到上添加以下内容,以绘制雨伞的顶部,不包括尖端。

.parasol {
  background:
    radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 9em 12em,
    radial-gradient(100% 200% at 50% 100%, $w 50%, $wT 50.25%) 50% 1.5em / 21em 12em,
    radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 30em 12em,
  ...
}

为了绘制构成这部分的半圆,我们使用了 100% 200% 的渐变大小,这使得每个直径适合其背景宽度,但高度是其两倍,并在底部居中。通过从下到上排序它们,使得最大的在底部,最小的在顶部,我们得到了我们想要的曲线。

随着我们的渐变堆栈越来越高,过一段时间后,确定哪个背景或背景组对应于图像的哪个部分将变得很困难。因此,为了便于识别,我们可以将它们分成由描述每个组用途的注释引导的组。现在,我们已将堆栈分为遮阳伞顶部和杆子的组。

.parasol {
  background:
    /* top */
    radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 9em 12em,
    radial-gradient(100% 200% at 50% 100%, $w 50%, $wT 50.25%) 50% 1.5em / 21em 12em,
    radial-gradient(100% 200% at 50% 100%, $r 50%, $rT 50.25%) 50% 1.5em / 30em 12em,
    
    /* pole */
    radial-gradient(200% 200% at 100% 100%, $do 49%, $doT 50%) 14em 0 / 1em 1em,
    radial-gradient(200% 200% at 0% 100%, $o 49%, $oT 50%) 15em 0 / 1em 1em,
    linear-gradient(90deg, $do 50%, $o 50%) 14em 1em / 2em 25em,
    radial-gradient(100% 200% at 50% 0, $oT 0.95em, $o 1em, $o 1.95em, $do 2em, $do 2.95em, $doT 3em) 14em 26em / 6em 3em,
    radial-gradient(200% 200% at 100% 100%, $o 49%, $oT 50%) 18em 25em / 1em 1em,
    radial-gradient(200% 200% at 0% 100%, $do 49%, $doT 50%) 19em 25em / 1em 1em;
    ...
}

然后,在顶部和杆子之间,我们将添加下一块背景,以呈现尖端。为了确定每个片段的宽度,我们必须获得红色和白色相遇的每个点的距离。它们都必须加起来为 30em。

从白色和最窄的红色半圆开始,我们将红色的宽度 9em 从白色的宽度 21em 中减去,并将结果除以 2,得到两个白色片段的宽度(图中的点“b”)。因此,结果将是 6em(b = (21 – 9) / 2 = 6)。然后中间的红色片段将是 9em (21 – (6 + 6) = 9)。现在我们剩下的是外侧的红色片段(图中的点“a”)。从我们现在拥有的总和中减去较大红色半圆的宽度,并将结果除以 2。这将使点 a 的值成为:(30em – (6 + 6 + 9)) / 2 = 4.5em。

.parasol {
   background:
     ...
     /* pointy ends */
     radial-gradient() 0 13.5em / 4.5em 3em,
     radial-gradient() 4.5em 13.5em / 6em 3em,
     radial-gradient() 50% 13.5em / 9em 3em,
     radial-gradient() 19.5em 13.5em / 6em 3em,
     radial-gradient() 25.5em 13.5em / 4.5em 3em,
     ...
}

为了绘制与我们绘制顶部部分类似的半圆,我们从每个形状的颜色透明对应部分开始,以便它们类似于弧形桥。我们还将为每个渐变宽度(不是背景框宽度)额外添加 5%,以便由相邻背景形成的每个点不会过于尖锐和细薄。

.parasol {
   background:
     ...
     /* pointy ends */
     radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 0 13.5em / 4.5em 3em,
     radial-gradient(105% 200% at 50% 100%, $wT 49%, $w 50%) 4.5em 13.5em / 6em 3em,
     radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 50% 13.5em / 9em 3em,
     radial-gradient(105% 200% at 50% 100%, $wT 49%, $w 50%) 19.5em 13.5em / 6em 3em,
     radial-gradient(105% 200% at 50% 100%, $rT 49%, $r 50%) 25.5em 13.5em / 4.5em 3em,
     ...
}

最后,您将不再需要 1px solid #aaa 的轮廓。我们的遮阳伞完成了!

请参阅 CodePen 上 Jon Kantner (@jkantner) 的 遮阳伞

带圆角矩形的图形

下一个示例将是一个旧款 iPhone 型号,其中包含比新款型号更多的细节。这个模型的特点是两个圆角矩形,它们是主页按钮的外侧和中间。

调色板将包含黑色 ($bk$bkT) 用于主页按钮边缘、深灰色 ($dg$dgT) 用于机身、灰色 ($g$gT) 用于摄像头和扬声器、浅灰色 ($lg$lgT) 用于外框、蓝色 ($bl$blT) 用于摄像头镜头,以及非常深的紫色 ($p$pT) 用于屏幕。

$bk: rgb(10,10,10);
$bkT: rgba(10,10,10,0);

$dg: rgb(50,50,50);
$dgT: rgba(50,50,50,0);

$g: rgb(70,70,70);
$gT: rgba(70,70,70,0);

$lg: rgb(120,120,120);
$lgT: rgba(120,120,120,0);

$bl: rgb(20,20,120);
$blT: rgba(20,20,120,0);

$p: rgb(25,20,25);
$pT: rgba(25,20,25,0);

让我们在 20 × 40em 处设置画布,并使用与遮阳伞相同的字体大小,10px

.iphone {
  // background goes here
  background-repeat: no-repeat;
  font-size: 10px;
  outline: 1px solid #aaa;
  width: 20em;
  height: 40em;
}

在我们开始绘制第一个圆角矩形之前,我们需要考虑圆角半径,它将是 2em。此外,我们希望在左侧留出一些空间用于锁定开关和音量按钮,它们将是 0.25em。因此,矩形将是 19.75 × 40em。考虑到 2em 的圆角半径,我们将需要两个相互交叉的线性渐变。一个必须有 15.75em 的宽度 (19.75em – 2 × 2),另一个必须有 36em 的高度 (40em – 2 × 2)。将第一个从左侧放置 2.25em,然后将第二个从左侧放置 0.25em,从顶部放置 2em。

.iphone {
  background:
    /* body */
    linear-gradient() 2.25em 0 / 15.75em 40em,
    linear-gradient() 0.25em 2em / 19.75em 36em;
  ...
}

由于浅灰色边框将是 0.5em 厚,让我们让每个渐变立即停止,从浅灰色 ($lg) 转换为深灰色 ($dg),反之亦然,在末端之前 0.5em 和 0.5em(对于第一个渐变为 40em – 0.5 = 39.5em,对于第二个渐变为 19.75em – 0.5 = 19.25em)。为第二个设置 90deg 的角度,使其变为水平。

.iphone {
  background:
    /* body */
    linear-gradient($lg 0.5em, $dg 0.5em, $dg 39.5em, $lg 39.5em) 2.25em 0 / 15.75em 40em,
    linear-gradient(90deg, $lg 0.5em, $dg 0.5em, $dg 19.25em, $lg 19.25em) 0.25em 2em / 19.75em 36em;
  ...
}

在每个方形角中,如图中橙色框所示,我们将放置圆角。为了创建这些形状,我们使用大小是它们的边界框两倍的径向渐变,并且位于每个角中。将它们插入机身背景之上。

.iphone {
  background:
    /* corners */
    radial-gradient(200% 200% at 100% 100%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 0.25em 0 / 2em 2em,
    radial-gradient(200% 200% at 0% 100%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 18em 0 / 2em 2em,
    radial-gradient(200% 200% at 100% 0%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 0.25em 38em / 2em 2em,
    radial-gradient(200% 200% at 0% 0%, $dg 1.45em, $lg 1.5em, $lg 50%, $lgT 51%) 18em 38em / 2em 2em,
    ...
}

为了获得 0.5em 厚的浅灰色末端,考虑渐变的起点,然后进行数学计算。因为浅灰色位于末端,所以我们从 2em 中减去 0.5em 以正确放置第一个停止点。为了平滑过渡,我们从第一个 1.5em 中减去一点,并在第二个 50% 中添加 1%,以便圆角与平角融合。

现在,如果我们通过将字体大小更改为 40px 或更大来放大图像,我们会注意到圆角和平角之间的接缝(橙色圈起来的部分)。

由于它们看起来很小,我们可以通过返回机身背景并稍微调整渐变停止点来轻松修补它们,只要在将字体大小更改回 10px 时一切看起来仍然正确即可。

.iphone {
  background:
    /* body */
    linear-gradient($lg 0.5em, $dg 0.55em, $dg 39.5em, $lg 39.55em) 2.25em 0 / 15.75em 40em,
    linear-gradient(90deg, $lg 0.5em, $dg 0.55em, $dg 19.175em, $lg 19.25em) 0.25em 2em / 19.75em 36em;
  ...
}

然后在一个线性渐变中,我们将添加锁定开关和音量按钮,以填充左侧的 0.25em 空间。如果按钮和机身之间将有 1px 的空间,我们可以在背景宽度中添加 0.05em 的微小出血(使其变为 0.3em),这样它就不会伸出深灰色部分。

.iphone {
  background:
    /* volume buttons */
    linear-gradient($lgT 5em, $lg 5em, $lg 7.5em, $lgT 7.5em, $lgT 9.5em, $lg 9.5em, $lg 11em, $lgT 11em, $lgT 13em, $lg 13em, $lg 14.5em, $lgT 14.5em) 0 0 / 0.3em 100%,
    ...
}

看起来我们可以使用三个浅灰色到浅灰色的渐变,但由于它可以实现,因此只需要一个。它只是很多从透明浅灰色到不透明浅灰色的突然过渡向下运行。

接下来,让我们添加 Home 按钮的边缘,以及它内部正方形的平边。现在 Home 按钮内的正方形将是 1.5 × 1.5em,并遵循与主体基本相同的步骤:两个相交的线性渐变和径向渐变来填充角。为了将它们水平放置在中心,calc() 会派上用场。表达式将是 50% + 0.125em;如果我们只将手机主体居中,则两侧将有 0.125em 的间距。因此,我们将它向右移动 0.125em。相同的 x 位置将应用于上面的两个背景。

.iphone {
  background:
    /* home button */
    linear-gradient() calc(50% + 0.125em) 36.5em / 0.5em 1.5em,
    linear-gradient() calc(50% + 0.125em) 37em / 1.5em 0.5em,
    radial-gradient(3em 3em at calc(50% + 0.125em) 37.25em, $bkT 1.25em, $bk 1.3em, $bk 49%, $bkT 50%),
    ...
}

与我们对手机主体线性渐变部分进行阴影处理的方式类似,停靠点将以浅灰色开头和结尾,但中间为透明。注意,我们在每个灰色到透明过渡(反之亦然)之间留下了 0.05em 的间隙。就像主体角一样,这样做是为了确保圆角和内部平坦端之间的混合。

.iphone {
  background:
    /* home button */
    linear-gradient($lg 0.15em, $lgT 0.2em, $lgT 1.35em, $lg 1.35em) calc(50% + 0.125em) 36.5em / 0.5em 1.5em,
    linear-gradient(90deg, $lg 0.15em, $lgT 0.2em, $lgT 1.3em, $lg 1.35em) calc(50% + 0.125em) 37em / 1.5em 0.5em,
    radial-gradient(3em 3em at calc(50% + 0.125em) 37.25em, $bkT 1.25em, $bk 1.3em, $bk 49%, $bkT 50%),
    ...
}

顺便说一句,由于轮廓将像您之前看到的那样很小,因此我们可以通过将字体大小增加到至少 20px 来更好地查看我们的操作。这就像在图像编辑软件中使用缩放工具一样。

现在,为了将灰色正方形的角精确地放到它们应该在的位置,让我们首先关注 x 位置。我们从 calc(50% + 0.125em) 开始,然后我们添加或减去每个部分的宽度,或者我应该说正方形的边框半径。这些背景将位于最后三个背景之上。

.iphone {
  background:
    /* home button */
    radial-gradient(200% 200% at 100% 100%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em - 0.5em) 36.5em / 0.5em 0.5em,
    radial-gradient(200% 200% at 0% 100%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em + 0.5em) 36.5em / 0.5em 0.5em,
    radial-gradient(200% 200% at 100% 0%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em - 0.5em) 37.5em / 0.5em 0.5em,
    radial-gradient(200% 200% at 0% 0%, $lgT 0.3em, $lg 0.35em, $lg 0.48em, $lgT 0.5em) calc(50% + 0.125em + 0.5em) 37.5em / 0.5em 0.5em,
    ...
}

然后,屏幕将是一个 17.25 × 30em 的矩形。就像 Home 按钮的某些部分一样,我们将使用 calc(50% + 0.125em) 将其水平居中。从顶部开始,它将是 5em。

.iphone {
  background:
    /* screen */
    linear-gradient($p, $p) calc(50% + 0.125em) 5em / 17.25em 30em,
    ...
}

最后,我们将添加摄像头和扬声器。摄像头是一个简单的 1 × 1 蓝色到灰色的径向渐变,没有花哨的计算。纯灰色的扬声器将稍微复杂一些。它将是一个 5 × 1em 的矩形,并有一个 0.5em 的边框半径。为了绘制它,我们首先使用前 4em 的宽度绘制一个矩形,并使用 calc(50% + 0.125em) 将其居中。然后,我们使用 0.5 × 1em 的半圆,其中渐变位置分别是 100% 50%0% 50%。将这些半圆放置在矩形的左侧和右侧的最佳方法是使用一些新的 calc() 表达式。对于左侧,我们将从主体中心减去一半矩形宽度和一半半圆宽度(50% + 0.125em - 2em - 0.25em)。右侧将遵循相同的模式,但使用加法,因此是 50% + 0.125em + 2em + 0.25em

.iphone {
  background:
    /* camera */
    radial-gradient(1em 1em at 6.25em 2.5em, $bl 0.2em, $g 0.21em, $g 49%, $gT 50%),
    /* speaker */
    radial-gradient(200% 100% at 100% 50%, $g 49%, $gT 50%) calc(50% + 0.125em - 2em - 0.25em) 2em / 0.5em 1em,
    radial-gradient(200% 100% at 0% 50%, $g 49%, $gT 50%) calc(50% + 0.125em + 2em + 0.25em) 2em / 0.5em 1em,
    linear-gradient($g, $g) calc(50% + 0.125em) 2em / 4em 1em,
    ...
}

删除 div 周围的灰色轮廓,iPhone 就完成了!

查看 CodePen 上 Jon Kantner (@jkantner) 创建的 Pen iPhone

动画图像

您可能在想,您可以使用 background-position 来为这类图像制作动画,但您只能做这么多。例如,无法单独为单个背景的旋转制作动画。事实上,background-position 动画的性能通常不如 transform 动画,因此我不推荐使用它。

为了以我们希望的任何方式为图像的任何部分制作动画,我们可以让 :before:after 伪元素负责该部分。如果我们需要更多选择,那么我们可以恢复到使用多个子 div,但不需要为每个小细节使用一个 div。对于我们的动画图像示例,我们将创建这个动画雷达

我们首先绘制静态部分,即除灰色框架和旋转指针以外的所有部分。在此之前,让我们提供调色板(注意:我们不需要 $dgnT 对应 $dgn)和基础代码。

$gn: rgb(0,192,0);
$gnT: rgba(0,192,0,0);
$dgn: rgb(0,48,0);
$gy: rgb(128,128,128);
$gyT: rgba(128,128,128,0);
$bk: rgb(0,0,0);
$bkT: rgba(0,0,0,0);

.radar {
  background-repeat: no-repeat;
  font-size: 10px;
  outline: 1px solid #aaa;
  width: 20em;
  height: 20em;
}

由于此图像将完全圆形,因此我们可以安全地应用 50% 的边框半径。然后,我们可以使用重复的径向渐变来绘制圆环——彼此之间的距离约为 1/3。

.radar {
  background:
    /* rings */
    repeating-radial-gradient($dgn, $dgn 2.96em, $gn 3em, $gn 3.26em, $dgn 3.3em);
  background-repeat: no-repeat;
  border-radius: 50%;
  ...
}

还要注意开头处的额外 $dgn。为了让重复的渐变按预期开始、结束和循环,我们需要在 0 处指定起始颜色(或者没有 0)。

与之前的示例不同,我们没有使用 calc() 来居中线条,因为当我们稍后使用伪元素时,Internet Explorer 将以笨拙的方式渲染整个内容。为了绘制四条相交于中心的 0.4em 线条,请知道线条的一半应该是 div 一半,即 10em。因此,我们在每侧减去和加上 0.4 的一半(0.4 / 2 = 0.2)。换句话说,绿色的左侧应该是 9.8em,右侧应该是 10.2em。但是,对于 45deg 对角线,我们必须将 10em 乘以 2 的平方根才能得到它们的中心(10 × √2 ≈ 14.14)。这是 10em 直角三角形最长边的长度。因此,每条对角线的边将大约位于 13.94 和 14.34em 处。

.radar {
  background:
    /* lines */
    linear-gradient($gnT 9.8em, $gn 9.8em, $gn 10.2em, $gnT 10.2em),
    linear-gradient(45deg,$gnT 13.94em, $gn 13.98em, $gn 14.3em, $gnT 14.34em),
    linear-gradient(90deg,$gnT 9.8em, $gn 9.8em, $gn 10.2em, $gnT 10.2em),
    linear-gradient(-45deg,$gnT 13.94em, $gn 13.98em, $gn 14.3em, $gnT 14.34em),
  ...
}

为了防止对角线出现像素化,我们在绿色和透明绿色之间留下了 0.04em 的微小间隙。然后,为了创建一些光照效果,添加这个透明到黑色的径向渐变

.radar {
  background:
    /* lighting */
    radial-gradient(100% 100%, $bkT, $bk 9.9em,$bkT 10em),
  ...
}

这样就完成了雷达的静态部分。现在,我们在 :before 下的另一个背景堆栈中绘制灰色框架和指针,并添加动画。我们没有在此处包含框架是有原因的。由于指针容器应适合整个 div,因此我们不希望它与框架重叠。

此伪元素将填充空间,为了确保它保持在其中,让我们绝对定位它。我们将使用相同的边框半径,以便它在 Safari 中动画时保持圆形。

.radar {
  ...
  position: relative;
  &:before {
    background-repeat: no-repeat;
    border-radius: 50%;
    content: "";
    position: absolute;
    width: 100%;
    height: 100%;
  }
}

然后,为了绘制指针,我们将其大小设置为容器的一半,并将其保持在左上角。最后,在它的上面,我们绘制框架。

.radar {
  ...
  &:before {
    animation: scan 5s linear infinite;
    background:
      /* frame */
      radial-gradient($gyT 9.20em, $gy 9.25em, $gy 10em, $gyT 10.05em),
      /* hand */
      linear-gradient(45deg, $gnT 6em, $gn) 0 0 / 50% 50%;
    ...
  }
}

@keyframes scan {
  from {
    transform: rotate(0);
  }
  to {
    transform: rotate(1turn);
  }
}

现在我们的小工具就完成了!

查看 CodePen 上 Jon Kantner (@jkantner) 创建的 Pen 雷达

优点(以及一个缺点)

这种绘制 CSS 图像的方法有几个优点。首先,与栅格化图像文件相比,HTML 将非常轻量级。其次,对于那些没有使用实验性属性和 API(可能不支持)就无法很好绘制的图像,它非常有用。

这并不是说这种方法比使用嵌套有子元素的父元素来创建形状更好。它确实有一个缺点。您必须放弃使用浏览器开发者工具突出显示单个形状的能力。您需要注释和取消注释背景以识别它是哪个。只要您对每组背景进行分组和标记,就可以更快地找到特定的背景。

结论

简而言之,我们在本文中介绍的 CSS 图像绘制方法使我们能够

  1. 设置一个由颜色的变量组成的调色板。
  2. 禁用背景重复,使用 font-size 设置比例,并使用 em 单位为目标元素设置画布宽度和高度。
  3. 使用临时的 outline 来显示我们操作时的边缘。
  4. 从下到上绘制每个形状,因为背景是按此顺序渲染的。每个形状的 background 语法遵循 图像位置/大小(带或不带位置和大小)。

还有一些超乎寻常的思考,以及为了获得预期结果而进行的实验。我们创建的三个示例足以演示该概念。我们已经了解了如何对每个背景进行排序,绘制圆形的部分,绘制圆角矩形,以及稍微调整渐变停靠点以获得平滑的边缘。要了解更多信息,请随时分解和研究我在 这个 CodePen 收藏集 中制作的其他示例!