Skip to content

Instantly share code, notes, and snippets.

@arce
Last active March 26, 2020 22:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arce/9dd11df69ad73e422c7a7b106f54c78e to your computer and use it in GitHub Desktop.
Save arce/9dd11df69ad73e422c7a7b106f54c78e to your computer and use it in GitHub Desktop.
Arc by bezier
I stumbled upon this problem recently. I compiled a solution from the articles mentioned here in the form of a module.
It accepts start angle, end angle, center and radius as input.
It approximates small arcs (<= PI/2) pretty well. If you need to approximate something arcs from PI/2 to 2*PI you can always break them in parts < PI/2, calculate the according curves and join them afterward.
This solution is start and end angle order agnostic - it always picks the minor arc.
As a result you get all four points you need to define a cubic bezier curve in absolute coordinates.
I think this is best explained in code and comments:
'use strict';
module.exports = function (angleStart, angleEnd, center, radius) {
// assuming angleStart and angleEnd are in degrees
const angleStartRadians = angleStart * Math.PI / 180;
const angleEndRadians = angleEnd * Math.PI / 180;
// Finding the coordinates of the control points in a simplified case where the center of the circle is at [0,0]
const relControlPoints = getRelativeControlPoints(angleStartRadians, angleEndRadians, radius);
return {
pointStart: getPointAtAngle(angleStartRadians, center, radius),
pointEnd: getPointAtAngle(angleEndRadians, center, radius),
// To get the absolute control point coordinates we just translate by the center coordinates
controlPoint1: {
x: center.x + relControlPoints[0].x,
y: center.y + relControlPoints[0].y
},
controlPoint2: {
x: center.x + relControlPoints[1].x,
y: center.y + relControlPoints[1].y
}
};
};
function getRelativeControlPoints(angleStart, angleEnd, radius) {
// factor is the commonly reffered parameter K in the articles about arc to cubic bezier approximation
const factor = getApproximationFactor(angleStart, angleEnd);
// Distance from [0, 0] to each of the control points. Basically this is the hypotenuse of the triangle [0,0], a control point and the projection of the point on Ox
const distToCtrPoint = Math.sqrt(radius * radius * (1 + factor * factor));
// Angle between the hypotenuse and Ox for control point 1.
const angle1 = angleStart + Math.atan(factor);
// Angle between the hypotenuse and Ox for control point 2.
const angle2 = angleEnd - Math.atan(factor);
return [
{
x: Math.cos(angle1) * distToCtrPoint,
y: Math.sin(angle1) * distToCtrPoint
},
{
x: Math.cos(angle2) * distToCtrPoint,
y: Math.sin(angle2) * distToCtrPoint
}
];
}
function getPointAtAngle(angle, center, radius) {
return {
x: center.x + radius * Math.cos(angle),
y: center.y + radius * Math.sin(angle)
};
}
// Calculating K as done in https://pomax.github.io/bezierinfo/#circles_cubic
function getApproximationFactor(angleStart, angleEnd) {
let arc = angleEnd - angleStart;
// Always choose the smaller arc
if (Math.abs(arc) > Math.PI) {
arc -= Math.PI * 2;
arc %= Math.PI * 2;
}
return (4 / 3) * Math.tan(arc / 4);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment