Repository: ManimCommunity/manim

Issue:
`Rotate` breaks when rotating multiples of 2pi
## Description of bug / unexpected behavior
```py
class RotationAnim(Scene):
    def construct(self):
        a = Circle().move_to(RIGHT)
        self.play(Rotate(a, 2*PI, about_point=ORIGIN), run_time=5)
```

gives:

https://user-images.githubusercontent.com/9102529/130183273-e101d2fe-65cc-42af-80ee-82d3adb0c732.mp4

`Rotating` works, but does not take into account the `rate_func` (and according to @behackl, is probably intentional)? Perhaps the best way to fix this is to use the method used for `Rotating`, just taking into account the `rate_func` (and maybe `Rotating` can inherit from `Rotate` and simply ignore the rate_func).


## Expected behavior
<!-- Add a clear and concise description of what you expected to happen. -->


## How to reproduce the issue
<!-- Provide a piece of code illustrating the undesired behavior. -->

<details><summary>Code for reproducing the problem</summary>

```py
Paste your code here.
```

</details>


## Additional media files
<!-- Paste in the files manim produced on rendering the code above. -->

<details><summary>Images/GIFs</summary>

<!-- PASTE MEDIA HERE -->

</details>


## Logs
<details><summary>Terminal output</summary>
<!-- Add "-v DEBUG" when calling manim to generate more detailed logs -->

```
PASTE HERE OR PROVIDE LINK TO https://pastebin.com/ OR SIMILAR
```

<!-- Insert screenshots here (only when absolutely necessary, we prefer copy/pasted output!) -->

</details>


## System specifications

<details><summary>System Details</summary>

- OS (with version, e.g Windows 10 v2004 or macOS 10.15 (Catalina)):
- RAM:
- Python version (`python/py/python3 --version`):
- Installed modules (provide output from `pip list`):
```
PASTE HERE
```
</details>

<details><summary>LaTeX details</summary>

+ LaTeX distribution (e.g. TeX Live 2020):
+ Installed LaTeX packages:
<!-- output of `tlmgr list --only-installed` for TeX Live or a screenshot of the Packages page for MikTeX -->
</details>

<details><summary>FFMPEG</summary>

Output of `ffmpeg -version`:

```
PASTE HERE
```
</details>

## Additional comments
<!-- Add further context that you think might be relevant for this issue here. -->



Discussion:
If I had to guess, my money would be on this line here causing the breakage:

https://github.com/ManimCommunity/manim/blob/232bb85338331c18ba5ca3c8b8c2bbbe9346a08f/manim/utils/paths.py#L45

I don't think it is worthwhile to change the implementation so that it looks similar to `Rotating`; it rather seems the way `path_arc` is processed is not robust when integer multiples of $2\pi$ are passed.

Resolving patch:
diff --git a/manim/animation/rotation.py b/manim/animation/rotation.py
--- a/manim/animation/rotation.py
+++ b/manim/animation/rotation.py
@@ -62,7 +62,9 @@ def __init__(
         self.axis = axis
         self.about_edge = about_edge
         self.about_point = about_point
-        super().__init__(mobject, **kwargs)
+        if self.about_point is None:
+            self.about_point = mobject.get_center()
+        super().__init__(mobject, path_arc_centers=self.about_point, **kwargs)
 
     def create_target(self) -> "Mobject":
         target = self.mobject.copy()
diff --git a/manim/animation/transform.py b/manim/animation/transform.py
--- a/manim/animation/transform.py
+++ b/manim/animation/transform.py
@@ -50,10 +50,12 @@ def __init__(
         path_func: Optional[Callable] = None,
         path_arc: float = 0,
         path_arc_axis: np.ndarray = OUT,
+        path_arc_centers: np.ndarray = None,
         replace_mobject_with_target_in_scene: bool = False,
         **kwargs,
     ) -> None:
         self.path_arc_axis: np.ndarray = path_arc_axis
+        self.path_arc_centers: np.ndarray = path_arc_centers
         self.path_arc: float = path_arc
         self.path_func: Optional[Callable] = path_func
         self.replace_mobject_with_target_in_scene: bool = (
@@ -71,7 +73,11 @@ def path_arc(self) -> float:
     @path_arc.setter
     def path_arc(self, path_arc: float) -> None:
         self._path_arc = path_arc
-        self._path_func = path_along_arc(self._path_arc, self.path_arc_axis)
+        self._path_func = path_along_arc(
+            arc_angle=self._path_arc,
+            axis=self.path_arc_axis,
+            arc_centers=self.path_arc_centers,
+        )
 
     @property
     def path_func(
diff --git a/manim/utils/paths.py b/manim/utils/paths.py
--- a/manim/utils/paths.py
+++ b/manim/utils/paths.py
@@ -27,7 +27,7 @@ def straight_path(start_points, end_points, alpha):
     return interpolate(start_points, end_points, alpha)
 
 
-def path_along_arc(arc_angle, axis=OUT):
+def path_along_arc(arc_angle, axis=OUT, arc_centers=None):
     """
     If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
     perpendicular to vect in the left direction.
@@ -39,10 +39,13 @@ def path_along_arc(arc_angle, axis=OUT):
     unit_axis = axis / np.linalg.norm(axis)
 
     def path(start_points, end_points, alpha):
-        vects = end_points - start_points
-        centers = start_points + 0.5 * vects
-        if arc_angle != np.pi:
-            centers += np.cross(unit_axis, vects / 2.0) / np.tan(arc_angle / 2)
+        if arc_centers is None:
+            vects = end_points - start_points
+            centers = start_points + 0.5 * vects
+            if arc_angle != np.pi:
+                centers += np.cross(unit_axis, vects / 2.0) / np.tan(arc_angle / 2)
+        else:
+            centers = arc_centers
         rot_matrix = rotation_matrix(alpha * arc_angle, unit_axis)
         return centers + np.dot(start_points - centers, rot_matrix.T)
 


Test patch:
diff --git a/tests/control_data/graphical_units_data/transform/FullRotation.npz b/tests/control_data/graphical_units_data/transform/FullRotation.npz
new file mode 100644
Binary files /dev/null and b/tests/control_data/graphical_units_data/transform/FullRotation.npz differ
diff --git a/tests/test_graphical_units/test_transform.py b/tests/test_graphical_units/test_transform.py
--- a/tests/test_graphical_units/test_transform.py
+++ b/tests/test_graphical_units/test_transform.py
@@ -18,6 +18,17 @@ def test_TransformFromCopy(scene):
     scene.play(TransformFromCopy(square, circle))
 
 
+@frames_comparison(last_frame=False)
+def test_FullRotation(scene):
+    s = VGroup(*[Square() for _ in range(4)]).arrange()
+    scene.play(
+        Rotate(s[0], -2 * TAU),
+        Rotate(s[1], -1 * TAU),
+        Rotate(s[2], 1 * TAU),
+        Rotate(s[3], 2 * TAU),
+    )
+
+
 @frames_comparison(last_frame=False)
 def test_ClockwiseTransform(scene):
     square = Square()