unit ascolors;

{ **************************************************************************** }
{ Rejbrand AlgoSim colour structures and functions                             }
{ Copyright © 2017 Andreas Rejbrand                                            }
{ https://english.rejbrand.se/                                                 }
{ **************************************************************************** }

{$WARN SYMBOL_PLATFORM OFF}
{$WARN DUPLICATE_CTOR_DTOR OFF}

interface

//                             TColor
//                               | (*)
//      Component constr. ->   TRGB      -----}
//                             //\\           }  -> Zero-cost cast (memory
//                            //  \\          }     reinterpretation) to
//                           //    \\         }     TColorComponents possible.
//   Component constr. ->   THSV  THSL   -----}
//                            \    /
//                             \  /
//                              \/
//                            TColor
//
//          Legend: Single lines: implicit cast possible
//                             *: low-cost cast
//                  Double lines: (only) explicit cast possible

uses
  Windows, SysUtils, Types, Math,
  {$IF CompilerVersion >= 22}Vcl.Graphics{$ELSE}Graphics{$IFEND},
  Generics.Defaults, Generics.Collections;

type
  EColorException = class(Exception);

  PRGB = ^TRGB;
  TRGB = packed record
    Red, Green, Blue: Double;
    constructor Create(const ARed, AGreen, ABlue: Double); overload;
    class operator Implicit(const APascalColor: TColor): TRGB;
    class operator Implicit(const AColor: TRGB): TColor; inline;
    class operator Equal(const AColor1, AColor2: TRGB): Boolean;
    class operator NotEqual(const AColor1, AColor2: TRGB): Boolean; inline;
    class operator Add(const AColor1, AColor2: TRGB): TRGB; inline;
    class operator Subtract(const AColor1, AColor2: TRGB): TRGB; inline;
    class operator Multiply(const AFactor: Double; const AColor: TRGB): TRGB; inline;
    class operator Divide(const AColor: TRGB; const ADenominator: Double): TRGB; inline;
    function IsValid: Boolean;
    function Invert: TRGB;
    function Average: Double;
  end;

  PHSL = ^THSL;
  THSL = packed record
    Hue, Saturation, Lightness: Double;
    constructor Create(const AHue, ASaturation, ALightness: Double); overload;
    class operator Implicit(const APascalColor: TColor): THSL; inline;
    class operator Implicit(const AColor: THSL): TColor; inline;
    class operator Explicit(const AColor: THSL): TRGB;
    class operator Explicit(const AColor: TRGB): THSL;
    class operator Equal(const AColor1, AColor2: THSL): Boolean;
    class operator NotEqual(const AColor1, AColor2: THSL): Boolean; inline;
    function IsValid: Boolean;
  end;

  PHSV = ^THSV;
  THSV = packed record
    Hue, Saturation, Value: Double;
    constructor Create(const AHue, ASaturation, AValue: Double);  overload;
    class operator Implicit(const APascalColor: TColor): THSV; inline;
    class operator Implicit(const AColor: THSV): TColor; inline;
    class operator Explicit(const AColor: THSV): TRGB;
    class operator Explicit(const AColor: TRGB): THSV;
    class operator Equal(const AColor1, AColor2: THSV): Boolean;
    class operator NotEqual(const AColor1, AColor2: THSV): Boolean; inline;
    function IsValid: Boolean;
  end;

  PColorComponents = ^TColorComponents;
  TColorComponents = record
    Components: array[0..2] of Double;
    function Min: Double; inline;
    function Max: Double; inline;
    class operator Equal(const AColor1, AColor2: TColorComponents): Boolean; inline;
    class operator NotEqual(const AColor1, AColor2: TColorComponents): Boolean; inline;
  end;

const
  MIN_COLOR_COMPONENT = 0;
  MAX_COLOR_COMPONENT = 2;

function SameColor(AColor1, AColor2: TColor): Boolean; inline;
function RandomColor: TColor; inline;

function CompareColor(const A, B: THSV): TValueRelationship; overload;
function CompareColor(const A, B: TRGB): TValueRelationship; overload;

type
  TNamedColor = record
    Name: string;
    Value: TColor;
  end;

const
  NamedColors: array[0..146] of TNamedColor =
    (
      (Name: 'aliceblue'; Value: $00FFF8F0),
      (Name: 'antiquewhite'; Value: $00D7EBFA),
      (Name: 'aqua'; Value: $00FFFF00),
      (Name: 'aquamarine'; Value: $00D4FF7F),
      (Name: 'azure'; Value: $00FFFFF0),
      (Name: 'beige'; Value: $00DCF5F5),
      (Name: 'bisque'; Value: $00C4E4FF),
      (Name: 'black'; Value: $00000000),
      (Name: 'blanchedalmond'; Value: $00CDEBFF),
      (Name: 'blue'; Value: $00FF0000),
      (Name: 'blueviolet'; Value: $00E22B8A),
      (Name: 'brown'; Value: $002A2AA5),
      (Name: 'burlywood'; Value: $0087B8DE),
      (Name: 'cadetblue'; Value: $00A09E5F),
      (Name: 'chartreuse'; Value: $0000FF7F),
      (Name: 'chocolate'; Value: $001E69D2),
      (Name: 'coral'; Value: $00507FFF),
      (Name: 'cornflowerblue'; Value: $00ED9564),
      (Name: 'cornsilk'; Value: $00DCF8FF),
      (Name: 'crimson'; Value: $003C14DC),
      (Name: 'cyan'; Value: $00FFFF00),
      (Name: 'darkblue'; Value: $008B0000),
      (Name: 'darkcyan'; Value: $008B8B00),
      (Name: 'darkgoldenrod'; Value: $000B86B8),
      (Name: 'darkgray'; Value: $00A9A9A9),
      (Name: 'darkgreen'; Value: $00006400),
      (Name: 'darkgrey'; Value: $00A9A9A9),
      (Name: 'darkkhaki'; Value: $006BB7BD),
      (Name: 'darkmagenta'; Value: $008B008B),
      (Name: 'darkolivegreen'; Value: $002F6B55),
      (Name: 'darkorange'; Value: $00008CFF),
      (Name: 'darkorchid'; Value: $00CC3299),
      (Name: 'darkred'; Value: $0000008B),
      (Name: 'darksalmon'; Value: $007A96E9),
      (Name: 'darkseagreen'; Value: $008FBC8F),
      (Name: 'darkslateblue'; Value: $008B3D48),
      (Name: 'darkslategray'; Value: $004F4F2F),
      (Name: 'darkslategrey'; Value: $004F4F2F),
      (Name: 'darkturquoise'; Value: $00D1CE00),
      (Name: 'darkviolet'; Value: $00D30094),
      (Name: 'deeppink'; Value: $009314FF),
      (Name: 'deepskyblue'; Value: $00FFBF00),
      (Name: 'dimgray'; Value: $00696969),
      (Name: 'dimgrey'; Value: $00696969),
      (Name: 'dodgerblue'; Value: $00FF901E),
      (Name: 'firebrick'; Value: $002222B2),
      (Name: 'floralwhite'; Value: $00F0FAFF),
      (Name: 'forestgreen'; Value: $00228B22),
      (Name: 'fuchsia'; Value: $00FF00FF),
      (Name: 'gainsboro'; Value: $00DCDCDC),
      (Name: 'ghostwhite'; Value: $00FFF8F8),
      (Name: 'gold'; Value: $0000D7FF),
      (Name: 'goldenrod'; Value: $0020A5DA),
      (Name: 'gray'; Value: $00808080),
      (Name: 'green'; Value: $00008000),
      (Name: 'greenyellow'; Value: $002FFFAD),
      (Name: 'grey'; Value: $00808080),
      (Name: 'honeydew'; Value: $00F0FFF0),
      (Name: 'hotpink'; Value: $00B469FF),
      (Name: 'indianred'; Value: $005C5CCD),
      (Name: 'indigo'; Value: $0082004B),
      (Name: 'ivory'; Value: $00F0FFFF),
      (Name: 'khaki'; Value: $008CE6F0),
      (Name: 'lavender'; Value: $00FAE6E6),
      (Name: 'lavenderblush'; Value: $00F5F0FF),
      (Name: 'lawngreen'; Value: $0000FC7C),
      (Name: 'lemonchiffon'; Value: $00CDFAFF),
      (Name: 'lightblue'; Value: $00E6D8AD),
      (Name: 'lightcoral'; Value: $008080F0),
      (Name: 'lightcyan'; Value: $00FFFFE0),
      (Name: 'lightgoldenrodyellow'; Value: $00D2FAFA),
      (Name: 'lightgray'; Value: $00D3D3D3),
      (Name: 'lightgreen'; Value: $0090EE90),
      (Name: 'lightgrey'; Value: $00D3D3D3),
      (Name: 'lightpink'; Value: $00C1B6FF),
      (Name: 'lightsalmon'; Value: $007AA0FF),
      (Name: 'lightseagreen'; Value: $00AAB220),
      (Name: 'lightskyblue'; Value: $00FACE87),
      (Name: 'lightslategray'; Value: $00998877),
      (Name: 'lightslategrey'; Value: $00998877),
      (Name: 'lightsteelblue'; Value: $00DEC4B0),
      (Name: 'lightyellow'; Value: $00E0FFFF),
      (Name: 'lime'; Value: $0000FF00),
      (Name: 'limegreen'; Value: $0032CD32),
      (Name: 'linen'; Value: $00E6F0FA),
      (Name: 'magenta'; Value: $00FF00FF),
      (Name: 'maroon'; Value: $00000080),
      (Name: 'mediumaquamarine'; Value: $00AACD66),
      (Name: 'mediumblue'; Value: $00CD0000),
      (Name: 'mediumorchid'; Value: $00D355BA),
      (Name: 'mediumpurple'; Value: $00DB7093),
      (Name: 'mediumseagreen'; Value: $0071B33C),
      (Name: 'mediumslateblue'; Value: $00EE687B),
      (Name: 'mediumspringgreen'; Value: $009AFA00),
      (Name: 'mediumturquoise'; Value: $00CCD148),
      (Name: 'mediumvioletred'; Value: $008515C7),
      (Name: 'midnightblue'; Value: $00701919),
      (Name: 'mintcream'; Value: $00FAFFF5),
      (Name: 'mistyrose'; Value: $00E1E4FF),
      (Name: 'moccasin'; Value: $00B5E4FF),
      (Name: 'navajowhite'; Value: $00ADDEFF),
      (Name: 'navy'; Value: $00800000),
      (Name: 'oldlace'; Value: $00E6F5FD),
      (Name: 'olive'; Value: $00008080),
      (Name: 'olivedrab'; Value: $00238E6B),
      (Name: 'orange'; Value: $0000A5FF),
      (Name: 'orangered'; Value: $000045FF),
      (Name: 'orchid'; Value: $00D670DA),
      (Name: 'palegoldenrod'; Value: $00AAE8EE),
      (Name: 'palegreen'; Value: $0098FB98),
      (Name: 'paleturquoise'; Value: $00EEEEAF),
      (Name: 'palevioletred'; Value: $009370DB),
      (Name: 'papayawhip'; Value: $00D5EFFF),
      (Name: 'peachpuff'; Value: $00B9DAFF),
      (Name: 'peru'; Value: $003F85CD),
      (Name: 'pink'; Value: $00CBC0FF),
      (Name: 'plum'; Value: $00DDA0DD),
      (Name: 'powderblue'; Value: $00E6E0B0),
      (Name: 'purple'; Value: $00800080),
      (Name: 'red'; Value: $000000FF),
      (Name: 'rosybrown'; Value: $008F8FBC),
      (Name: 'royalblue'; Value: $00E16941),
      (Name: 'saddlebrown'; Value: $0013458B),
      (Name: 'salmon'; Value: $007280FA),
      (Name: 'sandybrown'; Value: $0060A4F4),
      (Name: 'seagreen'; Value: $00578B2E),
      (Name: 'seashell'; Value: $00EEF5FF),
      (Name: 'sienna'; Value: $002D52A0),
      (Name: 'silver'; Value: $00C0C0C0),
      (Name: 'skyblue'; Value: $00EBCE87),
      (Name: 'slateblue'; Value: $00CD5A6A),
      (Name: 'slategray'; Value: $00908070),
      (Name: 'slategrey'; Value: $00908070),
      (Name: 'snow'; Value: $00FAFAFF),
      (Name: 'springgreen'; Value: $007FFF00),
      (Name: 'steelblue'; Value: $00B48246),
      (Name: 'tan'; Value: $008CB4D2),
      (Name: 'teal'; Value: $00808000),
      (Name: 'thistle'; Value: $00D8BFD8),
      (Name: 'tomato'; Value: $004763FF),
      (Name: 'turquoise'; Value: $00D0E040),
      (Name: 'violet'; Value: $00EE82EE),
      (Name: 'wheat'; Value: $00B3DEF5),
      (Name: 'white'; Value: $00FFFFFF),
      (Name: 'whitesmoke'; Value: $00F5F5F5),
      (Name: 'yellow'; Value: $0000FFFF),
      (Name: 'yellowgreen'; Value: $0032CD9A)
    );

var
  NamedColorsDict: TDictionary<string, TColor>;

function TryGetColorName(const AColor: TColor; out AName: string): Boolean;

function TryStrToColor(const AStr: string; out AColor: TColor): Boolean;
function StrToColor(const AStr: string): TColor;
function ColorToHex(AColor: TColor): string;
function InvertColor(AColor: TColor): TColor; overload; inline;
function InvertColor(const AColor: TRGB): TRGB; overload; inline;
function InvertValue(const AColor: THSV): THSV; overload; inline;
function InvertValue(const AColor: TRGB): TRGB; overload; inline;
function InvertLightness(const AColor: THSL): THSL; overload; inline;
function InvertLightness(const AColor: TRGB): TRGB; overload; inline;
function Whiten(const AColor: TRGB): TRGB; inline;
function Darken(const AColor: TRGB): TRGB; inline;
function FadeToColor(const ABaseColor, ATargetColor: TRGB;
  const AFraction: Double = 0.5): TRGB;
function ColorIsDark(AColor: TColor): Boolean;
function RBSwap(AColor: Integer): Integer; inline;

implementation

{ Utilities }

function rmod(const x, y: Double): Double; inline;
begin
  Result := x - Floor(x / y) * y;
end;

function Fix360(const x: Double): Double; inline;
begin
  Result := rmod(x, 360);
end;

{ TRGB }

function TRGB.Average: Double;
begin
  Result := (Red + Green + Blue) / 3;
end;

constructor TRGB.Create(const ARed, AGreen, ABlue: Double);
begin
  Red := ARed;
  Green := AGreen;
  Blue := ABlue;
end;

class operator TRGB.Implicit(const APascalColor: TColor): TRGB;
begin
  Result.Red := (APascalColor and $000000FF) / 255;
  Result.Green := ((APascalColor and $0000FF00) shr 8) / 255;
  Result.Blue := ((APascalColor and $00FF0000) shr 16) / 255
end;

class operator TRGB.Equal(const AColor1, AColor2: TRGB): Boolean;
begin
  Result := TColorComponents(AColor1) = TColorComponents(AColor2);
end;

class operator TRGB.NotEqual(const AColor1, AColor2: TRGB): Boolean;
begin
  Result := not (AColor1 = AColor2)
end;

class operator TRGB.Add(const AColor1, AColor2: TRGB): TRGB;
begin
  Result.Red := AColor1.Red + AColor2.Red;
  Result.Green := AColor1.Green + AColor2.Green;
  Result.Blue := AColor1.Blue + AColor2.Blue;
end;

class operator TRGB.Subtract(const AColor1, AColor2: TRGB): TRGB;
begin
  Result.Red := AColor1.Red - AColor2.Red;
  Result.Green := AColor1.Green - AColor2.Green;
  Result.Blue := AColor1.Blue - AColor2.Blue;
end;

class operator TRGB.Multiply(const AFactor: Double; const AColor: TRGB): TRGB;
begin
  Result.Red := AFactor * AColor.Red;
  Result.Green := AFactor * AColor.Green;
  Result.Blue := AFactor * AColor.Blue;
end;

class operator TRGB.Divide(const AColor: TRGB; const ADenominator: Double): TRGB;
begin
  Result := (1 / ADenominator) * AColor;
end;

class operator TRGB.Implicit(const AColor: TRGB): TColor;
begin
  with AColor do
    Result := Round(255*Red) or (Round(255*Green) shl 8) or (Round(255*Blue) shl 16);
end;

function TRGB.Invert: TRGB;
begin
  Result.Red := 1 - Red;
  Result.Green := 1 - Green;
  Result.Blue := 1 - Blue;
end;

function TRGB.IsValid: Boolean;
begin
  Result := InRange(Red, 0, 1) and InRange(Green, 0, 1) and InRange(Blue, 0, 1);
end;

{ THSL }

constructor THSL.Create(const AHue, ASaturation, ALightness: Double);
begin
  Hue := Fix360(AHue);
  Saturation := ASaturation;
  Lightness := ALightness;
end;

class operator THSL.Implicit(const APascalColor: TColor): THSL;
begin
  Result := THSL(TRGB(APascalColor));
end;

class operator THSL.Implicit(const AColor: THSL): TColor;
begin
  Result := TColor(TRGB(AColor));
end;

class operator THSL.Explicit(const AColor: THSL): TRGB;
var
  q, p, hk, tr, tg, tb: Double;
begin

  with AColor, Result do
  begin

    if Lightness < 0.5 then
      q := Lightness * (1 + Saturation)
    else
      q := Lightness + Saturation - (Lightness * Saturation);

    p := 2 * Lightness - q;

    hk := Hue / 360;

    tr := hk + 1/3;
    tg := hk;
    tb := hk - 1/3;

    if tr < 0 then tr := tr + 1;
    if tg < 0 then tg := tg + 1;
    if tb < 0 then tb := tb + 1;

    if tr > 1 then tr := tr - 1;
    if tg > 1 then tg := tg - 1;
    if tb > 1 then tb := tb - 1;

    if tr < 1/6 then
      Red := p + ((q - p) * 6 * tr)
    else if tr < 0.5 then
      Red := q
    else if tr < 2/3 then
      Red := p + ((q - p) * 6 * (2/3 - tr))
    else
      Red := p;

    if tg < 1/6 then
      Green := p + ((q - p) * 6 * tg)
    else if tg < 0.5 then
      Green := q
    else if tg < 2/3 then
      Green := p + ((q - p) * 6 * (2/3 - tg))
    else
      Green := p;

    if tb < 1/6 then
      Blue := p + ((q - p) * 6 * tb)
    else if tb < 0.5 then
      Blue := q
    else if tb < 2/3 then
      Blue := p + ((q - p) * 6 * (2/3 - tb))
    else
      Blue := p;

  end;
end;

class operator THSL.Equal(const AColor1, AColor2: THSL): Boolean;
begin
  Result := TColorComponents(AColor1) = TColorComponents(AColor2);
end;

class operator THSL.NotEqual(const AColor1, AColor2: THSL): Boolean;
begin
  Result := not (AColor1 = AColor2);
end;

class operator THSL.Explicit(const AColor: TRGB): THSL;
var
  cmax, cmin, cdiff, csum: Double;
begin
  cmax := TColorComponents(AColor).Max;
  cmin := TColorComponents(AColor).Min;
  cdiff := cmax - cmin;
  csum := cmax + cmin;

  with AColor, Result do
  begin

    // Hue
    if cmax = cmin then
      Hue := 0
    else if cmax = Red then
      Hue := (60 * (Green - Blue) / cdiff)
    else if cmax = Green then
      Hue := (60 * (Blue - Red) / cdiff) + 120
    else
      Hue := (60 * (Red - Green) / cdiff) + 240;

    Hue := Fix360(Hue);

    // Saturation
    if cmax = cmin then
      Saturation := 0
    else if csum <= 1 then
      Saturation := cdiff / csum
    else
      Saturation := cdiff / (2 - csum);

    // Lightness
    Lightness := csum / 2;

  end;

end;

function THSL.IsValid: Boolean;
begin
  Result := InRange(Hue, 0, 360) and InRange(Saturation, 0, 1) and InRange(Lightness, 0, 1);
end;

{ THSV }

constructor THSV.Create(const AHue, ASaturation, AValue: Double);
begin
  Hue := Fix360(AHue);
  Saturation := ASaturation;
  Value := AValue;
end;

class operator THSV.Implicit(const APascalColor: TColor): THSV;
begin
  Result := THSV(TRGB(APascalColor));
end;

class operator THSV.Implicit(const AColor: THSV): TColor;
begin
  Result := TColor(TRGB(AColor));
end;

class operator THSV.Explicit(const AColor: THSV): TRGB;
var
  hi: Integer;
  f, q, p, t: Double;
begin

  with AColor do
  begin

    hi := Floor(Hue / 60) mod 6;
    f := Hue / 60 - floor(Hue / 60);
    p := Value * (1 - Saturation);
    q := Value * (1 - f * Saturation);
    t := Value * (1 - (1 - f) * Saturation);

    case hi of
      0: Result := TRGB.Create(Value, t, p);
      1: Result := TRGB.Create(q, Value, p);
      2: Result := TRGB.Create(p, Value, t);
      3: Result := TRGB.Create(p, q, Value);
      4: Result := TRGB.Create(t, p, Value);
      5: Result := TRGB.Create(Value, p, q);
    end;

  end;

end;

class operator THSV.Explicit(const AColor: TRGB): THSV;
var
  cmax, cmin, cdiff: Double;
begin
  cmax := TColorComponents(AColor).Max;
  cmin := TColorComponents(AColor).Min;
  cdiff := cmax - cmin;

  with AColor, Result do
  begin

    // Hue
    if cmax = cmin then
      Hue := 0
    else if cmax = Red then
      Hue := (60 * (Green - Blue) / cdiff)
    else if cmax = Green then
      Hue := (60 * (Blue - Red) / cdiff) + 120
    else
      Hue := (60 * (Red - Green) / cdiff) + 240;

    Hue := Fix360(Hue);

    // Saturation
    if cmax = 0 then
      Saturation := 0
    else
      Saturation := 1 - cmin / cmax;

    // Value
    Value := cmax;

  end;

end;

class operator THSV.Equal(const AColor1, AColor2: THSV): Boolean;
begin
  Result := TColorComponents(AColor1) = TColorComponents(AColor2);
end;

class operator THSV.NotEqual(const AColor1, AColor2: THSV): Boolean;
begin
  Result := not (AColor1 = AColor2);
end;

function THSV.IsValid: Boolean;
begin
  Result := InRange(Hue, 0, 360) and InRange(Saturation, 0, 1) and InRange(Value, 0, 1);
end;

{ TColorComponents }

class operator TColorComponents.Equal(const AColor1,
  AColor2: TColorComponents): Boolean;
begin
  Result := SameValue(AColor1.Components[0], AColor2.Components[0]) and
    SameValue(AColor1.Components[1], AColor2.Components[1]) and
    SameValue(AColor1.Components[2], AColor2.Components[2]);
end;

class operator TColorComponents.NotEqual(const AColor1,
  AColor2: TColorComponents): Boolean;
begin
  Result := not (AColor1 = AColor2);
end;

function TColorComponents.Max: Double;
begin
  Result := MaxValue(Components);
end;

function TColorComponents.Min: Double;
begin
  Result := MinValue(Components);
end;

{ Functions }

function SameColor(AColor1, AColor2: TColor): Boolean;
begin
  Result := (AColor1 and $00FFFFFF) = (AColor2 and $00FFFFFF);
end;

function RandomColor: TColor;
begin
  Result := Random(256) or (Random(256) shl 8) or (Random(256) shl 16);
end;

function TryGetColorName(const AColor: TColor; out AName: string): Boolean;
var
  i: Integer;
begin
  for i := 0 to High(NamedColors) do
    if NamedColors[i].Value = AColor then
    begin
      AName := NamedColors[i].Name;
      Exit(True);
    end;
  AName := '';
  Exit(False);
end;

function TryStrToColor(const AStr: string; out AColor: TColor): Boolean;
var
  Str: string;

  function OnlyHexDigitsAfterPrefix: Boolean;
  var
    i: Integer;
  begin
    for i := 2 to Str.Length do
      if not CharInSet(Str[i], ['0'..'9', 'A'..'F', 'a'..'f']) then
        Exit(False);
    Result := True;
  end;

var
  IntVal: integer;

begin

  Str := AStr.Trim;

  // Hex RGB string with # prefix?
  if (Str.Length = 7) and (Str[1] = '#') and OnlyHexDigitsAfterPrefix then
  begin
    AColor := StrToInt('$' + Copy(Str, 2, 2)) or
      (StrToInt('$' + Copy(Str, 4, 2)) shl 8) or
      (StrToInt('$' + Copy(Str, 6, 2)) shl 16);
    Exit(True);
  end;

  // Colour code (32-bit integer)?
  if TryStrToInt(Str, IntVal) then
  begin
    AColor := TColor(IntVal);
    Exit(True);
  end;

  // Named colour?
  if NamedColorsDict.TryGetValue(Str, AColor) then
    Exit(True);

  // Fail
  Result := False;

end;

function StrToColor(const AStr: string): TColor;
begin
  if not TryStrToColor(AStr, Result) then
    raise EColorException.CreateFmt('Unsupported colour descriptor "%s".', [AStr]);
end;

function ColorToHex(AColor: TColor): string;
begin
  AColor := ColorToRGB(AColor);
  Result := Format('#%.2x%.2x%.2x',
    [GetRValue(AColor), GetGValue(AColor), GetBValue(AColor)]);
end;

function InvertColor(AColor: TColor): TColor;
begin
  Result := ColorToRGB(AColor) xor $FFFFFF;
end;

function InvertColor(const AColor: TRGB): TRGB;
begin
  with AColor do
    Result := TRGB.Create(1 - Red, 1 - Green, 1 - Blue);
end;

function InvertValue(const AColor: THSV): THSV;
begin
  with AColor do
    Result := THSV.Create(Hue, Saturation, 1 - Value);
end;

function InvertValue(const AColor: TRGB): TRGB;
begin
  Result := TRGB(InvertValue(THSV(AColor)));
end;

function InvertLightness(const AColor: THSL): THSL;
begin
  with AColor do
    Result := THSL.Create(Hue, Saturation, 1 - Lightness);
end;

function InvertLightness(const AColor: TRGB): TRGB;
begin
  Result := TRGB(InvertLightness(THSL(AColor)));
end;

function Whiten(const AColor: TRGB): TRGB;
begin
  with AColor do
    Result := TRGB.Create(
      Red + (1 - Red) / 2,
      Green + (1 - Green) / 2,
      Blue + (1 - Blue) / 2
    );
end;

function Darken(const AColor: TRGB): TRGB;
begin
  with AColor do
    Result := TRGB.Create(Red / 2, Green / 2, Blue / 2);
end;

function FadeToColor(const ABaseColor, ATargetColor: TRGB;
  const AFraction: Double): TRGB;
begin
  Result := TRGB.Create(
    (1 - AFraction) * ABaseColor.Red + AFraction * ATargetColor.Red,
    (1 - AFraction) * ABaseColor.Green + AFraction * ATargetColor.Green,
    (1 - AFraction) * ABaseColor.Blue + AFraction * ATargetColor.Blue
  );
end;

function ColorIsDark(AColor: TColor): Boolean;
begin
  AColor := ColorToRGB(AColor);
  Result := 0.299 * GetRValue(AColor) + 0.587 * GetGValue(AColor) + 0.114 * GetBValue(AColor) < 149;
end;

function RBSwap(AColor: Integer): Integer;
begin
  Result := AColor and $FF00FF00 or Byte(AColor shr 16) or Byte(AColor) shl 16;
end;

function CompareColor(const A, B: THSV): TValueRelationship; overload;
begin
  Result := CompareValue(A.Hue, B.Hue);
  if Result = 0 then
    Result := CompareValue(A.Saturation, B.Saturation);
  if Result = 0 then
    Result := CompareValue(A.Value, B.Value);
end;

function CompareColor(const A, B: TRGB): TValueRelationship; overload;
begin
  Result := CompareColor(THSV(A), THSV(B));
end;

var
  i: Integer;

initialization
  NamedColorsDict := TDictionary<string, TColor>.Create;
  for i := 0 to High(NamedColors) do
    NamedColorsDict.Add(NamedColors[i].Name, NamedColors[i].Value);

finalization
  NamedColorsDict.Free;

end.
