使用CSS渐变绘图

发布于 大漠

特别声明,本文根据@JON KANTNER的《Drawing Images with CSS Gradients》一文所整理。

这里所说的绘制是指CSS图像,即使用HTML元素和CSS属性绘制的图像。它们看起来像是Adobe Illustrator绘制的svg,但它们是在浏览器中渲染出来的。我所见过的一些技巧是使用borderbox-shadowclip-path来绘制图像。如果你在Codepen搜索“daily css images”,你会发现有很多优秀的案例。我自己也画了一些,也做过一些极限挑战,就是在一个元素上使用background和尽量使用其他属性来绘制图像。

让我们来睦看如何创建CSS图像。

方法

了解background语法和CSS渐变的工作原理是使用一个元素绘制任何东西所需要基础。先来看background语法:

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

除了background-positionbackground-size之间必须要有一个/来隔开之外,其他任何一个属性都可以以任何顺序出现。这里需要特别注意的是:background-positionbackground-size在简写的background属性中必须要有/,否则会得到意想不到的结果。另外不是所有属性都必须使用上的,比如,我们有可能不会使用background-colorbackground-repeatbackground-attachmentbackground-originbackground-clip。如此下来就剩下了background-imagebackground-sizebackground-position。由于background-repeat的默认值是repeat,所以必须将其设置为no-repeat。如果背景中有内容需要重复,我们可以使用repeating-linear-gradient()repeating-radial-gradient()两个渐变属性。在这种情况之下,我们的CSS可以这样写:

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

如果您从未接触过CSS渐变相关的知识,建议您花点时间先阅读下面几篇文章:

我们甚至可以使用多组背景参数(浏览器支持多背景的运用)!因此,我们只需要使用逗号将每组背景参数分隔开来:

.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;
}

简单的理解,background中有多组背景时,渲染出来的背景顺序和书写的顺序一致。

绘制

我们将使用Sass(SCSS)来绘制这些图像,这样做主要是可以将变量用于调色板。这将代码变得更短,更易于阅读和更易于修改颜色(将颜色变得更暗或更浅)。我们可以使用CSS自定义属性,而不用Sass,但由于IE还不支持CSS自定义属性,所以还是继续使用Sass。为了解释这是如何工作的,我们将使用线性和径向渐变来绘制CSS图像。

特别声明,文章主要出发点是向大家阐述如何使用CSS的渐变来绘制CSS图像,另外为了加强CSS自定义属性的理解和积累相关经验,接下来的示例,译者改变了原作者的初衷,将在示例中使用CSS自定义属性来替代原文中的Sass变量。

设置一个调色板

我们的调色板将用RGBHSL颜色组成。稍后我将解释为什么要用这两种格式来声明颜色。在本例中,我将使用RGB颜色格式。

有关于颜色更深入的介绍,可以阅读@Jamie Wong的《Color: From Hexcodes to Eyeballs》一文和《优化Web上的颜色》一文。

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

我个人喜欢使用简短的代码,使用至少一字母来表示每种颜色(例如,--r表示red颜色),这样易于阅读。如果使用较深或较浅的一种颜色,我将在字母前增加字母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等于元素的font-size,因此如果要改变图像大小,只需要相应调整font-size大小。我们将font-size设置为10pxwidthheight设置为24em。将font-size设置为10px是最简单的,因为基于它做数学计算是最简单的,因此24em你就知道它对应的是240px。然后画布的边框设置1px的灰色。

emCSS单位中的其中一个,也是易于造成计算混乱的单位之一,如果你感兴趣,可以点击这里进行了解。当然,你也可以使用CSS处理器中的函数功能来做相应的转换,比如px2em这样的。

--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;
}

另外前面提到,为了易于计算,将font-size的值设置为10px,但要注意的是,Chrome浏览器的最小的font-size的值为12px,所以在实际使用的时候,要注意这个细节。

此外,你还可以通过使用calc()viewport单位来启用响应性。也许我们可以使用像calc(10px + 2vmin)这样的东西,不过为了简单易于理解,这里还是使用10px吧。

calc()viewport结合在一起可以计算出混合字号,这样可以实现在一个范围内的视窗下有具体的像素值。其有一个典型的计算公式:

有关于这方面更详细的介绍,可以阅读下面的相关文章:

绘制图形

有趣的部分从这里开始。在正中间绘制一个8em x 8em在小的红色正方形。这个红色正方使用使用linear-gradient()来绘制,不过起始颜色和终止颜色相同。

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

    ...
}

如果想绘制一个梯形,只需要在渐变中设置一个60deg的角度值,用来指定渐变的方向。与此同时,在颜色面板上添加--T自定义属性的值设置为transparent。然后将--r--T的位置都设置为63%(右上角被截):

--T: transparent;

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

    ...
}

在两个值上设置相同的停止位置,斜切的一侧会有毛边。如果你仔细观察它,它看起来像下面这样:

为了让效果不会有毛边,将其中一个停止的值稍微调整一点(大约1%),这样就会使用毛边去除掉,边缘更平滑。在上面的示例中,将--r63%换成62%

这将是一个圆边的问题,同时在径向渐变中也存在,稍后会看到。如果用的不是Safari浏览器查看效果,那么即使切换到非透明颜色(比如orange),一切看起来都很不错。但在Safari中,你会注意到斜切的一边有一些黑虚边。

这是因为Safari中的transparent关键词始终是黑色透明的,因此我们会看到一些黑色的虚边。我真的希望苹果能解决这个问题,但他们永远不会。现在,让我们在--r添加一个新的属性--redT,并将其值设置为红色透明度(rgba(255, 0, 0, 0))。再把--T删除,因为将不会再用到这个自定义属性。

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

然后,我们使用--redT替换transparent。这就解决了Safari浏览器中虚边的问题。

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

你可能会感到好奇,为什么我们不使用十六进制颜色?那是因为IE和Edge不支持#rgba#rrggbbaa的语法(事实上,HEX 早在2016年末就有了alpha通道,你不知道吧)。我也希望它能尽可能地得到所有浏览器支持。我们还是将颜色格式保持一致吧。

现在把图形垂直移动到20%,并且在其下面绘制一个相同尺寸的orange圆。此外,为其透明版本添加另一个自定义属性--oT。同样的,为了边缘光滑,起始值和终址值之间也有一个1%的差值。

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

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

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

图形位置

渐变的位置取决于单位采用的是固定的还是百分比。假设我们把这两个渐度都变成正方形,并试着把它们横向放到div中。

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

红色方块完全移出画布,橙色方块的右边与画布右边相连接。使用固定单位就像在HTML5的canvas中放置绝对定位的元素或绘制图形。从这个意义上说,原点在左上方。当使用百分比设置背景大小时,div获得“假填充”是background-size的一半。同时,背景的原点是居中的(不要与background-origin混淆,背景原点是指盒模型的左上角)。

现在,我们把渐变换成径向渐变来绘制圆,x位置设置为24em100%,最后的效果是两个圆的另一半都被移到画布的外面(被切掉一半)。这是因为,我们这样写背景,原点总是在中间:

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

如果我们重写背景,让位置和大小都在渐变之后,并且使用100% 100% at center,这样一来,它们会被认为是线性渐变。红色的移到画布外面,橙色的在画布最右边。“假填充”再次出现在橙色部分。

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

没有一种正确的方法来定位形状,但是要将其定位成一个绝对的或固定的HTML元素,最好是使用固定的单位。如果需要一个快速的方法来放置一个形状(使用position/size)在死中心,50%是最好的选择,因为形状的原点将是它的中心。如果它应该触及可侧,那就用100%

图形尺寸

CSS背景下的大小调整和我们预期的一样,但是它们仍然受到固定或百分比的单位类型的影响。同样拿方块来举例,把它们的宽度改变10em,红色的方块向右展开,橙色的方块向两边展开。

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

如果在y位置使用em单位,形状会向上或向上收缩改变高度。如果我们用百分比单位,会在两个方向展开。

刚才,我们讨论了用径向渐变画圆的两种方法。第一种方法是在(at指定widthheight,然后在at后指定位置:

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

第二种方法是在(at之间使用100% 100%,然后给出位置和尺寸:

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

这些方法都可以使用径向渐变画圆,但会产生不同的输出,那是因为:

  • 第一种方法占用整个div,因为没有真正的background-positionbackground-size
  • 第二种方法设置了一个边框,有实际的位置和大小。因此,它就像一个线性渐变

假设我们用--o替换--rT。你会看到橙色覆盖白色。如果使用第二种方法,你将很容易地注意到橙色所显示的边框。

另外,使用circleellipse替换100% 100%的目的是让圆占据整个包围盒。它甚至能让我们完全控制它的尺寸。那样的话,如果你把50% 50%换成别的东西,它就会保持不变。如果使用这两个关键字中的一个,在居中时圆的边缘只有大约71%的距离,在调整位置时变得更加扭曲。例如,当我们将circleellipsex坐标改为0时,会发生以下情况:

从长远来看,你可以将语法重新想象为radial-gradient(width height at x y)radial-gradient(100% 100% at 限定框x位置 限定框y位置) x y / width height。如果你只是绘制一个圆或椭圆,你可以使用第一种简化的代码。如果画一个圆的一部分或者一个环的一部分,那么第二种简化的方法是很好的选择。在接下来的示例中,我们将会有很多这样的应用。

案例

准备好了吗?我们将一步一步地介绍三个例子。前两个是静态的,一个有很多半圆,另一个有圆形也有矩形。最后一个例子将更小,但重点是动画。

绘制雨伞

这个雨伞将是我们要绘制的第一个静态图像:

我们的调色板配置有red--r--rT),white--w--wT),orange--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);

绘图区域的大小为30em x 29em

.parasol {
    // 背景相关的样式放在这里

    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%, var(--do) 49%, var(--doT) 50%) 14em 0 / 1em 1em,
        radial-gradient(200% 200% at 0% 100%, var(--o) 49%, var(--oT) 50%) 15em 0 / 1em 1em,
        // 2
        linear-gradient(90deg, var(--do) 50%, var(--o) 50%) 14em 1em / 2em 25em,
        // 3
        radial-gradient(100% 200% at 50% 0, var(--oT) 0.95em, var(--o) 1em, var(--o) 1.95em, var(--do) 2em, var(--do) 2.95em, var(--doT) 3em) 14em 26em / 6em 3em,
        // 4
        radial-gradient(200% 200% at 100% 100%, var(--o) 49%, var(--oT) 50%) 18em 25em / 1em 1em,
        radial-gradient(200% 200% at 0% 100%, var(--do) 49%, var(--doT) 50%) 19em 25em / 1em 1em;
    ...
}

上面一坨代码,估计一下子无法理解(对于初学CSS的同学而言)。把上面的代码拆分一下:

// 1
radial-gradient(200% 200% at 100% 100%, var(--do) 49%, var(--doT) 50%) 14em 0 / 1em 1em,
radial-gradient(200% 200% at 0% 100%, var(--o) 49%, var(--oT) 50%) 15em 0 / 1em 1em,

绘制伞把顶部,即一个1em x 1em的半圆。为了让它们占据整个容器,把整个圆圈放大了两倍(设置 200% 200%),它们分别位于右下角和左下角。我们也可以使用关键字来设置圆的位置,比如bottom rightbottom left,但使用百分比,要简短些。注意,两个颜色停止之间有一个1%的位置差距,以确保绘制的图形效果平滑。

// 2
linear-gradient(90deg, var(--do) 50%, var(--o) 50%) 14em 1em / 2em 25em,

接着绘制最长的那部分,这部分是一个从深橙色到橙色的长矩形。这两个颜色的停止位置不需要有一个很小的差值,因为这部分没有曲线,也没有斜切角。

// 3
radial-gradient(100% 200% at 50% 0, var(--oT) 0.95em, var(--o) 1em, var(--o) 1.95em, var(--do) 2em, var(--do) 2.95em, var(--doT) 3em) 14em 26em / 6em 3em,

第三部分相对而言要复杂一些,因为我们要保持一个2em直径的弧形。画这个圆弧,将background-size设置为6em x 3em ,两者之间有一个2em的间距。然后在中心处使用径向渐变,每一个停止的位置都发生在1em处,为了让圆弧平滑,设置了一个差值为.05em

// 4
radial-gradient(200% 200% at 100% 100%, var(--o) 49%, var(--oT) 50%) 18em 25em / 1em 1em,
radial-gradient(200% 200% at 0% 100%, var(--do) 49%, var(--doT) 50%) 19em 25em / 1em 1em;

最后一部分和第一部分一样,只是它们的位置调整了。另外颜色也互换了一下。

把这四个部分结合起来,就绘制出雨伞的雨把。

雨伞的伞把绘制出来了,接下来把下面的代码添加到最顶部,绘制伞布:

// 雨伞伞布
radial-gradient(100% 200% at 50% 100%, var(--r) 50%, var(--rT) 50.25%) 50% 1.5em / 9em 12em,
radial-gradient(100% 200% at 50% 100%, var(--w) 50%, var(--wT) 50.25%) 50% 1.5em / 21em 12em,
radial-gradient(100% 200% at 50% 100%, var(--r) 50%, var(--rT) 50.25%) 50% 1.5em / 30em 12em,

为了画出这部分的半圆,我们使用了100% 200%的径向渐变,使每个直径与背景宽度相匹配,但高度是背景的两倍,并且在底部居中。通过从下到上的排序,使得最大的在下面,最小的在上面。这样就得到了我们想要的曲线。

随着渐变叠加越来越多,代码也就逐渐变得多起来,过一段时间就很难区分出哪个背景或一组背景对应图像的哪个部分。因此,为了更容易地确定它们,我们把它们分成几个小组,每个小组都添加上注释。这样易于后期能更读懂代码。

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

    
        // 雨伞伞把
        // 1
        radial-gradient(200% 200% at 100% 100%, var(--do) 49%, var(--doT) 50%) 14em 0 / 1em 1em,
        radial-gradient(200% 200% at 0% 100%, var(--o) 49%, var(--oT) 50%) 15em 0 / 1em 1em,
        
        // 2
        linear-gradient(90deg, var(--do) 50%, var(--o) 50%) 14em 1em / 2em 25em,
        
        // 3
        radial-gradient(100% 200% at 50% 0, var(--oT) 0.95em, var(--o) 1em, var(--o) 1.95em, var(--do) 2em, var(--do) 2.95em, var(--doT) 3em) 14em 26em / 6em 3em,
        
        // 4
        radial-gradient(200% 200% at 100% 100%, var(--o) 49%, var(--oT) 50%) 18em 25em / 1em 1em,
        radial-gradient(200% 200% at 0% 100%, var(--do) 49%, var(--doT) 50%) 19em 25em / 1em 1em;


    background-repeat: no-repeat;
    font-size: 10px;
    outline: 1px solid #aaa;
    width: 30em;
    height: 29em;
}

然后在雨伞的伞布和伞把之间添加另一部分背景,用来绘制伞布边缘的弧形效果。为了确定每个线段的宽度,我们必须得到红白相交点之间的距离。它们加起来必须是30em

从白色和最窄的红色半圆开始,从白色的宽度21em中减去红色的9em宽度,再除以2,得到两个白色部分的宽度(也就是上图中b的宽度),结果是6emb = (21 - 9) / 2 = 6em)。中间的红色线段是9em21 - (6 + 6) = 9em)。现在剩下的是最外面的红色线段(图中的a点),从最大的红色半圆的宽度30em减去现在得到的和(也就是中间白色的宽度21em),再除以2。这样a点的值为(30 - 21) / 2 = 4.5em

.parasol {
    background:
        ...

        // 雨伞伞布边缘的弧形
        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:
        // 雨伞伞布
        ...

        // 雨伞伞布边缘的弧形
        radial-gradient(105% 200% at 50% 100%, var(--rT) 49%, var(--r) 50%) 0 13.5em / 4.5em 3em,
        radial-gradient(105% 200% at 50% 100%, var(--wT) 49%, var(--w) 50%) 4.5em 13.5em / 6em 3em,
        radial-gradient(105% 200% at 50% 100%, var(--rT) 49%, var(--r) 50%) 50% 13.5em / 9em 3em,
        radial-gradient(105% 200% at 50% 100%, var(--wT) 49%, var(--w) 50%) 19.5em 13.5em / 6em 3em,
        radial-gradient(105% 200% at 50% 100%, var(--rT) 49%, var(--r) 50%) 25.5em 13.5em / 4.5em 3em,

        // 雨伞伞把
        ...
}

最后,将不再需要那个灰色的辅助边框线,这时可以把outline: 1px solid #aaa样式删除。最终的雨伞效果 就完成了。如下所示:

绘制带圆角的矩形

接下来这个示例是使用同样的手法来绘制一个旧的iPhone模型,在这个模型中有比新模型更多的细节。这个模型有两个特点:两个带圆形的矩形和Home键。

接下来的示例和Devices.css中绘制的iPhone模型有所不同,接下来的示例是使用CSS渐变在一个HTML元素上完成的。

和前面的示例一样,同样先创建一个调色板。这个调色板包括用于Home键按钮边缘的黑色(--bk--bkT),用于相机和话筒的灰色(--g--gT),模型外边框的浅灰色(--lg--lgT),镜头的蓝色(--blblT)和屏幕的深紫色(--p--pT)。

:root {
    --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);
}

设置一个20em x 40em画布和使用10pxfont-size

.iphone {
    // 背景样式放置在这

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

在开始绘制第一个圆角矩形之前,我们需要考虑圆角的半径,这里设置为2em。另外,我们还需要考虑给锁开关和音量按钮留一些空间,这里设置为0.25em。出于这个原因,矩形的大小将是19.75em x 40em。考虑到2em的圆角,我们需要两个线性渐变相交。那么矩形的宽度是15.75em19.75 - 2 x 2 = 15.75em)和高度是36em40 - 2 x 2 = 36em)。第一个位置是2.25em 0,第二个位置是0.25em 2em

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

    ...
}

模型浅灰色的边框厚度是.5em,第一个线性渐变从浅灰(--lg)到深灰(--dg),它们的结束位置都在0.5em位置,接着从深灰(--dg)到浅灰(--lg),而它们的结束位置都在39.5em40 - 0.5 = 39.5em)。第二个线性渐变设置了一个90deg的角度,让渐变是一个水平渐变,这个渐变同样的从浅灰(--lg)到深灰(--dg),两者结束位置是0.5em,接着从深灰(--dg)到浅灰(--lg),它们的结束位置是19.25em19.75 - 0.5 = 19.25em)。

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

分解一下,易于理解:

linear-gradient(var(--lg) 0.5em, var(--dg) 0.5em, var(--dg) 39.5em, var(--lg) 39.5em) 2.25em 0 / 15.75em 40em

linear-gradient(90deg, var(--lg) 0.5em, var(--dg) 0.5em, var(--dg) 19.25em, var(--lg) 19.25em) 0.25em 2em / 19.75em 36em

动态组合一下:

每个方形的角落将放置圆形的边缘。要创建这些形状,我们就需要使用径向渐变,它的大小是它们的边框的两倍,并且位于每个角落。把这四个圆形的代码放在模型主体之上:

.iphone {
    background:
        // 四个圆角
        radial-gradient(200% 200% at 100% 100%, var(--dg) 1.45em, var(--lg) 1.5em, var(--lg) 50%, var(--lgT) 51%) 0.25em 0 / 2em 2em,
        radial-gradient(200% 200% at 0% 100%, var(--dg) 1.45em, var(--lg) 1.5em, var(--lg) 50%, var(--lgT) 51%) 18em 0 / 2em 2em,
        radial-gradient(200% 200% at 100% 0%, var(--dg) 1.45em, var(--lg) 1.5em, var(--lg) 50%, var(--lgT) 51%) 0.25em 38em / 2em 2em,
        radial-gradient(200% 200% at 0% 0%, var(--dg) 1.45em, var(--lg) 1.5em, var(--lg) 50%, var(--lgT) 51%) 18em 38em / 2em 2em,

    ...
}

要得到0.5em厚的浅灰色末端,考虑一下渐变从哪里开始,然后算一下。因为浅灰色在最后,我们从2em中减去.5em。对于平滑度,从1.5em中去掉一点小距离0.05em,然后在后面的停止位置添加1%,变成51%

现在,如果我们将font-size修改为40px或更大来放大图像,我们会注意到圆角和平角之间的接缝(用橙色圈起来的位置):

因为它们看起来很小,我们可以很容易地把它们补上,只要把字体大小改回10px就可以了。

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

然后在一个线性渐变中,将添加锁开关和音量按钮来填充左边的0.25em空间。如果按钮和主体之间有一个1px的空间,我们可以在背景宽度(0.3em)上增加0.05em,这样它就不会突出到深灰色上。

.iphone {
    background:
        // 锁开关和音量按钮
        linear-gradient(var(--lgT) 5em, var(--lg) 5em, var(--lg) 7.5em, var(--lgT) 7.5em, var(--lgT) 9.5em, var(--lg) 9.5em, var(--lg) 11em, var(--lgT) 11em, var(--lgT) 13em, var(--lg) 13em, var(--lg) 14.5em, var(--lgT) 14.5em) 0 0 / 0.3em 100%,
        ...
}

看起来我们可以使用三个浅灰到浅灰的渐变。只是在透明和不透明之间发生了些变化。

接下来,添加Home键以及它里面正方形的平边。Home键大小是1.5em x 1.5em,其绘制和模型主体有相同的过程:两个相交线性渐变和圆角来组成。只不过要将它们放置水平方向正中间,这个时候calc()就非常有用了。50% + 0.125em为表达式,如果我们只将模型主体居中,那么每边将有0.125em空间。因此,我们向右移0.125em。相同的x定位将适用于两个背景上。

.iphone {
    background:
        // Home键
        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, var(--bkT) 1.25em, var(--bk) 1.3em, var(--bk) 49%, var(--bkT) 50%),

        ...
}

类似于我们模型主体的线性渐变,停止将以浅灰色开始和结束,但中间是透明的。注意,我们在每个灰度到透明的转换之间留下了0.05em的间距,就像模型主体的圆形一样,为了确保圆角和内部的平角能融合起来。

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

顺便说一下,和前面一样,我们可以通过将font-size增加到至少20px来看看我们在做什么。这有点像图像编辑软件中的缩放工具。

现在要把灰色方块的角精确到它们应该在的位置,首先要关注的是x的位置。同样从calc(50% + 0.125em)开始,然后加上或减去每一块的宽度,或者应该说是正方形的圆角半径。这些背景将超过最后三个。

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

然后制作屏幕,屏幕是一个17.25em x 30em的矩形。就像Home键的一部分一样,使用calc(50% + 0.125em)让矩形水平居中。而且这个矩形从顶部5em位置处开始。

.iphone {
    background:
        // 屏幕
        linear-gradient(var(--p), var(--p)) calc(50% + 0.125em) 5em / 17.25em 30em,
    
    ...
}

最后,将添加摄像头和扬声器。摄像头是一个简单的1em x 1em从蓝色到灰色的径向渐变。不过,纯灰色的扬声器会更复杂一些。这将是一个5em x 1em矩形和有带两个0.5em半径的圆弧。要画出来,先画一个矩形,宽度是4em,水平居中使用calc(50% + 0.125em)。然后使用0.5em x 1em的径向渐变画圆弧,这两个圆弧的位置分别是100% 50%50% 0%。把圆弧放置到矩形左右两边,其最佳方式还是要使用calc()表达式。左边的将从主体中心减去矩形一半宽度和半圆宽度(50% + 0.125em - 2em - 0.25em)。右边的遵循同样的模式,但不是做减法,是做加法(50% + 0.125em + 2em + 0.25em)。

.iphone {
    background:
        // 摄像头
        radial-gradient(1em 1em at 6.25em 2.5em, var(--bl) 0.2em, var(--g) 0.21em, var(--g) 49%, var(--gT) 50%),
    
        // 扬声器
        radial-gradient(200% 100% at 100% 50%, var(--g) 49%, var(--gT) 50%) calc(50% + 0.125em - 2em - 0.25em) 2em / 0.5em 1em,
        radial-gradient(200% 100% at 0% 50%, var(--g) 49%, var(--gT) 50%) calc(50% + 0.125em + 2em + 0.25em) 2em / 0.5em 1em,
        linear-gradient(var(--g), var(--g)) calc(50% + 0.125em) 2em / 4em 1em,
        
        ...
}

最终的效果如下:

雷达图

你可能认为可以使用background-position让这些背景图像动起来(有动画效果),但是你只能做这么多。例如,一个独立的背景是不可能产生动画效果的。事实上,background-position动画通常不如transform的动画效果好,所以我不推荐它。

如果想使图像的任何部分以我们希望的方式动画,我们可以借助伪元素:before:after来完成。如果我们需要更多的选择,那么我们可以使用多个div。接下来我们要做一个雷达扫描的动画效果,如下图所示:

我们先画静态部分,灰色的框架和转盘。同样的,先创建一个调色板和写一些基本代码:

:root {
    --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;
}

雷达图完全是个圆的,所以我们可以使用border-radius: 50%来搞定圆形。然后,可以使用一个repeating-radial-gradient来绘制这些圆环,它们之间的距离约为1/3

.radar {
    background:
        /* rings */
        repeating-radial-gradient(var(--dgn), var(--dgn) 2.96em, var(--gn) 3em, var(--gn) 3.26em, var(--dgn) 3.3em);

        background-repeat: no-repeat;
        border-radius: 50%;
        ...
}

与前面的示例不同,这个示例没有使用calc()来设置水平居中,因为稍后使用伪元素时,IE将会渲染的很慢。接下来要在中间画四条相交的0.4em的直线,要知道在10em时,这条线的一半应该是div的一半。然后,两边同时减去0.20.4 / 2 = 0.2em)。换句话说,绿色的左边应该是9.8em,右边应该是10.2em45度的对角线必须用10em乘以2的平方根,计算出它的中心(10 × √2 ≈ 14.14)。它是10em直角三角开最长边的长度。因此,每条对角线的边长大约为13.94em14.34em

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

为了防止对角线的像素化,我们在greentransparent之间留下0.04em的间隙。然后,为了模拟照明效果,添加一个透明到黑色的径向渐变。

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

这就完成了雷达的静态部分。现在我们把灰色的框架和手柄放在:before,主要是用来添加动画。有一个原因,在这里没有包含坐标系统。因为手感器应该适合整个div,我们不希望它与框架重叠。

这个伪元素要填满整个div的空间,这里使用绝对定位来做。

.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(var(--gyT) 9.20em, var(--gy) 9.25em, var(--gy) 10em, var(--gyT) 10.05em),
        
            // hand
            linear-gradient(45deg, var(--gnT) 6em, var(--gn)) 0 0 / 50% 50%;
        ...
    }
}

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

最终效果如下:

总结

简单地说,这篇文章介绍了CSS绘制图像的方法。这里主要用到了:

  • 使用CSS自定义属性为颜色设置调色板
  • 禁用background-repeatrepeat,并且使用em为单位,更好的基于font-size做缩放
  • 要想得到想要的结果,需要进地大量的思考和实验
  • 从下到上绘制每个图形,这主要是考虑背景是按这个顺序呈现的,每个形状的背景语法遵循background-positon / size(是否包含位置和大小)

从上面这几个示例中可以看出,基于一个HTML元素,使用CSS渐变可以绘制很多图像。不过要想得到想的结果,需要进行大量的思考和实验。从文章中我们学会了如何对生个背景进行排序,如何绘制圆的部分,圆角矩形,以及如何为圆弧和斜切角边缘平滑。要了解更多,请自由的剖析和研究我在Codepen上收集的其他例子

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.fedev.cn/css/drawing-images-with-css-gradients.htmlNeo Cloudfoam Pure