cpython
cpython copied to clipboard
Unnecessarily slow turtle rotations
Bug report
Two similar drawings can widely differ in terms of speed, resulting in a counter-intuitive experience for the end-user. This is independent from the plateform (OS, architecture) and from python version (due to the current implementation of turtle).
Example
Consider the following script (which is a simplified version of a real demo program with animation turned on). it draws two 7-points stars. The first one is drawn normally but the second, while similar, is much slower. This is very confusing (also for the programmer trying to understand what happens).
import turtle
def star7(t, a, b, c):
t.left(a)
for i in range(7):
t.forward(80)
t.left(b)
t.forward(80)
t.left(c)
t.left(-a)
t = turtle.Turtle()
t.hideturtle()
star7(t, 64.6, -77.7, 129.1) # draw star 1 (normal speed)
star7(t, 295.4, -282.3, 590.9) # draw star 2 (really too slow !)
Explanation
The problem comes from turtle rotation: in case of animation (by default speed > 0), the time taken by the rotation is proportional to the amplitude of the angle even if the turtle is not visible resulting in a confusing slow drawing. This can be confirmed with a visible turtle, commenting out:
# t.hideturtle()
Indeed, the second star7()
invocation contains "large" angles (these values are actually computed, not written by a human). Obviously, it is possible to give smaller equal angles (here taking the opposite of angles of the first star), resulting in a fast drawing e.g.:
star7(t, -64.6, 77.7, -129.1)
Obviously, the programmer can normalize all rotation angles in case of animation but this is painfull and error-prone (the programmer can misses some). And above all, there is no justification for such delays if the turtle is not shown !
Looking at the code of function _rotate()
in class RawTurtle
(in turtle.py:3279):
def _rotate(self, angle):
"""Turns pen clockwise by angle.
"""
if self.undobuffer:
self.undobuffer.push(("rot", angle, self._degreesPerAU))
angle *= self._degreesPerAU # angle is now in degrees
neworient = self._orient.rotate(angle)
tracing = self.screen._tracing
if tracing == 1 and self._speed > 0: # test: rotation animation ?
anglevel = 3.0 * self._speed
steps = 1 + int(abs(angle)/anglevel) # animation: split into steps (proportionally to abs(angle))
delta = 1.0*angle/steps
for _ in range(steps):
self._orient = self._orient.rotate(delta)
self._update()
self._orient = neworient
self._update()
In case of animation (speed>0), the rotation is split in several steps, each invoking _update()
, thus the delays.
Solutions
A first solution is to only do this if the turtle is visible. This can be achieved replacing the test by:
if self._shown and tracing == 1 and self._speed > 0:
But maybe this results in a too fast drawing.
A second solution consists in normalizing the angle if the turtle is not visible. This can be achived with:
if tracing == 1 and self._speed > 0:
if not self._shown:
angle -= math.ceil(angle / 360.0 - 0.5) * 360.0 # normalize angle in (-180;180]
This normalization ensures the angle (in degrees since the instruction angle *= self._degreesPerAU
above) is now in (-180;180] and prevents too long delays. The result is nice. I'm in favor of this second solution.
What do you think ?
Is there a good reason not to always normalise the angle to the appropriate value between -180° and +180°, regardless of whether the turtle is visible or not?
import turtle
t = turtle.Turtle()
t.right(360*1000)
Rotating by exact multiples of 360° is a no-op, but this takes a very long time, after which the turtle is facing the same way it was when it started.
From a scientific point of view, there is no reason to not normalize any angle. However, it is perhaps educational (for young programmers, children,...) to see the turtle turning on itself if the angle is greater than 360° (this is consistent with the original goal of the logo language and its turtle). However, this seems useless if the turtle is not shown.
There are sure somewhere applications which use the turtle module not for drawing but for animation of shapes (for example showing sky with stars spinning around, motorcycle racing with the motorcycle spinning around own axis for fun). Such applications expect an animated turtle rotation for any angle as it is. Rotating 10 times around own axis by 360 degree is a feature and not a bug which need to be fixed. It makes maybe not much sense to animate rotation if a turtle is hidden or animate drawing with hidden turtle and pen up, but I would prefer a reliable and same behavior of the speed animation not depending on visibility or pen up setting over speculating what does make sense and what doesn't modifying deliberately the angle of rotation away from the specified value.
Turtle graphic is all about visual and intuitive. As I said, I have no problem with the current behavior when the turtle is shown since the behavior is graphically understandable. But when the turtle is not shown, the result is absolutely confusing for the user (again, run the example I provided...). Is it a desirable "feature" ? Not for me...
Anyway, I understand the reluctance to change what already exists (and calling it a "feature" is a good way to freeze it). At least, this "feature" should be clearly mentioned in the documentation of left()
, right()
, _rotate()
; something like "Beware: if animation is on and even if the turtle is hidden, large angles (e.g. outside -180°..180°) can result in very slow drawings".
A suggestion to go further: provide an additional optional argument to rotation functions (e.g. normalize_angle=False
) allowing he user to chose if the angle should be normalized: This is a simple change which does not break any existing code (full backward compatibility):
def _rotate(self, angle, normalize_angle=False):
"""Turns pen clockwise by angle.
"""
Argument:
angle -- a number (integer or float)
normalize_angle (optional): a boolean (False by default)
Turn turtle by angle units. If normalize_angle is True, the angle is first normalized, e.g. to be in
(-180,+180] degrees or in (-pi,+pi] radians.
This is useful in case of animation (even with a hidden turtle) since the rotation animation
uses the actual angle and takes a time proportional to abs(angle).
Passing `True` to `normalize_angle` prevents too long animation delay.
"""
if normalize_angle:
angle = _normalize_angle(angle)
# the rest of the function is unchanged
.....
def _normalize_angle(self, angle):
m = 360.0 / self._degreesPerAU;
angle -= math.ceil(angle / m - 0.5) * m
return angle
The modification of left()
and right()
is straightforward, simply passing normalize_angle
to _rotate()
. I wrote a tentative documentation for the new parameter in _rotate()
(to be also copied in left()
and right()
).
for example showing sky with stars spinning around,
All other processing stops while the turtle spins, so you cannot animate twenty stars by setting twenty turtles, then commanding each one to rotate by 3600 degrees. That would spin the first star ten times, then the second star ten times, then the third star, and so on.
To spin multiple stars, motorcycles, etc:
- you cannot use GIFs (they do not rotate);
- you have to spin each shape by a small increment, then move on to the next shape;
- otherwise all processing stops until the rotation ends, which makes for a really bad animated user experience.
As far as I can see, using angles greater than 360° can only be a bug in the user's code. Maybe I'm wrong, but I think we would need more than just an assertion that people are deliberately spinning the turtle by large angles.
Rotating 10 times around own axis by 360 degree is a feature and not a bug which need to be fixed.
I don't think so. I think it is a bug.
I am sure that there are lots and lots and lots of applications which need to spin shapes by more than 360° but they don't do it by calling turtle.rotate(36000). See, for example, the "round_dance" demo script.
python3.10 -m turtledemo
I believe that large rotations greater than 360° are a nuisance that nobody relies on. Am I wrong? If you are right, it should be easy to find many turtles scripts that rely on rotating by large angles.
One way we can get past this argument is by adding a new turtle mode,
say turtle.full_rotations(flag)
. Setting full rotations to true keeps
the current behaviour; setting it to false normalises the angle to
within one full circle.
I don't see any advantage in limiting or manipulating users choice for angle of rotation in turtle (POINT). It doesn't improve anything and would make things unnecessary more complicated with additional settings possible. If you are willing to invest time in improving turtle I suggest you look into providing a true full screen experience without any outside border. The 1 pixel thin border around the turtle canvas in full screen mode (with all borders set to 0 width) is probably actually a kind of bug being in my eyes worth the effort to be resolved.
Having actually taught a 7-year-old to program using the Turtle module, my estimation of user expectations may have some value here.
I think the default should be for the speed of a hidden turtle to exactly match the speed of a shown turtle in all ways.
I think "normalize angles" is a terrible idea from a teaching standpoint because it adds in more magic and will lead to the incorrect assumption by a beginner programmer that the computer will just magically do the right thing when given a silly instruction.
If you tell the turtle to spin around 100 times, the turtle should spin around 100 times. Maybe you want to animate the turtle spinning and so it's a feature; it would be odd indeed to have to separately turn on a flag to say, "really do spin the way I told you." And maybe you didn't realize that it was silly to calculate your turning values in the way that you're now telling the turtle to spin 100 times. But for the beginner to program in the angle normalization themselves should be well within beginner capabilities, and can give them a nice simple problem to sink into, if they don't like the delay in the spinning.
Now this brings me to the last comment I want to make. Part of the problem I have with the "normalize angles" idea is that it changes the angle given to some other angle. But keeping the angle the same, and just modifying the speed of turning, could be useful. I would support a new setting for a turtle, something like "animate_rotation." This could be turned off independently of turning off animation generally. Maybe have a setting for "only when visible" or some such. So then if you want to draw slowly with a hidden turtle but not have ANY delays at the corners (i.e. when turning before drawing the next line), you can set this parameter accordingly. That's a feature that I can easily envision being used happily by the 7-year-old I was teaching.