Круглый прогресс бар с процентом сверху текущего положения бара

Рейтинг: 1Ответов: 1Опубликовано: 09.02.2023

Решил сделать круглый прогресс бар, сверху текущего положения бара должен находиться текущий процент. Вот пример (Цвет текста на ваше усмотрение, я сделал его чёрным, чтобы его было лучше видно):

Взял основание у своего прошлого ответа (Второе решение). Вот то, что я смог придумать:

class CircleProgress {
  constructor(progress, circleSize = 100) {
    this.progress = progress
    this.circleSize = circleSize
  }

  init(parentNode) {
    let progress = this.progress
    let circleSize = this.circleSize
    let parentElement = parentNode || document.body
    let progressNode = document.createElement('div')
    let percentageNode = document.createElement('div')

    progressNode.className = 'progress-circle'
    progressNode.style.cssText = `
    --progress: ${progress}%;
    --circle-size: ${circleSize}px;`

    let angle = progress * 360 / 100
    let radius = circleSize / 2
    let rad = angle * Math.PI / 180
    let left = radius + radius * Math.sin(rad)
    let top = radius + radius * Math.cos(rad)

    percentageNode.className = 'progress-circle-percentage'
    percentageNode.textContent = progress + '%'
    percentageNode.style.cssText = `
    left: ${left.toFixed(2)}px;
    top: ${top.toFixed(2)}px;
    transform: rotate(${angle}deg);`

    progressNode.appendChild(percentageNode)
    parentElement.appendChild(progressNode)
  }
}

new CircleProgress(25)
  .init()

new CircleProgress(50)
  .init()

new CircleProgress(75, 150)
  .init()

new CircleProgress(42, 150)
  .init()
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.progress-circle {
  --circle-size: 100px;

  width: var(--circle-size);
  height: var(--circle-size);
  border-radius: 50%;
  margin: 1.5em;
  background-image: conic-gradient(rgb(100, 150, 220) var(--progress), rgb(240, 240, 240) 0);
  position: relative;
}

.progress-circle::before {
  --border-width: 10px;
  --inner-circle-size: calc(var(--circle-size) - var(--border-width));
  --inner-circle-gap: calc(var(--border-width) / 2);

  content: '';
  display: block;
  width: var(--inner-circle-size);
  height: var(--inner-circle-size);
  background-color: rgb(255, 255, 255);
  border-radius: 50%;
  position: relative;
  left: var(--inner-circle-gap);
  top: var(--inner-circle-gap);
}

.progress-circle-percentage {
  position: absolute;
  font-family: 'Montserrat', 'Segoe UI';
  color: rgb(80, 120, 170);
}

Как мы можем наблюдать, в первом кейсе всё работает как планировалось. Дальше я добавил ещё 3 круга и по какой-то причине, что-то пошло нет так, в чём может быть проблема? Я предполагаю, что я что-то не так написал в переменных left и top, возможно и радиан хранящийся в переменной (Переменная rad) тоже не правильный, честно говоря, не уверен.

Ответы

▲ 3Принят

Думаю, что всё делается проще. Но, раз нужно исправить существующее, то вот:

class CircleProgress {
  constructor(progress, circleSize = 100) {
    this.progress = progress
    this.circleSize = circleSize
  }

  init(parentNode) {
    let progress = this.progress
    let circleSize = this.circleSize
    let parentElement = parentNode || document.body
    let progressNode = document.createElement('div')
    let percentageNode = document.createElement('div')

    progressNode.className = 'progress-circle'
    progressNode.style.cssText = `
    --progress: ${progress}%;
    --circle-size: ${circleSize}px;`

    let angle = progress * 360 / 100

    percentageNode.className = 'progress-circle-percentage'
    percentageNode.textContent = progress + '%'
    percentageNode.style.cssText = `transform: translate(-50%,0) rotate(${angle}deg);`

    progressNode.appendChild(percentageNode)
    parentElement.appendChild(progressNode)
  }
}

new CircleProgress(0)
  .init()

new CircleProgress(25)
  .init()

new CircleProgress(50)
  .init()

new CircleProgress(75, 150)
  .init()

new CircleProgress(42, 150)
  .init()
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

.progress-circle {
  --circle-size: 100px;
  width: var(--circle-size);
  height: var(--circle-size);
  border-radius: 50%;
  margin: 1.5em;
  background-image: conic-gradient(rgb(100, 150, 220) var(--progress), rgb(240, 240, 240) 0);
  position: relative;
}

.progress-circle::before {
  --border-width: 10px;
  --inner-circle-size: calc(var(--circle-size) - var(--border-width));
  --inner-circle-gap: calc(var(--border-width) / 2);
  content: '';
  display: block;
  width: var(--inner-circle-size);
  height: var(--inner-circle-size);
  background-color: rgb(255, 255, 255);
  border-radius: 50%;
  position: relative;
  left: var(--inner-circle-gap);
  top: var(--inner-circle-gap);
}

.progress-circle-percentage {
  position: absolute;
  top: -1.5em;
  left: 50%;
  height: calc(var(--circle-size) / 2 + 1.5em);
  font-family: 'Montserrat', 'Segoe UI';
  color: rgb(80, 120, 170);
  transform-origin: 50% 100%;
}


В варианте ниже:

  • Добавлена прозрачность центральной части;
  • Создаётся только один элемент, остальное - псевдоэлементы;
  • Добавлен параметр толщины линии;
  • Все вычисления проводятся в CSS, а скрипт только создаёт элемент с заданными параметрами:

class CircleProgress {
  constructor(progress, circleSize = 100, borderWidth = 5) {
    this.progress = progress;
    this.circleSize = circleSize;
    this.borderWidth = borderWidth;
  }

  init(parentNode) {
    let progress = this.progress;
    let circleSize = this.circleSize;
    let borderWidth = this.borderWidth;
    let parentElement = parentNode || document.body;
    let progressNode = document.createElement('div');

    progressNode.dataset.percentage = progress;
    progressNode.className = 'progress-circle';
    progressNode.style.cssText = `--progress: ${progress}; --circle-size: ${circleSize}px; --border-width: ${borderWidth}px;`;
    parentElement.appendChild(progressNode);
  }
}

new CircleProgress(17, 30).init();
new CircleProgress(25).init();
new CircleProgress(50, 40, 15).init();
new CircleProgress(75, 80, 40).init();
new CircleProgress(42, 150).init();
body { background-image: repeating-linear-gradient(45deg, #8888 0 1px, #fff8 1px 2px); }

.progress-circle {
  --progress: 0;
  --circle-size: 100px;
  --border-width: 5px;
  position: relative;
  margin: 1.5em;
  height: var(--circle-size);
  width: var(--circle-size);
  filter: drop-shadow(0 5px 4px #0008);
}

.progress-circle::before,
.progress-circle::after {
  position: absolute;
  color: #6496dc;
}

.progress-circle::before {
  content: '';
  height: var(--circle-size);
  width: var(--circle-size);
  border-radius: 50%;
  background-image: conic-gradient(currentcolor calc(var(--progress) * 1%), #f0f0f0 0);
  -webkit-mask-image: radial-gradient(transparent 0 calc(66% - var(--border-width)), #000 calc(66% - var(--border-width) + 1px));
  mask-image: radial-gradient(transparent 0 calc(66% - var(--border-width)), #000 calc(66% - var(--border-width) + 1px));
}

.progress-circle::after {
  content: attr(data-percentage)'%';
  top: -1.4em;
  left: 50%;
  height: calc(var(--circle-size) / 2 + 1.4em);
  font-family: 'Montserrat', 'Segoe UI';
  transform-origin: 50% 100%;
  transform: translate(-50%, 0) rotate(calc(var(--progress) * 360deg / 100));
}

Можно ещё немного усложнить, добавив анимацию:

class CircleProgress {
  constructor(progress = 0, circleSize = 100, borderWidth = 5, animationTime = 0) {
    this.progressFull = progress;
    this.circleSize = circleSize;
    this.borderWidth = borderWidth;
    this.animationTime = animationTime && animationTime > 0 ? progress / animationTime / 100 : progress;
    this.progressCurr = 0;
    this.requestId = 0;
    this.el = document.createElement('div');
  }

  init(parentNode) {
    this.el.className = 'progress-circle';
    (parentNode || document.body).appendChild(this.el);
    this.el.style.cssText = `--circle-size: ${this.circleSize}px; --border-width: ${this.borderWidth}px;`;
    this.animate();
  }

  tick() {
    this.el.dataset.percentage = Math.ceil(this.progressCurr);
    this.el.style.setProperty('--progress', this.progressCurr);
  }

  animate() {
    this.tick();
    if (Math.ceil((this.progressCurr += this.animationTime)) <= this.progressFull) {
      this.requestId = requestAnimationFrame(() => this.animate());
    } else {
      cancelAnimationFrame(this.requestId);
      this.el.style.setProperty('--finish-color', 'red');
    }
  }
}

new CircleProgress(20,  80,  2, 1).init();
new CircleProgress(40,  80, 20, 2).init();
new CircleProgress(60, 100, 10, 3).init();
new CircleProgress(80,  80, 40, 4).init();
new CircleProgress(100, 80, 20, 5).init();
body { display: flex; justify-content: space-around; overflow: hidden; background-image: repeating-linear-gradient(45deg, #8884 0 1px, #fc8f 1px 2px); }

.progress-circle {
  --progress: 0;
  --circle-size: 100px;
  --border-width: 5px;
  position: relative;
  margin: 1.5em 0;
  height: var(--circle-size); width: var(--circle-size);
  filter: drop-shadow(0 5px 4px #0008);
}
.progress-circle::before,
.progress-circle::after {
  position: absolute;
  color: var(--finish-color, #6496dc);
  transition: color .5s ease;
}
.progress-circle::before {
  content: '';
  height: var(--circle-size); width: var(--circle-size);
  border-radius: 50%;
  background-image: conic-gradient(currentcolor calc(var(--progress) * 360deg / 100), #f0f0f0 calc(var(--progress) * 360deg / 100 + 5deg));
  -webkit-mask-image: radial-gradient(transparent 0 calc(66% - var(--border-width)), #000 calc(66% - var(--border-width) + 1px));
  mask-image: radial-gradient(transparent 0 calc(66% - var(--border-width)), #000 calc(66% - var(--border-width) + 1px));
}
.progress-circle::after {
  content: attr(data-percentage)'%';
  top: -1.4em; left: 50%;
  height: calc(var(--circle-size) / 2 + 1.4em);
  font-family: 'Montserrat', 'Segoe UI';
  transform-origin: 50% 100%;
  transform: translate(-50%, 0) rotate(calc(var(--progress) * 360deg / 100));
}