ofPath and lineTo vs. curveTo

I have a vector of ofVec3f points and I want to draw a line that curves from one to the next. Here’s the code that I’m trying in the draw function for each of the “ribbon” objects:

void chRibbon::draw(){

		ofPath p = ofPath();
		p.setStrokeColor(ribbonColor);
		p.setFilled(false);
		p.setStrokeWidth(5);
		p.setCurveResolution(200);

		ofSetColor(128);

		ofPtr<ofVec3f> startVec = points[0];
		p.curveTo(startVec->x,startVec->y,startVec->z);

		for(int i=1;i<points.size() -1; i++){
			ofPtr<ofVec3f> v = points[i];
			p.curveTo(v->x,v->y,v->z);
		}

		ofPtr<ofVec3f> endVec = points.back();
		p.curveTo(endVec->x,endVec->y,endVec->z);

		// The line only curves to endVec point if 
		// the following line is in place
		p.curveTo(startVec->x,startVec->y,startVec->z);

		p.draw();
}

The problem is that I always end up with one straight segment that does not properly join with the rest of the line, like those circled in red here.

What is causing that straight segment and how do I avoid it?

I’m reviving this question, just so that there can be an answer to it floating around the thread for anyone else who is wondering about the differences between lineTo(...) and curveTo(...). Hope someone eventually finds this useful.

If we have an empty ofPolyline p, and we do:

p.lineTo(v0); 
p.lineTo(v1); 
p.lineTo(v2); 
p.lineTo(v3); 

We end up with a line that starts at v0, heads to v1, then heads to v2 and finally ends at v3. But if we had instead done:

p.curveTo(v0); 
p.curveTo(v1); 
p.curveTo(v2); 
p.curveTo(v3); 

We end up with a curve that goes from v1 to v2. That might seem initially puzzling…

So, we have to dive under the hood. The curveTo(...) method makes Catmull-Rom splines (wiki). The relevant bit to understand is that this type of spline is defined by four points:

P1 and P2 are the endpoints of our curve, while P0 and P3 are the control that define the shape of the curve.

When using curveTo(...) with ofPolyline, there’s an internal vector that stores P0, P1, P2 and P3. When our ofPolyline is empty then the internal vector is also empty. When we call:

p.curveTo(v0); // Internal vector is [v0], so only P0 is defined 
p.curveTo(v1); // Internal vector is [v0, v1], so only P0 and P1 are defined
p.curveTo(v2); // Internal vector is [v0, v1, v2], so only P0, P1 and P2 are defined
p.curveTo(v3); // Internal vector is [v0, v1, v2, v3], so all points are defined

Only once all of those points (P0 through P4) have been defined, then a spline has been fully defined and vertices can finally be added to our ofPolyline. When curveTo(v3) is called above, then the curve between P1 (v1) and P2 (v2) is sampled (at a resolution defined by the optional parameter curveResolution). That sampling slides along the curve and adds vertices to your ofPolyline.

The curve doesn’t start at v0 and end at v3 because those points are acting as the control points (i.e. the points connected by dotted lines in the image above).

And now that we have P0 through P4 defined, when we call curveTo(...) next:

curveTo(v4); // Internal vector becomes [v1, v2, v3, v4]

The oldest point, v0, was bumped and everything shifted down. The sampling occurs again, but this time it is between v2 and v3.

2 Likes

this is a great explanation, i’ve added to the docs in the site

Ah - thanks. I’ve been thinking of working on the doc of ofPolyline once ofBook stuff is done.

Mike, great answer…thank you for taking the time to explain this. I am not sure however to prevent drawing P0 if my points are in an array. Thoughts?

int line1X[8] = {100, 100, 200, 300, 400, 500, 600, 700};
int line1Y[8] = {300, 300, 320, 300, 320, 300, 320, 300};
int line1Z[8] = {0, 0, 0, 0, 0, 0, 0, 0};
for(int i=0; i< 8; i++){
    line1.curveTo( line1X[i], line1Y[i], line1Z[i] );
}

Hey @andehlu, thanks. Does removing the duplicate of P0 from your arrays give you the result you are looking for?

int line1X[7] = {100, 200, 300, 400, 500, 600, 700};
int line1Y[7] = {300, 320, 300, 320, 300, 320, 300};
int line1Z[7] = {0, 0, 0, 0, 0, 0, 0};
for(int i=0; i<7; i++) line1.curveTo(line1X[i], line1Y[i], line1Z[i]);

With this code, P0 and P7 are not drawn since they are only ever interpreted as control points.

1 Like

Thanks for getting back to me Mike. I tried adding the duplicate to avoid the straight line that happens in the initial question above. It seems to have made it a better but you can still see the first inivisble anchor point is missing

Duplicate P0

Without Duplicate

What am I missing exactly? Thanks for your time Mike.

No problem, though I might be the one who is missing something here. You are trying to draw a curve through a series of N points, but you want point 0 and point N to only be control points and not be connected to the line?

If I run this code:

int line1X[7] = {100, 200, 300, 400, 500, 600, 700};
int line1Y[7] = {300, 320, 300, 320, 300, 320, 300};
int line1Z[7] = {0, 0, 0, 0, 0, 0, 0};
ofPolyline lineWithDuplicate;
ofPolyline lineWithoutDuplicate;

void testApp::setup(){

    for(int i=0; i<7; i++) lineWithoutDuplicate.curveTo(line1X[i], line1Y[i], line1Z[i]);

    lineWithDuplicate.curveTo(line1X[0], line1Y[0]-100, line1Z[0]);
    for(int i=0; i<7; i++) lineWithDuplicate.curveTo(line1X[i], line1Y[i]-100, line1Z[i]);

}

void testApp::draw(){
    ofBackground(0);

    ofSetColor(255);
    for(int i=0; i<7; i++) ofCircle(line1X[i], line1Y[i], line1Z[i], 5);
    for(int i=0; i<7; i++) ofCircle(line1X[i], line1Y[i]-100, line1Z[i], 5);

    lineWithoutDuplicate.draw();
    lineWithDuplicate.draw();
}

I get two polylines: the top one which has the first point added twice; and the second one which has the first point added once. Neither ends up with a straight line, and it sounds like the bottom line (without a duplicate) is what you want?

If I’m understanding what you are trying to achieve, then I’m wondering what your full source code looks like.

3 Likes

Amazing… I sorted out my issue from your code above. I (and I suspect the original poster) was using ofPath instead of ofPolyline.

Thanks for your help, Mike.

@andehlu, ah good point! I just did a little digging, and you are right. Here’s a short fix for you: call line1.setMode(ofPath::POLYLINES) right after you declare line1 as an ofPath and don’t add your first point twice. Unfortunately, this warrants a bit of explanation as to why that will fix your issue…

It is the way that ofPath works that is causing you (and the original poster) headaches. ofPath starts out using COMMAND mode. This means that anytime you call a function like curveTo(...), your path is storing that internally in a list of commands that it can use to generate a series of polylines when it needs them. This allows ofPath to have methods like setCurveResolution(...) where you can change the resolution of your curves after you’ve added them to your path.

The issue is how ofPath handles the first command that you give it. You are asking for a curve command, but ofPath will add in a moveTo command before any other command. (moveTo is basically an alias for the polyline method addVertex(...) which is used to create straight line segments.) Here’s a snippet of the source code:

void ofPath::addCommand(const ofPath::Command & command){
	if((commands.empty() || commands.back().type==Command::close) && command.type!=Command::moveTo){
		commands.push_back(Command(Command::moveTo,command.to));
	}
	commands.push_back(command);
}

So even though you are saying that you want 16 curveTo(...) commands, you are getting a polyline that has one addVertex(...) command followed by 16 curveTo(...).

When you don’t duplicate your first point, you end up with a polyline that has the following commands: straight line from P0 to P1, curve from P2 to P3, etc. When you do duplicate your first point, you end up with a polyline that has the following commands: straight line from P0 to P0, curve from P1 to P2, etc.

Long story short, by switching from the default COMMAND mode to POLYLINES mode, you will lose the ability to do things like setCurveResolution(...), but you will avoid the unnecessary addVertex(...) command.

Hope that helps.

EDIT: I just submitted this as a bug here

2 Likes

@mikewesthad Again, amazing, understandable response. I will use the setMode for now and be watching the bug conversation. Hopefully, the oF team comes out with a fix to be able to go back to true ofPath.

Thanks you so much.

Mike’s original explanation was hugely helpful to me, as well - thanks.

2 Likes