Skip to content

GSoC update: Draw Ellipses in Krita

Sunday, 24 July 2022 | Xu Che's GSoC updates


I’ve been working on Krita’s ellipse tool as my GSoC project. Here’s a status update and demo of what I have till now. I’m trying to improve the reading experience by using no code and reducing technical details in the post; if you are interested in those details, just visit the merge request page.

If you have ever tried to draw an ellipse of small size in Krita, you must have noticed that the ellipse is asymmetric and twisted. These minor errors can be negligible when drawing ellipses of larger size, but it became annoying when trying to draw some small-sized ellipses for pixel arts. To fully understand the cause, let’s look at how Krita draws the ellipse.

Asymmetric and twisted

So you chose the ellipse tool, and you dragged to draw an ellipse. After you release the LMB, Krita uses the inputted parameter to create a four-segment close bezier curve approximation of that ellipse. Then, it breaks each bezier curve into smaller pieces of straight lines and draws. Every step here can bring some error and asymmetry.

Now on to solving the problem. During my community bonding period, I tried to analyse and reduce the error in the approximated drawing process as it sounded feasible. However, I eventually found it challenging to work perfectly, so on 30th May, I started coding on another method, which worked well.

You may probably know that the equation of an ellipse is $ \frac{x^2}{a^2}+\frac{y^2}{b^2}=1 $, and the basic idea here is to iterate through every pixel, or integer coordinate, in the $ x ∈ [-a,a],\ y ∈ [-b,b] $ bounding box, and draw on them if it’s close enough to the ellipse. This overview is greatly simplified; it’s not hard to tell that there are many problems to solve.

You may press ctrl+alt to rotate the ellipse, and we can’t handle that now since there’s no rotation angle variable in our equation. So I used a more general equation from this article. $$ -a^2 b^2 + x^2 (b^2 \cos^2 c + a^2 \sin^2 c) + y^2 (a^2 \cos^2 c + b^2 \sin c^2) + (-a^2 + b^2) x y \sin 2c $$ In the first week of the coding period, I changed my code to handle rotation and made the first commit and the merge request. My mentor pointed out the less optimal efficiency of iterating through every pixel in the bounding box. That’s the second issue I have to deal with. Checking every pixel in the bounding box is of $ O(n^2) $ time complexity; that’s too slow for drawing larger ellipses. I expect an $ O(n) $ algorithm that spends time proportional to the circumference of the ellipse rather than its area.

I devised a BFS-like solution for the second issue in the third and fourth weeks by leveraging the property that any two pixels on an ellipse are connected. By starting from a pixel which is sure to be on the ellipse, all the undrawn pixels must be adjacent to the drawn ones. I only have to look for adjacent pixels of recently drawn pixels; that’s roughly $ 3C_e $ pixels to check. The process ended up fast, so performance was not an issue anymore.

Let’s take a look at what we have by now! Looks great, especially when compared to what we used to have, right?

However, if you look closer, you may find some edges that look thicker than 1 pixel. The cause is that when the ellipse is going diagonally in a 2*2 area, three pixels were drawn and made an L shape, while only the two pixels on the opposite corner should be taken.

This kind of line is called “pixel perfect”, as it’s 1 pixel wide precisely. To filter out excessive pixels is simple, just iterate through each of them, and check if they are on an L-shape. There are also noteworthy exceptions for pixels on L-shapes, as a line may pass them twice when the curvature is too big on the ellipse: When the pixel is traversed twice, obviously, the width of the line will not be 1. I’ve tested a lot, then found exceptions for a 2*2 square and a T-shape are needed.

I worked on the pixel-perfect algorithm in the second and the fifth week(I didn’t write in chronological order). Finally, I have everything figured out; let’s look at the result. You can expect to see this feature in Krita soon!