#!/usr/bin/env python import gtk, cairo import math class Values(object): pass def key_press_event (win, event, val): if event.string in ['q', 'Q']: gtk.main_quit () elif event.string in ['f', 'F']: val.full = not val.full if val.full: win.fullscreen () else: win.unfullscreen () elif event.string in ['z', 'Z']: val.zoom = (val.zoom + 1) % 3 win.queue_draw () elif event.string in ['+']: val.steps = min (val.steps * 2, 4096) win.queue_draw () elif event.string in ['-']: val.steps = max (val.steps / 2, 2) win.queue_draw () else: return False return True def expose_event (win, event, val): w, h = win.get_size () bez = [-0.8, 0.0, 0.6, 0.0, 0.8, 0.03, 0.8, 0.0] bez = [-0.8, -0.8, -0.7, -0.8, 0.7, 1.05, 0.7, 0.7] stroke_radius = 0.2 cr = gtk.gdk.Drawable.cairo_create (win.window) cr.rectangle (*event.area) cr.clip () cr.translate (w/2.0, h/2.0) cr.scale (min (w/2., h/2.), -min (w/2., h/2.)) if val.zoom != 0: cr.scale (4, 4) if val.zoom == 1: cr.translate (-bez[0], -bez[1]) else: cr.translate (-bez[6], -bez[7]) pxl = cr.device_to_user_distance (1, 0)[0] # draw the black thick stroke cr.set_line_width (stroke_radius * 2) cr.move_to (*bez[0:2]) cr.curve_to (*bez[2:8]) cr.stroke () cr.set_line_width (1 * pxl) for draw in [0, 1]: for t in xrange (val.steps + 1): dt = float (t) / val.steps # bezier curve x = bez[0] + 3*dt*(bez[2]-bez[0]) + 3*dt*dt*(bez[4]-2*bez[2]+bez[0]) + dt*dt*dt*(bez[6]-3*bez[4]+3*bez[2]-bez[0]) y = bez[1] + 3*dt*(bez[3]-bez[1]) + 3*dt*dt*(bez[5]-2*bez[3]+bez[1]) + dt*dt*dt*(bez[7]-3*bez[5]+3*bez[3]-bez[1]) # first derivative of bezier curve dx = 3*(bez[2]-bez[0]) + 6*dt*(bez[4]-2*bez[2]+bez[0]) + 3*dt*dt*(bez[6]-3*bez[4]+3*bez[2]-bez[0]) dy = 3*(bez[3]-bez[1]) + 6*dt*(bez[5]-2*bez[3]+bez[1]) + 3*dt*dt*(bez[7]-3*bez[5]+3*bez[3]-bez[1]) # second derivative of bezier curve ddx = 6*(bez[4]-2*bez[2]+bez[0]) + 6*dt*(bez[6]-3*bez[4]+3*bez[2]-bez[0]) ddy = 6*(bez[5]-2*bez[3]+bez[1]) + 6*dt*(bez[7]-3*bez[5]+3*bez[3]-bez[1]) # curvature = (dx**2 + dy**2)**1.5 / (dx*ddy - ddx*dy) # not exactly the curvature, but the factor necessary to calculate the # midpoint of the osculating circle curvnorm = (dx**2 + dy**2) / (dx*ddy - ddx*dy) l = (dx**2 + dy**2)**0.5 # draw the blue dabs if draw == 0: cr.set_source_rgba (0.0, 0.0, 1.0, 1.0) cr.move_to (x - dy/l * stroke_radius, y + dx/l * stroke_radius) cr.line_to (x + dy/l * stroke_radius, y - dx/l * stroke_radius) cr.stroke () # draw red dots at the centers of the osculating circles. else: cr.set_source_rgba (1.0, 0.0, 0.0, 1.0) cr.arc (x - dy * curvnorm, y + dx * curvnorm, 1.5*pxl, 0, 2*math.pi) cr.fill () # draw green thin bezier curve plus its handles cr.set_line_width (1 * pxl) cr.set_source_rgba (0.0, 1.0, 0.0, 0.7) cr.move_to (*bez[0:2]) cr.curve_to (*bez[2:8]) cr.move_to (bez[0], bez[1]) cr.line_to (bez[2], bez[3]) cr.move_to (bez[4], bez[5]) cr.line_to (bez[6], bez[7]) cr.stroke () cr.set_source_rgba (1.0, 0.0, 0.0, 0.7) cr.arc (bez[0], bez[1], 3*pxl, 0, 2*math.pi) cr.new_sub_path () cr.arc (bez[2], bez[3], 3*pxl, 0, 2*math.pi) cr.new_sub_path () cr.arc (bez[4], bez[5], 3*pxl, 0, 2*math.pi) cr.new_sub_path () cr.arc (bez[6], bez[7], 3*pxl, 0, 2*math.pi) cr.fill () return True def setup_ui (val): win = gtk.Window () win.set_default_size (500, 500) win.set_events (gtk.gdk.EXPOSURE_MASK | gtk.gdk.KEY_PRESS_MASK) win.connect ("expose-event", expose_event, val) win.connect ("key-press-event", key_press_event, val) win.connect ("delete-event", gtk.main_quit) win.show_all () val.full = False val.zoom = 0 val.steps = 128 if __name__=='__main__': val = Values () setup_ui (val) gtk.main ()