GR32.Lines.Thick.pas 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. unit GR32.Lines.Thick;
  2. (* ***** BEGIN LICENSE BLOCK *****
  3. * Version: MPL 1.1 or LGPL 2.1 with linking exception
  4. *
  5. * The contents of this file are subject to the Mozilla Public License Version
  6. * 1.1 (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. * http://www.mozilla.org/MPL/
  9. *
  10. * Software distributed under the License is distributed on an "AS IS" basis,
  11. * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  12. * for the specific language governing rights and limitations under the
  13. * License.
  14. *
  15. * Alternatively, the contents of this file may be used under the terms of the
  16. * Free Pascal modified version of the GNU Lesser General Public License
  17. * Version 2.1 (the "FPC modified LGPL License"), in which case the provisions
  18. * of this license are applicable instead of those above.
  19. * Please see the file LICENSE.txt for additional information concerning this
  20. * license.
  21. *
  22. * The Original Code is Thick Lines for Graphics32
  23. *
  24. * The Initial Developer of the Original Code is
  25. * Anders Melander
  26. *
  27. * Portions created by the Initial Developer are Copyright (C) 2023
  28. * the Initial Developer. All Rights Reserved.
  29. *
  30. * ***** END LICENSE BLOCK ***** *)
  31. interface
  32. uses
  33. Math,
  34. GR32;
  35. //------------------------------------------------------------------------------
  36. //
  37. // DrawThickLine
  38. //
  39. //------------------------------------------------------------------------------
  40. // Aliased, opaque thick line.
  41. // For anti-aliased & alpha blended lines use TCanvas32 instead.
  42. //------------------------------------------------------------------------------
  43. //
  44. // Draws a thick line using a modified Bresenham algorithm.
  45. //
  46. // We basicall draw a thick line by drawing a number of single-pixel width
  47. // lines, one pixel apart. We do this with a Bresenham loop inside a Bresenham
  48. // loop. The inner loop draws a line and the outer loop moves the line end-points.
  49. //
  50. // For a very similar algorithm see:
  51. //
  52. // - Line Thickening by Modification To Bresenham's Algorithm
  53. // Alan S. Murphy
  54. // IBM Technical Disclosure Bulletin, Vol. 20, No. 12, May 1978.
  55. // http://www.zoo.co.uk/murphy/thickline/
  56. // http://homepages.enterprise.net/murphy/thickline/index.html
  57. //
  58. // See the following for a good explanation of the above:
  59. // - http://kt8216.unixcab.org/murphy/index.html
  60. //
  61. // Murphy's algorithm above moves along the base line and draw lines
  62. // perpendicular to the base line.
  63. // This version however draw lines parallel to the base line.
  64. // This implementation was adapted from code by
  65. // - Armin Joachimsmeyer, Copyright (C) 2013-2022
  66. //
  67. // Missing features:
  68. // - Anti-aliasing.
  69. // - Blending
  70. // - Clipping
  71. //
  72. //------------------------------------------------------------------------------
  73. type
  74. TLineWidthMode = (
  75. LineWidthMiddle, // Start point is on the line at center of the thick line
  76. LineWidthDrawClockWise, // Start point is on the counter-clockwise border line
  77. LineWidthDrawCounterClockwise // Start point is on the clockwise border line
  78. );
  79. procedure DrawThickLine(Bitmap: TBitmap32; StartX, StartY, EndX, EndY: integer; Width: SmallInt; Color: TColor32;
  80. WidthMode: TLineWidthMode = LineWidthMiddle);
  81. //------------------------------------------------------------------------------
  82. //------------------------------------------------------------------------------
  83. //------------------------------------------------------------------------------
  84. implementation
  85. type
  86. TLineOverlap = set of (
  87. // None: No line overlap, like in standard Bresenham
  88. LineOverlapMajor, // First go major then minor direction. Pixel is drawn as extension after actual line
  89. LineOverlapMinor // First go minor then major direction. Pixel is drawn as extension before next line
  90. );
  91. //------------------------------------------------------------------------------
  92. //
  93. // InnerDrawThickLine
  94. //
  95. //------------------------------------------------------------------------------
  96. procedure InnerDrawThickLine(Bitmap: TBitmap32; StartX, StartY, EndX, EndY: integer; Overlap: TLineOverlap; Color: TColor32);
  97. var
  98. DeltaX, DeltaY, TwoDeltaX, TwoDeltaY: SmallInt;
  99. Error: SmallInt;
  100. StepX, StepY: SmallInt;
  101. begin
  102. if (StartX = EndX) then
  103. begin
  104. if (StartY < EndY) then
  105. Bitmap.VertLineS(StartX, StartY, EndY, Color)
  106. else
  107. Bitmap.VertLineS(StartX, EndY, StartY, Color);
  108. end else
  109. if (StartY = EndY) then
  110. begin
  111. if (StartX < EndX) then
  112. Bitmap.HorzLineS(StartX, StartY, EndX, Color)
  113. else
  114. Bitmap.HorzLineS(StartX, EndX, StartY, Color);
  115. end else
  116. begin
  117. { calculate direction }
  118. DeltaX := EndX - StartX;
  119. DeltaY := EndY - StartY;
  120. if DeltaX < 0 then
  121. begin
  122. DeltaX := -DeltaX;
  123. StepX := -1;
  124. end else
  125. StepX := 1;
  126. if DeltaY < 0 then
  127. begin
  128. DeltaY := -DeltaY;
  129. StepY := -1;
  130. end else
  131. StepY := 1;
  132. TwoDeltaX := DeltaX * 2;
  133. TwoDeltaY := DeltaY * 2;
  134. { draw start pixel }
  135. Bitmap.PixelS[StartX, StartY] := Color;
  136. if DeltaX > DeltaY then
  137. begin
  138. { start value represents a half step in Y direction }
  139. Error := TwoDeltaY - DeltaX;
  140. while (StartX <> EndX) do
  141. begin
  142. { step in main direction }
  143. Inc(StartX, StepX);
  144. if (Error >= 0) then
  145. begin
  146. if (LineOverlapMajor in Overlap) then
  147. { draw pixel in major direction before changing }
  148. Bitmap.PixelS[StartX, StartY] := Color;
  149. { change Y }
  150. Inc(StartY, StepY);
  151. if (LineOverlapMinor in Overlap) then
  152. { draw pixel in minor direction before changing }
  153. Bitmap.PixelS[StartX - StepX, StartY] := Color;
  154. Dec(Error, TwoDeltaX);
  155. end;
  156. Inc(Error, TwoDeltaY);
  157. Bitmap.PixelS[StartX, StartY] := Color;
  158. end;
  159. end else
  160. begin
  161. { start value represents a half step in X direction }
  162. Error := TwoDeltaX - DeltaY;
  163. while (StartY <> EndY) do
  164. begin
  165. Inc(StartY, StepY);
  166. if (Error >= 0) then
  167. begin
  168. if (LineOverlapMajor in Overlap) then
  169. { draw pixel in major direction before changing }
  170. Bitmap.PixelS[StartX, StartY] := Color;
  171. { change X }
  172. Inc(StartX, StepX);
  173. if (LineOverlapMinor in Overlap) then
  174. { draw pixel in minor direction before changing }
  175. Bitmap.PixelS[StartX, StartY - StepY] := Color;
  176. Dec(Error, TwoDeltaY);
  177. end;
  178. Inc(Error, TwoDeltaX);
  179. Bitmap.PixelS[StartX, StartY] := Color;
  180. end;
  181. end;
  182. end;
  183. end;
  184. //------------------------------------------------------------------------------
  185. //
  186. // InnerDrawThickLine
  187. //
  188. //------------------------------------------------------------------------------
  189. procedure DrawThickLine(Bitmap: TBitmap32; StartX, StartY, EndX, EndY: integer; Width: SmallInt; Color: TColor32; WidthMode: TLineWidthMode = LineWidthMiddle);
  190. var
  191. i: integer;
  192. DeltaX, DeltaY, TwoDeltaX, TwoDeltaY: SmallInt;
  193. Error: SmallInt;
  194. StepX, StepY: SmallInt;
  195. MirrorQuadrant: Boolean;
  196. Overlap: TLineOverlap;
  197. DrawStartAdjustCount: integer;
  198. begin
  199. if (StartX = EndX) and (StartY = EndY) then
  200. exit;
  201. DeltaY := EndX - StartX;
  202. DeltaX := EndY - StartY;
  203. // Since we're not drawing anti-aliased we have to adjust the width for the
  204. // angle of the line. Otherwise diagonal lines would be wider (by Sqrt(2))
  205. // than straight lines.
  206. // Note:
  207. // - Even though were executing a costly Sqrt and float division,
  208. // the adjustment actually makes the routine faster on average since we're
  209. // potentially reducing the number of lines drawn.
  210. // - We're using Ceil instead of Trunc or Round to make the width better
  211. // match the width of a GDI thick line.
  212. Width := Ceil(Width * Hypot(DeltaX, DeltaY) / (Abs(DeltaX) + Abs(DeltaY)));
  213. if (Width <= 1) then
  214. begin
  215. InnerDrawThickLine(Bitmap, StartX, StartY, EndX, EndY, [], Color);
  216. exit;
  217. end;
  218. // Bresenham's algorithm only works in quadrant 1, so mirror 4 quadrants to one
  219. // and adjust deltas and stepping direction.
  220. // Make sure we are in quadrant 1 or 4
  221. MirrorQuadrant := True;
  222. if (DeltaX < 0) then
  223. begin
  224. DeltaX := -DeltaX;
  225. StepX := -1;
  226. MirrorQuadrant := not MirrorQuadrant;
  227. end else
  228. StepX := 1;
  229. // Make sure we are in quadrant 1
  230. if (DeltaY < 0) then
  231. begin
  232. DeltaY := -DeltaY;
  233. StepY := -1;
  234. MirrorQuadrant := not MirrorQuadrant;
  235. end else
  236. StepY := 1;
  237. // Now Delta* are positive and Step* define the direction.
  238. // MirrorQuadrant is False if we mirrored only once.
  239. TwoDeltaX := DeltaX * 2;
  240. TwoDeltaY := DeltaY * 2;
  241. // Adjust for right direction of thickness from line origin
  242. case WidthMode of
  243. LineWidthMiddle:
  244. DrawStartAdjustCount := Width div 2;
  245. LineWidthDrawCounterClockwise:
  246. DrawStartAdjustCount := Width - 1;
  247. LineWidthDrawClockWise:
  248. DrawStartAdjustCount := 0
  249. else
  250. DrawStartAdjustCount := 0; // Shut compiler up
  251. end;
  252. // Which octant are we now?
  253. if (DeltaX >= DeltaY) then
  254. begin
  255. // Octant 1, 3, 5, 7 (between 0 and 45, 90 and 135, etc. degree)
  256. if (MirrorQuadrant) then
  257. begin
  258. DrawStartAdjustCount := (Width - 1) - DrawStartAdjustCount;
  259. StepY := -StepY;
  260. end else
  261. StepX := -StepX;
  262. // Vector for draw direction of the starting points of lines is perpendicular
  263. // and counter-clockwise to main line direction.
  264. // Therefore no pixel will be missed if LineOverlapMajor is used on change
  265. // in minor perpendicular direction.
  266. // Adjust draw start point
  267. Error := TwoDeltaY - DeltaX;
  268. for i := 0 to DrawStartAdjustCount-1 do
  269. begin
  270. // Change X (main direction here)
  271. Dec(StartX, StepX);
  272. Dec(EndX, StepX);
  273. // Change Y
  274. if (Error >= 0) then
  275. begin
  276. Dec(StartY, StepY);
  277. Dec(EndY, StepY);
  278. Dec(Error, TwoDeltaX);
  279. end;
  280. Inc(Error, TwoDeltaY);
  281. end;
  282. // Draw start line.
  283. InnerDrawThickLine(Bitmap, StartX, StartY, EndX, EndY, [], Color);
  284. // Draw Width-1 number of lines (-1 because we have already drawn one)
  285. Error := TwoDeltaY - DeltaX;
  286. for i := 0 to Width-2 do
  287. begin
  288. // Change X (main direction here)
  289. Inc(StartX, StepX);
  290. Inc(EndX, StepX);
  291. Overlap := [];
  292. // Change Y
  293. if (Error >= 0) then
  294. begin
  295. Inc(StartY, StepY);
  296. Inc(EndY, StepY);
  297. Dec(Error, TwoDeltaX);
  298. Overlap := [LineOverlapMajor];
  299. end;
  300. Inc(Error, TwoDeltaY);
  301. InnerDrawThickLine(Bitmap, StartX, StartY, EndX, EndY, Overlap, Color);
  302. end;
  303. end else
  304. begin
  305. // Octant 2, 4, 6, 8 (between 45 and 90, 135 and 180, etc. degree)
  306. if (MirrorQuadrant) then
  307. StepX := -StepX
  308. else
  309. begin
  310. DrawStartAdjustCount := (Width - 1) - DrawStartAdjustCount;
  311. StepY := -StepY;
  312. end;
  313. // Adjust draw start point
  314. Error := TwoDeltaX - DeltaY;
  315. for i := 0 to DrawStartAdjustCount-1 do
  316. begin
  317. Dec(StartY, StepY);
  318. Dec(EndY, StepY);
  319. if (Error >= 0) then
  320. begin
  321. Dec(StartX, StepX);
  322. Dec(EndX, StepX);
  323. Dec(Error, TwoDeltaY);
  324. end;
  325. Inc(Error, TwoDeltaX);
  326. end;
  327. // Draw start line.
  328. InnerDrawThickLine(Bitmap, StartX, StartY, EndX, EndY, [], Color);
  329. // Draw Width-1 number of lines (-1 because we have already drawn one)
  330. Error := TwoDeltaX - DeltaY;
  331. for i := 0 to Width-2 do
  332. begin
  333. // Change Y (main direction here)
  334. Inc(StartY, StepY);
  335. Inc(EndY, StepY);
  336. Overlap := [];
  337. // Change X
  338. if (Error >= 0) then
  339. begin
  340. Inc(StartX, StepX);
  341. Inc(EndX, StepX);
  342. Dec(Error, TwoDeltaY);
  343. Overlap := [LineOverlapMajor];
  344. end;
  345. Inc(Error, TwoDeltaX);
  346. InnerDrawThickLine(Bitmap, StartX, StartY, EndX, EndY, Overlap, Color);
  347. end;
  348. end;
  349. end;
  350. end.