2

I'm writing a program for users to quickly approximate the shape of a curved or straight bendable pipe with a quadratic bezier curve. They will enter the start and end points and provide the length of the pipe. An analogy would be someone staking down the start and end of a rope of known length.

In order to determine the shape of the curve they would click a point along the edge of the displayed pipe/rope shape where it "bows the furthest" from the start/end points. This is shown by the red cross in the diagram below. I'm showing the red cross not on the pipe to indicate that user's are allowed/likely to make an error in this selection. It seems unlikely they could select a point exactly on the curve.

pipe showing start/end points as grey circles and a red cross positioned roughly by the user

Once the user has selected this point the result will be displayed as an SVG quadratic bezier: https://www.w3.org/TR/SVG/paths.html#PathDataQuadraticBezierCommands

Given that the start and end points are two of the quadratic's control points. I'd like to calculate the third control point of the quadratic bezier assuming the start/end points and length are accurate. In other words, the red point should "tension" the curve in the direction of the third control point.

I'm imagining I could initially calculate the third control point assuming the user's click (red cross) is actually on the quadratic curve using this algorithm.

Given the three control points that define the curve I could then calculate the length of the curve using this equation.

If the curve's length is within an error tolerance I'm done. If not, I could iteratively move the third control point, recalculate the length, repeating until the control point puts the length within the desired tolerance.

How would I adjust the third control point's x,y location while still maintaining the shape of the curve?

Or is there a better solution overall?

Clarifications:

The domain here is the sport of dog agility. The intent is to allow users to identify "tube tunnel" shapes on an existing competition course diagram image. The course diagrams will give the tunnel lengths and the start and end locations as specified by the judge.

Only a very rough reproduction of the tunnel shape is required.

At dog agility events the tunnel end points will be located on the course via tape measure/measuring wheel at best w/in +/- 1 ft and then the tunnel is fully extended in a visual approximation of the shape and held in place by sandbags. No measurement of the shape is performed.

The idea for this application is to allow someone on a mobile phone to only click one point (generally) on the curve to create a visual starting point of the shape. The user will be able to further refine the shape if they desire.

There are only a few allowed tunnel lengths and shapes. Most, but not all, allowed shapes can be represented using a quadratic bezier, it will be used as a starting point while maintaining the start/end/length constraints.

saschwarz
  • 1,123
  • 2
  • 13
  • 26
  • I would imagine a **base line** between the start and end points, and a **perpendicular line** that passes through the red X. Then you can increase the curve length by moving the X away from the base line along the perpendicular line, or shorten the curve length by moving closer to the base line. – user3386109 Mar 08 '23 at 20:07
  • @user3386109 I was thinking that or moving the point along a line drawn from the control point to the center of a base line between the start and end points(?) – saschwarz Mar 08 '23 at 20:13
  • 1
    Now that you say that, I agree that the perpendicular line may not be the best choice. The center point of the base line is worth a try. Another possibility is the line that passes through the X and the [incenter of the triangle](https://en.wikipedia.org/wiki/Incenter) formed by the two endpoints and the X. – user3386109 Mar 08 '23 at 20:26
  • download some SVG editors and see how they approach drawing bezier curves. – Robert Longson Mar 08 '23 at 20:28
  • 1
    Not sure: But if actual goal of your app is to simulate bending of a **real life pipe** – the **length should be the main constant**, whereas the starting and end points should change according to the user defined bending/control point. So if you move the red cross cursor - start and end point will need to adapt to keep the same length. – herrstrietzel Mar 08 '23 at 23:52
  • 1
    Typically the "tension point" *is* the control point. – Matt Timmermans Mar 09 '23 at 00:27
  • @herrstrietzel Good point. I've added clarification on the domain for this arc. Here the start/end/length are known/constraints and the shape need only be approximated. – saschwarz Mar 09 '23 at 11:52

2 Answers2

1

For quadratic curves, you can find that red cross by finding the intersection of the tangents at the start and end point. Now, you don't have those, but you do have the approximate tangents, because close to the start and end points, a line between a point and the star/tend is approximately equal to its tangent

However, if we do that for the shape you're showing, we can see that this is very much not a quadratic Bezier curve:

bgimg.onload = () => {
  cvs.width = 400;
  cvs.height = 350;
  const ctx = cvs.getContext(`2d`);
  ctx.drawImage(bgimg, 0, 0, 400, 350);
  ctx.lineWidth = 2;
  ctx.strokeStyle = `red`;
  
  // quadratic
  ctx.beginPath();
  ctx.moveTo(58,275);
  ctx.quadraticCurveTo(104, 92, 320, 50);
  ctx.moveTo(104, 92);
  ctx.arc(104, 92, 3, 0, 2*Math.PI);
  ctx.stroke(); 
};
.hidden {
  display: none;
}
canvas {
  border:1px solid black;
}
<canvas id="cvs"></canvas>
<img id="bgimg" class="hidden" src="https://i.stack.imgur.com/ZcqMD.png">

You can, of course, use the curve-to-baseline ratio for quadratic curves to find a different control point so that the middle of the curve overlaps with your pipe:

bgimg.onload = () => {
  cvs.width = 400;
  cvs.height = 350;
  const ctx = cvs.getContext(`2d`);
  ctx.drawImage(bgimg, 0, 0, 400, 350);
  ctx.lineWidth = 2;
  ctx.strokeStyle = `red`;

  // point "c" at the baseline midpoint
  ctx.beginPath();
  ctx.arc(177, 172, 3, 0, 2*Math.PI);  
  ctx.moveTo(58,275);
  ctx.lineTo(320, 50);
  ctx.moveTo(177,172);
  ctx.lineTo(20,0);
  // point "b" is on our curve
  ctx.moveTo(128,118);
  ctx.arc(128, 118, 3, 0, 2*Math.PI);  
  // point "a" is at b - (c-b)
  const ax = 128 - (177 - 128);
  const ay = 118 - (172 - 118);
  ctx.moveTo(ax, ay);
  ctx.arc(ax, ay, 3, 0, 2*Math.PI);
  ctx.moveTo(58,275);
  ctx.quadraticCurveTo(ax, ay, 320,50)
  ctx.stroke(); 
};
.hidden {
  display: none;
}
canvas {
  border:1px solid black;
}
<canvas id="cvs"></canvas>
<img id="bgimg" class="hidden" src="https://i.stack.imgur.com/ZcqMD.png">

But then it'll be bowed out too much so you're probably going to have to use cubic Bezier curves here, because the shapes you're working with are not quadratic.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
  • @mike-pomax-kamermans thank you for such a detailed answer! I will study it. Your 2nd solution is well within the accuracy needed. "a line between a point and the start/end is approximately equal to its tangent" makes sense. But how do I find a second point near the start/end to generate the tangent when I don't yet have an equation for the arc? Are you saying to use the start/end and red cross point to generate the initial arc/length and then adjust as in your second example? To adjust should I use equation of line through control point & midpoint to adjust control point to change arc length? – saschwarz Mar 08 '23 at 22:46
  • 1
    If you have only three points and a "desired length" then yes: use the start, end, and "cross" as control point to build an initial guess, then check the arc length. Then you can move the control point closer to, or further away from the baseline to get the curve to the length it needs to be. Using the ratio like in the second snippet relies on already knowing where your on-curve point is, so if the exercise is to come up with a curve, you won't have that yet. – Mike 'Pomax' Kamermans Mar 08 '23 at 22:53
  • @mikepomaxkamermans thank you too for creating this library which will make this task so much easier! https://pomax.github.io/bezierjs/#fromPoints – saschwarz Mar 09 '23 at 14:14
  • always good to hear it's finding use. – Mike 'Pomax' Kamermans Mar 09 '23 at 15:21
1

I'd like to propose a different interpretation of the position the user sets the cross to. As Pomax has pointed out, the curve drawn by you is anything but a quadratic bezier. But, maybe intuitively, the cross sits at a point near where the tangents from the endpoints meet. I think this is an easy to grasp concept: Draw the cross to indicate where the tangents of the pipe ends should point.

That one is relatively easy to implement with a cubic Bezier, as the two control points must sit on the respective line connecting the endpoint to that target point. Move them nearer to or further from that point until the length approximates the given value.

Obviously, the position of the cross then has a limit of where it can be placed, otherwise the control points would have to go beyond the target point. But unlike your approach, the user can correct that error by choosing a point that leaves more "space" for the pipe length - something that can be done intuitively.

const pipeLength = 160;

const bezier = document.querySelector('#bezier');
const start = [0, 0];
const end = [100, 100];
const target = [90, 10];

let curveLength = Math.hypot(target[0] - start[0], target[1] - start[1]) +
                  Math.hypot(target[0] - end[0], target[1] - end[1]);
let part = 1, low = 0, high = 1, i= 0;

while ((i++ < 20) && Math.abs(curveLength - pipeLength) > 1) {
  const control1 = [
    target[0] * part + start[0] * (1 - part),
    target[1] * part + start[1] * (1 - part)
  ];
  const control2 = [
    target[0] * part + end[0] * (1 - part),
    target[1] * part + end[1] * (1 - part)
  ];
  
  bezier.setAttribute('d', 'M ' + [...start, 'C', ...control1, ...control2, ...end].join(' '));
  
  curveLength = bezier.getTotalLength();
  
  if (curveLength > pipeLength) {
    high = part;
  } else {
    low = part;
  }
  part = (high + low) / 2;
}
<svg viewBox="-10 -10 120 120" height = "100vh">
  <path d="M80,10h20M90,0v20" stroke="red" />
  <path d="M0,0 90,10 100,100" fill="none" stroke="red" stroke-dasharray="3 3" />
  <path id="bezier" d="M0,0 C 90,10 90,10 100,100" fill="none" stroke="blue" />
</svg>
ccprog
  • 20,308
  • 4
  • 27
  • 44
  • Thank you for the detailed solution. This is an interesting suggestion and would greatly simplify subsequent editing when the shape can't be represented as a quadratic. I just wonder if my user base would understand clicking the intersection of the tangents versus "click on the tunnel itself". – saschwarz Mar 09 '23 at 12:05