bgraknob.pas 26 KB

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