1

I would like to create a rounded edge for a corner where the user can specify the corner's radius and number of subdivisions. To find these new points, I will have three vectors: p, p1, & p2 and the subsequent line segments: PP1 and PP2. Desired Result

I found a post on Stack Overflow that has potential solutions, but the answers are for C# and I'm working in Javascript. I have included my interpretation of the code.

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var radius_slider = document.getElementById("radius");
var subdivs_slider = document.getElementById("subdivs");
var generateBtn = document.getElementById("generate");
var radiusReadout = document.getElementById("radiusReadout");
var subdivReadout = document.getElementById("subdivReadout");
    

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

function DrawRoundedCorner(angularPoint, p1, p2, radius, subdivisions) {
  //Vector 1
  dx1 = angularPoint.x - p1.x;
  dy1 = angularPoint.y - p1.y;
  //Vector 2
  dx2 = angularPoint.x - p2.x;
  dy2 = angularPoint.y - p2.y;
  //Angle between vector 1 and vector 2 divided by 2
  angle = (Math.atan2(dy1, dx1) - Math.atan2(dy2, dx2)) / 2;
  // The length of segment between angular point and the
  // points of intersection with the circle of a given radius
  tan = Math.abs(Math.tan(angle));
  segment = radius / tan;
  //Check the segment
  length1 = GetLength(dx1, dy1);
  length2 = GetLength(dx2, dy2);
  length = Math.min(length1, length2);
  if (segment > length) {
    segment = length;
    radius = length * tan;
  }
  // Points of intersection are calculated by the proportion between 
  // the coordinates of the vector, length of vector and the length of the segment.
  var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1);
  var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2);
  // Calculation of the coordinates of the circle 
  // center by the addition of angular vectors.
  dx = angularPoint.x * 2 - p1Cross.x - p2Cross.x;
  dy = angularPoint.y * 2 - p1Cross.y - p2Cross.y;
  L = GetLength(dx, dy);
  d = GetLength(segment, radius);
  var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy);
  //StartAngle and EndAngle of arc
  var startAngle = Math.atan2(p1Cross.y - circlePoint.y, p1Cross.x - circlePoint.x);
  var endAngle = Math.atan2(p2Cross.y - circlePoint.y, p2Cross.x - circlePoint.x);
  //Sweep angle
  var sweepAngle = endAngle - startAngle;
  //Some additional checks
  if (sweepAngle < 0) {
    startAngle = endAngle;
    sweepAngle = -sweepAngle;
  }
  if (sweepAngle > Math.PI)
    sweepAngle = Math.PI - sweepAngle;
  ctx.lineWidth = 2;
  ctx.strokeStyle = '#000000';
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);
  ctx.lineTo(p1Cross.x, p1Cross.y);
  ctx.moveTo(p2.x, p2.y);
  ctx.lineTo(p2Cross.x, p2Cross.y);
  ctx.lineWidth = this.linewidth;
  ctx.strokeStyle = this.color;
  ctx.stroke();
  var left = circlePoint.x - radius;
  var top = circlePoint.y - radius;
  var diameter = 2 * radius;
  var degreeFactor = Math.PI*subdivisions;
  /*
  ctx.beginPath();
  ctx.arc(left,top,diameter,(startAngle * degreeFactor),(endAngle*degreeFactor));
  ctx.strokeStyle = "#00FF00";
  ctx.stroke();
  ctx.strokeStyle = "#000000";
  */
  //To get points of arc you can use this:
  pointsCount = Math.abs(sweepAngle * degreeFactor);
  sign = Math.sign(sweepAngle);
  points = [];
// you can remove the Math.ceil**0.5 thing and just have it as pointsCount - 1 or something.
  for (i = 0; i < pointsCount + (Math.round(subdivisions) % 2 == 0 ? 1 : 0); i++) {
    var pointX = circlePoint.x + Math.cos(startAngle + ((i/pointsCount) * sign * 2)) * radius;
    var pointY = circlePoint.y + Math.sin(startAngle + ((i/pointsCount) * sign * 2)) * radius;
    points[i] = new Point(pointX, pointY);
    ctx.beginPath();
    ctx.arc(points[i].x, points[i].y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = "#00FF00";
    ctx.fill();
  }
}
function GetLength(dx, dy) {
  return Math.sqrt(dx * dx + dy * dy);
}
function GetProportionPoint(point, segment, length, dx, dy) {
  factor = segment / length;
  return new Point(point.x - dx * factor, point.y - dy * factor);
}

function randomNumber(min, max) {  
    return Math.random() * (max - min) + min; 
}  

var p = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));
var p1 = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));
var p2 = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));

var dotRadius = 5;
function drawAll(radius, subdivisions) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  DrawRoundedCorner(p, p1, p2, radius, subdivisions);
  ctx.font = '18px sans-serif';
  ctx.fillText('P', p.x - 20, p.y + 5);
  ctx.beginPath();
  ctx.arc(p.x, p.y, dotRadius, 0, 2 * Math.PI);
  ctx.fillStyle = "#00FF00";
  ctx.fill();
  ctx.fillStyle = "#000000";
  ctx.fillText('P1', p1.x + 10, p1.y + 6);
  ctx.beginPath();
  ctx.arc(p1.x, p1.y, dotRadius, 0, 2 * Math.PI);
  ctx.fill();
  ctx.beginPath();
  ctx.fillText('P2', p2.x + 10, p2.y + 6);
  ctx.arc(p2.x, p2.y, dotRadius, 0, 2 * Math.PI);
  ctx.fill();
}
let radiusValue = radius_slider.value;
let subdivsValue = subdivs_slider.value;
function handleSlider(){
  radiusValue = radius_slider.value;
  subdivsValue = subdivs_slider.value;
  radiusReadout.innerText = radius_slider.value;
  subdivReadout.innerText = subdivs_slider.value;
  drawAll(radiusValue, subdivsValue);
}
radius_slider.oninput = handleSlider;
subdivs_slider.oninput = handleSlider;
generateBtn.onclick = function()
{
  p = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));
  p1 = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));
  p2 = new Point(randomNumber(0,canvas.width),randomNumber(0,canvas.height));
  drawAll(radiusValue, subdivsValue);
}
drawAll(radiusValue, subdivsValue);
radiusReadout.innerText = radius_slider.value;
subdivReadout.innerText = subdivs_slider.value;
body {
  font-family: sans-serif;
  text-align: center;
}

#canvas {
  width: 150px;
  height: 150px;
  border: 1px solid rgb(200, 200, 200);
}
<canvas id="canvas" width=150 height=150></canvas><div>
<label for="radius">Radius</label>
<input type="range" min="1" max="100" value="10" class="slider" id="radius"><span id="radiusReadout"></span>
</div>
<div>
<label for="subdivs">Subdivs</label>
<input type="range" min="1" max="16" value="2" class="slider" id="subdivs"><span id="subdivReadout"></span>
</div>
<div>
<button id="generate" type="button">Generate New Points</button>     
</div>
I have two questions:
  1. Why don't the arc points consistently connect the lines in my version? They are sometimes correct, but often under/overshoot.
  2. How would I draw the arc? I can draw the points, but I couldn't understand how to translate this to HTML Canvas:
graphics.DrawArc(pen, left, top, diameter, diameter, 
                     (float)(startAngle * degreeFactor), 
                     (float)(sweepAngle * degreeFactor));

Here is a larger version on Codepen. Thank you!

  • It's possible to add a live code snippet similar to CodePen embedded in your question, and this can make it easier to answer. Could you do that instead? – George Oct 10 '20 at 01:21
  • With the `ctx.arc` function, you can draw a circle, or a partial circle (e.g. an arc). The last 2 arguments specify where the arc begins and ends, in radians (from 0 to 2PI). – George Oct 10 '20 at 01:24
  • @George Thanks for the tip, I added a live snippet. I am familiar with `ctx.arc` (it's commented out in the snippet because) I'm not sure how to get it working the same way as the c# example that uses `DrawArc`. – Dr. Pontchartrain Oct 10 '20 at 01:30
  • OK so reading the code I can see the arc in your drawing is actually created by looping all points in the arc and drawing a circle for each point (using `.arc`). That is pretty mad. What do you mean 'subdivide' ? I'm not familiar with the geometry of this but I can give it a try. – George Oct 10 '20 at 01:37
  • Are you certain you need subdivide? A single arc would be accurate without any need for subdivision. – George Oct 10 '20 at 01:38
  • @George Thanks for the reply. I want to draw the arc with `ctx.arc` as well. I skipped that bit because I couldn't get it working. In the original c# post, the author added the arc points code after the original sample. I implemented it in my version because I'm going to allow the user to specify how many points they want to complete that arc. I won't be using `.arc` in production...it was just to get see the arc points code working. I don't understand why they don't extend all the way. And I will remove many of these points to get the level of subdivision specified. – Dr. Pontchartrain Oct 10 '20 at 01:43
  • To be honest, the easiest solution would to use a bezier curve, which is a built in 2d canvas function too. Give a min.. – George Oct 10 '20 at 01:46
  • @George This isn't code for a Javascript app so the canvas code isn't what's important to me. The ability to generate the points is most important because I'll eventually be translating this math to a 3D application. I appreciate your help with the bezier curve, but the points are what matter. – Dr. Pontchartrain Oct 10 '20 at 01:47
  • OK well you can generate points along a bezier curve easily. – George Oct 10 '20 at 01:47
  • @George Okay, I didn't want you to go to any trouble. Thank you. Would a bezier corner have a dynamic corner radius based on a circle like the snippet? Again, I wouldn't be really using the bezier curve for anything more than placement of the points, so any methods specific to HTML canvas won't be useful for my ultimate purpose. – Dr. Pontchartrain Oct 10 '20 at 01:49
  • Theoretically as in your gif, you would keep the control point `P` static, but move `P1` and `P2` towards or away from `P` to be able to control the radius of the rounded corner. – George Oct 10 '20 at 01:51
  • @George, P1 & P2 are not moving in the .gif or the snippet. They will be static. In the .gif, the green lines represent new line segments from the arc points. – Dr. Pontchartrain Oct 10 '20 at 01:55
  • yeah maybe bezier is overkill – George Oct 10 '20 at 01:57

1 Answers1

1

OK, I can't guarantee this is accurate because I didn't write the code from scratch and don't fully understand it, but i've gone ahead and got it seemingly working, and have included the subdivision thing.

    var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var radius_slider = document.getElementById("radius");
var subdivs_slider = document.getElementById("subdivs");
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}
function DrawRoundedCorner(angularPoint, p1, p2, radius, subdivisions) {
  //Vector 1
  dx1 = angularPoint.x - p1.x;
  dy1 = angularPoint.y - p1.y;
  //Vector 2
  dx2 = angularPoint.x - p2.x;
  dy2 = angularPoint.y - p2.y;
  //Angle between vector 1 and vector 2 divided by 2
  angle = (Math.atan2(dy1, dx1) - Math.atan2(dy2, dx2)) / 2;
  // The length of segment between angular point and the
  // points of intersection with the circle of a given radius
  tan = Math.abs(Math.tan(angle));
  segment = radius / tan;
  //Check the segment
  length1 = GetLength(dx1, dy1);
  length2 = GetLength(dx2, dy2);
  length = Math.min(length1, length2);
  if (segment > length) {
    segment = length;
    radius = length * tan;
  }
  // Points of intersection are calculated by the proportion between 
  // the coordinates of the vector, length of vector and the length of the segment.
  var p1Cross = GetProportionPoint(angularPoint, segment, length1, dx1, dy1);
  var p2Cross = GetProportionPoint(angularPoint, segment, length2, dx2, dy2);
  // Calculation of the coordinates of the circle 
  // center by the addition of angular vectors.
  dx = angularPoint.x * 2 - p1Cross.x - p2Cross.x;
  dy = angularPoint.y * 2 - p1Cross.y - p2Cross.y;
  L = GetLength(dx, dy);
  d = GetLength(segment, radius);
  var circlePoint = GetProportionPoint(angularPoint, d, L, dx, dy);
  //StartAngle and EndAngle of arc
  var startAngle = Math.atan2(p1Cross.y - circlePoint.y, p1Cross.x - circlePoint.x);
  var endAngle = Math.atan2(p2Cross.y - circlePoint.y, p2Cross.x - circlePoint.x);
  //Sweep angle
  var sweepAngle = endAngle - startAngle;
  //Some additional checks
  if (sweepAngle < 0) {
    startAngle = endAngle;
    sweepAngle = -sweepAngle;
  }
  if (sweepAngle > Math.PI)
    sweepAngle = Math.PI - sweepAngle;
  ctx.lineWidth = 2;
  ctx.strokeStyle = '#000000';
  ctx.beginPath();
  ctx.moveTo(p1.x, p1.y);
  ctx.lineTo(p1Cross.x, p1Cross.y);
  ctx.moveTo(p2.x, p2.y);
  ctx.lineTo(p2Cross.x, p2Cross.y);
  ctx.lineWidth = this.linewidth;
  ctx.strokeStyle = this.color;
  ctx.stroke();
  var left = circlePoint.x - radius;
  var top = circlePoint.y - radius;
  var diameter = 2 * radius;
  var degreeFactor = Math.PI*subdivisions;
  /*
  ctx.beginPath();
  ctx.arc(left,top,diameter,(startAngle * degreeFactor),(endAngle*degreeFactor));
  ctx.strokeStyle = "#00FF00";
  ctx.stroke();
  ctx.strokeStyle = "#000000";
  */
  //To get points of arc you can use this:
  pointsCount = Math.abs(sweepAngle * degreeFactor);
  sign = Math.sign(sweepAngle);
  points = [];
// you can remove the Math.ceil**0.5 thing and just have it as pointsCount - 1 or something.
  for (i = 0; i < pointsCount + (Math.round(subdivisions) % 2 == 0 ? 1 : 0); i++) {
    var pointX = circlePoint.x + Math.cos(startAngle + ((i/pointsCount) * sign * 2)) * radius;
    var pointY = circlePoint.y + Math.sin(startAngle + ((i/pointsCount) * sign * 2)) * radius;
    points[i] = new Point(pointX, pointY);
    ctx.beginPath();
    ctx.arc(points[i].x, points[i].y, 1, 0, 2 * Math.PI);
    ctx.fillStyle = "#00FF00";
    ctx.fill();
  }
}
function GetLength(dx, dy) {
  return Math.sqrt(dx * dx + dy * dy);
}
function GetProportionPoint(point, segment, length, dx, dy) {
  factor = segment / length;
  return new Point(point.x - dx * factor, point.y - dy * factor);
}

var p = new Point(50,75);
var p1 = new Point(200,25);
var p2 = new Point(225,250);

var dotRadius = 5;
function drawAll(radius, subdivisions) {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  DrawRoundedCorner(p, p1, p2, radius, subdivisions);
  ctx.font = '18px sans-serif';
  ctx.fillText('P', p.x - 20, p.y + 5);
  ctx.beginPath();
  ctx.arc(p.x, p.y, dotRadius, 0, 2 * Math.PI);
  ctx.fillStyle = "#00FF00";
  ctx.fill();
  ctx.fillStyle = "#000000";
  ctx.fillText('P1', p1.x + 10, p1.y + 6);
  ctx.beginPath();
  ctx.arc(p1.x, p1.y, dotRadius, 0, 2 * Math.PI);
  ctx.fill();
  ctx.beginPath();
  ctx.fillText('P2', p2.x + 10, p2.y + 6);
  ctx.arc(p2.x, p2.y, dotRadius, 0, 2 * Math.PI);
  ctx.fill();
}
let radiusValue = radius_slider.value;
let subdivsValue = subdivs_slider.value;
function handleSlider(){
  radiusValue = radius_slider.value;
  subdivsValue = subdivs_slider.value;
  drawAll(radiusValue, subdivsValue);
}
radius_slider.oninput = handleSlider;
subdivs_slider.oninput = handleSlider;
drawAll(radiusValue, subdivsValue);
body {
  text-align: center;
}

#canvas {
  width: 160px;
  height: 160px;
  border: 1px solid rgb(200, 200, 200);
}
<canvas id="canvas" width=160 height=160></canvas><br>
<label for="radius">Radius</label>
<input type="range" min="1" max="100" value="50" class="slider" id="radius">
<div>
<label for="subdivs">Subdivs</label>
<input type="range" min="1" max="16" value="2" class="slider" id="subdivs">
</div>
George
  • 2,330
  • 3
  • 15
  • 36
  • I greatly appreciate your effort, thank you. The subdivisions addition is fantastic! When I tried this on a larger scale, however, there is still a gap in the arcs. I don't think I can add a snippet in the comments, so here's an example of the larger scale: https://codepen.io/blastframe/pen/RwRNdWV – Dr. Pontchartrain Oct 10 '20 at 02:54
  • @DrPontchartrain OK I've fixed that, it feels hacky but it works; add 1 from pointsCount in the loop if rounded subdivisions is an even number, else keep pointsCount the same. – George Oct 10 '20 at 13:25
  • Again, I am very grateful for your help. Please don't feel like you need to address it, but that solution doesn't seem to work. If you click on 'Generate New Points' in this example you will quickly find a point arrangement that doesn't meet all the way. https://codepen.io/blastframe/pen/gOMbEQj – Dr. Pontchartrain Oct 10 '20 at 18:58