首页 《CSS揭秘》笔记2 — 形状
文章
取消

《CSS揭秘》笔记2 — 形状

此篇博客是学习《CSS 揭秘》一书的学习笔记。

1. 自适应椭圆

完整椭圆

我们知道,给一个正方形盒子设置 border-radius: 50%; 时,我们会得到一个圆形:

HTML:

1
<div class="box adaptive-ellipse"></div>

CSS:

1
2
3
4
5
6
7
8
.box {
  width: 100px;
  height: 100px;
  background-color: #ff7875;
}
.adaptive-ellipse {
  border-radius: 50%;
}

当我们改变盒子的宽高,使其变为矩形时,我们会得到一个椭圆:

CSS:

1
2
3
4
5
.box {
  width: 150px;
  height: 100px;
  background-color: #ff7875;
}

实际上,border-radius: 50%;border-radius: 50% / 50%; 的简写,前后两个值分别指定了水平和垂直半径。

半椭圆

border-radius 是下面四个属性的简写:

  • border-top-left-radius: 左上角半径
  • border-top-right-radius: 右上角半径
  • border-bottom-right-radius: 右下角半径
  • border-bottom-left-radius: 左下角半径

当我们想得到一个垂直的半椭圆时,我们可以设置:

  • 左上角/右上角半径为 50% 100%
  • 左下角/右下角半径为 0 0

CSS 如下:

1
2
3
4
5
6
.adaptive-half-ellipse-ver {
  border-top-left-radius: 50% 100%;
  border-top-right-radius: 50% 100%;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}

效果如下:

真正简洁的方法还是使用 border-radius 简写属性,然后使用 / 分隔水平和垂直半径:

1
2
3
.adaptive-half-ellipse-ver {
  border-radius: 50% 50% 0 0 / 100% 100% 0 0;
}

当我们给左下角/右下角的垂直半径设置为 0 时,其水平半径由 0 改为为 50% 时,并不会影响左下角/右下角的直角,因此我们可以进一步简化属性:

1
2
3
.adaptive-half-ellipse-ver {
  border-radius: 50% / 100% 100% 0 0;
}

同理,如下代码会得到水平的半椭圆:
CSS:

1
2
3
.adaptive-half-ellipse-hor {
  border-radius: 0 100% 100% 0 / 50%;
}

效果如下:

四分之一椭圆

根据前面的思路,当把左上角的半径设 100% ,其余三个角的半径设为 0 时,我们就会得到四分之一椭圆:

CSS:

1
2
3
.adaptive-quarter-ellipse {
  border-radius: 100% 0 0 0;
}

2. 平行四边形

我们可以通过 skew() 变形属性来生成一个平行四边形:

parallelogram

CSS:

1
2
3
.parallelogram {
  transform: skewX(-30deg);
}

我们生成了一个平行四边形,但是内容也跟着一起变形了。怎样让内容保持不变呢?

两个元素方案

parallelogram

内层的内容元素使用一次反向的 skew() ,从而抵消外层容器的变形效果。

HTML:

1
2
3
<div class="parallelogram-1">
  <div class="parallelogram-2">parallelogram</div>
</div>

CSS:

1
2
3
4
5
6
.parallelogram-1 {
  transform: skewX(-30deg);
}
.parallelogram-2 {
  transform: skewX(30deg);
}

伪元素方案

此方案的重点是把所有的样式应用到为元素上,然后在对为元素进行变形

parallelogram

HTML:

1
<div class="parallelogram">parallelogram</div>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
.parallelogram {
  position: relative;
  z-index: 0; /* 修复和页面样式冲突的bug */
}
.parallelogram::after {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background: #ff7875;
  transform: skew(-30deg);
  z-index: -1;
}

注:

  • 为了使伪元素自动继承宿主元素的尺寸,
    • 我们设置了宿主元素的 position: relative;
    • 然后给伪元素设置 position: absolute; ,并设置 top/right/bottom/left0;
  • 由于伪元素会覆盖在宿主元素之上,所以我们设置伪元素 z-index: -1;

3. 菱形

在视觉设计中,把图片裁切为菱形是一种常见的设计手法。

两个元素方案

HTML:

1
2
3
<div class="rhombus">
  <img src="/images/2019-06-01-css-secrets/cat.jpg" />
</div>

CSS:

1
2
3
4
5
6
7
8
9
.rhombus {
  transform: rotate(45deg);
  overflow: hidden;
}

.rhombus img {
  width: 100%;
  transform: rotate(-45deg);
}

可以看到,图片被裁剪成了八角形。因此我们应该放大图片,使其充满其父元素。

CSS:

1
2
3
4
.rhombus img {
  width: 100%;
  transform: rotate(-45deg) scale(1.42);
}

注:

  1. 我们保留了图片宽度为 100% 这个值,当浏览器不支持 transform 时,仍然可以得到一个合理的布局,因此没有使用 width: 142%;
  2. scale() 缩放的参考点默认为图片中心点,除非我们人为指定 transform-origin 的值;通过 width 属性放大图片时,参考点为左上角。

裁剪路径方案(不完全支持)

clip-path: 可以创建一个只有元素的部分区域可以显示的剪切区域。区域内的部分显示,区域外的隐藏。

剪切区域是被引用内嵌的 URL 定义的路径或者外部 svg 的路径,或者是一个形状例如 circle()

clip-path 属性代替了现在已经弃用的 clip 属性。

HTML

1
<img class="box-3-3-2" src="/images/2019-06-01-css-secrets/cat-1.jpg" />

CSS:

1
2
3
4
5
6
7
8
9
.box-3-3-2 {
  width: 300px;
  height: 200px;
  clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  transition: clip-path 1s;
}
.box-3-3-2:hover {
  clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}

注:

  1. polygon() 用于生成一个多边形,其接受至少三对坐标值,使用逗号分隔。
  2. clip-path 可以参与动画,上述代码实现了鼠标悬停时图片平滑地显示其完整形状。
  3. safari 浏览器不支持该属性
  4. clip-path 可以适应非正方形的图片。

4. 切角效果

切角效果在网页设计中非常流行。

一个切角

我们首先用无所不能的 CSS 渐变实现一个切角。

HTML:

1
<div class="single-clip-corner"></div>

CSS:

1
2
3
4
.single-clip-corner {
  background: #ff7875;
  background: linear-gradient(-45deg, transparent 15px, #ff7875 0);
}

注:background: #ff7875; 作为回退属性,在不支持 CSS 渐变的浏览器中,我们仍能看到简单的实色背景。

两个切角

根据前面的思路,当我们想用两层渐变去实现底部的两个切角时,CSS 如下:

1
2
3
4
5
.btm-clip-corner {
  background: #ff7875;
  background: linear-gradient(-45deg, transparent 15px, #ff7875 0),
              linear-gradient(45deg, transparent 15px, #69c0ff 0);
}

实际效果如下:

这样是行不通的,两层渐变背景默认会填满整个元素,且会相互覆盖。

我们可以通过如下几步,把两层背景分别放在左侧和右侧:

  1. background-repeat 设为 no-repeat
  2. background-size 设为 50% 100%
  3. backgound-position 分别设为 rightleft

CSS:

1
2
3
4
5
6
7
.btm-clip-corner {
  background: #ff7875;
  background: linear-gradient(-45deg, transparent 15px, #ff7875 0) right,
              linear-gradient(45deg, transparent 15px, #69c0ff 0) left;
  background-size: 50% 100%;
  background-repeat: no-repeat;
}

效果如下:

四个切角

同理,实现四个切角的 CSS 如下:

1
2
3
4
5
6
7
8
9
.four-clip-corner {
  background: #ff7875;
  background: linear-gradient(135deg, transparent 15px, #ff7875 0) left top,
              linear-gradient(-135deg, transparent 15px, #69c0ff 0) right top,
              linear-gradient(-45deg, transparent 15px, #ff7875 0) right bottom,
              linear-gradient(45deg, transparent 15px, #69c0ff 0) left bottom;
  background-size: 50% 50%;
  background-repeat: no-repeat;
}

效果如下:

此效果也可以使用 clip-path 属性实现。

弧形切角

CSS:

1
2
3
4
5
6
7
8
9
.scoop-corners {
  background: #ff7875;
  background: radial-gradient(circle at top left, transparent 15px, #ff7875 0) left top,
              radial-gradient(circle at top right, transparent 15px, #69c0ff 0) right top,
              radial-gradient(circle at bottom right, transparent 15px, #ff7875 0) right bottom,
              radial-gradient(circle at bottom left, transparent 15px, #69c0ff 0) left bottom;
  background-size: 50% 50%;
  background-repeat: no-repeat;
}

5. 梯形标签页

我们可以 3D 旋转伪元素生成梯形:

trapezoid

HTML:

1
<div class="trapezoid">trapezoid</div>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.trapezoid {
  position: relative;
  width: fit-content;
  padding: 0 .8em;
  z-index: 0;
}
.trapezoid::after {
  position: absolute;
  content: '';
  top: 0; right: 0; bottom: 0; left: 0;
  transform: scaleY(1.3) perspective(.5em) rotateX(5deg);
  transform-origin: bottom;
  background: #ff7875;
  z-index: -1;
}

要点:

  1. 元素本身相对定位,其伪元素绝对定位;
  2. 伪元素通过 transform 属性缩放和旋转:
    • scaleY(1.3) Y 方向拉伸 1.3 倍,抵消 X 方向旋转 5 度后视觉上高度降低的效果;
    • rotateX(5deg) X 方向旋转 5 度,用于生成视觉上的梯形效果;
    • perspective(.5em) 指定了观察者与 z=0 平面的距离;
  3. transform-origin: bottom; 指定元素变形的原点。

根据上述特点,我们可以设计出复杂的梯形标签页:

Projects

HTML:

1
2
3
4
5
6
<ul class="nav-3-5">
  <li>Home</li>
  <li class="selected">Projects</li>
  <li>About</li>
</ul>
<div class="content-3-5">Projects</div>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ul.nav-3-5 {
  padding-left: .8em;
  margin-bottom: 0;
}
ul.nav-3-5 > li {
  position: relative;
  display: inline-block;
  margin: 0 -.4em !important;
  padding: .3em 1em 0;
  cursor: pointer;
  z-index: 0;
}
ul.nav-3-5 > li::before,
.content-3-5 {
  border: 1px solid rgba(0, 0, 0, .4);
}
ul.nav-3-5 > li::before {
  content: '';
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  background: #ccc;
  background-image: linear-gradient(
    hsla(0, 0%, 100%, .6), 
    hsla(0, 0%, 100%, 0));
  border: 1px solid rgba(0, 0, 0, .4);
  border-bottom: none;
  border-radius: .5em .5em 0 0;
  box-shadow: 0 .15em white inset;
  transform: scale(1, 1.3) perspective(.5em) rotateX(5deg);
  transform-origin: bottom;
  z-index: -1;
}
.content-3-5 {
  margin-bottom: 1em;
  background: #eee;
  padding: 1em;
  border-radius: .15em;
}
ul.nav-3-5 li.selected {
  z-index: 2;
}
ul.nav-3-5 li.selected::before {
  background-color: #eee;
  margin-bottom: -1px;
}

JS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function() {
  var navDom = document.querySelector('.nav-3-5');
  var liDomArr = navDom.querySelectorAll('li');
  var contentDom = document.querySelector('.content-3-5');
  navDom.addEventListener('click', clickTab);
  // 点击 tab 页的回调函数
  function clickTab(event) {
    removeClass();
    event.target.classList.add('selected');
    contentDom.innerHTML = event.target.innerHTML;
  }
  // 移除所有 li 元素的 'selected' 类
  function removeClass() {
    liDomArr.forEach(function(dom) {
      dom.classList.remove('selected');
    });
  }
})();

6. 简单的饼图

6.1 固定比例的饼图

我们来一步一步完成一个 20% + 80% 的饼图。

首先完成一个左右颜色不一样的圆形:

HTML

1
<div class="circle-3-6"></div>

CSS:

1
2
3
4
5
6
7
.circle-3-6 {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  background-color: #ff7875;
  background-image: linear-gradient(to right, transparent 50%, #69c0ff 0);
}

第二步,设置伪元素的样式,让它起到遮罩层的作用(见虚线方框)。

1
2
3
4
5
6
7
.circle-3-6.stage-2::after {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border: 1px dashed #ccc;
}

第三步,给伪元素设置背景色,并以左中为中心旋转 (20% * 360) 度,即 0.2turn

HTML

1
<div class="circle-3-6 stage-2 stage-3 margin-btm-14"></div>

CSS

1
2
3
4
5
.circle-3-6.stage-3::after {
  background-color: inherit;
  transform: rotate(0.2turn);
  transform-origin: left;
}

第四步,把伪元素变为半圆形,并去掉虚线方框。

HTML

1
<div class="circle-3-6 stage-2 stage-3 stage-4"></div>

CSS

1
2
3
4
.circle-3-6.stage-4::after {
  border-radius: 0 100% 100% 0 / 50%;
  border: none;
}

注:除了把伪元素变为半圆形外,给.circle-3-6 设置 overflow: hidden; 也可以达到同样的效果。

最终的 CSS 如下:

1
2
3
4
5
6
7
8
9
10
.circle-3-6::after {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform: rotate(0.2turn);
  transform-origin: left;
}

现在我们可以调整 transform: rotate(0.2turn); 中的角度来调整蓝色部分的比例。

但是,当角度调整至 0.5turn 以上时,如 0.7turn 会得到不符合我们预期的情况:

CSS

1
2
3
.circle-3-6.turn-70::after {
  transform: rotate(0.7turn);
}

我们预期的是,蓝色占 70% ,而当前蓝色占 30%

从另一个角度考虑,我们把伪元素的颜色改为蓝色,然后把旋转角度减去 0.5turn ,就会得到预期的情况:

CSS

1
2
3
4
.circle-3-6.turn-70-right::after {
  background-color: #69c0ff;
  transform: rotate(0.2turn);
}

6.2 饼图进度指示器

我们还可以结合动画,生成一个从 0% 变为 100% 的饼图。

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@keyframes bg-color {
  50% {
    background-color: #69c0ff;
  }
}
@keyframes spin {
  to {
    transform: rotate(0.5turn);
  }
}
.pointer::after {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
}
.circle-3-6.auto-run::after {
  animation: bg-color 6s step-end infinite,
             spin 3s linear infinite;
}

6.3 静态饼图封装

用同一套代码实现不同比例的饼图这个需求更加常见,比如我们希望通过如下方式来生成两个不同比例的饼图:

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

由于无法给伪元素设置内联样式,因此我们通过让上一节中的动画暂定的方式来封装代码。

20%
60%

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.pie {
  position: relative;
  display: inline-block;
  width: 100px;
  height: 100px;
  line-height: 100px; /* 文字垂直居中 */
  border-radius: 50%;
  background-color: #ff7875;
  background-image: linear-gradient(to right, transparent 50%, #69c0ff 0);
  text-align: center; /* 文字水平居中 */
  color: transparent; /* 隐藏文字 */
}
.pie::after {
  content: '';
  position: absolute; /* 绝对定位 */
  display: block;
  top: 0; right: 0;
  width: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: bg-color 100s step-end infinite,
             spin 50s linear infinite;
  animation-delay: inherit; /* 继承自 .pie 元素 */
  animation-play-state: paused; /* 动画暂定 */
}

JS

1
2
3
4
5
6
7
(function() {
  var pieDomArr = document.querySelectorAll(".pie");
  pieDomArr.forEach(function(pieDom) {
    var ratial = parseFloat(pieDom.innerHTML); // `20%` 转 20 
    pieDom.style.animationDelay = -ratial + "s";
  });
})();

重点:

  1. 给伪元素设置 animation-delay: inherit;,使其值继承自 .pie 元素。
  2. 通过 js ,给每个 .pie 元素设置内联样式 pieDom.style.animationDelay = -ratial + "s";
  3. 负的延时是合法的,就好像动画在过去已经播了指定延时的时间一样。因此动画第一帧为延时值的绝对值处的状态。
  4. animation-play-state: paused; 让动画永久暂停。

6.4 SVG 方案

首先生成一个圆形,并加上描边。

HTML

1
2
3
<svg width="100" height="100">
<circle class="stage-1" r="30" cx="50" cy="50" />
</svg>

CSS

1
2
3
4
5
circle.stage-1 {
  fill: #ff7875;
  stroke: #69c0ff;
  stroke-width: 30;
}

注:

  1. stroke 属性定义了给定图形元素的外轮廓的颜色。
  2. stroke-width 属性指定了当前对象的轮廓的宽度,分布在轮廓线的两侧。

第二步,我们给它添加 stroke-dasharray: 20 11.5; 属性,第一个值指定了线段的长度,第二个指定了缺口的长度。

此圆形的周长为: 2πr = 2 x 3.14 x 30 ≈ 189 ,当我们把缺口长度设为其周长时:
stroke-dasharray: 20 189; ,会出现如下情况。

图中的线段长度就是我们指定的 20px

第三步,调整圆形半径和线段的宽度,就可以得到我们想要的扇形了。

HTML

1
2
3
<svg width="100" height="100">
<circle class="stage-4" r="25" cx="50" cy="50" />
</svg>

CSS

1
2
3
4
5
6
circle.stage-4 {
  fill: #ff7875;
  stroke: #69c0ff;
  stroke-width: 50;
  stroke-dasharray: 20 158;
}

最后,我们给 svg 元素添加背景颜色,并把它逆时针旋转 90deg

HTML

1
2
3
<svg class="final" width="100" height="100">
<circle class="stage-4" r="25" cx="50" cy="50" />
</svg>

CSS

1
2
3
4
5
svg.final {
  transform: rotate(-90deg);
  border-radius: 50%;
  background-color: #ff7875;
}

6.5 饼图进度指示器(SVG)

我们创建一个动画,把 stroke-dasharray 的值从 0 158 变为 158 158

HTML

1
2
3
<svg class="final" width="100" height="100">
<circle class="animation" r="25" cx="50" cy="50" />
</svg>

CSS

1
2
3
4
5
6
7
8
9
10
11
12
@keyframes fillup {
  to {
    stroke-dasharray: 158 158;
  }
}
circle.animation {
  fill: #ff7875;
  stroke: #69c0ff;
  stroke-width: 50;
  stroke-dasharray: 0 158;
  animation: fillup 6s linear infinite;
}

6.6 静态饼图封装(SVG)

我们按照 3.6.3 的方式去封装饼图,使用方式如下:
HTML

1
2
3
<div class="pie-svg">20%</div> 
<div class="pie-svg">60%</div>
<div class="pie-svg animated">0%</div>

效果如下:

20%
60%
0%

CSS

1
2
3
4
5
6
7
8
9
10
11
12
13
.pie-svg {
  display: inline-block;
  width: 100px;
  height: 100px;
}
@keyframes grow {
  to {
    stroke-dasharray: 100 100;
  }
}
.pie-svg.animated circle {
  animation: grow 6s linear infinite;
}

JS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(function() {
  var pieDomArr = document.querySelectorAll('.pie-svg');
  pieDomArr.forEach(function(pieDom) {
    var ratial = parseFloat(pieDom.innerHTML); // `20%` 转 20
    var ns = 'http://www.w3.org/2000/svg';
    var svgDom = document.createElementNS(ns, 'svg');
    var titleDom = document.createElementNS(ns, 'title');
    var circleDom = document.createElementNS(ns, 'circle');
    /* svg 元素 */
    svgDom.setAttribute('viewBox', '0 0 32 32'); //四个参数为: min-x min-y width height
    svgDom.style.transform = 'rotate(-90deg)';
    svgDom.style.borderRadius = '50%';
    /* title 元素 */
    titleDom.innerHTML = pieDom.innerHTML;
    /* circle 元素 */
    circleDom.setAttribute('r', 16);
    circleDom.setAttribute('cx', 16);
    circleDom.setAttribute('cy', 16);
    circleDom.setAttribute('fill', '#ff7875');
    circleDom.setAttribute('stroke', '#69c0ff');
    circleDom.setAttribute('stroke-width', 32);
    circleDom.setAttribute('stroke-dasharray', ratial + ' 100');
    /* 插入至 DOM 中 */
    svgDom.appendChild(titleDom);
    svgDom.appendChild(circleDom);
    pieDom.innerHTML = '';
    pieDom.appendChild(svgDom);
  });
})();

通过 js 生成的 HTML 代码如下:
HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="pie-svg">
  <svg viewBox="0 0 32 32" style="transform: rotate(-90deg); border-radius: 50%;">
    <title>20%</title>
    <circle r="16" cx="16" cy="16" fill="#ff7875" stroke="#69c0ff" stroke-width="32" 
            stroke-dasharray="20 100"></circle>
  </svg>
</div>
<div class="pie-svg">
  <svg viewBox="0 0 32 32" style="transform: rotate(-90deg); border-radius: 50%;">
    <title>60%</title>
    <circle r="16" cx="16" cy="16" fill="#ff7875" stroke="#69c0ff" stroke-width="32" 
            stroke-dasharray="60 100"></circle>
  </svg>
</div>
<div class="pie-svg animated">
  <svg viewBox="0 0 32 32" style="transform: rotate(-90deg); border-radius: 50%;">
    <title>0%</title>
    <circle r="16" cx="16" cy="16" fill="#ff7875" stroke="#69c0ff" stroke-width="32" 
            stroke-dasharray="0 100"></circle>
  </svg>
</div>

注:

  1. svg 元素设置 viewBox="0 0 32 32" ,四个值分别为 min-x min-y width height ,此元素将自动适应容器的大小。
  2. 我们调整 circle 元素的半径,使其周长接近 100 ,最终其 r100 / 2π ≈ 16 , 这样设置饼图的比例时更加方便,如 stroke-dasharray: 38 100 会得到比例为 38% 的饼图。
  3. createElementNS(namespaceURI, qualifiedName): 创建一个具有指定的命名空间URI和限定名称的元素。
  4. 为确保可访问性,在 <svg> 内增加一个 <title> 元素,这样屏幕阅读器的读者也可以知道这个图像显示的是什么比率了。
本文由作者按照 CC BY 4.0 进行授权