Three parallel beziers

Hi there.

I was wondering if there is a method to make three beziers parallel to each other with the same spacing between them. I have already made in example with three beziers.

1 Like

I would be very thankfull if someone can help me with this. The main issue is to make the ‘anchor points’ of the ‘side’ beziers in de right position.

Do you want the lines of the curves be parallel without touching?

The anchor points are known.

Set the control points proportional to the distance between the two anchor points?

1 Like

hi

1 Like

I want the lines the curves flexibel with ‘touching’. So I can change te end and begin position of the middle bezierpoints and the three beziers will still have the same spacing. That means that the position of the three begin bezierpoints (of the three beziers) has to relate to each other. Am i right?

1 Like

That’s correct

The anchor points change but the control points also need to change

I am not on a computer right now but when the distances of the control points to the anchors are proportional the same, they should be parallel, no?

Hi, Yes! Thats correct. The controll points has already a proportional distance from the middle bezier. In the attached (processing) example the red en blue dots are anchor points of the ‘side’ beziers.

1 Like

Okay

The middle Bezier is off

For the outer Beziers:

Decrease the y of the Control Points to -200

Maybe their y must be anti-proportional to the width of their Bezier

The answer is yes but the solution is not as simple as it might seem. The problem is that any curve that is “parallel and constant distance from a Bezier curve” (aka an offset curve) is not a Bezier curve so cannot be drawn using the bezier(...) function.

The solution is described here but as you can see it seems complicated.

Fortunately Processing provides the necessary functions that allow us to calculate points on the offset curve which can be used to draw the offset curve. This image shows 5 curves, the black curve is a Bezier curve and the 4 red curves are offset curves at -30, -15 ,15 and 30 pixels from the Bezier curve.

Although the red curves look like Bezier curves they are not.

Basically this sketch calculates the unit vector perpendicular to the Bezier curve and at a known position on the Bezier curve. This vector is multiplied by the offset and used to calculate the matching point on the offset curve. This is repeated many times along the length of the Bezier curve so that we can draw the offset curve.

// XY coordinate pairs for a cubic Bezier curve
float[] b0 = new float[] {340, 80, 40, 40, 360, 360, 60, 320};
// This array holds the distance of each offset curve from the Bezier curve
float[] offsets = new float[] {-30, -15, 15, 30};

void setup() {
  size(400, 400);
}

void draw() {
  background(220);
  drawBezier(b0, offsets, 200);
}

void drawBezier(final float[] bp, final float[] d, int steps) {
  stroke(0);
  strokeWeight(1);
  noFill();
  // Draw Bezier curve (black)
  bezier(bp[0], bp[1], bp[2], bp[3], bp[4], bp[5], bp[6], bp[7]); 
  // Draw offset curves (red)
  stroke(200, 0, 0);
  float prev_x = 0, prev_y = 0;
  for (int j = 0; j < d.length; j++) {
    for (int i = 0; i <= steps; i++) {
      float t = i / float(steps);
      float bx = bezierPoint(bp[0], bp[2], bp[4], bp[6], t);
      float by = bezierPoint(bp[1], bp[3], bp[5], bp[7], t);
      float tx = bezierTangent(bp[0], bp[2], bp[4], bp[6], t);
      float ty = bezierTangent(bp[1], bp[3], bp[5], bp[7], t);
      float a = atan2(ty, tx) - HALF_PI;
      if (i == 0) {
        prev_x = cos(a) * d[j] + bx;
        prev_y = sin(a) * d[j] + by;
      } else {
        float cx = cos(a) * d[j] + bx;
        float cy = sin(a) * d[j] + by;
        line(prev_x, prev_y, cx, cy);
        prev_x = cx;
        prev_y = cy;
      }
    }
  }
}
6 Likes

Thank you @quark for your solution, it helps me a lot for the project i am working for. Can you explain or give a solution why the offset curve is not ‘smooth’ anymore if I increase the distance between the beziers?

I have taken your image and added some information to explain why the red line is not smooth.

Two things to remember

  1. The black line is a cubic Bezier curve (cubic Bezier has 4 control points)
  2. The red lines are not Bezier curves, I have called them offset curves.

If we look at the offset curve nearest the Bezier curve we can see that it is created it by joining the ends of the normal vectors (green lines). Now if we increase the offset to get the second curve the normal vectors (blue lines) are longer and intersect causing the kinky curve.

This can only happen if the original Bezier curve has an inflection point. In this case the curve AB has an inflection point shown in purple.

It is easy to visually confirm whether a Bezier curve has an inflection point. If we imagine traveling along the curve from A to B we initially start turning to the left but towards the later half we have to change direction and turn right. The fact that we had to change direction meant the curve has an inflection point. If you try this on the original curves you posted you will find they have no inflection point.

If the original Bezier curve has no inflection points then the offset curve will always be smooth.

2 Likes

Hi @quark thank you for your clear vision and explenation. Unfortunately I don’t know how to fix this in code. Can you do a suggestion? Besides that I want to try to controll the distance of point A of the Bezier and the offset points seperetaly from de B points and the distance offset points. Is that possible? Kind Regards!

It seems to me that you have two requirements -

  1. the ability to draw offset curves that are parallel and constant distance from a Bezier curve
  2. the offset curve should be smooth and free from ‘kinks’

My sketch shows how how we can achieve requirement (1) but the code cannot guarantee requirement (2) is met.

So we come to your question

Unfortunately not :frowning_face:

I can’t think of any realistic approach because even if you managed to remove kinks in the offset curve it would then break requirement (1)

You can try and minimize the risk of non-smooth offset curves by

  • avoiding Bezier curves with inflection points (no inflection point means smooth offset curves for all offset values)
  • use small offsets so that normal vectors to the Bezier curve cannot intersect

I have created a small sketch where you can explore Bezier curve offsets. You can change the position of all four control points and define the maximum offset and the number of offsets. To run the sketch code below you must install the G4P library from Processing’s Contribution Manager.

import g4p_controls.*;

// XY coordinate pairs for a cubic Bezier curve
float[] b0 = new float[] {60, 320, 360, 360, 40, 40, 340, 80};
// This array holds the distance of each offset curve from the Bezier curve
float[] offsets; // Get values from GUI

public void setup() {
  size(600, 420, JAVA2D);
  createGUI();
  sdrP0.setValueXY(b0[0], b0[1]);
  sdrP1.setValueXY(b0[2], b0[3]);
  sdrP2.setValueXY(b0[4], b0[5]);
  sdrP3.setValueXY(b0[6], b0[7]);
  updateOffset();
}

public void draw() {
  background(245, 255, 220);
  drawBezier(b0, offsets, 500);
}

void drawBezier(final float[] bp, final float[] d, int steps) {
  stroke(0);
  strokeWeight(1);
  noFill();
  // Draw Bezier curve (black)
  bezier(bp[0], bp[1], bp[2], bp[3], bp[4], bp[5], bp[6], bp[7]); 
  // Draw offset curves (red)
  stroke(200, 0, 0);
  float prev_x = 0, prev_y = 0;
  for (int j = 0; j < d.length; j++) {
    for (int i = 0; i <= steps; i++) {
      float t = i / float(steps);
      float bx = bezierPoint(bp[0], bp[2], bp[4], bp[6], t);
      float by = bezierPoint(bp[1], bp[3], bp[5], bp[7], t);
      float tx = bezierTangent(bp[0], bp[2], bp[4], bp[6], t);
      float ty = bezierTangent(bp[1], bp[3], bp[5], bp[7], t);
      float a = atan2(ty, tx) - HALF_PI;
      if (i == 0) {
        prev_x = cos(a) * d[j] + bx;
        prev_y = sin(a) * d[j] + by;
      } else {
        float cx = cos(a) * d[j] + bx;
        float cy = sin(a) * d[j] + by;
        line(prev_x, prev_y, cx, cy);
        prev_x = cx;
        prev_y = cy;
      }
    }
  }
}

void updateOffset() {
  int noffs = sdrNbrOffsets.getValueI();
  float delta = sdrOffset.getValueF() / noffs;
  float offset0 = -sdrOffset.getValueF();
  float[] newOffsets = new float[2 * noffs];
  for (int i = 0; i < noffs; i++) {
    newOffsets[i] = offset0 + i * delta;
    newOffsets[2 * noffs - i - 1] = -newOffsets[i];
  }
  offsets = newOffsets;
}

public void p0Change(GSlider2D source, GEvent event) {
  b0[0] = source.getValueXF();
  b0[1] = source.getValueYF();
}

public void p1Change(GSlider2D source, GEvent event) {
  b0[2] = source.getValueXF();
  b0[3] = source.getValueYF();
}

public void p2Change(GSlider2D source, GEvent event) {
  b0[4] = source.getValueXF();
  b0[5] = source.getValueYF();
}

public void p3Change(GSlider2D source, GEvent event) {
  b0[6] = source.getValueXF();
  b0[7] = source.getValueYF();
}

public void offsetChange(GSlider source, GEvent event) {
  updateOffset();
}

public void nbrOffsetsChange(GSlider source, GEvent event) {
  updateOffset();
}

public void createGUI(){
  G4P.messagesEnabled(false);
  G4P.setGlobalColorScheme(GCScheme.BLUE_SCHEME);
  G4P.setMouseOverEnabled(false);
  surface.setTitle("Bezier Curve Offsets Explorer");
  lblP0 = new GLabel(this, 400, 0, 100, 20);
  lblP0.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblP0.setText("CP 0");
  lblP0.setOpaque(true);
  lblP1 = new GLabel(this, 500, 0, 100, 20);
  lblP1.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblP1.setText("CP 1");
  lblP1.setOpaque(true);
  lblP2 = new GLabel(this, 400, 130, 100, 20);
  lblP2.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblP2.setText("CP 2");
  lblP2.setOpaque(true);
  lblP3 = new GLabel(this, 500, 130, 100, 20);
  lblP3.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblP3.setText("CP 3");
  lblP3.setOpaque(true);
  sdrP0 = new GSlider2D(this, 400, 20, 100, 100);
  sdrP0.setLimitsX(0.5, 0.0, 400.0);
  sdrP0.setLimitsY(0.5, 0.0, 400.0);
  sdrP0.setNumberFormat(G4P.DECIMAL, 2);
  sdrP0.setOpaque(true);
  sdrP0.addEventHandler(this, "p0Change");
  sdrP1 = new GSlider2D(this, 500, 20, 100, 100);
  sdrP1.setLimitsX(0.5, 0.0, 400.0);
  sdrP1.setLimitsY(0.5, 0.0, 400.0);
  sdrP1.setNumberFormat(G4P.DECIMAL, 2);
  sdrP1.setOpaque(true);
  sdrP1.addEventHandler(this, "p1Change");
  sdrP2 = new GSlider2D(this, 400, 150, 100, 100);
  sdrP2.setLimitsX(0.5, 0.0, 400.0);
  sdrP2.setLimitsY(0.5, 0.0, 400.0);
  sdrP2.setNumberFormat(G4P.DECIMAL, 2);
  sdrP2.setOpaque(true);
  sdrP2.addEventHandler(this, "p2Change");
  sdrP3 = new GSlider2D(this, 500, 150, 100, 100);
  sdrP3.setLimitsX(0.5, 0.0, 400.0);
  sdrP3.setLimitsY(0.5, 0.0, 400.0);
  sdrP3.setNumberFormat(G4P.DECIMAL, 2);
  sdrP3.setOpaque(true);
  sdrP3.addEventHandler(this, "p3Change");
  lblOffset = new GLabel(this, 400, 260, 200, 20);
  lblOffset.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblOffset.setText("Maximum Offset Distance");
  lblOffset.setOpaque(true);
  sdrOffset = new GSlider(this, 400, 280, 200, 50, 10.0);
  sdrOffset.setShowValue(true);
  sdrOffset.setShowLimits(true);
  sdrOffset.setLimits(30.0, 20.0, 80.0);
  sdrOffset.setNbrTicks(4);
  sdrOffset.setShowTicks(true);
  sdrOffset.setNumberFormat(G4P.DECIMAL, 0);
  sdrOffset.setOpaque(true);
  sdrOffset.addEventHandler(this, "offsetChange");
  lblNbrOffsets = new GLabel(this, 400, 340, 200, 20);
  lblNbrOffsets.setTextAlign(GAlign.CENTER, GAlign.MIDDLE);
  lblNbrOffsets.setText("Nbr of offsets either side");
  lblNbrOffsets.setOpaque(true);
  sdrNbrOffsets = new GSlider(this, 400, 360, 200, 50, 10.0);
  sdrNbrOffsets.setShowValue(true);
  sdrNbrOffsets.setShowLimits(true);
  sdrNbrOffsets.setLimits(2, 1, 5);
  sdrNbrOffsets.setNbrTicks(5);
  sdrNbrOffsets.setStickToTicks(true);
  sdrNbrOffsets.setShowTicks(true);
  sdrNbrOffsets.setNumberFormat(G4P.INTEGER, 0);
  sdrNbrOffsets.setOpaque(true);
  sdrNbrOffsets.addEventHandler(this, "nbrOffsetsChange");
}

GLabel lblP0, lblP1, lblP2, lblP3, lblOffset, lblNbrOffsets; 
GSlider2D sdrP0, sdrP1, sdrP2, sdrP3;  
GSlider sdrOffset, sdrNbrOffsets; 
3 Likes

What do you mean with A and B ? Only the anchor points or the control points too?

Example

In case you need this, here is an example with one Bezier where you can drag all 4 points

  • C to clear list.

  • You can drag and drop all 4 points with the mouse.

  • Hit Enter to run a ball on the curve (Enter to stop).

  • After clearing the list, enter 4 new points with the mouse (0 and 3 are anchors, 1 and 2 control points).

ArrayList<SimplePoint> bList = new ArrayList();
String[] stringFromIndex = {
  " (anchor)", 
  " (control)", 
  " (control)", 
  " (anchor)"
}; 

boolean runPoints=false;
float t; 

// drag with mouse
SimplePoint holding; 
boolean hold=false; 

// ---------------------------------------------------------------------------------------------------

void setup() {
  size(1800, 900);
  bList = new ArrayList();

  float yPos=height-100;
  float yPos2=height-500; 

  SimplePoint myBezier = new SimplePoint(width/2-150, yPos);
  bList.add(myBezier);

  myBezier = new SimplePoint(width/2-150, yPos2);
  bList.add(myBezier);

  myBezier = new SimplePoint(width/2+150, yPos2);
  bList.add(myBezier);

  myBezier = new SimplePoint(width/2+150, yPos);
  bList.add(myBezier); 

  background(0);
}

void draw() {
  background(0);

  // Run green ball (with ENTER key)
  if (runPoints && bList.size()==4) {
    displayRunningBall();
  }

  // decoration and showing curve ---------
  // show points
  showPoints(); 

  // show Bezier
  if (bList.size()==4) {
    showBezier();
  }

  // text 
  fill(255);
  text("C to clear list.\nYou can drag and drop all 4 points with the mouse. \n"
    +"Hit Enter to run a ball on the curve (Enter to stop). "
    +"\n\nAfter clearing the list, enter 4 new points with the mouse (0 and 3 are anchors, 1 and 2 control points). \n", 
    20, 20);

  // drag mouse 
  if (hold) {
    holding.x=mouseX; 
    holding.y=mouseY;
  }//if
}//func draw()

// ---------------------------------------------------------------------

void keyPressed() {
  if (keyCode == ENTER||keyCode == RETURN) { 
    runPoints = !runPoints; 
    // reset 
    t=0;
  } else if (key=='c') {
    bList.clear();
  }
}

void mousePressed() {
  // Do we have 4 points already? 
  if (bList.size()<4) {
    // No
    SimplePoint myBezier = new SimplePoint(mouseX, mouseY);  
    bList.add(myBezier); 
    runPoints=false;
  } else 
  {
    // Yes, drag with mouse 
    for (SimplePoint bz : bList) {
      if (dist(mouseX, mouseY, bz.x, bz.y) < 10) {
        hold=true; 
        holding=bz;
      }
    }
  }//else
}//

void mouseReleased() {
  hold    = false; 
  holding = null;
}

// -------------------------------------------------------------------

void displayRunningBall() {

  int i=0;
  SimplePoint a1Point=(SimplePoint)bList.get(i);
  SimplePoint c1Point=(SimplePoint)bList.get(i+1);
  SimplePoint c2Point=(SimplePoint)bList.get(i+2);
  SimplePoint a2Point=(SimplePoint)bList.get(i+3);

  float x = bezierPoint(a1Point.x, c1Point.x, c2Point.x, a2Point.x, t/10);
  float y = bezierPoint(a1Point.y, c1Point.y, c2Point.y, a2Point.y, t/10);

  noStroke(); 
  fill(0, 255, 0); 
  ellipse(x, y, 10, 10); 

  strokeWeight (1);
  t += 0.1;
  if (t>10) {
    t=0;
  }
}// func

void showBezier() {
  int i2=0; 
  SimplePoint a1Point=(SimplePoint)bList.get(i2);
  SimplePoint c1Point=(SimplePoint)bList.get(i2+1);
  SimplePoint c2Point=(SimplePoint)bList.get(i2+2);
  SimplePoint a2Point=(SimplePoint)bList.get(i2+3);

  stroke(255);
  noFill(); 
  bezier ( 
    a1Point.x, a1Point.y, 
    c1Point.x, c1Point.y, 
    c2Point.x, c2Point.y, 
    a2Point.x, a2Point.y);
}//func 

void showPoints() {

  // show points
  int i_show_points=0;
  for (SimplePoint bz : bList) {
    bz.display( i_show_points, stringFromIndex[i_show_points] );
    i_show_points++;
  }
}

// ===================================================================

class SimplePoint {

  float x; 
  float y; 

  SimplePoint(float _x, float _y ) {
    x = _x; 
    y = _y;
  }

  void display( int i, String additionalText ) {
    stroke(255); 
    point(x, y); 
    // point(x+1, y+1);
    noFill(); 
    ellipse(x, y, 7, 7);
    fill(255);
    text(i+additionalText, 
      x+6, y+5);
  }
}//class 
//