Canvas прямоугольник (индикатор выполнения) с закругленными концами - проблема с низкими значениями - PullRequest
2 голосов
/ 19 марта 2020

Я пытаюсь создать прямоугольник с шириной, зависящей от процента, у меня все получалось идеально, пока я не проверил что-то с 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>

КОД:

Ответы [ 2 ]

2 голосов
/ 20 марта 2020

То, как вы его реализуете, всегда будет иметь какое-то значение, когда вы находитесь на 0%. Если вы хотите иметь ничего, когда оно 0% и что оно остается неизменным при росте процента, вы не хотите использовать ctx.lineJoin = "round"

В качестве обходного пути, вместо этого вы можете нарисовать закругленный угол, используя метод ар c () .

На arc(x, y, radius, startAngle, endAngle) мы знаем x = r, y = r и radius = r

calcul of the angle to draw

Нам нужна только некоторая геометрия Вычислить, чтобы получить необходимые значения startAngle (α) и endAngle (α + Δ).

С тригонометрией c функция косинус , мы имеем 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 < rp < 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>

Теперь, если мы хотим, чтобы этот вид был скорее (красная часть, мы хотим получить серую часть). Метод состоит в том, чтобы сделать то же самое, но для половины прогрессии, а затем повторить ту же фигуру в симметрии (заштрихованная область).

enter image description here

Рисовать форму symetri c мы будем использовать ctx.scale(-1, 1) и с save() restore() методами. Положение x для центра второго ar c будет - (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 <= h после того, как нам нужно изменить наш код, принимая во внимание прямоугольную часть angular. Мы будем использовать 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(). Он понадобится вам, если вы хотите анимировать индикатор выполнения. Так как это метод stati c, вам нужно вызвать его в классе progressBar:

progressBar.clear();
1 голос
/ 19 марта 2020

Я начал исправлять ваш код и переписал его. Вот несколько важных вещей о том, как лучше подойти к этому.

  1. Вы действительно должны сделать функцию для рисования линии с указанными c позицией и длиной. Тогда у вас есть место, чтобы выполнить всю математику, в которой вы нуждаетесь, по одной строке за раз.
function drawLine(x, y, length) { /* ... */ }
Если вы хотите, чтобы длина вашей линии составляла 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
  }
Поскольку вы рисуете штриховую линию, а не прямоугольник, это упрощает некоторые математические операции, чтобы можно было просто нарисовать линию от начальной точки до конечной точки.
  // 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()
Наконец, чтобы поместить обводку в обводку открытого пути, вам просто нужно установить свойство lineCap контекста на 'round'.
  ctx.lineCap = "round";

Нажмите здесь для рабочего демо .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...