Escribiendo un salvapantallas para Gnu/Linux [C]

Los salvapantallas son algo que siempre me llamó poderosamente la antención pero hasta hace poco no sabía como se escribían, hoy aprenderemos a hacer uno para el XScreensaver de Gnu/Linux.

El primer paso es hacernos con el archivo vroot.h del xscreensaver (si hacemos apt-get source xscreensaver nos lo encontraremos en el directorio utils/ [o aquí]), y meterlo en el mismo directorio en el que desarrollaremos el resto del código. vroot.h es un archivo (bajo licencia de estilo BSD) usado comunmente para tomar al ventana_raíz, en la que tenemos que escribir si, por ejemplo, queremos hacer un salvapantallas.

Empezamos entonces con nuestro archivo, lo llamaremos salvapantallas.c y empezamos con el código, si bien va todo junto los comentarios hacen las "presentaciones":

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Cabeceras de X11 */
#include <X11/Xlib.h>
#include "vroot.h"

/* Esto no es necesario para el programa en si,
   per es muy útil en el caso de querer hacer
   comprobaciones y en el caso de que resulten
   mal, detener el programa.
*/
#include <assert.h>



/* Esta será la función que dibuje el salvapantallas
   por ahora no hace nada
*/
void drawScreen(Display* display, Window window,
                XWindowAttributes window_attributes,
                GC gc){

}

/* En la función principal tomaremos la ventana en
   la que escribiremos y la pasamos a la función anterior.
*/
int main (int argc, char **argv){

    /* Tomamos el identificador de pantalla. */
    char *display_id = getenv("DISPLAY");

    /* Suponemos que el display_id no es nulo. */
    assert(display_id != NULL);

    /* Conectamos con la pantalla. */
    Display *display = XOpenDisplay(display_id);

    /* Obtenemos la ventana, es la que mostrará el programa. */
    Window root = DefaultRootWindow(display);

    /* Creamos un contexto gráfico, este es el lienzo
       donde dibujamos.
    */
    GC gc = XCreateGC(display, root, 0, NULL);

    /* Por último leemos las dimensiones de la ventana, 
       que siempre es bueno saber cuales son
    */
    XWindowAttributes window_attributes; 
    XGetWindowAttributes(display, root, &window_attributes);

    /* Y solo queda dar vueltas esperando al usuario */
    while (1){
        /* Dibujamos algo. */
        drawScreen(display, root, window_attributes, gc);

        /* Forzamos los cambios. */
        XFlush(display);

    /* Y le damos un respiro a CPU. */
        usleep(2000);
    }

    /* Esto solo está para que el compilador esté tranquilo ;)  */
    return 0;
}

Y ya tenemos el esqueleto de un salvapantallas, vamos a meter algún relleno en drawScreen y ya lo configuramos, así ya se puede ir probando antes de meterse a hacer alguna animación en concreto.

/* Esta es la función que dibuja el salvapantallas. */
void drawScreen(Display* display, Window window,
                XWindowAttributes window_attributes,
                GC gc){

    /* Creamos un segundo buffer, en este se van haciendo los
       cambios antes de pasarlos a la pantalla, de esta forma
       conseguimos que todo valla suave.
       (Realmente ahora no hace falta, pero es una técnica
       que es bueno saber si se va a trabajar con los gráficos
       directamente.

    Creamos entonces un bitmap basado en la ventana con la
       misma altura, anchura y profundidad.
    */ 
    Pixmap double_buffer = XCreatePixmap(display, window,
                                         window_attributes.width,
                                         window_attributes.height,
                                         window_attributes.depth);

    /* Pintamos de negro. */
    XSetForeground(display, gc, 0);

    /* Y limpiamos el doble buffer. */
    XFillRectangle(display, double_buffer, gc,
                   0, 0, window_attributes.height,
                   window_attributes.width);

    /* El código de ejemplo dibuja un cuadrado de 4 pixeles
       que va dando vueltas alrededor del centro de la pantalla.
    */

    /* Primero determinamos la posición. */
    static int next_position = 0;


    /* Esto abulta mucho pero no tiene gran misterio,
       solo calcula la posición en la que deberá­a estar el cuadrado.
    */
    int x_center = window_attributes.width / 2,
        y_center = window_attributes.height / 2;

int x_position, y_position;

if (next_position < 128){      // Borde superior
        x_position = x_center + (next_position - 64);
        y_position = y_center - 64;
    }
    else if (next_position < 256){ // Borde derecho
        x_position = x_center + 64;
        y_position = y_center + ((next_position % 128) - 64);
    }
    else if (next_position < 384){ // Borde inferior
        x_position = x_center + (64 - (next_position % 128));
        y_position = y_center + 64;
    }
    else{                          // Borde izquierdo
        x_position = x_center - 64;
        y_position = y_center + (64 - (next_position % 128));
    }
    next_position = (next_position + 1) % 512;

    /* Ahora dibujamos el rectángulo.

       (Hay que tener en cuenta que la posición indica la 
       distancia desde la esquina superior izquierda.)
    */
    /* De color blanco. */
    XSetForeground(display, gc, 0xFFFFFF);

    /* Con centro en x_position, y_position . */
    XFillRectangle(display, double_buffer, gc,
                   x_position - 2, y_position - 2, 4, 4);

    /* Y lo pasamos a la pantalla. */
    XCopyArea(display, double_buffer, window,
              gc, 0, 0, 
              window_attributes.width, window_attributes.height,
              0, 0);


    /* Por último eliminamos el doble buffer de la memoria. */
    XFreePixmap(display, double_buffer);
}

El grupo de if/elses se hace un poco pesando, pero es lo más simple que encontré que pudiese producir una animación coherente y mostrase un poco de todo :/. Bien, ahora a probarlo, primero lo compilamos, hace falta avisarle al

gcc de que tiene que enlazarlo con la librería de X11:
gcc salvapantallas.c -o salvapantallas -lX11

Hecho esto, tenemos que guardarlo en /usr/lib/xscreensaver/ sudo mv salvapantallas /usr/lib/xscreensaver/

Luego solo queda añadirlo al archivo .xscreensaver de la carpeta $HOME, al final de la lista para poder encontrarlo fácilmente:

- salvapantallas -root \n\

Y ya lo podemos probar, lanzamos xscreensaver-demo y seleccionandolo se nos muestra una animación:

Bien, así se hace un salvapantallas, ahora vamos a ponerle otra animación, lo primero: borrar todo el código de drawAnimation :), vamos a dibujar la curva del dragón

/* Esta es la función que dibuja el salvapantallas. */
void drawScreen(Display* display, Window window,
                XWindowAttributes window_attributes,
                GC gc){


    static int step = 0; // nº de paso
    static int lastg = 0; // Último valor calculado (necesario para el fractal)
    static int x_position = -1; // Posicion actual en la horizontal
    static int y_position = -1; // Posicion actual en la vertical
    static int direction = 0; // 0 = arriba, 1 = derecha, 2 = abajo, 3 = izquierda
    static long color = 0;    // Color actual, 0 = negro, 0xFFFFFF = blanco

    /* Si es la primeira vez o salímos de la pantaia, reiniciamos las coordeadas, la
       dirección y el número de paso y vamos cambiando el color entre blanco y negro.
    */
    if ((x_position < 0) || (x_position >= window_attributes.width) ||
        (y_position < 0) || (y_position >= window_attributes.height)){

        x_position = window_attributes.width / 2;
        y_position = window_attributes.height / 2 ;

        // Dirección, numero de paso y último valor calculado a 0
        direction = step = lastg = 0;

        color ^= 0xFFFFFF; // Si es negro cambia a blanco y al revés
    }

    XSetForeground(display, gc, color);

    // Cálculos del fractal
    int g = step ^ (step >> 1);
    int t = (~g) & lastg;
    lastg = g;

    if (t == 0){ // Se t == 0, giramos 90 graos
        direction = (direction + 1) % 4;
    }
    else{
        // Dado que en C (-1 % 4) == -1,
        // usamos 3 como equivalente % 4
        direction = (direction + 3) % 4;
    }

    // Avanzamos según la dirección
    switch(direction){
    case 0:
        y_position--;
        break;

    case 1:
        x_position++;
        break;

    case 2:
        y_position++;
        break;

    case 3:
        x_position--;
        break;
    }

    XFillRectangle(display, window, gc,
                   x_position, y_position,
                   1, 1);

    // Nos preparamos para el siguiente paso
    step++;
}

Este es el resultado:

Que os parece, animaos a hacer uno vosotros también, todo sea por mirar los colorinchos :P.

ps: Aúa que tirara de tantos static, no se debe hacer, fue para evitar andar a hacer cambios por todo el código, non es algo que se deba hacer en código limpio ;)

Saludos

Activando los logs generales de MySQL sobre la marcha [tip] » « Esquivando nuestro antidebuggers simple a golpe de LD_PRELOAD