Simulation of a rotating cube using python + GTk.
Moving the mouse while the button1 is pressed will produce a rotation of the cube along XZ, and YX axis. Otherwise you will shift the point of view on the XY plane. Still looking for a more efficient way of drawing on the screen.
#!/usr/bin/env python
import gtk
import gobject
from math import pi, cos, sin
# perspective
ZOOM = 100.0
FOCUS = 100.0
# misc
FPS=60
DEPTH = 16
PALETTE = {
'black': (0, 0, 0),
'blue': (0, 0, 65535),
'green': (0, 65535, 0),
'cyan': (0, 65535, 65535),
'red': (65335, 0, 0),
'magenta': (65535, 0, 65535),
'yellow': (65535, 65535, 0),
'white': (65535, 65535, 65535),
}
ZSHIFT = 3
ZMOD = 200/DEPTH
class Scene():
def __init__(self):
window = gtk.Window()
window.set_size_request(640, 480)
window.add_events(gtk.gdk.BUTTON_PRESS_MASK
| gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.POINTER_MOTION_HINT_MASK)
window.connect('delete_event', self.delete_event_cb)
window.connect('button_press_event', self.button_press_event)
window.connect('motion_notify_event', self.motion_notify_event)
self.darea = gtk.DrawingArea()
self.darea.connect('configure_event', self.configure_event)
self.darea.connect('expose_event', self.expose_event)
window.add(self.darea)
self.darea.show()
window.show()
self.refresh = 0
self.refresh = gobject.timeout_add(1000/FPS, self.draw_scene)
def delete_event_cb(self, widget, event):
if self.refresh != 0:
gobject.source_remove(self.refresh)
self.refresh = 0
gtk.main_quit()
def button_press_event(self, widget, event):
if event.button == 1:
x, y, state = event.window.get_pointer()
self.oldx, self.oldy = self.convert_dev_to_user(x, y)
return True
def motion_notify_event(self, widget, event):
if event.is_hint:
x, y, state = event.window.get_pointer()
else:
x = event.x
y = event.y
state = event.state
# the origin is in the middle of the windonw
x, y = self.convert_dev_to_user(x, y)
if state & gtk.gdk.BUTTON1_MASK:
angle_xz = angle_yz = 0
if x > self.oldx:
angle_xz = .05
elif x < self.oldx:
angle_xz = -.05
if y > self.oldy:
angle_yz = .05
elif y < self.oldy:
angle_yz = -.05
self.cube.rotate(angle_xz, angle_yz)
self.oldx = x
self.oldy = y
else:
self.vx = x
self.vy = y
return True
def configure_event(self, widget, event):
x, y, self.width, self.height = widget.get_allocation()
self.cx = self.width/2
self.cy = self.height/2
self.unit = min(self.cx, self.cy)/2
self.vx = self.vy = 0
self.oldx = self.oldy = 0
self.pixmap = gtk.gdk.Pixmap(widget.window, self.width,
self.height)
self.cube = Cube(self)
self.draw_scene()
return True
def expose_event(self, widget, event):
x , y, width, height = event.area
self.darea_realize(x, y, width, height)
return False
def darea_realize(self, x, y, width, height):
self.darea.window.draw_drawable(
self.darea.get_style().fg_gc[gtk.STATE_NORMAL],
self.pixmap, x, y, x, y, width, height)
def convert_dev_to_user(self, x, y):
return x - self.cx, (self.height - 1 - y) - self.cy
def z_scale(self, z):
return ZOOM/(FOCUS + z)
def convert_3d_to_2d(self, point):
px, py, pz = point
px *= self.unit
py *= self.unit
z = (pz + ZSHIFT)*ZMOD
x = self.cx + self.vx + (px - self.vx)*self.z_scale(z)
y = self.cy + self.vy + (py - self.vy)*self.z_scale(z)
return int(x), int(y)
def color_shading(self, color, z):
z += ZSHIFT
r, g, b = PALETTE[color]
# quadratic shading
ratio = (float(DEPTH) - z)**2/(DEPTH**2)
r = int(r*ratio)
g = int(g*ratio)
b = int(b*ratio)
return r, g, b
def draw_line(self, src, dst, color='white', dashed=False):
gc = self.darea.window.new_gc()
r, g, b = self.color_shading(color, src[2])
gc.foreground = self.darea.get_colormap().alloc_color(r, g, b)
if dashed:
gc.set_line_attributes(1, gtk.gdk.LINE_DOUBLE_DASH,
gtk.gdk.CAP_BUTT, gtk.gdk.JOIN_BEVEL)
x0, y0 = self.convert_3d_to_2d(src)
x1, y1 = self.convert_3d_to_2d(dst)
self.pixmap.draw_line(gc,
x0, self.height - 1 - (y0),
x1, self.height - 1 - (y1))
def draw_scene(self):
self.pixmap.draw_rectangle(self.darea.get_style().black_gc,
True, 0, 0, self.width, self.height)
self.cube.draw()
self.darea_realize(0, 0, -1, -1)
return True
class Cube(Scene):
def __init__(self, scene):
self.scene = scene
self.front = [(-1, 1, -1), (1, 1, -1), (1, -1, -1), (-1, -1, -1)]
self.rear = [(-1, 1, 1), (1, 1, 1), (1, -1, 1), (-1, -1, 1)]
def rotate(self, angle_XZ, angle_YZ):
scene = self.scene
for i in range(4):
# Front face
x, y, z = self.front[i]
theta = angle_XZ
x, y, z = (x*cos(theta) - z*sin(theta), y, x*sin(theta) + z*cos(theta))
theta = angle_YZ
x, y, z = (x, y*cos(theta) - z*sin(theta), y*sin(theta) + z*cos(theta))
self.front[i] = (x, y, z)
# Rear face
x, y, z = self.rear[i]
theta = angle_XZ
x, y, z = (x*cos(theta) - z*sin(theta), y, x*sin(theta) + z*cos(theta))
theta = angle_YZ
x, y, z = (x, y*cos(theta) - z*sin(theta), y*sin(theta) + z*cos(theta))
self.rear[i] = (x, y, z)
def draw(self):
scene = self.scene
for i in range(4):
scene.draw_line(self.front[i], self.front[(i + 1)%4])
scene.draw_line(self.rear[i], self.rear[(i + 1)%4])
scene.draw_line(self.front[i], self.rear[i])
if __name__ == '__main__':
Scene()
gtk.main()