Slider Spliner

Back to the photo sequence and music. Each photo is on the screen for only two seconds, and in between each there is a click as of a slide projector changing or even the sound of the shutter of a camera. The photos show in sequence: Anthony Barber, Katy Boyle, Edgar Allan Poe, a loony head and shoulders. He has ping-pong ball eyes, several teeth blocked out, a fright wig and his chest is bare but across it is written ‘A Loony’, Reginald Maudling, Tony Jacklin. A buzzer sounds.

In the last tutorial I used the term “quadradic Bezier curve” (which is a particular sort of way of drawing a curved line through three points).  A more generic name for this way of drawing curves is “spline”, named after a strip of flexible material used by draftsmen to draw curves, also now called flexicurves).   In this tutorial we’re going to learn about the Scale widget (but it looks like a slider) and use it to show how a computer draws those smooth curves.

Our starting point is the canvasCurve program from the previous tutorial, but we’ll jettison the mouse motion stuff, make it a bit larger and just draw a fixed, rather large, unhappy looking spline:

 

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
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.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12
    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  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()

To this we will add our Scale widget/slider bar.  The Scale widget can be configured in a variety of ways.  In this case we’ve passed it from_ (note the trailing underscore) and to parameters (self.slider = Scale(from_=1, to =20, orient = HORIZONTAL)).  These are the numbers that the widget will range from and to respectively.  If you grab and move the slider you will see its values change, but nothing happens to the curve:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
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.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  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()

Exercise:  slide the slider from left to right and confirm that it runs over all of the values from from_ to to.

Exercise: try different values for from and to.  Does the slider change its appearance?  If to is much greater than from_  does the slider miss some values in between when you slide it?

I could hook up a listener here to track whether the slider has changed, but I’m just going to add a button.  When you press the button it gets the slider’s value and updates the drawing based on that value:

# -*- coding: utf-8 -*-
from Tkinter import *

TITLE = "Drawing a Curve"
WIDTH = 400
HEIGHT = 400
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.slider = Scale(from_=1, to =20, orient = HORIZONTAL)
    self.slider.pack()
    self.updateButton = Button(text="Update!",command = self.update)
    self.updateButton.pack()
    self.readout = Label(text="This is a label")
    self.readout.pack()

    self.line = None
    self.canvas.master.wm_title(string = TITLE)
    self.points = [ (0,HEIGHT-10), (CENTREX,0),(WIDTH,HEIGHT-10) ]
    self.splinesteps = 12

    self.drawTheSpline()

  def drawTheSpline(self):
    allItems = self.canvas.find_all()
    for i in allItems:  # delete all the items on the canvas
      self.canvas.delete(i)

    self.line = self.canvas.create_line(self.points,  width=2, fill = LINE_COLOUR, smooth = True, splinesteps = self.splinesteps)
    for p in self.points:
      self.drawNode(p)
    self.readout.config(text="Steps: %s"%self.splinesteps)

  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)

  def update(self):
    # important bit here is to "get" the value in the slider
    self.splinesteps = self.slider.get()
    self.drawTheSpline()

Canvassing()
mainloop()

Exercise: Click the button for different values of the slider.

So, what’s happening here?  When Tkinter draws a smooth line (using create_line) it doesn’t actually draw a curved line.  Rather, it draws a number of much shorter straight lines.  You will notice that the create_line entry has a parameter called splinesteps.  This parameter tells Tkinter how many of these shorter straight lines should be drawn when approximating the curve.  If no number is passed to this parameter, it defaults to 12.  You will see that the spline becomes increasingly more refined as this number is increased.  You should also note that there is a great deal of improvement in the curve when these numbers are small, but much less improvement when the numbers are large (do the next exercise).

Exercise:  See that there is a big change in the curve when this number changes from 1 to 2, and 2 to 3, but very little improvement from 11 to 12 (or even from 12 to 20).

Finally, I want to point out that, since self.slider is a widget, it doesn’t have a value as such so an assignment such as someVariable = self.slider doesn’t make sense to Python, although it is perfectly logical for us. Rather, since self.slider is a Tkinter Scale Widget, it is really an instance of the Scale Class.  This means it has a lot of different attributes and methods (try print dir(self.slider)).  To get the value of the slider at any point, you need to use its get() method.  This is a common pattern for Tkinter widgets so you’ll need to get() used to it.

Quadratic Bezier Curves

An open field. A large hamper, with an attendant in a brown coat standing behind it. The attendant opens the hamper and three pigeon fanciers, (in very fast motion) leap out and run off across the field, wheeling in a curve as birds do.

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:

A Short Aside – Some Python Minecraft Stuff

The blog has been on holiday for many weeks now, following my own holiday. I am looking to get back into it over the next little while. In the meantime I have done some Google searches for Minecraft related programs written in Python that might be of interest/use.  I have not run any of them myself though, so reviews welcome.
Minecraft Screenshot Viewer in wxPython by Davy Mitchell. This viewer is written using wxPython, which is a Tkinter substitute.  You may need to install wxPython separately for it to work.  The code itself is here.

A variety of Minecraft scripts for editing save files. The site’s maintainer, Paul Spooner has asked for feedback on the scripts (see comments below) so get in contact if you have any.

MCSuperServer, which will automatically start your server for you when someone tries to connect.

Handle a front end for bukkit it allows you to easily automate mundane tasks associated with running a server.

Homework: Open at least one of these in a text editor/online and try to understand how the code works/does what it does.