community icon indicating copy to clipboard operation
community copied to clipboard

ellipse start and stop angles problems

Open petaflot opened this issue 5 months ago • 20 comments

Software Versions

  • Python: 3.12
  • OS: Gentoo Linux (default/linux/amd64/23.0/desktop (stable))
  • Kivy: 2.3.0
  • Kivy installation method: pip

Describe the bug I have sth like self.arc_daylight.ellipse = (x, y, w, h, sunrise, sunset) where sunrise and sunset are 120.75 and 42.0 degrees[^1] ; unfortunately the arc is drawn in inverted range (and swapping the values doesn't help)

I also notice that 0 degrees is "up" and +90 degrees is right, which definitely doesn't follow the trigonometric convention where 0 is right, pi (in radians, or 90 degrees) is up, 2*pi (or 180 degrees) is left and so on.

[^1]: FYI this is today's sunrise and sunset times in Yakutsk, on a 24h clock set to CEST

Expected behavior the arc should span most of the circle (the other way than what is currently drawn, from start to stop and not the shortest way or somewhere weird and unexpected (see Ellipse angle_start and angle_end seem to use different ranges ), and should be going counter-clockwise instead of clockwise

To Reproduce

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line

class ConcentricEllipses(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        cx, cy = self.center_x, self.center_y
        # will update once widget has size
        self.bind(size=self._draw, pos=self._draw)

    def _draw(self, *args):
        self.canvas.clear()
        cx, cy = self.center_x, self.center_y
        with self.canvas:
            Color(1, 0, 0)
            # outer ellipse: width=2
            Line(ellipse=(cx - 100, cy - 50, 200, 100, 120, 42), width=2)
            Color(0, 0, 1)
            # inner ellipse: width=2
            Line(ellipse=(cx - 75, cy - 37.5, 150, 75, 42, 120), width=2)

class MinimalApp(App):
    def build(self):
        return ConcentricEllipses()

if __name__ == '__main__':
    MinimalApp().run()

Code and Logs and screenshots

Image

petaflot avatar Jul 14 '25 18:07 petaflot

You're encountering two frustrating quirks with Kivy's built-in ellipse arc drawing:

1/ Inverted range behavior: When angle_start > angle_end (like your 120.75° to 42.0°), Kivy draws the shorter arc instead of the longer daylight arc you want

2 / Non-standard angle convention: Kivy uses 0° = up with clockwise rotation, rather than the mathematical standard of 0° = right with counter-clockwise rotation

The Solution - Manual Arc Generation

The best approach is to bypass Kivy's problematic ellipse method entirely and draw the arc manually using standard trigonometry. Here's the complete working solution ( not tested ! ) :

==============

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.graphics import Color, Line
import math

class ConcentricEllipses(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.bind(size=self._draw, pos=self._draw)

    def draw_ellipse_arc(self, cx, cy, width, height, start_rad, end_rad, 
                        num_points=100, line_width=2):
        """
        Draw an elliptical arc using mathematical convention
        
        Args:
            cx, cy: center coordinates
            width, height: ellipse dimensions
            start_rad, end_rad: angles in RADIANS (0 = right, counter-clockwise)
            num_points: smoothness (more points = smoother curve)
            line_width: thickness of the arc
        
        Returns:
            Line object for the arc
        """
        # Semi-axes
        a = width / 2
        b = height / 2
        
        # Ensure counter-clockwise sweep for the longer arc
        if end_rad < start_rad:
            end_rad += 2 * math.pi
        
        # Generate arc points
        points = []
        for i in range(num_points + 1):
            theta = start_rad + (end_rad - start_rad) * i / num_points
            x = cx + a * math.cos(theta)
            y = cy + b * math.sin(theta)
            points.extend([x, y])
        
        return Line(points=points, width=line_width)

    def _draw(self, *args):
        self.canvas.clear()
        cx, cy = self.center_x, self.center_y
        
        with self.canvas:
            # Red outer ellipse: daylight arc (most of the circle)
            Color(1, 0, 0)
            # Convert original values to mathematical convention
            # Original: 120.75° to 42.0° (Kivy convention)
            # In mathematical radians: roughly 5.76 rad to 0.84 rad
            start_daylight = 5.76  # ~330° in radians
            end_daylight = 0.84    # ~48° in radians
            self.draw_ellipse_arc(cx, cy, 200, 100, start_daylight, end_daylight, line_width=2)
            
            # Blue inner ellipse: night arc (smaller portion)
            Color(0, 0, 1)
            # Original: 42.0° to 120.75° (Kivy convention)
            # In mathematical radians: roughly 0.84 rad to 5.76 rad
            start_night = 0.84     # ~48° in radians
            end_night = 5.76       # ~330° in radians
            self.draw_ellipse_arc(cx, cy, 150, 75, start_night, end_night, line_width=2)
            
            # Green reference circle to show the full ellipse
            Color(0, 1, 0)
            self.draw_ellipse_arc(cx, cy, 100, 50, 0, 2 * math.pi, line_width=1)

class MinimalApp(App):
    def build(self):
        return ConcentricEllipses()

if __name__ == '__main__':
    MinimalApp().run()

iyotee avatar Jul 14 '25 18:07 iyotee

@iyotee : works for me 👍

petaflot avatar Jul 14 '25 18:07 petaflot

👋 We use the issue tracker exclusively for bug reports and feature requests. However, this issue appears to be a support request. Please use our support channels to get help with the project.

If you're having trouble installing Kivy, make sure to check out the installation docs for Windows, Linux and macOS.

Let us know if this comment was made in error, and we'll be happy to reopen the issue.

github-actions[bot] avatar Jul 15 '25 13:07 github-actions[bot]

👋 We use the issue tracker exclusively for bug reports and feature requests. However, this issue appears to be a support request. Please use our support channels to get help with the project.

If you're having trouble installing Kivy, make sure to check out the installation docs for Windows, Linux and macOS.

Let us know if this comment was made in error, and we'll be happy to reopen the issue.

this IS NOT a support request, it's definitely a bug because if the start and stop range is inverted, the ellipse MUST be inverted too ; the solution proposed above by @iyotee is a workaround (and pretty much code that should substitute the current Line(ellipse=...) code in the kivy system

petaflot avatar Jul 16 '25 08:07 petaflot

This is not a bug. Changing the operation of an existing primitive would break compatibility with existing code. I would invite you to do a PR improving the documentation.

Here is a small example you can play with to explore the behavior.

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.label import Label
from kivy.properties import NumericProperty

kv = """
BoxLayout:
    orientation: 'vertical'
    CircleLabel:
        id: circle_label
        text: 'Circle Control'
        canvas:
            Color:
                rgb: 1, 0, 0
            Line:
                circle: (*self.center, self.width/4, self.start_angle, self.end_angle)
    BoxLayout:
        orientation: 'vertical'
        size_hint_y: None
        height: dp(100)
        BoxLayout:
            Label:
                size_hint_x: None
                width: dp(100)
                text: 'Angle Start'
            Slider:
                id: start
                min: -360
                max: 360
                on_value: circle_label.start_angle = self.value
            Label:
                size_hint_x: None
                width: dp(100)
                text: f'{start.value: 0.1f}'
        BoxLayout:
            Label:
                size_hint_x: None
                width: dp(100)
                text: 'Angle End'
            Slider:
                id: end
                min: -360
                max: 360
                on_value: circle_label.end_angle = self.value
            Label:
                size_hint_x: None
                width: dp(100)
                text: f'{end.value: 0.1f}'
    Button:
        size_hint_y: None
        height: dp(48)
        text: 'Reset'
        on_release: start.value = end.value = 0
"""

class CircleLabel(Label):
    start_angle = NumericProperty(0)
    end_angle = NumericProperty(0)


class EllipseStartStopApp(App):
    def build(self):
        return Builder.load_string(kv)

EllipseStartStopApp().run()

ElliotGarbus avatar Jul 16 '25 16:07 ElliotGarbus

@ElliotGarbus and what exactly am I supposed to "explore" with your code snippet?

I understand that breaking existing code is bad ; may I therefore suggest to add sth like Line(ellipse_rad=...) with a behaviour that include

  • radians (because internally the math module uses radians, we don't want to convert back and forth for no reasons, wasting CPU resources and precision)
  • zero at "east"
  • CCW (because that's the trigonometric standard)

this way code compatibility is not broken, and new code can use something that actually makes sense

petaflot avatar Jul 17 '25 17:07 petaflot

If you run the code I shared, you can explore the values that you need to get the desired outcome. There are 2 sliders that show you the results of different start and stop angles.

I'd invite you to create a PR that implements the functionality you are interested in. I'm reopening the issue, and tagging it as a feature request.

ElliotGarbus avatar Jul 17 '25 18:07 ElliotGarbus

This is not a bug. Changing the operation of an existing primitive would break compatibility with existing code. I would invite you to do a PR improving the documentation.

Angle zero at the top increasing clockwise seems like a bug to me XD or at least a pretty questionable design choice (let's call it a historical mistake). I do believe we should use standard mathematical and graphical conventions. Honestly, I don’t see any reasonable justification for current behavior unless the goal is to surprise people or make them confused.

Great example, @ElliotGarbus ; I found that changing (*self.center, self.width/4, -(self.start_angle - 90), - (self.end_angle - 90)) to this will fix it:

Line:
    circle: (*self.center, self.width/4, -(self.start_angle - 90), - (self.end_angle - 90))

Since Kivy 3.0.0 is on the horizon, it feels like the right time to address these inconsistencies. I think it’s reasonable and healthy for the project in the long run. Break the behavior and document the change

@petaflot let us know if you'll work on a PR to fix this

FilipeMarch avatar Jul 17 '25 19:07 FilipeMarch

@FilipeMarch I would not support a change that breaks legacy code. I would support adding a new keyword arguments or a new method that uses radians and changes the “starting point”.

ElliotGarbus avatar Jul 17 '25 20:07 ElliotGarbus

Many things will inevitably break after Kivy 3.0, and users can always pin their versions to < 3.0 if they need the legacy behavior. We could coordinate for this major release by adding deprecation warnings ahead of any breaking changes. There’s no reason to carry legacy errors forever.

My hope is that, if this change happens, it will be well-communicated and well-documented like a proper breaking change in a major release, not something silent or ambiguous in a minor update. The idea that we can never fix issues in the codebase just because they're old doesn’t make sense to me. That’s just my opinion, but I truly believe most people would support fixing this in a major release. just check pandas, tensorflow, django, sqlalchemy, numpy, any other big library; they regularly deprecate and remove APIs across major releases, requiring code updates

FilipeMarch avatar Jul 17 '25 22:07 FilipeMarch

@FilipeMarch We will simply have to disagree. As I see it changing an API, that does not add any new functionality is not good for the community. A change for degrees to radians would need to impact all graphics privatives that specify an angle(circle, ellipse, rotate, vector...). I don't see any significant value that comes from changing the start point of a ellipse.

Restating, adding keyword arguments, or new methods as a way to support the desired "modes" seems reasonable. I'm not a decision maker here, this decision will ultimately be made by a maintainer.

ElliotGarbus avatar Jul 18 '25 06:07 ElliotGarbus

I am against changing from degrees to radians. But adding an option to change the default would be great.

The value of using standard math convention for the starting point of the ellipse is to avoid having all future developers getting their ellipse wrong the first time they use this feature in Kivy.

I appreciate your input, I'm fine whatever happens.

FilipeMarch avatar Jul 18 '25 06:07 FilipeMarch

@ElliotGarbus

I don't see any significant value that comes from changing the start point of a ellipse.

as I mentioned above, the math module uses radians internally, so there's an obvious benefit in performance if the user is using radians ; if the user wants degrees they can just use math.radians(angle)

[...] that does not add any new functionality [...]

how does being able to invert the range not add new functionality? the current behavior is like having a subtraction that sometimes returns an absolute value, this makes no sense at all.

petaflot avatar Jul 18 '25 22:07 petaflot

@petaflot The performance difference would not be significant. In the current source code, angles are converted to radians. This is one multiply in cython code.

In the current configuration I can draw any arbitrary arc. Using radians, and changing the starting point by 90 degrees does not add new functionality. Again, I'm not the decision maker here.

I understand you want the option to use radians instead of degrees and set the starting point of a ellipse to 3 o'clock.

ElliotGarbus avatar Jul 19 '25 15:07 ElliotGarbus

@ElliotGarbus

The performance difference would not be significant.

from now on, I suggest that every time you go through an open door, you close it first and the open it again ; when you need to go through a closed door, you open it, close it, and open it again and then only go through.

please don't act like a troll.

I can draw any arbitrary arc

I wouldn't be so sure, see this example and spot the problem:

#!/usr/bin/env python

from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.graphics import Line, Color
from kivy.clock import Clock
from math import sin, cos, pi

Window.size = 400,400
steps = 10

class ArcWidget(Widget):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.angle = 0
        self.bind(size=self.update_arc, pos=self.update_arc)
        Clock.schedule_interval(self.update_arc, 1/30)

    def update_arc(self, *args):
        self.canvas.clear()
        with self.canvas:
            Color(1, 0.5, 0, 1)  # orange
            cx, cy = self.center
            w, h = self.width / 2-10, self.height / 2-10
            angle_start = (self.angle % 360)
            angle_end = (self.angle//(360/steps)) % 360
            Line(ellipse=(cx - w, cy - h, 2*w, 2*h, angle_start, angle_end), width=2)
        self.angle += steps


class ArcApp(App):
    def build(self):
        return ArcWidget()


if __name__ == '__main__':
    ArcApp().run()

petaflot avatar Jul 20 '25 07:07 petaflot

and if someones needs another reason to fix the direction of rotation, kivy.Graphics.Rotate works CCW - just as it should (although it does use degrees as well, instead of radians)

petaflot avatar Jul 20 '25 09:07 petaflot

What is the issue I am supposed to see in your example?

The performance of a fp multiply in Cython is about 1 cpu clock on a modern CPU. https://github.com/kivy/kivy/blob/8d103908e6381471858684b28d46a7f66bb8caf7/kivy/graphics/vertex_instructions_line.pxi#L1038 Some frameworks use radians, some use degrees. If you are making performance claims please share data, not metaphors.

You seem to feel very strongly about these changes, I suggest you create a PR.

ElliotGarbus avatar Jul 20 '25 13:07 ElliotGarbus

angle_start = angle_start * 0.017453292519943295 Some frameworks use radians, some use degrees.

this is obviously radians. you're wasting my time.

it's not only CPU cycles, also the lost of precision ; see https://www.engrenage.ch/i18n/scripts/unicomplex/

If you are making performance claims please share data, not metaphors.

see above. about my metaphor, I'm sure you'd get tired and bored pretty quickly if you tried my suggestion with the doors.

What is the issue I am supposed to see in your example?

some arcs can be drawn from two different inputs, and some cannot be drawn at all unless you add a rotation (and this will only work for circles (ellipses with a==b) ; if you can't see this I suggest you let the animation run and meditate in front of it for as long as is required for you to suddenly realize how bad (and stupid) the problem is.

I suggest you create a PR.

Why don't you do it, once you have finally been able to spot the problem after I've explained it over and over again and you're just i n denial? You might actually learn something. Plus, there's another potential bug I spotted that I need to confirm. And FYI I've got this medical condition that prevents me from spending too much time on the computer, and since I've explained and even posted two very explicit examples (one specific, one generic) of what is wrong I'm not going to answer anything else about this bug report from now on. Cheers

petaflot avatar Jul 21 '25 00:07 petaflot

If you run the example code I posted, you can see you can create any arbitrary arc - and the values required to create that arc.

ElliotGarbus avatar Jul 21 '25 00:07 ElliotGarbus

If you run the example code I posted, you can see you can create any arbitrary arc - and the values required to create that arc.

now try this without using negative values

petaflot avatar Jul 21 '25 10:07 petaflot