| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056 |
- // SPDX-License-Identifier: LGPL-3.0-linking-exception
- {
- Initially written by Circular.
- }
- {******************************* CONTRIBUTOR(S) ******************************
- - Edivando S. Santos Brasil | [email protected]
- (Compatibility with delphi VCL 11/2018)
- - Sandy Ganz | [email protected]
- Added range, sector, and other features
- 12/30/2024 - Added option for audio taper, and no position draw (kptNone)
- ***************************** END CONTRIBUTOR(S) *****************************}
- unit BGRAKnob;
- {$I bgracontrols.inc}
- interface
- uses
- Classes, SysUtils, {$IFDEF FPC}LResources,{$ENDIF} Forms, Controls, Graphics,
- {$IFNDEF FPC}BGRAGraphics, GraphType, FPImage, {$ENDIF}
- BCBaseCtrls, BGRAGradients, BGRABitmap, BGRABitmapTypes;
- type
- TBGRAKnobPositionType = (kptLineSquareCap, kptLineRoundCap, kptFilledCircle,
- kptHollowCircle, kptNone);
- TKnobType = (ktRange, ktSector);
- TKnobTaperType = (kttLinear, kttAudioSlow, kttAudioFast);
- 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; // In RADIANS
- FPositionColor: TColor;
- FPositionMargin: single;
- FPositionOpacity: byte;
- FPositionType: TBGRAKnobPositionType;
- FPositionWidth: single;
- FSettingAngularPos: boolean;
- FTaperType: TKnobTaperType;
- FUsePhongLighting: boolean;
- FMinValue, FMaxValue: single; // Knob Values
- FStartAngle, FEndAngle: single; // Knob Angles
- FKnobType: TKnobType;
- FOnKnobValueChange: TBGRAKnobValueChangedEvent;
- FStartFromBottom: boolean;
- FWheelSpeed: byte; // 0 : no wheel, 1 slowest, 255 fastest
- FWheelWrap: boolean;
- FSlowSnap: boolean;
- FReverseScale: boolean;
- FSectorDivisions: integer; // Computed internally from FMinValue/FMaxValue
- function AudioTaperMapping(x, K : single): single;
- function InverseAudioTaperMapping(y, K : single): single;
- procedure CreateKnobBmp;
- function GetLightIntensity: integer;
- function GetValue: single;
- function AngularPosToDeg(RadPos: single): single;
- function DegPosToAngular(DegPos: single): 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 SetStartAngle(AValue: single);
- procedure SetEndAngle(AValue: single);
- procedure SetKnobType(const AValue: TKnobType);
- 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);
- procedure SetWheelSpeed(AValue: byte);
- procedure SetReverseScale(AValue: boolean);
- procedure SetTaperType(AValue: TKnobTaperType);
- protected
- { Protected declarations }
- class function GetControlClassDefaultSize: TSize; override;
- 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;
- function DoMouseWheel(Shift: TShiftState; WheelDelta: integer; MousePos: TPoint): boolean; override;
- procedure MouseWheelPos({%H-}Shift: TShiftState; WheelDelta: integer); virtual;
- function RemapRange(OldValue: single; OldMin, OldMax, NewMin, NewMax: single): single;
- function AngularPosSector(AValue: single): single;
- 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 nodefault;
- property KnobColor: TColor read FKnobColor write SetKnobColor default clBtnFace;
- property LightIntensity: integer read GetLightIntensity write SetLightIntensity default 300;
- property PositionColor: TColor read FPositionColor write SetPositionColor default clBtnText;
- property PositionWidth: single read FPositionWidth write SetPositionWidth default 4;
- property PositionOpacity: byte read FPositionOpacity write SetPositionOpacity default 192;
- property PositionMargin: single read FPositionMargin write SetPositionMargin default 4;
- property PositionType: TBGRAKnobPositionType
- read FPositionType write SetPositionType default kptLineSquareCap;
- property UsePhongLighting: boolean read FUsePhongLighting write SetUsePhongLighting default true;
- property MinValue: single read FMinValue write SetMinValue nodefault;
- property MaxValue: single read FMaxValue write SetMaxValue nodefault;
- property StartFromBottom: boolean read FStartFromBottom write SetStartFromBottom default true;
- property StartAngle: single read FStartAngle write SetStartAngle default 30;
- property EndAngle: single read FEndAngle write SetEndAngle default 330;
- property KnobType: TKnobType read FKnobType write SetKnobType default ktRange;
- property TaperType: TKnobTaperType read FTaperType write SetTaperType default kttLinear;
- property Value: single read GetValue write SetValue nodefault;
- property OnValueChanged: TBGRAKnobValueChangedEvent
- read FOnKnobValueChange write FOnKnobValueChange;
- property WheelSpeed: byte read FWheelSpeed write SetWheelSpeed default 0;
- property WheelWrap: boolean read FWheelWrap write FWheelWrap default false;
- property SlowSnap: boolean read FSlowSnap write FSlowSnap default false;
- property ReverseScale: boolean read FReverseScale write SetReverseScale default false;
- property OnMouseWheel;
- property OnClick;
- property OnDblClick;
- property OnMouseDown;
- property OnMouseUp;
- property OnMouseMove;
- property OnMouseEnter;
- property OnMouseLeave;
- end;
- {$IFDEF FPC}
- procedure Register;
- {$ENDIF}
- const
- VERSIONSTR = '2.2'; // knob version
- implementation
- uses Math;
- const
- WHEELSPEEDFACTOR = 20.0; // used to calculate mouse wheel speed
- WHEELSPEEDBASE = 300;
- AUDIO_TAPER_SLOW_K = 8;
- AUDIO_TAPER_FAST_K = 4;
- {$IFDEF FPC}
- procedure Register;
- begin
- RegisterComponents('BGRA Controls', [TBGRAKnob]);
- end;
- {$ENDIF}
- { TBGRAKnob }
- // AudioTaperMapping will estimate the curve of an Audio Taper
- // potentiometer. The value of 'x' typically from a linear set
- // and is mapped to a curve that will simulate the curve
- // of an Audio taper potentiometer. A few types of exists, but
- // for here we are looking at 10% of the Max Value as 'AudioSlow'
- // when knob at 50%. 'AudioFast' is the same but at 50% the
- // value is at 15% of Max.
- // Typically the Max should be at 100 and Min at 0 for this
- // to make sense. Other values may not do what you think.
- //
- // The value to be mapped is 'x', and the factor 'K' is
- // how 'curvey' the line is.
- //
- // For MinValue = 0 and MaxValue = 100 Below are the goal
- //
- // For values of K = 8, this gives a slow acting curve
- // where at mid position (50%) the value is around 10% of
- // the Max.
- //
- // For values of K = 4, this gives a faster acting curve
- // where at mid position (50%) the value is around 15% of
- // the Max.
- //
- // The Mapping/Inverse must both use the same 'K'
- //
- // While MinValue and MaxValue can be anything, typically
- // MinValue = 0, and MaxValue 100. Think in percent. Experiment
- // and see. MinValue = 0, and MaxValue = 1.0 also works well.
- // Linear to AudioTaper
- function TBGRAKnob.AudioTaperMapping(x, K : single): single;
- var
- sign_change : single;
- begin
- // simple version
- sign_change := 1;
- if x < 0 then
- begin
- x := abs(x);
- sign_change := -1;
- end;
- x := x / FMaxValue; // scale
- // Simulate the curve from a linear space
- Result := x / (1 + (1 - x) * K) * FMaxValue * sign_change;
- end;
- // Same Idea here but the inverse so we can map back an Audio taper
- // value back to a linear one for the knob to be set.
- function TBGRAKnob.InverseAudioTaperMapping(y, K : single): single;
- var
- sign_change : single;
- begin
- sign_change := 1;
- if y < 0 then
- begin
- y := abs(y);
- sign_change := -1;
- end;
- y := y / FMaxValue; // scale
- // reverse the curve to a linear space
- Result := (y + y * K) / (1 + y * K) * FMaxValue * sign_change;
- end;
- // Override the base class which has a rectangular dimension, odd for a knob
- class function TBGRAKnob.GetControlClassDefaultSize: TSize;
- begin
- Result.CX := 50;
- Result.CY := 50;
- end;
- 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
- // Maintains the correct value range based on knobtype, result in terms of
- // FMinValue and FMaxValue
- Result := RemapRange(AngularPosToDeg(FAngularPos), FStartAngle,
- FEndAngle, FMinValue, FMaxValue);
- // Check to Reverse the scale and fix value
- if FReverseScale then
- Result := FMaxValue + FMinValue - Result;
- if FKnobType = ktSector then
- Result := Round(Result);
- // After all the mess above, map it to AudioTaper curves if needed.
- if FTaperType = kttAudioSlow THEN
- Result := AudioTaperMapping(Result, AUDIO_TAPER_SLOW_K)
- else
- if FTaperType = kttAudioFast THEN
- Result := AudioTaperMapping(Result, AUDIO_TAPER_FAST_K)
- end;
- function TBGRAKnob.AngularPosToDeg(RadPos: single): single;
- begin
- // helper to convert AnglePos in radians to degrees, wraps as needed
- Result := RadPos * 180 / Pi;
- if Result < 0 then
- Result := Result + 360;
- Result := 270 - Result;
- if Result < 0 then
- Result := Result + 360;
- end;
- function TBGRAKnob.DegPosToAngular(DegPos: single): single;
- begin
- // helper to convert Angle in degrees to radians
- Result := 3 * Pi / 2 - DegPos * Pi / 180;
- if Result > Pi then
- Result := Result - (2 * Pi);
- if Result < -Pi then
- Result := Result + (2 * Pi);
- end;
- function TBGRAKnob.AngularPosSector(AValue: single): single;
- var
- sector: integer;
- begin
- // AValue is the degree angle of FAngularPos of where the mouse is
- // typically. So no restrictions on values, 0 to < 360
- if AValue > FEndAngle then
- Avalue := FEndAngle;
- if AValue < FStartAngle then
- Avalue := FStartAngle;
- // from the current angular pos get the value
- sector := Round(RemapRange(AValue, FStartAngle, FEndAngle, FMinValue, FMaxValue));
- // now get back the FAngularPos after mapping
- Result := DegPosToAngular(RemapRange(sector, FMinValue, FMaxValue, FStartAngle, FEndAngle));
- end;
- function TBGRAKnob.ValueCorrection(var AValue: single): boolean;
- begin
- if AValue < FStartAngle then
- begin
- AValue := FStartAngle;
- Result := True;
- end
- else
- if AValue > FEndAngle then
- begin
- AValue := FEndAngle;
- Result := True;
- end
- else
- Result := False;
- end;
- function TBGRAKnob.ValueCorrection: boolean;
- var
- LValue: single;
- begin
- LValue := AngularPosToDeg(FAngularPos);
- // this always needs to be in Degrees of position (NOT VALUE)
- Result := ValueCorrection(LValue); // LValue modified by call
- if Result then
- FAngularPos := DegPosToAngular(LValue); // Back to Radians
- end;
- function TBGRAKnob.RemapRange(OldValue: single;
- OldMin, OldMax, NewMin, NewMax: single): single;
- begin
- // Generic mapping of ranges. Value is the number to remap, returns number
- // in the new range. Looks for odd div by 0 condition and fixes
- if OldMin = OldMax then
- begin
- if OldValue <= OldMin then
- exit(NewMin)
- else
- exit(NewMax);
- end;
- Result := (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin;
- 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.SetWheelSpeed(AValue: byte);
- begin
- // Sets the mouse wheel speed
- FWheelSpeed := AValue;
- end;
- procedure TBGRAKnob.SetReverseScale(AValue: boolean);
- var
- oldVal: single;
- begin
- // Sets the direction of the scale
- if FReverseScale = AValue then
- exit;
- oldVal := GetValue;
- FReverseScale := AValue;
- SetValue(oldVal);
- 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
- // first things, if we are doing audio taper, then inverse map it
- if FTaperType = kttAudioSlow THEN
- AValue := InverseAudioTaperMapping(AValue, AUDIO_TAPER_SLOW_K)
- else
- if FTaperType = kttAudioFast THEN
- AValue := InverseAudioTaperMapping(AValue, AUDIO_TAPER_FAST_K);
- // carry on with range checks, AValue is in user space not degrees until later
- if AValue > FMaxValue then
- AValue := FMaxValue;
- if AValue < FMinValue then
- AValue := FMinValue;
- // Get the integeral value from given sector,
- if FKnobType = ktSector then
- AValue := Round(AValue); // Round to sector
- AValue := RemapRange(AValue, FMinValue, FMaxValue, FStartAngle, FEndAngle);
- // Reverse the scale if needed
- if FReverseScale then
- AValue := FEndAngle + FStartAngle - AValue;
- 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.SetEndAngle(AValue: single);
- var
- oldValue: single;
- begin
- // degrees for position of start position
- if (FEndAngle = AValue) or (FStartAngle >= AValue) or (AValue < 0) or
- (AValue >= 360) then
- exit;
- // If we are going to change the angle, we need to save off the current value
- // as it will change it if we don't reset it
- oldValue := GetValue;
- FEndAngle := AValue;
- if FStartAngle > FEndAngle then
- FStartAngle := FEndAngle;
- SetValue(oldValue); // Invalidate the hard way, preserve Value for user
- end;
- procedure TBGRAKnob.SetStartAngle(AValue: single);
- var
- oldValue: single;
- begin
- // Start angle in degrees
- if (FStartAngle = AValue) or (FEndAngle <= AValue) or (AValue < 0) or
- (AValue >= 360) then
- exit;
- oldValue := GetValue;
- FStartAngle := AValue;
- if FEndAngle < FStartAngle then
- FEndAngle := FStartAngle;
- SetValue(oldValue); // Invalidate the hard way, preserve Value for user
- end;
- procedure TBGRAKnob.SetMaxValue(AValue: single);
- var
- oldValue: single;
- IntMinVal, IntMaxVal: integer;
- begin
- // Note : MinValue and MaxValue can span negative ranges and be increasing
- // decreasing
- // If sector mode do some math, set number of sector divisions
- if FKnobType = ktSector then
- begin
- IntMinVal := Round(FMinValue);
- IntMaxVal := Round(AValue);
- FSectorDivisions := IntMaxVal - IntMinVal;
- // Just to be safe, ensure at least 1 sector division
- if FSectorDivisions < 1 then
- FSectorDivisions := 1;
- FMinValue := IntMinVal; // force to an integeral value if in sector mode
- AValue := IntMaxVal;
- end;
- // Min and Max Can't be the same in any case
- if (FMinValue >= AValue) then
- exit;
- oldValue := GetValue;
- FMaxValue := AValue;
- SetValue(oldValue);
- end;
- procedure TBGRAKnob.SetMinValue(AValue: single);
- var
- oldValue: single;
- IntMinVal, IntMaxVal: integer;
- begin
- // Note : MinValue and MaxValue can span negative ranges and be increasing
- // decreasing
- // If sector mode do some math, set number of sector divisions
- if FKnobType = ktSector then
- begin
- IntMinVal := Round(AValue);
- IntMaxVal := Round(FMaxValue);
- FSectorDivisions := IntMaxVal - IntMinVal;
- // Just to be safe, ensure at least 1 sector division
- if FSectorDivisions < 1 then
- FSectorDivisions := 1;
- FMaxValue := IntMaxVal; // force to an integeral value if in sector mode
- AValue := IntMinVal;
- end;
- // Min and Max Can't be the same in any case, rounding can also cause this
- if (FMaxValue <= AValue) then
- exit;
- // Save and refresh with proper value
- oldValue := GetValue;
- FMinValue := AValue;
- SetValue(oldValue);
- end;
- procedure TBGRAKnob.SetKnobType(const AValue: TKnobType);
- var
- IntMinVal, IntMaxVal: integer;
- begin
- // Set the knobtype, if ktRange nothing really needed,
- // if ktSector then calc and check value for divisions.
- FKnobType := AValue;
- if FKnobType = ktSector then
- begin
- IntMinVal := Round(FMinValue);
- IntMaxVal := Round(FMaxValue);
- FSectorDivisions := IntMaxVal - IntMinVal;
- if FSectorDivisions < 1 then
- FSectorDivisions := 1;
- end;
- // No other changes for ktRange mode
- end;
- procedure TBGRAKnob.SetTaperType(AValue: TKnobTaperType);
- begin
- if FTaperType = AValue then
- Exit;
- FTaperType := AValue;
- 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;
- prevAngle, currAngle: single;
- begin
- // Saves a previous position for the SlowSnap functionality.
- // Uses that to see how far we have moved to see if we should move
- FPreviousPos := FAngularPos;
- prevAngle := AngularPosToDeg(FAngularPos); // Need these in degrees!
- if FStartFromBottom then
- Sign := 1
- else
- Sign := -1;
- FAngularPos := ArcTan2((-Sign) * (Y - ClientHeight / 2) / ClientHeight,
- Sign * (X - ClientWidth / 2) / ClientWidth);
- currAngle := AngularPosToDeg(FAngularPos);
- // If sector mode then we need to translate angle to sector and back to simulate each sector
- if FKnobType = ktSector then
- FAngularPos := AngularPosSector(currAngle);
- ValueCorrection;
- // If SlowSnap mode make sure past the Min/Max angles before snapping.
- // This is less twitchy behavior near endpoints. This may not make sense
- // when in ktSector mode so skip if that
- if FSlowSnap and (FKnobType <> ktSector) then
- if ((currAngle <= FStartAngle) and (prevAngle = FEndAngle)) or
- ((CurrAngle >= FEndAngle) and (PrevAngle = FStartAngle)) then
- FAngularPos := FPreviousPos;
- Invalidate;
- if (FPreviousPos <> FAngularPos) and Assigned(FOnKnobValueChange) then
- FOnKnobValueChange(Self, Value); // Value passes back with data based on knobtype
- 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;
- 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;
- FTaperType := kttLinear; // Should be default for compatibility
- FUsePhongLighting := True;
- FOnKnobValueChange := nil;
- FStartFromBottom := True;
- FWheelSpeed := 0; // 0, no wheel, 1 slowest, 255 fastest
- FWheelWrap := False; // don't allow the mouse wheel to wrap around
- FSlowSnap := False; // True : less snap around on min/max
- FReverseScale := False; // Flips direction around if True
- FSectorDivisions := 1; // Number of divisions for sector knob, computed
- FKnobType := ktRange; // Defaults ranges to match orig knob
- FStartAngle := 30;
- FEndAngle := 330;
- FMinValue := 30;
- FMaxValue := 330;
- SetValue(30);
- 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;
- function TBGRAKnob.DoMouseWheel(Shift: TShiftState; WheelDelta: integer;
- MousePos: TPoint): boolean;
- begin
- Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
- MouseWheelPos(Shift, WheelDelta);
- end;
- procedure TBGRAKnob.MouseWheelPos(Shift: TShiftState; WheelDelta: integer);
- var
- newValue: single;
- begin
- // WheelSpeed is a Base Value and a factor to slow or speed up the wheel affect.
- // FWheelSpeed = 0 then no wheel, 1 slowest movement, 255 fastest movement
- //
- // Note if Mouse Wheel is used in AudioSlow or AudioFast mode, the wheel
- // will not be compensated so will seem faster at 0 side, and slower as
- // it gets to the MaxValue since it's values curved. (assumes 0min, 100max)
- // Setting the wheel speed to a low value (like 1) will help these modes
- if FWheelSpeed > 0 then
- begin
- if FKnobType = ktRange then
- begin
- newValue := Value + (FMaxValue - FMinValue) * WheelDelta /
- ((WHEELSPEEDBASE - FWheelSpeed) * WHEELSPEEDFACTOR);
- // Check for wrap in either direction
- if FWheelWrap then
- begin
- if newValue > FMaxValue then
- newValue := FMinValue
- else
- if newValue < FMinValue then
- newValue := FMaxValue;
- end;
- end
- else
- begin
- // ktSector
- // Jumps are now always 1 or -1, in terms of sectors, note wheel speed
- // does not make any difference in ktSector mode since we can only bump 1/-1
- // value or it will rounded back to an integral value an not move
- if WheelDelta < 0 then
- begin
- // Move Backwards, check for wrap
- newValue := Value - 1.0;
- if newValue < FMinValue then
- begin
- if FWheelWrap then
- newValue := FMaxValue
- else
- newValue := FMinValue;
- end;
- end
- else
- begin
- // Move Forward, check for wrap
- newValue := Value + 1.0;
- if newValue >= FMaxValue then
- begin
- if FWheelWrap then
- newValue := FMinValue
- else
- newValue := FMaxValue;
- end;
- end;
- end;
- SetValue(newValue);
- end;
- if Assigned(FOnKnobValueChange) then
- FOnKnobValueChange(Self, Value);
- end;
- end.
|