ORIGINALLY POSTED NOVEMBER 16, 2011
This week: Bezier curves, how to draw them in Python and particularly, how to decide where to put the control points.
A Bezier curve, is a special type of parametric curve used frequently in computer graphics. You may have seen them in Powerpoint, or using the pen tool in illustrator, but for my purpose, they are a lovely way to visualise flow paths on a map, like this:
This particular visualisation explores the London public transport network and was made by my rather awesome colleague Dr Ed Manley. It shows London's most popular destinations by origin, derived from a large dataset of morning peak Oyster Card trips.
The curves themselves are anchored at both ends (origin P0, destination P3) and get their shape from two control points (blue P1 and red P2), as in the following picture:
The curve itself is created from the fairly simple expression (everything in bold is a vector):
The parameterisation t starts from zero, so that the curve begins at P0, and as it increases bends the line towards the two control points, first P1 and then P2. Finally, when t is 1, the line finishes at P3.
It’s all very clever stuff, and there is lots and lots of literature available on the web about them. But – as far as I can see – no one ever tells you how to decide where to put your control points, and certainly doesn’t offer an algorithm to automatically do so.
But fear not people! I have written one. And ugly and fudgy as it is, it does the job.
The code below takes 6 inputs:
-a length for the blue line,
-a length for the red line (both as a fraction of the length of the green line)
-the angle between the blue and green lines
-the angle between the red and green lines (both in radians. I am a mathematician after all).
And it will spit out a totally delicious plot of your curves.
It’s set up to bend the curves clockwise (as in the image at the top of the page), which generally looks nicer when plotting more than one bezier curve.
Have a play with the last four inputs: the longer the length, the more the curve will deviate from the green line. The higher the angle, the steeper the curve will roll in to it’s destination. Indeed, choosing the angles to be on the other side of the green line will result in an s-shaped curve. Anyway – the idea is for you to hack out the bits of the code that are useful.
import scipy as sp import pylab as plt #### Inputs #A list of P0's and P3's. Must be the same length origins = [[1,0],[-0.5,sp.sqrt(3)/2], [-0.5,-sp.sqrt(3)/2]] destinations = [[0,0],[0,0],[0,0]] #The angle the control point will make with the green line blue_angle = sp.pi/6 red_angle = sp.pi/4 #And the lengths of the lines (as a fraction of the length of the green one) blue_len = 1./5 red_len = 1./3 ### Workings #Generate the figure fig = plt.figure() ax = fig.add_subplot(111) ax.hold(True) #Setup the parameterisation t = sp.linspace(0,1,100) for i in xrange(len(origins)): #Read in the origin & destination points POx,POy = origins[i], origins[i] P3x,P3y = destinations[i], destinations[i] #Add those to the axes ax.plot(POx,POy, 'ob') ax.plot(P3x,P3y, 'or') ax.plot((POx,P3x),(POy,P3y), 'g') #Work out r and theta (as if based at P3) r = ((POx-P3x)**2 + (POy-P3y)**2)**0.5 theta = sp.arctan2((POy-P3y),(POx-P3x)) #Find the relevant angles for the control points aO =theta + blue_angle+ sp.pi aD = theta - red_angle #Work out the control points P1x, P1y = POx+ blue_len*r*sp.cos(aO), POy + blue_len*r*sp.sin(aO) P2x, P2y = P3x+ red_len*r*sp.cos(aD), P3y + red_len*r*sp.sin(aD) #Plot the control points and their vectors ax.plot((P3x,P2x),(P3y,P2y), 'r') ax.plot((POx,P1x),(POy,P1y), 'b') ax.plot(P1x, P1y, 'ob') ax.plot(P2x, P2y, 'or') #Use the Bezier formula Bx = (1-t)**3*POx + 3*(1-t)**2*t*P1x + 3*(1-t)*t**2*P2x + t**3*P3x By = (1-t)**3*POy + 3*(1-t)**2*t*P1y + 3*(1-t)*t**2*P2y + t**3*P3y #Plot the Bezier curve ax.plot(Bx, By, 'k') #Save it plt.savefig('totally-awesome-bezier.png') plt.show() #Bosch.