Search code examples
cgtk3cairo

How to draw rainbow cairo linear gradient using rgb values from an array?


I'm trying to draw a Cairo rainbow gradient on a gtk drawing surface. To do this, the RGB color data will be stored in an array and from that array, the rainbow will be drawn. I tried some functions from this website but didn't get the expected result. The script and images are given below:

My expectation: A smooth rainbow gradient from red to violet

This is the script:

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

uint32_t rgb(double ratio)
{
    int normalized = (int)(ratio * 256 * 6);

    //find the region for this position
    int region = normalized / 256;

    //find the distance to the start of the closest region
    int x = normalized % 256;

    uint8_t r = 0, g = 0, b = 0;
    switch (region)
    {
    case 0: r = 255; g = 0;   b = 0;   g += x; break;
    case 1: r = 255; g = 255; b = 0;   r -= x; break;
    case 2: r = 0;   g = 255; b = 0;   b += x; break;
    case 3: r = 0;   g = 255; b = 255; g -= x; break;
    case 4: r = 0;   g = 0;   b = 255; r += x; break;
    case 5: r = 255; g = 0;   b = 255; b -= x; break;
    }
    return (r << 16) | (g << 8) | b;
            //return rgb value
}
//get red color
uint8_t getRed(uint32_t c) {
    return c >> 16;
}
//get green color
uint8_t getGreen(uint32_t c) {
    return c >> 8;
}
//get blue color
uint8_t getBlue(uint32_t c) {
    return c;
}

//draw 
void draw_gradient(cairo_t *);
static gboolean on_draw_event(GtkWidget *widget, cairo_t *cr, gpointer user_data)
{             
  draw_gradient(cr);
  return FALSE;
}
void draw_gradient(cairo_t *cr)
{
    int width = 256; //window width
    int height = 50; //window height

    cairo_pattern_t *gradient;
    gradient =cairo_pattern_create_linear(0.0, 0.0,  width, 0.0);

    double range = width;
    for (int i = 0; i < range; i++)
{
        uint32_t color = rgb(i / range);//get rgb 

        printf("colors %d | %d %d %d\n", i, getRed(color), getGreen(color), getBlue(color));
        cairo_pattern_add_color_stop_rgba (gradient, i, getRed(color), getGreen(color), getBlue(color), 1.0);
    }

    cairo_set_source (cr, gradient);
    cairo_paint (cr);
    cairo_pattern_destroy (gradient);
}

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);

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

  gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
  gtk_window_set_default_size(GTK_WINDOW(window), 400, 30); 
  gtk_window_set_title(GTK_WINDOW(window), "Linear gradients");

  gtk_widget_set_app_paintable(window, TRUE);
  gtk_widget_show_all(window);

  gtk_main();

  return 0;
}

It only draws a 2 color linear gradient, check the image:

Gradient where the left two thirds are a gradient from dark blue to cyan, and the remaining third is uniform magenta


Solution

  • Cairo expects lots of things to be between 0.0 and 1.0, but you are passing in values between 0.0 and 256.0 (the colors) or 0 and width.

    Here is a patch that fixes lots of things for me:

    --- t.c.orig    2024-06-20 18:01:42.188560553 +0200
    +++ t.c 2024-06-20 18:04:05.225964541 +0200
    @@ -53,7 +53,7 @@ void draw_gradient(cairo_t *cr)
             uint32_t color = rgb(i / range);//get rgb 
     
             printf("colors %d | %d %d %d\n", i, getRed(color), getGreen(color), getBlue(color));
    -        cairo_pattern_add_color_stop_rgba (gradient, i, getRed(color), getGreen(color), getBlue(color), 1.0);
    +        cairo_pattern_add_color_stop_rgba (gradient, i / range, getRed(color) / 256.0, getGreen(color) / 256.0, getBlue(color) / 256.0, 1.0);
         }
     
         cairo_set_source (cr, gradient);
    

    I do not have GTK headers installed and so I made a cairo-only version of your program.

    #include <cairo.h>
    #include <stdint.h>
    #include <stdio.h>
    
    uint32_t rgb(double ratio)
    {
        int normalized = (int)(ratio * 256 * 6);
    
        //find the region for this position
        int region = normalized / 256;
    
        //find the distance to the start of the closest region
        int x = normalized % 256;
    
        uint8_t r = 0, g = 0, b = 0;
        switch (region)
        {
        case 0: r = 255; g = 0;   b = 0;   g += x; break;
        case 1: r = 255; g = 255; b = 0;   r -= x; break;
        case 2: r = 0;   g = 255; b = 0;   b += x; break;
        case 3: r = 0;   g = 255; b = 255; g -= x; break;
        case 4: r = 0;   g = 0;   b = 255; r += x; break;
        case 5: r = 255; g = 0;   b = 255; b -= x; break;
        }
        return (r << 16) | (g << 8) | b;
                //return rgb value
    }
    //get red color
    uint8_t getRed(uint32_t c) {
        return c >> 16;
    }
    //get green color
    uint8_t getGreen(uint32_t c) {
        return c >> 8;
    }
    //get blue color
    uint8_t getBlue(uint32_t c) {
        return c;
    }
    
    //draw 
    void draw_gradient(cairo_t *cr)
    {
        int width = 256; //window width
        int height = 50; //window height
    
        cairo_pattern_t *gradient;
        gradient =cairo_pattern_create_linear(0.0, 0.0,  width, 0.0);
    
        double range = width;
        for (int i = 0; i < range; i++)
    {
            uint32_t color = rgb(i / range);//get rgb 
    
            printf("colors %d | %d %d %d\n", i, getRed(color), getGreen(color), getBlue(color));
            cairo_pattern_add_color_stop_rgba (gradient, i / range, getRed(color) / 256.0, getGreen(color) / 256.0, getBlue(color) / 256.0, 1.0);
        }
    
        cairo_set_source (cr, gradient);
        cairo_paint (cr);
        cairo_pattern_destroy (gradient);
    }
    
    int main(int argc, char *argv[])
    {
        cairo_surface_t *s = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, 256, 50);
        cairo_t *cr = cairo_create(s);
        draw_gradient(cr);
        cairo_surface_write_to_png(s, "gradient.png");
        cairo_surface_destroy(s);
        cairo_destroy(cr);
    
      return 0;
    }
    

    Output: Resulting output

    This still does not look like what you expect it to look like, but that is a question on where you got the rgb() function from.


    Note that this can be greatly simplified. rgb() looks like it has 6 points and just linearly interpolates between those points. You can just let cairo do that for you: Here is a version that produces the same output and does not use your rgb() function.

    --- t.c.own 2024-06-20 18:06:27.364129263 +0200
    +++ t.c 2024-06-20 18:07:36.103342891 +0200
    @@ -47,14 +47,12 @@ void draw_gradient(cairo_t *cr)
         cairo_pattern_t *gradient;
         gradient =cairo_pattern_create_linear(0.0, 0.0,  width, 0.0);
     
    -    double range = width;
    -    for (int i = 0; i < range; i++)
    -{
    -        uint32_t color = rgb(i / range);//get rgb 
    -
    -        printf("colors %d | %d %d %d\n", i, getRed(color), getGreen(color), getBlue(color));
    -        cairo_pattern_add_color_stop_rgba (gradient, i / range, getRed(color) / 256.0, getGreen(color) / 256.0, getBlue(color) / 256.0, 1.0);
    -    }
    +    cairo_pattern_add_color_stop_rgb (gradient, 0.0 / 5.0, 1, 0, 0);
    +    cairo_pattern_add_color_stop_rgb (gradient, 1.0 / 5.0, 1, 1, 0);
    +    cairo_pattern_add_color_stop_rgb (gradient, 2.0 / 5.0, 0, 1, 0);
    +    cairo_pattern_add_color_stop_rgb (gradient, 3.0 / 5.0, 0, 1, 1);
    +    cairo_pattern_add_color_stop_rgb (gradient, 4.0 / 5.0, 0, 0, 1);
    +    cairo_pattern_add_color_stop_rgb (gradient, 5.0 / 5.0, 1, 0, 1);
     
         cairo_set_source (cr, gradient);
         cairo_paint (cr);