使用圆锥渐变和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 上看到了一个例子,我的第一个想法是这个案例可以只用三个元素完成:一个容器,一个 类型的 和一个 。在 CSS 方面,涉及到使用一个把 CSS 自定义属性作为范围渲染参数的圆锥渐变函数 。

在2015年年中,@Lea Verou 在一次会议演讲中发布了一个 的 polyfill,并演示了如何将它们用于创建饼图。这个 polyfill 非常适合 的入门学习,因为它使我们能够更全面的使用这个函数来构建我们想要的东西。不幸的是,它不适用于 CSS自定义属性,而 CSS自定义属性现在已成为编写高效代码的关键组成部分。

但好消息是,在过去的两年半时间里,情况有所转变。 一般来说,Chrome 浏览器和使用暴露标志的 Blink 引擎浏览器(例如 Opera )现在都支持原生的 ,这意味着已经有可能尝试以 CSS自定义属性作为 的范围值 。 我们所需要做的就是在 启用标志(或者,如果您使用 Opera , ):

好吧,现在我们可以开始了!

初始结构

一开始我们需要一个容器和一个 类型的 :

请注意,我们没有 元素。 因为当 JavaScript 由于某种原因被禁用或加载失败时,未更新元素就会出现在页面里,所以我们需要通过 JavaScript 来动态更新 标签的值。同样的也需要通过 JavaScript 来判断浏览器是否支持 ,并在容器上动态添加一个 作为标识。

如果我们的浏览器支持 ,则容器将获得一个 样式, 下的 样式将会显示到图表上。 否则,我们只有一个没有图表的简单滑动条, 位于滑动条按钮上。

浏览器支持 (上边)和浏览器不支持时的兜底方案(下边)。

基本样式

在着手之前,我们希望滑动条在所有浏览器上显示都是没问题的。

我们先从最基本的样式重置开始,并设置 的 :

$bg:#3d3d4a;*{margin:}body{background:$bg }

第二步是准备滑动条在 WebKit 浏览器中的样式,这里我们要通过设置 和其按钮样式(因为某种原因系统设置了该轨道的默认样式),为避免不同浏览器中默认属性的不一致,如 , 或 ,我们要给出明确值:

[type='range'] { &, &::-webkit-slider-thumb{ -webkit-appearance:none }display:block;padding:;

background:transparent;

font:inherit}

如果您需要了解滑动条及其组件在各种浏览器中的工作方式,请查看我以前写的一篇有关于Rang Input的文章。

现在我们可以进入更有趣的部分了。 设置轨道和按钮的尺寸,并通过相应的指令将它们绑到滑动条组件上。通过添加 让其在屏幕上可见,设置 来对其进行美化。为了与预期的效果一致,我们将这两个元素的 设为 。

我们添加一些属性,如在容器上设置 ,给定明确的 和 :

.wrap{

margin:2em auto;width:$track-w;font:2vmin trebuchet ms, arial, sans-serif}

我们不想让它变得太小或太大,所以我们限制了:

.wrap {

@media(max-width:500px), (max-height:500px) { font-size:10px }

@media(min-width:1600px), (min-height:1600px) { font-size:32px }}

然后,现在我们有了一个不错的跨浏览器滑动条:

JavaScript

首先我们要获取到滑动条、容器和创建的 元素。

const_R = document.getElementById('r'), _W = _R.parentNode, _O = document.createElement('output');

创建一个变量 ,用于存储 类型的 的当前值:

letval =null;

接下来,我们创建一个 函数,用于检查当前滑块值是否等于已存的值。 如果不是,则更新 JavaScript 里 变量、 的文本内容和外框上的 CSS自定义属性 。

functionupdate(){letnewval = +_R.value;

if(val !== newval) _W.style.setProperty('--val', _O.value = val = newval)};

在我们继续编写JavaScript之前,在 的 CSS 中设置一个 :

output{

background:conic-gradient(#e64c65 calc(var(--val)*1%),#41a8ab%)}

我们通过调用 函数,将 作为子 DOM 元素添加到容器上,然后测试 的 是否可设置 (注意,这步需要确定 DOM 元素添加之后,才可这么做)。

如果测试出的 不是 (如果是 ,则没有原生 支持),则在容器上添加一个 样式。 并通过 属性将 与 类型的 绑在一起 。

通过事件监听器,我们确保每次移动滑块时都能调用 函数。

_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);

现在我们有了一个滑动条和一个 (如果我们的浏览器支持原生 ,可以查看到它的值显示在 背景上)。 虽然在这个阶段仍然很丑,但它的功能基本实现——当我们拖动滑块时, 的值会随着变化:

我们给 加了一个浅色值,以便我们可以更好地看到它,并通过 伪类在末尾添加 。 还需要 设置为 来隐藏 Edge 中的工具提示()。

没有图表的情况

当我们没有 支持时,就会出现没有图表这种情况。 我们想要实现的效果如下图:

美化输出样式

在上面的基础上,我们给 设置绝对定位,获取滑块按钮的尺寸并将输出的文本居中显示:

.wrap:not(.full) {

position:relative; output {position:absolute;

top:;

width:$thumb-d;height:$thumb-d }}output {

display:flex; align-items:center; justify-content:center;}

如果您需要进一步了解 和 ,请参阅 @Patrick Brosset 的 《揭开 CSS 对齐的神秘面纱》或者查阅W3cplus上有关于Flexbox布局相关的资料。

结果可以在下面的 Codepen 中看到,我们依然增加了一个 以便清楚地看到 的边框:

这乍看起来像是那么回事儿,但是我们的 的文字不随着滑块按钮移动。

使输出文字移动

为了解决这个问题,我们首先要清楚滑块按钮是如何运动的。 在 Chrome 中,滑块按钮的 在 中的滑动轨道 的范围内移动,而在 Firefox 和 Edge 中,滑块按钮的 在实际 滑动条 的范围内移动。

虽然这种差异可能会在某些情况下出现问题,但我们的用例很简单, 并没有在滑动条或其组件上设置 , 或 ,所以滑动条本身、滑动轨道、滑动按钮的这三个属性 ( , 和 )是重合的。 此外,实际 的这三个属性的宽度与其轨道的三个属性的宽度重合。

这意味着当滑块值最小时(我们没有明确设置,因此它最小值默认是 0 ),滑动按钮框的左边缘与 input 的左边缘(和轨道的左边缘)重合 。

同样,当滑块值达到其最大值(未明确设置时,它最大值默认 )时,滑动按钮框的右边缘与 的右边缘(以及轨道的右边缘)重合。将按钮的左边缘放在滑块的右边缘之前,向左大概一个按钮宽度( )。

下图显示了输入框的宽度 ( )—— 显示为 。按钮宽 ( )设为 (因为我们已将它设置为 ),长度为输入框的宽度的 一个分数 。

滑块按钮处于最小值和最大值(实例)。

至此,我们得到左边按钮在 上的可滑动范围为 的 ( )减去按钮宽度( ),这就是它最小值到最大值的距离。

为了以相同的方式移动 ,我们用一个 过渡按钮位置来说明。 当滑动条值处于最小位置时, 的初始位置位于按钮的最左边,所以此时 是 。 为最大值时的位置,就是滑动条值达到最大值的位置,我们需要将它转换为 。

按钮的运动范围以及 (实例)的范围。

好吧,但是它们之间的值呢?

那么,记住每次更新滑动条值时,我们不仅要更新 输出的文本内容,还要将绑在容器上的 CSS自定义属性 进行更新。 这个 CSS自定义属性在 (当滑块值最小时为 )到 (当滑块值最大时为)之间。

所以如果我们通过 沿水平轴(轴)平移 ,它会随着滑动按钮移动,而不需要我们做任何事:

需要注意的是,如果点击轨道上的其他位置,上述方法会工作,但如果我们尝试拖动按钮 ,则不会有反应。 这是因为 现在位于 滑动按钮之上,滑动按钮捕捉不到点击事件。

我们通过在 设置 解决这个问题。

在上面的演示中,删除了 元素上的 辅助线,因为我们不再需要它了。

现在我们对不支持 浏览器有了很好的兜底方案,可以继续构建我们想要的结果了(有启用标志的 Chrome / Opera)。

有图表的情况

绘制所需布局

在开始编写代码之前,我们需要清楚地知道我们想要实现的目标。为了明确这点,我们做了一个尺寸等于轨道 ( )的布局草图,这也是 的宽度和容器 的边长(容器 不包含在内)。

这意味着我们容器的 是边长为的正方形(等于轨道 ), 是一个边长等于容器边长的长方形,且另一个边长是容器边长的分数 ,则滑动按钮是一个 的方块。

有图表情况下所需的布局 (实例)。

该图表是边长为 的正方形,容器中图表距滑动条有 间隙,与滑动条相对方向的容器边缘没有间隙。考虑到容器的边长是 ,图表的边长是 ,所以容器距图表上下边缘之间有间隙。

调整我们的元素

获得这种布局的第一步是使容器为正方形,并将 的尺寸设置为 :

$k: .1;

$track-w:25em;

$chart-d: (1-2*$k)*100%;

.wrap.full { width:$track-w; output { width:$chart-d; height:$chart-d }}

结果可以在下面看到,我们还添加了一些辅助线以更好地看到事物:

第一阶段的结果( 实例 ,只有支持原生浏览器可见)。

这是一个好的开始,因为 已经在我们想要的位置上了。

制作垂直滑块

WebKit 浏览器的“官方”方式是在 类 上设置 。 但是,这会破坏自定义样式,因为它们要求我们将 设置为 ,而我们不能给 同时设置两个不同的值。

所以我们只能使用简便的解决方案 。我们想要的是在容器的底部有最小值,在容器的顶部有最大值。 但实际上,在容器的左端是滑块是最小值,最右端是最大值的。

滑块的初始位置与我们预期达到的最终位置(实例)。

这看起来像是在右上角(以水平方向 和垂直 的中心点 )沿负方向旋转 (因为顺时针方向是正方向)

这是一个好的开始,但现在我们的滑块在容器边界之外。 为了让滑动条旋转到期望的位置,我们需要了解这个旋转做了什么。 它不仅旋转了 元素,而且还旋转了它自身的坐标系。 现在它的 轴向上, 轴向右。

因此,为了将其放入容器右侧内部,我们需要在旋转之后将其沿着其轴的负方向平移自身 的距离。 这意味着我们应用的最终 链是 。 (请记住, 函数中使用的 值与被翻转元素的尺寸有关。)

.wrap.full { input { transform-origin:100%; transform: rotate(-90deg) translatey(-100%)}}

通过上述操作,我们得到所需布局:

第二阶段的结果( 实例 ,只有支持原生 浏览器可见)。

设计图表的样式

当然,第一步是 使图表变圆,并调整 , 和 属性。

.wrap.full { output { border-radius:50%;color:#7a7a7a;font-size:4.25em; font-weight:700}}

您可能已经注意到我们已经将图表的尺寸设置为 而不是 。 这是因为 是 值,这意味着计算出相等的像素值取决于该元素的 属性 。

但是,我们希望能够通过增加 控制,而不必调整 值。 这是并不复杂,但与仅将尺寸设置为不依赖于 的 值相比,它仍然有点额外的工作。

第三阶段的结果( 实例 ,只有支持原生 浏览器可见)。

从饼图到环形图

在文字中间模拟一个洞的最简单方法是在 上添加另一个 层。 我们也可以通过添加混合模式来完成这个目标,这需要有背景图片,但没这个必要。要做一个实心的 ,一个简单的遮罩层就可以做到。

$p:39%;

background: radial-gradient($bg$p, transparent$p+ .5%), conic-gradient(#e64c65 calc(var(--val)*1%), #41a8ab 0%);

好的,按以上这么做图表就完成了!

第四阶段的结果( 实例 ,只有支持原生 浏览器可见)。

在按钮上显示数值

在容器上用一个绝对定位的 伪类来完成此操作。 设置这个伪类尺寸为按钮大小,并将它定位在容器的右下角,滑块值最小时按钮所在的位置。

.wrap.full {

position:relative; &::after{

position:absolute;

right:;bottom:;

width:$thumb-d;height:$thumb-d;

content:''; }}

我们也给它一个线框,以便我们可以看到它。

第五阶段的结果( 实例 ,只有支持原生 浏览器可见)

将它与按钮一起移动,与没有图表的情况下类似,只是这次移动是沿轴在负方向(而不是沿轴正方向)移动。

transform: translatey(calc(var(--val)/-100*#{$track-w -$thumb-d}))

为了能够拖动伪类下的按钮,我们还必须在这个伪元素上设置 。 结果可以在下图看到 —— 拖动按钮还会移动容器的 伪类。

第六阶段的结果( 实例 ,只有支持原生 浏览器可见)。

看起来不错,但我们真正想要的是使用这个伪类显示当前值。 如果将其 属性设置为 ,则不会执行任何操作,因为 是数字,而不是字符串。 如果我们将它设置为字符串,则它可成为 的值,但就不能再将它用于 。

幸运的是,我们可以通过使用CSS计数器的方法来解决这个问题:

counter-reset:val var(--val);content:counter(val)'%';

现在所有功能已完成,耶!

第七阶段的结果( 实例 ,只有支持原生 浏览器可见)。

接下来,让我们继续添加一些属性来优化它。 我们把文本放在滑动按钮的中间,字体颜色设为白色,去掉所有辅助线,并在 上设置 :

.wrap.full { &::after{ line-height:$thumb-d;

color:#fff;text-align:center }}[type='range'] {

cursor:pointer}

优化后的效果:

图表情况的最终效果( 实例 ,只有支持原生 浏览器可见)。

清除重复样式代码

代码中还有要优化的地方,在没有图表情况下的 样式和有图表的 伪类中存在一堆重复的样式。

在无图表情况下 的样式与有图表情况下的 样式

我们可以对此做些优化, 然后我们使用一个简短的扩展样式:

%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; }}

不错的焦点样式

比方说,我们不想在 时出现 ,但又希望在视觉上清楚地区分获焦这种状态。 那我们该如何做? 我们可以在 没有获焦时,缩小滑动按钮,降低色彩饱和度,并且隐藏输出的文字。

这听起来像个很酷……但是,由于我们没有父选择器,所以当滑动条获焦或失焦时,我们无法在改变滑动条父级的 属性。 额….

但可以做的是使用 的其他伪元素()来显示按钮上的值。 这并不难做到,稍后我们会讨论,它允许我们做如下操作:

[type='range']:focus+output:before{/* focus styles */}

采取这种方法的问题在于,我们如何放大 的字体 ,但又不改变容器 伪类的大小和粗细。

我们可以通过设置Sass变量来解决这个问题,将相对字体大小定义为变量 ,然后使该值在实际 上放大字体 ,在 伪类上将其恢复为之前的大小,如下 。

$fsr:4;.wrap {

color:$fg; &.full { output { font-size:$fsr*1em; &:before{ font-size:1em/$fsr; font-weight:200; } } }}

除此之外,我们只需要移动我们在 上的 CSS自定义属性到 上 。

容器伪元素上的样式与 伪元素上的样式。

好的,现在我们可以进入区分正常和聚焦效果的最后一步。

当滑块没有被聚焦时,我们首先隐藏丑陋的 默认 状态和按钮上的值:

%thumb-val{

opacity:;

}

[type='range']:focus{

outline:none;.wrap:not(.full)& + output, .wrap.full & + output:before { opacity:1}}

只有当滑动条获得焦点时,按钮上的值才可见( 实例 ,只有支持原生 浏览器可见)。

接下来,我们为滑块按钮的正常和聚焦状态设置不同的样式:

@mixinthumb() {

transform:scale(.7);

filter:saturate(.7)}

@mixinthumb-focus() {

transform:none;

filter:none}[type='range']:focus{ &::-webkit-slider-thumb{@includethumb-focus } &::-moz-range-thumb{@includethumb-focus } &::-ms-thumb{@includethumb-focus }}

只要滑块没有聚焦,按钮才缩小和去饱和( 实例 ,只有支持原生 浏览器可见)。

最后一步是添加这些状态之间的转换:

$t:.5s;@mixinthumb() {

transition:transform$tlinear, filter$t}%thumb-val {

transition:opacity$tease-in-out}

该例子显示正常状态和聚焦状态之间的转换( 实例 ,只有支持原生 浏览器可见)。

什么是屏幕读取?

由于屏幕读取最近生成的内容,因此在这种情况下,我们会将 值读取两次。 所以我们通过在 上设置 来解决这个问题,然后把我们想要读取的当前值放在 属性中:

letconic =false;functionupdate(){letnewval = +_R.value;if(val !== newval) { _W.style.setProperty('--val', _O.value = val = newval);if(conic) _O.setAttribute('aria-label', `$%`) }};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', `$%`)}

最后的演示可以在下面链接中找到。 请注意,如果您的浏览器没有原生 支持,你只会看到兜底样式。

最后的话

尽管浏览器对 的支持仍然很差,但情况将会有所改变。 目前只有暴露标志的 Blink 浏览器支持,但 Safari 将 列为正在开发中 ,所以事情已经越来越好了。

如果您希望跨浏览器支持早日成为现实,您可以通过在 Edge 中投票实现 或通过对 Firefox 错误发表评论来说明为什么您认为这很重要或是什么让您使用它。 这里是我发表的作品。

  • 发表于:
  • 原文链接:http://kuaibao.qq.com/s/20180417B1TO3V00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券