1

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();
}
delxa
  • 99
  • 1
  • 8

2 Answers2

3

First of all consider your original program works flawlessly on my system, so this could potentially be a problem on your system. You can try to raise the priority of the program but basically your code is susceptible to this kind of problems: see g_timeout_add for the rationale.

There is anyway a conceptual problem: you are using draw() to do two things. It is used as draw signal and as timeout callback. This is wrong: the draw signal must be idempotent because you don't know when and how many times it is called.

Here is your example with the original function split into move() and draw():

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

static int width, height,
           posX = 0,
           vX = 2;

static gboolean move(GtkWidget* widget)
{
    GtkWidget* window = gtk_widget_get_toplevel(widget);
    gtk_window_get_size(GTK_WINDOW(window), &width, &height);

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

    gtk_widget_queue_draw(widget);
    return TRUE;
}

static gboolean draw(GtkWidget* widget, cairo_t* cr)
{
    cairo_set_source_rgb(cr, 0, 0, 0);
    cairo_set_line_width(cr, 100);

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

    return FALSE;
}

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)move, window);

    gtk_widget_show_all(window);
    gtk_main();
}

Not sure your issue will be solved, but this must be done anyway.

ntd
  • 7,372
  • 1
  • 27
  • 44
2

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:

liberforce
  • 11,189
  • 37
  • 48