Frontend Development 14 min read

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.

Yuewen Frontend Team
Yuewen Frontend Team
Yuewen Frontend Team
How to Build a Smooth Semi‑Circular Progress Bar with Canvas and SVG

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 &lt;circle&gt; with stroke-dasharray and stroke-linecap="round" to define the visible segment.

<code>&lt;svg width="440" height="440" viewBox="0 0 440 440"&gt;
  &lt;circle cx="220" cy="220" r="140" stroke-width="16" stroke="#FFF" fill="none"/&gt;
  &lt;circle cx="220" cy="220" r="140" stroke-width="16" stroke="#00A5E0"
          fill="none" stroke-dasharray="260 879"/&gt;
&lt;/svg&gt;
</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>&lt;svg width="440" height="440" viewBox="0 -100 440 440"&gt;
  &lt;circle cx="180" cy="220" r="140" stroke-width="16" stroke="#FFF"
          fill="none" stroke-dasharray="430" stroke-linecap="round"/&gt;
&lt;/svg&gt;
</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

animationJavaScriptfrontend developmentCanvassvgprogress bar
Yuewen Frontend Team
Written by

Yuewen Frontend Team

Click follow to learn the latest frontend insights in the cultural content industry. We welcome you to join us.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.