一.場景
需要實現一個漸變色的圓環進度條,進度條長這樣子:
[caption id="attachment_1238" align="alignnone" width="200"]
circle[/caption]
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 也能實現,原理一樣
三.嘗試經典解法
「兩扇門」原理好像通用,照搬到漸變進度條上,那麼需要把進度條圖片水平翻轉,再分為左右兩半,如圖:
[caption id="attachment_1239" align="alignnone" width="404"]
circle-left-right[/caption]
具體如下:
<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.
[caption id="attachment_1240" align="alignnone" width="307"]
stroke-dashoffset[/caption]
詳細見 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 實現漸變邊框不可行(因為漸變相接處有很明顯縫隙)
暫無評論,快來發表你的看法吧