【CSS】762- 如何实现一个圆弧倒计时进度条
一、前言
最近的项目中,需要实现一个圆弧形倒计时进度条,对于本来 css 知识薄弱的我当场就懵逼,脑海里总是不断思考如何实现,不幸的是脑袋里没能蹦出半个想法。然后立马百度查看网上是否有相似的解决方案,百度下来初步知道如何来实现了,那我们就一步一步从 0 到有开始这段旅程。
首先展示一下最终的成果,最终效果图如下:
实现要点:浅色圆弧需要分成左右两边,左右两边都需要用一个同心原来实现,亮色圆弧也需要左右分开,各自用一个同心圆来实现。让我们开始吧!
二、实现步骤
添加容器
让整个容器是 position: fixed
方便可以在整个页面上随意放置
html 代码:
<div class="task-container">div>
css 代码:
.task-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
width: 65px;
height: 65px;
display: flex;
justify-content: center;
align-items: center;
}
画底盘
加点阴影,让它看起来有点立体的感觉 html 代码:
<div class="task-container">
<div class="task-cicle">div>
div>
css 代码:
.task-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
width: 65px;
height: 65px;
display: flex;
justify-content: center;
align-items: center;
.task-cicle {
display: flex;
justify-content: center;
align-items: center;
width: 53px;
height: 53px;
border-radius: 50%;
background: #FFFFFF;
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
}
}
效果:
重点来了,接下来实现圆弧
我们先画右圆弧,我们用右半边矩形来实现,右半圆只设置上方和右边的边框颜色 html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
div>
div>
div>
css 代码:
.task-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
width: 65px;
height: 65px;
display: flex;
justify-content: center;
align-items: center;
.task-cicle {
display: flex;
justify-content: center;
align-items: center;
width: 53px;
height: 53px;
border-radius: 50%;
background: #FFFFFF;
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
}
.task-inner {
position: relative;
width: 46px;
height: 46px;
}
.right-cicle {
width: 23px;
height: 46px;
position: absolute;
top: 0;
right: 0;
overflow: hidden;
}
.cicle-progress {
position: absolute;
top: 0;
width: 46px;
height: 46px;
border: 3px solid transparent;
box-sizing: border-box;
border-radius: 50%;
}
.cicle1-inner {
left: -23px;
border-right: 3px solid #e0e0e0;
border-top: 3px solid #e0e0e0;
transform: rotate(-15deg);
}
}
right-cicle 需要设置 overflow: hidden;对子元素超出的部分进行裁剪。cicle1-inner 中的旋转-15 度,其实可以根据设计稿来调整你需要展示的弧度 如果父节点,没有进行裁剪,右半圆就会延伸到左边
裁剪之后的效果
画左边的弧
接下来根据同样的原理画左边的弧。左边的圆,只设置上方和左边的边框颜色 html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle2-inner">div>
div>
div>
div>
div>
css 代码:
.left-cicle {
width: 23px;
height: 46px;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
.cicle2-inner {
left: 0;
border-left: 3px solid #e0e0e0;
border-top: 3px solid #e0e0e0;
transform: rotate(15deg);
}
效果如下:
ok,圆弧的基本轮廓已经完成,接下来实现亮色进度条,进度条也是分左右边各自实现
画右半边进度条
右半边圆只设置上方和右边的边框颜色 html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle2-inner">div>
div>
<div class="right-cicle">
<div class="cicle-progress cicle3-inner" id="rightCicle">div>
div>
div>
div>
div>
css 代码:
.cicle3-inner {
left: -23px;
border-right: 3px solid #feca02;
border-top: 3px solid #feca02;
transform: rotate(-135deg);
}
效果如下:为什么是旋转-135 度?进度条是从左边蔓延到右边的,让亮色进度条旋转到左右两边的临界点,也就是初始角度是-135 度,随着时间推移增加旋转角度,进度条就蔓延到右边了转到哪个角度为止呢?转到亮色边框和右边灰色边框重合,也就是-15 度,那么右边亮色进度条的旋转角度范围就是-135 度到-15 度,共 120 度的。右半边进度条已经完成,初始角度是-135 度,随着时间的推移,慢慢旋转到-15 度的位置
画左半边的进度条
左半圆只设置上方和左边的边框颜色 html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle2-inner">div>
div>
<div class="right-cicle">
<div class="cicle-progress cicle3-inner" id="rightCicle">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle4-inner" id="leftCicle">div>
div>
div>
div>
div>
css 代码:
.cicle4-inner {
left: 0;
border-left: 3px solid #feca02;
border-top: 3px solid #feca02;
transform: rotate(195deg);
}
效果如下(为了演示,父节点为设置了 overflow: inherit;不裁剪,能更清楚来龙去脉):
为什么要旋转 195 度?进度条是从左边开始由无到有的,我们让亮色进度条旋转到左边灰色圆弧起始点的临界点位置,随着时间的推移增加旋转角度。左边进度条要转 120 度,所以左边进度条旋转角度范围:195 到 315 度 我们把父节点的 overflow 设置回原来的 hidden,对子节点超出的部分进行裁剪。
what?裁剪之后还露出了一个小尾巴,如何把这个小尾巴给掩盖掉?这时候我们需要在左边再画一个同心圆来遮盖掉它
画遮盖圆
注意:遮罩圆边框宽度要比左边亮色进度条圆的边框宽度要大,不然会遮盖不完全,会出现金色余晖,且要和亮色进度条是同心圆 html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle2-inner">div>
div>
<div class="right-cicle">
<div class="cicle-progress cicle3-inner" id="rightCicle">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle4-inner" id="leftCicle">div>
div>
<div class="left-cicle">
<div class="mask-inner">div>
div>
div>
div>
div>
css 代码(为了展示遮罩圆是完全覆盖的,我把父节点的 overflow: inherit;不裁剪,圆的边框颜色设置为蓝色):
.mask-inner {
position: absolute;
left: 0;
top: 0;
width: 39px;
height: 39px;
// border: 4px solid transparent;
border: 4px solid blue;
border-radius: 50%;
// border-left: 4px solid #FFFFFF;
// border-top: 4px solid #FFFFFF;
// transform: rotate(195deg);
}
看,我们的遮罩圆已经完全遮罩了其他圆,遮盖圆和左边进度条圆一样,都是旋转 195 度,只设置上方和左边的边框颜色,边框颜色是和底盘颜色一样,我们把父节点 overflow 设置为 hidden 裁剪 css 代码:
.mask-inner {
position: absolute;
left: 0;
top: 0;
width: 39px;
height: 39px;
border: 4px solid transparent;
border-radius: 50%;
border-left: 4px solid blue;
border-top: 4px solid blue;
transform: rotate(197deg);
}
蓝色部分就是我们的小尾巴的位置,我们用白色替换蓝色边框
.mask-inner {
position: absolute;
left: 0;
top: 0;
width: 39px;
height: 39px;
border: 4px solid transparent;
border-radius: 50%;
border-left: 4px solid #FFFFFF;
border-top: 4px solid #FFFFFFl
transform: rotate(197deg);
}
效果:
哇,看看,小尾巴已经不见了。如果遮盖圆和左边亮色进度条设置一样的边框大小,会出现金色边
好吧,样式方面已经基本完成,其他点缀的样式就不在这里列出了,可以看看下面的源码。要让进度条动起来,需要通过 js 来操作,js 里的源码我已经写了比较清楚的注释,方便理解。html 代码:
<div class="task-container">
<div class="task-cicle">
<div class="task-inner">
<div class="right-cicle">
<div class="cicle-progress cicle1-inner">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle2-inner">div>
div>
<div class="right-cicle">
<div class="cicle-progress cicle3-inner" id="rightCicle">div>
div>
<div class="left-cicle">
<div class="cicle-progress cicle4-inner" id="leftCicle">div>
div>
<div class="left-cicle">
<div class="mask-inner">div>
div>
<div class="inner">
<img src="https://img12.360buyimg.com/img/jfs/t1/150018/30/1001/2042/5eec2f8eEfd3c853a/e7982308423ce71a.png" alt="" srcset="">
<div class="water-count">10div>
div>
div>
<div class="task-bottom">
<div class="task-btn" id="time">div>
div>
div>
div>
<script>
const rightCicle = document.getElementById('rightCicle');
const leftCicle = document.getElementById('leftCicle');
const timeDom = document.getElementById('time');
let isStop = false;
let timer;
const totalTime = 10; // 总时间
const halfTime = totalTime / 2; // 总时间的一半
const initRightDeg = -135; // 右半边进度条初始角度
const initLeftDeg = 195; // 左半边进度条初始角度
const halfCicle = 120; // 左右连边各要转的总角度
const perDeg = 120 / halfTime; // 每秒转的角度
let inittime = 10;
let begTime; // 倒计时开始时间戳
let stopTime; // 倒计时停止时间戳
function run() {
const time = inittime;
let animation;
if (time > halfTime) {
// 左半边还没转完
// 左半边:动画的初始角度=左半边进度条初始角度+已经转的角度,最终角度=初始角度+120 度,动画持续时间=左半边还剩需要转的时间
// 右半边:动画的初始角度=右半边进度条初始角度,最终角度=初始角度+120 度,动画持续时间=一半的时间,动画延迟=左半边还剩需要转的时间
animation = `
@keyframes task-left {
0% {
transform: rotate(${initLeftDeg + (totalTime - time) * perDeg}deg);
}
100% {
transform: rotate(${initLeftDeg + halfCicle}deg);
}
}
.task-left {
animation-name: task-left;
animation-duration: ${time - halfTime}s;
animation-timing-function: linear;
animation-delay: 0s;
animation-fill-mode: forwards;
animation-direction: normal;
animation-iteration-count: 1;
}
@keyframes task-right {
0% {
transform: rotate(${initRightDeg}deg);
}
100% {
transform: rotate(${initRightDeg + halfCicle}deg);
}
}
.task-right {
animation-name: task-right;
animation-duration: ${halfTime}s;
animation-timing-function: linear;
animation-delay: ${time - halfTime}s;
animation-fill-mode: forwards;
animation-direction: normal;
animation-iteration-count: 1;
}
`;
} else {
// 左半边已经转完
// 左半边动画:起始帧和重点帧都=左半边进度条初始角度+120 度
// 右半边动画:动画的初始角度=右半边进度条初始角度+右半边已经角度,最终角度=初始角度+120 度,动画持续时间=剩余时间
animation = `
@keyframes task-left {
0% {
transform: rotate(${initLeftDeg + halfCicle}deg);
}
100% {
transform: rotate(${initLeftDeg + halfCicle}deg);
}
}
.task-left {
animation-name: task-left;
animation-duration: 0s;
animation-timing-function: linear;
animation-delay: 0s;
animation-fill-mode: forwards;
animation-direction: normal;
animation-iteration-count: 1;
}
@keyframes task-right {
0% {
transform: rotate(${initRightDeg + (halfTime - time) * perDeg}deg);
}
100% {
transform: rotate(${initRightDeg + halfCicle}deg);
}
}
.task-right {
animation-name: task-right;
animation-duration: ${time}s;
animation-timing-function: linear;
animation-delay: 0s;
animation-fill-mode: forwards;
animation-direction: normal;
animation-iteration-count: 1;
}
`;
}
// 增加动画暂停和开始类
animation += `.stop {animation-play-state: paused;} .run {animation-play-state: running;}`
const styleDom = document.createElement('style');
styleDom.type = 'text/css';
styleDom.innerHTML = animation;
document.getElementsByTagName('head').item(0).appendChild(styleDom);
leftCicle.classList.add('task-left');
rightCicle.classList.add('task-right');
begTime = Date.now();
countDown();
}
function countDown() {
if (begTime && stopTime) {
// 从 1 秒到 1.6 秒后暂停,动画一直在走,而倒计时因为未到 2 秒,定时器就清除了,下次还是会从 1 开始计时,
// 这就会导致倒计时和动画的不同步,之类稍微校正一下,如果结束时间和开始时间取余数大于 500,就把倒计时-1 秒
const runtime = stopTime - begTime;
console.log(runtime % 1000);
if (runtime % 1000 > 500) {
inittime -= 1;
}
}
begTime = Date.now();
timeDom.innerText = `${inittime}秒后获得 `;
timer = setInterval(() => {
inittime -= 1;
timeDom.innerText = `${inittime}秒后获得 `;
if (inittime <= 0) {
clearInterval(timer);
}
}, 1000);
}
// 点击可暂停倒计时和动画
timeDom.addEventListener('click', () => {
if (isStop) {
isStop = false;
countDown();
leftCicle.classList.remove('stop');
leftCicle.classList.add('run');
rightCicle.classList.remove('stop');
rightCicle.classList.add('run');
} else {
stopTime = Date.now();
isStop = true;
clearInterval(timer);
leftCicle.classList.remove('run');
leftCicle.classList.add('stop');
rightCicle.classList.remove('run');
rightCicle.classList.add('stop');
}
}, false);
run();
script>
css 代码:
.task-container {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
width: 65px;
height: 65px;
display: flex;
justify-content: center;
align-items: center;
.task-cicle {
display: flex;
justify-content: center;
align-items: center;
width: 53px;
height: 53px;
border-radius: 50%;
background: #FFFFFF;
box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.05);
}
.task-inner {
position: relative;
width: 46px;
height: 46px;
}
.right-cicle {
width: 23px;
height: 46px;
position: absolute;
top: 0;
right: 0;
overflow: hidden;
}
.cicle-progress {
position: absolute;
top: 0;
width: 46px;
height: 46px;
border: 3px solid transparent;
box-sizing: border-box;
border-radius: 50%;
}
.cicle1-inner {
left: -23px;
border-right: 3px solid #e0e0e0;
border-top: 3px solid #e0e0e0;
transform: rotate(-15deg);
}
.left-cicle {
width: 23px;
height: 46px;
position: absolute;
top: 0;
left: 0;
overflow: hidden;
}
.cicle2-inner {
left: 0;
border-left: 3px solid #e0e0e0;
border-top: 3px solid #e0e0e0;
transform: rotate(15deg);
}
.cicle3-inner {
left: -23px;
border-right: 3px solid #feca02;
border-top: 3px solid #feca02;
transform: rotate(-135deg);
}
.cicle4-inner {
left: 0;
border-left: 3px solid #feca02;
border-top: 3px solid #feca02;
transform: rotate(195deg);
}
.mask-inner {
position: absolute;
left: 0;
top: 0;
width: 39px;
height: 39px;
border: 4px solid transparent;
border-radius: 50%;
border-left: 4px solid #FFFFFF;
border-top: 4px solid #FFFFFF;
transform: rotate(195deg);
}
.inner {
position: absolute;
left: 0;
top: -2px;
right: 0;
bottom: 0;
width: 22px;
height: 26px;
margin: auto;
img {
width: 100%;
height: 100%;
}
}
.water-count {
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
font-family: "JDZhengHei-01-Regular";
font-size: 12px;
color: #FFFFFF;
}
.task-bottom {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 60px;
height: 15px;
left: 50%;
transform: translateX(-50%);
bottom: 2px;
}
.task-btn {
display: flex;
justify-content: center;
align-items: center;
height: 15px;
border-radius: 7px;
background-image: linear-gradient(-45deg, #FEB402 0%, #FF8407 100%);
font-size: 8px;
color: #FFFFFF;
line-height: 15px;
padding: 0 4px;
}
}
三、总结
浅色圆弧和亮色进度条的实现比较绕,一眼看过去不太好理解,我们可以把每一步拆分开。4 个圆弧的实现,父节点都进行了裁剪,裁剪之后很难看出子元素原本的样子,我们可以先把裁剪去掉,看看未裁剪时,各个圆的表现。