unit HSColorMap;

interface

uses
  Windows, Messages, SysUtils, Types, Classes, Controls, Menus, Graphics,
  RColors, UITypes;

type
  THSColorMap = class(TCustomControl)
  private
    FHue, FSat, FVal: double;
    FMarkerColor: TColor;
    FOnChange: TNotifyEvent;
    FBackground: TBitmap;
    procedure SetSat(const Value: double);
    procedure SetVal(const Value: double);
    procedure SetHue(const Value: double);
    procedure UpdateMarkerColor;
    procedure DoChange;
    function GetColor: THSV;
    procedure SetColor(const Value: THSV);
    procedure DrawBackground;
  protected
    procedure Paint; override;
    procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X: Integer;
      Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X: Integer; Y: Integer); override;
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    property Color: THSV read GetColor write SetColor;
  published
    property Align;
    property Anchors;
    property Cursor default crCross;
    property Enabled;
    property PopupMenu;
    property Saturation: double read FSat write SetSat;
    property Value: double read FVal write SetVal;
    property Hue: double read FHue write SetHue;
    property OnChange: TNotifyEvent read FOnChange write FOnChange;
  end;

procedure Register;

implementation

uses Math;

procedure Register;
begin
  RegisterComponents('Rejbrand 2015', [THSColorMap]);
end;

{ THSColorMap }

constructor THSColorMap.Create(AOwner: TComponent);
begin
  inherited;
  Cursor := crCross;
  FHue := 0;
  FVal := 1;
  FSat := 1;
  FBackground := TBitmap.Create;
end;

destructor THSColorMap.Destroy;
begin
  FBackground.Free;
  inherited;
end;

procedure THSColorMap.DoChange;
begin
  if Assigned(FOnChange) then
    FOnChange(Self);
end;

procedure THSColorMap.DrawBackground;
type
  PRGBTripleArray = ^TRGBTripleArray;
  TRGBTripleArray = array[word] of TRGBTriple;
var
  hstep, sstep: double;
  y: Integer;
  x: Integer;
  SL: PRGBTripleArray;
begin

  if (ClientWidth < 4) or (ClientHeight < 4) then
    Exit;

  FBackground.SetSize(ClientWidth, ClientHeight);
  FBackground.PixelFormat := pf24bit;

  hstep := 360 / (ClientWidth - 1);
  sstep := 1 / (ClientHeight - 1);

  for y := 0 to ClientHeight - 1 do
  begin
    SL := FBackground.ScanLine[y];
    for x := 0 to ClientWidth - 1 do
      SL[x] := RGBToRGBTriple(HSVToRGB(HSV(x * hstep, y * sstep, FVal)));
  end;

end;

function THSColorMap.GetColor: THSV;
begin
  result := HSV(FHue, FSat, FVal);
end;

procedure THSColorMap.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
var
  R: TRect;
begin

  inherited;

  if (ClientWidth < 4) or (ClientHeight < 4) then
    Exit;

  FHue := 360 * X / (ClientWidth - 1);
  FSat := Y / (ClientHeight - 1);
  Invalidate;
  Update;
  DoChange;

  with ClientToScreen(Point(0, 0)) do
    R := Rect(X, Y, X + ClientWidth, Y + ClientHeight);

  ClipCursor(@R);

end;

procedure THSColorMap.MouseMove(Shift: TShiftState; X, Y: Integer);
begin

  inherited;

  if not (csLButtonDown in ControlState) then
    Exit;

  if (ClientWidth < 4) or (ClientHeight < 4) then
    Exit;

  Hue := EnsureRange(360 * X / (ClientWidth - 1), 0, 360);
  FSat := EnsureRange(Y / (ClientHeight - 1), 0, 1);
  Invalidate;
  Update;
  DoChange;

end;

procedure THSColorMap.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
begin
  inherited;
  ClipCursor(nil);
end;

procedure THSColorMap.Paint;
var
  bm: TBitmap;
  x, y: integer;
begin

  inherited;

  if (ClientWidth < 4) or (ClientHeight < 4) then
    Exit;

  if (FBackground.Width <> ClientWidth) or (FBackground.Height <> ClientHeight) then
    DrawBackground;

  UpdateMarkerColor;

  bm := TBitmap.Create;
  try
    bm.SetSize(ClientWidth, ClientHeight);
    BitBlt(bm.Canvas.Handle, 0, 0, ClientWidth, ClientHeight,
      FBackground.Canvas.Handle, 0, 0, SRCCOPY);
    x := round((ClientWidth-1) * FHue / 360);
    y := round((ClientHeight-1) * FSat);
    bm.Canvas.Pen.Color := FMarkerColor;
    bm.Canvas.MoveTo(0, y);
    bm.Canvas.LineTo(ClientWidth, y);
    bm.Canvas.MoveTo(x, 0);
    bm.Canvas.LineTo(x, ClientHeight);
    BitBlt(Canvas.Handle, 0, 0, ClientWidth, ClientHeight,
      bm.Canvas.Handle, 0, 0, SRCCOPY);
  finally
    bm.Free;
  end;

end;

procedure THSColorMap.SetColor(const Value: THSV);
begin
  if not SameHSV(Color, Value) then
    with Value do
    begin
      FHue := hsvHue;
      FSat := hsvSaturation;
      FVal := hsvValue;
      DrawBackground;
      Invalidate;
    end;
end;

procedure THSColorMap.SetHue(const Value: double);
begin
  if FHue <> Value then
  begin
    FHue := Value;
    Invalidate;
  end;
end;

procedure THSColorMap.SetSat(const Value: double);
begin
  if FSat <> Value then
  begin
    FSat := Value;
    Invalidate;
  end;
end;

procedure THSColorMap.SetVal(const Value: double);
begin
  if FSat <> Value then
  begin
    FVal := Value;
    DrawBackground;
    Invalidate;
  end;
end;

procedure THSColorMap.UpdateMarkerColor;
begin
  if FVal > 0.5 then
    FMarkerColor := clBlack
  else
    FMarkerColor := clWhite;
end;

procedure THSColorMap.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
  Message.Result := 1;
end;

procedure THSColorMap.WMSize(var Message: TWMSize);
begin
  DrawBackground;
  Invalidate;
end;

end.
