《CSS揭秘》精读笔记(二)形状

802 阅读15分钟

前言

本文为《CSS揭秘》的第二部分-形状的笔记,介绍了自适应椭圆、平行四边形、棱形图片、切角效果、梯形标签页、简单饼图的实现原理,并在每节中附上了个人的代码链接。文中对各类形状的实现给出了多种方案,不得不感慨新属性(如clip-path、conic-gradient)的出现给我们带来的便捷,其中conic-gradient在实现饼图上简直绝了,强烈推荐~

以下是一张总体概要的脑图,方便帮助大家进行知识梳理。其中标♥的部分为一些比较妙的方案,可以重点学习。

image.png

1.自适应椭圆

背景知识
border-radius 属性的基本用法

原理

border-radius的三个小知识:

  1. border-radius可以单独指定水平和垂直半径,只要用一个斜杠( / )分隔这两个值即可;
  2. border-radius不仅可以接受长度值,还可以接受百分比值;
  3. border-radius是这个简写属性,他可以写成以空格分开的四个值,这四个值分别从左上角开始以顺时针顺序应用到元素的各个拐角。
border-radius: 水平左上 水平右上 水平右下 水平左下 /
               垂直左上 垂直右上 垂直右下 垂直左下;

案例

有了上面的知识我们可以轻松搞定自适应的椭圆。以下代码详情点此处

1.自适应椭圆:

利用百分比值会基于元素的尺寸进行解析,从而达到自适应的目的。

.full {
    border-radius: 50%; 
}

image.png

2.x轴方向左半椭圆

这个形状,水平方向上,左边的两个圆角占据了整个元素的宽度,而且右边没有圆角,因此在水平方向border-radius的值为100% 0 0 100%;垂直方向上,左上角垂直半径和左下角的垂直半径相同,都为50%,同时因为右边两个圆角水平方向的border-radius为0,故右边两个圆角垂直方向的border-radius已经不重要了,可以为任意值。

.x-semi {
    border-radius: 100% 0 0 100% / 50%;  /* 相当于100% 0 0 100%/ 50% 50% 50% 50% */ ;
}

image.png

3.四分之一椭圆

.quarter {
    border-radius: 100% 0 0 0;  /* 相当于100% 0 0 0 / 100% 0 0 0; */
}   

image.png

2.平行四边形

背景知识
CSS 变形 skew

要生成平行四边形,我们可以很自然地想到利用transform的skew属性来对矩形进行斜向拉伸。

transform: skewX(-45deg);

image.png

但是这样做,导致平行四边形内的内容也发生了斜向变形。下面提供两种只让容器的形状倾斜,而保持其内容不变的方案。

嵌套元素方案

嵌套一层DOM元素,再对内容进行一次反向的 skew() 变形

核心代码如下:(代码链接

<div class="parallelogram-1">
    <div class="text">CLICK ME</div>
</div>
.parallelogram-1 {
     transform: skewX(-45deg);
}
.parallelogram-1>.text {
    transform: skewX(45deg);
}

嵌套元素方案

缺点: 需要用两层DOM标签

伪元素方案

把所有样式(背景、边框等)应用到伪元素上,然后再对伪元素进行变形,因为我们的内容并不是包含在伪元素里的,所以内容并不会受到变形的影响。此时,用伪元素生成的方块是重叠在内容之上的,一旦给它设置背景,就会遮住内容,我们可以给伪元素设置z-index: -1 样式,这样它的堆叠层次就会被推到宿主元素之后。

最终代码如下:(代码链接

.parallelogram-2 {
    position: relative;
}
.parallelogram-2::before {
    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    /* 以下为重要代码 */
    transform: skewX(-45deg);
    z-index:-1;
}

伪元素方案

缺点: 变形后导致事件的点击区域与看见的实际元素区域不一致

3.棱形图片

背景知识
CSS 变形,“平行四边形”

基于变形的方案

基于“平行四边形”中讨论的第一个解决方案,需要把图片用一个div标签包裹起来,然后对其应用相反的 rotate()变形样式,代码如下:

<div class="diamond-1">
    <img src="nana.jpg" alt="">
</div>
.diamond-1 {
    width: 200px;
    height: 200px;
    background-color: yellow;
    overflow: hidden;
    transform: rotate(45deg);
}
.diamond-1>img {
    max-width: 100%;
    transform: rotate(-45deg);  
}

image.png

我们想要得到上面图片右边的效果,但实际却得到了左边的效果。主要问题在于 max-width: 100% 这条声明。 100% 会被解析为容器(.diamond-1)的边长,但是我们想让图片的宽度与容器的对角线相等,对角线的长度为边长的21.42√2≈ 1.42,因此我们可以设置max-width为142%,但此时效果如下图所示,因为通过 width 属性来放大图片时,只会以它的左上角为原点进行缩放。

image.png

最终我们可以利用 scale() 变形样式来把这个图片放大1.42倍来达到效果,最终代码如下:(代码链接)

.diamond-1 {
    width: 200px;
    height: 200px;
    background-color: yellow;
    overflow: hidden;
    /* 以下为重要代码 */
    transform: rotate(45deg);
}
.diamond-1>img {
     /* 以下为重要代码 */
    max-width: 100%;
    transform: rotate(-45deg) scale(1.42); 
}

image.png

缺点: 需要用两层DOM标签

裁切路径方案

使用新属性clip-path来对图片进行裁剪,但目前这个属性浏览器的支持程度还有限

.diamond-2 {
    width: 200px;
    height: 200px;
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
}

image.png

除了浏览器支持有限外,这个属性能创造的奇迹还不止于此,它还能通过polygon()函数裁剪出各种多边形,通过circle()函数裁剪出圆,通过ellipse()函数裁剪出椭圆。

4.切角效果

背景知识
CSS 渐变, background-size ,“条纹背景”,border-image, clip-path

CSS渐变方案

切一个角效果: 以右下角为例。我们可以只需要一个线性渐变就可以达到目标。这个渐变需要把一个透明色标放在切角处,然后在相同位置设置另一个色标,并且把它的颜色设置为我们想要的背景色。(代码链接

    background: #58a;
    background: linear-gradient(-45deg, transparent 15px, #58a 0);

image.png

注意:事实上,第一行声明并不是必需的,加上它是将其作为回退机制:如果某些浏览器不支持 CSS渐变,那第二行声明会被丢弃,而此时我们至少还能得到一个简单的实色背景。

切两个角效果: 以左下角和右下角切掉为例。我们需要把这个图形看成两部分,从中间分成两半,右边使用一个右下切角的线性渐变,左边使用一个左下切角的线性渐变,同时需要把background-repeat关掉,代码如下:

  background: #58a;
  background:linear-gradient(-45deg, transparent 15px, #58a 0) right,/*right指的background-position属性,表明从右边开始计算background-size*/
             linear-gradient(45deg, transparent 15px, #655 0) left;/*left指的background-position属性,表明从左边开始计算background-size*/
  background-size: 50% 100%;
  background-repeat: no-repeat;

image.png

切四个角效果: 明白了切两个角的原理,切四个角怎么难得到机智的我们。代码如下:

    background: #58a;
    background: linear-gradient(135deg, transparent 15px, #58a 0) top left,
                linear-gradient(-135deg, transparent 15px, #58a 0) top right,
                linear-gradient(-45deg, transparent 15px, #58a 0) bottom right,
                linear-gradient(45deg, transparent 15px, #58a 0) bottom left;
    background-size: 50% 50%;
    background-repeat: no-repeat;

image.png

弧形切角: 弧形切角原理通切四个角的效果,只不过把线性渐变变为径向渐变。代码如下:

    background: #58a;
    background: radial-gradient(circle at top left, transparent 15px, #58a 0) top left, 
                radial-gradient(circle at top right, transparent 15px, #58a 0) top right,
                radial-gradient(circle at bottom right, transparent 15px, #58a 0) bottom right,
                radial-gradient(circle at bottom left, transparent 15px, #58a 0) bottom left;
    background-repeat: no-repeat;
    background-size: 50% 50%;

image.png

内联 SVG 与 border-image 方案

在常规设计中,四个角的切角尺寸往往是一致的。由于border-image会解决缩放问题,而 SVG 可以实现与尺寸完全无关的完美缩放,将以下的svg应用于border-image,并设置border-image-slice为1,如虚线标注的切片方式,便可以得到一个切角效果。

<svg xmlns="http://www.w3.org/2000/svg" width="3" height="3" fill="#58a">
    <polygon points="0,1 1,0 2,0 3,1 3,2 2,3 1,3 0,2"/>
</svg>

image.png

切角代码如下:

border: 15px solid transparent;
border-image: 1 url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="3" height="3" fill="%2358a"><polygon points="0,1 1,0 2,0 3,1 3,2 2,3 1,3 0,2"/></svg>');

image.png

通过以上代码,我们发现两个问题,一个是背景图片的问题,一个是切角尺寸比原来小的问题。

我们先来解决背景图片的问题。要么给 border-image 属性值在border-image-slice后加上 fill 关键字——这样它就不会丢掉SVG中央的那个切片了;或者给他指定一个背景色,并且设置background-clip:padding-content,这样还能发挥一个回退的作用。

切角尺寸比原来小的原因,请看下图。原来通过渐变生成的切角,15px是沿着渐变轴来度量的,也就是下图中的斜向距离,而通过此方法用到的15px是边框的宽度,也就是下图的水平或者垂直距离,故要使斜向距离达到15px,border的宽度应为15×221.21315×√2≈ 21.213,近似取20。

image.png

另外,我们给边框加一个背景颜色,当border-image属性不被浏览器支持时提供一个回退方案。所以最终代码如下(代码链接):

    border: 20px solid #58a; /* 用背景色而不是transparent是为了在浏览器不支持border-image时,提供回退方案 */
    border-image: 1 url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="3" height="3" fill="%2358a"><polygon points="0,1 1,0 2,0 3,1 3,2 2,3 1,3 0,2"/></svg>');
    background: #58a;
    background-clip: padding-box;

image.png

裁切路径方案

background: #58a;
clip-path: polygon(20px 0, calc(100% - 20px) 0, 100% 20px, 100% calc(100% - 20px),
            calc(100% - 20px) 100%, 20px 100%, 0 calc(100% - 20px), 0 20px);

三种方案的对比

image.png

5.梯形标签页

背景知识
基本的 3D 变形,“平行四边形”

类似于平行四边形那一节学到的方法,我们给伪元素做3D变形,代码如下:

.trapezoid {
    position: relative;
    display: inline-block;
    padding: .5em 1em .35em;
    color: white;
}
.trapezoid::before {
    content: ''; /* 用伪元素来生成一个矩形 */
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: -1;
    background: #58a;
    transform: perspective(.5em) rotateX(5deg);
}

image.png

只给元素做3D变得到的结果为上图1所示,图2为图1变形前后的对照图,我们可以看出默认变形中心transform-orign在元素自身的中心线上,经过旋转,元素的宽度增加,梯形占据的位置会稍微下移,高度也会有少许缩减。而文章中提到一种方法如图3所示,把变形中心transform-orign设为bottom,这时我们只要补偿高度,而这个垂直方向上的缩放程度大概为130%左右,于是得到最终代码,如下:

.trapezoid {
    position: relative;
    display: inline-block;
    padding: .5em 1em .35em;
    color: white;
}
.trapezoid::before {
    content: ''; /* 用伪元素来生成一个矩形 */
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: -1;
    background: #58a;
    transform: scaleY(1.3) perspective(.5em) rotateX(5deg);
    transform-origin: bottom;
}

于是得到一个漂亮的梯形。

image.png

既然我们是要生成梯形标签,我们根据上述方案,就能轻而易举地得到如下梯形标签tab样式,由于篇幅原因,代码请点此处

image.png

6.简单的饼图

背景知识
CSS 渐变,基本的 SVG,CSS 动画,“条纹背景”,“自适应的椭圆”,conic-gradient

我们案例用黄绿色(yellowgreen)表示底色,并采用棕色(#655)来显示比率。

基于transform的方案

原理是这样的(篇幅比较长),我们先利用linear-gradient把圆分为两部分,然后再用伪元素构造成一个半圆盖上去,通过旋转伪元素来决定露出多大的扇区

先构造一个两种颜色的圆,代码如下:

.pie {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: yellowgreen;
    background-image:linear-gradient(to right, transparent 50%, #655 0);
}

image.png

然后再利用伪元素构造一个半圆盖在此元素上,并把旋转中心设置在圆心。

pie::before {
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    transform-origin: left;
}

image.png

若要得到一个显示率为0-50%的饼图,只需把伪元素背景设为黄绿色,然后旋转相应的度数,我们以20%为例,旋转 360deg×0.2=72deg=0.2turn360deg×0.2 = 72deg = 0.2turn (注:turn为CSS3角度单位,表示圈,1圈为360deg )。

pie::before {
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    transform-origin: left;
    background-color: inherit; /* 继承父元素的黄绿色 */
    transform: rotate(.2turn); /* 旋转0.2圈 */
}

image.png

若要得到一个显示率大于50%的饼图,只需把伪元素背景设为棕色,然后旋转相应的度数,我们以60%为例,旋转 360deg×(0.60.5)=36deg=0.1turn360deg×(0.6-0.5) = 36deg = 0.1turn

pie::before {
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    transform-origin: left;
    background-color: #655; /* 棕色 */
    transform: rotate(.1turn); /* 旋转0.1圈 */
}

image.png

知道了原理,我们接下来需要把这两种情况封装一下。我们先来看一个动画,以下代码实现了一个饼图从 0 变化到 100% 的动画。

@keyframes spin {
    to { transform: rotate(.5turn); }
}
@keyframes bg {
    50% { background: #655; }
}
.pie::before {
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    background-color: inherit;
    transform-origin: left;
    animation: spin 3s linear infinite,
    bg 6s step-end infinite;
}

再来看一个关于animation-delay的规范。

“一个负的延时值是合法的。与 0s 的延时类似,它意味着动画会立即开始播放,但会自动前进到延时值的绝对值处,就好像动画在过去已经播放了指定的时间一样。因此实际效果就是动画跳过指定时间而从中间开始播放了。”
——CSS 动画(第一版)(w3.org/TR/css-anim…

在了解了以上动画和animation-delay的规范后,我们将使用上面的动画来解决这个封装,但动画必须处于暂停状态,且必须暂停在我们想要的位置。我们要用负的动画延时来直接跳至动画中的任意时间点,并且定格在那里

我们设置动画的总时长为100s,我们可以用内联样式的方式为其设置 animation-delay 属性,然后再在伪元素上应用 animation-delay: inherit属性。综合以上要素,如果要让饼图显示为 20% 和 60%,则html结构代码为:

<div class="pie" style="animation-delay: -20s"></div>
<div class="pie" style="animation-delay: -60s"></div>

进一步优化html代码结构,优化后代码如下:

<div class="pie">20%</div>
<div class="pie">60%</div>

这时,我们采用一段js脚本来把animation-delay 写到内联样式中,同时用color: transparent 来把文字隐藏起来。

document.querySelectorAll('.pie').forEach(function (pie) {
    var p = parseFloat(pie.textContent);
    pie.style.animationDelay = '-' + p + 's';
});

最终改良版的css代码如下(详情点击此处):

.pie {
    position: relative;
    width: 100px;
    line-height: 100px;
    border-radius: 50%;
    background: yellowgreen;
    background-image: linear-gradient(to right, transparent 50%, #655 0);
    color: transparent;
    text-align: center;
}

@keyframes spin {
    to {
        transform: rotate(.5turn);
    }
}

@keyframes bg {
    50% {
        background: #655;
    }
}

.pie::before {
    content: '';
    position: absolute;
    top: 0;
    left: 50%;
    width: 50%;
    height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    background-color: inherit;
    transform-origin: left;
    animation: spin 50s linear infinite, bg 100s step-end infinite;
    animation-play-state: paused;
    animation-delay: inherit;
}

image.png

SVG方案

原理:我们先从一个svg开始,画一个半径为50的圆。

<svg width="100" height="100">
    <circle r="30" cx="50" cy="50" />
</svg>

给圆加点基础样式,描边,如下图1所示。

circle {
    fill: yellowgreen;  /* 填充颜色 */
    stroke: #655;  /* 描边颜色 */
    stroke-width: 30; /* 描边宽度 */
}

在上面的样式中追加下面一行代码,使描边变成虚线,如下图2所示。

.stroke-dasharray: 20 10; /* 虚线的线段长度为20 且间隙长度为10 */

image.png

当我们把这个虚线描边的线段长度指定为0 ,并且把虚线间隙的长度设置为等于或大于整个圆周的长度,这里是2π×301892π×30≈ 189时。我们可以看到,它完全去除了描边效果,只剩下绿色的圆形。当我们开始增加第一个值时,整个圆周上覆盖的长度正是我们给它指定的长度值。

QQ图片20210622222600.png

根据这个原理,我们优化一下代码,首先我们为了便于计算,我们把圆的周长设为100,那么半径就是100/2π16100/2π≈ 16,然后我们还需要把svg图形以逆时针方向旋转 90°,使描边起始点位置来到0点钟方向,这样我们就得到了一个60%比率的饼图,代码如下(详情点此处):

<svg viewBox="0 0 32 32">
    <circle class="circle" r="16" cx="16" cy="16" />
</svg>
svg {
    width: 100px; height: 100px;
    transform: rotate(-90deg);
    background: yellowgreen;
    border-radius: 50%;
}
.circle {
    fill: yellowgreen;  /* 填充颜色 */
    stroke: #655;  /* 描边颜色 */
    stroke-width: 32; /* 描边宽度 */
    stroke-dasharray: 60 100; /* 关键代码 虚线的线段长度为60 且间隙长度为100*/ 
}

image.png

利用这个原理,我们也可以实现多色饼图,我们每多一种颜色多添加一个circle层,同时越靠近起点的circle层在图层的越上层。代码如下(详情点此处):

<svg viewBox="0 0 32 32">
    <circle class="circle1" r="16" cx="16" cy="16" />
    <circle class="circle2" r="16" cx="16" cy="16" />
</svg>
svg {
    width: 100px; height: 100px;
    transform: rotate(-90deg);
    background: yellowgreen;
    border-radius: 50%;
}
.circle1 {
   fill: yellowgreen;
    stroke: #655; 
    stroke-width: 32;
    stroke-dasharray: 60 100; 
}
.circle2 {
    fill: transparent;
    stroke: grey;
    stroke-width: 32;
    stroke-dasharray: 20 100; 
}

image.png

conic-gradient方案

结合以前学的linear-gradient的知识,利用圆锥渐变conic-gradient实现多种颜色的饼图真是太easy了~墙裂推荐!代码如下(详情点此处):

.conic {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: conic-gradient(grey 0 20%, #655 0 60%, yellowgreen 0 100%);
}

image.png

通过三种生成饼图的方案,可以看出:svg方案和conic-gradient方案代码简洁,且能轻松实现多种颜色的饼图,非常值得推荐~