简体   繁体   中英

Canvas rectangle (progress bar) with rounded ends - Issue with low values

I am trying to make a rectangle that has a width dependant on a percentage, I got it perfectly working until I tested something with 0%.

I wanted it to disappear at 0% but it appears as I opted for rounded corners, that there is a minimum width. The same issue is apparent with lower percentage numbers, and from what I can gather, it pushes the object the opposite way if the percentage is lower than 6%, at that point the rectangle becomes a circle and can not get any smaller. Is there a workaround for this? I have my heart set on it looking the way it does, currently, just need this issue to be fixed.

 const canvas = $("#progressBar"); const ctx = canvas.get(0).getContext("2d"); // rectWidth = 630 * percent / 100 (in this case 100%) const rectX = 60; const rectY = 10; const rectWidth = 630 * 100 / 100; const rectHeight = 38; const cornerRadius = 37; ctx.lineJoin = "round"; ctx.lineWidth = cornerRadius; ctx.strokeStyle = '#FF1700'; ctx.fillStyle = '#FF1700'; ctx.strokeRect(rectX + (cornerRadius / 2), rectY + (cornerRadius / 2), rectWidth - cornerRadius, rectHeight - cornerRadius); ctx.fillRect(rectX + (cornerRadius / 2), rectY + (cornerRadius / 2), rectWidth - cornerRadius, rectHeight - cornerRadius); // rectWidth = 630 * percent / 100 (in this case 0%) const rectX2 = 60; const rectY2 = 60; const rectWidth2 = 630 * 0 / 100; const rectHeight2 = 38; const cornerRadius2 = 37; ctx.lineJoin = "round"; ctx.lineWidth = cornerRadius; ctx.strokeStyle = '#FF1700'; ctx.fillStyle = '#FF1700'; ctx.strokeRect(rectX2 + (cornerRadius2 / 2), rectY2 + (cornerRadius2 / 2), rectWidth2 - cornerRadius2, rectHeight2 - cornerRadius2); ctx.fillRect(rectX2 + (cornerRadius2 / 2), rectY2 + (cornerRadius2 / 2), rectWidth2 - cornerRadius2, rectHeight2 - cornerRadius2);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <canvas id="progressBar" width="750" height="120"> </canvas>

CODE:

The way you implement it will have always some of 'matter' when you are at 0% . If you want have nothing when it's 0% and that it remains consistent when percentage grow, you don't want use ctx.lineJoin = "round"

As a workaround, you might draw the rounded corner instead by using the method arc() .

On arc(x, y, radius, startAngle, endAngle) , we know x = r , y = r and radius = r

计算绘制的角度

We only need some geometry calcul to get the needed values startAngle (α) and endAngle (α+Δ).

With the trigonometric function cosine , we have Math.cos(θ) = (r - p) / rθ = Math.acos((r - p) / r) .

We have and α = Math.PI - θ and we know Δ = 2 * θ(α+Δ) = Math.PI + θ

So finally:

  • startAngle α = Math.PI - Math.acos((r - p) / r)
  • endAngle (α+Δ) = Math.PI + Math.acos((r - p) / r)

In our case, r = h /2 so when p < rp < h / 2 , that gives us:

ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - 2 * p) / h), Math.PI + Math.acos((h - 2 * p) / h))
ctx.fillStyle = '#FF1700';
ctx.fill();

 const canvas = $("#progressBar"); const ctx = canvas.get(0).getContext("2d"); const h = 100; const p = 30; /* To visalize ------------------------------------------------------*/ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI / 2, 3 / 2 *Math.PI); ctx.lineTo(500, 0); ctx.arc((h / 2) + 500, h / 2, h / 2, 3 / 2 *Math.PI,Math.PI / 2); ctx.lineTo(h / 2, h); ctx.strokeStyle = '#000000'; ctx.stroke(); ctx.closePath(); /* ------------------------------------------------------------------*/ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - 2 * p) / h), Math.PI + Math.acos((h - 2 * p) / h)); ctx.fillStyle = '#FF1700'; ctx.fill();
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <canvas id="progressBar" width="750" height="120"> </canvas>

Now if we want rather this look (red part, we want to get ride of the grey part). The methode consist to do the same but for half of the progression and then to repeat the same figure in symmetry (hatched area).

在此处输入图片说明

To draw the symetric shape we will use ctx.scale(-1, 1) and with the save() restore() methods. The x position for the center of the 2nd arc will be - (r - p)-((h / 2) - p) and like we will work in horizontal symmetry, it will be finally (h / 2) - p

ctx.beginPath();
ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h));
ctx.save();
ctx.scale(-1, 1);
ctx.arc((h / 2) - p, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h));
ctx.restore();
ctx.fillStyle = '#FF1700';
ctx.fill();

 const canvas = $("#progressBar"); const ctx = canvas.get(0).getContext("2d"); const h = 100; const p = 25; /* To visalize ------------------------------------------------------*/ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI / 2, 3 / 2 *Math.PI); ctx.lineTo(500, 0); ctx.arc((h / 2) + 500, h / 2, h / 2, 3 / 2 *Math.PI,Math.PI / 2); ctx.lineTo(h / 2, h); ctx.strokeStyle = '#000000'; ctx.stroke(); ctx.closePath(); /* ------------------------------------------------------------------*/ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h)); ctx.save(); ctx.scale(-1, 1); ctx.arc((h / 2) - p, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h)); ctx.restore(); ctx.fillStyle = '#FF1700'; ctx.fill();
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <canvas id="progressBar" width="750" height="120"> </canvas>

This will be true till p <= h after we need to change our code take in account the rectangular part. We will use a if...else to do that.

if(p <= h){
  ctx.beginPath();
  ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h));
  ctx.save();
  ctx.scale(-1, 1);
  ctx.arc((h / 2) - p, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h));
  ctx.restore();
  ctx.fillStyle = '#FF1700';
  ctx.fill();
} else {
  ctx.beginPath();
  ctx.arc(h / 2, h / 2, h / 2, Math.PI / 2, 3 / 2 *Math.PI);
  ctx.lineTo(p - 2 * h, 0);
  ctx.arc(p - (h / 2), h / 2, h / 2, 3 / 2 *Math.PI,Math.PI / 2);
  ctx.lineTo(h / 2, h);
  ctx.fillStyle = '#FF1700';
  ctx.fill();
}

 const canvas = $("#progressBar"); const ctx = canvas.get(0).getContext("2d"); const h = 100; const p = 350; /* To visalize ------------------------------------------------------*/ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI / 2, 3 / 2 *Math.PI); ctx.lineTo(500, 0); ctx.arc((h / 2) + 500, h / 2, h / 2, 3 / 2 *Math.PI,Math.PI / 2); ctx.lineTo(h / 2, h); ctx.strokeStyle = '#000000'; ctx.stroke(); ctx.closePath(); /* ------------------------------------------------------------------*/ if(p <= h){ ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h)); ctx.save(); ctx.scale(-1, 1); ctx.arc((h / 2) - p, h / 2, h / 2, Math.PI - Math.acos((h - p) / h), Math.PI + Math.acos((h - p) / h)); ctx.restore(); ctx.fillStyle = '#FF1700'; ctx.fill(); } else { ctx.beginPath(); ctx.arc(h / 2, h / 2, h / 2, Math.PI / 2, 3 / 2 *Math.PI); ctx.lineTo(p - 2 * h, 0); ctx.arc(p - (h / 2), h / 2, h / 2, 3 / 2 *Math.PI,Math.PI / 2); ctx.lineTo(h / 2, h); ctx.fillStyle = '#FF1700'; ctx.fill(); }
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <canvas id="progressBar" width="750" height="120"> </canvas>

Now, we are all good to wrap this up:

 const canvas = $("#progressBar"); const ctx = canvas.get(0).getContext("2d"); const canvasWidth = ctx.canvas.width; const canvasHeight = ctx.canvas.height; class progressBar { constructor(dimension, color, percentage){ ({x: this.x, y: this.y, width: this.w, height: this.h} = dimension); this.color = color; this.percentage = percentage / 100; this.p; } static clear(){ ctx.clearRect(0, 0, canvasWidth, canvasHeight); } draw(){ // Visualize ------- this.visualize(); // ----------------- this.p = this.percentage * this.w; if(this.p <= this.h){ ctx.beginPath(); ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI - Math.acos((this.h - this.p) / this.h), Math.PI + Math.acos((this.h - this.p) / this.h)); ctx.save(); ctx.scale(-1, 1); ctx.arc((this.h / 2) - this.p - this.x, this.h / 2 + this.y, this.h / 2, Math.PI - Math.acos((this.h - this.p) / this.h), Math.PI + Math.acos((this.h - this.p) / this.h)); ctx.restore(); ctx.closePath(); } else { ctx.beginPath(); ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 *Math.PI); ctx.lineTo(this.p - this.h + this.x, 0 + this.y); ctx.arc(this.p - (this.h / 2) + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 * Math.PI, Math.PI / 2); ctx.lineTo(this.h / 2 + this.x, this.h + this.y); ctx.closePath(); } ctx.fillStyle = this.color; ctx.fill(); } visualize(){ if (wholeprogressbar.checked === true){ this.showWholeProgressBar(); } } showWholeProgressBar(){ ctx.beginPath(); ctx.arc(this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, Math.PI / 2, 3 / 2 * Math.PI); ctx.lineTo(this.w - this.h + this.x, 0 + this.y); ctx.arc(this.w - this.h / 2 + this.x, this.h / 2 + this.y, this.h / 2, 3 / 2 *Math.PI, Math.PI / 2); ctx.lineTo(this.h / 2 + this.x, this.h + this.y); ctx.strokeStyle = '#000000'; ctx.stroke(); ctx.closePath(); } get PPercentage(){ return this.percentage * 100; } set PPercentage(x){ this.percentage = x / 100; } } // We create new progress bars progressbar2 = new progressBar({x: 10, y: 10, width: 400, height: 35}, "#FF1700", 50); // progressbar2.draw(); ---> No need coz we draw them later progressbar = new progressBar({x: 10, y: 60, width: 400, height: 35}, "#FF1700", 0); // progressbar.draw(); ---> No need coz we draw them later // For showing the current percentage (just for example) setInterval(function() { let currentPercentage = progressbar.PPercentage; document.getElementById("percentage").innerHTML = `${Math.round(currentPercentage)} %`; }, 20); // We draw the progress-bars (just for example, one fix at 50% and one moving on a range from 0 to 100 %) let i=0; setInterval(function() { const start = 0; const end = 100; const step = 0.3; progressbar.PPercentage = i * step; progressBar.clear(); progressbar.draw(); progressbar2.draw(); i++; if(progressbar.PPercentage > end){ i = start; } }, 20);
 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <canvas id="progressBar" width="420" height="100"></canvas> <div> <p> Progression at <span id="percentage"></span></p> <input type="checkbox" id="wholeprogressbar" name="wholeprogressbar" onclick="progressbar.draw()"> <label for="wholeprogressbar">Visualize all the progress bar (100%)</label> </div>

EDIT :

To create a progress bar, you just have to create a new instance

progressbar = new progressBar({x: PositionXinTheCanvas, y: PositionYinTheCanvas, width: WidthOfTheProgressBar, height: HeightOfTheProgressBar}, "ColorOfTheProgressBar", CurrentProgression);

..and then draw it

progressbar.draw();

If you need to clear the canvas, call the clear() method. You will need it if you want animate the progress bar. As it's a static method, you need to call it on the class progressBar :

progressBar.clear();

I started fixing your code, and ended up rewriting it. Here's some important things about how to approach this better.

  1. You should really make a function for drawing a line at a specific position and length. Then you have a space to do all the math you need cleanly for a single line at a time.
function drawLine(x, y, length) { /* ... */ }
  1. If you want your line to be 40 pixels long, but corner radius is 40, then then you want to stroke a single point of zero length and width, with a radius of 20, so that the overall width is 40 and the circle is smaller.
  // Get length of line that will be stroked
  let innerLength = length - cornerRadius * 2

  // If the line would have a length less than zero, set the length to zero.
  if (innerLength < 0) innerLength = 0

  // If the innerLength is less than the corner diameter, reduce the corner radius to fit.
  let actualCornerRadius = cornerRadius
  if (length < cornerRadius * 2) {
    actualCornerRadius = length / 2
  }
  1. Since you are drawing a stroked line, and not a rectangle, it simplifies some math to be able to just draw a line from the start point to the end point.
  // Find the left and right endpoints of the inner line.
  const leftX = x + actualCornerRadius
  const rightX = leftX + innerLength

  // Draw the path and then stroke it.
  ctx.beginPath()
  ctx.moveTo(leftX, y)
  ctx.lineTo(rightX, y)
  ctx.stroke()
  1. Lastly, to put a rounded stroke on an open path stroke, you just need to set the lineCap property of the context to 'round' .
  ctx.lineCap = "round";

Click here for a working demo .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM