【问题标题】:Canvas rectangle (progress bar) with rounded ends - Issue with low values带圆角的画布矩形(进度条) - 低值问题
【发布时间】:2020-06-30 18:40:12
【问题描述】:

我正在尝试制作一个宽度取决于百分比的矩形,我让它完美地工作,直到我测试了 0% 的东西。

我希望它在 0% 时消失,但当我选择圆角时,它会出现最小宽度。对于较低的百分比数字,同样的问题也很明显,据我所知,如果百分比低于 6%,它会以相反的方式推动对象,此时矩形变成一个圆形并且不能变得更小。有解决方法吗?我一心一意看它的样子,目前,只需要解决这个问题。

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>

代码:

【问题讨论】:

  • 如果线条宽度小于笔划直径,您需要进行数学计算以使您的笔划变细。如果是,那么矩形宽度应该为零并且笔划宽度应该更小。另外,你确定你不想用 CSS 来代替吗?
  • 抱歉,应该提到这是针对 Discord 机器人的,我只使用该代码 sn-p 以便查看者可以看到问题。

标签: javascript html canvas


【解决方案1】:

当您在0% 时,您实现它的方式总是有些“重要”。如果您希望在 0% 时一无所有,并且在百分比增长时保持一致,您不想使用 ctx.lineJoin = "round"

作为一种解决方法,您可以使用arc() 方法来绘制圆角。

arc(x, y, radius, startAngle, endAngle) 上,我们知道x = ry = rradius = r

我们只需要一些几何计算即可获得所需的值startAngle (α) 和 endAngle (α+Δ)。

使用三角函数cosine,我们有Math.cos(θ) = (r - p) / rθ = Math.acos((r - p) / r)

我们有 α = Math.PI - θ 并且我们知道 Δ = 2 * θ(α+Δ) = Math.PI + θ

最后:

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

在我们的例子中,r = h /2 所以当p &lt; rp &lt; h / 2 时,这给了我们:

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>

现在,如果我们想要这个外观(红色部分,我们想要摆脱灰色部分)。方法是做同样的事情,但进行一半,然后对称地重复相同的图形(阴影区域)。

要绘制对称形状,我们将使用ctx.scale(-1, 1)save() restore() 方法。第二个圆弧中心的 x 位置将是 - (r - p)-((h / 2) - p) 就像我们将在水平对称中工作一样,最终将是 (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>

直到p &lt;= h 在我们需要更改我们的代码并考虑到矩形部分之后,这将是正确的。我们将使用 if...else 来做到这一点。

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>

现在,我们都可以结束了:

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>

编辑:

要创建进度条,您只需创建一个新实例

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

..然后画出来

progressbar.draw();

如果您需要清除画布,请调用clear() 方法。如果您想为进度条设置动画,您将需要它。因为它是一个静态方法,所以你需要在 progressBar 类上调用它:

progressBar.clear();

【讨论】:

  • 我什至不会假装我理解这些。研究了一个多小时,它的深思熟虑和令人印象深刻仍然让我感到惊讶。太棒了。虽然我在设置 x 和 y 位置时遇到了一些麻烦,但这是因为我被这段代码吓了一跳。如果您能指出我正确的方向,我将不胜感激。老实说,干得好,如果我能颁奖的话,我会的。
  • @RagnarLothbrok 我的错,我忘了考虑这些,我现在更正并编辑了代码
  • @RagnarLothbrok 我在答案末尾添加了“如何使用”,希望有助于澄清
【解决方案2】:

我开始修复你的代码,最后重写了它。以下是有关如何更好地解决此问题的一些重要事项。

  1. 您确实应该创建一个函数来在特定位置和长度绘制一条线。然后,您就有了一个空间,可以一次干净地完成所有需要的数学运算。
function drawLine(x, y, length) { /* ... */ }
  1. 如果你想让你的线长 40 像素,而圆角半径是 40,那么你想描边一个长度和宽度都为零的点,半径为 20,这样总宽度是 40,而圆圈变小了。
  // 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. 由于您要绘制的是描边线,而不是矩形,它简化了一些数学运算,以便能够仅绘制一条从起点到终点的线。
  // 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. 最后,要将圆角笔划放在开放路径笔划上,只需将上下文的lineCap 属性设置为'round'
  ctx.lineCap = "round";

Click here for a working demo.

【讨论】:

  • 这是一个更好的方法!谢谢,不过问题依旧。我需要 X 位置保持不变,这个栏在一个盒子里面,它离开盒子的同样问题仍然存在,请参阅:imgur.com/a/ZiGtlmH
  • 好吧,改变数学,让它这样工作。提示:使用设置leftX 变量的数学。
  • 这并不能解决任何问题(改写为“如何使 lineCap 在定义的坐标处圆端”)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-06
  • 2017-10-21
  • 1970-01-01
  • 1970-01-01
  • 2017-12-04
  • 2015-03-16
相关资源
最近更新 更多