mirror of
https://github.com/3b1b/manim.git
synced 2026-04-23 02:02:50 -04:00
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import numpy as np
|
|
|
|
from manimlib.constants import OUT
|
|
from manimlib.utils.bezier import interpolate
|
|
from manimlib.utils.space_ops import get_norm
|
|
from manimlib.utils.space_ops import rotation_matrix_transpose
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Callable
|
|
from manimlib.typing import Vect3, Vect3Array
|
|
|
|
|
|
STRAIGHT_PATH_THRESHOLD = 0.01
|
|
|
|
|
|
def straight_path(
|
|
start_points: np.ndarray,
|
|
end_points: np.ndarray,
|
|
alpha: float
|
|
) -> np.ndarray:
|
|
"""
|
|
Same function as interpolate, but renamed to reflect
|
|
intent of being used to determine how a set of points move
|
|
to another set. For instance, it should be a specific case
|
|
of path_along_arc
|
|
"""
|
|
return interpolate(start_points, end_points, alpha)
|
|
|
|
|
|
def path_along_arc(
|
|
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 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):
|
|
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
|
|
|
|
|
|
def clockwise_path() -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]:
|
|
return path_along_arc(-np.pi)
|
|
|
|
|
|
def counterclockwise_path() -> Callable[[Vect3Array, Vect3Array, float], Vect3Array]:
|
|
return path_along_arc(np.pi)
|