【CSS】762- 如何实现一个圆弧倒计时进度条

前端自习课

共 14391字,需浏览 29分钟

 ·

2020-11-01 17:34

一、前言

最近的项目中,需要实现一个圆弧形倒计时进度条,对于本来 css 知识薄弱的我当场就懵逼,脑海里总是不断思考如何实现,不幸的是脑袋里没能蹦出半个想法。然后立马百度查看网上是否有相似的解决方案,百度下来初步知道如何来实现了,那我们就一步一步从 0 到有开始这段旅程。

首先展示一下最终的成果,最终效果图如下:

实现要点:浅色圆弧需要分成左右两边,左右两边都需要用一个同心原来实现,亮色圆弧也需要左右分开,各自用一个同心圆来实现。让我们开始吧!

二、实现步骤

添加容器

让整个容器是 position: fixed 方便可以在整个页面上随意放置 html 代码:

<div class="task-container">div>

css 代码:

.task-container {
    position: fixed;
    left0;
    right0;
    top0;
    bottom0;
    margin: auto;
    width65px;
    height65px;
    display: flex;
    justify-content: center;
    align-items: center;
}

画底盘

加点阴影,让它看起来有点立体的感觉 html 代码:

<div class="task-container">
    <div class="task-cicle">div>
div>

css 代码:

.task-container {
    position: fixed;
    left0;
    right0;
    top0;
    bottom0;
    margin: auto;
    width65px;
    height65px;
    display: flex;
    justify-content: center;
    align-items: center;
 
    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width53px;
        height53px;
        border-radius50%;
        background#FFFFFF;
        box-shadow0px 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;
    left0;
    right0;
    top0;
    bottom0;
    margin: auto;
    width65px;
    height65px;
    display: flex;
    justify-content: center;
    align-items: center;


    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width53px;
        height53px;
        border-radius50%;
        background#FFFFFF;
        box-shadow0px 0px 12px 0px rgba(0, 0, 0, 0.05);
    }

    .task-inner {
        position: relative;
        width46px;
        height46px;
    }

    .right-cicle {
        width23px;
        height46px;
        position: absolute;
        top0;
        right0;
        overflow: hidden;
    }

    .cicle-progress {
        position: absolute;
        top0;
        width46px;
        height46px;
        border3px solid transparent;
        box-sizing: border-box;
        border-radius50%;
    }

    .cicle1-inner {
        left: -23px;
        border-right3px solid #e0e0e0;
        border-top3px solid #e0e0e0;
        transformrotate(-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 {
    width23px;
    height46px;
    position: absolute;
    top0;
    left0;
    overflow: hidden;
}

.cicle2-inner {
    left0;
    border-left3px solid #e0e0e0;
    border-top3px solid #e0e0e0;
    transformrotate(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-right3px solid #feca02;
    border-top3px solid #feca02;
    transformrotate(-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 {
    left0;
    border-left3px solid #feca02;
    border-top3px solid #feca02;
    transformrotate(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;
    left0;
    top0;
    width39px;
    height39px;
    // border4px solid transparent;
    border4px solid blue;
    border-radius50%;
    // border-left4px solid #FFFFFF;
    // border-top4px solid #FFFFFF;
    // transformrotate(195deg);
}

看,我们的遮罩圆已经完全遮罩了其他圆,遮盖圆和左边进度条圆一样,都是旋转 195 度,只设置上方和左边的边框颜色,边框颜色是和底盘颜色一样,我们把父节点 overflow 设置为 hidden 裁剪 css 代码:

.mask-inner {
    position: absolute;
    left0;
    top0;
    width39px;
    height39px;
    border4px solid transparent;
    border-radius50%;
    border-left4px solid blue;
    border-top4px solid blue;
    transformrotate(197deg);
}

蓝色部分就是我们的小尾巴的位置,我们用白色替换蓝色边框

.mask-inner {
    position: absolute;
    left0;
    top0;
    width39px;
    height39px;
    border4px solid transparent;
    border-radius50%;
    border-left4px solid #FFFFFF;
    border-top4px 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;
    left0;
    right0;
    top0;
    bottom0;
    margin: auto;
    width65px;
    height65px;
    display: flex;
    justify-content: center;
    align-items: center;


    .task-cicle {
        display: flex;
        justify-content: center;
        align-items: center;
        width53px;
        height53px;
        border-radius50%;
        background#FFFFFF;
        box-shadow0px 0px 12px 0px rgba(0, 0, 0, 0.05);
    }

    .task-inner {
        position: relative;
        width46px;
        height46px;
    }

    .right-cicle {
        width23px;
        height46px;
        position: absolute;
        top0;
        right0;
        overflow: hidden;
    }

    .cicle-progress {
        position: absolute;
        top0;
        width46px;
        height46px;
        border3px solid transparent;
        box-sizing: border-box;
        border-radius50%;
    }

    .cicle1-inner {
        left: -23px;
        border-right3px solid #e0e0e0;
        border-top3px solid #e0e0e0;
        transformrotate(-15deg);
    }

    .left-cicle {
        width23px;
        height46px;
        position: absolute;
        top0;
        left0;
        overflow: hidden;
    }

    .cicle2-inner {
        left0;
        border-left3px solid #e0e0e0;
        border-top3px solid #e0e0e0;
        transformrotate(15deg);
    }

    .cicle3-inner {
        left: -23px;
        border-right3px solid #feca02;
        border-top3px solid #feca02;
        transformrotate(-135deg);
    }

    .cicle4-inner {
        left0;
        border-left3px solid #feca02;
        border-top3px solid #feca02;
        transformrotate(195deg);
    }

    .mask-inner {
        position: absolute;
        left0;
        top0;
        width39px;
        height39px;
        border4px solid transparent;
        border-radius50%;
        border-left4px solid #FFFFFF;
        border-top4px solid #FFFFFF;
        transformrotate(195deg);
    }

    .inner {
        position: absolute;
        left0;
        top: -2px;
        right0;
        bottom0;
        width22px;
        height26px;
        margin: auto;

        img {
            width100%;
            height100%;
        }
    }

    .water-count {
        position: absolute;
        top8px;
        left50%;
        transformtranslateX(-50%);
        font-family"JDZhengHei-01-Regular";
        font-size12px;
        color#FFFFFF;
    }

    .task-bottom {
        display: flex;
        justify-content: center;
        align-items: center;
        position: absolute;
        width60px;
        height15px;
        left50%;
        transformtranslateX(-50%);
        bottom2px;
    }

    .task-btn {
        display: flex;
        justify-content: center;
        align-items: center;
        height15px;
        border-radius7px;
        background-imagelinear-gradient(-45deg, #FEB402 0%, #FF8407 100%);
        font-size8px;
        color#FFFFFF;
        line-height15px;
        padding0 4px;
    }
}

三、总结

浅色圆弧和亮色进度条的实现比较绕,一眼看过去不太好理解,我们可以把每一步拆分开。4 个圆弧的实现,父节点都进行了裁剪,裁剪之后很难看出子元素原本的样子,我们可以先把裁剪去掉,看看未裁剪时,各个圆的表现。


分享时间


这里有我的 5 条 Draw.io 画图经验分享:
①需要知道自己要画什么:
②页面不宜使用太多颜色;
③为图片添加背景框;
④为图片添加作者;
⑤画图一定要有耐心。


浏览 63
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报