SUSI Linux has an assistant user interface to answer your queries. You may ask queries and SUSI answers interactively using a host of skills which range from entertainment, knowledge, media to science, technology and sports. While SUSI is finding the answer to the query, it makes sense to add some animations depicting the process. The UI for SUSI Linux app is wholly made using GTK+ 3 using PyGObject (PyGTK). Thus, I needed to find a way create animations in GTK+.
Animations in most frameworks are generally created using repetitive drawing after an interval which leads to the effect of an object’s movement. Thus, basic need was to find a way to draw a custom object in GTK+. Reading the documentation of the GTK+, I realized that this could be done with a GTKDrawingArea.
GTKDrawingArea defines an area on which application developers can do the drawing on their own. GTK itself does not provide Canvas and related object to draw the shapes. For that, we need to use Cairo graphics library. Cairo is an open source graphics library with support for multiple window systems. It can run on a variety of backends, though here, we are not concerned about them.
Cairo can be accessed in Python using Pycairo. Pycairo is a set of Python 2 & 3 bindings for the cairo graphics library. Resources for usage of Pycairo for animations with GTK+ 3 are very less thus I will try to explain it in this blog in detail. We will start by creating an Animator class extending the GTK DrawingArea class.
class Animator(Gtk.DrawingArea): def __init__(self, **properties): super().__init__(**properties) self.set_size_request(200, 80) self.connect("draw", self.do_drawing) GLib.timeout_add(50, self.tick) def tick(self): self.queue_draw() return True def do_drawing(self, widget, ctx): self.draw(ctx, self.get_allocated_width(), self.get_allocated_height()) def draw(self, ctx, width, height): pass
In the above code stub, we created the Animator class extending the GTK.DrawingArea. Animator class is meant to be an abstract class for the other animators. We defined the size request of the area we want and connected the “draw” signal to the do_drawing method. On notable thing to note here is that, we have “draw” signal on GTK+ 3 while on GTK+2 we have “on_expose” signal. On the creation of the widget, draw signal is fired. In the handler do_drawing method, we receive widget and ctx. Here ctx is the Cairo context. We can perform our drawing with the help of the of Cairo context. We further call the draw method passing the context, width and height of the widget. On notable thing here is that, even though we requested for a size for the widget, allocated size might be different depending upon a number of factors. Thus, drawing must be done according to allocated area instead of the absolute area.
The draw is an abstract method. All the animators must override this method to implement custom drawing on the widget area. Lastly, we add a timeout based call to tick method. This is what drives the animation. This is done with the help of GLib.timeout_add(). Here the first argument is the time in milliseconds after which callback should be fired and second argument is the callback that should be fired. We are calling tick method in the class. It is required for the method to return True if successful for proper functioning. We call queue_draw method from within the tick method. queue_draw leads to invalidation of current area and again generates the draw signal.
Now that we know how the core of the animation will work, let us define some cool animations for the Listening phase of the application. We define the Listening Animator for the same.
class ListeningAnimator(Animator): def __init__(self, window, **properties): super().__init__(**properties) self.window = window self.tc = 0 def draw(self, ctx, width, height): self.tc += 0.2 self.tc %= 2 * math.pi for i in range(-4, 5): ctx.set_source_rgb(0.2, 0.5, 1) ctx.set_line_width(6) ctx.set_line_cap(cairo.LINE_CAP_ROUND) if i % 2 == 0: ctx.move_to(width / 2 + i * 10, height / 2 + 3 - 8 * math.sin(self.tc + i)) ctx.line_to(width / 2 + i * 10, height / 2 - 3 + 8 * math.sin(self.tc + i)) else: ctx.set_source_rgb(0.2, 0.7, 1) ctx.move_to(width / 2 + i * 10, height / 2 + 3 - 8 * math.cos(self.tc - i)) ctx.line_to(width / 2 + i * 10, height / 2 - 3 + 8 * math.cos(self.tc - i)) ctx.stroke()
In this we are drawing some lines with round cap to create an effect like below.
Since, the lines must move we are using trigonometric functions to create sinusoidal movement with some phase difference between adjacent lines. To set color of the brush, we use set_source_rgb method. We may then move the pointer to desired position, draw lines or other shapes. Then, we can either fill the shape or draw strokes using the relevant methods. The full list of methods can be accessed here in the official documentation.
After creating the widget, it can be easily added to the UI depending on the type of the container, generally by add method. You may access the full code of SUSI Linux repository to learn more about the usage in SUSI Linux App. The final result can be seen in the following video.