Skip to content

Commit 6602421

Browse files
committed
Arc Length Parametrization example
1 parent 1be2cb8 commit 6602421

File tree

2 files changed

+350
-0
lines changed

2 files changed

+350
-0
lines changed
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
/*
2+
Arc Length parametrization of curves by Jakub Valtar
3+
4+
This example shows how to divide a curve into segments
5+
of an equal length and how to move along the curve with
6+
constant speed.
7+
8+
To demonstrate the technique, a cubic Bézier curve is used.
9+
However, this technique is applicable to any kind of
10+
parametric curve.
11+
*/
12+
13+
BezierCurve curve;
14+
15+
PVector[] points;
16+
PVector[] equidistantPoints;
17+
18+
float t = 0.0;
19+
float tStep = 0.004;
20+
21+
final int POINT_COUNT = 120;
22+
23+
int borderSize = 50;
24+
25+
void setup() {
26+
size(1000, 700, P2D);
27+
28+
frameRate(60);
29+
smooth(8);
30+
textAlign(CENTER);
31+
textSize(20);
32+
strokeWeight(2);
33+
34+
PVector a = new PVector( 0, 600);
35+
PVector b = new PVector( 600, 0);
36+
PVector c = new PVector(-200, 0);
37+
PVector d = new PVector( 400, 600);
38+
39+
curve = new BezierCurve(a, b, c, d);
40+
41+
points = curve.points(POINT_COUNT);
42+
equidistantPoints = curve.equidistantPoints(POINT_COUNT);
43+
}
44+
45+
46+
void draw() {
47+
48+
// Show static value when mouse is pressed, animate otherwise
49+
if (mousePressed) {
50+
int a = constrain(mouseX, borderSize, width - borderSize);
51+
t = map(a, borderSize, width - borderSize, 0.0, 1.0);
52+
} else {
53+
t += tStep;
54+
if (t > 1.0) t = 0.0;
55+
}
56+
57+
background(255);
58+
59+
60+
// draw curve and circle using standard parametrization
61+
pushMatrix();
62+
translate(borderSize, -80);
63+
64+
labelStyle();
65+
text("STANDARD\nPARAMETRIZATION", 200, 630);
66+
67+
curveStyle();
68+
beginShape(LINES);
69+
for (int i = 0; i < points.length - 1; i += 2) {
70+
vertex(points[i].x, points[i].y);
71+
vertex(points[i+1].x, points[i+1].y);
72+
}
73+
endShape();
74+
75+
circleStyle();
76+
PVector pos1 = curve.pointAtParameter(t);
77+
ellipse(pos1.x, pos1.y, 15, 15);
78+
79+
popMatrix();
80+
81+
82+
// draw curve and circle using arc length parametrization
83+
pushMatrix();
84+
translate(width/2 + borderSize, -80);
85+
86+
labelStyle();
87+
text("ARC LENGTH\nPARAMETRIZATION", 200, 630);
88+
89+
curveStyle();
90+
beginShape(LINES);
91+
for (int i = 0; i < equidistantPoints.length - 1; i += 2) {
92+
vertex(equidistantPoints[i].x, equidistantPoints[i].y);
93+
vertex(equidistantPoints[i+1].x, equidistantPoints[i+1].y);
94+
}
95+
endShape();
96+
97+
circleStyle();
98+
PVector pos2 = curve.pointAtFraction(t);
99+
ellipse(pos2.x, pos2.y, 15, 15);
100+
101+
popMatrix();
102+
103+
104+
// draw seek bar
105+
pushMatrix();
106+
translate(borderSize, 640);
107+
108+
int barLength = width - 2 * borderSize;
109+
110+
barBgStyle();
111+
line(0, 0, barLength, 0);
112+
line(barLength, -5, barLength, 5);
113+
114+
barStyle();
115+
line(0, -5, 0, 5);
116+
line(0, 0, t * barLength, 0);
117+
118+
barLabelStyle();
119+
text(nf(t, 0, 2), barLength/2, 35);
120+
popMatrix();
121+
122+
}
123+
124+
125+
// Styles -----
126+
127+
void curveStyle() {
128+
stroke(170);
129+
noFill();
130+
}
131+
132+
void labelStyle() {
133+
noStroke();
134+
fill(160);
135+
}
136+
137+
void circleStyle() {
138+
noStroke();
139+
fill(0);
140+
}
141+
142+
void barBgStyle() {
143+
stroke(220);
144+
noFill();
145+
}
146+
147+
void barStyle() {
148+
stroke(50);
149+
noFill();
150+
}
151+
152+
void barLabelStyle() {
153+
noStroke();
154+
fill(160);
155+
}
156+
157+
158+
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
This class represents a cubic Bézier curve.
3+
4+
getPointAtParameter() method works the same as bezierPoint().
5+
6+
Points returned from this method are closer to each other
7+
at places where the curve bends and farther apart where the
8+
curve runs straight.
9+
10+
On the orther hand, getPointAtFraction() and getPointAtLength()
11+
return points at fixed distances. This is useful in many scenarios:
12+
you may want to move an object along the curve at some speed
13+
or you may want to draw dashed Bézier curves.
14+
*/
15+
16+
17+
class BezierCurve {
18+
19+
private final int SEGMENT_COUNT = 100;
20+
21+
private PVector v0, v1, v2, v3;
22+
23+
private float arcLengths[] = new float[SEGMENT_COUNT + 1]; // there are n segments between n+1 points
24+
25+
private float curveLength;
26+
27+
28+
BezierCurve(PVector a, PVector b, PVector c, PVector d) {
29+
v0 = a.get(); // curve begins here
30+
v1 = b.get();
31+
v2 = c.get();
32+
v3 = d.get(); // curve ends here
33+
34+
// The idea here is to make a handy look up table, which contains
35+
// parameter values with their arc lengths along the curve. Later,
36+
// when we want a point at some arc length, we can go through our
37+
// table, pick the place where the point is going to be located and
38+
// interpolate the value of parameter from two surrounding parameters
39+
// in our table.
40+
41+
// we will keep current length along the curve here
42+
float arcLength = 0;
43+
44+
PVector prev = new PVector();
45+
prev.set(v0);
46+
47+
// i goes from 0 to SEGMENT_COUNT
48+
for (int i = 0; i <= SEGMENT_COUNT; i++) {
49+
50+
// map index from range (0, SEGMENT_COUNT) to parameter in range (0.0, 1.0)
51+
float t = (float) i / SEGMENT_COUNT;
52+
53+
// get point on the curve at this parameter value
54+
PVector point = pointAtParameter(t);
55+
56+
// get distance from previous point
57+
float distanceFromPrev = PVector.dist(prev, point);
58+
59+
// add arc length of last segment to total length
60+
arcLength += distanceFromPrev;
61+
62+
// save current arc length to the look up table
63+
arcLengths[i] = arcLength;
64+
65+
// keep this point to compute length of next segment
66+
prev.set(point);
67+
}
68+
69+
// Here we have sum of all segment lengths, which should be
70+
// very close to the actual length of the curve. The more
71+
// segments we use, the more accurate it becomes.
72+
curveLength = arcLength;
73+
}
74+
75+
76+
// Returns the length of this curve
77+
float length() {
78+
return curveLength;
79+
}
80+
81+
82+
// Returns a point along the curve at a specified parameter value.
83+
PVector pointAtParameter(float t) {
84+
PVector result = new PVector();
85+
result.x = bezierPoint(v0.x, v1.x, v2.x, v3.x, t);
86+
result.y = bezierPoint(v0.y, v1.y, v2.y, v3.y, t);
87+
result.z = bezierPoint(v0.z, v1.z, v2.z, v3.z, t);
88+
return result;
89+
}
90+
91+
92+
93+
// Returns a point at a fraction of curve's length.
94+
// Example: pointAtFraction(0.25) returns point at one quarter of curve's length.
95+
PVector pointAtFraction(float r) {
96+
float wantedLength = curveLength * r;
97+
return pointAtLength(wantedLength);
98+
}
99+
100+
101+
// Returns a point at a specified arc length along the curve.
102+
PVector pointAtLength(float wantedLength) {
103+
wantedLength = constrain(wantedLength, 0.0, curveLength);
104+
105+
// look up the length in our look up table
106+
int index = java.util.Arrays.binarySearch(arcLengths, wantedLength);
107+
108+
float mappedIndex;
109+
110+
if (index < 0) {
111+
// if the index is negative, exact length is not in the table,
112+
// but it tells us where it should be in the table
113+
// see http://docs.oracle.com/javase/7/docs/api/java/util/Arrays.html#binarySearch(float[], float)
114+
115+
// interpolate two surrounding indexes
116+
int nextIndex = -(index + 1);
117+
int prevIndex = nextIndex - 1;
118+
float prevLength = arcLengths[prevIndex];
119+
float nextLength = arcLengths[nextIndex];
120+
mappedIndex = map(wantedLength, prevLength, nextLength, prevIndex, nextIndex);
121+
122+
} else {
123+
// wanted length is in the table, we know the index right away
124+
mappedIndex = index;
125+
}
126+
127+
// map index from range (0, SEGMENT_COUNT) to parameter in range (0.0, 1.0)
128+
float parameter = mappedIndex / SEGMENT_COUNT;
129+
130+
return pointAtParameter(parameter);
131+
}
132+
133+
134+
// Returns an array of equidistant point on the curve
135+
PVector[] equidistantPoints(int howMany) {
136+
137+
PVector[] resultPoints = new PVector[howMany];
138+
139+
// we already know the beginning and the end of the curve
140+
resultPoints[0] = v0.get();
141+
resultPoints[howMany - 1] = v3.get();
142+
143+
int arcLengthIndex = 1;
144+
for (int i = 1; i < howMany - 1; i++) {
145+
146+
// compute wanted arc length
147+
float fraction = (float) i / (howMany - 1);
148+
float wantedLength = fraction * curveLength;
149+
150+
// move through the look up table until we find greater length
151+
while (wantedLength > arcLengths[arcLengthIndex] && arcLengthIndex < arcLengths.length) {
152+
arcLengthIndex++;
153+
}
154+
155+
// interpolate two surrounding indexes
156+
int nextIndex = arcLengthIndex;
157+
int prevIndex = arcLengthIndex - 1;
158+
float prevLength = arcLengths[prevIndex];
159+
float nextLength = arcLengths[nextIndex];
160+
float mappedIndex = map(wantedLength, prevLength, nextLength, prevIndex, nextIndex);
161+
162+
// map index from range (0, SEGMENT_COUNT) to parameter in range (0.0, 1.0)
163+
float parameter = mappedIndex / SEGMENT_COUNT;
164+
165+
resultPoints[i] = pointAtParameter(parameter);
166+
}
167+
168+
return resultPoints;
169+
}
170+
171+
172+
// Returns an array of points on the curve.
173+
PVector[] points(int howMany) {
174+
175+
PVector[] resultPoints = new PVector[howMany];
176+
177+
// we already know the first and the last point of the curve
178+
resultPoints[0] = v0.get();
179+
resultPoints[howMany - 1] = v3.get();
180+
181+
for (int i = 1; i < howMany - 1; i++) {
182+
183+
// map index to parameter in range (0.0, 1.0)
184+
float parameter = (float) i / (howMany - 1);
185+
186+
resultPoints[i] = pointAtParameter(parameter);
187+
}
188+
189+
return resultPoints;
190+
}
191+
192+
}

0 commit comments

Comments
 (0)