We're back after a server migration that caused effbot.org to fall over a bit harder than expected. Expect some glitches.

Tkinter Tricks: Using Complex Numbers to Rotate Canvas Items

Python’s complex number type can be used to represent coordinates in a 2-dimensional graphics system. Simply store the X coordinate as the real part, and the Y coordinate as the imaginary part.

Complex numbers provide a compact way of storing 2D coordinates, but they also make it really easy to manipulate coordinate values. For example, to translate a coordinate, just add the offset (represented as complex number). And to rotate a coordinate, all you have to do is to multiply the coordinate with a complex number representing the angle.

First, you need a complex number that represents the angle. If you have the angle, you can use the cmath.exp function:

import cmath, math
cangle = cmath.exp(angle*1j) # angle in radians

Alternatively, if you have two deltas (dx, dy), you can calculate the complex angle directly from the deltas:

cangle = complex(dx, dy)
cangle = cangle / abs(cangle)

(the cangle complex value can be seen as a unit vector, with one end at (0, 0)).

To rotate a coordinate around (0, 0), convert the coordinate to a complex number, and multiply it with the complex angle:

for x, y in coordinates:
    v = cangle * complex(x, y)
    print v.real, v.imag

To rotate around an arbitrary point, create a complex number corresponding to that point, subtract that number before you multiply with the angle, and add it back afterwards:

center = complex(xoffset, yoffset)
for x, y in coordinates:
    v = cangle * (complex(x, y) - center) + center
    print v.real, v.imag

The following Tkinter example allows the user to use the mouse to rotate an item around a given center point.

from Tkinter import *

import math

c = Canvas(width=200, height=200)
c.pack()

# a square
xy = [(50, 50), (150, 50), (150, 150), (50, 150)]

polygon_item = c.create_polygon(xy)

center = 100, 100

def getangle(event):
    dx = c.canvasx(event.x) - center[0]
    dy = c.canvasy(event.y) - center[1]
    try:
        return complex(dx, dy) / abs(complex(dx, dy))
    except ZeroDivisionError:
        return 0.0 # cannot determine angle

def press(event):
    # calculate angle at start point
    global start
    start = getangle(event)

def motion(event):
    # calculate current angle relative to initial angle
    global start
    angle = getangle(event) / start
    offset = complex(center[0], center[1])
    newxy = []
    for x, y in xy:
        v = angle * (complex(x, y) - offset) + offset
        newxy.append(v.real)
        newxy.append(v.imag)
    c.coords(polygon_item, *newxy)

c.bind("<Button-1>", press)
c.bind("<B1-Motion>", motion)

mainloop()

Note that for the Tkinter canvas, this only works properly for lines and polygons (all other items are always aligned with the canvas coordinate system). To rotate a rectangle, for example, you must first convert it to a polygon.