output

import numpy as np
%matplotlib notebook
import matplotlib.pyplot as plt
import traceback

def circle(x, y, r, rot, steps=100):
    a = rot + np.linspace(0., 2. * np.pi, steps)
    return np.append((np.cos(a) * r) + x, x), np.append((np.sin(a) * r) + y, y)

class Element:
    
    def __init__(self, rate, edge_radius, path_radius, path_offset, tracers):
        self.centre_x     = 0
        self.centre_y     = 0
        self.path_x       = 0
        self.path_y       = 0
        self.edge_radius  = edge_radius
        self.edge_handle, = plt.plot([],[], '-')
            
        self.path_radius  = path_radius
        self.path_offset  = path_offset
        self.path_handle, = plt.plot([],[], ':')
        
        self.tracers = tracers
        self.tracer_handles = []
        self.tracer_paths   = []
        for colour, fmt, _ in self.tracers:
            hnd, = plt.plot([],[], fmt, color=colour)
            self.tracer_handles += [ hnd ]
            self.tracer_paths   += [ [[], []] ]
        
        self.parent    = None
        self.children  = []
        self.rate      = rate
        self.position  = 0
        self.rotation  = 0
        self.acc_rotation = 0
    
    def step(self, show_elements):
        
        self.rotation += self.rate
        
        if (self.parent is not None): 
            self.acc_rotation = self.rotation + self.parent.acc_rotation
            
            if (self.parent.path_radius <= self.parent.edge_radius):
                offset = self.parent.path_radius - self.edge_radius
                delta  = (self.rate * self.edge_radius) / self.parent.path_radius
            else:
                offset = self.parent.path_radius + self.edge_radius
                delta  = -((self.rate * self.edge_radius) / self.parent.path_radius)
                
            self.position -= delta*2
        
            self.centre_x = self.parent.path_x + np.cos(self.position) * offset
            self.centre_y = self.parent.path_y + np.sin(self.position) * offset
        
        if show_elements:
            self.edge_handle.set_data(*circle(self.centre_x, 
                                              self.centre_y, 
                                              self.edge_radius, 
                                              self.acc_rotation))
        else:
            self.edge_handle.set_data([],[])
        
        self.path_x = self.centre_x + np.cos(self.acc_rotation) * self.path_offset
        self.path_y = self.centre_y + np.sin(self.acc_rotation) * self.path_offset
        
        if show_elements:
            self.path_handle.set_data(*circle(self.path_x, 
                                              self.path_y, 
                                              self.path_radius, 
                                              self.acc_rotation))
        else:
            self.path_handle.set_data([],[])
        
        for hnd, path, (_, _, offset) in zip(self.tracer_handles, 
                                                    self.tracer_paths, 
                                                    self.tracers):
            
            path[0] += [ self.path_x + np.cos(self.acc_rotation) * offset ]
            path[1] += [ self.path_y + np.sin(self.acc_rotation) * offset ]
            hnd.set_data(*path)
            
        for child in self.children:
            child.step(show_elements)
            
    def add(self, element):
        self.children += [ element ]
        self.children[-1].parent = self
    
class UI:
    def __init__(self):
        
        self.show_elements = True
        figw, figh, figdpi = 950, 950, 50
        self.figure = plt.figure(facecolor='w', figsize=(figw/figdpi, figh/figdpi), dpi=figdpi)
        self.axis   = plt.gca()
        plt.axis('off')
        plt.xlim([-1,1])
        plt.ylim([-1,1])
        
        radius = 0.45
        num_pens = 10
        cmap = [plt.get_cmap('RdBu')(int(idx)) 
                for idx in np.linspace(64, 256-64, num_pens)]
        
        pens = [(colour, '-', radius-delta) 
                for colour, delta in zip(cmap, np.linspace(0.1,0.3,num_pens))]
                
        elem_0            = Element(0.05, radius, radius, 0.0, pens)
        self.root_element = Element(0.0, 1.0, 1.0, 0.0, [])
        self.root_element.add(elem_0)
        
        plt.show()
    
    def on_click(self, event):
        if not (event.inaxes == self.axis): return
        plt.sca(self.axis)
        
        try:
            self.show_elements = not self.show_elements
            self.root_element.step(self.show_elements)      
        except Exception:
            plt.title(traceback.format_exc())
        
    def on_move(self, event):
        if not (event.inaxes == self.axis): return
        plt.sca(self.axis)
        
        try:
            self.root_element.step(self.show_elements)      
        except Exception:
            plt.title(traceback.format_exc())
        
    def attach(self, key, func):
        self.figure.canvas.mpl_connect(key, func)

ui = UI()

def on_click(event):
    global ui
    ui.on_click(event)
    
def on_move(event):
    global ui
    ui.on_move(event)
        
ui.attach('button_press_event',  on_click)
ui.attach('motion_notify_event', on_move)