08月13日, 2015 3,157 views次
前言
最近 CSS 女神 Lea Verou 出版了一本书《CSS Secrets》,书中有大量的css技巧来解决日常开发会碰到的问题。这是一本让人感到惊喜的书,有兴趣的开发者朋友可以购来一看。本文是翻译了这本书中一个片段。
饼状图,是一种常见的网页技术,可以用来显示统计数据、计时器等。以往我们使用多图片重合或者 javascript 框架来构建饼状图。今天我们使用一种更简单的方式来构建可扩展控制的饼状图。
基于 Transform 的解决方案
这个方法只用到一个元素,剩下只需要定义它的伪元素、变形、渐变就行。我们先开始一个简单的例子:
1 2 |
<div class="pie"></div> |
现在我们假设需要饼状图能显示 20% 的统计比例,而且比例可以随时变化。我们先创造一个圆:
1 2 3 4 5 6 |
.pie { width: 100px; height: 100px; border-radius: 50%; background: yellowgreen; } |
我们的饼状图用yellowgreen
当背景色,用棕色#655
显示百分比。把这两种颜色显示在圆圈的两边,并通过旋转伪元素覆盖出我们需要显示的百分比。
要获得拥有两种颜色的圈,我们只需要使用一个简单的渐变:
1 2 |
background-image:linear-gradient(to right, transparent 50%, #655 0); |
然后,我们继续定义一个伪元素充当遮罩:
1 2 3 4 5 6 7 |
.pie::before { content: ''; display: block; margin-left: 50%; height: 100%; } |
从图3可以看到伪元素相对于饼形图的位置,目前伪元素没有任何样式和内容,仅仅是个无形的矩形。在填充样式之前,我们考虑一下几点:
- 我们希望它能覆盖圆的棕色部分,这需要让它也显示绿色,可以使用
background-color: inherit
来避免重复,表示它有与父元素相同的背景色。 - 我们希望它绕着圆的中心旋转,这个中心现在是位于伪元素的左侧,所以需要使
transform-origin
的值为0 50%
或left
,让伪元素的重心靠左。 - 我们不希望伪元素是方形的,因此需要给
.pie
加上overflow: hidden
,以切除伪元素多余的部分。或者给伪元素设置合适的border-radius
让它像个扇形。
考虑完这些,伪元素的 CSS 可以这样设置:
1 2 3 4 5 6 7 8 9 10 |
.pie::before { content: ''; display: block; margin-left: 50%; height: 100%; border-radius: 0 100% 100% 0 / 50%; background-color: inherit; transform-origin: left; } |
提示:注意不要使用background: inherit;
,应该使用background-color: inherit;
以替代,否则渐变也会被继承!
现在我们的饼状图就是图片4的样子,接下来乐趣就开始了。我们可以使用transform
的rotate()
旋转伪元素,如果是旋转20%,可以使用72deg
(0.2 * 360 = 72),或者使用更容易阅读与理解的.2turn
,当然也可以设置其他不同的值,如图片5中所示:
看起来工作似乎完成了,事实上并没那么简单。饼状图现在能很好的显示0%-50%,但如果要旋转60%,图片6就出现了。不过可以使用一个简单的方法解决这个问题。
如果把50%-100%的情况单独分离出来,就可以注意到做个对立版本:只需要把伪元素的颜色改成棕色。我们可以给伪元素在0%-50%、50%-100%时定义不同的颜色。对于60%的饼状图可以这样写:
1 2 3 4 5 6 7 8 9 10 11 |
.pie::before { content: ''; display: block; margin-left: 50%; height: 100%; border-radius: 0 100% 100% 0 / 50%; background: #655; transform-origin: left; transform: rotate(.1turn); } |
结果如图片7显示,现在就能显示任意百分比了,我们使用css动画做个从0%-100%变化的指示图:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@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; } |
这样一切看起来都很好,但我们如何在HTML里用一种通用简单易读易修改的方式设置不同的百分比显示?如果 HTML 只需要这样写就好了:
1 2 3 |
<div class="pie">20%</div> <div class="pie">60%</div> |
上面代码中定义了两个饼状图,一个显示20%,一个显示60%。首先要考虑如何获取数值并加入到内联样式,这个使用一段简单的 js 就可以完成。不过我们是要在伪元素上设置样式以显示百分比,如你所知,伪元素是无法添加内联样式的,所以我们需要做点创新。
解决方案来自很难被想到的地方——动画的 paused(暂停)属性。我们将使用负的 animation delays 将动画提前暂停在我们需要的时间点。是不是有点迷惑?负的animation-delay
不仅仅是规范允许的,而且非常有用:
提示:animation-delay
可以指定在动画启动之前要等待的时间量,该属性的默认值为 0,并且该属性不会被继承。将 animation-delay 设置为负值会导致动画启动,就好像已经过指定的延迟。如果你在其他的项目中需要使用某些不需要重复和复杂计算的元素的值,或者需要调试动画,都可以使用这种方法。
这里有个简单的例子
因为动画是paused(暂停)的,所以第一帧就会暂停在我们使用负的animation-delay
指定的位置,饼状图上的比例就是通过animation-delay
定义的相对整个动画运行一周所需时间的比例。例如当一个完成的动画需要6s
时,我们可以定义animation-delay
为-1.2s
来显示20%。为了方便计算,我们把接下来整个动画时间设定为100s
来完成饼形图。
现在还有一个问题:动画是要运行在伪元素上,但我们只能给.pie
设置内联样式。.pie
本身没有动画,所以我们可以给它设置animation-delay
,然后再给伪元素设置animation-delay: inherit;
以继承该属性。考虑完这些,20% 跟 60% 的饼形图可以这样写:
1 2 3 |
<div class="pie" style="animation-delay: -20s">20%</div> <div class="pie" style="animation-delay: -20s">60%</div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@keyframes spin { to { transform: rotate(.5turn); } } @keyframes bg { 50% { background: #655; } } .pie::before { /* [Rest of styling stays the same] */ animation: spin 50s linear infinite, bg 100s step-end infinite; animation-play-state: paused; animation-delay: inherit; } |
我们最初是要把需要显示的百分比写在HTML里,而不是直接写成内联样式。这个可以通过一段简单的js把百分比提取出来作为animation-delay
内联样式:
1 2 3 4 5 |
$('.pie').each(function(index,pie) { var p = parseFloat(pie.textContent); pie.style.animationDelay = '-' + p + 's'; }); |
现在的饼状图如图片8所示,左下角存在个文本,我们可以给文本应用color: transparent
以隐藏文本,同时是它仍然可选择可打印。另外可以把它放在饼状图的中间位置,以便用户点选。要做到这点,我们需要做这些:
- 把 pie 的
height
转换成line-height
,以便使内容能垂直居中,同时有了line-height
后元素会自动计算高度。 - 把伪元素设置为绝对定位,这样它就不会把文本挤下来。
- 添加
text-align: center;
使文本水平居中。
最后的代码如下所示:
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 |
.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; } |
SVG 解决方案
SVG使很多图形绘画处理变得方便。但是使用路径绘画出一个饼状图需要很复杂的数学,我们将使用一个简单的技巧来绘制。
我们先画一个圆:
1 2 3 4 |
<svg width="100" height="100"> <circle r="30" cx="50" cy="50" /> </svg> |
然后给它定义点基本样式:
1 2 3 4 5 6 |
circle { fill: yellowgreen; stroke: #655; stroke-width: 30; } |
提示:你可能已经知道了,CSS属性也可以作为SVG元素的属性。
你可以在图片9看到一个带描边的圆,不过 SVG 的描边并不只有stroke
跟stroke-with
两种属性,还有很多不常用的描边属性来定义描边。比如stroke-dasharray
,用它可以创造一个虚线的描边,下面有个例子:
1 2 |
stroke-dasharray: 20 10; |
如果定义了虚线长为20
间隔为10
就会得到图片10。你可能还是很奇怪饼状图跟这个 SVG 的描边属性有什么关系。如果我把描边的长度定为0,描边间隔定为大于等于它的周长(C = 2πr,描边的宽度为30,所以它的周长是2π * 30 ≈ 189),事件就清晰很多了:
1 2 |
stroke-dasharray: 0 189; |
可以看到图片11中的第一个圆已经完全没有了描边,只剩一个绿色圆。当我们增加第一个属性值的大小,因为虚线间距太长以致无法显示出第二条虚线,只显示了第一条虚线,这条虚线的长度我们可以随意定义的。
你可能已经猜到我是要干嘛:如果我们减少圆的半径,一直到它能完全被描边覆盖,我们就能得到一个类似闭合的饼状图。如图片12我们把圆的半径定义为25
,把描边的宽定义为50
:
注意:SVG 的描边总是一半在元素外部一半在元素内部。现在是无法定义它处于外部或内部的,可能在未来新的css协议下我们能控制这个行为。
1 2 3 4 |
<svg width="100" height="100"> <circle r="25" cx="50" cy="50" /> </svg> |
1 2 3 4 5 6 7 |
circle { fill: yellowgreen; stroke: #655; stroke-width: 50; stroke-dasharray: 60 158; /* 2π × 25 ≈ 158 */ } |
现在我们只需要在描边下方添加一个大的绿色圆圈,并且把描边逆时针旋转90°让它的起点处于顶部的中间。由于 SVG 也是一个 HTML 元素,我们可以给它添加样式:
1 2 3 4 5 6 |
svg { transform: rotate(-90deg); background: yellowgreen; border-radius: 50%; } |
你可以在图片13看到最终结果,这个方法可以更简单的使用动画让比例从 0% 变化到 100%,我们只需要添加一个 CSS 动画让stroke-dasharray
从0 158
变化到158 158
:
1 2 3 4 5 6 7 8 9 10 11 12 |
@keyframes fillup { to { stroke-dasharray: 158 158; } } circle { fill: yellowgreen; stroke: #655; stroke-width: 50; stroke-dasharray: 0 158; animation: fillup 5s linear infinite; } |
为了更简单指定不同比例饼形图,我们可以给圆指定一个半径,让它的周长等于(无限接近)100,这样就可以很简单明了的指定虚线的百分比,而不用计算。周长等于2πr,我们需要的半径就是 100 ÷ 2π ≈ 15.915494309,把它约等于16。然后再给 SVG 指定一个viewBox
属性来代替width
和height
以使他放大充满画布。
经过这些修改,代码变成:
1 2 3 4 |
<svg viewBox="0 0 32 32"> <circle r="16" cx="16" cy="16" /> </svg> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
svg { width: 100px; height: 100px; transform: rotate(-90deg); background: yellowgreen; border-radius: 50%; } circle { fill: yellowgreen; stroke: #655; stroke-width: 32; stroke-dasharray: 38 100; /* for 38% */ } |
如果要简单的定义出更多的饼状图,我们肯定不希望重复的绘制SVG。可以让JavaScript自动化的做这件事,只需写一段简单的 HTML,剩下的交给 JavaScript:
1 2 3 |
<div class="pie">20%</div> <div class="pie">60%</div> |
把 SVG 加入到每个.pie
元素里。同时可以添加个<title>
元素,以便屏幕阅读器的用户可以了解是什么比例呈现在显示器上。最后的 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 |
$('.pie').each(function(index,pie) { var p = parseFloat(pie.textContent); pie.style.animationDelay = '-' + p + 's'; }); $('.pieSvg').each(function(index, pie) { var p = parseFloat(pie.textContent); var NS = "http://www.w3.org/2000/svg"; var svg = document.createElementNS(NS, "svg"); var circle = document.createElementNS(NS, "circle"); var title = document.createElementNS(NS, "title"); $(svg).attr('viewBox', '0 0 32 32'); $(circle).attr({ r: '16', cx: '16', cy: '16', 'stroke-dasharray': p + " "+ "100" }); $(title).textContent = pie.textContent; pie.textContent = ''; $(svg).append(circle); $(pie).append(svg) }); |
好了,SVG 的方法到此为止。你可能觉得 CSS Transform 的解决方法更好,因为代码更简单易读,但 SVG 的方法相对 纯粹地CSS是有好处的:
- 它可以简单的添加第三种颜色:只需添加另外一种颜色的描边并使用
stroke-dashoffset
偏移上一个描边的长度量。或者也可以让这个新描边的长度大于上一个描边,并置于上一个描边底部。 - 我们不需要额外的再考虑打印问题:SVG 元素就像
img
元素一样是可以直接打印的,第一种解决方案颜色是取于背景色,无法打印。 - 我们可以通过内联样式改变颜色,这意味这我们可以通过 js (例如,通过用户输入框)直接修改样式。第一种元素依赖于伪元素,只能使用继承,无法使用内联样式,这是不方便的。
未来的饼状图
一个新的 CSS 属性有望添加进 CSS4 标准中,锥形渐变(Conical gradients)。这个只需要个圆形元素,然后用锥形渐变定义两种颜色,就可以简单的做个饼状图。
1 2 3 4 5 6 |
.pie { width: 100px; height: 100px; border-radius: 50%; background: conic-gradient(#655 40%, yellowgreen 0); } |
此外,通过attr()
控制 HTML 属性,你可以轻易地改变饼状图显示比例:
1 2 |
background: conic-gradient(#655 attr(data-value %), yellowgreen 0); |
而且它也能很简单的添加第三种颜色,例如我们要在一个饼状图上添加两种颜色以显示比例,只需在渐变上多加两种颜色:
1 2 |
background: conic-gradient(deeppink 20%, #fb3 0, #fb3 30%, yellowgreen 0); |
现在已经有一部分浏览器支持conic-gradient
了(例如最新的谷歌浏览器),这里有锥形渐变的详细介绍: Lea’s Conic Gradient polyfill。
终于翻译完了!!!囧rz,《CSS Secrets》是本不错的书,本文只是这本书中的一个奇技淫巧,有兴趣的可以买原版一看或者等译版出版后再买。这里是Lea Verou的博客,学习前端的可以收藏下。本文如果转载请注明版权跟原文地址。
人生舞台的大幕随时都可能拉开,关键是你愿意表演,还是选择躲避。