Quadratic Bezier Curves
September 19, 2012 6 Comments
Or, in other words, smooth lines. This is not the tutorial I had intended to write, but the tutorial I had intended to write is too long in coming. I have been working on a stick figure program in Python and need to prototype drawing curved lines using Tkinter. I figured I may as well incorporate it into a tutorial.
A Loose End
At the end of the Being Animated tutorial I set some exercises – I hope you’ve done them by now. One of the questions I asked was why I chose a width of 21 pixels and not 20? There is no magic in the number 21 per se. Rather, the issue is whether or not the number is an even or an odd number. Harking back to our discussion about pixels you should realise [sic] that pixels on the screen are not like marks on a ruler. Marks on a ruler take up no space pixels, on the other hand, do. If you chose an even number of pixels for something you want to centre [sic] somewhere the centre pixel will be at the centre. Then you will have a different number of pixels on either side. To take an extreme example, if it was two pixels wide, then one pixel would be at the mouse’s location, and one would be left hanging. There would not be a way to balance it. However, if it was three pixels wide, one could be at the mouse position, and one could be on either side.
Introduction
In the Being Animated tutorial we also saw how to draw a line on a Tkinter Canvas Widget. In this tutorial we’re going to look at how to draw a curved line. I have used the code from canvasLine2B.py in that tutorial as a starting point for the code below – save this to canvasCurve1.py:
# -*- coding: utf-8 -*- from Tkinter import * TITLE = "Drawing a Curve" WIDTH = 200 HEIGHT = 200 CENTREX = WIDTH/2 CENTREY = HEIGHT/2 formatString = "x: %03d, y: %03d" class Canvassing(): def __init__(self, parent = None): self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue") self.canvas.pack() self.readout = Label(text="This is a label") self.readout.pack() self.canvas.bind("<Motion>",self.onMouseMotion) self.line = None self.canvas.master.wm_title(string = TITLE) self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4), (CENTREX, CENTREY) ] def onMouseMotion(self,event): # really should rename this as we're doing something different now self.readout.config(text = formatString%(event.x, event.y)) self.canvas.delete(self.line) # first time through this will be None # but Tkinter is ok with that self.line = self.canvas.create_line(self.points, (event.x,event.y), width=2, fill = "yellow") Canvassing() mainloop()
You can see that I’ve:
- removed the magic number 100 from the code, and replaced it by WIDTH and HEIGHT – capitalised variable names to indicate that they are (or should be) global constants;
- added a list called self.points which, at the moment, contains two points;
- change the name of the second method to onMouseMotion (because it’s what is done when there’s a movement of the mouse);
- increased the width of the line so it is a little easier to see;
- changed the drawing code. Now the two points above and the current position of the mouse (event.x, event.y) are fed into create_line;
and - added a title to the window.
This gives us something that looks like this (except that the second line segment follows the mouse, something this still image doesn’t do justice to):
Exercise: Try some other values of WIDTH and HEIGHT to verify that the program still works.
Exercise: Try adding more entries to self.points (in the form (x,y) with commas between them all, but none after the last one)
Drawing nodes
To demonstrate that there are three points involved I am going to add some code to draw a small circle around each point, and moving the one under the mouse pointer with the mouse:
# -*- coding: utf-8 -*- from Tkinter import * TITLE = "Drawing a Curve" WIDTH = 200 HEIGHT = 200 CENTREX = WIDTH/2 CENTREY = HEIGHT/2 NODE_RADIUS = 3 NODE_COLOUR = "red" LINE_COLOUR= "yellow" formatString = "x: %03d, y: %03d" class Canvassing(): def __init__(self, parent = None): self.canvas = Canvas(width=WIDTH,height=HEIGHT, bg = "blue") self.canvas.pack() self.readout = Label(text="This is a label") self.readout.pack() self.canvas.bind("<Motion>",self.onMouseMotion) self.line = None self.canvas.master.wm_title(string = TITLE) self.points = [(CENTREX-WIDTH/4, CENTREY-HEIGHT/4), (CENTREX, CENTREY) ] def onMouseMotion(self,event): # really should rename this as we're doing something different now self.readout.config(text = formatString%(event.x, event.y)) allItems = self.canvas.find_all() for i in allItems: # delete all the items on the canvas self.canvas.delete(i) # deleting everything every time is inefficient, but it doesn't matter for our purposes. for p in self.points: self.drawNode(p) p = (event.x, event.y) # now repurpose p to be the point under the mouse self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR) self.drawNode(p) def drawNode(self, p): boundingBox = (p[0]-NODE_RADIUS, p[1]+NODE_RADIUS, p[0]+NODE_RADIUS,p[1]-NODE_RADIUS) # mixed + and - because y runs from top to bottom not bottom to top self.canvas.create_oval(boundingBox, fill=NODE_COLOUR) Canvassing() mainloop()
This gives something similar to the picture above, but with the nodes clearly shown:
Magic Pixie Dust
Once we’ve done this, drawing a curved line is a snap. We simply add a new parameter, smooth, to the create_line method call:
self.line = self.canvas.create_line(self.points, p, width=2, fill = LINE_COLOUR, smooth = True)
Now you can see the curve clearly shown as well as where the points are:
Same problem with the tag being eaten by the website.
argh! Thanks, corrected
Pingback: Python4Kids New Tutorial: Quadratic Bezier Curves | Tutorial WPAP
La mayoría viene de todo, usted será capaz de construir nuevos objetos y tesoros infinitos en el deporte.
I loved the article! I looked for this in several places and only found it here. Congratulations on quality content!
That was really what I was looking for! Many thanks and congratulations for the blog!