Chapter 58 Bézier Curves 2
So far you have had a nice gentle introduction to the linear, quadratic and cubic Bézier curves.
You should all now be familiar with the fact that a cubic Bézier, the one inbuilt to the Processing language, needs two anchor points, where the curve start and finish and in addition, two control points. I like to think of these Bézier control points as being analogous to little magnetic attractors. When drawing a Bézier curve a little blob of ink is pulled along a curve by these magnetic attractors before finally settling down at the end point. In the last chapter you were introduced to using the
‘Bézier’ function to draw a cubic Bézier curve. In this chapter we will introduce new Bézier functions which will allow you to place some graphical object at some point on that curve, allowing us to have some cool looking animations. We will also show you how to use groups of Bézier curves to define more complex shapes.
Approximating a Polynomial function with a group of Cubic Béziers
This example sketch shows how we can use two Bézier curves to approximate a polynomial function. Because Processing uses Cubic Bézier curves, we can use this to approximate polynomials with two turning points. A quadratic curve such as y=4x2 + 2x + 3 has one turning point. A cubic polynomial (one which has x3 terms) has at most two turning points. In our example below we will use two Bézier curves, joined by a common anchor point to make a more complex shape, a curve with three turning points. Also in this application, we compare the joined Bézier plotted curve with a simple function that plots a similar curve using the point function.
Let’s crack on with the code!
[approxPolyBezSimple.pde]
// Constants
final int kCtrlPointRadius = 4 ;
final float ratio = 5.5734533E-9 ;
final int displayWidth=400,displayHeight=400 ;
static final PVector [] cPoints =
{
new PVector(9, 153), // Anchor Point1
new PVector(48, 315), // Control for 1st Curve
new PVector(77, 297), // Control for 1st Curve
new PVector(146, 233),// Shared Anchor Point
new PVector(226, 159),// Control Point for 2nd Curve
new PVector(268, 219),// Control Point for 2nd Curve
new PVector(310, 177) // End Anchor Point.
} ;
The first few lines of our sketchbook define our constants. The variable kCtrlPointRadius is used to define the radius of our Bézier control and anchor points, which are displayed on the Bézier plot section of the code.
The ratio variable is simply used to scale our polynomial values to fit on the sketch canvas. The displayWidth and displayHeight variables are used as the dimensions of our sketch canvas.
We then go on to define the Bézier curve data, which is two cubic Bézier curves. Remember a Bézier curve in Processing is defined with two anchor points and two control points. Two Bézier curves would normally be defined with twice as many values, 8 data points. Since we are joining our curves to make one seemless curve we will be sharing one anchor point. This means our two joined curves only need 7 data points.
For the sake of programming simplicity you will notice that I use the same array variable cPoints to store both anchor and control points for our two Bézier curves. The application makes use of another Processing type known as the PVector. The PVector type is very useful for storing 2D and 3D points. It is also useful for representing 2D and 3D vectors, something I will go into more detail, later on in the chapter. For now we will only need to use the PVector to store the x and y position of our Bézier Curve’s anchor and control points. The PVector constructor we use in this application has two parameters needed to store the x,y position. The first parameter is the x position, the second is the y position.
To go back to our application, I have placed comments in the portion of code that defines the data for our Bézier curves to make it clear which are anchor and control points.
The anchor point at x = 146 and y = 233, initialised by use of the PVector(146,233) statement, is special in that it is used as an end anchor point for the first Bézier curve and a start anchor point for the second Bézier curve.
The two control points, PVector(77,297) and PVector(226,159) that surround this shared anchor point have a common relationship which you may notice when you run the application. The three points are all on a straight line. Mathematicians call sets of points that lie on a common straight line, colinear. Why is this colinear property so important to our joined curve?
To find an answer to this question, try changing the shared anchor point to something like PVector(146,270). You may also find it best to comment out the function call to drawControlPoints() in the draw() function. This will remove the plotting of the control and anchor points. Once you have modified and run the application you should notice the ugly kink in the joined curve. The colinear property is very useful to keep our joined Bézier curves smooth, something I will show you how to achieve with a simple function, at the end of this chapter.
We now go on to the next part of the application.
boolean drawPoly = false ;
void setup()
{
size(displayWidth, displayHeight,JAVA2D);
loop() ;
}
void draw()
{
fill( 255, 255, 255, 255 ) ;
rect( 0, 0, width, height ) ;
if (drawPoly)
{
drawPoly() ;
} else
{
drawControlPoints() ;
drawJoinedBézier() ;
}
}
The boolean variable drawPoly, used in our draw() callback, is used to toggle between drawing the Bézier curves and our polynomial function. Note in the draw() function how we either draw the Polynomial or the joined Bézier Curves with the Bézier control points.
The drawJoinedBézier() function draws the two Bézier curves in two different colours, using the data in the array cPoints. Note the last anchor point in the first Bézier call cPoints[3], is the first anchor point in the second Bézier call.The PVector cPoints[3] is our shared anchor point, used in both Bézier curves.
void drawJoinedBézier()
{
stroke(0,255,0) ;
noFill() ;
Bézier(
cPoints[0].x, cPoints[0].y,
cPoints[1].x, cPoints[1].y,
cPoints[2].x, cPoints[2].y,
cPoints[3].x, cPoints[3].y) ;
// Draw 2nd Bézier Curve -
stroke(0,0,255) ;
noFill() ;
Bézier(
cPoints[3].x, cPoints[3].y,
cPoints[4].x, cPoints[4].y,
cPoints[5].x, cPoints[5].y,
cPoints[6].x, cPoints[6].y) ;
}
The calcFunction is used to calculate the y value of the polynomial function
53x4+1950x3-1019530x2+60750000x
Any returned value used will be scaled down and translated to fit to sketch canvas in our drawPoly() function
float calcFunction(float x)
{
float xs = x*x ; // x squared term
float xc = xs*x ; // x cubed term
float xf = xc*x ; // x to power of 4 term
return (53*xf+1950*xc-1019530*xs+60750000*x) ;
}
Our drawPoly() function is the main portion of code to plot our polynomial function, after clearing the sketch canvas we save the current transformation state, ie. scale, translation and rotation, used prior to this call. The function pushMatrix() saves this transformation which is later restored by use of the function call popMatrix(). The translate() function is then used to set the origin for any drawing to the centre of the canvas. Plotting the Polynomial is then done by simply going through all possible x positions from the new origin. Note how we negate and scale the returned y value.
void drawPoly()
{
stroke(0,0,0) ;
fill(0,0,0) ;
pushMatrix() ;
translate(displayWidth/2,displayHeight/2) ;
strokeWeight(2.5) ;
smooth() ;
for (float x=-displayWidth/2 ; x < displayWidth/2 ; x=x+1)
{
float y = calcFunction(x) ;
point(x,-y*ratio) ;
}
popMatrix() ;
}
To highlight the Bézier curves control and anchor points we define the function drawControlPoints. This simply goes through the cPoints array drawing circles at each defined PVector point.
// Draw Control Point lines
// from our four Vector points
void drawControlPoints()
{
// Currently problems with Processing 1.0
// casting the type to PVector []
for(int i = 0 ; i < cPoints.length ; i++)
{
PVector pv = cPoints[i] ;
fill(0,0,0) ;
stroke(0,0,0) ;
ellipseMode(RADIUS) ;
ellipse(pv.x, pv.y, kCtrlPointRadius,kCtrlPointRadius) ;
}
}
Finally we make use of the mouse pressed event, which triggers the calling of the method mousePressed(). This simple toggles the drawPoly boolean variable from false to true or vice-versa.
public void mousePressed()
{
drawPoly = !drawPoly ;
}
When the sketchbook is running, clicking on the mouse button will toggle between the Bézier plot and the polynomial plot. Make sure the mouse is over the sketchbook canvas for your mouse clicks to have any effect. Viewing both curves, you should notice how well the set of Bézier curves approximates the Polynomial function. You should also notice the gaps in our Polynomial plot which is down to the fact that we can are only plotting at the discreet points on our sketchbook canvas.
Okay, I have to admit this application is a little contrived in the sense that the close mapping of our polynomial function with our joined Bézier curves will ‘fall apart’ if we change the displayWidth and displayHeight values in our setup() function. Our variable values and Bézier curve data are very much ‘hand-crafted’ to make sure everything fits. However, what the program should demonstrate, is how we can use groups of Bézier curves to approximate any continuos polynomial function.
“Hold on!” I hear you cry, “Why not always use a polynomial function to draw or animate curves with more than two turning points”
A good question! And to be honest, apart from the problem of ‘filling in the gaps’- for very simple processing applications you are probably better off using a polynomial function or even the Processing ‘Curve’ function which allows you to define the points on a curve.
However, there are a couple of caveats.
Finding the equation of a unknown polynomial for those functions which have more than two turning points is difficult, whereas using a simple package to move, add and delete control points is very intuitive and simple. After a few goes with manipulating with a Bézier curve’s control points - you will start to predict the shape of curve the points generate.
Bare in mind that future versions of Processing may use one part of the computer’s video hardware, the ‘graphical processing unit’ (GPU) to calculate and render graphics.
Because the Bézier family of functions are inherently very simple, it is possible to use a GPU to render these curves at a wickedly fast rate, much faster than your computer’s Central Processing Unit (CPU).
Bézier curves have been used in all aspects of games programming - from defining the shape of 3D creatures and objects to defining and calculating 3D and 2D movements of objects such as ‘Enemy Objects’ and ‘Missiles’. In fact, take a look at any 3D game on your PS3, XBox or Wii (gaming console) and it is a sure bet that the camera movements which track round a scene at the start of each game level will be using the good old Bézier curve. Allez, Allez Monsieur Bézier !
Bézier curves are ideal for defining the many curves found on 2D and 3D objects because of their simplistic storage requirement. In 3D terms, only 12 numbers (4 Vectors) are needed to define a simple curve which can have up to two turning points. Joining groups of Bézier curves will allow us to define very complex shapes, something I intend to show with our next sketchbook application ‘Flower and Bees’. The application shows a group of animated Bees hovering around an animated flower. The Bees flight path is a series of joined Bézier curves and the main parts of the flower are made up of petals and sepals, drawn using Bézier curves (see diagram below) in which the anchor and control points can be calculated mathematically, given a finite number of variables. To reduce calculations we draw our flower from a fixed origin and therefore each petal, made up from a single cubic Bézier curve,has its anchor points calculated using three variables
Torus radius,
Petal Spread angle
Petal axis angle
The control points for each petal are then calculated using two additional variables
The tilt angle
The Petal ‘Length’
The Sepals are calculated in an identical way to the Petals.
Bézier Petal[Bézierpetal.graffle,Bézierpetal.pdf]
The Flower and Bees application will demonstrate the power of joined Béziers in animation and introduce us to a new Bézier function BézierPoint(). The BézierPoint method is used to return an x,y or z component value for a given Bézier curve at some given point represented by a single value. This single value, sometimes known as the parametric value, ranges from 0 for the first anchor point to 1 for the last anchor point. Any value between 0 and 1 will return a component value on the given Bézier curve. The follow example code will help to explain.
// Example Application which highlights the use of the BézierPoint function
final PVector [] cpts =
{
new PVector(85,25),
new PVector(20,20),
new PVector(80,80),
new PVector(15,80)
} ;
noFill() ;
Bézier( cpts[0].x,cpts[0].y,
cpts[1].x,cpts[1].y,
cpts[2].x,cpts[2].y,
cpts[3].x,cpts[3].y) ;
fill(255);
for (float t = 0.0 ; t <= 1.0 ; t+=0.2)
{
float x = BézierPoint(cpts[0].x, cpts[1].x, cpts[2].x, cpts[3].x, t) ;
float y = BézierPoint(cpts[0].y, cpts[1].y, cpts[2].y, cpts[3].y, t) ;
ellipse(x, y, 5, 5);
}
[BézierPointExample.pde]
In the above code we use an array of PVectors, cpts, to hold a 2D Bézier curve which is plotted with the Bézier() function. We finally work out the x and y components on this curve using six parametric values (0.0, 0.2, 0.4, 0.6, 0.8 and 1.0). At each of these 6 places a small ellipse is plotted. So it can been seen that the BézierPoint function is very useful for drawing at arbitrary points on a Bézier curve. We will use the BézierPoint method in our final example application to draw a Bee on a flight path at some point in time.
Before going into the nitty gritty of our second application, I think this is an appropriate point in our text to go through a couple of classes which are used in this application, in finer detail. Namely the PVector and PMatrix2D classes.
The PVector class is a useful set of methods and properties to describe two or three dimensional vectors. So far in this chapter, we have used it to store two dimensional screen display positions, but it could easily be used to store other types of variables such as velocity and acceleration.
The methods defined in the PVector class are commonly used Vector operations such as adding, subtracting and scaling vectors Similar to our last demo application we will using PVector to store control and anchor points of Bézier curves. We will also be using some of PVector’s operations/methods.
The PVector constructor has three forms -
PVector()
PVector(float x,float y)
PVector(float x,float y,float z)
which allows us to define 2D and 3D Vectors, initialised with given values. The empty constructor PVector() initialises the x,y and z values to be 0. The 2D constructor PVector(float x,float y) initialises the z value to be 0.
Despite the fact that we are only going to be viewing a diagram representing a 2D plane, we shall be using all three constructors because we are going to use a basic PVector operation, the PVector ‘cross’ method, to calculate a tangent to a circle. The trick to calculating the tangent to a circle is cross product the PVector which represents a point on the circumference of the circle where we want to calculate the tangent with a PVector which is perpendicular to the 2D Plane which our circle is lying on.
Because we are defining points on our circle in terms of x and y, any PVector with zero values for x and y but has a non zero z component is perpendicular to our circle.
To make calculations simpler, we will first define our circle of having unit radius around a fixed origin (0,0). ‘Unit’ radius means that the magnitude of the line from the centre of the circle to the circumference is one unit.
So, PVector(0,1) PVector(1,0) and PVector(1∕√2, 1∕√2) all lie on a circle of radius 1 around the centre PVector(0,0). To find a 2D tangent to the circle at the point (1∕√2, 1∕√2) we simple apply the cross product to this circumference point with the 3D point (0,0,1). Remember the PVector stores any uninitialised z value as 0, which means that our 2D circumference point (1∕√2, 1∕√2) is the same as the 3D point (1∕√2, 1∕√2, 0).
The PMatrix2D class is used to apply rotations, scaling and translation transformations on PVectors. We will use it to calculate the control points. PMatrix2D is a very convenient wrapper to store transformations, allowing the programmer to quickly apply repetitive operations. Before going into the Bees and Flower application proper, it may help if we go through a simple example.
[titleTangent.pde]
float axisAngle = 0 ;
float tiltAngle = 0 ;
float tiltLength = 4 ;
int scalesize = 40 ;
void setup()
{
size(400,400,JAVA2D) ;
loop() ;
}
// Draw a Circle with 'unit' radius
// Draw a radial line at a given angle (axisAngle)
// Draw the tangent vector to this radial line
// Draw a line tilted from this tangent -
// from a given angle (tiltAngle) of
// a given length tiltLength
void draw()
{
fill(255) ;
rect(0,0,400,400) ;
pushMatrix() ;
translate(200,200) ;
scale(scalesize,scalesize) ;
strokeWeight(1/scalesize) ;
stroke(255,0,0) ;
noFill() ;
ellipseMode(RADIUS);
ellipse(0,0,1,1) ; // draw unit circle
PVector unitRadiusLine = new PVector(sin(axisAngle),cos(axisAngle)) ;
line(0,0,unitRadiusLine.x,unitRadiusLine.y) ; // Draw a radius line
// work out tangentLine vector - by cross product with
// unit vector along z axis.
// Since both Vectors (unitRadiusLine and PVector(0,0,1)
// are of unit length, then the resultant vector
// tagentLine will be of unit length!
PVector tangentLine = unitRadiusLine.cross(new PVector(0,0,1)) ;
println("tangent Line Magintude "+tangentLine.mag()) ;
// The magnitude of the tangent line should be 1.0
// Any discrepancies are down to rounding errors in Processing and Java
// draw tangent line on the circumference point
line(unitRadiusLine.x,unitRadiusLine.y,
unitRadiusLine.x+tangentLine.x,unitRadiusLine.y+tangentLine.y) ;
// Produce a tilted line from our existing tangent line
// We do this by producing a simple Rotation Matrix
// which, when we multiply by the tangent line Vector
// will give a new transformed vector, the tilted line.
PMatrix2D mat = new PMatrix2D() ;
mat.rotate(tiltAngle) ;
PVector unitTiltLine = new PVector() ;
mat.mult(tangentLine,unitTiltLine) ; // title tangent line to produce new tilt line
println("tiltLine magnitude "+unitTiltLine.mag()) ;
stroke(0,255,0) ; // draw tilt line in green
// note how we can multiply by tiltLength because the
// magnitude of tiltLine will be unit length
line(unitRadiusLine.x,unitRadiusLine.y,
unitRadiusLine.x+tiltLength*unitTiltLine.x,unitRadiusLine.y+tiltLength*unitTiltLine.y) ;
// we could have easily removed the scale by tiltLength here by
// using mat.scale(tiltLength) call
// after the mat.mult() call
// next update - adjust axis
axisAngle += (TWO_PI/20) ;
tiltAngle += (TWO_PI/400) ;
popMatrix() ;
delay(250) ; // delay drawing for 250ms
}
The above demo application draws a circle with unit radius, a radial line and a tangent to that point where the radial line touches the circumference. It also draws a line which is slightly tilted from the tangent line. This tilted line is of a fixed length, set by the variable tiltLength. You will notice the setscale() function is used to scale up our rendering. A PMatrix2D object is used to calculate the tilted PVector. First we create a PMatrix2D object and then set the tilt rotation.
PVector unitTiltLine = new PVector() ;
mat.mult(tangentLine,unitTiltLine) ; // title tangent line to produce new tilt line
The PMatrix2D mult() function will produce a PVector which is tilted by relative to the fixed origin (0,0). In order to draw the tilted line, we have to remember to add the x,y position of the point where the radial line touches the circumference.
line(unitRadiusLine.x,unitRadiusLine.y,
unitRadiusLine.x+tiltLength*unitTiltLine.x,unitRadiusLine.y+tiltLength*unitTiltLine.y) ;
You will notice in the above line of code that we add the relative x and y components to the calculated tilt line vector and scale it up, by multiplying by the required length, tiltLength.
We could have done all this in one composite PMatrix2D object, by making use of the PMatrix2D.scale() and PMatrix.translate() methods. [ NOTE TO ED - Not sure about coding style for Class Methods here]
Okay, you should now have the prerequisite knowledge to go through the Bees and Flower application proper. Here we go!
[BeesAndFlowerNew.pde]
This application draws 3 characature animated bees, flying around an animated flower. A bee is made of of several graphic primitives, (triangle, ellipse and circle). A bee’s flight path is made up from a series of four joined Bézier curves. A single flower is drawn, consisting of a circle and several Bézier curves.
First we define a few application constants and run time variables
// Flower Constants
final float kTorusRadius = 80.0f ;
final float kTilt = 80.0f ;
final float kPetalLength = 250.0f ;
final float kPetalSpreadAngle = 40.0f ;
final float kPetalStep = 30.0f ;
// Flower Auto Animation Constants
final float kTiltPeriod = 10.0f ;
final float kLowestTilt = 40.0f ;
final float kHighestTilt = 96.0f ;
final float kDegreesPerSecond = 12.0f ;
// Bee Path Flight constants
final int NUMBEROFBEES = 3 ;
final float flightDuration = 16.0f ;
final float flightLengthFactor = 250.0f ;
// global coordinates used to centre our flower and bees.
float CX,CY ;
// Flower variables
float animTime,torusRadius,tilt,petalLength,petalSpreadAngle,petalStep,numpetals ;
float phase = 0.0 ;
// Flower Colour
color petalCol = color(255,0,0,100) ;
color sepalCol = color(0,200,0,150) ;
color torusCol = color(255,255,0,120) ;
// Bee Colours
color yellow = color(255,255,0,255) ;
color black = color(0,0,0,255) ;
color white = color(255,255,255,255) ;
color pWhite = color(200,200,200,100) ;
// Bee Paths. 4 Bézier Curves
// Each Bézier Curve consists of 4 PVectors
PVector [][] flightPath = new PVector[4][4];
// Path Flight variables for each Bee
float [] roffset = new float[NUMBEROFBEES] ;
float [] time = new float[NUMBEROFBEES] ;
float [] rspeed = new float[NUMBEROFBEES] ;
The variable flightPath is an array of PVectors used to stores the bees flight path. This is made from a series of 4 Bézier curves. Since each Bézier curve needs four points (two anchor points and two control points), we use a two dimensional array.
The array time is used to stored the total flight time for each bee. The variable rspeed is the array used to store a speed factor constant for each of our bees. Finally the array roffset is used to start our bees at different points on the flight path, so the all do not appear clumped together.
The setup function initialises our main variables and sets up a flight path for the bees to travel on. A bee’s flight path is defined using two variables, flightWidth and flightRadius.
The bees are initialised on different Bézier sections by setting their own roffset variable.
They are also given different speeds.
void setup()
{
torusRadius = kTorusRadius ;
tilt =kTilt ;
petalLength = kPetalLength ;
petalSpreadAngle = kPetalSpreadAngle ;
petalStep = kPetalStep ;
numpetals = 360.0f / petalStep ;
int displayHeight = 3*screen.height/4 ;
int displayWidth = 3*screen.width/4 ;
CX = displayWidth / 2 ;
CY = displayHeight / 2 ;
float flightRadius = flightLengthFactor ;
float flyWidth = 3.5*flightLengthFactor ;
// Define flightPath as 4 Cubic Bézier Curves
flightPath[0][0] = new PVector(flightRadius/2,0) ;
flightPath[0][1] = new PVector(-flyWidth/2,-flightRadius) ;
flightPath[0][2] = new PVector(flyWidth/2,-flightRadius) ;
flightPath[0][3] = new PVector(-flightRadius/2,0) ;
flightPath[1][0] = new PVector(-flightRadius/2,0) ;
flightPath[1][1] = new PVector(-flyWidth/2,flightRadius) ;
flightPath[1][2] = new PVector(-flyWidth/2,-flightRadius) ;
flightPath[1][3] = new PVector(-flightRadius/2,0) ;
flightPath[2][0] = new PVector(-flightRadius/2,0) ;
flightPath[2][1] = new PVector(flyWidth/2,flightRadius) ;
flightPath[2][2] = new PVector(-flyWidth/2,flightRadius) ;
flightPath[2][3] = new PVector(flightRadius/2,0) ;
flightPath[3][0] = new PVector(flightRadius/2,0) ;
flightPath[3][1] = new PVector(flyWidth/2,-flightRadius) ;
flightPath[3][2] = new PVector(flyWidth/2,flightRadius) ;
flightPath[3][3] = new PVector(flightRadius/2,0) ;
for (int i = 0 ; i < roffset.length ; i++)
{
roffset[i] = random(flightDuration);
rspeed[i] = 0.5 + random(4) ;
time[i] = 0.0f ;
}
size(displayWidth, displayHeight) ;
loop() ;
}
The draw function, first calculates the time passed. We have cheated a little bit here - since we are only making use of the Processing reserved variable, frameRate. This is good enough approximation for our purpose. The total animation time is updated using variables dt and animTime before going on to changing some drawing variables for our flower. We change the flower’s tllt variable in a periodic way which allows us to animate our flower. The periodic function sinFunction which we have defined allows us to change the flower’s petal tilt angle over a period of time. We finally go onto drawing the flower and the bees.
void draw() {
// calculate time passed since last update.
// using processing's frameRate is a quick approximation.
float dt = 1/frameRate ;
// Change the angle of the whole flower
phase = phase + kDegreesPerSecond*dt ;
// Change Petal tilt over time
animTime += dt;
tilt = sinFunction(kLowestTilt,kHighestTilt,animTime,kTiltPeriod,0.0f) ;
smooth() ;
fill( 255, 200, 255, 255 ) ;
rect( 0, 0, width, height ) ;
pushMatrix() ;
translate(CX, CY) ;
drawFlower(torusCol,torusRadius,petalStep,numpetals,petalSpreadAngle,tilt,petalLength,phase) ;
popMatrix() ;
// finally, draw all our bees
drawBees(dt) ;
}
// sinFunction - Helper function for
// auto animation of Parameter values
// Parameters are changed over a
// 'period' of time (in seconds)
// From startValue to endValue - optional phase
// timeF is the current Time in seconds
float sinFunction(float startValue,float endValue,float timeF, float period, float phase)
{
float yI = (startValue +endValue) / 2 ;
float freq = 1.0f / period ;
float amp = endValue - yI ;
return yI+amp*sin(TWO_PI*timeF*freq + phase) ;
}
The function degreesToRadians allows us to define our patterns and movement in degrees.
// Support function
// which converts Degrees to
// Radians
float degreesToRadians(float th)
{
return PI * th /180.0f ;
}
The drawPetal function is used to draw a single Petal.
// Draw a Cubic Bézier Petal from
// four Vector points
void drawPetal(PVector [] cps,color col)
{
fill(col) ;
stroke(0,0,0) ;
Bézier (cps [0].x,cps[0].y,cps[1].x,cps[1].y,cps[2].x,cps[2].y,cps[3].x,cps[3].y) ;
}
// Draw Control Point lines
// from our four Vector points
void drawControlPoints(PVector[] cps)
{
stroke(255,0,0) ;
line(cps[0].x,cps[0].y,cps[1].x,cps[1].y) ;
stroke(0,255,0) ;
line(cps[2].x,cps[2].y,cps[3].x,cps[3].y) ;
}
The function calcControl points is one of our main functions, used to calculate the two anchor points and two control points used to draw a petal and sepal. On each axis of symmetry (see diagram of Bézier petal) an anchor point and control point is calculated using the function calcPetalPointPair. The final points which go to make our Bézier petal are returned as an array of four PVectors.
// Calculate Anchor points and Control points for complete Bézier Curve
// returns an Array of 4 PVectors @array[0] and @array[3] are
// start and finish anchor points
// @array[1] and @array[2] are the control points
PVector [] calcControlPoints(float axisAngle,float petalAngle,float radius,
float tiltAngle,float petalLength)
{
// store anchor points and control points in one array
PVector [] bezControlPoints = new PVector[4] ;
float halfPetalAngle = petalAngle / 2.0f ;
PVector[] cPoints = calcPetalPointPair(axisAngle- halfPetalAngle,tiltAngle,petalLength) ;
PVector startPt = cPoints[0] ;
PVector fPoint = cPoints[1] ;
// calcPetalPointPair returns relative coordinates
// around the origin (0,0)
// Coordinates are based around a circle of 'unit' radius
// so we need to scale up our results by multiplying by the
// radius of our torus
bezControlPoints[0] = new PVector(startPt.x * radius,startPt.y * radius) ;
bezControlPoints[1] = new PVector(startPt.x * radius + fPoint.x,startPt.y * radius + fPoint.y) ;
// Now caclulate last control point
// and end anchor point.
PVector [] cPoints2 = calcPetalPointPair(axisAngle+ halfPetalAngle,- tiltAngle,petalLength) ;
startPt = cPoints2[0] ;
fPoint = cPoints2[1] ;
bezControlPoints[3] = new PVector(startPt.x * radius,startPt.y * radius) ;
bezControlPoints[2] = new PVector(startPt.x * radius + fPoint.x,startPt.y * radius + fPoint.y) ;
return bezControlPoints ;
}
The function calcPetalPointPair is very much based around our tiltTangent example. The three parameters which describe the angle of one of the petal’s anchor point, the petal’s tilt and length are used to calculate the actual anchor and control points.
// Function calculates an array of 2 PVectors
// one half of a cubic Bézier
// @array[0] is the anchor point
// @array[1] is the control point
// The points are based around the origin (0,0)
// on a circle with unit radius.
PVector[] calcPetalPointPair(float anchorAngle,float tiltAngle,float petalLength)
{
// 1st work out start point based on the angle of
// anchor point we are calculating.
// This will be a unit length from the origin (0,0)
PVector startPt = new PVector(sin(degreesToRadians(anchorAngle)),cos(degreesToRadians(anchorAngle)));
PVector vert = new PVector(0,0,1.0f) ; // Unit Vector Perp to XY Plane
PVector nCPc = startPt.cross(vert) ; // Calculate Tangent to Circle at startPt.x,startPt.y
PMatrix2D iMat = new PMatrix2D() ;
// generate a rotation matrix based on the tilt of our petal
iMat.rotate(degreesToRadians(tiltAngle < 0 ? 180+tiltAngle : tiltAngle)) ;
// generate an othoganal matrix based on our anchor angle
PMatrix2D cMat = new PMatrix2D(nCPc.x,startPt.x,0.0f,nCPc.y,startPt.y,0.0f) ;
// Othoganal Matrix
// | nCPc.x startPt.x |
// | nCPc.y startPt.y |
// Apply tilt rotation to our othoganal matrix
// to produce a composite matrix which
// we can use to calulate control points
cMat.preApply(iMat) ;
// cMat is now our composite transformation Matrix.
PVector fPoint = new PVector() ;
PVector sPoint = new PVector(petalLength,0) ;
cMat.mult(sPoint,fPoint) ; // Apply Full transformation to petal Vector
PVector [] points = new PVector[2] ;
points[0] = startPt ;
points[1] = fPoint ;
// we now have our PVector anchor point based on the anchorAngle
// positioned on a circle with unit radius
// and our PVector control point based on the petalLength and tilt.
return points;
}
The drawPetals function is used to draw the petals around the torus. The number of petals drawn is dependent on the parameters numPetals and petalStepSize. The anchor and control points of each petal is calculated and pass back is an array of PVectors by the function calcControlPoints. This array, cps, is used in
The anchor and control points are calculated
void drawPetals(float torusRadius,float petalStepSize,float numPetals,
float petalSpreadAngle,float tiltAngle,
float petalLength,color col,float phase)
{
// Draw all petals on our flower
for (float petalAxisAngle = 0.0f ; petalAxisAngle < petalStepSize*numPetals ; petalAxisAngle+=petalStepSize)
{
PVector [] cps =
calcControlPoints(petalAxisAngle+phase,petalSpreadAngle,torusRadius,tiltAngle,petalLength) ;
drawPetal(cps,col) ;
// uncomment code below to see control points
//drawControlPoints(cps) ;
}
}
The centre portion of our flower, known as the torus is drawn using the function drawTorus. This is simply a circle with a colour and radius given as parameters.
void drawTorus(color torusColour,float torusRadius)
{
fill(torusColour) ;
ellipseMode(RADIUS) ;
stroke(0,0,0.40) ;
ellipse(0, 0, torusRadius,torusRadius) ;
}
The function drawFlower is the main function called from our draw method. It draws the three parts of our flower. The torus, the petals and the sepals. Note the sepals use the same drawing function but with dependent parameters.
void drawFlower(color torusColour,float torusRadius,
float petalStepSize,float numPetals,
float petalSpreadAngle,float tiltAngle,
float petalLength,
float phase)
{
/// draw Torus
drawTorus(torusColour,torusRadius) ;
// Draw Petals
drawPetals(torusRadius, petalStepSize, numPetals,
petalSpreadAngle, tiltAngle,
petalLength, petalCol,phase) ;
// Draw Sepals
drawPetals(torusRadius, petalStepSize*2, numPetals/2,
petalSpreadAngle,tiltAngle/2,
petalLength/2, sepalCol,phase+petalStepSize/2) ;
}
drawPath and drawFlightPath are support functions for drawing the flight path. You should notice a commented call to drawFlightPath in the drawBees function. Uncommenting this line will allow you to study the bees flight path.
// Draw Support functions
void drawPath(PVector [] cps)
{
noFill() ;
stroke(0,0,0) ;
Bézier (cps [0].x,cps[0].y,cps[1].x,cps[1].y,cps[2].x,cps[2].y,cps[3].x,cps[3].y) ;
}
void drawFlightPath()
{
for(int i = 0 ; i < flightPath.length ; i++)
{
drawPath(flightPath[i]) ;
}
}
drawEllipse and drawCircle are another set of support functions for drawing our bees.
void drawEllipse(float x,float y,float a,float b,color col)
{
fill(col) ;
ellipseMode(RADIUS) ;
stroke(0,0,0.40) ;
ellipse(x, y, a,b) ;
}
void drawCircle(float x,float y,float rad,color col)
{
drawEllipse(x,y,rad,rad,col) ;
}
The drawBee function uses very rudimentary drawing functions ellipses, circles and a triangle to draw a cute looking bee. We have added some animation by moving the bees wings using the random function. This function draws a bee around the fixed axis (0,0) - so if we are going to use it to draw more than one bee we need to proceed a call to drawBee
with a call to the translate function.
// Draw one Bee around current Origin.
void drawBee()
{
// Save current Draw Matrix
pushMatrix() ;
float wobbleX = 2 ;
float wobbleY = 1 ;
float rx = wobbleX-random(wobbleX*2) ;
float ry = wobbleY-random(wobbleY*2) ;
translate(rx,ry) ;
float rnd = random(4) ;
float wRnd = 4-random(8) ;
// variables used to wobble Bee's wings
float wLen = 20- wRnd;
float adj = -(28-wLen) ;
// 1st draw Bee's backside
drawCircle(0,0,30,black) ;
// Draw left wing
pushMatrix() ;
rotate(-10);
translate(0,55) ;
drawEllipse(8+rnd,-6+adj,10,wLen,pWhite) ;
popMatrix() ;
// Draw right wing.
pushMatrix() ;
rotate(10);
translate(0,55) ;
drawEllipse(-8+rnd,-6+adj,10,wLen,pWhite) ;
popMatrix() ;
// Draw stinger
fill(0,0,0);
float x = 2 ;
float w = 4 ;
float y = -30 ;
triangle(-x, y, -x+w, y, -x+w/2, y-10);
// Draw Bee's body
drawCircle(0,0,25,yellow) ;
drawCircle(0,0,20,black) ;
drawCircle(0,0,15,yellow) ;
drawCircle(0,0,10,black) ;
// draw left eye
drawEllipse(-8,-6,8,10,white) ;
drawEllipse(-8,-2,2,3,black) ;
// draw right eye
drawEllipse(8,-6,8,10,white) ;
drawEllipse(8,-2,2,3,black) ;
// Restore Draw Matrix
popMatrix() ;
}
drawBeeAtTime is used to first calculate what Bézier path a bee is on, given its normalised time. Normalised time is used to represent some time value as a fraction of some known total time. A normalised variable has some decimal value between 0 and 1. For example, let’s say that the bee completes his total flight path in 16 seconds. A normalised time of 0.5 means that the bee is 8 seconds into her flight. A normalised time of 0 means it is in 0 seconds of their flight. And finally, a normalised time of 1 means that they are in 16 seconds (the total time) into their flight. The bees flight path consists of 4 Bézier curves, so we first need to calculate which one of these four curves it is on.
The line
int section = Math.min(flightPath.length - 1,(int)(nTime * flightPath.length)) ;
calculates the particular Bézier curve need to use. Since, in our example we have defined our flight path from four curves, the section value should be a integer between 0 to 3. Remember offset 0 is used in Java Arrays. After we have worked out the paricular section the bee is on, we need to then work out the normalised time for that particular curve. We need to use the normalised time for the BézierPoint function. We are working on the assumption that our bee takes equal amounts of time on each section to make our calculation as easier as
nTime = flightPath.length * (nTime - (section*1.0f/flightPath.length)) ;
// Calculate which Bézier Path our bee is on according to their
// normalised time over the complete number of paths.
// Then normalised the time again for that particular path section.
// Use BézierPoint to work out their position on that Bézier curve
// and draw.
void drawBeeAtTime(float nTime)
{
PVector []path ;
// decide which section the Bee is on
//
int section = Math.min(flightPath.length - 1,(int)(nTime * flightPath.length)) ;
// Calculate new Normalised Time
nTime = flightPath.length * (nTime - (section*1.0f/flightPath.length)) ;
// set path to current Bézier Curve our bee is on.
path = flightPath[section] ;
// Calculate Bézier X,Y position from new Normalised Time
// and Path defined by Bézier points in array 'path'
float xPos = BézierPoint(path[0].x,path[1].x,path[2].x,path[3].x,nTime) ;
float yPos = BézierPoint(path[0].y,path[1].y,path[2].y,path[3].y,nTime) ;
// Draw a Simple Bee
pushMatrix() ;
// set our drawing origin
translate(xPos,yPos) ;
// and draw
drawBee() ;
popMatrix() ;
}
For the final part of drawing bees - we have the function which draws all of our bees. The function drawBees has one parameter dt (delta-time). This is the time that has passed from the last time it was called. The for loop goes through calculating the normalised time for each bee. To add some variance to the bees movement, we use different speed constants (rspeed[]) which is multiplied by dt parameter and added to a record of the total flight time for the particular bee we are going to draw. It is this normalised time that is used in the function drawBeeAtTime.
void drawBees(float dt)
{
pushMatrix() ;
translate(CX,CY) ;
// Uncomment code below to view flight path
//drawFlightPath() ;
for(int i = 0 ; i < NUMBEROFBEES ; i++)
{
time[i] = time[i] + dt*rspeed[i] ;
// calculate position for our bee - based
// on their time and speed
float ltime = time[i] + roffset[i] ;
while (ltime > flightDuration)
{
ltime -= flightDuration ;
}
float nTime = ltime/flightDuration ;
drawBeeAtTime(((i & 1) == 1) ? nTime : 1.0f - nTime);
}
popMatrix() ;
}
That’s it! You should be able to make very simple changes to the above application by changing the constants and removing comment code.
Finally, I would like to suggest further improvements you could make to the Bees and Flower application.
Make further use of the sinFunction() to change other Flower variables like the petalStep,petalAngle and petalLength. Make sure that, if you change petalStep, you will also have to calculate a new value for the variable numpetals. This is simply done by use of the line.
numpetals = 360.0f / petalStep ;
If you uncomment the drawFlightPath() function call found in drawBees() and execute the application, you should notice the slight ‘kinks’ in the joined Bézier curves. As shown in our first example program, this kink is down to the fact that the shared anchor point and its adjacent control points are not in a straight line. To remove the kink, we can modify either, the last control point of the proceeding flight path, the shared anchor point or the first control point of the next flight path. Using the equation of the straight line (see chapter x), we can develop a function, which, given the 2 control points and common anchor point, calculate a modified control point (or anchor point) which will leave our path smooth.
The following function smoothPath() will modify the a shared anchor point (the 1st input parameter), given the last and first control points from the two paths.
void smoothPath(PVector sharedAnchorPoint, PVector ingres, PVector outgres)
{
float newX, newY ;
foat xDiff = ingres.x - outgres.x ;
float yDiff = ingres.y - outgres.y ;
// Calculate Gradient and Intercept
float m = yDiff / xDiff ;
float c = ingres.y - m * ingres.x ;
// now use these values to calculate a new y position for
// the shared anchor point
sharedAnchorPoint.y = m*sharedAnchorPoint.x + c ;
}
I will leave it to the reader to modify the application to remove these kinks. All play and no work makes Jack and Jill, dull programmers, as the saying goes! Have fun!