lunes, 6 de agosto de 2012

Escrebendo un salvapantallas para Gnu/Linux [C]

Os salvapantallas son algo que sempre me chamou poderosamente a atención pero ata fai pouco non sabía como se escribían, hoxe aprenderemos a facer un para o XScreensaver do Gnu/Linux.



O primeiro paso e facernos co arquivo vroot.h do xscreensaver (se facemmos apt-get source xscreensaver atoparemos un no directorio utils/ [ou aquí]), e metelo no mesmo directorio no que desenrrolaremos o resto do código. vroot.h é un arquivo (baixo licencia de estilo BSD) usado comunmente para tomar a xanela raíz, na que temos que escreber, por exemplo, se queremos facer un salvapantallas.

Nota: Os comentarios serán en galego, pero os nomes de variables e funcións serán en inglés.
Nota2: Por experiencia aconsello firmemente que non vos limitedes a copiar-encolar o código, senón que o escribades vos mesmos, serve para comprendelo moito mellor ;).

Empezamos logo co noso arquivo, chamaremolo salvapantallas.c e empezamos co código, se ben vai todo xunto os comentarios fan as "presentacións":
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Cabeceiras do X11 */
#include <X11/Xlib.h>
#include "vroot.h"

/* Este non é necesario para o programa mesmo,
   mais é moi útil no caso de querer facer
   comprobacións e que no caso de que resulten
   mal, deter o programa.
*/
#include <assert.h>



/* Esta será a función que debuxe o salvapantallas
   por agora non fai nada. 
*/
void drawScreen(Display* display, Window window,
                XWindowAttributes window_attributes,
                GC gc){
    
}

/* Na función principal tomaremos a xanela na que escreberemos
   e pasaremola á función anterior.
*/
int main (int argc, char **argv){
    
    /* Tomamos o identificador da pantaia. */
    char *display_id = getenv("DISPLAY");
    
    /* Supoñemos que o display_id non é nulo. */
    assert(display_id != NULL);

    /* Conectamos coa pantaia. */
    Display *display = XOpenDisplay(display_id);

    /* Obtemos a xanela raíz, é a que amosará o programa. */
    Window root = DefaultRootWindow(display);

    /* Creamos un contexto gráfico, este é o lenzo onde debuxamos. */
    GC gc = XCreateGC(display, root, 0, NULL);
    
    /* Por último lemos as dimensións da xanela, que sempre é bo saber cales son :P */
    XWindowAttributes window_attributes; 
    XGetWindowAttributes(display, root, &window_attributes);

    /* É só queda dar voltas esperando o usuario */
    while (1){
        /* Debuxamos algo. */
        drawScreen(display, root, window_attributes, gc);
 
        /* Forzamos os cambios. */
        XFlush(display);

        /* E damoslle un respiro a CPU. */
        usleep(2000);
    }

    /* Isto só está para que o compilador esté quedo ;) */
    return 0;
}

É xa temos ó esqueleto dun salvapantallas, imos meter algun recheo no drawScreen é xa ó configuramos, así se pode ir probando antes de meterse a facer algunha animación en concreto.

(Este código iría no drawScreen)
/* Esta é a función que debuxa o salvapantallas. */
void drawScreen(Display* display, Window window,
                XWindowAttributes window_attributes,
                GC gc){

    /* Creamos un segundo buffer, neste vanse facendo os 
       cambios antes de pasalos a pantaia, de esta forma
       acadamos que todo vaia suave.
       (No primeiro exemplo non fai falla, pero é unha
       técnica que compre coñecer se se vai traballar cos
       gráficos directamente.

       Entón creamos un bitmap baseado na ventá coa mesma
       altura, anchura e profundidade.
    */ 
    Pixmap double_buffer = XCreatePixmap(display, window,
                                         window_attributes.width,
                                         window_attributes.height,
                                         window_attributes.depth);

    /* Debuxamos en negro. */
    XSetForeground(display, gc, 0);

    /* E limpamos o dobre buffer. */
    XFillRectangle(display, double_buffer, gc,
                   0, 0, window_attributes.height,
                   window_attributes.width);

    /* O código de exemplo é moi sinxelo, debuxa un cadrado de 4 pixeles 
       que vai dando voltas arredor do centro da pantaia.
    */

    /* Primeiro determinamos a posición. */
    static int next_position = 0;


    /* Isto abulta moito pero non ten gran misterio, 
       so calcula a posición no que debería estar o cadrado.
    */
    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 dereito
        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 esquerdo
        x_position = x_center - 64;
        y_position = y_center + (64 - (next_position % 128));
    }
    next_position = (next_position + 1) % 512;

    /* Agora debullamos o rectángulo.
       
       (Hay que ter en conta que a posición indica a esquina
       superior esquerda.)
    */
    /* De cor branca. */
    XSetForeground(display, gc, 0xFFFFFF);

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

    /* E pasamolo á pantaia. */
    XCopyArea(display, double_buffer, window,
              gc, 0, 0, 
              window_attributes.width, window_attributes.height,
              0, 0);


    /* Por último eliminamo-lo dobre buffer da memoria. */
    XFreePixmap(display, double_buffer);
}
O grupo de if/elses faise un pouco pesado no medio, pero é o máis sinxelo que atopei que puidese producir unha animación coherente e amosase un pouco de todo :/. Ben, agora a probalo, primeiro compilamolo, fai falla avisarlle o gcc que ten que linkalo coa librería do X11:
gcc salvapantallas.c -o salvapantallas -lX11
Feito isto, temos que gardalo en /usr/lib/xscreensaver/
sudo mv salvapantallas /usr/lib/xscreensaver/
Feito isto so queda añadilo o arquivo .xscreensaver na carpeta do $HOME, o final da lista para poder atopalo facilmente:
- salvapantallas -root \n\
E xa o podemos probar, lanzamos xscreensaver-demo e seleccionandoo amosasenos unha demostración:


Ben, así se fai un salvapantallas, agora imos ver de poñerlle outra animación, o primeiro: borrar todo o código de drawAnimation :), imos debuxar a curva do dragón, de forma simple, e directamente na pantaia:
/* Esta é a función que debuxa o 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 o fractal)
    static int x_position = -1; // Posicion actual na horizontal
    static int y_position = -1; // Posicion actual na vertical
    static int direction = 0; // 0 = arriba, 1 = dereita, 2 = abaixo, 3 = esquerda
    static long color = 0;    // Cor actual, 0 = negro, 0xFFFFFF = branco

    /* Se é a primeira vez ou saímos da pantaia, reiniciamos as coordeadas, a
       dirección e o número do paso e imos cambiando a cor entre branco e 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 e último valor calculado a 0
        direction = step = lastg = 0;

        color ^= 0xFFFFFF; // Se e negro cambia a branco e o revés
    }

    XSetForeground(display, gc, color);

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

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

    // Avanzamos segundo a 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);
    
    // Preparamonos para o seguinte paso
    step++;
}

E este é o resultado:


Que vos parece, animadevos a facer un vos tamén, todo sexa por mirar para os colorinchos :P.

ps: Aínda que tirara de tantos static, non se debe facer, foi por mor de evitar andar a facer cambios arredor de todo o código, non é algo que se deba facer en código limpo ;)

Saúdos

No hay comentarios:

Publicar un comentario