(*
** This is the TEnvelope control as used in the Slashstone SoundFX Builder.
** The control is licensed under the terms of the MIT License. Read LICENSE
** or license.txt for more information.
*)

unit EnvelopeControl;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Controls, LCLType, LMessages, Graphics, Dialogs;
  
type
  TEnvelopePoint = record
    x: Real;
    y: Real;
  end;
  
  TEnvelope = class(TCustomControl)
  private
    Moving: Boolean;
    MovingIndex: Integer;
    FPosition: Real;
    
    FOnPositionChanged: TNotifyEvent;
    FOnEnvelopeModified: TNotifyEvent;
    
    procedure SetPosition(APosition: Real);
  protected
    procedure Paint; override;
    procedure WMLButtonDown(var Msg: TLMLButtonDown); message LM_LBUTTONDOWN;
    procedure WMLButtonUp(var Msg: TLMLButtonUp); message LM_LBUTTONUP;
    procedure WMRButtonDown(var Msg: TLMRButtonDown); message LM_RBUTTONDOWN;
    procedure WMRButtonUp(var Msg: TLMRButtonDown); message LM_RBUTTONUP;
    procedure WMMouseMove(var Msg: TLMMouseMove); message LM_MOUSEMOVE;
  public
    Point: array of TEnvelopePoint;
    
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    
    procedure Reset;
    procedure ClearPoints;
    procedure InsertPoint(Index: Integer; x, y: Real);
    procedure AddPoint(x, y: Real);
    
    procedure GraphPointToEnvelopePoint(gx, gy: Integer; out x, y: Real);
    procedure EnvelopePointToGraphPoint(x, y: Real; out gx, gy: Integer);
    
    procedure Draw;
  published
    property Position: Real read FPosition write SetPosition;
    property OnPositionChanged: TNotifyEvent read FOnPositionChanged write FOnPositionChanged;
    property OnEnvelopeModified: TNotifyEvent read FOnEnvelopeModified write FOnEnvelopeModified;
  end;

implementation

{ TEnvelope }
constructor TEnvelope.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  Reset;
  FPosition:=2;
end;

destructor TEnvelope.Destroy;
begin
  ClearPoints;
  inherited Destroy;
end;

procedure TEnvelope.SetPosition(APosition: Real);
begin
  FPosition:=APosition;
  Draw;
end;

procedure TEnvelope.Paint;
begin
  Draw;
end;

procedure TEnvelope.WMLButtonDown(var Msg: TLMLButtonDown);
var
  i, gx, gy: Integer;
begin
  for i:=0 to Length(Point)-1 do begin
    EnvelopePointToGraphPoint(Point[i].x, Point[i].y, gx, gy);
    if Sqr(gx - Msg.XPos)+Sqr(gy - Msg.YPos) < 40 then begin
      Moving:=True;
      MovingIndex:=i;
      MouseCapture:=True;
      Exit;
    end;
  end;
end;

procedure TEnvelope.WMLButtonUp(var Msg: TLMLButtonUp);
begin
  Moving:=False;
  MouseCapture:=False;
end;

procedure TEnvelope.WMRButtonDown(var Msg: TLMRButtonDown);
var
  i, gx, gy, Index: Integer;
  x, y: Real;
begin
  Index:=-1;
  for i:=0 to Length(Point)-1 do begin
    EnvelopePointToGraphPoint(Point[i].x, Point[i].y, gx, gy);
    if Sqr(gx - Msg.XPos)+Sqr(gy - Msg.YPos) < 40 then begin
      Index:=i;
      Break;
    end;
  end;
  
  { Delete point }
  if Index <> -1 then begin
    if (Index = 0) or (Index = Length(Point)-1)  then Exit;
    for i:=Index to Length(Point)-2 do Point[i]:=Point[i + 1];
    SetLength(Point, Length(Point) - 1);
    Draw;
    if Assigned(FOnEnvelopeModified) then FOnEnvelopeModified(Self);
    Exit;
  end;
  
  { Add new point }
  GraphPointToEnvelopePoint(Msg.XPos, Msg.YPos, x, y);
  for i:=0 to Length(Point)-2 do begin
    if (Point[i].x < x) and (Point[i + 1].x > x) then begin
      InsertPoint(i + 1, x, y);
      Draw;
      Moving:=True;
      MovingIndex:=i + 1;
      MouseCapture:=True;
      if Assigned(FOnEnvelopeModified) then FOnEnvelopeModified(Self);
      Exit;
    end;
  end;
end;

procedure TEnvelope.WMRButtonUp(var Msg: TLMRButtonDown);
begin
  Moving:=False;
  MouseCapture:=False;
end;

procedure TEnvelope.WMMouseMove(var Msg: TLMMouseMove);
var
  x, y: Real;
begin
  GraphPointToEnvelopePoint(Msg.XPos, Msg.YPos, x, y);
  if y < 0 then
    y:=0
  else if y > 1 then
    y:=1;
  Position:=x;
  if Moving then begin
    if MovingIndex=0 then
      x:=0.0
    else if MovingIndex=Length(Point)-1 then
      x:=1.0
    else if x < Point[MovingIndex - 1].x + 0.001 then
      x:=Point[MovingIndex - 1].x + 0.001
    else if x > Point[MovingIndex + 1].x - 0.001 then
      x:=Point[MovingIndex + 1].x - 0.001;
    Point[MovingIndex].x:=x;
    Point[MovingIndex].y:=y;
    Draw;
    if Assigned(FOnEnvelopeModified) then FOnEnvelopeModified(Self);
  end;
  if Assigned(FOnPositionChanged) then FOnPositionChanged(Self);
end;

procedure TEnvelope.Reset;
begin
  ClearPoints;
  AddPoint(0.0, 0.5);
  AddPoint(1.0, 0.5);
  Draw;
  if Assigned(FOnEnvelopeModified) then FOnEnvelopeModified(Self);
end;

procedure TEnvelope.ClearPoints;
begin
  SetLength(Point, 0);
end;

procedure TEnvelope.InsertPoint(Index: Integer; x, y: Real);
var
  i: Integer;
begin
  SetLength(Point, Length(Point) + 1);
  for i:=Length(Point) - 1 downto Index + 1 do
    Point[i]:=Point[i - 1];
  Point[Index].x:=x;
  Point[Index].y:=y;
end;

procedure TEnvelope.AddPoint(x, y: Real);
var
  Index: Integer;
begin
  Index:=Length(Point);
  SetLength(Point, Index + 1);
  Point[Index].x:=x;
  Point[Index].y:=y;
end;

procedure TEnvelope.GraphPointToEnvelopePoint(gx, gy: Integer; out x, y: Real);
begin
  x:=gx / Int(Width - 1);
  y:=1.0 - (gy / Int(Height - 1));
end;

procedure TEnvelope.EnvelopePointToGraphPoint(x, y: Real; out gx, gy: Integer);
begin
  gx:=Trunc(x * (Width - 1));
  gy:=Height - Trunc(y * (Height - 1)) - 1;
end;

procedure TEnvelope.Draw;
var
  i, gx, gy: Integer;
  BackBuffer: TBitmap;
begin
  if (Width = 0) or (Height = 0) then Exit;
  
  BackBuffer:=TBitmap.Create;
  BackBuffer.Width:=Width;
  BackBuffer.Height:=Height;
  
  BackBuffer.Canvas.Brush.Style:=bsSolid;
  BackBuffer.Canvas.Brush.Color:=$00003000;
  BackBuffer.Canvas.Rectangle(-1, -1, Width, Height);
  
  EnvelopePointToGraphPoint(FPosition, 0, gx, gy);
  BackBuffer.Canvas.Pen.Color:=$00005000;
  BackBuffer.Canvas.Line(gx, 0, gx, Height);
  
  BackBuffer.Canvas.Pen.Color:=$00008000;

  for i:=0 to Length(Point)-1 do begin
    EnvelopePointToGraphPoint(Point[i].x, Point[i].y, gx, gy);
    if i=0 then
      BackBuffer.Canvas.MoveTo(gx, gy)
    else
      BackBuffer.Canvas.LineTo(gx, gy);
  end;

  BackBuffer.Canvas.Brush.Color:=$0000C000;
  BackBuffer.Canvas.Pen.Color:=$00006000;
  for i:=0 to Length(Point)-1 do begin
    EnvelopePointToGraphPoint(Point[i].x, Point[i].y, gx, gy);
    BackBuffer.Canvas.Ellipse(gx - 3, gy - 3, gx + 4, gy + 4);
  end;
  Canvas.Draw(0, 0, BackBuffer);
  BackBuffer.Free;
end;

end.

