Таймер обратного отсчета с круглым progress bar

Я создал таймер обратного отсчета. У меня получился бордюр, который я сделал круглым. Поскольку таймер стремится к нулю, круглая граница должна менять цвет с уменьшением в секундах

Я создал JSFIDDLE

var displayminutes;
var displayseconds;
var initializeTimer = 1.5 // enter in minutes
var minutesToSeconds = initializeTimer*60;

$("#document").ready(function(){
    setTime = getTime();
    $(".btn-timer").html(setTime[0]+":"+setTime[1])
});


$(".btn-timer").click(function(){
    var startCountDownTimer = setInterval(function(){
          minutesToSeconds = minutesToSeconds-1;
        var timer = getTime();
         $(".btn-timer").html(timer[0]+":"+timer[1]);
        if(minutesToSeconds == 0){
            clearInterval(startCountDownTimer);
            console.log("completed");
        }
      },1000)
});


function getTime(){

    displayminutes = Math.floor(minutesToSeconds/60);
    displayseconds = minutesToSeconds - (displayminutes*60);
    if(displayseconds < 10)
    {   
        displayseconds ="0"+displayseconds;
    }
     if(displayminutes < 10)
    {   
        displayminutes = "0"+displayminutes;
    }

    return [displayminutes, displayseconds];
}
body{
background-color:#ebebeb;
margin : 0 auto;
padding: 0 auto;
}

.outer{
border-radius: 50%;
 height:115px;
    width:115px;
    border:13px solid #e0e0e0;

}

.btn-timer{

    border-radius: 50%;
    position:relative;
    height:115px;
    width:115px;
    border:none !important;
    font-size:25px;
        
    background: rgba(255,255,255,1);
background: -moz-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(246,246,246,1) 47%, rgba(237,237,237,1) 100%);
background: -webkit-gradient(left top, left bottom, color-stop(0%, rgba(255,255,255,1)), color-stop(47%, rgba(246,246,246,1)), color-stop(100%, rgba(237,237,237,1)));
background: -webkit-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(246,246,246,1) 47%, rgba(237,237,237,1) 100%);
background: -o-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(246,246,246,1) 47%, rgba(237,237,237,1) 100%);
background: -ms-linear-gradient(top, rgba(255,255,255,1) 0%, rgba(246,246,246,1) 47%, rgba(237,237,237,1) 100%);
background: linear-gradient(to bottom, rgba(255,255,255,1) 0%, rgba(246,246,246,1) 47%, rgba(237,237,237,1) 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ededed', GradientType=0 );

    -webkit-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75);
-moz-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75);
box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75);
}
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>

<div class="outer">
    <button class="btn btn-default btn-timer">0.00</button>
</div>

Как получить круглый индикатор выполнения. Я искал плагин jQuery, но он не соответствует моим требованиям. Я ищу результат, похожий на эту ссылку

Свободный перевод вопроса https://stackoverflow.com/q/31198304/7394871 от участника @Alaksandar Jesus Gene.


Ответы (3 шт):

Автор решения: Alexandr_TT

Ниже приведен пример таймера обратного отсчета с круглым индикатором выполнения, который меняет цвет по мере уменьшения значения.

В основном мы делаем следующее: (дополнительные сведения см. Во встроенных комментариях кода)

  • 4 дополнительных div абсолютно спозиционированы поверх родительского. Каждый представляет квадрант окружности.
  • изначально угол наклона всех из них равен 0 градусов, поэтому все они полностью видны и покрывают весь родительский элемент. Это скрывает тень родительского блока и, таким образом, делает его похожим на сплошной круг.
  • На каждой итерации мы изменяем угол наклона каждого квадранта (div) таким образом, чтобы квадранты в конечном итоге становились невидимыми один за другим, открывая тем самым прямоугольную тень родительского элемента.
  • Квадранты становятся невидимыми, когда угол наклона достигает +/- 90 градусов, поэтому на каждой итерации угол вычисляется как (90 градусов / количество шагов, пройденных в этом квадранте).
  • По мере того, как прогресс перемещается от одного квадранта к другому, тень родительского блока изменяется, чтобы создать вид индикатора выполнения, меняющего свой цвет.
  • Исходный CodePen использует значение атрибута data-progress непосредственно как содержимое псевдоэлемента. Но это значение увеличивается с каждой итерацией. Поскольку он также используется при вычислении углов skew, я оставил его как есть и использовал отдельное поле для таймера обратного отсчета. Содержимое псевдоэлементов не может быть установлено с помощью JS, поэтому я добавил еще один div для текста таймера.

window.onload = function() {
  var progressbar = document.querySelector('div[data-progress]'),
    quad1 = document.querySelector('.quad1'),
    quad2 = document.querySelector('.quad2'),
    quad3 = document.querySelector('.quad3'),
    quad4 = document.querySelector('.quad4'),
    counter = document.querySelector('.counter');

  var progInc = setInterval(incrementProg, 1000); // вызывать функцию каждую секунду

  function incrementProg() {
    progress = progressbar.getAttribute('data-progress'); //получить текущее значение
    progress++; // увеличивать значение индикатора выполнения на 1 с каждой итерацией
    progressbar.setAttribute('data-progress', progress); //установить значение атрибута
    counter.textContent = 100 - parseInt(progress, 10); // установить значение таймера обратного отсчета
    setPie(progress); // вызвать функцию индикатора выполнения рисования на основе значения прогресса
    if (progress == 100) {
      clearInterval(progInc); // сбросить таймер, когда обратный отсчет завершится
    }
  }

  function setPie(progress) {
    /* Если прогресс меньше 25, измените угол наклона первого квадранта.t */
    if (progress <= 25) {
      quad1.setAttribute('style', 'transform: skew(' + progress * (-90 / 25) + 'deg)');
    }

    /* От 25 до 50: скрыть 1-й квадрант + изменить угол перекоса 2-го квадранта */
    else if (progress > 25 && progress <= 50) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // полностью скрывает 1-й квадрант
      quad2.setAttribute('style', 'transform: skewY(' + (progress - 25) * (90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px orange'); 
    }

    /* От 50 до 75: скрыть первые 2 квадранта + изменить угол наклона 3-го квадранта. */
    else if (progress > 50 && progress <= 75) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // полностью скрывает 1-й
      quad2.setAttribute('style', 'transform: skewY(90deg)'); // полностью скрывает второй
      quad3.setAttribute('style', 'transform: skew(' + (progress - 50) * (-90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px yellow');
    }

    /* Аналогично приведенному выше для значения от 75 до 100 */
    else if (progress > 75 && progress <= 100) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // hides 1st completely
      quad2.setAttribute('style', 'transform: skewY(90deg)'); // hides 2nd completely
      quad3.setAttribute('style', 'transform: skew(-90deg)'); // hides 3rd completely
      quad4.setAttribute('style', 'transform: skewY(' + (progress - 75) * (90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px green');
    }
  }
}
div[data-progress] {
  box-sizing: border-box;
  position: relative;
  height: 200px;
  width: 200px;
  background: beige;
  border-radius: 50%;
  box-shadow: inset 0px 0px 0px 20px red;
  transition: all 1s;
  overflow: hidden;
}
.counter {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0%;
  left: 0%;
  text-align: center;
  line-height: 200px;
  border-radius: 50%;
  background: transparent;
  z-index: 4;
}
div > div {
  position: absolute;
  height: 50%;
  width: 50%;
  background: inherit;
  border-radius: 0%;
}
.quad1,
.quad2 {
  left: 50%;
  transform-origin: left bottom;
}
.quad3,
.quad4 {
  left: 0%;
  transform-origin: right top;
}
.quad1,
.quad4 {
  top: 0%;
}
.quad2,
.quad3 {
  top: 50%;
}
.quad1,
.quad3 {
  transform: skew(0deg); /* invisible at -90deg */
}
.quad2,
.quad4 {
  transform: skewY(0deg); /* invisible at 90deg */
}

/* Just for demo */

body {
  background: linear-gradient(90deg, crimson, indianred, purple);
}
div[data-progress] {
  margin: 40px auto;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<div data-progress="0">
  <div class="quad1"></div>
  <div class="quad2"></div>
  <div class="quad3"></div>
  <div class="quad4"></div>
  <div class='counter'>100</div>
</div>

В приведенном ниже фрагменте я добавил другой фон для каждого квадранта, чтобы обеспечить лучшую визуальную иллюстрацию того, что именно происходит.

window.onload = function() {
  var progressbar = document.querySelector('div[data-progress]'),
    quad1 = document.querySelector('.quad1'),
    quad2 = document.querySelector('.quad2'),
    quad3 = document.querySelector('.quad3'),
    quad4 = document.querySelector('.quad4'),
    counter = document.querySelector('.counter');

  var progInc = setInterval(incrementProg, 1000); // call function every second

  function incrementProg() {
    progress = progressbar.getAttribute('data-progress'); //get current value
    progress++; // increment the progress bar value by 1 with every iteration
    progressbar.setAttribute('data-progress', progress); //set value to attribute
    counter.textContent = 100 - parseInt(progress, 10); // set countdown timer's value
    setPie(progress); // call the paint progress bar function based on progress value
    if (progress == 100) {
      clearInterval(progInc); // clear timer when countdown is complete
    }
  }

  function setPie(progress) {
    /* If progress is less than 25, modify skew angle the first quadrant */
    if (progress <= 25) {
      quad1.setAttribute('style', 'transform: skew(' + progress * (-90 / 25) + 'deg)');
    }

    /* Between 25-50, hide 1st quadrant + modify skew angle of 2nd quadrant */
    else if (progress > 25 && progress <= 50) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // hides 1st completely
      quad2.setAttribute('style', 'transform: skewY(' + (progress - 25) * (90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px orange');
    }

    /* Between 50-75, hide first 2 quadrants + modify skew angle of 3rd quadrant */
    else if (progress > 50 && progress <= 75) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // hides 1st completely
      quad2.setAttribute('style', 'transform: skewY(90deg)'); // hides 2nd completely
      quad3.setAttribute('style', 'transform: skew(' + (progress - 50) * (-90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px yellow');
    }

    /* Similar to above for value between 75-100 */
    else if (progress > 75 && progress <= 100) {
      quad1.setAttribute('style', 'transform: skew(-90deg)'); // hides 1st completely
      quad2.setAttribute('style', 'transform: skewY(90deg)'); // hides 2nd completely
      quad3.setAttribute('style', 'transform: skew(-90deg)'); // hides 3rd completely
      quad4.setAttribute('style', 'transform: skewY(' + (progress - 75) * (90 / 25) + 'deg)');
      progressbar.setAttribute('style', 'box-shadow: inset 0px 0px 0px 20px green');
    }
  }
}
div[data-progress] {
  box-sizing: border-box;
  position: relative;
  height: 200px;
  width: 200px;
  background: beige;
  border-radius: 50%;
  box-shadow: inset 0px 0px 0px 20px red;
  transition: all 1s;
  overflow: hidden;
}
.counter {
  position: absolute;
  height: 100%;
  width: 100%;
  top: 0%;
  left: 0%;
  text-align: center;
  line-height: 200px;
  border-radius: 50%;
  background: transparent;
  z-index: 4;
}
div > div {
  position: absolute;
  height: 50%;
  width: 50%;
  border-radius: 0%;
}
.quad1,
.quad2 {
  left: 50%;
  transform-origin: left bottom;
}
.quad3,
.quad4 {
  left: 0%;
  transform-origin: right top;
}
.quad1, .quad4{
  top: 0%;
}
.quad2, .quad3{
  top: 50%;
}
.quad1, .quad3{
  transform: skew(0deg);
}
.quad2, .quad4{
  transform: skewY(0deg);
}

/* Just for demo */

body {
  background: linear-gradient(90deg, crimson, indianred, purple);
}
div[data-progress] {
  margin: 40px auto;
}
.quad1 {
  background: blue;
}
.quad2 {
  background: pink;
}
.quad3 {
  background: tan;
}
.quad4 {
  background: teal;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
<div data-progress="0">
  <div class="quad1"></div>
  <div class="quad2"></div>
  <div class="quad3"></div>
  <div class="quad4"></div>
  <div class='counter'>100</div>
</div>

Свободный перевод ответа от участника @Harry.

→ Ссылка
Автор решения: UModeL

С развитием браузеров и появлением новых технологий (таких, как CSS-переменные и conic-gradient), можно существенно сократить код, но при этом, расширить функциональность и улучшить визуальную составляющую:

let oCount = document.querySelector("div[countdown]");
oCount.innerText = oCount.countValue = +oCount.getAttribute("countdown");
oCount.countRatio = 360 / oCount.countValue;
oCount.countColor = 100 / oCount.countValue;
oCount.countLight = oCount.countValue / 20;
oCount.countTimer = setInterval(fCountdown.bind(oCount), 1000);

function fCountdown() {
  let nCount = this.countValue;
  if (nCount > 0) {
    nCount--;
    this.innerText = this.countValue = nCount;
    this.style.setProperty("--deg", 361 - nCount * this.countRatio);
    this.style.setProperty("--col", `hsla(${nCount * this.countColor}, 100%, ${50 - nCount / this.countLight}%, 1)`);
  } else {
    clearInterval(this.countTimer);
    console.log("Complete " + this.getAttribute('countdown'));
  }
}
body { display: flex; justify-content: space-around; align-items: center; height: 100vh; margin: 0; background-image: radial-gradient(#aef, #000); }

div[countdown] {
  --deg: -1;
  --col: hsla(0, 100%, 50%, 1);
  height: 120px; width: 120px;
  border-radius: 50%;
  text-align: center;
  font: bold 36px/120px monospace;
  background-image: radial-gradient(#fff 49px, #f000 51px), conic-gradient(var(--col) calc(var(--deg) * 1deg - 1deg), transparent calc(var(--deg) * 1deg + 1deg)), radial-gradient(#fff3 40px, #4441 60px);
  box-shadow: inset 0 0 10px -5px #000a;
}
<div countdown="60"></div>

Если необходимо использовать несколько подобных блоков, то достаточно добавить в скрипт пару строк:

[...document.querySelectorAll("div[countdown]")].forEach(function(oCount) {
  oCount.innerText = oCount.countValue = +oCount.getAttribute("countdown");
  oCount.countRatio = 360 / oCount.countValue;
  oCount.countColor = 100 / oCount.countValue;
  oCount.countLight = oCount.countValue / 20;
  oCount.countTimer = setInterval(fCountdown.bind(oCount), 100);
});

function fCountdown() {
  let nCount = this.countValue;
  if (nCount > 0) {
    nCount--;
    this.innerText = this.countValue = nCount;
    this.style.setProperty("--deg", 361 - nCount * this.countRatio);
    this.style.setProperty("--col", `hsla(${nCount * this.countColor}, 100%, ${50 - nCount / this.countLight}%, 1)`);
  } else {
    clearInterval(this.countTimer);
    console.log("Complete " + this.getAttribute('countdown'));
  }
}
body { display: flex; justify-content: space-around; align-items: center; height: 100vh; margin: 0; background-image: radial-gradient(#aef, #000); }

div[countdown] {
  --deg: -1;
  --col: hsla(0, 100%, 50%, 1);
  height: 120px; width: 120px;
  border-radius: 50%;
  text-align: center;
  font: bold 36px/120px monospace;
  background-image: radial-gradient(#fff 49px, #f000 51px), conic-gradient(var(--col) calc(var(--deg) * 1deg - 1deg), transparent calc(var(--deg) * 1deg + 1deg)), radial-gradient(#fff3 40px, #4441 60px);
  box-shadow: inset 0 0 10px -5px #000a;
}
<div countdown="100"></div><div countdown="50"></div><div countdown="75"></div>

Добавляем ещё несколько строк в JS - теперь запуск происходит по клику на блоке:

[...document.querySelectorAll("div[countdown]")].forEach(function(oCount) {
  oCount.innerText = oCount.countValue = +oCount.getAttribute("countdown");
  oCount.countRatio = 360 / oCount.countValue;
  oCount.countColor = 100 / oCount.countValue;
  oCount.countLight = oCount.countValue / 20;
  oCount.addEventListener("click", function() {
    if (!this.countTimer || this.countTimer === "undefined") {
      this.innerText = this.countValue = +this.getAttribute("countdown");
      this.countTimer = setInterval(fCountdown.bind(this), 100);
    }
  });
});

function fCountdown() {
  let nCount = this.countValue;
  if (nCount > 0) {
    nCount--;
    this.innerText = this.countValue = nCount;
    this.style.setProperty("--deg", 361 - nCount * this.countRatio);
    this.style.setProperty("--col", `hsla(${nCount * this.countColor}, 100%, ${50 - nCount / this.countLight}%, 1)`);
  } else {
    clearInterval(this.countTimer);
    delete this.countTimer;
    console.log("Complete " + this.getAttribute("countdown"));
  }
}
body { display: flex; justify-content: space-around; align-items: center; height: 100vh; margin: 0; background-image: radial-gradient(#aef, #000); }

div[countdown] {
  --deg: -1;
  --col: hsla(0, 100%, 50%, 1);
  height: 120px; width: 120px;
  border-radius: 50%;
  text-align: center;
  font: bold 36px/120px monospace;
  background-image: radial-gradient(#fff 49px, #f000 51px), conic-gradient(var(--col) calc(var(--deg) * 1deg - 1deg), transparent calc(var(--deg) * 1deg + 1deg)), radial-gradient(#fff3 40px, #4441 60px);
  box-shadow: inset 0 0 10px -5px #000a;
}
<div countdown="100"></div><div countdown="50"></div><div countdown="75"></div>

И напоследок - если нужна прозрачность блока, тогда переносим градиент в псевдоэлемент, применяем к нему свойство clip-path с SVG-маской и располагаем его позади блока:

[...document.querySelectorAll("div[countdown]")].forEach(function(oCount) {
  oCount.innerText = oCount.countValue = +oCount.getAttribute("countdown");
  oCount.countRatio = 360 / oCount.countValue;
  oCount.countColor = 100 / oCount.countValue;
  oCount.countLight = oCount.countValue / 20;
  oCount.addEventListener("click", function() {
    if (!this.countTimer || this.countTimer === "undefined") {
      this.innerText = this.countValue = +this.getAttribute("countdown");
      this.countTimer = setInterval(fCountdown.bind(this), 100);
    }
  });
});

function fCountdown() {
  let nCount = this.countValue;
  if (nCount > 0) {
    nCount--;
    this.innerText = this.countValue = nCount;
    this.style.setProperty("--deg", 361 - nCount * this.countRatio);
    this.style.setProperty("--col", `hsla(${nCount * this.countColor}, 100%, ${50 - nCount / this.countLight}%, 1)`);
  } else {
    clearInterval(this.countTimer);
    delete this.countTimer;
    console.log("Complete " + this.getAttribute("countdown"));
  }
}
body { display: flex; justify-content: space-around; align-items: center; height: 100vh; margin: 0; background-image: radial-gradient(#aef, #000); }

div[countdown] {
  --deg: 360;
  --col: hsla(0, 100%, 50%, 0);
  position: relative;
  height: 120px;
  width: 120px;
  border-radius: 50%;
  font: bold 36px/120px monospace;
  text-align: center;
  user-select: none;
  cursor: pointer;
  box-shadow: inset 0 0 10px -5px #000a;
  overflow: hidden;
}
div[countdown]::before {
  content: attr(countdown);
  position: absolute;
  top:0; left:0;
  z-index: -1;
  height: inherit; width: inherit;
  background-image: conic-gradient(var(--col) calc(var(--deg) * 1deg - 1deg), transparent calc(var(--deg) * 1deg + 1deg)), radial-gradient(#fff3 40px, #4441 60px);
  clip-path: url(#ring-clip-path);
}
.svg { position: absolute; height: 0; width: 0; }
<div countdown="100"></div><div countdown="50"></div><div countdown="75"></div>

<svg class="svg">
  <defs>
    <clipPath id="ring-clip-path">
      <path d="M 0 0 L 60 0 L 60 10 C 30 10 10 30 10 60 C 10 90 30 110 60 110 C 90 110 110 90 110 60 C 110 30 90 10 60 10 L 60 0 L 120 0 L 120 120 L 0 120 Z"></path>
    </clipPath>
  </defs>
</svg>

→ Ссылка
Автор решения: Проста Miha

Вот мой вариант, который никому не нужен ^-^

var timer = setInterval(timerFunction, 1000),
  time = 31,
  procent =  100 / time,
  per = 0;
const timerBlock = document.querySelector(".timer");

function timerFunction() {
  per += procent;
  //Hour
  hour = Math.floor(time / 3600);
  hour = hour < 10 ? "0" + hour : hour;
  //Minutes
  minute = Math.floor((time - hour * 3600) / 60);
  minute = minute < 10 ? "0" + minute : minute;
  minute = minute > 59 ? "00" : minute;
  //Seconds
  second = time % 60;
  second = second < 10 ? "0" + second : second;

  timerBlock.innerHTML = hour + ":" + minute + ":" + second;
  time--;
  timerBlock.style.cssText = 
      "background: conic-gradient(hsla(180, 100%, 50%, 0.5) " + per +
      "%, transparent 0 " + (100 - per) + "%)";
  
  if (time == -1) clearInterval(timer);
}
body {
  width: 100%;
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  margin: 0;
}

.timer {
  width: 240px;
  height: 240px;
  box-shadow: 0 0 15px 4px rgba(0, 0, 0, .4);
  background-color: #fff;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 48px;
  font-family: consolas;
  font-weight: 700;
}
<div class="timer"></div>

→ Ссылка