14

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?

nebuch
  • 6,475
  • 4
  • 20
  • 39
  • It's a very good question, but I'm not sure it has an answer. A Bezier isn't calculated from its X or Y coordinate, but from an independent variable T that isn't correlated with either. – Mark Ransom Aug 01 '15 at 02:14
  • I speculated that you could calculate the step-length in Bézier's parametric equation so that it would be one-pixel long, then plot each step, but the length-calculation for a Bézier is crazy expensive. – nebuch Aug 01 '15 at 02:19
  • The expensive part of length calculations is `Math.sqrt`. Instead, you can (1) plot points along the curve, (2) convert each [x,y] to integers, (3) if the currently calculated integer [x,y] is not the same as the previously calculated integer [x,y] then the current [x,y] is unique and should be added to the solution set of points along the curve. I've posted an example of this relatively efficient solution. :-) – markE Aug 01 '15 at 04:52

4 Answers4

11

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.

Edward Doolittle
  • 4,002
  • 2
  • 14
  • 27
  • but is only worked out for quadratic curves, and limited to those curves that have no sign change in their gradient. So it's a nice toy implementation, but not very useful without additional code to segment a curve into such subsections. – Mike 'Pomax' Kamermans Aug 01 '15 at 16:56
  • @MPK The additional link I just posted has the information you want, I believe. – Edward Doolittle Aug 01 '15 at 18:33
  • not so much "I want" as "is crucial to properly answer the original question" =) Nice link though, although the author does point out their solution for cubic curves is not complete, so there *is* that to consider. – Mike 'Pomax' Kamermans Aug 01 '15 at 18:35
  • The licensing for the code there is ambiguous. Like it contains statements such as: "There is no copyright to the source code (GPL) so they can be used and modified for own applications." That's for the colorfilter application. The Bresenham code contains no licensing indications anywhere. Which means it's all rights reserved copyright. --- I have emailed for clarification. – Tatarize Jun 16 '19 at 21:24
  • I emailed the author and he said MIT licensing if fine. "'Free and open source' means you can do anything with it like the MIT licence." – Tatarize Dec 05 '19 at 11:29
7

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.

enter image description here

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>
markE
  • 102,905
  • 11
  • 164
  • 176
  • 1
    I foresee three problems. First you can't guarantee 1000 is oversampling. If it's undersampling you have 1000 non-contiguous pixels that do not form a complete anything. Secondly you oversample in the first place, that's a waste. If my curve is literally, (0,0), (0,0), (0,0), then I've done something rather bad with my time. And third, Bezier curves travel at different speeds with regard to `t` it's entirely possible given this fact that I still undersampled, even when I do demonstratively remove pixels. I might actually do both. – Tatarize Jun 16 '19 at 19:13
3

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.

Mike 'Pomax' Kamermans
  • 49,297
  • 16
  • 112
  • 153
1

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:

  1. Put your cursor at the first control point, find the surrounding pixel.
  2. For each side of the pixel (four total), check when your bezier curves intersects its line by solving quadratic equations.
  3. Among all the calculated side intersection times, choose the one which will happen strictly in future, but as early as possible.
  4. Move to neighboring pixel depending on which side was best.
  5. Set current time to time of that best side intersection.
  6. 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.

stgatilov
  • 5,333
  • 31
  • 54