Вычисление концентрической дуги в холсте

Я пытаюсь рассчитать 2 концентрические дуги (кубический Безье) из заданной дуги (квадратный Безье). Я подумал, что могу рассчитать контрольные точки для кубика в масштабах 1/3 и 2/3, но это не совсем совпадает.

  var u = 1 / 3; // fraction of curve where Px1 and Py1 are 
  var v = 2 / 3; // fraction of curve where Px2 and Py2 are 
  //Calculate control points (Cx1, Cy1, Cx2, Cy2)
  var a = 3 * (1 - u) * (1 - u) * u;
  var b = 3 * (1 - u) * u * u;
  var c = 3 * (1 - v) * (1 - v) * v;
  var d = 3 * (1 - v) * v * v;
  var det = a * d - b * c;
  var Qx1 = Px1 - ((1 - u) * (1 - u) * (1 - u) * Px0 + u * u * u * Px3);
  var Qy1 = Py1 - ((1 - u) * (1 - u) * (1 - u) * Py0 + u * u * u * Py3);
  var Qx2 = Px2 - ((1 - v) * (1 - v) * (1 - v) * Px0 + v * v * v * Px3);
  var Qy2 = Py2 - ((1 - v) * (1 - v) * (1 - v) * Py0 + v * v * v * Py3);
  var Cx1 = (d * Qx1 - b * Qx2) / det;
  var Cy1 = (d * Qy1 - b * Qy2) / det;
  var Cx2 = ((-c) * Qx1 + a * Qx2) / det;
  var Cy2 = ((-c) * Qy1 + a * Qy2) / det;
  ctx.beginPath();
  ctx.moveTo(Px0, Py0);
  ctx.bezierCurveTo(Cx1, Cy1, Cx2, Cy2, Px3, Py3);
  ctx.strokeStyle = "#0000FF";
  ctx.stroke();

Контрольные точки тоже зависят от радиуса дуги или что-то совсем другое? Является ли куб Безье хорошим вариантом для рисования концентрической дуги? Квадратичный безье определенно не работает, а кубический определенно приблизил меня к тому, что мне нужно.

Вот ссылка: http://codepen.io/davidreed0/full/zGqPxQ/

Используйте ползунок положения, чтобы переместить эллипс.


person dreed75    schedule 01.06.2015    source источник
comment
Вы используете 2 разных набора в значительной степени непримиримых терминов: кубические и квадратичные кривые Безье относятся к изогнутым путям, а дуга и радиус относятся к [полу-]круговым путям. Эти виды кривых не могут представлять круговые пути и наоборот. Пожалуйста, уточните, что вы пытаетесь сделать. :-)   -  person markE    schedule 01.06.2015
comment
Почему бы просто не использовать arc() и не рассчитать начальный/конечный углы?   -  person    schedule 01.06.2015
comment
@ K3N, вот о чем я думал. Вы понимаете, чего хочет вопрос... Мне непонятно? :-/   -  person markE    schedule 01.06.2015
comment
@markE Я не совсем уверен, я предполагаю, что он хочет, чтобы центры синего и зеленого выровнялись (концентрически), и что синий покрывает/очерчивает/встраивает (?) зеленый эллипс, но я не знаю, есть ли у Безье являются требованием. Я думаю, что с дугами (или, возможно, с новым эллипсом) будет проще и точнее работать.   -  person    schedule 01.06.2015
comment
Я использую дугу для описания кривой. Извиняюсь. Это не настоящая дуга. Квадратичное Безье задано, то есть я не могу его изменить. Я пытаюсь сопоставить это с кривой вверху и кривой внизу. Очевидно, я не могу просто нарисовать другую квадратичную кривую выше и ниже, потому что кривая выше или ниже меньше или больше данной средней кривой.   -  person dreed75    schedule 01.06.2015


Ответы (1)


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

Основные шаги:

  • На закадровом холсте:
  • Определите толщину линии с радиусом, установленным в зеленой области
  • Определить круглые заглушки для строки
  • Нарисуйте линию Безье сплошным цветом
  • Нарисуйте результат на основном холсте с различными смещениями относительно толщины синей линии.
  • Очистите центр, и у вас будет синий контур.
  • Реализуйте ручной Безье, чтобы вы могли рисовать зеленую дугу/эллипс в любой точке этой фигуры.

Радиус/диаметр можно увеличить. Если вам нужен переменный радиус, вы можете просто использовать формулу Безье, чтобы вместо этого построить серию синих дуг друг над другом.

Доказательство концепции

Это покажет процесс шаг за шагом.

Шаг 1

На закадровом холсте (показан на экране здесь для демонстрации, мы переключимся на следующем шаге):

step1snap

var c = document.querySelector("canvas"),
    ctx = c.getContext("2d"),
    dia = 90;                                        // diameter of graphics

ctx.strokeStyle = "blue";                            // color
ctx.lineWidth = dia;                                 // line-width = dia
ctx.lineCap = "round";                               // round caps

// draw bezier (quadratic, one control point)
ctx.moveTo(dia, dia);
ctx.quadraticCurveTo(300, 230, c.width - dia, dia);
ctx.stroke();
<canvas width=600 height=300></canvas>

Сделанный. Теперь у нас есть основная форма. Отрегулируйте точки по мере необходимости.

Шаг 2

Поскольку теперь у нас есть основная форма, мы создадим контур, используя эту форму:

  • Нарисуйте это на основном холсте, сместите его круг (например, 8-е положение вокруг основной области)
  • Выбить центр с помощью комп. режим назначения-выход, чтобы оставить только контур

step2snap

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    dia = 90;

co.width = c.width;
co.height = c.height;

ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(dia, dia);
ctxo.quadraticCurveTo(300, 230, c.width - dia, dia);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;

for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);
<canvas width=600 height=300></canvas>

Шаг 3

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

Единственный расчет, который нам нужен здесь, - это квадратичный Безье, где мы подставляем t в диапазоне [0, 1], чтобы получить точку:

function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}

Результат будет (используя значения ближе к исходному codepen):

step3snap

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    radius = 150,
    dia = radius * 2;

co.width = c.width;
co.height = c.height;

ctxo.translate(2,2);           // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);

// back-up result by reusing off-screen canvas
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);

// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over";  // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";

var t = 0, dlt = 0.01;

(function loop(){
  
  ctx.clearRect(0, 0, c.width, c.height);
  t += dlt;
  
  // calc position based on t [0, 1] and the same points as for the blue
  var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
  
  // draw the arc
  ctx.beginPath();
  ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
  ctx.fill();
  
  // draw center line
  ctx.beginPath();
  ctx.moveTo(radius, radius);
  ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
  ctx.stroke();

  // draw blue outline on top
  ctx.drawImage(co, 0, 0);
  
  if (t <0  || t >= 1) dlt = -dlt;  // ping-pong for demo
  requestAnimationFrame(loop);
})();

// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}
<canvas width=600 height=600></canvas>

Пример использования оси не 1:1 по масштабу:

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    radius = 150,
    dia = radius * 2;

co.width = c.width;
co.height = c.height;

ctxo.translate(2,2);           // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 2, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);

// back-up result by reusing off-screen canvas
ctxo.setTransform(1,0,0,1,0,0);  // remove scale
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);

// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over";  // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";

ctx.scale(1, 0.4);            // create ellipse

var t = 0, dlt = 0.01;

(function loop(){
  
  ctx.clearRect(0, 0, c.width, c.height * 1 / 0.4);
  t += dlt;
  
  // calc position based on t [0, 1] and the same points as for the blue
  var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
  
  // draw the arc
  ctx.beginPath();
  ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
  ctx.fill();
  
  // draw center line
  ctx.beginPath();
  ctx.moveTo(radius, radius);
  ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
  ctx.stroke();

  // draw blue outline on top
  ctx.drawImage(co, 0, 0);
  
  if (t <0  || t >= 1) dlt = -dlt;  // ping-pong for demo
  requestAnimationFrame(loop);
})();

// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}
<canvas width=600 height=600></canvas>

person Community    schedule 01.06.2015
comment
Ух ты! Спасибо за подробный ответ! Мне это очень нравится, и я могу переключиться на эту идею. Это прекрасно решило бы мою проблему, если бы оси были равны. В моем примере, который был не совсем ясен, оси x и y немного отличаются. В реальном мире они могут еще больше отличаться друг от друга с коэффициентом 2. Я, вероятно, отмечу этот ответ как правильный, поскольку это полезный способ решить проблему и поможет большинству людей с похожей проблемой. Я бы хотел, чтобы вы посмотрели на мой модифицированный пример, чтобы понять, что я имею в виду по поводу разных осей: codepen.io /davidreed0/full/GJrVev - person dreed75; 02.06.2015
comment
Я могу просто заставить свои оси быть равными, и тогда я смогу полностью использовать ваш ответ для своей проблемы. - person dreed75; 02.06.2015
comment
@ dreed75, возможно, удастся обойтись простым масштабированием, примененным к холсту для одной из осей. Существует также возможность использовать эллипс (это часть стандарта холста, но не все браузеры его поддерживают), используя встроенный или пользовательский. Просто визуализируйте контур, следуя t-значению с высоким разрешением. - person ; 02.06.2015
comment
@dreed75 добавил пример для этого внизу (свернутый), но лучший подход — нарисовать реальный эллипс, так как это сохранит правильную внутреннюю дугу) - person ; 02.06.2015
comment
Попался. Спасибо! Мне также нравится ваш getQuadraticPoint(). Я пытался найти способ сделать это на холсте. Это была единственная причина, по которой я использовал svg для этой квадратичной кривой Безье, поскольку у нее была встроенная функция для получения точки в определенном проценте. - person dreed75; 02.06.2015
comment
@ dreed75 ах да, я сделал это для псевдоконтекста (ретро-графика), поэтому мне пришлось преобразовать формулу в JS :) - person ; 02.06.2015