使用圆锥渐变和CSS自定义属性创建一个Range Input控制的环形图
特别声明:此篇文章内容来源于@Vicky.Ye翻译的《使用圆锥渐变和CSS变量创建一个Range Input控制的环形图》一文。英文原文来自于@Ana Tudor的《Using Conic Gradients and CSS Variables to Create a Doughnut Chart Output for a Range Input》一文。
最近我在 Codepen 上看到了一个例子,我的第一个想法是这个案例可以只用三个元素完成:一个容器,一个 range
类型的 input
和一个 output
。在 CSS 方面,涉及到使用一个把 CSS 自定义属性作为范围渲染参数的圆锥渐变函数 conic-gradient()
。
在2015年年中,@Lea Verou 在一次会议演讲中发布了一个 conic-gradient()
的 polyfill,并演示了如何将它们用于创建饼图。这个 polyfill 非常适合 conic-gradient()
的入门学习,因为它使我们能够更全面的使用这个函数来构建我们想要的东西。不幸的是,它不适用于 CSS自定义属性,而 CSS自定义属性现在已成为编写高效代码的关键组成部分。
但好消息是,在过去的两年半时间里,情况有所转变。 一般来说,Chrome 浏览器和使用暴露标志的 Blink 引擎浏览器(例如 Opera )现在都支持原生的 conic-gradient()
,这意味着已经有可能尝试以 CSS自定义属性作为 conic-gradient()
的范围值 。 我们所需要做的就是在 chrome://flags
启用 Experimental Web Platform Features
标志(或者,如果您使用 Opera ,opera://flags
):
好吧,现在我们可以开始了!
初始结构
一开始我们需要一个容器和一个 range
类型的 input
:
<div class="wrap">
<input id="r" type="range"/>
</div>
请注意,我们没有 output
元素。 因为当 JavaScript 由于某种原因被禁用或加载失败时,未更新元素就会出现在页面里,所以我们需要通过 JavaScript 来动态更新 output
标签的值。同样的也需要通过 JavaScript 来判断浏览器是否支持 conic-gradient()
,并在容器上动态添加一个 class
作为标识。
如果我们的浏览器支持 conic-gradient()
,则容器将获得一个 .full
样式, .full
下的 output
样式将会显示到图表上。 否则,我们只有一个没有图表的简单滑动条, output
位于滑动条按钮上。
浏览器支持 conic-gradient()
(上边)和浏览器不支持时的兜底方案(下边)。
基本样式
在着手之前,我们希望滑动条在所有浏览器上显示都是没问题的。
我们先从最基本的样式重置开始,并设置 body
的 background
:
$bg: #3d3d4a;
* {
margin: 0
}
body {
background: $bg
}
第二步是准备滑动条在 WebKit 浏览器中的样式,这里我们要通过设置 -webkit-appearance: none
和其按钮样式(因为某种原因系统设置了该轨道的默认样式),为避免不同浏览器中默认属性的不一致,如 padding
,background
或 font
,我们要给出明确值:
[type='range'] {
&, &::-webkit-slider-thumb {
-webkit-appearance: none
}
display: block;
padding: 0;
background: transparent;
font: inherit
}
如果您需要了解滑动条及其组件在各种浏览器中的工作方式,请查看我以前写的一篇有关于Rang Input的文章。
现在我们可以进入更有趣的部分了。 设置轨道和按钮的尺寸,并通过相应的@mixin
指令将它们绑到滑动条组件上。通过添加 background
让其在屏幕上可见,设置 border-radius
来对其进行美化。为了与预期的效果一致,我们将这两个元素的 border
设为 none
。
$k: .1;
$track-w: 25em;
$track-h: .02*$track-w;
$thumb-d: $k*$track-w;
@mixin track() {
border: none;
width: $track-w;
height: $track-h;
border-radius: .5*$track-h;
background: #343440
}
@mixin thumb() {
border: none;
width: $thumb-d;
height: $thumb-d;
border-radius: 50%;
background: #e6323e
}
[type='range'] {
/* same styles as before */
width: $track-w;
height: $thumb-d;
&::-webkit-slider-runnable-track {
@include track
}
&::-moz-range-track {
@include track
}
&::-ms-track {
@include track
}
&::-webkit-slider-thumb {
margin-top: .5*($track-h - $thumb-d);
@include thumb
}
&::-moz-range-thumb {
@include thumb
}
&::-ms-thumb {
margin-top: 0;
@include thumb
}
}
我们添加一些属性,如在容器上设置 margin
,给定明确的 width
和 font
:
.wrap {
margin: 2em auto;
width: $track-w;
font: 2vmin trebuchet ms, arial, sans-serif
}
我们不想让它变得太小或太大,所以我们限制了font-size
:
.wrap {
@media (max-width: 500px), (max-height: 500px) {
font-size: 10px
}
@media (min-width: 1600px), (min-height: 1600px) {
font-size: 32px
}
}
然后,现在我们有了一个不错的跨浏览器滑动条:
JavaScript
首先我们要获取到滑动条、容器和创建的 output
元素。
const _R = document.getElementById('r'),
_W = _R.parentNode,
_O = document.createElement('output');
创建一个变量 val
,用于存储 range
类型的 input
的当前值:
let val = null;
接下来,我们创建一个 update()
函数,用于检查当前滑块值是否等于已存的值。 如果不是,则更新 JavaScript 里 val
变量、 output
的文本内容和外框上的 CSS自定义属性 --val
。
function update() {
let newval = +_R.value;
if(val !== newval)
_W.style.setProperty('--val', _O.value = val = newval)
};
在我们继续编写JavaScript之前,在 output
的 CSS 中设置一个 conic-gradient()
:
output {
background: conic-gradient(#e64c65 calc(var(--val)*1%), #41a8ab 0%)
}
我们通过调用 update()
函数,将 output
作为子 DOM 元素添加到容器上,然后测试 output
的 background-image
是否可设置 conic-gradient()
(注意,这步需要确定 DOM 元素添加之后,才可这么做)。
如果测试出的 background-image
不是 none
(如果是 none
,则没有原生 conic-gradient()
支持),则在容器上添加一个 full
样式。 并通过 for
属性将 output
与 range
类型的input
绑在一起 。
通过事件监听器,我们确保每次移动滑块时都能调用 update()
函数。
_O.setAttribute('for', _R.id);
update();
_W.appendChild(_O);
if(getComputedStyle(_O).backgroundImage !== 'none')
_W.classList.add('full');
_R.addEventListener('input', update, false);
_R.addEventListener('change', update, false);
现在我们有了一个滑动条和一个 output
(如果我们的浏览器支持原生 conic-gradient()
,可以查看到它的值显示在 conic-gradient()
背景上)。 虽然在这个阶段仍然很丑,但它的功能基本实现——当我们拖动滑块时, output
的值会随着变化:
我们给 output
加了一个浅色值,以便我们可以更好地看到它,并通过 ::after
伪类在末尾添加 %
。 还需要 display
设置为 none
来隐藏 Edge 中的工具提示(::-ms-tooltip
)。
没有图表的情况
当我们没有 conic-gradient()
支持时,就会出现没有图表这种情况。 我们想要实现的效果如下图:
美化输出样式
在上面的基础上,我们给 output
设置绝对定位,获取滑块按钮的尺寸并将输出的文本居中显示:
.wrap:not(.full) {
position: relative;
output {
position: absolute;
top: 0;
width: $thumb-d;
height: $thumb-d
}
}
output {
display: flex;
align-items: center;
justify-content: center;
}
如果您需要进一步了解
align-items
和justify-content
,请参阅 @Patrick Brosset 的 《揭开 CSS 对齐的神秘面纱》或者查阅W3cplus上有关于Flexbox布局相关的资料。
结果可以在下面的 Codepen 中看到,我们依然增加了一个 outline
以便清楚地看到 output
的边框:
这乍看起来像是那么回事儿,但是我们的 output
的文字不随着滑块按钮移动。
使输出文字移动
为了解决这个问题,我们首先要清楚滑块按钮是如何运动的。 在 Chrome 中,滑块按钮的 border-box
在 input
中的滑动轨道 content-box
的范围内移动,而在 Firefox 和 Edge 中,滑块按钮的 border-box
在实际 input
滑动条 content-box
的范围内移动。
虽然这种差异可能会在某些情况下出现问题,但我们的用例很简单, 并没有在滑动条或其组件上设置 margin
,padding
或 border
,所以滑动条本身、滑动轨道、滑动按钮的这三个属性 (content-box
, padding-box
和 border-box
)是重合的。 此外,实际 input
的这三个属性的宽度与其轨道的三个属性的宽度重合。
这意味着当滑块值最小时(我们没有明确设置,因此它最小值默认是 0 ),滑动按钮框的左边缘与 input 的左边缘(和轨道的左边缘)重合 。
同样,当滑块值达到其最大值(未明确设置时,它最大值默认 100
)时,滑动按钮框的右边缘与 input
的右边缘(以及轨道的右边缘)重合。将按钮的左边缘放在滑块的右边缘之前,向左大概一个按钮宽度( $thumb-d
)。
下图显示了输入框的宽度 width
( $track-w
)—— 显示为 1
。按钮宽 width
( $thumb-d
)设为 k
(因为我们已将它设置为 $thumb-d: $k * $track-w
),长度为输入框的宽度的 一个分数 。
滑块按钮处于最小值和最大值(实例)。
至此,我们得到左边按钮在 input
上的可滑动范围为 input
的 width
( $track-w
)减去按钮宽度( $thumb-d
),这就是它最小值到最大值的距离。
为了以相同的方式移动 output
,我们用一个 translation
过渡按钮位置来说明。 当滑动条值处于最小位置时,output
的初始位置位于按钮的最左边,所以此时 transform
是 translate(0)
。output
为最大值时的位置,就是滑动条值达到最大值的位置,我们需要将它转换为 $track-w - $thumb-d = $track-w * (1 - $k)
。
按钮的运动范围以及 output
(实例)的范围。
好吧,但是它们之间的值呢?
那么,记住每次更新滑动条值时,我们不仅要更新 output
输出的文本内容,还要将绑在容器上的 CSS自定义属性 --val
进行更新。 这个 CSS自定义属性在 0
(当滑块值最小时为 0
)到 100
(当滑块值最大时为100
)之间。
所以如果我们通过 calc(var(--val) / 100 * #{$track-w - $thumb-d})
沿水平轴(x
轴)平移 output
,它会随着滑动按钮移动,而不需要我们做任何事:
需要注意的是,如果点击轨道上的其他位置,上述方法会工作,但如果我们尝试拖动按钮 ,则不会有反应。 这是因为 output
现在位于 input
滑动按钮之上,滑动按钮捕捉不到点击事件。
我们通过在 output
设置 pointer-events: none
解决这个问题。
在上面的演示中,删除了 output
元素上的 outline
辅助线,因为我们不再需要它了。
现在我们对不支持 conic-gradient()
浏览器有了很好的兜底方案,可以继续构建我们想要的结果了(有启用标志的 Chrome / Opera)。
有图表的情况
绘制所需布局
在开始编写代码之前,我们需要清楚地知道我们想要实现的目标。为了明确这点,我们做了一个尺寸等于轨道 width
( $track-w
)的布局草图,这也是 input
的宽度和容器 content-box
的边长(容器 padding
不包含在内)。
这意味着我们容器的 content-box
是边长为1
的正方形(等于轨道 width
),input
是一个边长等于容器边长的长方形,且另一个边长是容器边长的分数 k
,则滑动按钮是一个 k * k
的方块。
有图表情况下所需的布局 (实例)。
该图表是边长为 1 - 2 * k
的正方形,容器中图表距滑动条有 k
间隙,与滑动条相对方向的容器边缘没有间隙。考虑到容器的边长是 1
,图表的边长是 1 - 2 * k
,所以容器距图表上下边缘之间有k
间隙。
调整我们的元素
获得这种布局的第一步是使容器为正方形,并将 output
的尺寸设置为 (1 - 2 * $k) * 100%
:
$k: .1;
$track-w: 25em;
$chart-d: (1 - 2*$k)*100%;
.wrap.full {
width: $track-w;
output {
width: $chart-d;
height: $chart-d
}
}
结果可以在下面看到,我们还添加了一些辅助线以更好地看到事物:
第一阶段的结果( 实例 ,只有支持原生conic-gradient()
浏览器可见)。
这是一个好的开始,因为 output
已经在我们想要的位置上了。
制作垂直滑块
WebKit 浏览器的“官方”方式是在 range
类 input
上设置 -webkit-appearance: vertical
。 但是,这会破坏自定义样式,因为它们要求我们将 -webkit-appearance
设置为 none
,而我们不能给 -webkit-appearance
同时设置两个不同的值。
所以我们只能使用简便的解决方案 transform
。我们想要的是在容器的底部有最小值,在容器的顶部有最大值。 但实际上,在容器的左端是滑块是最小值,最右端是最大值的。
滑块的初始位置与我们预期达到的最终位置(实例)。
这看起来像是在右上角(以水平方向 100%
和垂直 0%
的中心点 transform-origin
)沿负方向旋转 90°
(因为顺时针方向是正方向)
这是一个好的开始,但现在我们的滑块在容器边界之外。 为了让滑动条旋转到期望的位置,我们需要了解这个旋转做了什么。 它不仅旋转了 input
元素,而且还旋转了它自身的坐标系。 现在它的 x
轴向上, y
轴向右。
因此,为了将其放入容器右侧内部,我们需要在旋转之后将其沿着其y
轴的负方向平移自身 height
的距离。 这意味着我们应用的最终 transform
链是 rotate(-90deg) translatey(-100%)
。 (请记住, translate()
函数中使用的 %
值与被翻转元素的尺寸有关。)
.wrap.full {
input {
transform-origin: 100% 0;
transform: rotate(-90deg) translatey(-100%)
}
}
通过上述操作,我们得到所需布局:
第二阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
设计图表的样式
当然,第一步是 border-radius
使图表变圆,并调整 color
,font-size
和 font-weight
属性。
.wrap.full {
output {
border-radius: 50%;
color: #7a7a7a;
font-size: 4.25em;
font-weight: 700
}
}
您可能已经注意到我们已经将图表的尺寸设置为 (1 - 2 * $k) * 100%
而不是 (1 - 2 * $k) * $track-w
。 这是因为 $track-w
是 em
值,这意味着计算出相等的像素值取决于该元素的 font-size
属性 。
但是,我们希望能够通过增加 font-size
控制,而不必调整 em
值。 这是并不复杂,但与仅将尺寸设置为不依赖于 font-size
的 %
值相比,它仍然有点额外的工作。
第三阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
从饼图到环形图
在文字中间模拟一个洞的最简单方法是在 conic-gradient()
上添加另一个 background
层。 我们也可以通过添加混合模式来完成这个目标,这需要有背景图片,但没这个必要。要做一个实心的 background
,一个简单的遮罩层就可以做到。
$p: 39%;
background: radial-gradient($bg $p, transparent $p + .5%),
conic-gradient(#e64c65 calc(var(--val)*1%), #41a8ab 0%);
好的,按以上这么做图表就完成了!
第四阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
在按钮上显示数值
在容器上用一个绝对定位的 ::after
伪类来完成此操作。 设置这个伪类尺寸为按钮大小,并将它定位在容器的右下角,滑块值最小时按钮所在的位置。
.wrap.full {
position: relative;
&::after {
position: absolute;
right: 0;
bottom: 0;
width: $thumb-d;
height: $thumb-d;
content: '';
}
}
我们也给它一个线框,以便我们可以看到它。
第五阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)
将它与按钮一起移动,与没有图表的情况下类似,只是这次移动是沿y
轴在负方向(而不是沿x
轴正方向)移动。
transform: translatey(calc(var(--val)/-100*#{$track-w - $thumb-d}))
为了能够拖动伪类下的按钮,我们还必须在这个伪元素上设置 pointer-events: none
。 结果可以在下图看到 —— 拖动按钮还会移动容器的 ::before
伪类。
第六阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
看起来不错,但我们真正想要的是使用这个伪类显示当前值。 如果将其 content
属性设置为 var (--val)
,则不会执行任何操作,因为 --val
是数字,而不是字符串。 如果我们将它设置为字符串,则它可成为 content
的值,但就不能再将它用于 calc()
。
幸运的是,我们可以通过使用CSS计数器的方法来解决这个问题:
counter-reset: val var(--val);
content: counter(val)'%';
现在所有功能已完成,耶!
第七阶段的结果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
接下来,让我们继续添加一些属性来优化它。 我们把文本放在滑动按钮的中间,字体颜色设为白色,去掉所有辅助线,并在 input
上设置 cursor: pointer
:
.wrap.full {
&::after {
line-height: $thumb-d;
color: #fff;
text-align: center
}
}
[type='range'] {
cursor: pointer
}
优化后的效果:
图表情况的最终效果( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
清除重复样式代码
代码中还有要优化的地方,在没有图表情况下的 output
样式和有图表的 :after
伪类中存在一堆重复的样式。
在无图表情况下 output
的样式与有图表情况下的 .wrap:after
样式
我们可以对此做些优化, 然后我们使用一个简短的扩展样式:
%thumb-val {
position: absolute;
width: $thumb-d;
height: $thumb-d;
color: #fff;
pointer-events: none
}
.wrap {
&:not(.full) output {
@extend %thumb-val;
}
&:after {
@extend %thumb-val;
}
}
不错的焦点样式
比方说,我们不想在 :focus
时出现 outline
,但又希望在视觉上清楚地区分获焦这种状态。 那我们该如何做? 我们可以在 input
没有获焦时,缩小滑动按钮,降低色彩饱和度,并且隐藏输出的文字。
这听起来像个很酷……但是,由于我们没有父选择器,所以当滑动条获焦或失焦时,我们无法在改变滑动条父级的 ::after
属性。 额….
但可以做的是使用 output
的其他伪元素(::before
)来显示按钮上的值。 这并不难做到,稍后我们会讨论,它允许我们做如下操作:
[type='range']:focus + output:before {
/* focus styles */
}
采取这种方法的问题在于,我们如何放大 output
的字体 font
,但又不改变容器 ::before
伪类的大小和粗细。
我们可以通过设置Sass变量来解决这个问题,将相对字体大小定义为变量 $fsr
,然后使该值在实际 output
上放大字体 font
,在 output:before
伪类上将其恢复为之前的大小,如下 。
$fsr: 4;
.wrap {
color: $fg;
&.full {
output {
font-size: $fsr*1em;
&:before {
font-size: 1em/$fsr;
font-weight: 200;
}
}
}
}
除此之外,我们只需要移动我们在 .wrap:after
上的 CSS自定义属性到 output:before
上 。
容器伪元素上的样式与 output
伪元素上的样式。
好的,现在我们可以进入区分正常和聚焦效果的最后一步。
当滑块没有被聚焦时,我们首先隐藏丑陋的 :focus
默认 outline
状态和按钮上的值:
%thumb-val {
opacity: 0;
}
[type='range']:focus {
outline: none;
.wrap:not(.full) & + output,
.wrap.full & + output:before {
opacity: 1
}
}
只有当滑动条获得焦点时,按钮上的值才可见( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
接下来,我们为滑块按钮的正常和聚焦状态设置不同的样式:
@mixin thumb() {
transform: scale(.7);
filter: saturate(.7)
}
@mixin thumb-focus() {
transform: none;
filter: none
}
[type='range']:focus {
&::-webkit-slider-thumb {
@include thumb-focus
}
&::-moz-range-thumb {
@include thumb-focus
}
&::-ms-thumb {
@include thumb-focus
}
}
只要滑块没有聚焦,按钮才缩小和去饱和( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
最后一步是添加这些状态之间的转换:
$t: .5s;
@mixin thumb() {
transition: transform $t linear, filter $t
}
%thumb-val {
transition: opacity $t ease-in-out
}
该例子显示正常状态和聚焦状态之间的转换( 实例 ,只有支持原生 conic-gradient()
浏览器可见)。
什么是屏幕读取?
由于屏幕读取最近生成的内容,因此在这种情况下,我们会将 %
值读取两次。 所以我们通过在 output
上设置 role='img'
来解决这个问题,然后把我们想要读取的当前值放在 aria-label
属性中:
let conic = false;
function update() {
let newval = +_R.value;
if(val !== newval) {
_W.style.setProperty('--val', _O.value = val = newval);
if(conic) _O.setAttribute('aria-label', `${val}%`)
}
};
update();
_O.setAttribute('for', _R.id);
_W.appendChild(_O);
if(getComputedStyle(_O).backgroundImage !== 'none') {
conic = true;
_W.classList.add('full');
_O.setAttribute('role', 'img');
_O.setAttribute('aria-label', `${val}%`)
}
最后的演示可以在下面链接中找到。 请注意,如果您的浏览器没有原生 conic-gradient()
支持,你只会看到兜底样式。
最后的话
尽管浏览器对 conic-gradient()
的支持仍然很差,但情况将会有所改变。 目前只有暴露标志的 Blink 浏览器支持,但 Safari 将 conic-gradient()
列为正在开发中 ,所以事情已经越来越好了。
如果您希望跨浏览器支持早日成为现实,您可以通过在 Edge 中投票实现 conic-gradient()
或通过对 Firefox 错误发表评论来说明为什么您认为这很重要或是什么让您使用它。 这里是我发表的作品。
如需转载,烦请注明出处:https://www.fedev.cn/css/using-conic-gradients-css-variables-create-doughnut-chart-output-range-input.htmlnike air max 1 red