Search code examples
cgtkgtk3cairo

Gtk3 and cairo animation twitch


I've made a very simple animation with gtk3 and cairo. Once a sec it's a little bit twitch. It's really annoying and it doesn't look well. Why it happens and how can I fix this?

#include <gtk/gtk.h>
#include <cairo.h>

static int width, height,
           posX = 0,
           vX = 2;
gboolean draw(GtkWidget* widget, cairo_t* cr)
{
    GtkWidget* window = gtk_widget_get_toplevel(widget);
    gtk_window_get_size(GTK_WINDOW(window), &width, &height);

    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, 100);

    cairo_rectangle(cr, posX, height/2, 100, 100);
    cairo_stroke(cr);

    if(posX + vX >= width || posX + vX == 0)
        vX = -vX;
    posX += vX;

    gtk_widget_queue_draw(widget);
    return TRUE;
}
int main(int argc, char** argv)
{
    GtkWidget* window;
    GtkWidget* darea;

    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    darea = gtk_drawing_area_new();

    gtk_container_add(GTK_CONTAINER(window), darea);
    gtk_window_set_default_size(GTK_WINDOW(window), 500, 400);

    g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(darea), "draw", G_CALLBACK(draw), NULL);

    g_timeout_add(16, (GSourceFunc)draw, window);

    gtk_widget_show_all(window);
    gtk_main();
}

Solution

  • I think the problem you have is that you're using g_timeout_add as if it was an exact timing source, which is not as, stated by the documentation:

    Note that timeout functions may be delayed, due to the processing of other event sources. Thus they should not be relied on for precise timing. After each call to the timeout function, the time of the next timeout is recalculated based on the current time and the given interval (it does not try to 'catch up' time lost in delays).

    This means your code in the draw callback may be called slightly later (or much later), at each call. As the timeout is not recalculated, the errors add up. You'll get out of sync and draw at the wrong position. This for example happens in video players when a frame is decoded: if the frame takes too long to be decoded, it may be dropped, because maybe we're already too late and need to display the next frame.

    I'm not sure of what is the right solution to this, maybe GTK+ (or Clutter, which is really made for animation) developers could give you some hints, so it's a good idea to ask them through their IRC channel.

    However, I encountered that issue myself while programming a metronome. If you try to synchronize with g_timeout_add, errors add up and you get out of sync. Here's what I've done and worked for me.

    First, I fire up a GTimer at the very beginning, so I have a reliable, precise and absolute time reference. Then when my callback is called, I:

    • calculate the time elapsed since last tick (since last frame in your case)
    • play my tick (display the object at right position for that timing in your case)
    • calculate how much time is left before the next tick (next frame)
    • call g_timeout_add with that value (it creates a new event source)
    • return G_SOURCE_REMOVE so that the current event source is not called anymore (I replace it by the new one, basically)

    Here's my metronome code for reference: https://github.com/liberforce/metrognome/blob/master/metronome.c

    You may also use g_timeout_add_full instead of g_timeout_add so you can use a higher priority.

    I also recommend reading a series of articles from Owen Taylor about animation synchronization in gnome-shell: