Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create part of a rainbow gradient

I need to generate a gradient bitmap that displays a rainbow gradient between two colors which are chosen by a user. To generate a rainbow is easy. The code below I got from Wiki and slightly adapted it. It has the advantage of being fast and simple.

 function TColor_Dialog.GiveRainbowColor (fraction: double): TAlphaColor;
  var
    m: Double;
    r, g, b, mt: Byte;
  begin
    if fraction <= 0 then m := 0 else
    if fraction >= 1 then m := 6
                     else m := fraction * 6;
    mt := (round (frac (m) * $FF));
    case Trunc (m) of
    0: begin
        R := $FF;
        G := mt;
        B := 0;
      end;
    1: begin
        R := $FF - mt;
        G := $FF;
        B := 0;
      end;
    2: begin
        R := 0;
        G := $FF;
        B := mt;
      end;
    3: begin
        R := 0;
        G := $FF - mt;
        B := $FF;
      end;
    4: begin
        R := mt;
        G := 0;
        B := $FF;
      end;
    5: begin
        R := $FF;
        G := 0;
        B := $FF - mt;
      end;
    end; // case

    Result := ColorToQuad (r, g, b);
  end; // GiveRainbowColor //

Trouble with this algorithm is it can't show a partial rainbow between two colors. Well, of course it can but than you have to approach the fraction of each color and I don't like that solution. I tried decomposing the color into its r, g, b channels but that did not work. The reason is quite obvious by hindsight. Suppose you need a gradient between FF0000 and 0000FF. You'll have a red color transforming from FF->00 and a blue from 00->FF. However, there is no green (00FF00) which is clearly present in a rainbow gradient.

What I need is a gradient function that I can give two colors and a fraction and it generates a color. Can anyone point me to an article, algorithm or even code?

Update

NGLN's answer is the right answer for this question. Both he and Warren wondered what to do when a a color is not a bright color (a color containing a 0, a $FF and a value). I tried several angles: up/downscaling and HSL interpolation. I finally settled down for the last one as being the most simple.

Basically you have two colors: from and to. Use RGBtoHSL to extract the HSL parameters from each color: RGBtoHSL (col_from, hf, sf, lf). Next compute the hue, saturation and luminance between both colors and reconstruct a new color. This is what NGLN mentions in his update about hue, but if you generalise this principle you have a rainbow between any color.

function TColor_Dialog.interpolate_hsl (col_from, col_to: TAlphaColor; fraction: double): TAlphaColor;
  var af, at, ad: uInt8;
      hf, ht, hd: single;
      sf, st, sd: single;
      lf, lt, ld: single;
  begin
  // Get each rgb color channel
     af := GetAValue (col_from);
     at := GetAValue (col_to);
     RGBtoHSL (col_from, hf, sf, lf);
     RGBtoHSL (col_to,   ht, st, lt);

  // Compute differences
     ad := af + Round (fraction * (at - af));
     hd := hf + fraction * (ht - hf);
     sd := sf + fraction * (st - sf);
     ld := lf + fraction * (lt - lf);

     Result := MakeColor (HSLtoRGB (hd, sd, ld), ad);
  end; // interpolate_hsl //

This gives a rainbow for all colors possible. I apply the same interpolation for the opacity, hence the use of MakeColor to 'fumble' the interpolated alpha channel into the color.

enter image description here

like image 303
Arnold Avatar asked Oct 31 '13 20:10

Arnold


1 Answers

Then you need to calculate the position of a color in the Rainbow; the inverse of GiveRainbowColor:

function RainbowIndex(BrightColor: TColor): Double;
var
  R: Byte;
  G: Byte;
  B: Byte;
begin
  R := GetRValue(ColorToRGB(BrightColor));
  G := GetGValue(ColorToRGB(BrightColor));
  B := GetBValue(ColorToRGB(BrightColor));
  if (R * G * B <> 0) or ((R <> 255) and (G <> 255) and (B <> 255)) then
    Result := -1
  else if B = 0 then
    if R = 255 then
      Result := 0 + G / 255
    else
      Result := 1 + (255 - R) / 255
  else if R = 0 then
    if G = 255 then
      Result := 2 + B / 255
    else
      Result := 3 + (255 - G) / 255
  else { G = 0 }
    if B = 255 then
      Result := 4 + R / 255
    else
      Result := 5 + (255 - B) / 255;
  Result := Result / 6;
end;

(But this displays a problem for colors not having both a 0 and a 255 part. In other words: you would also need to calculate the bright color from a shaded, tinted or grayed color. See update below.)

Example usage to get the rainbow slice from clRed to clBlue:

enter image description here

procedure TForm1.FormPaint(Sender: TObject);
var
  Start: Double;
  Finish: Double;
  X: Integer;
begin
  Start := RainbowIndex(clRed);
  Finish := RainbowIndex(clBlue);
  for X := 0 to ClientWidth - 1 do
  begin
    Canvas.Brush.Color := GiveRainbowColor(0, ClientWidth - 1, X);
    Canvas.FillRect(Bounds(X, 0, 1, 50));
    Canvas.Brush.Color :=
      GiveRainbowColor(0, ClientWidth - 1, Round(Start + (Finish - Start) * X));
    Canvas.FillRect(Bounds(X, 50, 1, 50));
  end;
end;

Update:

The RainbowIndex routine above really does nothing more then calculate the hue property of the color. The GraphUtil unit provides conversion routines for the HSL color model which makes this RainbowIndex routine kind of obsolete and gives the advantage to be able to feed any TColor value:

uses
  GraphUtil;

const
  HLSMAX = 240;

function Hue(AColor: TColor): Double;
var
  Hue: Word;
  Luminance: Word;
  Saturation: Word;
begin
  ColorRGBToHLS(ColorToRGB(AColor), Hue, Luminance, Saturation);
  Result := Hue / HLSMAX;
end;

Example usage to get the rainbow slice from clMoneyGreen to clPurple:

enter image description here

function RainbowColor(Hue: Double): TColor; overload;
begin
  Hue := EnsureRange(Hue, 0, 1) * 6;
  case Trunc(Hue) of
    0: Result := RGB(255, Round(Frac(Hue) * 255), 0);
    1: Result := RGB(255 - Round(Frac(Hue) * 255), 255, 0);
    2: Result := RGB(0, 255, Round(Frac(Hue) * 255));
    3: Result := RGB(0, 255 - Round(Frac(Hue) * 255), 255);
    4: Result := RGB(Round(Frac(Hue) * 255), 0, 255);
  else
    Result := RGB(255, 0, 255 - Round(Frac(Hue) * 255));
  end;
end;

function RainbowColor(MinHue, MaxHue, Hue: Integer): TColor; overload;
begin
  Result := RainbowColor((Hue - MinHue) / (MaxHue - MinHue + 1));
end;

procedure TForm1.FormPaint(Sender: TObject);
var
  X: Integer;
  Start: Double;
  Finish: Double;
begin
  Start := Hue(clMoneyGreen);
  Finish := Hue(clPurple);
  for X := 0 to ClientWidth - 1 do
  begin
    Canvas.Brush.Color := RainbowColor(0, ClientWidth - 1, X);
    Canvas.FillRect(Bounds(X, 0, 1, 50));
    Canvas.Brush.Color :=
      RainbowColor(Start + (Finish - Start) * X / ClientWidth);
    Canvas.FillRect(Bounds(X, 50, 1, 50));
  end;
end;

Furthermore, the RainbowColor routine could be shortened to:

function RainbowColor(Hue: Double): TColor; overload;
begin
  Result := ColorHLStoRGB(Round(Hue * HLSMAX), HLSMAX div 2, HLSMAX);
end;
like image 150
NGLN Avatar answered Nov 18 '22 05:11

NGLN