unit HSCirclePicker;

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

interface

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

type
  THSCirclePicker = class(TmbHSLVColorPickerControl)
  private
    FSatCircColor, FHueLineColor: TColor;
    FShowSatCirc: boolean;
    FShowHueLine: boolean;
    FShowSelCirc: boolean;
    procedure SetSatCircColor(c: TColor);
    procedure SetHueLineColor(c: TColor);
    procedure DrawSatCirc;
    procedure DrawHueLine;
    procedure DrawMarker(x, y: integer);
    procedure SetShowSatCirc(s: boolean);
    procedure SetShowSelCirc(s: boolean);
    procedure SetShowHueLine(s: boolean);
    procedure UpdateCoords;
  protected
    procedure CreateGradient; override;
//    procedure CreateWnd; override;
    function GetGradientColor2D(X, Y: Integer): TColor; override;
    procedure Paint; override;
    procedure Resize; override;
    procedure SelectColor(x, y: integer); override;
    procedure SetRelHue(H: Double); override;
    procedure SetRelSat(S: Double); override;
    procedure SetSelectedColor(c: TColor); override;
  public
    constructor Create(AOwner: TComponent); override;
    function GetColorAtPoint(x, y: integer): TColor; override;
  published
    property BrightnessMode default bmValue;
    property SelectedColor default clRed;
    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 SaturationCircleColor: TColor read FSatCircColor write SetSatCircColor default clSilver;
    property HueLineColor: TColor read FHueLineColor write SetHueLineColor default clGray;
    property ShowSaturationCircle: boolean read FShowSatCirc write SetShowSatCirc default true;
    property ShowHueLine: boolean read FShowHueLine write SetShowHueLine default true;
    property ShowSelectionCircle: boolean read FShowSelCirc write SetShowSelCirc default true;
    property MarkerStyle default msCrossCirc;
    property OnChange;
  end;

implementation

uses
  mbUtils;

{ THSCirclePicker }

constructor THSCirclePicker.Create(AOwner: TComponent);
begin
  inherited;
  SetInitialBounds(0, 0, 204, 204);
  FHue := 0;
  FSat := 1.0;
  FLum := 0.5;
  FVal := 1.0;
  SetSelectedColor(clRed);
  BrightnessMode := bmValue;
  FSatCircColor := clSilver;
  FHueLineColor := clGray;
  FShowSatCirc := true;
  FShowHueLine := true;
  FShowSelCirc := true;
  MarkerStyle := msCrossCirc;
end;

procedure THSCirclePicker.CreateGradient;
begin
  FGradientWidth := Min(Width, Height);
  FGradientHeight := FGradientWidth;
  inherited;
end;
                  (*
procedure THSCirclePicker.CreateWnd;
begin
  inherited;
  CreateGradient;
  UpdateCoords;
end;                *)

procedure THSCirclePicker.DrawSatCirc;
var
  delta: integer;
  radius: integer;
begin
  if not FShowSatCirc then
    exit;
  if (FSat > 0) and (FSat < 1.0) then
  begin
    radius := Min(Width, Height) div 2;
    Canvas.Pen.Color := FSatCircColor;
    Canvas.Brush.Style := bsClear;
    delta := round(radius * FSat);
    Canvas.Ellipse(radius - delta, radius - delta, radius + delta, radius + delta);
  end;
end;

procedure THSCirclePicker.DrawHueLine;
var
  angle: double;
  sinAngle, cosAngle: Double;
  radius: integer;
begin
  if not FShowHueLine then
    exit;
  radius := Min(Width, Height) div 2;
  if (FHue >= 0) and (FHue <= 1.0) then
  begin
    angle := -FHue * 2 * 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;

procedure THSCirclePicker.DrawMarker(x, y: integer);
var
  c: TColor;
begin
  if not FShowSelCirc then
    exit;
  if Focused or (csDesigning in ComponentState) then
    c := clBlack
  else
    c := clGray;
  InternalDrawMarker(x, y, c);
end;

function THSCirclePicker.GetColorAtPoint(x, y: integer): TColor;
var
  angle: Double;
  dx, dy, r, radius: integer;
  h, s: double;
begin
  radius := Min(Width, Height) div 2;
  dx := x - Radius;
  dy := y - Radius;

  r := round(sqrt(sqr(dx) + sqr(dy)));
  if r <= radius then
  begin
    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;
    s := r / radius;
    Result := HSLVtoColor(h, s, FLum, FVal);
    if WebSafe then
      Result := GetWebSafe(Result);
  end else
    Result := clNone;
end;

{ Outer loop: Y, Inner loop: X }
function THSCirclePicker.GetGradientColor2D(X, Y: Integer): TColor;
var
  dx, dy: Integer;
  dSq, radiusSq: Integer;
  radius, size: Integer;
  S, H: Double;
begin
  size := FGradientWidth;  // or Height, they are the same...
  radius := size div 2;
  radiusSq := sqr(radius);
  dx := X - radius;
  dy := Y - radius;
  dSq := sqr(dx) + sqr(dy);
  if dSq <= radiusSq then
  begin
    if radius <> 0 then
      S := sqrt(dSq) / radius
    else
      S := 0;
    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, S, FLum, FVal);
    if WebSafe then
      Result := GetWebSafe(Result);
  end else
    Result := GetDefaultColor(dctBrush);
end;

procedure THSCirclePicker.Paint;
var
  rgn: HRGN;
  R: TRect;
begin
  PaintParentBack(Canvas);
  R := ClientRect;
  R.Right := R.Left + Min(Width, Height);
  R.Bottom := R.Top + Min(Width, Height);
  InflateRect(R, -1, -1);  // Avoid spurious black pixels at the border
  rgn := CreateEllipticRgnIndirect(R);
  SelectClipRgn(Canvas.Handle, rgn);
  Canvas.Draw(0, 0, FBufferBmp);
  DeleteObject(rgn);
  DrawSatCirc;
  DrawHueLine;
  DrawMarker(mx, my);
end;

procedure THSCirclePicker.Resize;
begin
  inherited;
  CreateGradient;
  UpdateCoords;
end;

procedure THSCirclePicker.SelectColor(x, y: integer);
var
  angle: Double;
  dx, dy, r, radius: integer;
  H, S: Double;
begin
  mx := x;
  my := y;

  radius := Min(Width, Height) div 2;
  dx := x - radius;
  dy := y - radius;
  r := round(sqrt(sqr(dx) + sqr(dy)));

  if r > radius then  // point outside circle
  begin
    SetSelectedColor(clNone);
    exit;
  end;

  //FSelectedColor := clWhite;         // ????
  angle := 360 + 180*arctan2(-dy, dx) / pi;   // wp: "-y, x" correct? The others have "x, y"
  if angle < 0 then
    angle := angle + 360
  else if angle > 360 then
    angle := angle - 360;
  H := angle / 360;
  if r > radius then
    S := 1.0
  else
    S := r / radius;

  if (H = FHue) and (S = FSat) then
    exit;

  FHue := H;
  FSat := S;
  FSelected := HSLVToColor(FHue, FSat, FLum, FVal);
  UpdateCoords;
  Invalidate;
  DoChange;
end;

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

procedure THSCirclePicker.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;
    FSelected := HSLVtoColor(FHue, FSat, FLum, FVal);
    UpdateCoords;
    Invalidate;
    DoChange;
  end;
end;

procedure THSCirclePicker.SetRelSat(S: Double);
begin
  Clamp(S, 0.0, 1.0);
  if FSat <> S then
  begin
    FSat := s;
    FSelected := HSLVToColor(FHue, FSat, FLum, FVal);
    UpdateCoords;
    Invalidate;
    DoChange;
  end;
end;

procedure THSCirclePicker.SetSatCircColor(c: TColor);
begin
  if FSatCircColor <> c then
  begin
    FSatCircColor := c;
    Invalidate;
  end;
end;

procedure THSCirclePicker.SetSelectedColor(c: TColor);
begin
  if WebSafe then
    c := GetWebSafe(c);
  if c = FSelected then
    exit;
  ColorToHSLV(c, FHue, FSat, FLum, FVal);
  FSelected := c;
  UpdateCoords;
  Invalidate;
  DoChange;
end;

procedure THSCirclePicker.SetShowHueLine(s: boolean);
begin
  if FShowHueLine <> s then
  begin
    FShowHueLine := s;
    Invalidate;
  end;
end;

procedure THSCirclePicker.SetShowSatCirc(s: boolean);
begin
  if FShowSatCirc <> s then
  begin
    FShowSatCirc := s;
    Invalidate;
  end;
end;

procedure THSCirclePicker.SetShowSelCirc(s: boolean);
begin
  if FShowSelCirc <> s then
  begin
    FShowSelCirc := s;
    Invalidate;
  end;
end;

procedure THSCirclePicker.UpdateCoords;
var
  r, angle: double;
  sinAngle, cosAngle: Double;
  radius: integer;
begin
  radius := Min(Width, Height) div 2;
  r := -FSat * radius;
  angle := -(FHue * 2 + 1) * pi;
  SinCos(angle, sinAngle, cosAngle);
  mx := round(cosAngle * r) + radius;
  my := round(sinAngle * r) + radius;
end;

end.