Browse Source

Ellipse operation optimizations part 1

flabbet 7 months ago
parent
commit
bbe97c2ba0

+ 124 - 7
src/ChunkyImageLib/Operations/EllipseHelper.cs

@@ -106,7 +106,7 @@ public class EllipseHelper
         float radiusY = (rect.Height - 1) / 2.0f;
         float radiusY = (rect.Height - 1) / 2.0f;
         if (rotationRad == 0)
         if (rotationRad == 0)
             return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
             return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y);
-        
+
         return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
         return GenerateMidpointEllipse(radiusX, radiusY, rect.Center.X, rect.Center.Y, rotationRad);
     }
     }
 
 
@@ -185,6 +185,118 @@ public class EllipseHelper
         return listToFill;
         return listToFill;
     }
     }
 
 
+    /// <summary>
+    ///     Constructs pixel-perfect ellipse outline represented as a vector path.
+    ///  This function is quite heavy, for less precise but faster results use <see cref="GenerateEllipseVectorFromRect"/>.
+    /// </summary>
+    /// <param name="rectangle">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an ellipse outline.</returns>
+    public static VectorPath ConstructEllipseOutline(RectI rectangle)
+    {
+        if (rectangle.Width < 3 || rectangle.Height < 3)
+        {
+            VectorPath rectPath = new();
+            rectPath.AddRect((RectD)rectangle);
+
+            return rectPath;
+        }
+
+        if (rectangle is { Width: 3, Height: 3 })
+        {
+            return CreateThreePixelCircle((VecI)rectangle.Center);
+        }
+
+        var center = rectangle.Center;
+        var points = GenerateEllipseFromRect(rectangle, 0).ToList();
+        points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
+        List<VecI> finalPoints = new();
+        for (int i = 0; i < points.Count; i++)
+        {
+            VecI prev = points[Mod(i - 1, points.Count)];
+            VecI point = points[i];
+            VecI next = points[Mod(i + 1, points.Count)];
+
+            bool atBottom = point.Y >= center.Y;
+            bool onRight = point.X >= center.X;
+            if (atBottom)
+            {
+                if (onRight)
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                    finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                    finalPoints.Add(new(point.X, point.Y + 1));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(point);
+                }
+            }
+            else
+            {
+                if (onRight)
+                {
+                    if (prev.X != point.X)
+                        finalPoints.Add(point);
+                    finalPoints.Add(new(point.X + 1, point.Y));
+                    if (next.Y != point.Y)
+                        finalPoints.Add(new(point.X + 1, point.Y + 1));
+                }
+                else
+                {
+                    if (prev.Y != point.Y)
+                        finalPoints.Add(new(point.X, point.Y + 1));
+                    finalPoints.Add(point);
+                    if (next.X != point.X)
+                        finalPoints.Add(new(point.X + 1, point.Y));
+                }
+            }
+        }
+
+        VectorPath path = new();
+
+        path.MoveTo(new VecF(finalPoints[0].X, finalPoints[0].Y));
+        for (var index = 1; index < finalPoints.Count; index++)
+        {
+            var point = finalPoints[index];
+            path.LineTo(new VecF(point.X, point.Y));
+        }
+
+        path.Close();
+
+        return path;
+    }
+
+    public static VectorPath CreateThreePixelCircle(VecI rectanglePos)
+    {
+        var path = new VectorPath();
+        path.MoveTo(new VecF(0, 0));
+        path.LineTo(new VecF(0, -1));
+        path.LineTo(new VecF(1, -1));
+        path.LineTo(new VecF(1, 0));
+        path.LineTo(new VecF(2, 0));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(2, 1));
+        path.LineTo(new VecF(1, 1));
+        path.LineTo(new VecF(1, 2));
+        path.LineTo(new VecF(0, 2));
+        path.LineTo(new VecF(0, 1));
+        path.LineTo(new VecF(-1, 1));
+        path.LineTo(new VecF(-1, 0));
+        path.Close();
+        
+        path.Transform(Matrix3X3.CreateTranslation(rectanglePos.X, rectanglePos.Y));
+        
+        return path;
+    }
+
+    private static int Mod(int x, int m) => (x % m + m) % m;
+
     // This function works, but honestly Skia produces better results, and it doesn't require so much
     // This function works, but honestly Skia produces better results, and it doesn't require so much
     // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
     // computation on the CPU. I'm leaving this, because once I (or someone else) figure out how to
     // make it better, and it will be useful.
     // make it better, and it will be useful.
@@ -203,7 +315,7 @@ public class EllipseHelper
 
 
         // less than, because y grows downwards
         // less than, because y grows downwards
         //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
         //VecD actualTopmost = possiblyTopmostPoint.Y < possiblyMinPoint.Y ? possiblyTopmostPoint : possiblyMinPoint;
-        
+
         //rotationRad = double.Round(rotationRad, 1);
         //rotationRad = double.Round(rotationRad, 1);
 
 
         double currentTetha = 0;
         double currentTetha = 0;
@@ -221,13 +333,13 @@ public class EllipseHelper
 
 
             currentTetha += tethaStep;
             currentTetha += tethaStep;
         } while (currentTetha < Math.PI * 2);
         } while (currentTetha < Math.PI * 2);
-        
+
         return listToFill;
         return listToFill;
     }
     }
 
 
     private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
     private static void AddPoint(HashSet<VecI> listToFill, VecI floored, VecI[] lastPoints)
     {
     {
-        if(!listToFill.Add(floored)) return;
+        if (!listToFill.Add(floored)) return;
 
 
         if (lastPoints[0] == default)
         if (lastPoints[0] == default)
         {
         {
@@ -247,7 +359,7 @@ public class EllipseHelper
 
 
             lastPoints[0] = floored;
             lastPoints[0] = floored;
             lastPoints[1] = default;
             lastPoints[1] = default;
-            
+
             return;
             return;
         }
         }
 
 
@@ -345,13 +457,18 @@ public class EllipseHelper
         }
         }
     }
     }
 
 
+    /// <summary>
+    ///     This function generates a vector path that represents an oval. For pixel-perfect circle use <see cref="ConstructEllipseOutline"/>.
+    /// </summary>
+    /// <param name="location">The rectangle that the ellipse should fit into.</param>
+    /// <returns>A vector path that represents an oval.</returns>
     public static VectorPath GenerateEllipseVectorFromRect(RectD location)
     public static VectorPath GenerateEllipseVectorFromRect(RectD location)
     {
     {
         VectorPath path = new();
         VectorPath path = new();
         path.AddOval(location);
         path.AddOval(location);
-       
+
         path.Close();
         path.Close();
-        
+
         return path;
         return path;
     }
     }
 }
 }

+ 24 - 13
src/ChunkyImageLib/Operations/EllipseOperation.cs

@@ -46,14 +46,22 @@ internal class EllipseOperation : IMirroredDrawOperation
         {
         {
             if (Math.Abs(rotation) < 0.001)
             if (Math.Abs(rotation) < 0.001)
             {
             {
-                var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
+                if (strokeWidth == 0)
+                {
+                    ellipseOutline = EllipseHelper.ConstructEllipseOutline((RectI)location);
+                }
+                else
+                {
+                    var ellipseList = EllipseHelper.GenerateEllipseFromRect((RectI)location);
 
 
-                ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
+                    ellipse = ellipseList.Select(a => new VecF(a)).ToArray();
 
 
-                if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
-                {
-                    (var fill, ellipseFillRect) = EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
-                    ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
+                    {
+                        (var fill, ellipseFillRect) =
+                            EllipseHelper.SplitEllipseFillIntoRegions(ellipseList.ToList(), (RectI)location);
+                        ellipseFill = fill.Select(a => new VecF(a)).ToArray();
+                    }
                 }
                 }
             }
             }
             else
             else
@@ -98,7 +106,7 @@ internal class EllipseOperation : IMirroredDrawOperation
         paint.IsAntiAliased = false;
         paint.IsAntiAliased = false;
         if (strokeWidth - 1 < 0.01)
         if (strokeWidth - 1 < 0.01)
         {
         {
-            if (Math.Abs(rotation) < 0.001)
+            if (Math.Abs(rotation) < 0.001 && strokeWidth > 0)
             {
             {
                 if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
                 if (fillColor.A > 0 || paint.BlendMode != BlendMode.SrcOver)
                 {
                 {
@@ -122,12 +130,15 @@ internal class EllipseOperation : IMirroredDrawOperation
                     paint.Style = PaintStyle.Fill;
                     paint.Style = PaintStyle.Fill;
                     surf.Canvas.DrawPath(ellipseOutline!, paint);
                     surf.Canvas.DrawPath(ellipseOutline!, paint);
                 }
                 }
-                
-                paint.Color = strokeColor;
-                paint.Style = PaintStyle.Stroke;
-                paint.StrokeWidth = 1f;
-                
-                surf.Canvas.DrawPath(ellipseOutline!, paint);
+
+                if (strokeWidth > 0)
+                {
+                    paint.Color = strokeColor;
+                    paint.Style = PaintStyle.Stroke;
+                    paint.StrokeWidth = 1;
+
+                    surf.Canvas.DrawPath(ellipseOutline!, paint);
+                }
 
 
                 surf.Canvas.Restore();
                 surf.Canvas.Restore();
             }
             }

+ 5 - 0
src/PixiEditor.ChangeableDocument/Changeables/Graph/Nodes/ImageLayerNode.cs

@@ -193,6 +193,11 @@ public class ImageLayerNode : LayerNode, IReadOnlyImageNode
 
 
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     private KeyFrameData GetFrameWithImage(KeyFrameTime frame)
     {
     {
+        if (keyFrames.Count == 1)
+        {
+            return keyFrames[0];
+        }
+        
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         var imageFrame = keyFrames.OrderBy(x => x.StartFrame).LastOrDefault(x => x.IsInFrame(frame.Frame));
         if (imageFrame?.Data is not ChunkyImage)
         if (imageFrame?.Data is not ChunkyImage)
         {
         {

+ 2 - 92
src/PixiEditor/Views/Overlays/BrushShapeOverlay/BrushShapeOverlay.cs

@@ -57,7 +57,7 @@ internal class BrushShapeOverlay : Overlay
     public BrushShapeOverlay()
     public BrushShapeOverlay()
     {
     {
         IsHitTestVisible = false;
         IsHitTestVisible = false;
-        threePixelCircle = CreateThreePixelCircle();
+        threePixelCircle = EllipseHelper.CreateThreePixelCircle(VecI.Zero);
     }
     }
 
 
     protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
     protected override void OnOverlayPointerMoved(OverlayPointerArgs args)
@@ -124,7 +124,7 @@ internal class BrushShapeOverlay : Overlay
         {
         {
             if (BrushSize != lastSize)
             if (BrushSize != lastSize)
             {
             {
-                var geometry = ConstructEllipseOutline(new RectI(0, 0, rectI.Width, rectI.Height));
+                var geometry = EllipseHelper.ConstructEllipseOutline(new RectI(0, 0, rectI.Width, rectI.Height));
                 lastNonTranslatedCircle = new VectorPath(geometry);
                 lastNonTranslatedCircle = new VectorPath(geometry);
                 lastSize = BrushSize;
                 lastSize = BrushSize;
             }
             }
@@ -150,94 +150,4 @@ internal class BrushShapeOverlay : Overlay
     {
     {
         paint.StrokeWidth = (float)(1.0f / newZoom);
         paint.StrokeWidth = (float)(1.0f / newZoom);
     }
     }
-
-    private static int Mod(int x, int m) => (x % m + m) % m;
-
-    private static VectorPath CreateThreePixelCircle()
-    {
-        var path = new VectorPath();
-        path.MoveTo(new VecF(0, 0));
-        path.LineTo(new VecF(0, -1));
-        path.LineTo(new VecF(1, -1));
-        path.LineTo(new VecF(1, 0));
-        path.LineTo(new VecF(2, 0));
-        path.LineTo(new VecF(2, 1));
-        path.LineTo(new VecF(2, 1));
-        path.LineTo(new VecF(1, 1));
-        path.LineTo(new VecF(1, 2));
-        path.LineTo(new VecF(0, 2));
-        path.LineTo(new VecF(0, 1));
-        path.LineTo(new VecF(-1, 1));
-        path.LineTo(new VecF(-1, 0));
-        path.Close();
-        return path;
-    }
-
-    private static VectorPath ConstructEllipseOutline(RectI rectangle)
-    {
-        var center = rectangle.Center;
-        var points = EllipseHelper.GenerateEllipseFromRect(rectangle, 0).ToList();
-        points.Sort((vec, vec2) => Math.Sign((vec - center).Angle - (vec2 - center).Angle));
-        List<VecI> finalPoints = new();
-        for (int i = 0; i < points.Count; i++)
-        {
-            VecI prev = points[Mod(i - 1, points.Count)];
-            VecI point = points[i];
-            VecI next = points[Mod(i + 1, points.Count)];
-
-            bool atBottom = point.Y >= center.Y;
-            bool onRight = point.X >= center.X;
-            if (atBottom)
-            {
-                if (onRight)
-                {
-                    if (prev.Y != point.Y)
-                        finalPoints.Add(new(point.X + 1, point.Y));
-                    finalPoints.Add(new(point.X + 1, point.Y + 1));
-                    if (next.X != point.X)
-                        finalPoints.Add(new(point.X, point.Y + 1));
-                }
-                else
-                {
-                    if (prev.X != point.X)
-                        finalPoints.Add(new(point.X + 1, point.Y + 1));
-                    finalPoints.Add(new(point.X, point.Y + 1));
-                    if (next.Y != point.Y)
-                        finalPoints.Add(point);
-                }
-            }
-            else
-            {
-                if (onRight)
-                {
-                    if (prev.X != point.X)
-                        finalPoints.Add(point);
-                    finalPoints.Add(new(point.X + 1, point.Y));
-                    if (next.Y != point.Y)
-                        finalPoints.Add(new(point.X + 1, point.Y + 1));
-                }
-                else
-                {
-                    if (prev.Y != point.Y)
-                        finalPoints.Add(new(point.X, point.Y + 1));
-                    finalPoints.Add(point);
-                    if (next.X != point.X)
-                        finalPoints.Add(new(point.X + 1, point.Y));
-                }
-            }
-        }
-
-        VectorPath path = new();
-
-        path.MoveTo(new VecF(finalPoints[0].X, finalPoints[0].Y));
-        for (var index = 1; index < finalPoints.Count; index++)
-        {
-            var point = finalPoints[index];
-            path.LineTo(new VecF(point.X, point.Y));
-        }
-
-        path.Close();
-
-        return path;
-    }
 }
 }