unit HRingPicker;

{$IFDEF FPC}
  {$MODE DELPHI}
{$ENDIF}

interface

uses
  LCLIntf, LCLType, SysUtils, Classes, Controls, Graphics, Math, Forms,
  HTMLColors, mbColorConv, mbColorPickerControl;

type
  THRingPicker = class(TmbHSLVColorPickerControl)
  private
    FSelectedColor: TColor;
    FHueLineColor: TColor;
    FRadius: integer;
    procedure SetRadius(r: integer);
    procedure SetHueLineColor(c: TColor);
  protected
    procedure CreateGradient; override;
    procedure DrawHueLine;
    function GetGradientColor2D(X, Y: Integer): TColor; override;
    function GetSelectedColor: TColor; override;
//    function MouseOnPicker(X, Y: Integer): Boolean;
    procedure Paint; override;
    procedure Resize; override;
    procedure SelectColor(x, y: integer); override;
    procedure SetRelHue(H: Double); override;
    procedure SetSelectedColor(c: TColor); override;
    procedure UpdateCoords;
  public
    constructor Create(AOwner: TComponent); override;
    function GetColorAtPoint(x, y: integer): TColor; override;
    property ColorUnderCursor;
  published
    property Hue default 0;
    property Luminance default 127;
    property Saturation default 255;
    property Value default 255;
    property MaxHue default 360;
    property MaxLuminance default 255;
    property MaxSaturation default 255;
    property MaxValue default 255;
    property HueLineColor: TColor read FHueLineColor write SetHueLineColor default clGray;
    property Radius: integer read FRadius write SetRadius default 40;
    property SelectedColor default clRed; //clNone;
    property OnChange;
  end;


implementation

uses
  mbUtils;

{ THRingPicker }

constructor THRingPicker.Create(AOwner: TComponent);
begin
  inherited;
  SetInitialBounds(0, 0, 204, 204);
  FHue := 0.0;
  FVal := 1.0;
  FLum := 0.5;
  FSat := 1.0;
  SetSelectedColor(clRed);
  FRadius := 40;
  FHueLineColor := clGray;
  HintFormat := 'Hue: %h (selected)';
  TabStop := true;
end;

procedure THRingPicker.CreateGradient;
begin
  FGradientWidth := Min(Width, Height);
  FGradientHeight := FGradientWidth;
  inherited;
end;

procedure THRingPicker.DrawHueLine;
var
  angle: double;
  sinAngle, cosAngle: Double;
  radius: integer;
begin
  radius := Min(Width, Height) div 2;
  if (FHue >= 0) and (FHue <= 1.0) then
  begin
    angle := -FHue * TWO_PI;
    SinCos(angle, sinAngle, cosAngle);
    Canvas.Pen.Color := FHueLineColor;
    Canvas.MoveTo(radius, radius);
    Canvas.LineTo(radius + round(radius*cosAngle), radius + round(radius*sinAngle));
  end;
end;

function THRingPicker.GetColorAtPoint(x, y: integer): TColor;
var
  angle: Double;
  dx, dy, radius: integer;
  h: Double;
begin
  radius := Min(Width, Height) div 2;

  if PointInCircle(Point(x, y), Min(Width, Height)) then
  begin
    dx := x - Radius;
    dy := y - Radius;
    angle := 360 + 180 * arctan2(-dy, dx) / pi;
    if angle < 0 then
      angle := angle + 360
    else if angle > 360 then
      angle := angle - 360;
    h := angle / 360;
    Result := HSLVtoColor(h, FSat, FLum, FVal);
  end
  else
    Result := clNone;
end;

{ Outer loop: Y, Inner loop: X }
function THRingPicker.GetGradientColor2D(X, Y: Integer): TColor;
var
  dx, dy: Integer;
  dSq, rSq: Integer;
  radius, size: Integer;
  H: Double;
begin
  size := FGradientWidth;  // or Height, they are the same...
  radius := size div 2;
  rSq := sqr(radius);
  dx := X - radius;
  dy := Y - radius;
  dSq := sqr(dx) + sqr(dy);
  if dSq <= rSq then
  begin
    H := 180 * (1 + arctan2(dx, dy) / pi);  // wp: order (x,y) is correct!
    H := H + 90;
    if H > 360 then H := H - 360;
    Result := HSLVtoColor(H/360, FSat, FLum, FVal);
    if WebSafe then
      Result := GetWebSafe(Result);
  end else
    Result := GetDefaultColor(dctBrush);
end;

function THRingPicker.GetSelectedColor: TColor;
begin
  if FSelectedColor <> clNone then
    Result := HSLVtoColor(FHue, FSat, FLum, FVal)
  else
    Result := clNone;
end;
                                 {
function THRingPicker.MouseOnPicker(X, Y: Integer): Boolean;
var
  diameter, r: Integer;
  P, ctr: TPoint;
begin
  diameter := Min(Width, Height);
  r := diameter div 2;      // outer radius
  P := Point(x, y);
  ctr := Point(r, r);
  Result := PtInCircle(P, ctr, r) and not PtInCircle(P, ctr, Radius);
end;                              }

procedure THRingPicker.Paint;
var
  rgn, r1, r2: HRGN;
  r: TRect;
  size: Integer;
  ringwidth: Integer;
begin
  PaintParentBack(Canvas);
  size := Min(Width, Height);         // diameter of circle
  ringwidth := size div 2 - FRadius;  // FRadius is inner radius
  r := ClientRect;
  r.Right := R.Left + size;
  R.Bottom := R.Top + size;
  InflateRect(R, -1, -1);      // Remove spurious black pixels at the border
  r1 := CreateEllipticRgnIndirect(R);
  if ringwidth > 0 then
  begin
    rgn := r1;
    InflateRect(R, -ringwidth, - ringwidth);
    r2 := CreateEllipticRgnIndirect(R);
    CombineRgn(rgn, r1, r2, RGN_DIFF);
  end;
  SelectClipRgn(Canvas.Handle, rgn);
  Canvas.Draw(0, 0, FBufferBmp);
  DeleteObject(rgn);
  DrawHueLine;
  DoChange;
end;

procedure THRingPicker.Resize;
begin
  inherited;
  if Min(Width, Height) <> FGradientWidth then
    CreateGradient;
  UpdateCoords;
end;

procedure THRingPicker.SelectColor(x, y: integer);
var
  angle, dx, dy, Radius: integer;
begin
  mx := y;
  my := y;
  FSelectedColor := clWhite;
  radius := Min(Width, Height) div 2;
  dx := x - radius;
  dy := y - radius;
  angle := round(360 + 180*arctan2(-dy, dx) / pi);
  SetRelHue(angle/360);
end;

procedure THRingPicker.SetHueLineColor(c: TColor);
begin
  if FHueLineColor <> c then
  begin
    FHueLineColor := c;
    Invalidate;
  end;
end;

procedure THRingPicker.SetRadius(r: integer);
begin
  if FRadius <> r then
  begin
    FRadius := r;
    Invalidate;
  end;
end;

procedure THRingPicker.SetRelHue(H: Double);
begin
  if H > 1 then H := H - 1;
  if H < 0 then H := H + 1;
  if FHue <> h then
  begin
    FHue := h;
    UpdateCoords;
    Invalidate;
    DoChange;
  end;
end;

(*
procedure THRingPicker.SetSat(s: integer);
begin
  Clamp(s, 0, FMaxSat);
  if Saturation <> s then
  begin
    FSat := s / FMaxSat;
    UpdateCoords;
    Invalidate;
    DoChange;
  end;
end;
*)
procedure THRingPicker.SetSelectedColor(c: TColor);
var
  H: Double = 0;
  S: Double = 0;
  L: Double = 0;
  V: Double = 0;
  needNewGradient: Boolean;
begin
  if WebSafe then
    c := GetWebSafe(c);
  if c = GetSelectedColor then
    Exit;

  ColorToHSLV(c, H, S, L, V);
  case BrightnessMode of
    bmLuminance:
      begin
        needNewGradient := (S <> FSat) or (L <> FLum);
        FLum := L;
      end;
    bmValue:
      begin
        needNewGradient := (S <> FSat) or (V <> FVal);
        FVal := V;
      end;
  end;
  FHue := h;
  FSat := s;
  UpdateCoords;
  if needNewGradient then
    CreateGradient;
  Invalidate;
  DoChange;
end;

(*
procedure THRingPicker.SetVal(v: integer);
begin
  Clamp(v, 0, FMaxVal);
  if Value <> V then
  begin
    FVal := V / FMaxVal;
    if BrightnessMode = bmValue then
    begin
      CreateGradient;
      Invalidate;
    end;
    DoChange;
  end;
end;
*)
procedure THRingPicker.UpdateCoords;
var
  r, angle: double;
  radius: integer;
  sinAngle, cosAngle: Double;
begin
  radius := Min(Width, Height) div 2;
  r := -radius * FSat;
  angle := -(FHue * 2 + 1) * pi;
  SinCos(angle, sinAngle, cosAngle);
  mx := round(cosAngle * r) + radius;
  my := round(sinAngle * r) + radius;
end;

end.