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

Взял основание у своего прошлого ответа (Второе решение). Вот то, что я смог придумать:
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) тоже не правильный, честно говоря, не уверен.
Ответы (1 шт):
Думаю, что всё делается проще. Но, раз нужно исправить существующее, то вот:
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));
}