bgraknob.pas 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056
  1. // SPDX-License-Identifier: LGPL-3.0-linking-exception
  2. {
  3. Initially written by Circular.
  4. }
  5. {******************************* CONTRIBUTOR(S) ******************************
  6. - Edivando S. Santos Brasil | [email protected]
  7. (Compatibility with delphi VCL 11/2018)
  8. - Sandy Ganz | [email protected]
  9. Added range, sector, and other features
  10. 12/30/2024 - Added option for audio taper, and no position draw (kptNone)
  11. ***************************** END CONTRIBUTOR(S) *****************************}
  12. unit BGRAKnob;
  13. {$I bgracontrols.inc}
  14. interface
  15. uses
  16. Classes, SysUtils, {$IFDEF FPC}LResources,{$ENDIF} Forms, Controls, Graphics,
  17. {$IFNDEF FPC}BGRAGraphics, GraphType, FPImage, {$ENDIF}
  18. BCBaseCtrls, BGRAGradients, BGRABitmap, BGRABitmapTypes;
  19. type
  20. TBGRAKnobPositionType = (kptLineSquareCap, kptLineRoundCap, kptFilledCircle,
  21. kptHollowCircle, kptNone);
  22. TKnobType = (ktRange, ktSector);
  23. TKnobTaperType = (kttLinear, kttAudioSlow, kttAudioFast);
  24. TBGRAKnobValueChangedEvent = procedure(Sender: TObject; Value: single) of object;
  25. { TBGRAKnob }
  26. TBGRAKnob = class(TBGRAGraphicCtrl)
  27. private
  28. { Private declarations }
  29. FPhong: TPhongShading;
  30. FCurveExponent: single;
  31. FKnobBmp: TBGRABitmap;
  32. FKnobColor: TColor;
  33. FAngularPos: single; // In RADIANS
  34. FPositionColor: TColor;
  35. FPositionMargin: single;
  36. FPositionOpacity: byte;
  37. FPositionType: TBGRAKnobPositionType;
  38. FPositionWidth: single;
  39. FSettingAngularPos: boolean;
  40. FTaperType: TKnobTaperType;
  41. FUsePhongLighting: boolean;
  42. FMinValue, FMaxValue: single; // Knob Values
  43. FStartAngle, FEndAngle: single; // Knob Angles
  44. FKnobType: TKnobType;
  45. FOnKnobValueChange: TBGRAKnobValueChangedEvent;
  46. FStartFromBottom: boolean;
  47. FWheelSpeed: byte; // 0 : no wheel, 1 slowest, 255 fastest
  48. FWheelWrap: boolean;
  49. FSlowSnap: boolean;
  50. FReverseScale: boolean;
  51. FSectorDivisions: integer; // Computed internally from FMinValue/FMaxValue
  52. function AudioTaperMapping(x, K : single): single;
  53. function InverseAudioTaperMapping(y, K : single): single;
  54. procedure CreateKnobBmp;
  55. function GetLightIntensity: integer;
  56. function GetValue: single;
  57. function AngularPosToDeg(RadPos: single): single;
  58. function DegPosToAngular(DegPos: single): single;
  59. procedure SetCurveExponent(const AValue: single);
  60. procedure SetLightIntensity(const AValue: integer);
  61. procedure SetStartFromBottom(const AValue: boolean);
  62. procedure SetValue(AValue: single);
  63. procedure SetMaxValue(AValue: single);
  64. procedure SetMinValue(AValue: single);
  65. procedure SetStartAngle(AValue: single);
  66. procedure SetEndAngle(AValue: single);
  67. procedure SetKnobType(const AValue: TKnobType);
  68. procedure SetPositionColor(const AValue: TColor);
  69. procedure SetPositionMargin(AValue: single);
  70. procedure SetPositionOpacity(const AValue: byte);
  71. procedure SetPositionType(const AValue: TBGRAKnobPositionType);
  72. procedure SetPositionWidth(const AValue: single);
  73. procedure SetUsePhongLighting(const AValue: boolean);
  74. procedure UpdateAngularPos(X, Y: integer);
  75. procedure SetKnobColor(const AValue: TColor);
  76. procedure SetWheelSpeed(AValue: byte);
  77. procedure SetReverseScale(AValue: boolean);
  78. procedure SetTaperType(AValue: TKnobTaperType);
  79. protected
  80. { Protected declarations }
  81. class function GetControlClassDefaultSize: TSize; override;
  82. procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
  83. procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer); override;
  84. procedure MouseMove(Shift: TShiftState; X, Y: integer); override;
  85. procedure Paint; override;
  86. procedure Resize; override;
  87. function ValueCorrection(var AValue: single): boolean; overload; virtual;
  88. function ValueCorrection: boolean; overload; virtual;
  89. function DoMouseWheel(Shift: TShiftState; WheelDelta: integer; MousePos: TPoint): boolean; override;
  90. procedure MouseWheelPos({%H-}Shift: TShiftState; WheelDelta: integer); virtual;
  91. function RemapRange(OldValue: single; OldMin, OldMax, NewMin, NewMax: single): single;
  92. function AngularPosSector(AValue: single): single;
  93. public
  94. { Public declarations }
  95. constructor Create(AOwner: TComponent); override;
  96. destructor Destroy; override;
  97. public
  98. { Streaming }
  99. {$IFDEF FPC}
  100. procedure SaveToFile(AFileName: string);
  101. procedure LoadFromFile(AFileName: string);
  102. {$ENDIF}
  103. procedure OnFindClass({%H-}Reader: TReader; const AClassName: string;
  104. var ComponentClass: TComponentClass);
  105. published
  106. { Published declarations }
  107. property Anchors;
  108. property CurveExponent: single read FCurveExponent write SetCurveExponent nodefault;
  109. property KnobColor: TColor read FKnobColor write SetKnobColor default clBtnFace;
  110. property LightIntensity: integer read GetLightIntensity write SetLightIntensity default 300;
  111. property PositionColor: TColor read FPositionColor write SetPositionColor default clBtnText;
  112. property PositionWidth: single read FPositionWidth write SetPositionWidth default 4;
  113. property PositionOpacity: byte read FPositionOpacity write SetPositionOpacity default 192;
  114. property PositionMargin: single read FPositionMargin write SetPositionMargin default 4;
  115. property PositionType: TBGRAKnobPositionType
  116. read FPositionType write SetPositionType default kptLineSquareCap;
  117. property UsePhongLighting: boolean read FUsePhongLighting write SetUsePhongLighting default true;
  118. property MinValue: single read FMinValue write SetMinValue nodefault;
  119. property MaxValue: single read FMaxValue write SetMaxValue nodefault;
  120. property StartFromBottom: boolean read FStartFromBottom write SetStartFromBottom default true;
  121. property StartAngle: single read FStartAngle write SetStartAngle default 30;
  122. property EndAngle: single read FEndAngle write SetEndAngle default 330;
  123. property KnobType: TKnobType read FKnobType write SetKnobType default ktRange;
  124. property TaperType: TKnobTaperType read FTaperType write SetTaperType default kttLinear;
  125. property Value: single read GetValue write SetValue nodefault;
  126. property OnValueChanged: TBGRAKnobValueChangedEvent
  127. read FOnKnobValueChange write FOnKnobValueChange;
  128. property WheelSpeed: byte read FWheelSpeed write SetWheelSpeed default 0;
  129. property WheelWrap: boolean read FWheelWrap write FWheelWrap default false;
  130. property SlowSnap: boolean read FSlowSnap write FSlowSnap default false;
  131. property ReverseScale: boolean read FReverseScale write SetReverseScale default false;
  132. property OnMouseWheel;
  133. property OnClick;
  134. property OnDblClick;
  135. property OnMouseDown;
  136. property OnMouseUp;
  137. property OnMouseMove;
  138. property OnMouseEnter;
  139. property OnMouseLeave;
  140. end;
  141. {$IFDEF FPC}
  142. procedure Register;
  143. {$ENDIF}
  144. const
  145. VERSIONSTR = '2.2'; // knob version
  146. implementation
  147. uses Math;
  148. const
  149. WHEELSPEEDFACTOR = 20.0; // used to calculate mouse wheel speed
  150. WHEELSPEEDBASE = 300;
  151. AUDIO_TAPER_SLOW_K = 8;
  152. AUDIO_TAPER_FAST_K = 4;
  153. {$IFDEF FPC}
  154. procedure Register;
  155. begin
  156. RegisterComponents('BGRA Controls', [TBGRAKnob]);
  157. end;
  158. {$ENDIF}
  159. { TBGRAKnob }
  160. // AudioTaperMapping will estimate the curve of an Audio Taper
  161. // potentiometer. The value of 'x' typically from a linear set
  162. // and is mapped to a curve that will simulate the curve
  163. // of an Audio taper potentiometer. A few types of exists, but
  164. // for here we are looking at 10% of the Max Value as 'AudioSlow'
  165. // when knob at 50%. 'AudioFast' is the same but at 50% the
  166. // value is at 15% of Max.
  167. // Typically the Max should be at 100 and Min at 0 for this
  168. // to make sense. Other values may not do what you think.
  169. //
  170. // The value to be mapped is 'x', and the factor 'K' is
  171. // how 'curvey' the line is.
  172. //
  173. // For MinValue = 0 and MaxValue = 100 Below are the goal
  174. //
  175. // For values of K = 8, this gives a slow acting curve
  176. // where at mid position (50%) the value is around 10% of
  177. // the Max.
  178. //
  179. // For values of K = 4, this gives a faster acting curve
  180. // where at mid position (50%) the value is around 15% of
  181. // the Max.
  182. //
  183. // The Mapping/Inverse must both use the same 'K'
  184. //
  185. // While MinValue and MaxValue can be anything, typically
  186. // MinValue = 0, and MaxValue 100. Think in percent. Experiment
  187. // and see. MinValue = 0, and MaxValue = 1.0 also works well.
  188. // Linear to AudioTaper
  189. function TBGRAKnob.AudioTaperMapping(x, K : single): single;
  190. var
  191. sign_change : single;
  192. begin
  193. // simple version
  194. sign_change := 1;
  195. if x < 0 then
  196. begin
  197. x := abs(x);
  198. sign_change := -1;
  199. end;
  200. x := x / FMaxValue; // scale
  201. // Simulate the curve from a linear space
  202. Result := x / (1 + (1 - x) * K) * FMaxValue * sign_change;
  203. end;
  204. // Same Idea here but the inverse so we can map back an Audio taper
  205. // value back to a linear one for the knob to be set.
  206. function TBGRAKnob.InverseAudioTaperMapping(y, K : single): single;
  207. var
  208. sign_change : single;
  209. begin
  210. sign_change := 1;
  211. if y < 0 then
  212. begin
  213. y := abs(y);
  214. sign_change := -1;
  215. end;
  216. y := y / FMaxValue; // scale
  217. // reverse the curve to a linear space
  218. Result := (y + y * K) / (1 + y * K) * FMaxValue * sign_change;
  219. end;
  220. // Override the base class which has a rectangular dimension, odd for a knob
  221. class function TBGRAKnob.GetControlClassDefaultSize: TSize;
  222. begin
  223. Result.CX := 50;
  224. Result.CY := 50;
  225. end;
  226. procedure TBGRAKnob.CreateKnobBmp;
  227. var
  228. tx, ty: integer;
  229. h: single;
  230. d2: single;
  231. v: TPointF;
  232. p: PBGRAPixel;
  233. center: TPointF;
  234. yb: integer;
  235. xb: integer;
  236. mask: TBGRABitmap;
  237. Map: TBGRABitmap;
  238. BGRAKnobColor: TBGRAPixel;
  239. begin
  240. tx := ClientWidth;
  241. ty := ClientHeight;
  242. if (tx = 0) or (ty = 0) then
  243. exit;
  244. FreeAndNil(FKnobBmp);
  245. FKnobBmp := TBGRABitmap.Create(tx, ty);
  246. center := PointF((tx - 1) / 2, (ty - 1) / 2);
  247. BGRAKnobColor := KnobColor;
  248. if UsePhongLighting then
  249. begin
  250. //compute knob height map
  251. Map := TBGRABitmap.Create(tx, ty);
  252. for yb := 0 to ty - 1 do
  253. begin
  254. p := map.ScanLine[yb];
  255. for xb := 0 to tx - 1 do
  256. begin
  257. //compute vector between center and current pixel
  258. v := PointF(xb, yb) - center;
  259. //scale down to unit circle (with 1 pixel margin for soft border)
  260. v.x := v.x / (tx / 2 + 1);
  261. v.y := v.y / (ty / 2 + 1);
  262. //compute squared distance with scalar product
  263. d2 := v {$if FPC_FULLVERSION < 30203}*{$ELSE}**{$ENDIF} v;
  264. //interpolate as quadratic curve and apply power function
  265. if d2 > 1 then
  266. h := 0
  267. else
  268. h := power(1 - d2, FCurveExponent);
  269. p^ := MapHeightToBGRA(h, 255);
  270. Inc(p);
  271. end;
  272. end;
  273. //antialiased border
  274. mask := TBGRABitmap.Create(tx, ty, BGRABlack);
  275. Mask.FillEllipseAntialias(center.x, center.y, tx / 2, ty / 2, BGRAWhite);
  276. map.ApplyMask(mask);
  277. Mask.Free;
  278. FPhong.Draw(FKnobBmp, Map, 30, 0, 0, BGRAKnobColor);
  279. Map.Free;
  280. end
  281. else
  282. begin
  283. FKnobBmp.FillEllipseAntialias(center.x, center.y, tx / 2, ty / 2, BGRAKnobColor);
  284. end;
  285. end;
  286. function TBGRAKnob.GetLightIntensity: integer;
  287. begin
  288. Result := round(FPhong.LightSourceIntensity);
  289. end;
  290. function TBGRAKnob.GetValue: single;
  291. begin
  292. // Maintains the correct value range based on knobtype, result in terms of
  293. // FMinValue and FMaxValue
  294. Result := RemapRange(AngularPosToDeg(FAngularPos), FStartAngle,
  295. FEndAngle, FMinValue, FMaxValue);
  296. // Check to Reverse the scale and fix value
  297. if FReverseScale then
  298. Result := FMaxValue + FMinValue - Result;
  299. if FKnobType = ktSector then
  300. Result := Round(Result);
  301. // After all the mess above, map it to AudioTaper curves if needed.
  302. if FTaperType = kttAudioSlow THEN
  303. Result := AudioTaperMapping(Result, AUDIO_TAPER_SLOW_K)
  304. else
  305. if FTaperType = kttAudioFast THEN
  306. Result := AudioTaperMapping(Result, AUDIO_TAPER_FAST_K)
  307. end;
  308. function TBGRAKnob.AngularPosToDeg(RadPos: single): single;
  309. begin
  310. // helper to convert AnglePos in radians to degrees, wraps as needed
  311. Result := RadPos * 180 / Pi;
  312. if Result < 0 then
  313. Result := Result + 360;
  314. Result := 270 - Result;
  315. if Result < 0 then
  316. Result := Result + 360;
  317. end;
  318. function TBGRAKnob.DegPosToAngular(DegPos: single): single;
  319. begin
  320. // helper to convert Angle in degrees to radians
  321. Result := 3 * Pi / 2 - DegPos * Pi / 180;
  322. if Result > Pi then
  323. Result := Result - (2 * Pi);
  324. if Result < -Pi then
  325. Result := Result + (2 * Pi);
  326. end;
  327. function TBGRAKnob.AngularPosSector(AValue: single): single;
  328. var
  329. sector: integer;
  330. begin
  331. // AValue is the degree angle of FAngularPos of where the mouse is
  332. // typically. So no restrictions on values, 0 to < 360
  333. if AValue > FEndAngle then
  334. Avalue := FEndAngle;
  335. if AValue < FStartAngle then
  336. Avalue := FStartAngle;
  337. // from the current angular pos get the value
  338. sector := Round(RemapRange(AValue, FStartAngle, FEndAngle, FMinValue, FMaxValue));
  339. // now get back the FAngularPos after mapping
  340. Result := DegPosToAngular(RemapRange(sector, FMinValue, FMaxValue, FStartAngle, FEndAngle));
  341. end;
  342. function TBGRAKnob.ValueCorrection(var AValue: single): boolean;
  343. begin
  344. if AValue < FStartAngle then
  345. begin
  346. AValue := FStartAngle;
  347. Result := True;
  348. end
  349. else
  350. if AValue > FEndAngle then
  351. begin
  352. AValue := FEndAngle;
  353. Result := True;
  354. end
  355. else
  356. Result := False;
  357. end;
  358. function TBGRAKnob.ValueCorrection: boolean;
  359. var
  360. LValue: single;
  361. begin
  362. LValue := AngularPosToDeg(FAngularPos);
  363. // this always needs to be in Degrees of position (NOT VALUE)
  364. Result := ValueCorrection(LValue); // LValue modified by call
  365. if Result then
  366. FAngularPos := DegPosToAngular(LValue); // Back to Radians
  367. end;
  368. function TBGRAKnob.RemapRange(OldValue: single;
  369. OldMin, OldMax, NewMin, NewMax: single): single;
  370. begin
  371. // Generic mapping of ranges. Value is the number to remap, returns number
  372. // in the new range. Looks for odd div by 0 condition and fixes
  373. if OldMin = OldMax then
  374. begin
  375. if OldValue <= OldMin then
  376. exit(NewMin)
  377. else
  378. exit(NewMax);
  379. end;
  380. Result := (((OldValue - OldMin) * (NewMax - NewMin)) / (OldMax - OldMin)) + NewMin;
  381. end;
  382. procedure TBGRAKnob.SetCurveExponent(const AValue: single);
  383. begin
  384. if FCurveExponent = AValue then
  385. exit;
  386. FCurveExponent := AValue;
  387. FreeAndNil(FKnobBmp);
  388. Invalidate;
  389. end;
  390. procedure TBGRAKnob.SetKnobColor(const AValue: TColor);
  391. begin
  392. if FKnobColor = AValue then
  393. exit;
  394. FKnobColor := AValue;
  395. FreeAndNil(FKnobBmp);
  396. Invalidate;
  397. end;
  398. procedure TBGRAKnob.SetWheelSpeed(AValue: byte);
  399. begin
  400. // Sets the mouse wheel speed
  401. FWheelSpeed := AValue;
  402. end;
  403. procedure TBGRAKnob.SetReverseScale(AValue: boolean);
  404. var
  405. oldVal: single;
  406. begin
  407. // Sets the direction of the scale
  408. if FReverseScale = AValue then
  409. exit;
  410. oldVal := GetValue;
  411. FReverseScale := AValue;
  412. SetValue(oldVal);
  413. end;
  414. procedure TBGRAKnob.SetLightIntensity(const AValue: integer);
  415. begin
  416. if AValue <> FPhong.LightSourceIntensity then
  417. begin
  418. FPhong.LightSourceIntensity := AValue;
  419. FreeAndNil(FKnobBmp);
  420. Invalidate;
  421. end;
  422. end;
  423. procedure TBGRAKnob.SetStartFromBottom(const AValue: boolean);
  424. begin
  425. if FStartFromBottom = AValue then
  426. exit;
  427. FStartFromBottom := AValue;
  428. Invalidate;
  429. end;
  430. procedure TBGRAKnob.SetValue(AValue: single);
  431. var
  432. NewAngularPos: single;
  433. begin
  434. // first things, if we are doing audio taper, then inverse map it
  435. if FTaperType = kttAudioSlow THEN
  436. AValue := InverseAudioTaperMapping(AValue, AUDIO_TAPER_SLOW_K)
  437. else
  438. if FTaperType = kttAudioFast THEN
  439. AValue := InverseAudioTaperMapping(AValue, AUDIO_TAPER_FAST_K);
  440. // carry on with range checks, AValue is in user space not degrees until later
  441. if AValue > FMaxValue then
  442. AValue := FMaxValue;
  443. if AValue < FMinValue then
  444. AValue := FMinValue;
  445. // Get the integeral value from given sector,
  446. if FKnobType = ktSector then
  447. AValue := Round(AValue); // Round to sector
  448. AValue := RemapRange(AValue, FMinValue, FMaxValue, FStartAngle, FEndAngle);
  449. // Reverse the scale if needed
  450. if FReverseScale then
  451. AValue := FEndAngle + FStartAngle - AValue;
  452. ValueCorrection(AValue);
  453. NewAngularPos := 3 * Pi / 2 - AValue * Pi / 180;
  454. if NewAngularPos > Pi then
  455. NewAngularPos := NewAngularPos - (2 * Pi);
  456. if NewAngularPos < -Pi then
  457. NewAngularPos := NewAngularPos + (2 * Pi);
  458. if NewAngularPos <> FAngularPos then
  459. begin
  460. FAngularPos := NewAngularPos;
  461. Invalidate;
  462. end;
  463. end;
  464. procedure TBGRAKnob.SetEndAngle(AValue: single);
  465. var
  466. oldValue: single;
  467. begin
  468. // degrees for position of start position
  469. if (FEndAngle = AValue) or (FStartAngle >= AValue) or (AValue < 0) or
  470. (AValue >= 360) then
  471. exit;
  472. // If we are going to change the angle, we need to save off the current value
  473. // as it will change it if we don't reset it
  474. oldValue := GetValue;
  475. FEndAngle := AValue;
  476. if FStartAngle > FEndAngle then
  477. FStartAngle := FEndAngle;
  478. SetValue(oldValue); // Invalidate the hard way, preserve Value for user
  479. end;
  480. procedure TBGRAKnob.SetStartAngle(AValue: single);
  481. var
  482. oldValue: single;
  483. begin
  484. // Start angle in degrees
  485. if (FStartAngle = AValue) or (FEndAngle <= AValue) or (AValue < 0) or
  486. (AValue >= 360) then
  487. exit;
  488. oldValue := GetValue;
  489. FStartAngle := AValue;
  490. if FEndAngle < FStartAngle then
  491. FEndAngle := FStartAngle;
  492. SetValue(oldValue); // Invalidate the hard way, preserve Value for user
  493. end;
  494. procedure TBGRAKnob.SetMaxValue(AValue: single);
  495. var
  496. oldValue: single;
  497. IntMinVal, IntMaxVal: integer;
  498. begin
  499. // Note : MinValue and MaxValue can span negative ranges and be increasing
  500. // decreasing
  501. // If sector mode do some math, set number of sector divisions
  502. if FKnobType = ktSector then
  503. begin
  504. IntMinVal := Round(FMinValue);
  505. IntMaxVal := Round(AValue);
  506. FSectorDivisions := IntMaxVal - IntMinVal;
  507. // Just to be safe, ensure at least 1 sector division
  508. if FSectorDivisions < 1 then
  509. FSectorDivisions := 1;
  510. FMinValue := IntMinVal; // force to an integeral value if in sector mode
  511. AValue := IntMaxVal;
  512. end;
  513. // Min and Max Can't be the same in any case
  514. if (FMinValue >= AValue) then
  515. exit;
  516. oldValue := GetValue;
  517. FMaxValue := AValue;
  518. SetValue(oldValue);
  519. end;
  520. procedure TBGRAKnob.SetMinValue(AValue: single);
  521. var
  522. oldValue: single;
  523. IntMinVal, IntMaxVal: integer;
  524. begin
  525. // Note : MinValue and MaxValue can span negative ranges and be increasing
  526. // decreasing
  527. // If sector mode do some math, set number of sector divisions
  528. if FKnobType = ktSector then
  529. begin
  530. IntMinVal := Round(AValue);
  531. IntMaxVal := Round(FMaxValue);
  532. FSectorDivisions := IntMaxVal - IntMinVal;
  533. // Just to be safe, ensure at least 1 sector division
  534. if FSectorDivisions < 1 then
  535. FSectorDivisions := 1;
  536. FMaxValue := IntMaxVal; // force to an integeral value if in sector mode
  537. AValue := IntMinVal;
  538. end;
  539. // Min and Max Can't be the same in any case, rounding can also cause this
  540. if (FMaxValue <= AValue) then
  541. exit;
  542. // Save and refresh with proper value
  543. oldValue := GetValue;
  544. FMinValue := AValue;
  545. SetValue(oldValue);
  546. end;
  547. procedure TBGRAKnob.SetKnobType(const AValue: TKnobType);
  548. var
  549. IntMinVal, IntMaxVal: integer;
  550. begin
  551. // Set the knobtype, if ktRange nothing really needed,
  552. // if ktSector then calc and check value for divisions.
  553. FKnobType := AValue;
  554. if FKnobType = ktSector then
  555. begin
  556. IntMinVal := Round(FMinValue);
  557. IntMaxVal := Round(FMaxValue);
  558. FSectorDivisions := IntMaxVal - IntMinVal;
  559. if FSectorDivisions < 1 then
  560. FSectorDivisions := 1;
  561. end;
  562. // No other changes for ktRange mode
  563. end;
  564. procedure TBGRAKnob.SetTaperType(AValue: TKnobTaperType);
  565. begin
  566. if FTaperType = AValue then
  567. Exit;
  568. FTaperType := AValue;
  569. Invalidate;
  570. end;
  571. procedure TBGRAKnob.SetPositionColor(const AValue: TColor);
  572. begin
  573. if FPositionColor = AValue then
  574. exit;
  575. FPositionColor := AValue;
  576. Invalidate;
  577. end;
  578. procedure TBGRAKnob.SetPositionMargin(AValue: single);
  579. begin
  580. if FPositionMargin = AValue then
  581. exit;
  582. FPositionMargin := AValue;
  583. Invalidate;
  584. end;
  585. procedure TBGRAKnob.SetPositionOpacity(const AValue: byte);
  586. begin
  587. if FPositionOpacity = AValue then
  588. exit;
  589. FPositionOpacity := AValue;
  590. Invalidate;
  591. end;
  592. procedure TBGRAKnob.SetPositionType(const AValue: TBGRAKnobPositionType);
  593. begin
  594. if FPositionType = AValue then
  595. exit;
  596. FPositionType := AValue;
  597. Invalidate;
  598. end;
  599. procedure TBGRAKnob.SetPositionWidth(const AValue: single);
  600. begin
  601. if FPositionWidth = AValue then
  602. exit;
  603. FPositionWidth := AValue;
  604. Invalidate;
  605. end;
  606. procedure TBGRAKnob.SetUsePhongLighting(const AValue: boolean);
  607. begin
  608. if FUsePhongLighting = AValue then
  609. exit;
  610. FUsePhongLighting := AValue;
  611. FreeAndNil(FKnobBmp);
  612. Invalidate;
  613. end;
  614. procedure TBGRAKnob.UpdateAngularPos(X, Y: integer);
  615. var
  616. FPreviousPos, Sign: single;
  617. prevAngle, currAngle: single;
  618. begin
  619. // Saves a previous position for the SlowSnap functionality.
  620. // Uses that to see how far we have moved to see if we should move
  621. FPreviousPos := FAngularPos;
  622. prevAngle := AngularPosToDeg(FAngularPos); // Need these in degrees!
  623. if FStartFromBottom then
  624. Sign := 1
  625. else
  626. Sign := -1;
  627. FAngularPos := ArcTan2((-Sign) * (Y - ClientHeight / 2) / ClientHeight,
  628. Sign * (X - ClientWidth / 2) / ClientWidth);
  629. currAngle := AngularPosToDeg(FAngularPos);
  630. // If sector mode then we need to translate angle to sector and back to simulate each sector
  631. if FKnobType = ktSector then
  632. FAngularPos := AngularPosSector(currAngle);
  633. ValueCorrection;
  634. // If SlowSnap mode make sure past the Min/Max angles before snapping.
  635. // This is less twitchy behavior near endpoints. This may not make sense
  636. // when in ktSector mode so skip if that
  637. if FSlowSnap and (FKnobType <> ktSector) then
  638. if ((currAngle <= FStartAngle) and (prevAngle = FEndAngle)) or
  639. ((CurrAngle >= FEndAngle) and (PrevAngle = FStartAngle)) then
  640. FAngularPos := FPreviousPos;
  641. Invalidate;
  642. if (FPreviousPos <> FAngularPos) and Assigned(FOnKnobValueChange) then
  643. FOnKnobValueChange(Self, Value); // Value passes back with data based on knobtype
  644. end;
  645. procedure TBGRAKnob.MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
  646. begin
  647. inherited MouseDown(Button, Shift, X, Y);
  648. if Button = mbLeft then
  649. begin
  650. FSettingAngularPos := True;
  651. UpdateAngularPos(X, Y);
  652. end;
  653. end;
  654. procedure TBGRAKnob.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: integer);
  655. begin
  656. inherited MouseUp(Button, Shift, X, Y);
  657. if Button = mbLeft then
  658. FSettingAngularPos := False;
  659. end;
  660. procedure TBGRAKnob.MouseMove(Shift: TShiftState; X, Y: integer);
  661. begin
  662. inherited MouseMove(Shift, X, Y);
  663. if FSettingAngularPos then
  664. UpdateAngularPos(X, Y);
  665. end;
  666. procedure TBGRAKnob.Paint;
  667. var
  668. Bmp: TBGRABitmap;
  669. Center, Pos: TPointF;
  670. PosColor: TBGRAPixel;
  671. PosLen: single;
  672. begin
  673. if (ClientWidth = 0) or (ClientHeight = 0) then
  674. exit;
  675. if FKnobBmp = nil then
  676. begin
  677. CreateKnobBmp;
  678. if FKnobBmp = nil then
  679. Exit;
  680. end;
  681. Bmp := TBGRABitmap.Create(ClientWidth, ClientHeight);
  682. Bmp.BlendImage(0, 0, FKnobBmp, boLinearBlend);
  683. // draw current position
  684. PosColor := ColorToBGRA(ColorToRGB(FPositionColor), FPositionOpacity);
  685. Center := PointF(ClientWidth / 2, ClientHeight / 2);
  686. Pos.X := Cos(FAngularPos) * (ClientWidth / 2);
  687. Pos.Y := -Sin(FAngularPos) * (ClientHeight / 2);
  688. if not FStartFromBottom then
  689. Pos := -Pos;
  690. PosLen := VectLen(Pos);
  691. Pos := Pos * ((PosLen - PositionMargin - FPositionWidth) / PosLen);
  692. Pos := Center + Pos;
  693. case PositionType of
  694. kptLineSquareCap:
  695. begin
  696. Bmp.LineCap := pecSquare;
  697. Bmp.DrawLineAntialias(Center.X, Center.Y, Pos.X, Pos.Y,
  698. PosColor, FPositionWidth);
  699. end;
  700. kptLineRoundCap:
  701. begin
  702. Bmp.LineCap := pecRound;
  703. Bmp.DrawLineAntialias(Center.X, Center.Y, Pos.X, Pos.Y,
  704. PosColor, FPositionWidth);
  705. end;
  706. kptFilledCircle:
  707. begin
  708. Bmp.FillEllipseAntialias(Pos.X, Pos.Y, FPositionWidth,
  709. FPositionWidth, PosColor);
  710. end;
  711. kptHollowCircle:
  712. begin
  713. Bmp.EllipseAntialias(Pos.X, Pos.Y, FPositionWidth * 2 / 3,
  714. FPositionWidth * 2 / 3, PosColor, FPositionWidth / 3);
  715. end;
  716. end;
  717. Bmp.Draw(Canvas, 0, 0, False);
  718. Bmp.Free;
  719. end;
  720. procedure TBGRAKnob.Resize;
  721. begin
  722. inherited Resize;
  723. if (FKnobBmp <> nil) and ((ClientWidth <> FKnobBmp.Width) or
  724. (ClientHeight <> FKnobBmp.Height)) then
  725. FreeAndNil(FKnobBmp);
  726. end;
  727. constructor TBGRAKnob.Create(AOwner: TComponent);
  728. begin
  729. inherited Create(AOwner);
  730. with GetControlClassDefaultSize do
  731. SetInitialBounds(0, 0, CX, CY);
  732. FPhong := TPhongShading.Create;
  733. FPhong.LightPositionZ := 100;
  734. FPhong.LightSourceIntensity := 300;
  735. FPhong.NegativeDiffusionFactor := 0.8;
  736. FPhong.AmbientFactor := 0.5;
  737. FPhong.DiffusionFactor := 0.6;
  738. FKnobBmp := nil;
  739. FCurveExponent := 0.2;
  740. FKnobColor := clBtnFace;
  741. FPositionColor := clBtnText;
  742. FPositionOpacity := 192;
  743. FPositionWidth := 4;
  744. FPositionMargin := 4;
  745. FPositionType := kptLineSquareCap;
  746. FTaperType := kttLinear; // Should be default for compatibility
  747. FUsePhongLighting := True;
  748. FOnKnobValueChange := nil;
  749. FStartFromBottom := True;
  750. FWheelSpeed := 0; // 0, no wheel, 1 slowest, 255 fastest
  751. FWheelWrap := False; // don't allow the mouse wheel to wrap around
  752. FSlowSnap := False; // True : less snap around on min/max
  753. FReverseScale := False; // Flips direction around if True
  754. FSectorDivisions := 1; // Number of divisions for sector knob, computed
  755. FKnobType := ktRange; // Defaults ranges to match orig knob
  756. FStartAngle := 30;
  757. FEndAngle := 330;
  758. FMinValue := 30;
  759. FMaxValue := 330;
  760. SetValue(30);
  761. end;
  762. destructor TBGRAKnob.Destroy;
  763. begin
  764. FPhong.Free;
  765. FKnobBmp.Free;
  766. inherited Destroy;
  767. end;
  768. {$IFDEF FPC}
  769. procedure TBGRAKnob.SaveToFile(AFileName: string);
  770. var
  771. AStream: TMemoryStream;
  772. begin
  773. AStream := TMemoryStream.Create;
  774. try
  775. WriteComponentAsTextToStream(AStream, Self);
  776. AStream.SaveToFile(AFileName);
  777. finally
  778. AStream.Free;
  779. end;
  780. end;
  781. procedure TBGRAKnob.LoadFromFile(AFileName: string);
  782. var
  783. AStream: TMemoryStream;
  784. begin
  785. AStream := TMemoryStream.Create;
  786. try
  787. AStream.LoadFromFile(AFileName);
  788. ReadComponentFromTextStream(AStream, TComponent(Self), OnFindClass);
  789. finally
  790. AStream.Free;
  791. end;
  792. end;
  793. {$ENDIF}
  794. procedure TBGRAKnob.OnFindClass(Reader: TReader; const AClassName: string;
  795. var ComponentClass: TComponentClass);
  796. begin
  797. if CompareText(AClassName, 'TBGRAKnob') = 0 then
  798. ComponentClass := TBGRAKnob;
  799. end;
  800. function TBGRAKnob.DoMouseWheel(Shift: TShiftState; WheelDelta: integer;
  801. MousePos: TPoint): boolean;
  802. begin
  803. Result := inherited DoMouseWheel(Shift, WheelDelta, MousePos);
  804. MouseWheelPos(Shift, WheelDelta);
  805. end;
  806. procedure TBGRAKnob.MouseWheelPos(Shift: TShiftState; WheelDelta: integer);
  807. var
  808. newValue: single;
  809. begin
  810. // WheelSpeed is a Base Value and a factor to slow or speed up the wheel affect.
  811. // FWheelSpeed = 0 then no wheel, 1 slowest movement, 255 fastest movement
  812. //
  813. // Note if Mouse Wheel is used in AudioSlow or AudioFast mode, the wheel
  814. // will not be compensated so will seem faster at 0 side, and slower as
  815. // it gets to the MaxValue since it's values curved. (assumes 0min, 100max)
  816. // Setting the wheel speed to a low value (like 1) will help these modes
  817. if FWheelSpeed > 0 then
  818. begin
  819. if FKnobType = ktRange then
  820. begin
  821. newValue := Value + (FMaxValue - FMinValue) * WheelDelta /
  822. ((WHEELSPEEDBASE - FWheelSpeed) * WHEELSPEEDFACTOR);
  823. // Check for wrap in either direction
  824. if FWheelWrap then
  825. begin
  826. if newValue > FMaxValue then
  827. newValue := FMinValue
  828. else
  829. if newValue < FMinValue then
  830. newValue := FMaxValue;
  831. end;
  832. end
  833. else
  834. begin
  835. // ktSector
  836. // Jumps are now always 1 or -1, in terms of sectors, note wheel speed
  837. // does not make any difference in ktSector mode since we can only bump 1/-1
  838. // value or it will rounded back to an integral value an not move
  839. if WheelDelta < 0 then
  840. begin
  841. // Move Backwards, check for wrap
  842. newValue := Value - 1.0;
  843. if newValue < FMinValue then
  844. begin
  845. if FWheelWrap then
  846. newValue := FMaxValue
  847. else
  848. newValue := FMinValue;
  849. end;
  850. end
  851. else
  852. begin
  853. // Move Forward, check for wrap
  854. newValue := Value + 1.0;
  855. if newValue >= FMaxValue then
  856. begin
  857. if FWheelWrap then
  858. newValue := FMinValue
  859. else
  860. newValue := FMaxValue;
  861. end;
  862. end;
  863. end;
  864. SetValue(newValue);
  865. end;
  866. if Assigned(FOnKnobValueChange) then
  867. FOnKnobValueChange(Self, Value);
  868. end;
  869. end.