此篇博客是学习《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()
变形属性来生成一个平行四边形:
CSS:
1
2
3
.parallelogram {
transform: skewX(-30deg);
}
我们生成了一个平行四边形,但是内容也跟着一起变形了。怎样让内容保持不变呢?
两个元素方案
内层的内容元素使用一次反向的 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);
}
伪元素方案
此方案的重点是把所有的样式应用到为元素上,然后在对为元素进行变形
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/left
为0
;
- 我们设置了宿主元素的
- 由于伪元素会覆盖在宿主元素之上,所以我们设置伪元素
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);
}
注:
- 我们保留了图片宽度为
100%
这个值,当浏览器不支持transform
时,仍然可以得到一个合理的布局,因此没有使用width: 142%;
。 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%);
}
注:
polygon()
用于生成一个多边形,其接受至少三对坐标值,使用逗号分隔。clip-path
可以参与动画,上述代码实现了鼠标悬停时图片平滑地显示其完整形状。- safari 浏览器不支持该属性。
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);
}
实际效果如下:
这样是行不通的,两层渐变背景默认会填满整个元素,且会相互覆盖。
我们可以通过如下几步,把两层背景分别放在左侧和右侧:
background-repeat
设为no-repeat
;background-size
设为50% 100%
;backgound-position
分别设为right
和left
。
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 旋转伪元素生成梯形:
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;
}
要点:
- 元素本身相对定位,其伪元素绝对定位;
- 伪元素通过
transform
属性缩放和旋转:scaleY(1.3)
Y 方向拉伸 1.3 倍,抵消 X 方向旋转 5 度后视觉上高度降低的效果;rotateX(5deg)
X 方向旋转 5 度,用于生成视觉上的梯形效果;perspective(.5em)
指定了观察者与 z=0 平面的距离;
transform-origin: bottom;
指定元素变形的原点。
根据上述特点,我们可以设计出复杂的梯形标签页:
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>
由于无法给伪元素设置内联样式,因此我们通过让上一节中的动画暂定的方式来封装代码。
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";
});
})();
重点:
- 给伪元素设置
animation-delay: inherit;
,使其值继承自.pie
元素。 - 通过 js ,给每个
.pie
元素设置内联样式pieDom.style.animationDelay = -ratial + "s";
。 - 负的延时是合法的,就好像动画在过去已经播了指定延时的时间一样。因此动画第一帧为延时值的绝对值处的状态。
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;
}
注:
stroke
属性定义了给定图形元素的外轮廓的颜色。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>
效果如下:
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>
注:
- 给
svg
元素设置viewBox="0 0 32 32"
,四个值分别为min-x min-y width height
,此元素将自动适应容器的大小。 - 我们调整
circle
元素的半径,使其周长接近100
,最终其r
为100 / 2π ≈ 16
, 这样设置饼图的比例时更加方便,如stroke-dasharray: 38 100
会得到比例为38%
的饼图。 createElementNS(namespaceURI, qualifiedName)
: 创建一个具有指定的命名空间URI和限定名称的元素。- 为确保可访问性,在
<svg>
内增加一个<title>
元素,这样屏幕阅读器的读者也可以知道这个图像显示的是什么比率了。