EllipseHelper.cs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. using ChunkyImageLib.DataHolders;
  2. using Drawie.Backend.Core.Numerics;
  3. using Drawie.Backend.Core.Surfaces.Vector;
  4. using Drawie.Numerics;
  5. namespace ChunkyImageLib.Operations;
  6. public class EllipseHelper
  7. {
  8. /// <summary>
  9. /// Separates the ellipse's inner area into a bunch of horizontal lines and one big rectangle for drawing.
  10. /// </summary>
  11. public static (List<VecI> lines, RectI rect) SplitEllipseFillIntoRegions(IReadOnlyList<VecI> ellipse,
  12. RectI ellipseBounds)
  13. {
  14. if (ellipse.Count == 0)
  15. return (new(), RectI.Empty);
  16. List<VecI> lines = new();
  17. VecD ellipseCenter = ellipseBounds.Center;
  18. VecD inscribedRectSize = ellipseBounds.Size * Math.Sqrt(2) / 2;
  19. inscribedRectSize.X -= 2;
  20. inscribedRectSize.Y -= 2;
  21. RectI inscribedRect = (RectI)RectD.FromCenterAndSize(ellipseCenter, inscribedRectSize).RoundInwards();
  22. if (inscribedRect.IsZeroOrNegativeArea)
  23. inscribedRect = RectI.Empty;
  24. bool[] added = new bool[ellipseBounds.Height];
  25. for (var i = 0; i < ellipse.Count; i++)
  26. {
  27. var point = ellipse[i];
  28. if (!added[point.Y - ellipseBounds.Top] &&
  29. i > 0 &&
  30. ellipse[i - 1].Y == point.Y &&
  31. point.X - ellipse[i - 1].X > 1 &&
  32. point.Y > ellipseBounds.Top &&
  33. point.Y < ellipseBounds.Bottom - 1)
  34. {
  35. int fromX = ellipse[i - 1].X + 1;
  36. int toX = point.X;
  37. int y = ellipse[i - 1].Y;
  38. added[point.Y - ellipseBounds.Top] = true;
  39. if (y >= inscribedRect.Top && y < inscribedRect.Bottom)
  40. {
  41. lines.Add(new VecI(fromX, y));
  42. lines.Add(new VecI(inscribedRect.Left, y));
  43. lines.Add(new VecI(inscribedRect.Right, y));
  44. lines.Add(new VecI(toX, y));
  45. }
  46. else
  47. {
  48. lines.Add(new VecI(fromX, y));
  49. lines.Add(new VecI(toX, y));
  50. }
  51. }
  52. }
  53. return (lines, inscribedRect);
  54. }
  55. /// <summary>
  56. /// Splits the ellipse into a bunch of horizontal lines.
  57. /// The resulting list contains consecutive pairs of <see cref="VecI"/>s, each pair has one for the start of the line and one for the end.
  58. /// </summary>
  59. public static List<VecI> SplitEllipseIntoLines(HashSet<VecI> ellipse)
  60. {
  61. List<VecI> lines = new();
  62. var sorted = ellipse.OrderBy(
  63. a => a,
  64. Comparer<VecI>.Create((a, b) => a.Y != b.Y ? a.Y - b.Y : a.X - b.X)
  65. );
  66. int minX = int.MaxValue;
  67. int maxX = int.MinValue;
  68. VecI? prev = null;
  69. foreach (var point in sorted)
  70. {
  71. if (prev.HasValue && point.Y != prev.Value.Y)
  72. {
  73. int prevY = prev.Value.Y;
  74. lines.Add(new(minX, prevY));
  75. lines.Add(new(maxX, prevY));
  76. minX = int.MaxValue;
  77. maxX = int.MinValue;
  78. }
  79. minX = Math.Min(point.X, minX);
  80. maxX = Math.Max(point.X, maxX);
  81. prev = point;
  82. }
  83. if (prev != null)
  84. {
  85. lines.Add(new(minX, prev.Value.Y));
  86. lines.Add(new(maxX, prev.Value.Y));
  87. }
  88. return lines;
  89. }
  90. public static HashSet<VecI> GenerateEllipseFromRect(RectI rect, double rotationRad = 0)
  91. {
  92. if (rect.IsZeroOrNegativeArea)
  93. return new();
  94. float radiusX = (rect.Width - 1) / 2.0f;
  95. float radiusY = (rect.Height - 1) / 2.0f;
  96. if (rotationRad == 0)
  97. return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
  98. return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
  99. }
  100. /// <summary>
  101. /// Draws an ellipse using it's center and radii
  102. ///
  103. /// Here is a usage example:
  104. /// Let's say you want an ellipse that's 3 pixels wide and 3 pixels tall located in the top right corner of the canvas
  105. /// It's center is at (1.5; 1.5). That's in the middle of a pixel
  106. /// The radii are both equal to 1. Notice that it's 1 and not 1.5, since we want the ellipse to land in the middle of the pixel, not outside of it.
  107. /// See desmos (note the inverted y axis): https://www.desmos.com/calculator/tq9uqg0hcq
  108. ///
  109. /// Another example:
  110. /// 4x4 ellipse in the top right corner of the canvas
  111. /// Center is at (2; 2). It's a place where 4 pixels meet
  112. /// Both radii are 1.5. Making them 2 would make the ellipse touch the edges of pixels, whereas we want it to stay in the middle
  113. /// </summary>
  114. public static HashSet<VecI> GenerateMidpointEllipse(
  115. double halfWidth,
  116. double halfHeight,
  117. double centerX,
  118. double centerY,
  119. HashSet<VecI>? listToFill = null)
  120. {
  121. listToFill ??= new HashSet<VecI>();
  122. if (halfWidth < 1 || halfHeight < 1)
  123. {
  124. AddFallbackRectangle(halfWidth, halfHeight, centerX, centerY, listToFill);
  125. return listToFill;
  126. }
  127. // ellipse formula: halfHeight^2 * x^2 + halfWidth^2 * y^2 - halfHeight^2 * halfWidth^2 = 0
  128. // Make sure we are always at the center of a pixel
  129. double currentX = Math.Ceiling(centerX - 0.5) + 0.5;
  130. double currentY = centerY + halfHeight;
  131. double currentSlope;
  132. // from PI/2 to PI/4
  133. do
  134. {
  135. AddRegionPoints(listToFill, currentX, centerX, currentY, centerY);
  136. // calculate next pixel coords
  137. currentX++;
  138. if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX, 2)) +
  139. (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY - 0.5, 2)) -
  140. (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) >= 0)
  141. {
  142. currentY--;
  143. }
  144. // calculate how far we've advanced
  145. double derivativeX = 2 * Math.Pow(halfHeight, 2) * (currentX - centerX);
  146. double derivativeY = 2 * Math.Pow(halfWidth, 2) * (currentY - centerY);
  147. currentSlope = -(derivativeX / derivativeY);
  148. } while (currentSlope > -1 && currentY - centerY > 0.5);
  149. // from PI/4 to 0
  150. while (currentY - centerY >= 0)
  151. {
  152. AddRegionPoints(listToFill, currentX, centerX, currentY, centerY);
  153. currentY--;
  154. if ((Math.Pow(halfHeight, 2) * Math.Pow(currentX - centerX + 0.5, 2)) +
  155. (Math.Pow(halfWidth, 2) * Math.Pow(currentY - centerY, 2)) -
  156. (Math.Pow(halfWidth, 2) * Math.Pow(halfHeight, 2)) < 0)
  157. {
  158. currentX++;
  159. }
  160. }
  161. return listToFill;
  162. }
  163. // This function works, but honestly Skia produces better results, and it doesn't require so much
  164. // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
  165. // make it better, and it will be useful.
  166. // Desmos with all the math https://www.desmos.com/calculator/m9lgg7s9zu
  167. private static HashSet<VecI> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX,
  168. double centerY, double rotationRad)
  169. {
  170. var listToFill = new HashSet<VecI>();
  171. // formula ((x - h)cos(tetha) + (y - k)sin(tetha))^2 / a^2 + (-(x-h)sin(tetha)+(y-k)cos(tetha))^2 / b^2 = 1
  172. //double topMostTetha = GetTopMostAlpha(halfWidth, halfHeight, rotationRad);
  173. //VecD possiblyTopmostPoint = GetTethaPoint(topMostTetha, halfWidth, halfHeight, rotationRad);
  174. //VecD possiblyMinPoint = GetTethaPoint(topMostTetha + Math.PI, halfWidth, halfHeight, rotationRad);
  175. // less than, because y grows downwards
  176. //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
  177. //rotationRad = double.Round(rotationRad, 1);
  178. double currentTetha = 0;
  179. double tethaStep = 0.001;
  180. VecI[] lastPoints = new VecI[2];
  181. do
  182. {
  183. VecD point = GetTethaPoint(currentTetha, halfWidth, halfHeight, rotationRad);
  184. VecI floored = new((int)Math.Floor(point.X + centerX), (int)Math.Floor(point.Y + centerY));
  185. AddPoint(listToFill, floored, lastPoints);
  186. currentTetha += tethaStep;
  187. } while (currentTetha < Math.PI * 2);
  188. return listToFill;
  189. }
  190. private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
  191. {
  192. if(!listToFill.Add(floored)) return;
  193. if (lastPoints[0] == default)
  194. {
  195. lastPoints[0] = floored;
  196. return;
  197. }
  198. if (lastPoints[1] == default)
  199. {
  200. lastPoints[1] = floored;
  201. return;
  202. }
  203. if (IsLShape(lastPoints, floored))
  204. {
  205. listToFill.Remove(lastPoints[1]);
  206. lastPoints[0] = floored;
  207. lastPoints[1] = default;
  208. return;
  209. }
  210. lastPoints[0] = lastPoints[1];
  211. lastPoints[1] = floored;
  212. }
  213. private static bool IsLShape(VecI[] points, VecI third)
  214. {
  215. VecI first = points[0];
  216. VecI second = points[1];
  217. return first.X != third.X && first.Y != third.Y && (second - first).TaxicabLength == 1 &&
  218. (second - third).TaxicabLength == 1;
  219. }
  220. private static bool IsInsideEllipse(double x, double y, double centerX, double centerY, double halfWidth,
  221. double halfHeight, double rotationRad)
  222. {
  223. double lhs = Math.Pow(x * Math.Cos(rotationRad) + y * Math.Sin(rotationRad), 2) / Math.Pow(halfWidth, 2);
  224. double rhs = Math.Pow(-x * Math.Sin(rotationRad) + y * Math.Cos(rotationRad), 2) / Math.Pow(halfHeight, 2);
  225. return lhs + rhs <= 1;
  226. }
  227. private static VecD GetDerivative(double x, double halfWidth, double halfHeight, double rotationRad, double tetha)
  228. {
  229. double xDerivative = halfWidth * Math.Cos(tetha) * Math.Cos(rotationRad) -
  230. halfHeight * Math.Sin(tetha) * Math.Sin(rotationRad);
  231. double yDerivative = halfWidth * Math.Cos(tetha) * Math.Sin(rotationRad) +
  232. halfHeight * Math.Sin(tetha) * Math.Cos(rotationRad);
  233. return new VecD(xDerivative, yDerivative);
  234. }
  235. private static VecD GetTethaPoint(double alpha, double halfWidth, double halfHeight, double rotation)
  236. {
  237. double x =
  238. (halfWidth * Math.Cos(alpha) * Math.Cos(rotation) - halfHeight * Math.Sin(alpha) * Math.Sin(rotation));
  239. double y = halfWidth * Math.Cos(alpha) * Math.Sin(rotation) + halfHeight * Math.Sin(alpha) * Math.Cos(rotation);
  240. return new VecD(x, y);
  241. }
  242. private static double GetTopMostAlpha(double halfWidth, double halfHeight, double rotationRad)
  243. {
  244. if (rotationRad == 0)
  245. return 0;
  246. double tethaRot = Math.Cos(rotationRad) / Math.Sin(rotationRad);
  247. return Math.Atan((halfHeight * tethaRot) / halfWidth);
  248. }
  249. private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY,
  250. HashSet<VecI> coordinates)
  251. {
  252. int left = (int)Math.Floor(centerX - halfWidth);
  253. int top = (int)Math.Floor(centerY - halfHeight);
  254. int right = (int)Math.Floor(centerX + halfWidth);
  255. int bottom = (int)Math.Floor(centerY + halfHeight);
  256. for (int x = left; x <= right; x++)
  257. {
  258. coordinates.Add(new VecI(x, top));
  259. if (top != bottom)
  260. coordinates.Add(new VecI(x, bottom));
  261. }
  262. for (int y = top + 1; y < bottom; y++)
  263. {
  264. coordinates.Add(new VecI(left, y));
  265. if (left != right)
  266. coordinates.Add(new VecI(right, y));
  267. }
  268. }
  269. private static void AddRegionPoints(HashSet<VecI> coordinates, double x, double xc, double y, double yc)
  270. {
  271. int xFloor = (int)Math.Floor(x);
  272. int yFloor = (int)Math.Floor(y);
  273. int xFloorInv = (int)Math.Floor(-x + 2 * xc);
  274. int yFloorInv = (int)Math.Floor(-y + 2 * yc);
  275. //top and bottom or left and right
  276. if (xFloor == xFloorInv || yFloor == yFloorInv)
  277. {
  278. coordinates.Add(new VecI(xFloorInv, yFloorInv));
  279. coordinates.Add(new VecI(xFloor, yFloor));
  280. }
  281. //part of the arc
  282. else
  283. {
  284. coordinates.Add(new VecI(xFloorInv, yFloor));
  285. coordinates.Add(new VecI(xFloor, yFloor));
  286. coordinates.Add(new VecI(xFloorInv, yFloorInv));
  287. coordinates.Add(new VecI(xFloor, yFloorInv));
  288. }
  289. }
  290. public static VectorPath GenerateEllipseVectorFromRect(RectI location)
  291. {
  292. VectorPath path = new();
  293. path.AddOval(location);
  294. path.Close();
  295. return path;
  296. }
  297. }