unit CurvesControl;

interface

uses
  Windows, SysUtils, Classes, Math, ControlPoint,
  Graphics, Controls, Forms, Bezier, CustomDrawControl, Vcl.ExtCtrls;

const
  MAX_CHANNEL = 3;

type
  TCurvesChannel = (ccAll = 0, ccRed = 1, ccGreen = 2, ccBlue = 3);
  TCurvesControl = class(TFrame)
    Host: TPanel;
  private
    FRect: BezierRect;

    FPoints: array [0..3] of BezierPoints;
    FWeights: array [0..3] of BezierWeights;

    FDragging: boolean;
    FDragIndex: integer;

    FActiveChannel : TCurvesChannel;
    FChannelIndex : integer;

    FFrame : TCustomDrawControl;
    FCP: TControlPoint;

    p: array [0..MAX_CHANNEL] of BezierPoints;
    w: array [0..MAX_CHANNEL] of BezierWeights;
    wsum: array [0..MAX_CHANNEL] of double;

    procedure SetChannel(value: TCurvesChannel);
    procedure SetWeightLeft(value: double);
    procedure SetWeightRight(value: double);

    function GetChannel: TCurvesChannel;
    function GetWeightLeft: double;
    function GetWeightRight: double;

    procedure FrameMouseLeave(Sender: TObject);
    procedure FrameMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
    procedure FrameMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FrameMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
    procedure FrameResize(Sender: TObject);
    procedure FramePaint(Sender: TObject);
    procedure FrameCreate;

    procedure PaintCurve(Bitmap: TBitmap; c: integer; p: BezierPoints; w: BezierWeights; widgets: boolean);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    property WeightLeft : double read GetWeightLeft write SetWeightLeft;
    property WeightRight : double read GetWeightRight write SetWeightRight;
    property ActiveChannel : TCurvesChannel read GetChannel write SetChannel;

    procedure SetCp(cp: TControlPoint);
    procedure UpdateFlame;
  end;

implementation

{$R *.DFM}

uses Main, Editor, Mutate, Adjust;

const
  point_size: double = 8;

constructor TCurvesControl.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  FFrame := TCustomDrawControl.Create(self);
  FFrame.TabStop  := True;
  FFrame.TabOrder := 0;
  FFrame.Parent   := Host;
  FFrame.Align    := alClient;
  FFrame.Visible  := True;

  FFrame.OnPaint      := FramePaint;
  FFrame.OnMouseDown  := FrameMouseDown;
  FFrame.OnMouseMove  := FrameMouseMove;
  FFrame.OnMouseUp    := FrameMouseUp;
  FFrame.OnMouseLeave := FrameMouseLeave;

  FCP := TControlPoint.Create;

  FrameCreate;
end;

destructor  TCurvesControl.Destroy;
begin
  FCP.Destroy;
  inherited Destroy;
end;

procedure TCurvesControl.SetCp(cp: TControlPoint);
var i, j: smallint;
begin
  FCP.Copy(cp, true);
  for i := 0 to 3 do
    for j := 0 to 3 do begin
      FWeights[i,j] := FCP.curveWeights[i,j];
      FPoints[i,j].x := FCP.curvePoints[i,j].x;
      FPoints[i,j].y := FCP.curvePoints[i,j].y;
    end;
  Invalidate;
  FFrame.Invalidate;
end;

procedure TCurvesControl.UpdateFlame;
begin
  MainForm.StopThread;
  MainForm.UpdateUndo;
  MainCp.Copy(FCP, true);

  if EditForm.Visible then EditForm.UpdateDisplay;
  if MutateForm.Visible then MutateForm.UpdateDisplay;
  if AdjustForm.Visible then AdjustForm.UpdateDisplay(true);

  MainForm.RedrawTimer.enabled := true;
end;

procedure TCurvesControl.FrameMouseLeave(Sender: TObject);
begin
  FrameMouseUp(nil, mbLeft, [], 0, 0);
end;

procedure TCurvesControl.FrameMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
var
  ps_half: double;
  i, n: integer;
  p: BezierPoints;
begin
  BezierCopy(FPoints[FChannelIndex], p);
  BezierSetRect(p, true, FRect);

  FDragIndex := -1;
  FDragging := false;

  n := Length(p);
  for i := 1 to n - 2 do if
    (X >= p[i].x - point_size) and (X <= p[i].x + point_size) and
    (Y >= p[i].y - point_size) and (Y <= p[i].y + point_size) then
  begin
    FDragging := true;
    FDragIndex := i;
    Break;
  end;
end;

procedure TCurvesControl.FrameMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer);
var
  m: BezierPoints;
  tmp: BezierPoint;
  i, j: shortint;
begin

  if (y < 0) then Exit;
  if (x < 0) then Exit;

  m[0].x := x; m[0].y := y;
  BezierUnsetRect(m, true, FRect);

  if FDragging then
  begin
    FPoints[FChannelIndex][FDragIndex] := m[0];
    if (FPoints[FChannelIndex][FDragIndex].x <= 0)
    then FPoints[FChannelIndex][FDragIndex].x := 0;
    if (FPoints[FChannelIndex][FDragIndex].y <= 0)
    then FPoints[FChannelIndex][FDragIndex].y := 0;
    if (FPoints[FChannelIndex][FDragIndex].x >= 1)
    then FPoints[FChannelIndex][FDragIndex].x := 1;
    if (FPoints[FChannelIndex][FDragIndex].y >= 1)
    then FPoints[FChannelIndex][FDragIndex].y := 1;

    if (FPoints[FChannelIndex][1].x > FPoints[FChannelIndex][2].x) then
    begin
      tmp := FPoints[FChannelIndex][1];
      FPoints[FChannelIndex][1] := FPoints[FChannelIndex][2];
      FPoints[FChannelIndex][2] := tmp;
      if (FDragIndex = 1) then FDragIndex := 2
      else FDragIndex := 1;
    end;

    for i := 0 to 3 do
    for j := 0 to 3 do begin
      FCP.curveWeights[i,j] := FWeights[i,j];
      FCP.curvePoints[i,j].x := FPoints[i,j].x;
      FCP.curvePoints[i,j].y := FPoints[i,j].y;
    end;

    FFrame.Refresh;
  end;
end;

procedure TCurvesControl.FrameMouseUp(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  FDragIndex := -1;
  FDragging := false;

  if (sender <> nil) then UpdateFlame;
end;

procedure TCurvesControl.FrameCreate;
var i: shortint;
begin
  for i := 0 to MAX_CHANNEL do
  begin
    FPoints[i][0].x := 0.00; FPoints[i][0].y := 0.00; FWeights[i][0] := 1;
    FPoints[i][1].x := 0.00; FPoints[i][1].y := 0.00; FWeights[i][1] := 1;
    FPoints[i][2].x := 1.00; FPoints[i][2].y := 1.00; FWeights[i][2] := 1;
    FPoints[i][3].x := 1.00; FPoints[i][3].y := 1.00; FWeights[i][3] := 1;
  end;

  FDragIndex := -1;
  FDragging := false;
end;

procedure TCurvesControl.FrameResize(Sender: TObject);
begin
  FRect.x0 := 0; FRect.y0 := 0;
  FRect.x1 := self.Width - 1;
  FRect.y1 := self.Height - 1;
end;

procedure TCurvesControl.FramePaint(Sender: TObject);
var
  clientRect: TRect;
  i, j, x, y, sx, sy: integer;
  bitmap: TBitMap;
begin
  if (FFrame.Width <= 0) or (FFrame.Height <= 0) then Exit;
  FrameResize(Sender);

  Bitmap := TBitmap.Create;
  Bitmap.Width := FFrame.Width;
  Bitmap.Height := FFrame.Height;

  sx := Bitmap.Width;
  sy := Bitmap.Height;

  try
    with Bitmap.Canvas do
    begin
      Brush.Color := $000000;
      FillRect(Rect(0, 0, sx, sy));

      Pen.Color := $555555;
      Pen.Style := psSolid;
      Pen.Width := 1;

      for x := 1 to 7 do begin
        MoveTo(Round(0.125 * x * FRect.x1), Round(FRect.y0));
        LineTo(Round(0.125 * x * FRect.x1), Round(FRect.y1));
      end;
      for y := 1 to 3 do begin
        MoveTo(Round(FRect.x0), Round(0.25 * y * FRect.y1));
        LineTo(Round(FRect.x1), Round(0.25 * y * FRect.y1));
      end;

      for i := 0 to MAX_CHANNEL do begin
        for j := 0 to 3 do
          wsum[i] := wsum[i] + FWeights[i][j];
        for j := 0 to 3 do
          w[i][j] := FWeights[i][j] / wsum[i];

        BezierCopy(FPoints[i], p[i]);
        BezierSetRect(p[i], true, FRect);

        if i <> FChannelIndex then PaintCurve(Bitmap, i, p[i], w[i], false);
      end;
      PaintCurve(Bitmap, FChannelIndex, p[FChannelIndex], w[FChannelIndex], true);

      FFrame.Canvas.Draw(0, 0, Bitmap);
    end;
  finally
    Bitmap.Free;
  end;
end;

procedure TCurvesControl.PaintCurve(Bitmap: TBitmap; c: integer; p: BezierPoints; w: BezierWeights; widgets: boolean);
const
  step = 0.001;  // AV
var
  pos0, pos1: BezierPoint;
  t: Double;
  r, g, b: array [0 .. MAX_CHANNEL] of integer;
  rgbv: integer;
begin
  with Bitmap.Canvas do
  begin
    if c <> FChannelIndex then begin
      r[0] := $aa; r[1] := $aa; r[2] := $40; r[3] := $40;
      g[0] := $aa; g[1] := $40; g[2] := $aa; g[3] := $40;
      b[0] := $aa; b[1] := $40; b[2] := $40; b[3] := $aa;
    end else begin
      r[0] := $ff; r[1] := $ff; r[2] := $80; r[3] := $80;
      g[0] := $ff; g[1] := $80; g[2] := $ff; g[3] := $80;
      b[0] := $ff; b[1] := $80; b[2] := $80; b[3] := $ff;
    end;

    rgbv := RGB(r[c], g[c], b[c]);

    t := 0;

    BezierSolve(0, p, w, pos1);
    pos0.x := 0; pos0.y := pos1.y;

    if widgets then begin
      Pen.Color := $808080; Pen.Width := 1;
      MoveTo(Round(p[1].x), Round(p[1].y));
      LineTo(Round(p[2].x), Round(p[2].y));
      MoveTo(Round(FRect.x0), Round(FRect.y1));
      LineTo(Round(p[1].x), Round(p[1].y));
      MoveTo(Round(FRect.x1), Round(FRect.y0));
      LineTo(Round(p[2].x), Round(p[2].y));
    end;

    while t < 1 do begin
      BezierSolve(t, p, w, pos1);
      Pen.Color := rgbv;
      Pen.Width := 1;
      MoveTo(Round(pos0.x), Round(pos0.y));
      LineTo(Round(pos1.x), Round(pos1.y));
      t := t + step;
      pos0 := pos1;
    end;

    MoveTo(Round(pos0.x), Round(pos0.y));
    LineTo(Round(FRect.x1), Round(pos0.y));

    if widgets then begin
      t := point_size / 2.0; // AV
      Brush.Color := rgbv;
      Ellipse(
        Round(p[1].x - t),
        Round(p[1].y - t),
        Round(p[1].x + t),
        Round(p[1].y + t)
      );
      Ellipse(
        Round(p[2].x - t),
        Round(p[2].y - t),
        Round(p[2].x + t),
        Round(p[2].y + t)
      );
    end;
  end;
end;

procedure TCurvesControl.SetChannel(value: TCurvesChannel);
begin
  FActiveChannel := value;
  FChannelIndex := Integer(value);
  FFrame.Refresh;
end;

procedure TCurvesControl.SetWeightLeft(value: double);
begin
  FWeights[FChannelIndex][1] := value;
  FCP.curveWeights[FChannelIndex][1] := value;
  FFrame.Refresh;
end;

procedure TCurvesControl.SetWeightRight(value: double);
begin
  FWeights[FChannelIndex][2] := value;
  FCP.curveWeights[FChannelIndex][2] := value;
  FFrame.Refresh;
end;

function TCurvesControl.GetChannel: TCurvesChannel;
begin
  Result := FActiveChannel;
end;

function TCurvesControl.GetWeightLeft: double;
begin
  Result := FWeights[FChannelIndex][1];
end;

function TCurvesControl.GetWeightRight: double;
begin
  Result := FWeights[FChannelIndex][2];
end;

end.
