一.场景
需要实现一个渐变色的圆环进度条,进度条长这样子:
0进度时显示一个灰色的槽,加载过程中彩色条顺时针走完一圈
二.经典解法“两扇门”
圆环进度条有一种经典解法,原理很有意思,HTML结构如下:
<div class="circle progress-basic">
<div class="left">
<div class="circle left-circle"></div>
</div>
<div class="right">
<div class=" circle right-circle"></div>
</div>
</div>
先不考虑圆,想象一个双开门,progress-basic
是门框,left
是左半扇门,right
是右半扇门,如下:
.progress-basic {
position: relative;
}
.left, .right {
position: absolute;
width: 100px; height: 200px;
top: 0;
overflow: hidden;
}
.left {
left: 0;
}
.right {
right: 0;
}
然后考虑如何实现0-180deg
的效果:进度条需要从0点位置一点点出现,填满右边半圆环,180deg
的时候左边半圆环灰槽,右边半圆环进度条
想象正方形纸上左边画了一个半圆环,用左手遮住左边,然后顺时针旋转纸,半圆环就会从左手指尖一点点出现,转够180度时,正好左边是空白,右边是半圆环。此时不能再转了,会露馅(半圆环进度条刚好走完,再转就断了)
180-360deg
的方法与之类似,先把半圆环进度条藏好,再一点一点转出来。完整CSS如下:
.circle {
width: 200px; height: 200px;
box-sizing: border-box;
border-radius: 50%;
border: 10px solid silver;
}
.progress-basic {
position: relative;
}
.left, .right {
position: absolute;
width: 100px; height: 200px;
/*塞进父级border*/
top: -10px;
overflow: hidden;
}
.left {
/*塞进父级border*/
left: -10px;
}
.right {
/*塞进父级border*/
right: -10px;
}
.left-circle, .right-circle {
border-color: #077df8;
}
.left-circle {
margin-right: -100px;
border-left-color: transparent;
border-top-color: transparent;
transform: rotateZ(-45deg);
}
.right-circle {
margin-left: -100px;
border-right-color: transparent;
border-bottom-color: transparent;
transform: rotateZ(-45deg);
}
门框自带灰色槽,两扇门overflow: hidden
,左半扇门藏好右半圆环进度条,右半扇门藏好左半圆环进度条。先转右边的纸,纸上左边的半圆环一点点出现,到180度后去转左边的纸,纸上右边的半圆环一点点出现,360度时刚好拼接出完整圆环。如下:
var $ = document.querySelector.bind(document);
void function() {
var $rightCircle = $('.right-circle');
var $leftCircle = $('.left-circle');
var prog = 0;
var deg = 0;
var initialDeg = -45;
var halfDone;
var timer = setInterval(function() {
prog += 0.01;
deg = 360 * prog;
if (deg < 180) {
$rightCircle.style.transform = 'rotateZ(' + (initialDeg + deg) + 'deg)';
}
else if (deg < 360) {
if (!halfDone) {
console.log('half done');
$rightCircle.style.transform = 'rotateZ(' + (initialDeg + 180) + 'deg)';
halfDone = true;
}
$leftCircle.style.transform = 'rotateZ(' + (initialDeg + deg - 180) + 'deg)';
}
else {
console.log('done');
$leftCircle.style.transform = 'rotateZ(' + (initialDeg + 180) + 'deg)';
clearInterval(timer);
}
}, 100);
}();
很巧妙的方法,不知道有没有学名,暂且称之为“两扇门”
P.S.通过clip
也能实现,原理一样
三.尝试经典解法
“两扇门”原理好像通用,照搬到渐变进度条上,那么需要把进度条图片水平翻转,再分为左右两半,如图:
具体如下:
<div class="circle circle-image progress-basic">
<div class="left">
<div class="circle left-circle"></div>
</div>
<div class="right">
<div class=" circle right-circle"></div>
</div>
</div>
/*用渐变图片左右拼*/
.circle-image .left-circle {
margin-right: -100px;
border: none;
background: url(circle-right.png) no-repeat 100px 0;
-webkit-background-size: 100px 200px;
background-size: 100px 200px;
transform: rotateZ(0deg);
}
.circle-image .right-circle {
margin-left: -100px;
border: none;
background: url(circle-left.png) no-repeat 0 0;
-webkit-background-size: 100px 200px;
background-size: 100px 200px;
transform: rotateZ(0deg);
}
存在一个致命问题:180度时候另一半接不上,纯色场景下左右两半圆环颜色一样,看不到180-360deg
时的差异,不存在接不上的问题,但渐变圆环很明显能看到180度后左边圆环是整个转着出现的,而不是一点点出现。尝试失败,考虑其它方式
四.白块遮罩
另一种不同的思路是把进度条圆环先铺在下面,左右都用白块遮住,然后顺时针旋转右边白块,底下的右半圆环一点点出现,180度换左边白块转动,底下的左半圆环一点点出现
这样就不存在左右接不上的问题,因为圆环本来就是完整的。解决了条的问题,还要考虑槽,可以把槽画在左右白块上,旋转白块槽跟着转,进度条一点点出现,完美。具体如下:
<div class="circle circle-block progress-basic">
<div class="left">
<div class="block left-block">
<div class="circle"></div>
</div>
</div>
<div class="right">
<div class="block right-block">
<div class="circle"></div>
</div>
</div>
<div class="circleBar"></div>
</div>
/*用白块遮住*/
.circle-block .block {
width: 100%; height: 100%;
background: #fff;
}
.circle-block .right-block {
-webkit-transform-origin: 0 50%;
/*盖住露出的边缘*/
border-right: 1px solid #fff;
}
.right-block .circle {
margin-left: -100px;
border-left-color: transparent;
border-top-color: transparent;
transform: rotateZ(-45deg);
}
.circle-block .left-block {
-webkit-transform-origin: 100% 50%;
/*盖住露出的边缘*/
border-left: 1px solid #fff;
margin-left: -1px;
}
.left-block .circle {
border-right-color: transparent;
border-bottom-color: transparent;
transform: rotateZ(-45deg);
}
.circle-block .circleBar {
width: 200px; height: 200px;
margin-top: -10px; margin-left: -10px;
background: url(circle.png) no-repeat 0 0;
-webkit-background-size: 200px 200px;
background-size: 200px 200px;
}
在高分辨率设备上,白块遮得不够完美,因此需要:
/*盖住露出的边缘*/
border-right: 1px solid #fff;
效果还不错,但存在很明显的限制:只适用于纯色背景,因为白块的背景色要和页面背景色一致,否则就露馅了
五.canvas描边
条件允许的话,可以用canvas画一个,所有问题迎刃而解:
<canvas class="canvas">不支持canvas</canvas>
void function() {
var options = {
size: 200,
lineWidth: 10,
rotate: 0
};
var canvas = $('.canvas');
var ctx = canvas.getContext('2d');
canvas.width = canvas.height = options.size;
// 重置原点到中心点
ctx.translate(options.size / 2, options.size / 2);
// 旋转-90度,让x轴与-y方向重合
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI);
var radius = (options.size - options.lineWidth) / 2;
var drawCircle = function(color, lineWidth, percent) {
percent = Math.min(Math.max(0, percent || 1), 1);
ctx.beginPath();
ctx.arc(0, 0, radius, 0, Math.PI * 2 * percent, false);
ctx.strokeStyle = color;
// butt, round or square
// 线条两端的形状
// butt 默认,两端不加装饰,截断感
// round 给线条两端包上半圆
// square 给线条两端包上半方,看起来和butt一样,但比butt长一个lineWidth
ctx.lineCap = 'butt';
ctx.lineWidth = lineWidth;
ctx.stroke();
};
// 灰槽
drawCircle('#efefef', options.lineWidth, 100 / 100);
var prog = 0;
// 色条渐变
var gradient = ctx.createLinearGradient(0, 100, 0, -100);
gradient.addColorStop(0, '#ff0000');
gradient.addColorStop(0.15, '#ff00ff');
gradient.addColorStop(0.33, '#0000ff');
gradient.addColorStop(0.49, '#00ffff');
gradient.addColorStop(0.67, '#00ff00');
gradient.addColorStop(0.84, '#ffff00');
gradient.addColorStop(1, '#ff0000');
// '#077df8'
var timer = setInterval(function() {
prog += 1;
// 色条
drawCircle(gradient, options.lineWidth, prog / 100);
if (prog >= 360) {
clearInterval(timer);
}
}, 100);
}();
注意渐变的定义:
var gradient = ctx.createLinearGradient(0, 100, 0, -100);
这样定义的其实是从右向左的线性渐变,因为坐标系经过了两次变换:
// 重置原点到中心点
// 原点平移到(100, 100)
ctx.translate(options.size / 2, options.size / 2);
// 旋转-90度,让x轴与-y方向重合
// x轴y轴转置
ctx.rotate((-1 / 2 + options.rotate / 180) * Math.PI);
很容易让进度条变成圆头的:
ctx.lineCap = 'round';
// 线条两端的形状
// butt 默认,两端不加装饰,截断感
// round 给线条两端包上半圆
// square 给线条两端包上半方,看起来和butt一样,但比butt长一个lineWidth
但canvas描边在高分辨率设备上存在锯齿的问题,需要一点小技巧:
var width = canvas.width, height=canvas.height;
if (window.devicePixelRatio) {
canvas.style.width = width + "px";
canvas.style.height = height + "px";
canvas.height = height * window.devicePixelRatio;
canvas.width = width * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
几倍屏就用几倍图,这样可以解决锯齿问题
六.svg描边动画
同样,如果条件允许,还可以用svg描边动画来实现,如下:
<div class="progress-svg">
<!-- 条 -->
<svg viewBox="-10 -10 220 220">
<g fill="none" stroke-width="10" transform="translate(100,100)">
<path d="M 0,-100 A 100,100 0 0,1 86.6,-50" stroke="url(#cl1)"/>
<path d="M 86.6,-50 A 100,100 0 0,1 86.6,50" stroke="url(#cl2)"/>
<path d="M 86.6,50 A 100,100 0 0,1 0,100" stroke="url(#cl3)"/>
<path d="M 0,100 A 100,100 0 0,1 -86.6,50" stroke="url(#cl4)"/>
<path d="M -86.6,50 A 100,100 0 0,1 -86.6,-50" stroke="url(#cl5)"/>
<path d="M -86.6,-50 A 100,100 0 0,1 0,-100" stroke="url(#cl6)"/>
</g>
</svg>
<!-- 槽 -->
<svg viewBox="-10 -10 220 220">
<path d="M200,100 C200,44.771525 155.228475,0 100,0 C44.771525,0 0,44.771525 0,100 C0,155.228475 44.771525,200 100,200 C155.228475,200 200,155.228475 200,100 Z" stroke-dashoffset="629"></path>
</svg>
</div>
<!-- Defining Angle Gradient Colors -->
<svg width="0" height="0">
<defs>
<linearGradient id="cl1" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="1" y2="1">
<stop stop-color="#618099"/>
<stop offset="100%" stop-color="#8e6677"/>
</linearGradient>
<linearGradient id="cl2" gradientUnits="objectBoundingBox" x1="0" y1="0" x2="0" y2="1">
<stop stop-color="#8e6677"/>
<stop offset="100%" stop-color="#9b5e67"/>
</linearGradient>
<linearGradient id="cl3" gradientUnits="objectBoundingBox" x1="1" y1="0" x2="0" y2="1">
<stop stop-color="#9b5e67"/>
<stop offset="100%" stop-color="#9c787a"/>
</linearGradient>
<linearGradient id="cl4" gradientUnits="objectBoundingBox" x1="1" y1="1" x2="0" y2="0">
<stop stop-color="#9c787a"/>
<stop offset="100%" stop-color="#817a94"/>
</linearGradient>
<linearGradient id="cl5" gradientUnits="objectBoundingBox" x1="0" y1="1" x2="0" y2="0">
<stop stop-color="#817a94"/>
<stop offset="100%" stop-color="#498a98"/>
</linearGradient>
<linearGradient id="cl6" gradientUnits="objectBoundingBox" x1="0" y1="1" x2="1" y2="0">
<stop stop-color="#498a98"/>
<stop offset="100%" stop-color="#618099"/>
</linearGradient>
</defs>
</svg>
对应的CSS如下:
.progress-svg {
display: inline-block;
position: relative;
text-align: center;
}
.progress-svg svg {
width: 200px; height: 200px;
}
.progress-svg svg:nth-child(2) {
position: absolute;
left: 0;
top: 0;
-webkit-transform: rotate(-90deg);
transform: rotate(-90deg);
}
.progress-svg svg:nth-child(2) path {
fill: none;
stroke-width: 25;
stroke-dasharray: 629;
stroke: #fff;
opacity: .9;
-webkit-animation: load 10s;
animation: load 10s;
}
@keyframes load {
0% {
stroke-dashoffset: 0;
}
}
svg描边动画与path的两个东西有关:
stroke-dasharray:虚线每小段长度
stroke-dashoffset:虚线初始位置偏移量(向左偏移)
向左偏移很关键,否则想不通为什么要递减到0,而不是从0递增:
A dashed stroke with a non-zero dash offset. The dashing pattern is 20,10 and the dash offset is 15. The red line shows the actual path that is stroked.
详细见SVG规范
原理是让stroke-dasharray
长度大于path
,并让stroke-dashoffset
等于stroke-dasharray
,此时没有描边(向左偏移把虚线的第一段实线弄没了),然后让偏移量递减到0,视觉效果就是虚线的第一段实线从左边一点点露出来,为0时恰好覆盖path
反过来想,如果让stroke-dashoffset
为0,stroke-dasharray
从极小值递增至等于path
,虽然虚线第一段确实是一点点变长的,但后续小段没办法隐藏掉,所以不能用来实现描边动画
但是,svg渐变描边存在拼接的问题,仔细看能发现圆环上有4处断开,就是因为两段渐变接不上,虽然定义的渐变色值没有问题,理论上能完美接起来,但实际效果不理想
同样用CSS做渐变边框也存在左右拼接的问题,如下:
<div class="colorfulBorder"></div>
.colorfulBorder {
width:100px; height:100px; -webkit-transform:rotate(90deg);
}
.colorfulBorder:before {
content:"";
display:block;
width:100px; height:50px;
margin-top:10px; padding:10px; padding-bottom:0; box-sizing:border-box;
border-top-left-radius:50px;
border-top-right-radius:50px;
background:-webkit-gradient(
linear,
left top,
right top,
color-stop(0,#fff),
color-stop(1,#fff)
),-webkit-gradient(
linear,
left top,
right top,
color-stop(0,#077df8),
color-stop(1,#74baff)
);
background-clip:content-box,padding-box;
}
.colorfulBorder:after {
content:"";
display:block;
width:100px; height:50px;
padding:10px; padding-top:0;
box-sizing:border-box;
border-bottom-left-radius:50px;
border-bottom-right-radius:50px;
background:-webkit-gradient(
linear,
left top,
right top,
color-stop(0,#fff),
color-stop(1,#fff)
),-webkit-gradient(
linear,
left top,
right top,
color-stop(0,#fff),
color-stop(1,#74baff)
);
background-clip:content-box,padding-box;
}
这种瑕疵导致整个方案不可行
七.在线Demo
Demo地址:http://www.ayqy.net/temp/progress/circle.html
八.总结
对于纯色圆环进度条,建议采用经典“两扇门”解法,能够适应非纯色背景
如果是渐变圆环进度条,方案选择如下:
条件允许,优选canvas,能够适应非纯色背景
不行就用白块遮罩,但只适用于纯色背景
svg描边动画可以实现自定义形状进度条,比如一条弯弯的小河,但不适用于渐变描边的情况(渐变相接处有缝隙)
CSS通过多background
实现渐变边框不可行(因为渐变相接处有很明显缝隙)