diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 994e4dfa..2cc4e5a6 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -28,7 +28,7 @@ class Transform(Animation): self, mobject: Mobject, target_mobject: Mobject | None = None, - path_arc: float = 0.0, + path_arc: float | Tuple[float, float] = 0.0, path_arc_axis: np.ndarray = OUT, path_func: Callable | None = None, **kwargs @@ -43,7 +43,7 @@ class Transform(Animation): def init_path_func(self) -> None: if self.path_func is not None: return - elif self.path_arc == 0: + elif isinstance(self.path_arc, float) and self.path_arc == 0: self.path_func = straight_path else: self.path_func = path_along_arc( diff --git a/manimlib/utils/paths.py b/manimlib/utils/paths.py index 37473797..414b4a55 100644 --- a/manimlib/utils/paths.py +++ b/manimlib/utils/paths.py @@ -34,26 +34,43 @@ def straight_path( def path_along_arc( - arc_angle: float, + arc_angle: float | Tuple[float, float] | np.ndarray, axis: Vect3 = OUT ) -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]: """ + arc_angle can be a single angle, or a pair of angles, in which case + the range of all angles between that pair will be used. + If vect is vector from start to end, [vect[:,1], -vect[:,0]] is perpendicular to vect in the left direction. """ - if abs(arc_angle) < STRAIGHT_PATH_THRESHOLD: + if isinstance(arc_angle, float | int) and abs(arc_angle) < STRAIGHT_PATH_THRESHOLD: return straight_path if get_norm(axis) == 0: axis = OUT unit_axis = axis / get_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) / math.tan(arc_angle / 2) - rot_matrix_T = rotation_matrix_transpose(alpha * arc_angle, unit_axis) - return centers + np.dot(start_points - centers, rot_matrix_T) + if isinstance(arc_angle, float | int): + theta = arc_angle + else: + if isinstance(arc_angle, np.ndarray) and len(arc_angle) == len(start_points): + theta_range = arc_angle + else: + theta_range = np.linspace(arc_angle[0], arc_angle[-1], len(start_points)) + # Avoid zero, mildly hacky + theta_range[np.abs(theta_range) < STRAIGHT_PATH_THRESHOLD] = STRAIGHT_PATH_THRESHOLD + # Get shape to match + theta = theta_range[:, np.newaxis] * np.ones(start_points.shape[1]) + start_to_end = end_points - start_points + + with np.errstate(divide='ignore', invalid='ignore'): + adjustments = np.nan_to_num(np.cross(unit_axis, start_to_end / 2.0) / np.tan(theta / 2)) + arc_centers = start_points + 0.5 * start_to_end + adjustments + + c_to_start = start_points - arc_centers + c_to_perp = np.cross(unit_axis, c_to_start) + return arc_centers + np.cos(alpha * theta) * c_to_start + np.sin(alpha * theta) * c_to_perp return path