优秀的编程知识分享平台

网站首页 > 技术文章 正文

如何实现一个圆弧倒计时进度条(圆弧上的运动求时间)

nanyue 2024-08-02 17:51:29 技术文章 5 ℃

作者:郭世平

转发链接:https://mp.weixin.qq.com/s/f2ZhTN-5R6GOxrJDBrjcVQ

一、前言

最近的项目中,需要实现一个圆弧形倒计时进度条,对于本来 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">10</div>
            </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 个圆弧的实现,父节点都进行了裁剪,裁剪之后很难看出子元素原本的样子,我们可以先把裁剪去掉,看看未裁剪时,各个圆的表现。

推荐CSS学习相关文章

这33个超级好用的CSS选择器,你可能见都没见过

如何用一行 CSS 实现 10 种现代布局?

rem适配移动端的原理及应用场景

「干货」纯CSS实现瀑布流(Masonry)

手把手教你CSS grid布局「香」

手把手教你20个CSS 快速提升技巧

细品100道CSS知识点(上)「干货满满」

细品100道CSS知识点(下)「干货满满」

手把手教你CSS Flex布局「真香」

细品用SVG实现一个优雅的提示框

手把手整理CSS3知识汇总【思维导图】

手把手教你55 个提高CSS 开发效率的必备片段

手把手教你常见的CSS布局方式【实践】

让CSS flex布局最后一行左对齐的N种方法

妙用CSS变量,让你的CSS变得更心动

纯CSS实现简单骨骼动画【实践】

CSS揭秘实用技巧总结

你未必知道的49个CSS知识点

深入浅出超好用的 CSS 阴影技巧

关于前端CSS写法104个知识点汇总(一)

关于前端CSS写法104个知识点汇总(二)

前端开发规范:命名规范、html规范、css规范、js规范

CSS变量实现暗黑模式,我的小铺页面已经支持

深入浅出CSS中彻底搞懂word-break、word-wrap、white-space

深入浅出详细讲解CSS 渲染原理以及优化策略

手把手教你深入CSS实现一个粒子动效的按钮

手把手教你css 中多种边框的实现小窍门【实践】

手把手详细教你优化CSS提高网站加载速度的21种方法汇总【实践】

作者:郭世平

转发链接:https://mp.weixin.qq.com/s/f2ZhTN-5R6GOxrJDBrjcVQ

最近发表
标签列表