An algorithm to find bounding box of closed bezier curves? - algorithm

I'm looking for an algorithm to find bounding box (max/min points) of a closed quadratic bezier curve in Cartesian axis:
input: C (a closed bezier curve)
output: A B C D points
Image http://www.imagechicken.com/uploads/1270586513022388700.jpg
Note: above image shows a smooth curve. it could be not smooth. (have corners)

Ivan Kuckir's DeCasteljau is a brute force, but works in many cases. The problem with it is the count of iterations. The actual shape and the distance between coordinates affect to the precision of the result. And to find a precise enough answer, you have to iterate tens of times, may be more. And it may fail if there are sharp turns in curve.
Better solution is to find first derivative roots, as is described on the excellent site http://processingjs.nihongoresources.com/bezierinfo/. Please read the section Finding the extremities of the curves.
The link above has the algorithm for both quadratic and cubic curves.
The asker of question is interested in quadratic curves, so the rest of this answer may be irrelevant, because I provide codes for calculating extremities of Cubic curves.
Below are three Javascript codes of which the first (CODE 1) is the one I suggest to use.
** CODE 1 **
After testing processingjs and Raphael's solutions I find they had some restrictions and/or bugs. Then more search and found Bonsai and it's bounding box function, which is based on NISHIO Hirokazu's Python script. Both have a downside where double equality is tested using ==. When I changed these to numerically robust comparisons, then script succeeds 100% right in all cases. I tested the script with thousands of random paths and also with all collinear cases and all succeeded:
Various cubic curves
Random cubic curves
Collinear cubic curves
The code is as follows. Usually left, right, top and bottom values are the all needed, but in some cases it's fine to know the coordinates of local extreme points and corresponding t values. So I added there two variables: tvalues and points. Remove code regarding them and you have fast and stable bounding box calculation function.
// Source: http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
// Original version: NISHIO Hirokazu
// Modifications: Timo
var pow = Math.pow,
sqrt = Math.sqrt,
min = Math.min,
max = Math.max;
abs = Math.abs;
function getBoundsOfCurve(x0, y0, x1, y1, x2, y2, x3, y3)
{
var tvalues = new Array();
var bounds = [new Array(), new Array()];
var points = new Array();
var a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (var i = 0; i < 2; ++i)
{
if (i == 0)
{
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
}
else
{
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (abs(a) < 1e-12) // Numerical robustness
{
if (abs(b) < 1e-12) // Numerical robustness
{
continue;
}
t = -c / b;
if (0 < t && t < 1)
{
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
sqrtb2ac = sqrt(b2ac);
if (b2ac < 0)
{
continue;
}
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1)
{
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1)
{
tvalues.push(t2);
}
}
var x, y, j = tvalues.length,
jlen = j,
mt;
while (j--)
{
t = tvalues[j];
mt = 1 - t;
x = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3);
bounds[0][j] = x;
y = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3);
bounds[1][j] = y;
points[j] = {
X: x,
Y: y
};
}
tvalues[jlen] = 0;
tvalues[jlen + 1] = 1;
points[jlen] = {
X: x0,
Y: y0
};
points[jlen + 1] = {
X: x3,
Y: y3
};
bounds[0][jlen] = x0;
bounds[1][jlen] = y0;
bounds[0][jlen + 1] = x3;
bounds[1][jlen + 1] = y3;
tvalues.length = bounds[0].length = bounds[1].length = points.length = jlen + 2;
return {
left: min.apply(null, bounds[0]),
top: min.apply(null, bounds[1]),
right: max.apply(null, bounds[0]),
bottom: max.apply(null, bounds[1]),
points: points, // local extremes
tvalues: tvalues // t values of local extremes
};
};
// Usage:
var bounds = getBoundsOfCurve(532,333,117,305,28,93,265,42);
console.log(JSON.stringify(bounds));
// Prints: {"left":135.77684049079755,"top":42,"right":532,"bottom":333,"points":[{"X":135.77684049079755,"Y":144.86387466397255},{"X":532,"Y":333},{"X":265,"Y":42}],"tvalues":[0.6365030674846626,0,1]}
CODE 2 (which fails in collinear cases):
I translated the code from http://processingjs.nihongoresources.com/bezierinfo/sketchsource.php?sketch=tightBoundsCubicBezier to Javascript. The code works fine in normal cases, but not in collinear cases where all points lie on the same line.
For reference, here is the Javascript code.
function computeCubicBaseValue(a,b,c,d,t) {
var mt = 1-t;
return mt*mt*mt*a + 3*mt*mt*t*b + 3*mt*t*t*c + t*t*t*d;
}
function computeCubicFirstDerivativeRoots(a,b,c,d) {
var ret = [-1,-1];
var tl = -a+2*b-c;
var tr = -Math.sqrt(-a*(c-d) + b*b - b*(c+d) +c*c);
var dn = -a+3*b-3*c+d;
if(dn!=0) { ret[0] = (tl+tr)/dn; ret[1] = (tl-tr)/dn; }
return ret;
}
function computeCubicBoundingBox(xa,ya,xb,yb,xc,yc,xd,yd)
{
// find the zero point for x and y in the derivatives
var minx = 9999;
var maxx = -9999;
if(xa<minx) { minx=xa; }
if(xa>maxx) { maxx=xa; }
if(xd<minx) { minx=xd; }
if(xd>maxx) { maxx=xd; }
var ts = computeCubicFirstDerivativeRoots(xa, xb, xc, xd);
for(var i=0; i<ts.length;i++) {
var t = ts[i];
if(t>=0 && t<=1) {
var x = computeCubicBaseValue(t, xa, xb, xc, xd);
var y = computeCubicBaseValue(t, ya, yb, yc, yd);
if(x<minx) { minx=x; }
if(x>maxx) { maxx=x; }}}
var miny = 9999;
var maxy = -9999;
if(ya<miny) { miny=ya; }
if(ya>maxy) { maxy=ya; }
if(yd<miny) { miny=yd; }
if(yd>maxy) { maxy=yd; }
ts = computeCubicFirstDerivativeRoots(ya, yb, yc, yd);
for(i=0; i<ts.length;i++) {
var t = ts[i];
if(t>=0 && t<=1) {
var x = computeCubicBaseValue(t, xa, xb, xc, xd);
var y = computeCubicBaseValue(t, ya, yb, yc, yd);
if(y<miny) { miny=y; }
if(y>maxy) { maxy=y; }}}
// bounding box corner coordinates
var bbox = [minx,miny, maxx,miny, maxx,maxy, minx,maxy ];
return bbox;
}
CODE 3 (works in most cases):
To handle also collinear cases, I found Raphael's solution, which is based on the same first derivative method as the CODE 2. I added also a return value dots, which has the extrema points, because always it's not enough to know bounding boxes min and max coordinates, but we want to know the exact extrema coordinates.
EDIT: found another bug. Fails eg. in 532,333,117,305,28,93,265,42 and also many other cases.
The code is here:
Array.max = function( array ){
return Math.max.apply( Math, array );
};
Array.min = function( array ){
return Math.min.apply( Math, array );
};
var findDotAtSegment = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t) {
var t1 = 1 - t;
return {
x: t1*t1*t1*p1x + t1*t1*3*t*c1x + t1*3*t*t * c2x + t*t*t * p2x,
y: t1*t1*t1*p1y + t1*t1*3*t*c1y + t1*3*t*t * c2y + t*t*t * p2y
};
};
var cubicBBox = function (p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y) {
var a = (c2x - 2 * c1x + p1x) - (p2x - 2 * c2x + c1x),
b = 2 * (c1x - p1x) - 2 * (c2x - c1x),
c = p1x - c1x,
t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a,
t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a,
y = [p1y, p2y],
x = [p1x, p2x],
dot, dots=[];
Math.abs(t1) > "1e12" && (t1 = 0.5);
Math.abs(t2) > "1e12" && (t2 = 0.5);
if (t1 >= 0 && t1 <= 1) {
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
x.push(dot.x);
y.push(dot.y);
dots.push({X:dot.x, Y:dot.y});
}
if (t2 >= 0 && t2 <= 1) {
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
x.push(dot.x);
y.push(dot.y);
dots.push({X:dot.x, Y:dot.y});
}
a = (c2y - 2 * c1y + p1y) - (p2y - 2 * c2y + c1y);
b = 2 * (c1y - p1y) - 2 * (c2y - c1y);
c = p1y - c1y;
t1 = (-b + Math.sqrt(b * b - 4 * a * c)) / 2 / a;
t2 = (-b - Math.sqrt(b * b - 4 * a * c)) / 2 / a;
Math.abs(t1) > "1e12" && (t1 = 0.5);
Math.abs(t2) > "1e12" && (t2 = 0.5);
if (t1 >= 0 && t1 <= 1) {
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t1);
x.push(dot.x);
y.push(dot.y);
dots.push({X:dot.x, Y:dot.y});
}
if (t2 >= 0 && t2 <= 1) {
dot = findDotAtSegment(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, t2);
x.push(dot.x);
y.push(dot.y);
dots.push({X:dot.x, Y:dot.y});
}
// remove duplicate dots
var dots2 = [];
var l = dots.length;
for(var i=0; i<l; i++) {
for(var j=i+1; j<l; j++) {
if (dots[i].X === dots[j].X && dots[i].Y === dots[j].Y)
j = ++i;
}
dots2.push({X: dots[i].X, Y: dots[i].Y});
}
return {
min: {x: Array.min(x), y: Array.min(y)},
max: {x: Array.max(x), y: Array.max(y)},
dots: dots2 // these are the extrema points
};
};

Well, I would say you start by adding all endpoints to your bounding box. Then, you go through all the bezier elements. I assume the formula in question is this one:
From this, extract two formulas for X and Y, respectively. Test both for extrema by taking the derivative (zero crossings). Then add the corresponding points to your bounding box as well.

Use De Casteljau algorithm to approximate the curve of higher orders. Here is how it works for cubic curve
http://jsfiddle.net/4VCVX/25/
function getCurveBounds(ax, ay, bx, by, cx, cy, dx, dy)
{
var px, py, qx, qy, rx, ry, sx, sy, tx, ty,
tobx, toby, tocx, tocy, todx, tody, toqx, toqy,
torx, tory, totx, toty;
var x, y, minx, miny, maxx, maxy;
minx = miny = Number.POSITIVE_INFINITY;
maxx = maxy = Number.NEGATIVE_INFINITY;
tobx = bx - ax; toby = by - ay; // directions
tocx = cx - bx; tocy = cy - by;
todx = dx - cx; tody = dy - cy;
var step = 1/40; // precision
for(var d=0; d<1.001; d+=step)
{
px = ax +d*tobx; py = ay +d*toby;
qx = bx +d*tocx; qy = by +d*tocy;
rx = cx +d*todx; ry = cy +d*tody;
toqx = qx - px; toqy = qy - py;
torx = rx - qx; tory = ry - qy;
sx = px +d*toqx; sy = py +d*toqy;
tx = qx +d*torx; ty = qy +d*tory;
totx = tx - sx; toty = ty - sy;
x = sx + d*totx; y = sy + d*toty;
minx = Math.min(minx, x); miny = Math.min(miny, y);
maxx = Math.max(maxx, x); maxy = Math.max(maxy, y);
}
return {x:minx, y:miny, width:maxx-minx, height:maxy-miny};
}

I believe that the control points of a Bezier curve form a convex hull that encloses the curve. If you just want a axis-aligned bounding box, I think you need to find the min and max of each (x, y) for each control point of all the segments.
I suppose that might not be a tight box. That is, the box might be slightly larger than it needs to be, but it's simple and fast to compute. I guess it depends on your requirements.

I think the accepted answer is fine, but just wanted to offer a little more explanation for anyone else trying to do this.
Consider a quadratic Bezier with starting point p1, ending point p2 and "control point" pc. This curve has three parametric equations:
pa(t) = p1 + t(pc-p1)
pb(t) = pc + t(p2-pc)
p(t) = pa(t) + t*(pb(t) - pa(t))
In all cases, t runs from 0 to 1, inclusive.
The first two are linear, defining line segments from p1 to pc and from pc to p2, respectively. The third is quadratic once you substitute in the expressions for pa(t) and pb(t); this is the one that actually defines points on the curve.
Actually, each of these equations is a pair of equations, one for the horizontal dimension, and one for the vertical. The nice thing about parametric curves is that the x and y can be handled independently of one another. The equations are exactly the same, just substitute x or y for p in the above equations.
The important point is that the line segment defined in equation 3, that runs from pa(t) to pb(t) for a specific value of t is tangent to the curve at the corresponding point p(t). To find the local extrema of the curve, you need to find the parameter value where the tangent is flat (i.e., a critical point). For the vertical dimension, you want to find the value of t such that ya(t) = yb(t), which gives the tangent a slope of 0. For the horizontal dimension, find t such that xa(t) = xb(t), which gives the tangent an infinite slope (i.e., a vertical line). In each case, you can just plug the value of t back into equation 1 (or 2, or even 3) to get the location of that extrema.
In other words, to find the vertical extrema of the curve, take just the y-component of equations 1 and 2, set them equal to each other and solve for t; plug this back into the y-component of equation 1, to get the y-value of that extrema. To get the complete y-range of the curve, find the minimum of this extreme y value and the y-components of the two end points, and likewise find the maximum of all three. Repeat for x to get the horizontal limits.
Remember that t only runs in [0, 1], so if you get a value outside of this range, it means there is no local extrema on the curve (at least not between your two endpoints). This includes the case where you end up dividing by zero when solving for t, which you will probably need to check for before you do it.
The same idea can be applied to higher-order Beziers, there are just more equations of higher degree, which also means there are potentially more local extrema per curve. For instance, on a cubic Bezier (two control points), solving for t to find the local extrema is a quadratic equation, so you could get 0, 1, or 2 values (remember to check for 0-denominators, and for negative square-roots, both of which indicate that there are no local extrema for that dimension). To find the range, you just need to find the min/max of all the local extrema, and the two end points.

I answered this question in Calculating the bounding box of cubic bezier curve
this article explain the details and also has a live html5 demo:
Calculating / Computing the Bounding Box of Cubic Bezier
I found a javascript in Snap.svg to calculate that: here
see the bezierBBox and curveDim functions.
I rewrite a javascript function.
//(x0,y0) is start point; (x1,y1),(x2,y2) is control points; (x3,y3) is end point.
function bezierMinMax(x0, y0, x1, y1, x2, y2, x3, y3) {
var tvalues = [], xvalues = [], yvalues = [],
a, b, c, t, t1, t2, b2ac, sqrtb2ac;
for (var i = 0; i < 2; ++i) {
if (i == 0) {
b = 6 * x0 - 12 * x1 + 6 * x2;
a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
c = 3 * x1 - 3 * x0;
} else {
b = 6 * y0 - 12 * y1 + 6 * y2;
a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
c = 3 * y1 - 3 * y0;
}
if (Math.abs(a) < 1e-12) {
if (Math.abs(b) < 1e-12) {
continue;
}
t = -c / b;
if (0 < t && t < 1) {
tvalues.push(t);
}
continue;
}
b2ac = b * b - 4 * c * a;
if (b2ac < 0) {
continue;
}
sqrtb2ac = Math.sqrt(b2ac);
t1 = (-b + sqrtb2ac) / (2 * a);
if (0 < t1 && t1 < 1) {
tvalues.push(t1);
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (0 < t2 && t2 < 1) {
tvalues.push(t2);
}
}
var j = tvalues.length, mt;
while (j--) {
t = tvalues[j];
mt = 1 - t;
xvalues[j] = (mt * mt * mt * x0) + (3 * mt * mt * t * x1) + (3 * mt * t * t * x2) + (t * t * t * x3);
yvalues[j] = (mt * mt * mt * y0) + (3 * mt * mt * t * y1) + (3 * mt * t * t * y2) + (t * t * t * y3);
}
xvalues.push(x0,x3);
yvalues.push(y0,y3);
return {
min: {x: Math.min.apply(0, xvalues), y: Math.min.apply(0, yvalues)},
max: {x: Math.max.apply(0, xvalues), y: Math.max.apply(0, yvalues)}
};
}

Timo-s first variant adapted to Objective-C
CGPoint CubicBezierPointAt(CGPoint p1, CGPoint p2, CGPoint p3, CGPoint p4, CGFloat t) {
CGFloat x = CubicBezier(p1.x, p2.x, p3.x, p4.x, t);
CGFloat y = CubicBezier(p1.y, p2.y, p3.y, p4.y, t);
return CGPointMake(x, y);
}
// array containing TopLeft and BottomRight points for curve`s enclosing bounds
NSArray* CubicBezierExtremums(CGPoint p1, CGPoint p2, CGPoint p3, CGPoint p4) {
CGFloat a, b, c, t, t1, t2, b2ac, sqrtb2ac;
NSMutableArray *tValues = [NSMutableArray new];
for (int i = 0; i < 2; i++) {
if (i == 0) {
a = 3 * (-p1.x + 3 * p2.x - 3 * p3.x + p4.x);
b = 6 * (p1.x - 2 * p2.x + p3.x);
c = 3 * (p2.x - p1.x);
}
else {
a = 3 * (-p1.y + 3 * p2.y - 3 * p3.y + p4.y);
b = 6 * (p1.y - 2 * p2.y + p3.y);
c = 3 * (p2.y - p1.y);
}
if(ABS(a) < CGFLOAT_MIN) {// Numerical robustness
if (ABS(b) < CGFLOAT_MIN) {// Numerical robustness
continue;
}
t = -c / b;
if (t > 0 && t < 1) {
[tValues addObject:[NSNumber numberWithDouble:t]];
}
continue;
}
b2ac = pow(b, 2) - 4 * c * a;
if (b2ac < 0) {
continue;
}
sqrtb2ac = sqrt(b2ac);
t1 = (-b + sqrtb2ac) / (2 * a);
if (t1 > 0.0 && t1 < 1.0) {
[tValues addObject:[NSNumber numberWithDouble:t1]];
}
t2 = (-b - sqrtb2ac) / (2 * a);
if (t2 > 0.0 && t2 < 1.0) {
[tValues addObject:[NSNumber numberWithDouble:t2]];
}
}
int j = (int)tValues.count;
CGFloat x = 0;
CGFloat y = 0;
NSMutableArray *xValues = [NSMutableArray new];
NSMutableArray *yValues = [NSMutableArray new];
while (j--) {
t = [[tValues objectAtIndex:j] doubleValue];
x = CubicBezier(p1.x, p2.x, p3.x, p4.x, t);
y = CubicBezier(p1.y, p2.y, p3.y, p4.y, t);
[xValues addObject:[NSNumber numberWithDouble:x]];
[yValues addObject:[NSNumber numberWithDouble:y]];
}
[xValues addObject:[NSNumber numberWithDouble:p1.x]];
[xValues addObject:[NSNumber numberWithDouble:p4.x]];
[yValues addObject:[NSNumber numberWithDouble:p1.y]];
[yValues addObject:[NSNumber numberWithDouble:p4.y]];
//find minX, minY, maxX, maxY
CGFloat minX = [[xValues valueForKeyPath:#"#min.self"] doubleValue];
CGFloat minY = [[yValues valueForKeyPath:#"#min.self"] doubleValue];
CGFloat maxX = [[xValues valueForKeyPath:#"#max.self"] doubleValue];
CGFloat maxY = [[yValues valueForKeyPath:#"#max.self"] doubleValue];
CGPoint origin = CGPointMake(minX, minY);
CGPoint bottomRight = CGPointMake(maxX, maxY);
NSArray *toReturn = [NSArray arrayWithObjects:
[NSValue valueWithCGPoint:origin],
[NSValue valueWithCGPoint:bottomRight],
nil];
return toReturn;
}

Timo's CODE 2 answer has a small bug: the t parameter in computeCubicBaseValue function should be last. Nevertheless good job, works like a charm ;)
Solution in C# :
double computeCubicBaseValue(double a, double b, double c, double d, double t)
{
var mt = 1 - t;
return mt * mt * mt * a + 3 * mt * mt * t * b + 3 * mt * t * t * c + t * t * t * d;
}
double[] computeCubicFirstDerivativeRoots(double a, double b, double c, double d)
{
var ret = new double[2] { -1, -1 };
var tl = -a + 2 * b - c;
var tr = -Math.Sqrt(-a * (c - d) + b * b - b * (c + d) + c * c);
var dn = -a + 3 * b - 3 * c + d;
if (dn != 0) { ret[0] = (tl + tr) / dn; ret[1] = (tl - tr) / dn; }
return ret;
}
public double[] ComputeCubicBoundingBox(Point start, Point firstControl, Point secondControl, Point end)
{
double xa, ya, xb, yb, xc, yc, xd, yd;
xa = start.X;
ya = start.Y;
xb = firstControl.X;
yb = firstControl.Y;
xc = secondControl.X;
yc = secondControl.Y;
xd = end.X;
yd = end.Y;
// find the zero point for x and y in the derivatives
double minx = Double.MaxValue;
double maxx = Double.MinValue;
if (xa < minx) { minx = xa; }
if (xa > maxx) { maxx = xa; }
if (xd < minx) { minx = xd; }
if (xd > maxx) { maxx = xd; }
var ts = computeCubicFirstDerivativeRoots(xa, xb, xc, xd);
for (var i = 0; i < ts.Length; i++)
{
var t = ts[i];
if (t >= 0 && t <= 1)
{
var x = computeCubicBaseValue(xa, xb, xc, xd,t);
var y = computeCubicBaseValue(ya, yb, yc, yd,t);
if (x < minx) { minx = x; }
if (x > maxx) { maxx = x; }
}
}
double miny = Double.MaxValue;
double maxy = Double.MinValue;
if (ya < miny) { miny = ya; }
if (ya > maxy) { maxy = ya; }
if (yd < miny) { miny = yd; }
if (yd > maxy) { maxy = yd; }
ts = computeCubicFirstDerivativeRoots(ya, yb, yc, yd);
for (var i = 0; i < ts.Length; i++)
{
var t = ts[i];
if (t >= 0 && t <= 1)
{
var x = computeCubicBaseValue(xa, xb, xc, xd,t);
var y = computeCubicBaseValue(ya, yb, yc, yd,t);
if (y < miny) { miny = y; }
if (y > maxy) { maxy = y; }
}
}
// bounding box corner coordinates
var bbox = new double[] { minx, miny, maxx, maxy};
return bbox;
}

Related

Pixel by pixel Bézier Curve

The quadratic/cubic bézier curve code I find via google mostly works by subdividing the line into a series of points and connects them with straight lines. The rasterization happens in the line algorithm, not in the bézier one. Algorithms like Bresenham's work pixel-by-pixel to rasterize a line, and can be optimized (see Po-Han Lin's solution).
What is a quadratic bézier curve algorithm that works pixel-by-pixel like line algorithms instead of by plotting a series of points?
A variation of Bresenham's Algorithm works with quadratic functions like circles, ellipses, and parabolas, so it should work with quadratic Bezier curves too.
I was going to attempt an implementation, but then I found one on the web: http://members.chello.at/~easyfilter/bresenham.html.
If you want more detail or additional examples, the page mentioned above has a link to a 100 page PDF elaborating on the method: http://members.chello.at/~easyfilter/Bresenham.pdf.
Here's the code from Alois Zingl's site for plotting any quadratic Bezier curve. The first routine subdivides the curve at horizontal and vertical gradient changes:
void plotQuadBezier(int x0, int y0, int x1, int y1, int x2, int y2)
{ /* plot any quadratic Bezier curve */
int x = x0-x1, y = y0-y1;
double t = x0-2*x1+x2, r;
if ((long)x*(x2-x1) > 0) { /* horizontal cut at P4? */
if ((long)y*(y2-y1) > 0) /* vertical cut at P6 too? */
if (fabs((y0-2*y1+y2)/t*x) > abs(y)) { /* which first? */
x0 = x2; x2 = x+x1; y0 = y2; y2 = y+y1; /* swap points */
} /* now horizontal cut at P4 comes first */
t = (x0-x1)/t;
r = (1-t)*((1-t)*y0+2.0*t*y1)+t*t*y2; /* By(t=P4) */
t = (x0*x2-x1*x1)*t/(x0-x1); /* gradient dP4/dx=0 */
x = floor(t+0.5); y = floor(r+0.5);
r = (y1-y0)*(t-x0)/(x1-x0)+y0; /* intersect P3 | P0 P1 */
plotQuadBezierSeg(x0,y0, x,floor(r+0.5), x,y);
r = (y1-y2)*(t-x2)/(x1-x2)+y2; /* intersect P4 | P1 P2 */
x0 = x1 = x; y0 = y; y1 = floor(r+0.5); /* P0 = P4, P1 = P8 */
}
if ((long)(y0-y1)*(y2-y1) > 0) { /* vertical cut at P6? */
t = y0-2*y1+y2; t = (y0-y1)/t;
r = (1-t)*((1-t)*x0+2.0*t*x1)+t*t*x2; /* Bx(t=P6) */
t = (y0*y2-y1*y1)*t/(y0-y1); /* gradient dP6/dy=0 */
x = floor(r+0.5); y = floor(t+0.5);
r = (x1-x0)*(t-y0)/(y1-y0)+x0; /* intersect P6 | P0 P1 */
plotQuadBezierSeg(x0,y0, floor(r+0.5),y, x,y);
r = (x1-x2)*(t-y2)/(y1-y2)+x2; /* intersect P7 | P1 P2 */
x0 = x; x1 = floor(r+0.5); y0 = y1 = y; /* P0 = P6, P1 = P7 */
}
plotQuadBezierSeg(x0,y0, x1,y1, x2,y2); /* remaining part */
}
The second routine actually plots a Bezier curve segment (one without gradient changes):
void plotQuadBezierSeg(int x0, int y0, int x1, int y1, int x2, int y2)
{ /* plot a limited quadratic Bezier segment */
int sx = x2-x1, sy = y2-y1;
long xx = x0-x1, yy = y0-y1, xy; /* relative values for checks */
double dx, dy, err, cur = xx*sy-yy*sx; /* curvature */
assert(xx*sx <= 0 && yy*sy <= 0); /* sign of gradient must not change */
if (sx*(long)sx+sy*(long)sy > xx*xx+yy*yy) { /* begin with longer part */
x2 = x0; x0 = sx+x1; y2 = y0; y0 = sy+y1; cur = -cur; /* swap P0 P2 */
}
if (cur != 0) { /* no straight line */
xx += sx; xx *= sx = x0 < x2 ? 1 : -1; /* x step direction */
yy += sy; yy *= sy = y0 < y2 ? 1 : -1; /* y step direction */
xy = 2*xx*yy; xx *= xx; yy *= yy; /* differences 2nd degree */
if (cur*sx*sy < 0) { /* negated curvature? */
xx = -xx; yy = -yy; xy = -xy; cur = -cur;
}
dx = 4.0*sy*cur*(x1-x0)+xx-xy; /* differences 1st degree */
dy = 4.0*sx*cur*(y0-y1)+yy-xy;
xx += xx; yy += yy; err = dx+dy+xy; /* error 1st step */
do {
setPixel(x0,y0); /* plot curve */
if (x0 == x2 && y0 == y2) return; /* last pixel -> curve finished */
y1 = 2*err < dx; /* save value for test of y step */
if (2*err > dy) { x0 += sx; dx -= xy; err += dy += yy; } /* x step */
if ( y1 ) { y0 += sy; dy -= xy; err += dx += xx; } /* y step */
} while (dy < 0 && dx > 0); /* gradient negates -> algorithm fails */
}
plotLine(x0,y0, x2,y2); /* plot remaining part to end */
}
Code for antialiasing is also available on the site.
The corresponding functions from Zingl's site for cubic Bezier curves are
void plotCubicBezier(int x0, int y0, int x1, int y1,
int x2, int y2, int x3, int y3)
{ /* plot any cubic Bezier curve */
int n = 0, i = 0;
long xc = x0+x1-x2-x3, xa = xc-4*(x1-x2);
long xb = x0-x1-x2+x3, xd = xb+4*(x1+x2);
long yc = y0+y1-y2-y3, ya = yc-4*(y1-y2);
long yb = y0-y1-y2+y3, yd = yb+4*(y1+y2);
float fx0 = x0, fx1, fx2, fx3, fy0 = y0, fy1, fy2, fy3;
double t1 = xb*xb-xa*xc, t2, t[5];
/* sub-divide curve at gradient sign changes */
if (xa == 0) { /* horizontal */
if (abs(xc) < 2*abs(xb)) t[n++] = xc/(2.0*xb); /* one change */
} else if (t1 > 0.0) { /* two changes */
t2 = sqrt(t1);
t1 = (xb-t2)/xa; if (fabs(t1) < 1.0) t[n++] = t1;
t1 = (xb+t2)/xa; if (fabs(t1) < 1.0) t[n++] = t1;
}
t1 = yb*yb-ya*yc;
if (ya == 0) { /* vertical */
if (abs(yc) < 2*abs(yb)) t[n++] = yc/(2.0*yb); /* one change */
} else if (t1 > 0.0) { /* two changes */
t2 = sqrt(t1);
t1 = (yb-t2)/ya; if (fabs(t1) < 1.0) t[n++] = t1;
t1 = (yb+t2)/ya; if (fabs(t1) < 1.0) t[n++] = t1;
}
for (i = 1; i < n; i++) /* bubble sort of 4 points */
if ((t1 = t[i-1]) > t[i]) { t[i-1] = t[i]; t[i] = t1; i = 0; }
t1 = -1.0; t[n] = 1.0; /* begin / end point */
for (i = 0; i <= n; i++) { /* plot each segment separately */
t2 = t[i]; /* sub-divide at t[i-1], t[i] */
fx1 = (t1*(t1*xb-2*xc)-t2*(t1*(t1*xa-2*xb)+xc)+xd)/8-fx0;
fy1 = (t1*(t1*yb-2*yc)-t2*(t1*(t1*ya-2*yb)+yc)+yd)/8-fy0;
fx2 = (t2*(t2*xb-2*xc)-t1*(t2*(t2*xa-2*xb)+xc)+xd)/8-fx0;
fy2 = (t2*(t2*yb-2*yc)-t1*(t2*(t2*ya-2*yb)+yc)+yd)/8-fy0;
fx0 -= fx3 = (t2*(t2*(3*xb-t2*xa)-3*xc)+xd)/8;
fy0 -= fy3 = (t2*(t2*(3*yb-t2*ya)-3*yc)+yd)/8;
x3 = floor(fx3+0.5); y3 = floor(fy3+0.5); /* scale bounds to int */
if (fx0 != 0.0) { fx1 *= fx0 = (x0-x3)/fx0; fx2 *= fx0; }
if (fy0 != 0.0) { fy1 *= fy0 = (y0-y3)/fy0; fy2 *= fy0; }
if (x0 != x3 || y0 != y3) /* segment t1 - t2 */
plotCubicBezierSeg(x0,y0, x0+fx1,y0+fy1, x0+fx2,y0+fy2, x3,y3);
x0 = x3; y0 = y3; fx0 = fx3; fy0 = fy3; t1 = t2;
}
}
and
void plotCubicBezierSeg(int x0, int y0, float x1, float y1,
float x2, float y2, int x3, int y3)
{ /* plot limited cubic Bezier segment */
int f, fx, fy, leg = 1;
int sx = x0 < x3 ? 1 : -1, sy = y0 < y3 ? 1 : -1; /* step direction */
float xc = -fabs(x0+x1-x2-x3), xa = xc-4*sx*(x1-x2), xb = sx*(x0-x1-x2+x3);
float yc = -fabs(y0+y1-y2-y3), ya = yc-4*sy*(y1-y2), yb = sy*(y0-y1-y2+y3);
double ab, ac, bc, cb, xx, xy, yy, dx, dy, ex, *pxy, EP = 0.01;
/* check for curve restrains */
/* slope P0-P1 == P2-P3 and (P0-P3 == P1-P2 or no slope change) */
assert((x1-x0)*(x2-x3) < EP && ((x3-x0)*(x1-x2) < EP || xb*xb < xa*xc+EP));
assert((y1-y0)*(y2-y3) < EP && ((y3-y0)*(y1-y2) < EP || yb*yb < ya*yc+EP));
if (xa == 0 && ya == 0) { /* quadratic Bezier */
sx = floor((3*x1-x0+1)/2); sy = floor((3*y1-y0+1)/2); /* new midpoint */
return plotQuadBezierSeg(x0,y0, sx,sy, x3,y3);
}
x1 = (x1-x0)*(x1-x0)+(y1-y0)*(y1-y0)+1; /* line lengths */
x2 = (x2-x3)*(x2-x3)+(y2-y3)*(y2-y3)+1;
do { /* loop over both ends */
ab = xa*yb-xb*ya; ac = xa*yc-xc*ya; bc = xb*yc-xc*yb;
ex = ab*(ab+ac-3*bc)+ac*ac; /* P0 part of self-intersection loop? */
f = ex > 0 ? 1 : sqrt(1+1024/x1); /* calculate resolution */
ab *= f; ac *= f; bc *= f; ex *= f*f; /* increase resolution */
xy = 9*(ab+ac+bc)/8; cb = 8*(xa-ya);/* init differences of 1st degree */
dx = 27*(8*ab*(yb*yb-ya*yc)+ex*(ya+2*yb+yc))/64-ya*ya*(xy-ya);
dy = 27*(8*ab*(xb*xb-xa*xc)-ex*(xa+2*xb+xc))/64-xa*xa*(xy+xa);
/* init differences of 2nd degree */
xx = 3*(3*ab*(3*yb*yb-ya*ya-2*ya*yc)-ya*(3*ac*(ya+yb)+ya*cb))/4;
yy = 3*(3*ab*(3*xb*xb-xa*xa-2*xa*xc)-xa*(3*ac*(xa+xb)+xa*cb))/4;
xy = xa*ya*(6*ab+6*ac-3*bc+cb); ac = ya*ya; cb = xa*xa;
xy = 3*(xy+9*f*(cb*yb*yc-xb*xc*ac)-18*xb*yb*ab)/8;
if (ex < 0) { /* negate values if inside self-intersection loop */
dx = -dx; dy = -dy; xx = -xx; yy = -yy; xy = -xy; ac = -ac; cb = -cb;
} /* init differences of 3rd degree */
ab = 6*ya*ac; ac = -6*xa*ac; bc = 6*ya*cb; cb = -6*xa*cb;
dx += xy; ex = dx+dy; dy += xy; /* error of 1st step */
for (pxy = &xy, fx = fy = f; x0 != x3 && y0 != y3; ) {
setPixel(x0,y0); /* plot curve */
do { /* move sub-steps of one pixel */
if (dx > *pxy || dy < *pxy) goto exit; /* confusing values */
y1 = 2*ex-dy; /* save value for test of y step */
if (2*ex >= dx) { /* x sub-step */
fx--; ex += dx += xx; dy += xy += ac; yy += bc; xx += ab;
}
if (y1 <= 0) { /* y sub-step */
fy--; ex += dy += yy; dx += xy += bc; xx += ac; yy += cb;
}
} while (fx > 0 && fy > 0); /* pixel complete? */
if (2*fx <= f) { x0 += sx; fx += f; } /* x step */
if (2*fy <= f) { y0 += sy; fy += f; } /* y step */
if (pxy == &xy && dx < 0 && dy > 0) pxy = &EP;/* pixel ahead valid */
}
exit: xx = x0; x0 = x3; x3 = xx; sx = -sx; xb = -xb; /* swap legs */
yy = y0; y0 = y3; y3 = yy; sy = -sy; yb = -yb; x1 = x2;
} while (leg--); /* try other end */
plotLine(x0,y0, x3,y3); /* remaining part in case of cusp or crunode */
}
As Mike 'Pomax' Kamermans has noted, the solution for cubic Bezier curves on the site is not complete; in particular, there are issues with antialiasing cubic Bezier curves, and the discussion of rational cubic Bezier curves is incomplete.
You can use De Casteljau's algorithm to subdivide a curve into enough pieces that each subsection is a pixel.
This is the equation for finding the [x,y] point on a Quadratic Curve at interval T:
// Given 3 control points defining the Quadratic curve
// and given T which is an interval between 0.00 and 1.00 along the curve.
// Note:
// At the curve's starting control point T==0.00.
// At the curve's ending control point T==1.00.
var x = Math.pow(1-T,2)*startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2)*startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
To make practical use of this equation, you can input about 1000 T values between 0.00 and 1.00. This results in a set of 1000 points guaranteed to be along the Quadratic Curve.
Calculating 1000 points along the curve is probably over-sampling (some calculated points will be at the same pixel coordinate) so you will want to de-duplicate the 1000 points until the set represents unique pixel coordinates along the curve.
There is a similar equation for Cubic Bezier curves.
Here's example code that plots a Quadratic Curve as a set of calculated pixels:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var points=[];
var lastX=-100;
var lastY=-100;
var startPt={x:50,y:200};
var controlPt={x:150,y:25};
var endPt={x:250,y:100};
for(var t=0;t<1000;t++){
var xyAtT=getQuadraticBezierXYatT(startPt,controlPt,endPt,t/1000);
var x=parseInt(xyAtT.x);
var y=parseInt(xyAtT.y);
if(!(x==lastX && y==lastY)){
points.push(xyAtT);
lastX=x;
lastY=y;
}
}
$('#curve').text('Quadratic Curve made up of '+points.length+' individual points');
ctx.fillStyle='red';
for(var i=0;i<points.length;i++){
var x=points[i].x;
var y=points[i].y;
ctx.fillRect(x,y,1,1);
}
function getQuadraticBezierXYatT(startPt,controlPt,endPt,T) {
var x = Math.pow(1-T,2) * startPt.x + 2 * (1-T) * T * controlPt.x + Math.pow(T,2) * endPt.x;
var y = Math.pow(1-T,2) * startPt.y + 2 * (1-T) * T * controlPt.y + Math.pow(T,2) * endPt.y;
return( {x:x,y:y} );
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4 id='curve'>Q</h4>
<canvas id="canvas" width=350 height=300></canvas>
The thing to realise here is that "line segments", when created small enough, are equivalent to pixels. Bezier curves are not linearly traversible curves, so we can't easily "skip ahead to the next pixel" in a single step, like we can for lines or circular arcs.
You could, of course, take the tangent at any point for a t you already have, and then guess which next value t' will lie a pixel further. However, what typically happens is that you guess, and guess wrong because the curve does not behave linearly, then you check to see how "off" your guess was, correct your guess, and then check again. Repeat until you've converged on the next pixel: this is far, far slower than just flattening the curve to a high number of line segments instead, which is a fast operation.
If you pick the number of segments such that they're appropriate to the curve's length, given the display it's rendered to, no one will be able to tell you flattened the curve.
There are ways to reparameterize Bezier curves, but they're expensive, and different canonical curves require different reparameterization, so that's really not faster either. What tends to be the most useful for discrete displays is to build a LUT (lookup table) for your curve, with a length that works for the size the curve is on the display, and then using that LUT as your base data for drawing, intersection detection, etc. etc.
First of all, I'd like to say that the fastest and the most reliable way to render bezier curves is to approximate them by polyline via adaptive subdivision, then render the polyline. Approach by #markE with drawing many points sampled on the curve is rather fast, but it can skip pixels. Here I describe another approach, which is closest to line rasterization (though it is slow and hard to implement robustly).
I'll treat usually curve parameter as time. Here is the pseudocode:
Put your cursor at the first control point, find the surrounding pixel.
For each side of the pixel (four total), check when your bezier curves intersects its line by solving quadratic equations.
Among all the calculated side intersection times, choose the one which will happen strictly in future, but as early as possible.
Move to neighboring pixel depending on which side was best.
Set current time to time of that best side intersection.
Repeat from step 2.
This algorithm works until time parameter exceeds one. Also note that it has severe issues with curves exactly touching a side of a pixel. I suppose it is solvable with a special check.
Here is the main code:
double WhenEquals(double p0, double p1, double p2, double val, double minp) {
//p0 * (1-t)^2 + p1 * 2t(1 - t) + p2 * t^2 = val
double qa = p0 + p2 - 2 * p1;
double qb = p1 - p0;
double qc = p0 - val;
assert(fabs(qa) > EPS); //singular case must be handled separately
double qd = qb * qb - qa * qc;
if (qd < -EPS)
return INF;
qd = sqrt(max(qd, 0.0));
double t1 = (-qb - qd) / qa;
double t2 = (-qb + qd) / qa;
if (t2 < t1) swap(t1, t2);
if (t1 > minp + EPS)
return t1;
else if (t2 > minp + EPS)
return t2;
return INF;
}
void DrawCurve(const Bezier &curve) {
int cell[2];
for (int c = 0; c < 2; c++)
cell[c] = int(floor(curve.pts[0].a[c]));
DrawPixel(cell[0], cell[1]);
double param = 0.0;
while (1) {
int bc = -1, bs = -1;
double bestTime = 1.0;
for (int c = 0; c < 2; c++)
for (int s = 0; s < 2; s++) {
double crit = WhenEquals(
curve.pts[0].a[c],
curve.pts[1].a[c],
curve.pts[2].a[c],
cell[c] + s, param
);
if (crit < bestTime) {
bestTime = crit;
bc = c, bs = s;
}
}
if (bc < 0)
break;
param = bestTime;
cell[bc] += (2*bs - 1);
DrawPixel(cell[0], cell[1]);
}
}
Full code is available here.
It uses loadbmp.h, here it is.

Line Circle intersection for Vertical and Horizontal Lines

I'm trying to detect when a line intersects a circle in javascript. I found a function that works almost perfectly but I recently noticed that it does not work when the intersecting line is perfectly horizontal or vertical. Since I don't have a great understanding of how this function actually works, I'm not sure how to edit it to get the results I'd like.
function lineCircleCollision(circleX,circleY,radius,lineX1,lineY1,lineX2,lineY2) {
var d1 = pDist(lineX1,lineY1,circleX,circleY);
var d2 = pDist(lineX2,lineY2,circleX,circleY);
if (d1<=radius || d2<=radius) {
return true;
}
var k1 = ((lineY2-lineY1)/(lineX2-lineX1));
var k2 = lineY1;
var k3 = -1/k1;
var k4 = circleY;
var xx = (k1*lineX1-k2-k3*circleX+k4)/(k1-k3);
var yy = k1*(xx-lineX1)+lineY1;
var allow = true;
if (lineX2>lineX1) {
if (xx>=lineX1 && xx<=lineX2) {}
else {allow = false;}
} else {
if (xx>=lineX2 && xx<=lineX1) {}
else {allow = false;}
}
if (lineY2>lineY1) {
if (yy>=lineY1 && yy<=lineY2) {}
else {allow = false;}
} else {
if (yy>=lineY2 && yy<=lineY1) {}
else {allow = false;}
}
if (allow) {
if (pDist(circleX,circleY,xx,yy)<radius) {
return true;
}
else {
return false;
}
} else {
return false;
}
}
function pDist(x1,y1,x2,y2) {
var xd = x2-x1;
var yd = y2-y1;
return Math.sqrt(xd*xd+yd*yd);
}
You can express the line as two relations:
x = x1 + k * (x2 - x1) = x1 + k * dx
y = y1 + k * (y2 - y1) = y1 + k * dy
with 0 < k < 1. A point on the circle satisfies the equation:
(x - Cx)² + (y - Cy)² = r²
Replace x and y by the line equations and you'll get a quadratic equation:
a*k² + b*k + c = 0
a = dx² + dy²
b = 2*dx*(x1 - Cx) + s*dy*(y1 - Cy)
c = (x1 - Cx)² + (y1 - Cy)² - r²
Solve that and if any of the two possible solutions for k lies in the range between 0 and 1, you have a hit. This method checks real intersections and misses the case where the line is entirely contained in the circle, so an additional check whether the line's end points lie within the circle is necessary.
Here's the code:
function collision_circle_line(Cx, Cy, r, x1, y1, x2, y2) {
var dx = x2 - x1;
var dy = y2 - y1;
var sx = x1 - Cx;
var sy = y1 - Cy;
var tx = x2 - Cx;
var ty = y2 - Cy;
if (tx*tx + ty*ty < r*r) return true;
var c = sx*sx + sy*sy - r*r;
if (c < 0) return true;
var b = 2 * (dx * sx + dy * sy);
var a = dx*dx + dy*dy;
if (Math.abs(a) < 1.0e-12) return false;
var discr = b*b - 4*a*c;
if (discr < 0) return false;
discr = Math.sqrt(discr);
var k1 = (-b - discr) / (2 * a);
if (k1 >= 0 && k1 <= 1) return true;
var k2 = (-b + discr) / (2 * a);
if (k2 >= 0 && k2 <= 1) return true;
return false;
}
Another way to view the intersection check is that we're finding the point on the line segment closest to the circle center and then determining whether it's close enough. Since distance to the circle center is a convex function, there are three possibilities: the two endpoints of the segment, and the closest point on the line, assuming that it's on the segment.
To find the closest point on the line, we have an overdetermined linear system
(1 - t) lineX1 + t lineX2 = circleX
(1 - t) lineY1 + t lineY2 = circleY,
expressed as a matrix:
[lineX2 - lineX1] [t] = [circleX - lineX1]
[lineY2 - lineY1] [circleY - lineY1].
The closest point can be found by solving the normal equation
[(lineX2 - lineX1) (lineY2 - lineY1)] [lineX2 - lineX1] [t] =
[lineY2 - lineY1]
[(lineX2 - lineX1) (lineY2 - lineY1)] [circleX - lineX1]
[circleY - lineY1],
expressed alternatively as
((lineX2 - lineX1)^2 + (lineY2 - lineY1)^2) t =
(lineX2 - lineX1) (circleX - lineX1) + (lineY2 - lineY1) (circleY - lineY1),
and solved for t:
(lineX2 - lineX1) (circleX - lineX1) + (lineY2 - lineY1) (circleY - lineY1)
t = ---------------------------------------------------------------------------.
((lineX2 - lineX1)^2 + (lineY2 - lineY1)^2)
Assuming that t is between 0 and 1, we can plug it in and check the distance. When t is out of range, we can clamp it and check only that endpoint.
Untested code:
function lineCircleCollision(circleX, circleY, radius, lineX1, lineY1, lineX2, lineY2) {
circleX -= lineX1;
circleY -= lineY1;
lineX2 -= lineX1;
lineY2 -= lineY1;
var t = (lineX2 * circleX + lineY2 * circleY) / (lineX2 * lineX2 + lineY2 * lineY2);
if (t < 0) t = 0;
else if (t > 1) t = 1;
var deltaX = lineX2 * t - circleX;
var deltaY = lineY2 * t - circleY;
return deltaX * deltaX + deltaY * deltaY <= radius * radius;
}
If you do not need the point just want to know if line intersects then:
compute distance from circle center P0(x0,y0) and line endpoints P1(x1,y1),P2(x2,y2)
double d1=|P1-P0|=sqrt((x1-x0)*(x1-x0)+(y1-x0)*(y1-x0));
double d2=|P2-P0|=sqrt((x2-x0)*(x2-x0)+(y2-x0)*(y2-x0));
order d1,d2 ascending
if (d1>d2) { double d=d1; d1=d2; d2=d; }
check intersection
if ((d1<=r)&&(d2>=r)) return true; else return false;
r is circle radius
[notes]
you do not need sqrt distances
if you leave them un-sqrted then just compare them to r*r instead of r

convert bezier curve to polygonal chain?

I want to split a bezier curve into a polygonal chain with n straight lines. The number of lines being dependent on a maximum allowed angle between 2 connecting lines.
I'm looking for an algorithm to find the most optimal solution (ie to reduce as much as possible the number of straight lines).
I know how to split a bezier curve using Casteljau or Bernstein polynomals. I tried dividing the bezier into half calculate the angle between the straight lines, and split again if the angle between the connecting lines is within a certain threshold range, but i may run into shortcuts.
Is there a known algorithm or pseudo code available to do this conversion?
Use de Casteljau algorithm recursively until the control points are approximately collinear. See for instance http://www.antigrain.com/research/adaptive_bezier/index.html.
This was a fascinating topic. The only thing I'm adding is tested C# code, to perhaps save somebody the trouble. And I tried to write for clarity as opposed to speed, so it mostly follows the AGG web site's PDF doc (see above) on the Casteljau algorithm. The Notation follows the diagram in that PDF.
public class Bezier
{
public PointF P1; // Begin Point
public PointF P2; // Control Point
public PointF P3; // Control Point
public PointF P4; // End Point
// Made these global so I could diagram the top solution
public Line L12;
public Line L23;
public Line L34;
public PointF P12;
public PointF P23;
public PointF P34;
public Line L1223;
public Line L2334;
public PointF P123;
public PointF P234;
public Line L123234;
public PointF P1234;
public Bezier(PointF p1, PointF p2, PointF p3, PointF p4)
{
P1 = p1; P2 = p2; P3 = p3; P4 = p4;
}
/// <summary>
/// Consider the classic Casteljau diagram
/// with the bezier points p1, p2, p3, p4 and lines l12, l23, l34
/// and their midpoint of line l12 being p12 ...
/// and the line between p12 p23 being L1223
/// and the midpoint of line L1223 being P1223 ...
/// </summary>
/// <param name="lines"></param>
public void SplitBezier( List<Line> lines)
{
L12 = new Line(this.P1, this.P2);
L23 = new Line(this.P2, this.P3);
L34 = new Line(this.P3, this.P4);
P12 = L12.MidPoint();
P23 = L23.MidPoint();
P34 = L34.MidPoint();
L1223 = new Line(P12, P23);
L2334 = new Line(P23, P34);
P123 = L1223.MidPoint();
P234 = L2334.MidPoint();
L123234 = new Line(P123, P234);
P1234 = L123234.MidPoint();
if (CurveIsFlat())
{
lines.Add(new Line(this.P1, this.P4));
return;
}
else
{
Bezier bz1 = new Bezier(this.P1, P12, P123, P1234);
bz1.SplitBezier(lines);
Bezier bz2 = new Bezier(P1234, P234, P34, this.P4);
bz2.SplitBezier(lines);
}
return;
}
/// <summary>
/// Check if points P1, P1234 and P2 are colinear (enough).
/// This is very simple-minded algo... there are better...
/// </summary>
/// <returns></returns>
public bool CurveIsFlat()
{
float t1 = (P2.Y - P1.Y) * (P3.X - P2.X);
float t2 = (P3.Y - P2.Y) * (P2.X - P1.X);
float delta = Math.Abs(t1 - t2);
return delta < 0.1; // Hard-coded constant
}
The PointF is from System.Drawing, and the Line class follows:
public class Line
{
PointF P1; PointF P2;
public Line(PointF pt1, PointF pt2)
{
P1 = pt1; P2 = pt2;
}
public PointF MidPoint()
{
return new PointF((P1.X + P2.X) / 2f, (P1.Y + P2.Y) / 2f);
}
}
A sample call creates the Bezier object with 4 points (begin, 2 control, and end), and returns a list of lines that approximate the Bezier:
TopBezier = new Bezier(Point1, Point2, Point3, Point4 );
List<Line> lines = new List<Line>();
TopBezier.SplitBezier(lines);
Thanks to Dr Jerry, AGG, and all the other contributors.
There are some alternatives for RSA flattening that are reported to be faster:
RSA vs PAA:
http://www.cis.usouthal.edu/~hain/general/Theses/Ahmad_thesis.pdf
RSA vs CAA vs PAA:
http://www.cis.usouthal.edu/~hain/general/Theses/Racherla_thesis.pdf
RSA = Recursive Subdivision Algorithm
PAA = Parabolic Approximation Algorithm
CAA = Circular Approximation Algorithm
According to Rachela, CAA is slower than the PAA by a factor of 1.5–2. CAA is as slow as RSA, but achieves required flatness better in offset curves.
It seems that PAA is best choice for actual curve and CAA is best for offset's of curve (when stroking curves).
I have tested PAA of both thesis, but they fail in some cases. Ahmad's PAA fails in collinear cases (all points on same line) and Rachela's PAA fails in collinear cases and in cases where both control points are equal. With some fixes, it may be possible to get them work as expected.
A visual example on my website -> DXF -> polybezier.
it is basically a recursive split with casteljau.
Bezier2Poly.prototype.convert = function(array,init) {
if (init) {
this.vertices = [];
}
if (!init && (Math.abs(this.controlPointsDiff(array[0], array[2])) < this.threshold
|| Math.abs(this.controlPointsDiff({x:array[2].x-array[1].x, y:array[2]-array[1].y}, array[2])) < this.threshold)) {
this.vertices.push(array[2]);
} else {
var split = this.splitBezier(array);
this.convert(split.b1);
this.convert(split.b2);
}
return this.vertices;
}
And judgement by: calculating the angle between the controlpoints and the line through the endpoint.
Bezier2Poly.prototype.controlPointsDiff = function (vector1, vector2) {
var angleCp1 = Math.atan2(vector1.y, vector1.x);
var angleCp2 = Math.atan2(vector2.y, vector2.x);
return angleCp1 - angleCp2;
}
i solve it with qt for any svg path including bezier curve , i found in svg module a static function in qsvghandler.cpp which parsePathDataFast from your svg path to QPainterPath and the cherry on the cake!! QPainterPath have three native functions to convert your path to polygon (the big one toFillPolygon and the others which split in a list of polygon toSubpathPolygons or toFillPolygons) along with nice stuff like bounding box, intersected, translate ... ready to use with Boost::Geometry now, not so bad!
the header parsepathdatafast.h
#ifndef PARSEPATHDATAFAST_H
#define PARSEPATHDATAFAST_H
#include <QPainterPath>
#include <QString>
bool parsePathDataFast(const QStringRef &dataStr, QPainterPath &path);
#endif // PARSEPATHDATAFAST_H
the code parsepathdatafast.cpp
#include <QtCore/qmath.h>
#include <QtMath>
#include <QChar>
#include <QByteArray>
#include <QMatrix>
#include <parsepathdatafast.h>
Q_CORE_EXPORT double qstrtod(const char *s00, char const **se, bool *ok);
// '0' is 0x30 and '9' is 0x39
static inline bool isDigit(ushort ch)
{
static quint16 magic = 0x3ff;
return ((ch >> 4) == 3) && (magic >> (ch & 15));
}
static qreal toDouble(const QChar *&str)
{
const int maxLen = 255;//technically doubles can go til 308+ but whatever
char temp[maxLen+1];
int pos = 0;
if (*str == QLatin1Char('-')) {
temp[pos++] = '-';
++str;
} else if (*str == QLatin1Char('+')) {
++str;
}
while (isDigit(str->unicode()) && pos < maxLen) {
temp[pos++] = str->toLatin1();
++str;
}
if (*str == QLatin1Char('.') && pos < maxLen) {
temp[pos++] = '.';
++str;
}
while (isDigit(str->unicode()) && pos < maxLen) {
temp[pos++] = str->toLatin1();
++str;
}
bool exponent = false;
if ((*str == QLatin1Char('e') || *str == QLatin1Char('E')) && pos < maxLen) {
exponent = true;
temp[pos++] = 'e';
++str;
if ((*str == QLatin1Char('-') || *str == QLatin1Char('+')) && pos < maxLen) {
temp[pos++] = str->toLatin1();
++str;
}
while (isDigit(str->unicode()) && pos < maxLen) {
temp[pos++] = str->toLatin1();
++str;
}
}
temp[pos] = '\0';
qreal val;
if (!exponent && pos < 10) {
int ival = 0;
const char *t = temp;
bool neg = false;
if(*t == '-') {
neg = true;
++t;
}
while(*t && *t != '.') {
ival *= 10;
ival += (*t) - '0';
++t;
}
if(*t == '.') {
++t;
int div = 1;
while(*t) {
ival *= 10;
ival += (*t) - '0';
div *= 10;
++t;
}
val = ((qreal)ival)/((qreal)div);
} else {
val = ival;
}
if (neg)
val = -val;
} else {
bool ok = false;
val = qstrtod(temp, 0, &ok);
}
return val;
}
static inline void parseNumbersArray(const QChar *&str, QVarLengthArray<qreal, 8> &points)
{
while (str->isSpace())
++str;
while (isDigit(str->unicode()) ||
*str == QLatin1Char('-') || *str == QLatin1Char('+') ||
*str == QLatin1Char('.')) {
points.append(toDouble(str));
while (str->isSpace())
++str;
if (*str == QLatin1Char(','))
++str;
//eat the rest of space
while (str->isSpace())
++str;
}
}
/**
static QVector<qreal> parsePercentageList(const QChar *&str)
{
QVector<qreal> points;
if (!str)
return points;
while (str->isSpace())
++str;
while ((*str >= QLatin1Char('0') && *str <= QLatin1Char('9')) ||
*str == QLatin1Char('-') || *str == QLatin1Char('+') ||
*str == QLatin1Char('.')) {
points.append(toDouble(str));
while (str->isSpace())
++str;
if (*str == QLatin1Char('%'))
++str;
while (str->isSpace())
++str;
if (*str == QLatin1Char(','))
++str;
//eat the rest of space
while (str->isSpace())
++str;
}
return points;
}
**/
static void pathArcSegment(QPainterPath &path,
qreal xc, qreal yc,
qreal th0, qreal th1,
qreal rx, qreal ry, qreal xAxisRotation)
{
qreal sinTh, cosTh;
qreal a00, a01, a10, a11;
qreal x1, y1, x2, y2, x3, y3;
qreal t;
qreal thHalf;
sinTh = qSin(xAxisRotation * (M_PI / 180.0));
cosTh = qCos(xAxisRotation * (M_PI / 180.0));
a00 = cosTh * rx;
a01 = -sinTh * ry;
a10 = sinTh * rx;
a11 = cosTh * ry;
thHalf = 0.5 * (th1 - th0);
t = (8.0 / 3.0) * qSin(thHalf * 0.5) * qSin(thHalf * 0.5) / qSin(thHalf);
x1 = xc + qCos(th0) - t * qSin(th0);
y1 = yc + qSin(th0) + t * qCos(th0);
x3 = xc + qCos(th1);
y3 = yc + qSin(th1);
x2 = x3 + t * qSin(th1);
y2 = y3 - t * qCos(th1);
path.cubicTo(a00 * x1 + a01 * y1, a10 * x1 + a11 * y1,
a00 * x2 + a01 * y2, a10 * x2 + a11 * y2,
a00 * x3 + a01 * y3, a10 * x3 + a11 * y3);
}
// the arc handling code underneath is from XSVG (BSD license)
/*
* Copyright 2002 USC/Information Sciences Institute
*
* Permission to use, copy, modify, distribute, and sell this software
* and its documentation for any purpose is hereby granted without
* fee, provided that the above copyright notice appear in all copies
* and that both that copyright notice and this permission notice
* appear in supporting documentation, and that the name of
* Information Sciences Institute not be used in advertising or
* publicity pertaining to distribution of the software without
* specific, written prior permission. Information Sciences Institute
* makes no representations about the suitability of this software for
* any purpose. It is provided "as is" without express or implied
* warranty.
*
* INFORMATION SCIENCES INSTITUTE DISCLAIMS ALL WARRANTIES WITH REGARD
* TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL INFORMATION SCIENCES
* INSTITUTE BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL
* DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA
* OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
* TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
* PERFORMANCE OF THIS SOFTWARE.
*
*/
static void pathArc(QPainterPath &path,
qreal rx,
qreal ry,
qreal x_axis_rotation,
int large_arc_flag,
int sweep_flag,
qreal x,
qreal y,
qreal curx, qreal cury)
{
qreal sin_th, cos_th;
qreal a00, a01, a10, a11;
qreal x0, y0, x1, y1, xc, yc;
qreal d, sfactor, sfactor_sq;
qreal th0, th1, th_arc;
int i, n_segs;
qreal dx, dy, dx1, dy1, Pr1, Pr2, Px, Py, check;
rx = qAbs(rx);
ry = qAbs(ry);
sin_th = qSin(x_axis_rotation * (M_PI / 180.0));
cos_th = qCos(x_axis_rotation * (M_PI / 180.0));
dx = (curx - x) / 2.0;
dy = (cury - y) / 2.0;
dx1 = cos_th * dx + sin_th * dy;
dy1 = -sin_th * dx + cos_th * dy;
Pr1 = rx * rx;
Pr2 = ry * ry;
Px = dx1 * dx1;
Py = dy1 * dy1;
/* Spec : check if radii are large enough */
check = Px / Pr1 + Py / Pr2;
if (check > 1) {
rx = rx * qSqrt(check);
ry = ry * qSqrt(check);
}
a00 = cos_th / rx;
a01 = sin_th / rx;
a10 = -sin_th / ry;
a11 = cos_th / ry;
x0 = a00 * curx + a01 * cury;
y0 = a10 * curx + a11 * cury;
x1 = a00 * x + a01 * y;
y1 = a10 * x + a11 * y;
/* (x0, y0) is current point in transformed coordinate space.
(x1, y1) is new point in transformed coordinate space.
The arc fits a unit-radius circle in this space.
*/
d = (x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0);
sfactor_sq = 1.0 / d - 0.25;
if (sfactor_sq < 0) sfactor_sq = 0;
sfactor = qSqrt(sfactor_sq);
if (sweep_flag == large_arc_flag) sfactor = -sfactor;
xc = 0.5 * (x0 + x1) - sfactor * (y1 - y0);
yc = 0.5 * (y0 + y1) + sfactor * (x1 - x0);
/* (xc, yc) is center of the circle. */
th0 = qAtan2(y0 - yc, x0 - xc);
th1 = qAtan2(y1 - yc, x1 - xc);
th_arc = th1 - th0;
if (th_arc < 0 && sweep_flag)
th_arc += 2 * M_PI;
else if (th_arc > 0 && !sweep_flag)
th_arc -= 2 * M_PI;
n_segs = qCeil(qAbs(th_arc / (M_PI * 0.5 + 0.001)));
for (i = 0; i < n_segs; i++) {
pathArcSegment(path, xc, yc,
th0 + i * th_arc / n_segs,
th0 + (i + 1) * th_arc / n_segs,
rx, ry, x_axis_rotation);
}
}
bool parsePathDataFast(const QStringRef &dataStr, QPainterPath &path)
{
qreal x0 = 0, y0 = 0; // starting point
qreal x = 0, y = 0; // current point
char lastMode = 0;
QPointF ctrlPt;
const QChar *str = dataStr.constData();
const QChar *end = str + dataStr.size();
while (str != end) {
while (str->isSpace())
++str;
QChar pathElem = *str;
++str;
QChar endc = *end;
*const_cast<QChar *>(end) = 0; // parseNumbersArray requires 0-termination that QStringRef cannot guarantee
QVarLengthArray<qreal, 8> arg;
parseNumbersArray(str, arg);
*const_cast<QChar *>(end) = endc;
if (pathElem == QLatin1Char('z') || pathElem == QLatin1Char('Z'))
arg.append(0);//dummy
const qreal *num = arg.constData();
int count = arg.count();
while (count > 0) {
qreal offsetX = x; // correction offsets
qreal offsetY = y; // for relative commands
switch (pathElem.unicode()) {
case 'm': {
if (count < 2) {
num++;
count--;
break;
}
x = x0 = num[0] + offsetX;
y = y0 = num[1] + offsetY;
num += 2;
count -= 2;
path.moveTo(x0, y0);
// As per 1.2 spec 8.3.2 The "moveto" commands
// If a 'moveto' is followed by multiple pairs of coordinates without explicit commands,
// the subsequent pairs shall be treated as implicit 'lineto' commands.
pathElem = QLatin1Char('l');
}
break;
case 'M': {
if (count < 2) {
num++;
count--;
break;
}
x = x0 = num[0];
y = y0 = num[1];
num += 2;
count -= 2;
path.moveTo(x0, y0);
// As per 1.2 spec 8.3.2 The "moveto" commands
// If a 'moveto' is followed by multiple pairs of coordinates without explicit commands,
// the subsequent pairs shall be treated as implicit 'lineto' commands.
pathElem = QLatin1Char('L');
}
break;
case 'z':
case 'Z': {
x = x0;
y = y0;
count--; // skip dummy
num++;
path.closeSubpath();
}
break;
case 'l': {
if (count < 2) {
num++;
count--;
break;
}
x = num[0] + offsetX;
y = num[1] + offsetY;
num += 2;
count -= 2;
path.lineTo(x, y);
}
break;
case 'L': {
if (count < 2) {
num++;
count--;
break;
}
x = num[0];
y = num[1];
num += 2;
count -= 2;
path.lineTo(x, y);
}
break;
case 'h': {
x = num[0] + offsetX;
num++;
count--;
path.lineTo(x, y);
}
break;
case 'H': {
x = num[0];
num++;
count--;
path.lineTo(x, y);
}
break;
case 'v': {
y = num[0] + offsetY;
num++;
count--;
path.lineTo(x, y);
}
break;
case 'V': {
y = num[0];
num++;
count--;
path.lineTo(x, y);
}
break;
case 'c': {
if (count < 6) {
num += count;
count = 0;
break;
}
QPointF c1(num[0] + offsetX, num[1] + offsetY);
QPointF c2(num[2] + offsetX, num[3] + offsetY);
QPointF e(num[4] + offsetX, num[5] + offsetY);
num += 6;
count -= 6;
path.cubicTo(c1, c2, e);
ctrlPt = c2;
x = e.x();
y = e.y();
break;
}
case 'C': {
if (count < 6) {
num += count;
count = 0;
break;
}
QPointF c1(num[0], num[1]);
QPointF c2(num[2], num[3]);
QPointF e(num[4], num[5]);
num += 6;
count -= 6;
path.cubicTo(c1, c2, e);
ctrlPt = c2;
x = e.x();
y = e.y();
break;
}
case 's': {
if (count < 4) {
num += count;
count = 0;
break;
}
QPointF c1;
if (lastMode == 'c' || lastMode == 'C' ||
lastMode == 's' || lastMode == 'S')
c1 = QPointF(2*x-ctrlPt.x(), 2*y-ctrlPt.y());
else
c1 = QPointF(x, y);
QPointF c2(num[0] + offsetX, num[1] + offsetY);
QPointF e(num[2] + offsetX, num[3] + offsetY);
num += 4;
count -= 4;
path.cubicTo(c1, c2, e);
ctrlPt = c2;
x = e.x();
y = e.y();
break;
}
case 'S': {
if (count < 4) {
num += count;
count = 0;
break;
}
QPointF c1;
if (lastMode == 'c' || lastMode == 'C' ||
lastMode == 's' || lastMode == 'S')
c1 = QPointF(2*x-ctrlPt.x(), 2*y-ctrlPt.y());
else
c1 = QPointF(x, y);
QPointF c2(num[0], num[1]);
QPointF e(num[2], num[3]);
num += 4;
count -= 4;
path.cubicTo(c1, c2, e);
ctrlPt = c2;
x = e.x();
y = e.y();
break;
}
case 'q': {
if (count < 4) {
num += count;
count = 0;
break;
}
QPointF c(num[0] + offsetX, num[1] + offsetY);
QPointF e(num[2] + offsetX, num[3] + offsetY);
num += 4;
count -= 4;
path.quadTo(c, e);
ctrlPt = c;
x = e.x();
y = e.y();
break;
}
case 'Q': {
if (count < 4) {
num += count;
count = 0;
break;
}
QPointF c(num[0], num[1]);
QPointF e(num[2], num[3]);
num += 4;
count -= 4;
path.quadTo(c, e);
ctrlPt = c;
x = e.x();
y = e.y();
break;
}
case 't': {
if (count < 2) {
num += count;
count = 0;
break;
}
QPointF e(num[0] + offsetX, num[1] + offsetY);
num += 2;
count -= 2;
QPointF c;
if (lastMode == 'q' || lastMode == 'Q' ||
lastMode == 't' || lastMode == 'T')
c = QPointF(2*x-ctrlPt.x(), 2*y-ctrlPt.y());
else
c = QPointF(x, y);
path.quadTo(c, e);
ctrlPt = c;
x = e.x();
y = e.y();
break;
}
case 'T': {
if (count < 2) {
num += count;
count = 0;
break;
}
QPointF e(num[0], num[1]);
num += 2;
count -= 2;
QPointF c;
if (lastMode == 'q' || lastMode == 'Q' ||
lastMode == 't' || lastMode == 'T')
c = QPointF(2*x-ctrlPt.x(), 2*y-ctrlPt.y());
else
c = QPointF(x, y);
path.quadTo(c, e);
ctrlPt = c;
x = e.x();
y = e.y();
break;
}
case 'a': {
if (count < 7) {
num += count;
count = 0;
break;
}
qreal rx = (*num++);
qreal ry = (*num++);
qreal xAxisRotation = (*num++);
qreal largeArcFlag = (*num++);
qreal sweepFlag = (*num++);
qreal ex = (*num++) + offsetX;
qreal ey = (*num++) + offsetY;
count -= 7;
qreal curx = x;
qreal cury = y;
pathArc(path, rx, ry, xAxisRotation, int(largeArcFlag),
int(sweepFlag), ex, ey, curx, cury);
x = ex;
y = ey;
}
break;
case 'A': {
if (count < 7) {
num += count;
count = 0;
break;
}
qreal rx = (*num++);
qreal ry = (*num++);
qreal xAxisRotation = (*num++);
qreal largeArcFlag = (*num++);
qreal sweepFlag = (*num++);
qreal ex = (*num++);
qreal ey = (*num++);
count -= 7;
qreal curx = x;
qreal cury = y;
pathArc(path, rx, ry, xAxisRotation, int(largeArcFlag),
int(sweepFlag), ex, ey, curx, cury);
x = ex;
y = ey;
}
break;
default:
return false;
}
lastMode = pathElem.toLatin1();
}
}
return true;
}
One question, i doesn't find Q_PI constant in the standard qt headers and i replace it with M_PI hope is OK!!

Is there a name for this sampling algorithm used in Minicraft?

For Ludum Dare 22, Notch programmed a game in 48 hours called Minicraft. It's like a 2D minecraft.
Anyway the source is available (here: http://www.ludumdare.com/compo/ludum-dare-22/?action=preview&uid=398 ), and I was taking a look since I am interested in random generation of terrain and levels. In the code is a block of code which runs the core generation, and the algorithm to me seems familiar, but I can't put a name to it. I'd like to know exactly what it is so I can read more about it and learn how it works.
Specifically, the code is from levelGen.java:
do {
int halfStep = stepSize / 2;
for (int y = 0; y < w; y += stepSize) {
for (int x = 0; x < w; x += stepSize) {
double a = sample(x, y);
double b = sample(x + stepSize, y);
double c = sample(x, y + stepSize);
double d = sample(x + stepSize, y + stepSize);
double e = (a + b + c + d) / 4.0 + (random.nextFloat() * 2 - 1) * stepSize * scale;
setSample(x + halfStep, y + halfStep, e);
}
}
for (int y = 0; y < w; y += stepSize) {
for (int x = 0; x < w; x += stepSize) {
double a = sample(x, y);
double b = sample(x + stepSize, y);
double c = sample(x, y + stepSize);
double d = sample(x + halfStep, y + halfStep);
double e = sample(x + halfStep, y - halfStep);
double f = sample(x - halfStep, y + halfStep);
double H = (a + b + d + e) / 4.0 + (random.nextFloat() * 2 - 1) * stepSize * scale * 0.5;
double g = (a + c + d + f) / 4.0 + (random.nextFloat() * 2 - 1) * stepSize * scale * 0.5;
setSample(x + halfStep, y, H);
setSample(x, y + halfStep, g);
}
}
stepSize /= 2;
scale *= (scaleMod + 0.8);
scaleMod *= 0.3;
} while (stepSize > 1);
Those two for loops are running some kind of sampling algorithm, and I would just like to know if this is known named algorithm, or if notch just rolled his own.
This looks like the diamond-square algorithm.

HSL to RGB color conversion

I am looking for an algorithm to convert between HSL color to RGB.
It seems to me that HSL is not very widely used so I am not having much luck searching for a converter.
Garry Tan posted a Javascript solution on his blog (which he attributes to a now defunct mjijackson.com, but is archived here and the original author has a gist - thanks to user2441511).
The code is re-posted below:
HSL to RGB:
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* #param {number} h The hue
* #param {number} s The saturation
* #param {number} l The lightness
* #return {Array} The RGB representation
*/
function hslToRgb(h, s, l){
var r, g, b;
if(s == 0){
r = g = b = l; // achromatic
}else{
var hue2rgb = function hue2rgb(p, q, t){
if(t < 0) t += 1;
if(t > 1) t -= 1;
if(t < 1/6) return p + (q - p) * 6 * t;
if(t < 1/2) return q;
if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
RGB to HSL:
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes r, g, and b are contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* #param {number} r The red color value
* #param {number} g The green color value
* #param {number} b The blue color value
* #return {Array} The HSL representation
*/
function rgbToHsl(r, g, b){
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
}else{
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
case g: h = (b - r) / d + 2; break;
case b: h = (r - g) / d + 4; break;
}
h /= 6;
}
return [h, s, l];
}
Found the easiest way, python to the rescue :D
colorsys.hls_to_rgb(h, l, s)
Convert the color from HLS coordinates to RGB coordinates.
Java implementation of Mohsen's code
Note that all integer are declared as float (i.e 1f) and must be float, else you will optain grey colors.
HSL to RGB
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* #param h The hue
* #param s The saturation
* #param l The lightness
* #return int array, the RGB representation
*/
public static int[] hslToRgb(float h, float s, float l){
float r, g, b;
if (s == 0f) {
r = g = b = l; // achromatic
} else {
float q = l < 0.5f ? l * (1 + s) : l + s - l * s;
float p = 2 * l - q;
r = hueToRgb(p, q, h + 1f/3f);
g = hueToRgb(p, q, h);
b = hueToRgb(p, q, h - 1f/3f);
}
int[] rgb = {to255(r), to255(g), to255(b)};
return rgb;
}
public static int to255(float v) { return (int)Math.min(255,256*v); }
/** Helper method that converts hue to rgb */
public static float hueToRgb(float p, float q, float t) {
if (t < 0f)
t += 1f;
if (t > 1f)
t -= 1f;
if (t < 1f/6f)
return p + (q - p) * 6f * t;
if (t < 1f/2f)
return q;
if (t < 2f/3f)
return p + (q - p) * (2f/3f - t) * 6f;
return p;
}
RGB to HSL
/**
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes pR, pG, and bpBare contained in the set [0, 255] and
* returns h, s, and l in the set [0, 1].
*
* #param pR The red color value
* #param pG The green color value
* #param pB The blue color value
* #return float array, the HSL representation
*/
public static float[] rgbToHsl(int pR, int pG, int pB) {
float r = pR / 255f;
float g = pG / 255f;
float b = pB / 255f;
float max = (r > g && r > b) ? r : (g > b) ? g : b;
float min = (r < g && r < b) ? r : (g < b) ? g : b;
float h, s, l;
l = (max + min) / 2.0f;
if (max == min) {
h = s = 0.0f;
} else {
float d = max - min;
s = (l > 0.5f) ? d / (2.0f - max - min) : d / (max + min);
if (r > g && r > b)
h = (g - b) / d + (g < b ? 6.0f : 0.0f);
else if (g > b)
h = (b - r) / d + 2.0f;
else
h = (r - g) / d + 4.0f;
h /= 6.0f;
}
float[] hsl = {h, s, l};
return hsl;
}
Short but precise - JS
Use this JS code (more: rgb2hsl, hsv2rgb rgb2hsv and hsl2hsv) - php version here
// input: h as an angle in [0,360] and s,l in [0,1] - output: r,g,b in [0,1]
function hsl2rgb(h,s,l)
{
let a=s*Math.min(l,1-l);
let f= (n,k=(n+h/30)%12) => l - a*Math.max(Math.min(k-3,9-k,1),-1);
return [f(0),f(8),f(4)];
}
// oneliner version
let hsl2rgb = (h,s,l, a=s*Math.min(l,1-l), f= (n,k=(n+h/30)%12) => l - a*Math.max(Math.min(k-3,9-k,1),-1)) => [f(0),f(8),f(4)];
// r,g,b are in [0-1], result e.g. #0812fa.
let rgb2hex = (r,g,b) => "#" + [r,g,b].map(x=>Math.round(x*255).toString(16).padStart(2,0) ).join('');
console.log(`hsl: (30,0.2,0.3) --> rgb: (${hsl2rgb(30,0.2,0.3)}) --> hex: ${rgb2hex(...hsl2rgb(30,0.2,0.3))}`);
// ---------------
// UX
// ---------------
rgb= [0,0,0];
hs= [0,0,0];
let $ = x => document.querySelector(x);
function changeRGB(i,e) {
rgb[i]=e.target.value/255;
hs = rgb2hsl(...rgb);
refresh();
}
function changeHS(i,e) {
hs[i]=e.target.value/(i?255:1);
rgb= hsl2rgb(...hs);
refresh();
}
function refresh() {
rr = rgb.map(x=>x*255|0).join(', ')
hh = rgb2hex(...rgb);
tr = `RGB: ${rr}`
th = `HSL: ${hs.map((x,i)=>i? (x*100).toFixed(2)+'%':x|0).join(', ')}`
thh= `HEX: ${hh}`
$('.box').style.backgroundColor=`rgb(${rr})`;
$('.infoRGB').innerHTML=`${tr}`;
$('.infoHS').innerHTML =`${th}\n${thh}`;
$('#r').value=rgb[0]*255;
$('#g').value=rgb[1]*255;
$('#b').value=rgb[2]*255;
$('#h').value=hs[0];
$('#s').value=hs[1]*255;
$('#l').value=hs[2]*255;
}
function rgb2hsl(r,g,b) {
let a=Math.max(r,g,b), n=a-Math.min(r,g,b), f=(1-Math.abs(a+a-n-1));
let h= n && ((a==r) ? (g-b)/n : ((a==g) ? 2+(b-r)/n : 4+(r-g)/n));
return [60*(h<0?h+6:h), f ? n/f : 0, (a+a-n)/2];
}
refresh();
.box {
width: 50px;
height: 50px;
margin: 20px;
}
body {
display: flex;
}
<div>
<input id="r" type="range" min="0" max="255" oninput="changeRGB(0,event)">R<br>
<input id="g" type="range" min="0" max="255" oninput="changeRGB(1,event)">G<br>
<input id="b" type="range" min="0" max="255" oninput="changeRGB(2,event)">B<br>
<pre class="infoRGB"></pre>
</div>
<div>
<div class="box hsl"></div>
</div>
<div>
<input id="h" type="range" min="0" max="360" oninput="changeHS(0,event)">H<br>
<input id="s" type="range" min="0" max="255" oninput="changeHS(1,event)">S<br>
<input id="l" type="range" min="0" max="255" oninput="changeHS(2,event)">L<br>
<pre class="infoHS"></pre><br>
</div>
Here is formula which I discover and precisely describe in wiki + error analysis,
The article for HSL and HSV on wikipedia contains some formulas. The calculations are a bit tricky, so it might be useful to take a look at existing implementations.
If you're looking for something that definitely conforms with the CSS semantics for HSL and RGB, you could use the algorithm specified in the CSS 3 specification, which reads:
HOW TO RETURN hsl.to.rgb(h, s, l):
SELECT:
l<=0.5: PUT l*(s+1) IN m2
ELSE: PUT l+s-l*s IN m2
PUT l*2-m2 IN m1
PUT hue.to.rgb(m1, m2, h+1/3) IN r
PUT hue.to.rgb(m1, m2, h ) IN g
PUT hue.to.rgb(m1, m2, h-1/3) IN b
RETURN (r, g, b)
HOW TO RETURN hue.to.rgb(m1, m2, h):
IF h<0: PUT h+1 IN h
IF h>1: PUT h-1 IN h
IF h*6<1: RETURN m1+(m2-m1)*h*6
IF h*2<1: RETURN m2
IF h*3<2: RETURN m1+(m2-m1)*(2/3-h)*6
RETURN m1
I believe this is the source for some of the other answers here.
C# Code from Mohsen's answer.
Here is the code from Mohsen's answer in C# if anyone else wants it. Note: Color is a custom class and Vector4 is from OpenTK. Both are easy to replace with something else of your choosing.
Hsl To Rgba
/// <summary>
/// Converts an HSL color value to RGB.
/// Input: Vector4 ( X: [0.0, 1.0], Y: [0.0, 1.0], Z: [0.0, 1.0], W: [0.0, 1.0] )
/// Output: Color ( R: [0, 255], G: [0, 255], B: [0, 255], A: [0, 255] )
/// </summary>
/// <param name="hsl">Vector4 defining X = h, Y = s, Z = l, W = a. Ranges [0, 1.0]</param>
/// <returns>RGBA Color. Ranges [0, 255]</returns>
public static Color HslToRgba(Vector4 hsl)
{
float r, g, b;
if (hsl.Y == 0.0f)
r = g = b = hsl.Z;
else
{
var q = hsl.Z < 0.5f ? hsl.Z * (1.0f + hsl.Y) : hsl.Z + hsl.Y - hsl.Z * hsl.Y;
var p = 2.0f * hsl.Z - q;
r = HueToRgb(p, q, hsl.X + 1.0f / 3.0f);
g = HueToRgb(p, q, hsl.X);
b = HueToRgb(p, q, hsl.X - 1.0f / 3.0f);
}
return new Color((int)(r * 255), (int)(g * 255), (int)(b * 255), (int)(hsl.W * 255));
}
// Helper for HslToRgba
private static float HueToRgb(float p, float q, float t)
{
if (t < 0.0f) t += 1.0f;
if (t > 1.0f) t -= 1.0f;
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
if (t < 1.0f / 2.0f) return q;
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
return p;
}
Rgba To Hsl
/// <summary>
/// Converts an RGB color value to HSL.
/// Input: Color ( R: [0, 255], G: [0, 255], B: [0, 255], A: [0, 255] )
/// Output: Vector4 ( X: [0.0, 1.0], Y: [0.0, 1.0], Z: [0.0, 1.0], W: [0.0, 1.0] )
/// </summary>
/// <param name="rgba"></param>
/// <returns></returns>
public static Vector4 RgbaToHsl(Color rgba)
{
float r = rgba.R / 255.0f;
float g = rgba.G / 255.0f;
float b = rgba.B / 255.0f;
float max = (r > g && r > b) ? r : (g > b) ? g : b;
float min = (r < g && r < b) ? r : (g < b) ? g : b;
float h, s, l;
h = s = l = (max + min) / 2.0f;
if (max == min)
h = s = 0.0f;
else
{
float d = max - min;
s = (l > 0.5f) ? d / (2.0f - max - min) : d / (max + min);
if (r > g && r > b)
h = (g - b) / d + (g < b ? 6.0f : 0.0f);
else if (g > b)
h = (b - r) / d + 2.0f;
else
h = (r - g) / d + 4.0f;
h /= 6.0f;
}
return new Vector4(h, s, l, rgba.A / 255.0f);
}
This is how I do it which is easy to remember is to think of RGB as three spokes on a wheel, 120 degrees apart.
H = hue (0-360)
S = saturation (0-1)
L = luminance (0-1)
R1 = SIN( H ) * L
G1 = SIN( H + 120 ) * L
B1 = SIN( H + 240 ) * L
The tricky part is saturation, which is to a scale down to the average of those three.
AVERAGE = (R1 + G1 + B1) / 3
R2 = ((R1 - AVERAGE) * S) + AVERAGE
G2 = ((G1 - AVERAGE) * S) + AVERAGE
B2 = ((B1 - AVERAGE) * S) + AVERAGE
RED = R2 * 255
GREEN = G2 * 255
BLUE = B2 * 255
#Php Implementation of Chris's C# Code
Also from here, which explains the math of it very well.
This is basically a bunch of functions to convert to and from HSL (Hue Saturation Lightness)
Tested and working on PHP 5.6.15
TL;DR: The full code can be found here on Pastebin.
##Hex to HSL
Input: Hex color in format: [#]0f4 or [#]00ff44 (pound sign optional)
Output: HSL in Degrees, Percent, Percent
/**
* Input: hex color
* Output: hsl(in ranges from 0-1)
*
* Takes the hex, converts it to RGB, and sends
* it to RGBToHsl. Returns the output.
*
*/
function hexToHsl($hex) {
$r = "";
$g = "";
$b = "";
$hex = str_replace('#', '', $hex);
if (strlen($hex) == 3) {
$r = substr($hex, 0, 1);
$r = $r . $r;
$g = substr($hex, 1, 1);
$g = $g . $g;
$b = substr($hex, 2, 1);
$b = $b . $b;
} elseif (strlen($hex) == 6) {
$r = substr($hex, 0, 2);
$g = substr($hex, 2, 2);
$b = substr($hex, 4, 2);
} else {
return false;
}
$r = hexdec($r);
$g = hexdec($g);
$b = hexdec($b);
$hsl = rgbToHsl($r,$g,$b);
return $hsl;
}
RGB to HSL
Input: RGB in range 0-255
Output: HSL in Degrees, Percent, Percent.
/**
*
*Credits:
* https://stackoverflow.com/questions/4793729/rgb-to-hsl-and-back-calculation-problems
* http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
*
* Called by hexToHsl by default.
*
* Converts an RGB color value to HSL. Conversion formula
* adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/.
* Assumes r, g, and b are contained in the range [0 - 255] and
* returns h, s, and l in the format Degrees, Percent, Percent.
*
* #param Number r The red color value
* #param Number g The green color value
* #param Number b The blue color value
* #return Array The HSL representation
*/
function rgbToHsl($r, $g, $b){
//For the calculation, rgb needs to be in the range from 0 to 1. To convert, divide by 255 (ff).
$r /= 255;
$g /= 255;
$b /= 255;
$myMax = max($r, $g, $b);
$myMin = min($r, $g, $b);
$maxAdd = ($myMax + $myMin);
$maxSub = ($myMax - $myMin);
//luminence is (max + min)/2
$h = 0;
$s = 0;
$l = ($maxAdd / 2.0);
//if all the numbers are equal, there is no saturation (greyscale).
if($myMin != $myMax){
if ($l < 0.5) {
$s = ($maxSub / $maxAdd);
} else {
$s = (2.0 - $myMax - $myMin); //note order of opperations - can't use $maxSub here
$s = ($maxSub / $s);
}
//find hue
switch($myMax){
case $r:
$h = ($g - $b);
$h = ($h / $maxSub);
break;
case $g:
$h = ($b - $r);
$h = ($h / $maxSub);
$h = ($h + 2.0);
break;
case $b:
$h = ($r - $g);
$h = ($h / $maxSub);
$h = ($h + 4.0);
break;
}
}
$hsl = hslToDegPercPerc($h, $s, $l);
return $hsl;
}
##HSL (0-1 range) to Degrees, Percent, Percent format
For the math calculations, HSL is easier to deal with in the 0-1 range, but for human readability, it's easier in Degrees, Percent, Percent. This function takes HSL in the ranges 0-1, and returns HSL in Degrees, Percent, Percent.
/**
* Input: HSL in ranges 0-1.
* Output: HSL in format Deg, Perc, Perc.
*
* Note: rgbToHsl calls this function by default.
*
* Multiplies $h by 60, and $s and $l by 100.
*/
function hslToDegPercPerc($h, $s, $l) {
//convert h to degrees
$h *= 60;
if ($h < 0) {
$h += 360;
}
//convert s and l to percentage
$s *= 100;
$l *= 100;
$hsl['h'] = $h;
$hsl['s'] = $s;
$hsl['l'] = $l;
return $hsl;
}
##HSL (Degrees, Percent, Percent format) to HSL in range 0-1
This function converts HSL in the format Degrees, Percent, Percent, to the ranges 0-1 for easier computing.
/**
* Input: HSL in format Deg, Perc, Perc
* Output: An array containing HSL in ranges 0-1
*
* Divides $h by 60, and $s and $l by 100.
*
* hslToRgb calls this by default.
*/
function degPercPercToHsl($h, $s, $l) {
//convert h, s, and l back to the 0-1 range
//convert the hue's 360 degrees in a circle to 1
$h /= 360;
//convert the saturation and lightness to the 0-1
//range by multiplying by 100
$s /= 100;
$l /= 100;
$hsl['h'] = $h;
$hsl['s'] = $s;
$hsl['l'] = $l;
return $hsl;
}
##HSL to RGB
Input: HSL in the format Degrees, Percent, Percent
Output: RGB in the format 255, 255, 255.
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/.
* Assumes h, s, and l are in the format Degrees,
* Percent, Percent, and returns r, g, and b in
* the range [0 - 255].
*
* Called by hslToHex by default.
*
* Calls:
* degPercPercToHsl
* hueToRgb
*
* #param Number h The hue value
* #param Number s The saturation level
* #param Number l The luminence
* #return Array The RGB representation
*/
function hslToRgb($h, $s, $l){
$hsl = degPercPercToHsl($h, $s, $l);
$h = $hsl['h'];
$s = $hsl['s'];
$l = $hsl['l'];
//If there's no saturation, the color is a greyscale,
//so all three RGB values can be set to the lightness.
//(Hue doesn't matter, because it's grey, not color)
if ($s == 0) {
$r = $l * 255;
$g = $l * 255;
$b = $l * 255;
}
else {
//calculate some temperary variables to make the
//calculation eaisier.
if ($l < 0.5) {
$temp2 = $l * (1 + $s);
} else {
$temp2 = ($l + $s) - ($s * $l);
}
$temp1 = 2 * $l - $temp2;
//run the calculated vars through hueToRgb to
//calculate the RGB value. Note that for the Red
//value, we add a third (120 degrees), to adjust
//the hue to the correct section of the circle for
//red. Simalarly, for blue, we subtract 1/3.
$r = 255 * hueToRgb($temp1, $temp2, $h + (1 / 3));
$g = 255 * hueToRgb($temp1, $temp2, $h);
$b = 255 * hueToRgb($temp1, $temp2, $h - (1 / 3));
}
$rgb['r'] = $r;
$rgb['g'] = $g;
$rgb['b'] = $b;
return $rgb;
}
###Hue to RGB
This function is called by hslToRgb to convert the hue into the separate RGB values.
/**
* Converts an HSL hue to it's RGB value.
*
* Input: $temp1 and $temp2 - temperary vars based on
* whether the lumanence is less than 0.5, and
* calculated using the saturation and luminence
* values.
* $hue - the hue (to be converted to an RGB
* value) For red, add 1/3 to the hue, green
* leave it alone, and blue you subtract 1/3
* from the hue.
*
* Output: One RGB value.
*
* Thanks to Easy RGB for this function (Hue_2_RGB).
* http://www.easyrgb.com/index.php?X=MATH&$h=19#text19
*
*/
function hueToRgb($temp1, $temp2, $hue) {
if ($hue < 0) {
$hue += 1;
}
if ($hue > 1) {
$hue -= 1;
}
if ((6 * $hue) < 1 ) {
return ($temp1 + ($temp2 - $temp1) * 6 * $hue);
} elseif ((2 * $hue) < 1 ) {
return $temp2;
} elseif ((3 * $hue) < 2 ) {
return ($temp1 + ($temp2 - $temp1) * ((2 / 3) - $hue) * 6);
}
return $temp1;
}
##HSL to Hex
Input: HSL in format Degrees, Percent, Percent
Output: Hex in format 00ff22 (no pound sign).
Converts to RGB, then converts separately to hex.
/**
* Converts HSL to Hex by converting it to
* RGB, then converting that to hex.
*
* string hslToHex($h, $s, $l[, $prependPound = true]
*
* $h is the Degrees value of the Hue
* $s is the Percentage value of the Saturation
* $l is the Percentage value of the Lightness
* $prependPound is a bool, whether you want a pound
* sign prepended. (optional - default=true)
*
* Calls:
* hslToRgb
*
* Output: Hex in the format: #00ff88 (with
* pound sign). Rounded to the nearest whole
* number.
*/
function hslToHex($h, $s, $l, $prependPound = true) {
//convert hsl to rgb
$rgb = hslToRgb($h,$s,$l);
//convert rgb to hex
$hexR = $rgb['r'];
$hexG = $rgb['g'];
$hexB = $rgb['b'];
//round to the nearest whole number
$hexR = round($hexR);
$hexG = round($hexG);
$hexB = round($hexB);
//convert to hex
$hexR = dechex($hexR);
$hexG = dechex($hexG);
$hexB = dechex($hexB);
//check for a non-two string length
//if it's 1, we can just prepend a
//0, but if it is anything else non-2,
//it must return false, as we don't
//know what format it is in.
if (strlen($hexR) != 2) {
if (strlen($hexR) == 1) {
//probably in format #0f4, etc.
$hexR = "0" . $hexR;
} else {
//unknown format
return false;
}
}
if (strlen($hexG) != 2) {
if (strlen($hexG) == 1) {
$hexG = "0" . $hexG;
} else {
return false;
}
}
if (strlen($hexB) != 2) {
if (strlen($hexB) == 1) {
$hexB = "0" . $hexB;
} else {
return false;
}
}
//if prependPound is set, will prepend a
//# sign to the beginning of the hex code.
//(default = true)
$hex = "";
if ($prependPound) {
$hex = "#";
}
$hex = $hex . $hexR . $hexG . $hexB;
return $hex;
}
Here's a fast, super-simple, branchless version in GLSL:
vec3 hsl2rgb( vec3 c ) {
vec3 rgb = clamp(abs(mod(c.x*6.0 + vec3(0.0, 4.0, 2.0), 6.0)-3.0)-1.0, 0.0, 1.0);
return c.z + c.y * (rgb-0.5)*(1.0-abs(2.0*c.z-1.0));
}
Doesn't get much shorter than that ~
Link to the original proof-of-concept: https://www.shadertoy.com/view/XljGzV
(Disclaimer: not my code!)
Here is the modified javascript function, it outputs Hue in set 0-360 degrees.
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b), min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if(max == min){
h = s = 0; // achromatic
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch(max){
case r: h = (g - b) / d ; break;
case g: h = 2 + ( (b - r) / d); break;
case b: h = 4 + ( (r - g) / d); break;
}
h*=60;
if (h < 0) h +=360;
}
return([h, s, l]);
}
alert(rgbToHsl(125,115,145));
I got this from Brandon Mathis' HSL Picker source code.
It was originally written in CoffeeScript. I converted it to JavaScript using an online converter, and took out the mechanism to verify the user input was a valid RGB value. This answer worked for my usecase, as the most up-voted answer on this post I found to not produce a valid HSL value.
Note that it returns an hsla value, with a representing opacity/transparency. 0 is completely transparent, and 1 fully opaque.
function rgbToHsl(rgb) {
var a, add, b, diff, g, h, hue, l, lum, max, min, r, s, sat;
r = parseFloat(rgb[0]) / 255;
g = parseFloat(rgb[1]) / 255;
b = parseFloat(rgb[2]) / 255;
max = Math.max(r, g, b);
min = Math.min(r, g, b);
diff = max - min;
add = max + min;
hue = min === max ? 0 : r === max ? ((60 * (g - b) / diff) + 360) % 360 : g === max ? (60 * (b - r) / diff) + 120 : (60 * (r - g) / diff) + 240;
lum = 0.5 * add;
sat = lum === 0 ? 0 : lum === 1 ? 1 : lum <= 0.5 ? diff / add : diff / (2 - add);
h = Math.round(hue);
s = Math.round(sat * 100);
l = Math.round(lum * 100);
a = parseFloat(rgb[3]) || 1;
return [h, s, l, a];
}
An hsl|a color value, set in javascript, will be instantly
converted to rgb|a All you need to do then is access the
computed style value
document.body.style.color = 'hsla(44, 100%, 50%, 0.8)';
console.log(window.getComputedStyle(document.body).color);
// displays: rgba(255, 187, 0, 0.8)
Technically, I guess, this isn't even any lines of code - it's
just done automatically. So, depending on your environment, you
might be able to get away with just this. Not that there aren't
a lot of very thoughtful responses here. I don't know what your
goal is.
Now, what if you want to convert from rbg|a to hsl|a?
HSL to RGB in Typescript
All the options above didn't work on my code in TS.
I tweak one of those and now it works as a charm:
type HslType = { h: number; s: number; l: number }
const hslToRgb = (hsl: HslType): RgbType => {
let { h, s, l } = hsl
// IMPORTANT if s and l between 0,1 remove the next two lines:
s /= 100
l /= 100
const k = (n: number) => (n + h / 30) % 12
const a = s * Math.min(l, 1 - l)
const f = (n: number) =>
l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)))
return {
r: Math.round(255 * f(0)),
g: Math.round(255 * f(8)),
b: Math.round(255 * f(4)),
}
}
With H, S,and L in [0,1] range:
ConvertHslToRgb: function (iHsl)
{
var min, sv, sextant, fract, vsf;
var v = (iHsl.l <= 0.5) ? (iHsl.l * (1 + iHsl.s)) : (iHsl.l + iHsl.s - iHsl.l * iHsl.s);
if (v === 0)
return { Red: 0, Green: 0, Blue: 0 };
min = 2 * iHsl.l - v;
sv = (v - min) / v;
var h = (6 * iHsl.h) % 6;
sextant = Math.floor(h);
fract = h - sextant;
vsf = v * sv * fract;
switch (sextant)
{
case 0: return { r: v, g: min + vsf, b: min };
case 1: return { r: v - vsf, g: v, b: min };
case 2: return { r: min, g: v, b: min + vsf };
case 3: return { r: min, g: v - vsf, b: v };
case 4: return { r: min + vsf, g: min, b: v };
case 5: return { r: v, g: min, b: v - vsf };
}
}
For when you need RGB to HSV and vice versa instead:
function rgbToHsv(r, g, b)
{
r /= 255, g /= 255, b /= 255;
var min = Math.min(r, g, b),
max = Math.max(r, g, b),
delta = max - min,
h = 0, s = 0, v = max;
if (min != max)
{
s = (delta / max);
switch (max)
{
case r: h = (g - b) / delta + (g < b ? 6 : 0); break;
case g: h = (b - r) / delta + 2; break;
case b: h = (r - g) / delta + 4; break;
}
h /= 6;
}
return [h, s, v];
}
function hsvToRgb(h, s, v)
{
var step = h / (1 / 6),
pos = step - Math.floor(step), // the hue position within the current step
m = (Math.floor(step) % 2) ? (1 - pos) * v : pos * v, // mix color value adjusted to the brightness(v)
max = 1 * v,
min = (1 - s) * v,
med = m + ((1 - s) * (v - m)),
r, g, b;
switch (Math.floor(step))
{
case 0:
r = max;
g = med;
b = min;
break;
case 1:
r = med;
g = max;
b = min;
break;
case 2:
r = min;
g = max;
b = med;
break;
case 3:
r = min;
g = med;
b = max;
break;
case 4:
r = med;
g = min;
b = max;
break;
case 5:
r = max;
g = min;
b = med;
break;
}
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
}
Unity3D C# Code from Mohsen's answer.
Here is the code from Mohsen's answer in C# targeted specifically for Unity3D. It was adapted from the C# answer given by Alec Thilenius above.
using UnityEngine;
using System.Collections;
public class ColorTools {
/// <summary>
/// Converts an HSL color value to RGB.
/// Input: Vector4 ( X: [0.0, 1.0], Y: [0.0, 1.0], Z: [0.0, 1.0], W: [0.0, 1.0] )**strong text**
/// Output: Color ( R: [0.0, 1.0], G: [0.0, 1.0], B: [0.0, 1.0], A: [0.0, 1.0] )
/// </summary>
/// <param name="hsl">Vector4 defining X = h, Y = s, Z = l, W = a. Ranges [0, 1.0]</param>
/// <returns>RGBA Color. Ranges [0.0, 1.0]</returns>
public static Color HslToRgba(Vector4 hsl)
{
float r, g, b;
if (hsl.y == 0.0f)
r = g = b = hsl.z;
else
{
var q = hsl.z < 0.5f ? hsl.z * (1.0f + hsl.y) : hsl.z + hsl.y - hsl.z * hsl.y;
var p = 2.0f * hsl.z - q;
r = HueToRgb(p, q, hsl.x + 1.0f / 3.0f);
g = HueToRgb(p, q, hsl.x);
b = HueToRgb(p, q, hsl.x - 1.0f / 3.0f);
}
return new Color(r, g, b, hsl.w);
}
// Helper for HslToRgba
private static float HueToRgb(float p, float q, float t)
{
if (t < 0.0f) t += 1.0f;
if (t > 1.0f) t -= 1.0f;
if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
if (t < 1.0f / 2.0f) return q;
if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
return p;
}
/// <summary>
/// Converts an RGB color value to HSL.
/// Input: Color ( R: [0.0, 1.0], G: [0.0, 1.0], B: [0.0, 1.0], A: [0.0, 1.0] )
/// Output: Vector4 ( X: [0.0, 1.0], Y: [0.0, 1.0], Z: [0.0, 1.0], W: [0.0, 1.0] )
/// </summary>
/// <param name="rgba"></param>
/// <returns></returns>
public static Vector4 RgbaToHsl(Color rgba)
{
float max = (rgba.r > rgba.g && rgba.r > rgba.b) ? rgba.r :
(rgba.g > rgba.b) ? rgba.g : rgba.b;
float min = (rgba.r < rgba.g && rgba.r < rgba.b) ? rgba.r :
(rgba.g < rgba.b) ? rgba.g : rgba.b;
float h, s, l;
h = s = l = (max + min) / 2.0f;
if (max == min)
h = s = 0.0f;
else
{
float d = max - min;
s = (l > 0.5f) ? d / (2.0f - max - min) : d / (max + min);
if (rgba.r > rgba.g && rgba.r > rgba.b)
h = (rgba.g - rgba.b) / d + (rgba.g < rgba.b ? 6.0f : 0.0f);
else if (rgba.g > rgba.b)
h = (rgba.b - rgba.r) / d + 2.0f;
else
h = (rgba.r - rgba.g) / d + 4.0f;
h /= 6.0f;
}
return new Vector4(h, s, l, rgba.a);
}
}
For all who said that Garry Tan solution converting incorrect from RGB to HSL and back. It because he left out fraction part of number in his code.
I corrected his code (javascript).
Sorry for link on russian languadge, but on english absent - HSL-wiki
function toHsl(r, g, b)
{
r /= 255.0;
g /= 255.0;
b /= 255.0;
var max = Math.max(r, g, b);
var min = Math.min(r, g, b);
var h, s, l = (max + min) / 2.0;
if(max == min)
{
h = s = 0;
}
else
{
var d = max - min;
s = (l > 0.5 ? d / (2.0 - max - min) : d / (max + min));
if(max == r && g >= b)
{
h = 1.0472 * (g - b) / d ;
}
else if(max == r && g < b)
{
h = 1.0472 * (g - b) / d + 6.2832;
}
else if(max == g)
{
h = 1.0472 * (b - r) / d + 2.0944;
}
else if(max == b)
{
h = 1.0472 * (r - g) / d + 4.1888;
}
}
return {
str: 'hsl(' + parseInt(h / 6.2832 * 360.0 + 0.5) + ',' + parseInt(s * 100.0 + 0.5) + '%,' + parseInt(l * 100.0 + 0.5) + '%)',
obj: { h: parseInt(h / 6.2832 * 360.0 + 0.5), s: parseInt(s * 100.0 + 0.5), l: parseInt(l * 100.0 + 0.5) }
};
};
PHP implementation of #Mohsen's code (including Test!)
Sorry to re-post this. But I really haven't seen any other implementation that gives the quality I needed.
/**
* Converts an HSL color value to RGB. Conversion formula
* adapted from http://en.wikipedia.org/wiki/HSL_color_space.
* Assumes h, s, and l are contained in the set [0, 1] and
* returns r, g, and b in the set [0, 255].
*
* #param {number} h The hue
* #param {number} s The saturation
* #param {number} l The lightness
* #return {Array} The RGB representation
*/
function hue2rgb($p, $q, $t){
if($t < 0) $t += 1;
if($t > 1) $t -= 1;
if($t < 1/6) return $p + ($q - $p) * 6 * $t;
if($t < 1/2) return $q;
if($t < 2/3) return $p + ($q - $p) * (2/3 - $t) * 6;
return $p;
}
function hslToRgb($h, $s, $l){
if($s == 0){
$r = $l;
$g = $l;
$b = $l; // achromatic
}else{
$q = $l < 0.5 ? $l * (1 + $s) : $l + $s - $l * $s;
$p = 2 * $l - $q;
$r = hue2rgb($p, $q, $h + 1/3);
$g = hue2rgb($p, $q, $h);
$b = hue2rgb($p, $q, $h - 1/3);
}
return array(round($r * 255), round($g * 255), round($b * 255));
}
/* Uncomment to test * /
for ($i=0;$i<360;$i++) {
$rgb=hslToRgb($i/360, 1, .9);
echo '<div style="background-color:rgb(' .$rgb[0] . ', ' . $rgb[1] . ', ' . $rgb[2] . ');padding:2px;"></div>';
}
/* End Test */
C++ implementation with probably better performance than #Mohsen code. It uses a [0-6] range for the hue, avoiding the division and multiplication by 6. S and L range is [0,1]
void fromRGBtoHSL(float rgb[], float hsl[])
{
const float maxRGB = max(rgb[0], max(rgb[1], rgb[2]));
const float minRGB = min(rgb[0], min(rgb[1], rgb[2]));
const float delta2 = maxRGB + minRGB;
hsl[2] = delta2 * 0.5f;
const float delta = maxRGB - minRGB;
if (delta < FLT_MIN)
hsl[0] = hsl[1] = 0.0f;
else
{
hsl[1] = delta / (hsl[2] > 0.5f ? 2.0f - delta2 : delta2);
if (rgb[0] >= maxRGB)
{
hsl[0] = (rgb[1] - rgb[2]) / delta;
if (hsl[0] < 0.0f)
hsl[0] += 6.0f;
}
else if (rgb[1] >= maxRGB)
hsl[0] = 2.0f + (rgb[2] - rgb[0]) / delta;
else
hsl[0] = 4.0f + (rgb[0] - rgb[1]) / delta;
}
}
void fromHSLtoRGB(const float hsl[], float rgb[])
{
if(hsl[1] < FLT_MIN)
rgb[0] = rgb[1] = rgb[2] = hsl[2];
else if(hsl[2] < FLT_MIN)
rgb[0] = rgb[1] = rgb[2] = 0.0f;
else
{
const float q = hsl[2] < 0.5f ? hsl[2] * (1.0f + hsl[1]) : hsl[2] + hsl[1] - hsl[2] * hsl[1];
const float p = 2.0f * hsl[2] - q;
float t[] = {hsl[0] + 2.0f, hsl[0], hsl[0] - 2.0f};
for(int i=0; i<3; ++i)
{
if(t[i] < 0.0f)
t[i] += 6.0f;
else if(t[i] > 6.0f)
t[i] -= 6.0f;
if(t[i] < 1.0f)
rgb[i] = p + (q - p) * t[i];
else if(t[i] < 3.0f)
rgb[i] = q;
else if(t[i] < 4.0f)
rgb[i] = p + (q - p) * (4.0f - t[i]);
else
rgb[i] = p;
}
}
}
Java version:
/*
Converts color from HSL/A format to RGB/A format
0 <= h <= 360
0 <= s, l, a <= 1
Based on: https://en.wikipedia.org/wiki/HSL_and_HSV#:~:text=%5Bedit%5D-,HSL%20to%20RGB%5Bedit%5D,-Given%20a%20color
*/
public RGB toRGB(double h, double s, double l, Double a) {
double c = (1 - Math.abs(2 * l - 1)) * s;
double x = c * (1 - Math.abs((h / 60) % 2 - 1));
double m = l - c / 2;
int[] RGBTag = Arrays.stream(getRGBTag(c, x)).mapToInt(e -> (int)Math.round((e + m) * 255)).toArray();
return RGB(RGBTag[0], RGBTag[1], RGBTag[2], a);
}
private double[] getRGBTag(double c, double x) {
if (h < 60) {
return new double[] {c, x, 0};
} else if (h < 120) {
return new double[] {x, c, 0};
} else if (h < 180) {
return new double[] {0, c, x};
} else if (h < 240) {
return new double[] {0, x, c};
} else if (h < 300) {
return new double[] {x, 0, c};
}
return new double[] {c, 0, x};
}
I needed a really light weight one, Its not 100%, but it gets close enough for some usecases.
float3 Hue(float h, float s, float l)
{
float r = max(cos(h * 2 * UNITY_PI) * 0.5 + 0.5, 0);
float g = max(cos((h + 0.666666) * 2 * UNITY_PI) * 0.5 + 0.5, 0);
float b = max(cos((h + 0.333333) * 2 * UNITY_PI) * 0.5 + 0.5, 0);
float gray = 0.2989 * r + 0.5870 * g + 0.1140 * b;
return lerp(gray, float3(r, g, b), s) * smoothstep(0, 0.5, l) + 1 * smoothstep(0.5, 1, l);
}
PHP - shortest but precise
Here I rewrite my JS answer (math details are there) to PHP - you can run it here
function hsl2rgb($h,$s,$l)
{
$a = $s * min($l, 1-$l);
$k = function($n,$h) { return ($n+$h/30)%12;};
$f = function($n) use ($h,$s,$l,$a,$k) {
return $l - $a * max( min($k($n,$h)-3, 9-$k($n,$h), 1),-1);
};
return [ $f(0), $f(8), $f(4) ];
}
Since colorsys isn't supported in (or currently ported to) circuitpython, if you're trying to handle this conversion on Raspberry Pi, the following works (subject to the rounding limitation to accuracy mentioned elsewhere in this thread):
def hslToRgb (h, s, l): #in range 0-1 for h,s,l
if s == 0:
r = g = b = l #achromatic
else:
def hue2rgb(p, q, t):
if t < 0: t += 1
if t > 1: t -= 1
if t < 1.0 / 6.0: return p + (q - p) * 6 * t
if t < 1.0 / 2.0: return q
if t < 2.0 / 3.0: return p + (q - p) * ((2.0 / 3.0) - t) * 6
return p
if l < 0.5:
q = l * (1 + s)
else:
q = l + s - l * s
p = 2 * l - q
r = hue2rgb(p, q, h + 1.0/3.0)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1.0/3.0)
return [round(r * 255), round(g * 255), round(b * 255)]

Resources