EllipseHelper.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. using ChunkyImageLib.DataHolders;
  2. using Drawie.Backend.Core.Numerics;
  3. using Drawie.Backend.Core.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. /// <summary>
  164. /// Constructs pixel-perfect ellipse outline represented as a vector path.
  165. /// This function is quite heavy, for less precise but faster results use <see cref="GenerateEllipseVectorFromRect"/>.
  166. /// </summary>
  167. /// <param name="rectangle">The rectangle that the ellipse should fit into.</param>
  168. /// <returns>A vector path that represents an ellipse outline.</returns>
  169. public static VectorPath ConstructEllipseOutline(RectI rectangle)
  170. {
  171. if (EllipseCache.Ellipses.TryGetValue(rectangle.Size, out var cachedPath))
  172. {
  173. VectorPath finalPath = new(cachedPath);
  174. finalPath.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
  175. return finalPath;
  176. }
  177. if (rectangle.Width < 3 || rectangle.Height < 3)
  178. {
  179. VectorPath rectPath = new();
  180. rectPath.AddRect((RectD)rectangle);
  181. return rectPath;
  182. }
  183. if (rectangle is { Width: 3, Height: 3 })
  184. {
  185. return CreateThreePixelCircle((VecI)rectangle.Center);
  186. }
  187. var center = rectangle.Size / 2d;
  188. RectI rect = new RectI(0, 0, rectangle.Width, rectangle.Height);
  189. var points = GenerateEllipseFromRect(rect, 0).ToList();
  190. points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
  191. List<VecI> finalPoints = new();
  192. for (int i = 0; i < points.Count; i++)
  193. {
  194. VecI prev = points[Mod(i - 1, points.Count)];
  195. VecI point = points[i];
  196. VecI next = points[Mod(i + 1, points.Count)];
  197. bool atBottom = point.Y >= center.Y;
  198. bool onRight = point.X >= center.X;
  199. if (atBottom)
  200. {
  201. if (onRight)
  202. {
  203. if (prev.Y != point.Y)
  204. finalPoints.Add(new(point.X + 1, point.Y));
  205. finalPoints.Add(new(point.X + 1, point.Y + 1));
  206. if (next.X != point.X)
  207. finalPoints.Add(new(point.X, point.Y + 1));
  208. }
  209. else
  210. {
  211. if (prev.X != point.X)
  212. finalPoints.Add(new(point.X + 1, point.Y + 1));
  213. finalPoints.Add(new(point.X, point.Y + 1));
  214. if (next.Y != point.Y)
  215. finalPoints.Add(point);
  216. }
  217. }
  218. else
  219. {
  220. if (onRight)
  221. {
  222. if (prev.X != point.X)
  223. finalPoints.Add(point);
  224. finalPoints.Add(new(point.X + 1, point.Y));
  225. if (next.Y != point.Y)
  226. finalPoints.Add(new(point.X + 1, point.Y + 1));
  227. }
  228. else
  229. {
  230. if (prev.Y != point.Y)
  231. finalPoints.Add(new(point.X, point.Y + 1));
  232. finalPoints.Add(point);
  233. if (next.X != point.X)
  234. finalPoints.Add(new(point.X + 1, point.Y));
  235. }
  236. }
  237. }
  238. VectorPath path = new();
  239. path.MoveTo(new VecF(finalPoints[0].X, finalPoints[0].Y));
  240. for (var index = 1; index < finalPoints.Count; index++)
  241. {
  242. var point = finalPoints[index];
  243. path.LineTo(new VecF(point.X, point.Y));
  244. }
  245. path.Close();
  246. EllipseCache.Ellipses[rectangle.Size] = new VectorPath(path);
  247. path.Transform(Matrix3X3.CreateTranslation(rectangle.TopLeft.X, rectangle.TopLeft.Y));
  248. return path;
  249. }
  250. public static VectorPath CreateThreePixelCircle(VecI rectanglePos)
  251. {
  252. var path = new VectorPath();
  253. path.MoveTo(new VecF(0, 0));
  254. path.LineTo(new VecF(0, -1));
  255. path.LineTo(new VecF(1, -1));
  256. path.LineTo(new VecF(1, 0));
  257. path.LineTo(new VecF(2, 0));
  258. path.LineTo(new VecF(2, 1));
  259. path.LineTo(new VecF(2, 1));
  260. path.LineTo(new VecF(1, 1));
  261. path.LineTo(new VecF(1, 2));
  262. path.LineTo(new VecF(0, 2));
  263. path.LineTo(new VecF(0, 1));
  264. path.LineTo(new VecF(-1, 1));
  265. path.LineTo(new VecF(-1, 0));
  266. path.Close();
  267. path.Transform(Matrix3X3.CreateTranslation(rectanglePos.X, rectanglePos.Y));
  268. return path;
  269. }
  270. private static int Mod(int x, int m) => (x % m + m) % m;
  271. // This function works, but honestly Skia produces better results, and it doesn't require so much
  272. // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
  273. // make it better, and it will be useful.
  274. // Desmos with all the math https://www.desmos.com/calculator/m9lgg7s9zu
  275. private static HashSet<VecI> GenerateMidpointEllipse(double halfWidth, double halfHeight, double centerX,
  276. double centerY, double rotationRad)
  277. {
  278. var listToFill = new HashSet<VecI>();
  279. // formula ((x - h)cos(tetha) + (y - k)sin(tetha))^2 / a^2 + (-(x-h)sin(tetha)+(y-k)cos(tetha))^2 / b^2 = 1
  280. //double topMostTetha = GetTopMostAlpha(halfWidth, halfHeight, rotationRad);
  281. //VecD possiblyTopmostPoint = GetTethaPoint(topMostTetha, halfWidth, halfHeight, rotationRad);
  282. //VecD possiblyMinPoint = GetTethaPoint(topMostTetha + Math.PI, halfWidth, halfHeight, rotationRad);
  283. // less than, because y grows downwards
  284. //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
  285. //rotationRad = double.Round(rotationRad, 1);
  286. double currentTetha = 0;
  287. double tethaStep = 0.001;
  288. VecI[] lastPoints = new VecI[2];
  289. do
  290. {
  291. VecD point = GetTethaPoint(currentTetha, halfWidth, halfHeight, rotationRad);
  292. VecI floored = new((int)Math.Floor(point.X + centerX), (int)Math.Floor(point.Y + centerY));
  293. AddPoint(listToFill, floored, lastPoints);
  294. currentTetha += tethaStep;
  295. } while (currentTetha < Math.PI * 2);
  296. return listToFill;
  297. }
  298. private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
  299. {
  300. if (!listToFill.Add(floored)) return;
  301. if (lastPoints[0] == default)
  302. {
  303. lastPoints[0] = floored;
  304. return;
  305. }
  306. if (lastPoints[1] == default)
  307. {
  308. lastPoints[1] = floored;
  309. return;
  310. }
  311. if (IsLShape(lastPoints, floored))
  312. {
  313. listToFill.Remove(lastPoints[1]);
  314. lastPoints[0] = floored;
  315. lastPoints[1] = default;
  316. return;
  317. }
  318. lastPoints[0] = lastPoints[1];
  319. lastPoints[1] = floored;
  320. }
  321. private static bool IsLShape(VecI[] points, VecI third)
  322. {
  323. VecI first = points[0];
  324. VecI second = points[1];
  325. return first.X != third.X && first.Y != third.Y && (second - first).TaxicabLength == 1 &&
  326. (second - third).TaxicabLength == 1;
  327. }
  328. private static bool IsInsideEllipse(double x, double y, double centerX, double centerY, double halfWidth,
  329. double halfHeight, double rotationRad)
  330. {
  331. double lhs = Math.Pow(x * Math.Cos(rotationRad) + y * Math.Sin(rotationRad), 2) / Math.Pow(halfWidth, 2);
  332. double rhs = Math.Pow(-x * Math.Sin(rotationRad) + y * Math.Cos(rotationRad), 2) / Math.Pow(halfHeight, 2);
  333. return lhs + rhs <= 1;
  334. }
  335. private static VecD GetDerivative(double x, double halfWidth, double halfHeight, double rotationRad, double tetha)
  336. {
  337. double xDerivative = halfWidth * Math.Cos(tetha) * Math.Cos(rotationRad) -
  338. halfHeight * Math.Sin(tetha) * Math.Sin(rotationRad);
  339. double yDerivative = halfWidth * Math.Cos(tetha) * Math.Sin(rotationRad) +
  340. halfHeight * Math.Sin(tetha) * Math.Cos(rotationRad);
  341. return new VecD(xDerivative, yDerivative);
  342. }
  343. private static VecD GetTethaPoint(double alpha, double halfWidth, double halfHeight, double rotation)
  344. {
  345. double x =
  346. (halfWidth * Math.Cos(alpha) * Math.Cos(rotation) - halfHeight * Math.Sin(alpha) * Math.Sin(rotation));
  347. double y = halfWidth * Math.Cos(alpha) * Math.Sin(rotation) + halfHeight * Math.Sin(alpha) * Math.Cos(rotation);
  348. return new VecD(x, y);
  349. }
  350. private static double GetTopMostAlpha(double halfWidth, double halfHeight, double rotationRad)
  351. {
  352. if (rotationRad == 0)
  353. return 0;
  354. double tethaRot = Math.Cos(rotationRad) / Math.Sin(rotationRad);
  355. return Math.Atan((halfHeight * tethaRot) / halfWidth);
  356. }
  357. private static void AddFallbackRectangle(double halfWidth, double halfHeight, double centerX, double centerY,
  358. HashSet<VecI> coordinates)
  359. {
  360. int left = (int)Math.Floor(centerX - halfWidth);
  361. int top = (int)Math.Floor(centerY - halfHeight);
  362. int right = (int)Math.Floor(centerX + halfWidth);
  363. int bottom = (int)Math.Floor(centerY + halfHeight);
  364. for (int x = left; x <= right; x++)
  365. {
  366. coordinates.Add(new VecI(x, top));
  367. if (top != bottom)
  368. coordinates.Add(new VecI(x, bottom));
  369. }
  370. for (int y = top + 1; y < bottom; y++)
  371. {
  372. coordinates.Add(new VecI(left, y));
  373. if (left != right)
  374. coordinates.Add(new VecI(right, y));
  375. }
  376. }
  377. private static void AddRegionPoints(HashSet<VecI> coordinates, double x, double xc, double y, double yc)
  378. {
  379. int xFloor = (int)Math.Floor(x);
  380. int yFloor = (int)Math.Floor(y);
  381. int xFloorInv = (int)Math.Floor(-x + 2 * xc);
  382. int yFloorInv = (int)Math.Floor(-y + 2 * yc);
  383. //top and bottom or left and right
  384. if (xFloor == xFloorInv || yFloor == yFloorInv)
  385. {
  386. coordinates.Add(new VecI(xFloorInv, yFloorInv));
  387. coordinates.Add(new VecI(xFloor, yFloor));
  388. }
  389. //part of the arc
  390. else
  391. {
  392. coordinates.Add(new VecI(xFloorInv, yFloor));
  393. coordinates.Add(new VecI(xFloor, yFloor));
  394. coordinates.Add(new VecI(xFloorInv, yFloorInv));
  395. coordinates.Add(new VecI(xFloor, yFloorInv));
  396. }
  397. }
  398. /// <summary>
  399. /// This function generates a vector path that represents an oval. For pixel-perfect circle use <see cref="ConstructEllipseOutline"/>.
  400. /// </summary>
  401. /// <param name="location">The rectangle that the ellipse should fit into.</param>
  402. /// <returns>A vector path that represents an oval.</returns>
  403. public static VectorPath GenerateEllipseVectorFromRect(RectD location)
  404. {
  405. VectorPath path = new();
  406. path.AddOval(location);
  407. path.Close();
  408. return path;
  409. }
  410. }