How to Build a Smooth Semi‑Circular Progress Bar with Canvas and SVG
This tutorial walks through creating a semi‑circular progress bar using both Canvas and SVG, covering preparation, drawing techniques, angle calculations, animation loops, anti‑aliasing optimizations, and dynamic updates, and compares the visual and performance differences between the two approaches.
Introduction
In many UI scenarios we need progress bars such as rectangular, circular, or semi‑circular ones. This article explains how to create a semi‑circular progress bar using both Canvas and SVG, and how to enhance its user experience.
Visual Mockup and Demo
The examples use a single‑file Vue + HTML setup.
Canvas Implementation Steps
1. Preparation
Set up a canvas element and understand the arc method parameters (x, y, radius, startAngle, endAngle, counterclockwise).
<code>data() {
return {
canvas: null,
cWidth: 750,
cHeight: 750,
progress: 50
}
}
</code>Define initCircleProgress to calculate variables:
<code>let radius = 124; // outer radius
let thickness = 12; // ring thickness
let innerRadius = radius - thickness;
let startAngle = -180;
let endAngle = 0;
let x = 0;
let y = 0;
</code>Initialize canvas and translate origin to the center:
<code>this.canvas = document.getElementById('circleProgress');
let ctx = this.canvas.getContext('2d');
this.canvas.style.width = this.cWidth + 'px';
this.canvas.style.height = this.cHeight + 'px';
this.canvas.width = this.cWidth;
this.canvas.height = this.cHeight;
ctx.translate(document.body.clientWidth / 2, document.body.clientWidth / 2);
ctx.fillStyle = '#FFF';
</code>2. Draw the semi‑ring
Utility to convert degrees to radians:
<code>function angle2Radian(angle) {
return (angle * Math.PI) / 180;
}
</code>Render outer ring:
<code>function renderRing(startAngle, endAngle) {
ctx.beginPath();
ctx.arc(0, 0, radius, angle2Radian(startAngle), angle2Radian(endAngle));
}
</code>Draw inner ring using reverse arc, then connect start and end points with small circles calculated via trigonometric functions ( Math.cos , Math.sin ) and a helper calcRingPoint :
<code>function calcRingPoint(x, y, radius, angle) {
return {
x: x + radius * Math.cos((angle * Math.PI) / 180),
y: y + radius * Math.sin((angle * Math.PI) / 180)
};
}
</code>Combine arcs and circles, then fill.
3. Optimize and animate
Adjust start/end angles (e.g., -65 to 155) and rotate canvas to align with the design:
<code>let startAngle = -65;
let endAngle = 155;
ctx.rotate(angle2Radian(225));
</code>Animate by gradually increasing the angle and redrawing:
<code>let tempAngle = startAngle;
let total = 100;
let percent = this.progress / total;
let twoEndAngle = percent * 220 + startAngle;
let step = (twoEndAngle - startAngle) / 100;
function animLoop() {
if (tempAngle < twoEndAngle) {
tempAngle += step;
renderRing(startAngle, tempAngle);
window.requestAnimationFrame(animLoop);
}
}
animLoop();
</code>To reduce aliasing, increase canvas pixel ratio and scale:
<code>let devicePixelRatio = 4;
this.canvas.height = this.cHeight * devicePixelRatio;
this.canvas.width = this.cWidth * devicePixelRatio;
ctx.scale(devicePixelRatio, devicePixelRatio);
</code>SVG Implementation Steps
1. Create a full circle
Use <circle> with stroke-dasharray and stroke-linecap="round" to define the visible segment.
<code><svg width="440" height="440" viewBox="0 0 440 440">
<circle cx="220" cy="220" r="140" stroke-width="16" stroke="#FFF" fill="none"/>
<circle cx="220" cy="220" r="140" stroke-width="16" stroke="#00A5E0"
fill="none" stroke-dasharray="260 879"/>
</svg>
</code>Calculate circumference (c = 2πr) to set dash lengths.
2. Transform to a semi‑circle
Adjust stroke-dasharray to half the perimeter (≈430) and rotate the SVG:
<code><svg width="440" height="440" viewBox="0 -100 440 440">
<circle cx="180" cy="220" r="140" stroke-width="16" stroke="#FFF"
fill="none" stroke-dasharray="430" stroke-linecap="round"/>
</svg>
</code>Apply CSS transform to rotate the element.
3. Dynamic progress
Update stroke-dasharray of the inner circle based on progress (half of the full circle because we use a semi‑circle):
<code>mounted() {
this.calcSvgProgress(this.progress);
},
methods: {
calcSvgProgress(progress, delay = 500) {
let percent = progress / 100,
perimeter = Math.PI * 170;
setTimeout(() => {
document.querySelector('.inner')
.setAttribute('stroke-dasharray', perimeter * percent + " 879");
}, delay);
}
}
</code>Add CSS transition for smooth animation:
<code>.inner {
transition: stroke-dasharray 1s;
}
</code>Experience Optimization When Data Updates
Canvas redraws the whole scene for each change, causing the animation to start from zero each time. SVG, being vector‑based, can animate by simply changing attributes, resulting in smoother transitions. The article also shows a technique to simulate a “level‑up” effect by temporarily setting the dash array to the full value, hiding the element, resetting the dash array, and then showing it again with the new progress.
<code>svgLevelUp() {
let circle = document.querySelector('.inner');
let perimeter = Math.PI * 170;
circle.setAttribute('stroke-dasharray', perimeter + " 879");
setTimeout(() => {
circle.style.display = 'none';
circle.setAttribute('stroke-dasharray', '0 879');
setTimeout(() => {
circle.style.display = 'block';
this.calcSvgProgress(25, 100);
}, 10);
}, 1000);
}
</code>Conclusion
Canvas offers fine‑grained control and is suitable for complex, highly customized designs, while SVG provides a concise, resolution‑independent solution that is easier to animate but may require extra tricks for intricate visual requirements. Choose the approach that best fits the project’s needs.
References
[1] Canvas coordinate calculation using trigonometric functions: https://developer.aliyun.com/article/922877
Yuewen Frontend Team
Click follow to learn the latest frontend insights in the cultural content industry. We welcome you to join us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.