538 lines
14 KiB
ObjectPascal

// SPDX-License-Identifier: LGPL-3.0-linking-exception
{
Iintially written by Circular.
}
{******************************* CONTRIBUTOR(S) ******************************
- Edivando S. Santos Brasil | mailedivando@gmail.com
(Compatibility with delphi VCL 11/2018)
***************************** END CONTRIBUTOR(S) *****************************}
unit BGRAKnob;
{$I bgracontrols.inc}
interface
uses
Classes, SysUtils, {$IFDEF FPC}LResources,{$ENDIF} Forms, Controls, Graphics, Dialogs,
{$IFNDEF FPC}BGRAGraphics, GraphType, FPImage, {$ENDIF}
BCBaseCtrls, BGRAGradients, BGRABitmap, BGRABitmapTypes;
type
TBGRAKnobPositionType = (kptLineSquareCap, kptLineRoundCap, kptFilledCircle,
kptHollowCircle);
TBGRAKnobValueChangedEvent = procedure(Sender: TObject; Value: single) of object;
{ TBGRAKnob }
TBGRAKnob = class(TBGRAGraphicCtrl)
private
{ Private declarations }
FPhong: TPhongShading;
FCurveExponent: single;
FKnobBmp: TBGRABitmap;
FKnobColor: TColor;
FAngularPos: single;
FPositionColor: TColor;
FPositionMargin: single;
FPositionOpacity: byte;
FPositionType: TBGRAKnobPositionType;
FPositionWidth: single;
FSettingAngularPos: boolean;
FUsePhongLighting: boolean;
FMinValue, FMaxValue: single;
FOnKnobValueChange: TBGRAKnobValueChangedEvent;
FStartFromBottom: boolean;
procedure CreateKnobBmp;
function GetLightIntensity: integer;
function GetValue: single;
procedure SetCurveExponent(const AValue: single);
procedure SetLightIntensity(const AValue: integer);
procedure SetStartFromBottom(const AValue: boolean);
procedure SetValue(AValue: single);
procedure SetMaxValue(AValue: single);
procedure SetMinValue(AValue: single);
procedure SetPositionColor(const AValue: TColor);
procedure SetPositionMargin(AValue: single);
procedure SetPositionOpacity(const AValue: byte);
procedure SetPositionType(const AValue: TBGRAKnobPositionType);
procedure SetPositionWidth(const AValue: single);
procedure SetUsePhongLighting(const AValue: boolean);
procedure UpdateAngularPos(X, Y: integer);
procedure SetKnobColor(const AValue: TColor);
protected
{ Protected declarations }
procedure MouseDown(Button: TMouseButton; Shift: TShiftState;
X, Y: integer); override;
procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
procedure Paint; override;
procedure Resize; override;
function ValueCorrection(var AValue: single): boolean; overload; virtual;
function ValueCorrection: boolean; overload; virtual;
public
{ Public declarations }
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
public
{ Streaming }
{$IFDEF FPC}
procedure SaveToFile(AFileName: string);
procedure LoadFromFile(AFileName: string);
{$ENDIF}
procedure OnFindClass({%H-}Reader: TReader; const AClassName: string;
var ComponentClass: TComponentClass);
published
{ Published declarations }
property Anchors;
property CurveExponent: single read FCurveExponent write SetCurveExponent;
property KnobColor: TColor read FKnobColor write SetKnobColor;
property LightIntensity: integer read GetLightIntensity write SetLightIntensity;
property PositionColor: TColor read FPositionColor write SetPositionColor;
property PositionWidth: single read FPositionWidth write SetPositionWidth;
property PositionOpacity: byte read FPositionOpacity write SetPositionOpacity;
property PositionMargin: single read FPositionMargin write SetPositionMargin;
property PositionType: TBGRAKnobPositionType
read FPositionType write SetPositionType;
property UsePhongLighting: boolean read FUsePhongLighting write SetUsePhongLighting;
property MinValue: single read FMinValue write SetMinValue;
property MaxValue: single read FMaxValue write SetMaxValue;
property Value: single read GetValue write SetValue;
property OnValueChanged: TBGRAKnobValueChangedEvent
read FOnKnobValueChange write FOnKnobValueChange;
property StartFromBottom: boolean read FStartFromBottom write SetStartFromBottom;
end;
{$IFDEF FPC}procedure Register;{$ENDIF}
implementation
uses Math;
{$IFDEF FPC}
procedure Register;
begin
RegisterComponents('BGRA Controls', [TBGRAKnob]);
end;
{$ENDIF}
{ TBGRAKnob }
procedure TBGRAKnob.CreateKnobBmp;
var
tx, ty: integer;
h: single;
d2: single;
v: TPointF;
p: PBGRAPixel;
center: TPointF;
yb: integer;
xb: integer;
mask: TBGRABitmap;
Map: TBGRABitmap;
BGRAKnobColor: TBGRAPixel;
begin
tx := ClientWidth;
ty := ClientHeight;
if (tx = 0) or (ty = 0) then
exit;
FreeAndNil(FKnobBmp);
FKnobBmp := TBGRABitmap.Create(tx, ty);
center := PointF((tx - 1) / 2, (ty - 1) / 2);
BGRAKnobColor := KnobColor;
if UsePhongLighting then
begin
//compute knob height map
Map := TBGRABitmap.Create(tx, ty);
for yb := 0 to ty - 1 do
begin
p := map.ScanLine[yb];
for xb := 0 to tx - 1 do
begin
//compute vector between center and current pixel
v := PointF(xb, yb) - center;
//scale down to unit circle (with 1 pixel margin for soft border)
v.x := v.x /(tx / 2 + 1);
v.y := v.y / (ty / 2 + 1);
//compute squared distance with scalar product
d2 := v {$if FPC_FULLVERSION < 30203}*{$ELSE}**{$ENDIF} v;
//interpolate as quadratic curve and apply power function
if d2 > 1 then
h := 0
else
h := power(1 - d2, FCurveExponent);
p^ := MapHeightToBGRA(h, 255);
Inc(p);
end;
end;
//antialiased border
mask := TBGRABitmap.Create(tx, ty, BGRABlack);
Mask.FillEllipseAntialias(center.x, center.y, tx / 2, ty / 2, BGRAWhite);
map.ApplyMask(mask);
Mask.Free;
FPhong.Draw(FKnobBmp, Map, 30, 0, 0, BGRAKnobColor);
Map.Free;
end
else
begin
FKnobBmp.FillEllipseAntialias(center.x, center.y, tx / 2, ty / 2, BGRAKnobColor);
end;
end;
function TBGRAKnob.GetLightIntensity: integer;
begin
Result := round(FPhong.LightSourceIntensity);
end;
function TBGRAKnob.GetValue: single;
begin
Result := FAngularPos * 180 / Pi;
if Result < 0 then
Result := Result +360;
Result := 270 - Result;
if Result < 0 then
Result := Result +360;
end;
procedure TBGRAKnob.SetCurveExponent(const AValue: single);
begin
if FCurveExponent = AValue then
exit;
FCurveExponent := AValue;
FreeAndNil(FKnobBmp);
Invalidate;
end;
procedure TBGRAKnob.SetKnobColor(const AValue: TColor);
begin
if FKnobColor = AValue then
exit;
FKnobColor := AValue;
FreeAndNil(FKnobBmp);
Invalidate;
end;
procedure TBGRAKnob.SetLightIntensity(const AValue: integer);
begin
if AValue <> FPhong.LightSourceIntensity then
begin
FPhong.LightSourceIntensity := AValue;
FreeAndNil(FKnobBmp);
Invalidate;
end;
end;
procedure TBGRAKnob.SetStartFromBottom(const AValue: boolean);
begin
if FStartFromBottom = AValue then
exit;
FStartFromBottom := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetValue(AValue: single);
var
NewAngularPos: single;
begin
ValueCorrection(AValue);
NewAngularPos := 3 * Pi / 2 - AValue * Pi / 180;
if NewAngularPos > Pi then
NewAngularPos := NewAngularPos -(2 * Pi);
if NewAngularPos < -Pi then
NewAngularPos := NewAngularPos +(2 * Pi);
if NewAngularPos <> FAngularPos then
begin
FAngularPos := NewAngularPos;
Invalidate;
end;
end;
procedure TBGRAKnob.SetMaxValue(AValue: single);
begin
if AValue < 0 then
AValue := 0;
if AValue > 360 then
AValue := 360;
if FMaxValue = AValue then
exit;
FMaxValue := AValue;
if FMinValue > FMaxValue then
FMinValue := FMaxValue;
if ValueCorrection then
Invalidate;
end;
procedure TBGRAKnob.SetMinValue(AValue: single);
begin
if AValue < 0 then
AValue := 0;
if AValue > 360 then
AValue := 360;
if FMinValue = AValue then
exit;
FMinValue := AValue;
if FMaxValue < FMinValue then
FMaxValue := FMinValue;
if ValueCorrection then
Invalidate;
end;
procedure TBGRAKnob.SetPositionColor(const AValue: TColor);
begin
if FPositionColor = AValue then
exit;
FPositionColor := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetPositionMargin(AValue: single);
begin
if FPositionMargin = AValue then
exit;
FPositionMargin := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetPositionOpacity(const AValue: byte);
begin
if FPositionOpacity = AValue then
exit;
FPositionOpacity := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetPositionType(const AValue: TBGRAKnobPositionType);
begin
if FPositionType = AValue then
exit;
FPositionType := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetPositionWidth(const AValue: single);
begin
if FPositionWidth = AValue then
exit;
FPositionWidth := AValue;
Invalidate;
end;
procedure TBGRAKnob.SetUsePhongLighting(const AValue: boolean);
begin
if FUsePhongLighting = AValue then
exit;
FUsePhongLighting := AValue;
FreeAndNil(FKnobBmp);
Invalidate;
end;
procedure TBGRAKnob.UpdateAngularPos(X, Y: integer);
var
FPreviousPos, Sign: single;
begin
FPreviousPos := FAngularPos;
if FStartFromBottom then
Sign := 1
else
Sign := -1;
FAngularPos := ArcTan2((-Sign) * (Y - ClientHeight / 2) / ClientHeight,
Sign * (X - ClientWidth / 2) / ClientWidth);
ValueCorrection;
Invalidate;
if (FPreviousPos <> FAngularPos) and Assigned(FOnKnobValueChange) then
FOnKnobValueChange(Self, Value);
end;
procedure TBGRAKnob.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
begin
inherited MouseDown(Button, Shift, X, Y);
if Button = mbLeft then
begin
FSettingAngularPos := True;
UpdateAngularPos(X, Y);
end;
end;
procedure TBGRAKnob.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
begin
inherited MouseUp(Button, Shift, X, Y);
if Button = mbLeft then
FSettingAngularPos := False;
end;
procedure TBGRAKnob.MouseMove(Shift: TShiftState; X, Y: integer);
begin
inherited MouseMove(Shift, X, Y);
if FSettingAngularPos then
UpdateAngularPos(X, Y);
end;
procedure TBGRAKnob.Paint;
var
Bmp: TBGRABitmap;
Center, Pos: TPointF;
PosColor: TBGRAPixel;
PosLen: single;
begin
if (ClientWidth = 0) or (ClientHeight = 0) then
exit;
if FKnobBmp = nil then
begin
CreateKnobBmp;
if FKnobBmp = nil then
Exit;
end;
Bmp := TBGRABitmap.Create(ClientWidth, ClientHeight);
Bmp.BlendImage(0, 0, FKnobBmp, boLinearBlend);
//draw current position
PosColor := ColorToBGRA(ColorToRGB(FPositionColor), FPositionOpacity);
Center := PointF(ClientWidth / 2, ClientHeight / 2);
Pos.X := Cos(FAngularPos) * (ClientWidth / 2);
Pos.Y := -Sin(FAngularPos) * (ClientHeight / 2);
if not FStartFromBottom then
Pos := -Pos;
PosLen := VectLen(Pos);
Pos := Pos * ((PosLen - PositionMargin - FPositionWidth) / PosLen);
Pos := Center + Pos;
case PositionType of
kptLineSquareCap:
begin
Bmp.LineCap := pecSquare;
Bmp.DrawLineAntialias(Center.X, Center.Y, Pos.X, Pos.Y, PosColor, FPositionWidth);
end;
kptLineRoundCap:
begin
Bmp.LineCap := pecRound;
Bmp.DrawLineAntialias(Center.X, Center.Y, Pos.X, Pos.Y, PosColor, FPositionWidth);
end;
kptFilledCircle:
begin
Bmp.FillEllipseAntialias(Pos.X, Pos.Y, FPositionWidth, FPositionWidth, PosColor);
end;
kptHollowCircle:
begin
Bmp.EllipseAntialias(Pos.X, Pos.Y, FPositionWidth * 2 / 3,
FPositionWidth * 2 / 3, PosColor, FPositionWidth / 3);
end;
end;
Bmp.Draw(Canvas, 0, 0, False);
Bmp.Free;
end;
procedure TBGRAKnob.Resize;
begin
inherited Resize;
if (FKnobBmp <> nil) and ((ClientWidth <> FKnobBmp.Width) or
(ClientHeight <> FKnobBmp.Height)) then
FreeAndNil(FKnobBmp);
end;
function TBGRAKnob.ValueCorrection(var AValue: single): boolean;
begin
if AValue < MinValue then
begin
AValue := MinValue;
Result := True;
end
else
if AValue > MaxValue then
begin
AValue := MaxValue;
Result := True;
end
else
Result := False;
end;
function TBGRAKnob.ValueCorrection: boolean;
var
LValue: single;
begin
LValue := Value;
Result := ValueCorrection(LValue);
if Result then
Value := LValue;
end;
constructor TBGRAKnob.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
with GetControlClassDefaultSize do
SetInitialBounds(0, 0, CX, CY);
FPhong := TPhongShading.Create;
FPhong.LightPositionZ := 100;
FPhong.LightSourceIntensity := 300;
FPhong.NegativeDiffusionFactor := 0.8;
FPhong.AmbientFactor := 0.5;
FPhong.DiffusionFactor := 0.6;
FKnobBmp := nil;
FCurveExponent := 0.2;
FKnobColor := clBtnFace;
FPositionColor := clBtnText;
FPositionOpacity := 192;
FPositionWidth := 4;
FPositionMargin := 4;
FPositionType := kptLineSquareCap;
FUsePhongLighting := True;
FOnKnobValueChange := nil;
FStartFromBottom := True;
FMinValue := 30;
FMaxValue := 330;
end;
destructor TBGRAKnob.Destroy;
begin
FPhong.Free;
FKnobBmp.Free;
inherited Destroy;
end;
{$IFDEF FPC}
procedure TBGRAKnob.SaveToFile(AFileName: string);
var
AStream: TMemoryStream;
begin
AStream := TMemoryStream.Create;
try
WriteComponentAsTextToStream(AStream, Self);
AStream.SaveToFile(AFileName);
finally
AStream.Free;
end;
end;
procedure TBGRAKnob.LoadFromFile(AFileName: string);
var
AStream: TMemoryStream;
begin
AStream := TMemoryStream.Create;
try
AStream.LoadFromFile(AFileName);
ReadComponentFromTextStream(AStream, TComponent(Self), OnFindClass);
finally
AStream.Free;
end;
end;
{$ENDIF}
procedure TBGRAKnob.OnFindClass(Reader: TReader; const AClassName: string;
var ComponentClass: TComponentClass);
begin
if CompareText(AClassName, 'TBGRAKnob') = 0 then
ComponentClass := TBGRAKnob;
end;
end.