from __future__ import annotations import math import numpy as np from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UL, UP, UR from manimlib.constants import RED, BLACK, DEFAULT_MOBJECT_COLOR, DEFAULT_LIGHT_COLOR from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF from manimlib.constants import DEG, PI, TAU from manimlib.mobject.mobject import Mobject from manimlib.mobject.types.vectorized_mobject import DashedVMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import quadratic_bezier_points_for_arc from manimlib.utils.iterables import adjacent_n_tuples from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import clip from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import angle_between_vectors from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import cross2d from manimlib.utils.space_ops import compass_directions from manimlib.utils.space_ops import find_intersection from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import normalize from manimlib.utils.space_ops import rotate_vector from manimlib.utils.space_ops import rotation_matrix_transpose from manimlib.utils.space_ops import rotation_between_vectors from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Iterable, Optional from manimlib.typing import ManimColor, Vect3, Vect3Array, Self DEFAULT_DOT_RADIUS = 0.08 DEFAULT_SMALL_DOT_RADIUS = 0.04 DEFAULT_DASH_LENGTH = 0.05 DEFAULT_ARROW_TIP_LENGTH = 0.35 DEFAULT_ARROW_TIP_WIDTH = 0.35 # Deprecate? class TipableVMobject(VMobject): """ Meant for shared functionality between Arc and Line. Functionality can be classified broadly into these groups: * Adding, Creating, Modifying tips - add_tip calls create_tip, before pushing the new tip into the TipableVMobject's list of submobjects - stylistic and positional configuration * Checking for tips - Boolean checks for whether the TipableVMobject has a tip and a starting tip * Getters - Straightforward accessors, returning information pertaining to the TipableVMobject instance's tip(s), its length etc """ tip_config: dict = dict( fill_opacity=1.0, stroke_width=0.0, tip_style=0.0, # triangle=0, inner_smooth=1, dot=2 ) # Adding, Creating, Modifying tips def add_tip(self, at_start: bool = False, **kwargs) -> Self: """ Adds a tip to the TipableVMobject instance, recognising that the endpoints might need to be switched if it's a 'starting tip' or not. """ tip = self.create_tip(at_start, **kwargs) self.reset_endpoints_based_on_tip(tip, at_start) self.asign_tip_attr(tip, at_start) tip.set_color(self.get_stroke_color()) self.add(tip) return self def create_tip(self, at_start: bool = False, **kwargs) -> ArrowTip: """ Stylises the tip, positions it spacially, and returns the newly instantiated tip to the caller. """ tip = self.get_unpositioned_tip(**kwargs) self.position_tip(tip, at_start) return tip def get_unpositioned_tip(self, **kwargs) -> ArrowTip: """ Returns a tip that has been stylistically configured, but has not yet been given a position in space. """ config = dict() config.update(self.tip_config) config.update(kwargs) return ArrowTip(**config) def position_tip(self, tip: ArrowTip, at_start: bool = False) -> ArrowTip: # Last two control points, defining both # the end, and the tangency direction if at_start: anchor = self.get_start() handle = self.get_first_handle() else: handle = self.get_last_handle() anchor = self.get_end() tip.rotate(angle_of_vector(handle - anchor) - PI - tip.get_angle()) tip.shift(anchor - tip.get_tip_point()) return tip def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool) -> Self: if self.get_length() == 0: # Zero length, put_start_and_end_on wouldn't # work return self if at_start: start = tip.get_base() end = self.get_end() else: start = self.get_start() end = tip.get_base() self.put_start_and_end_on(start, end) return self def asign_tip_attr(self, tip: ArrowTip, at_start: bool) -> Self: if at_start: self.start_tip = tip else: self.tip = tip return self # Checking for tips def has_tip(self) -> bool: return hasattr(self, "tip") and self.tip in self def has_start_tip(self) -> bool: return hasattr(self, "start_tip") and self.start_tip in self # Getters def pop_tips(self) -> VGroup: start, end = self.get_start_and_end() result = VGroup() if self.has_tip(): result.add(self.tip) self.remove(self.tip) if self.has_start_tip(): result.add(self.start_tip) self.remove(self.start_tip) self.put_start_and_end_on(start, end) return result def get_tips(self) -> VGroup: """ Returns a VGroup (collection of VMobjects) containing the TipableVMObject instance's tips. """ result = VGroup() if hasattr(self, "tip"): result.add(self.tip) if hasattr(self, "start_tip"): result.add(self.start_tip) return result def get_tip(self) -> ArrowTip: """Returns the TipableVMobject instance's (first) tip, otherwise throws an exception.""" tips = self.get_tips() if len(tips) == 0: raise Exception("tip not found") else: return tips[0] def get_default_tip_length(self) -> float: return self.tip_length def get_first_handle(self) -> Vect3: return self.get_points()[1] def get_last_handle(self) -> Vect3: return self.get_points()[-2] def get_end(self) -> Vect3: if self.has_tip(): return self.tip.get_start() else: return VMobject.get_end(self) def get_start(self) -> Vect3: if self.has_start_tip(): return self.start_tip.get_start() else: return VMobject.get_start(self) def get_length(self) -> float: start, end = self.get_start_and_end() return get_norm(start - end) class Arc(TipableVMobject): ''' Creates an arc. Parameters ----- start_angle : float Starting angle of the arc in radians. (Angles are measured counter-clockwise) angle : float Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise) radius : float Radius of the arc arc_center : array_like Center of the arc Examples : arc = Arc(start_angle=TAU/4, angle=TAU/2, radius=3, arc_center=ORIGIN) arc = Arc(angle=TAU/4, radius=4.5, arc_center=(1,2,0), color=BLUE) Returns ----- out : Arc object An Arc object satisfying the specified parameters ''' def __init__( self, start_angle: float = 0, angle: float = TAU / 4, radius: float = 1.0, n_components: Optional[int] = None, arc_center: Vect3 = ORIGIN, **kwargs ): super().__init__(**kwargs) if n_components is None: # 16 components for a full circle n_components = int(15 * (abs(angle) / TAU)) + 1 self.set_points(quadratic_bezier_points_for_arc(angle, n_components)) self.rotate(start_angle, about_point=ORIGIN) self.scale(radius, about_point=ORIGIN) self.shift(arc_center) def get_arc_center(self) -> Vect3: """ Looks at the normals to the first two anchors, and finds their intersection points """ # First two anchors and handles a1, h, a2 = self.get_points()[:3] # Tangent vectors t1 = h - a1 t2 = h - a2 # Normals n1 = rotate_vector(t1, TAU / 4) n2 = rotate_vector(t2, TAU / 4) return find_intersection(a1, n1, a2, n2) def get_start_angle(self) -> float: angle = angle_of_vector(self.get_start() - self.get_arc_center()) return angle % TAU def get_stop_angle(self) -> float: angle = angle_of_vector(self.get_end() - self.get_arc_center()) return angle % TAU def move_arc_center_to(self, point: Vect3) -> Self: self.shift(point - self.get_arc_center()) return self class ArcBetweenPoints(Arc): ''' Creates an arc passing through the specified points with "angle" as the angle subtended at its center. Parameters ----- start : array_like Starting point of the arc end : array_like Ending point of the arc angle : float Angle subtended by the arc at its center in radians. (Angles are measured counter-clockwise) Examples : arc = ArcBetweenPoints(start=(0, 0, 0), end=(1, 2, 0), angle=TAU / 2) arc = ArcBetweenPoints(start=(-2, 3, 0), end=(1, 2, 0), angle=-TAU / 12, color=BLUE) Returns ----- out : ArcBetweenPoints object An ArcBetweenPoints object satisfying the specified parameters ''' def __init__( self, start: Vect3, end: Vect3, angle: float = TAU / 4, **kwargs ): super().__init__(angle=angle, **kwargs) if angle == 0: self.set_points_as_corners([LEFT, RIGHT]) self.put_start_and_end_on(start, end) class CurvedArrow(ArcBetweenPoints): ''' Creates a curved arrow passing through the specified points with "angle" as the angle subtended at its center. Parameters ----- start_point : array_like Starting point of the curved arrow end_point : array_like Ending point of the curved arrow angle : float Angle subtended by the curved arrow at its center in radians. (Angles are measured counter-clockwise) Examples : curvedArrow = CurvedArrow(start_point=(0, 0, 0), end_point=(1, 2, 0), angle=TAU/2) curvedArrow = CurvedArrow(start_point=(-2, 3, 0), end_point=(1, 2, 0), angle=-TAU/12, color=BLUE) Returns ----- out : CurvedArrow object A CurvedArrow object satisfying the specified parameters ''' def __init__( self, start_point: Vect3, end_point: Vect3, **kwargs ): super().__init__(start_point, end_point, **kwargs) self.add_tip() class CurvedDoubleArrow(CurvedArrow): ''' Creates a curved double arrow passing through the specified points with "angle" as the angle subtended at its center. Parameters ----- start_point : array_like Starting point of the curved double arrow end_point : array_like Ending point of the curved double arrow angle : float Angle subtended by the curved double arrow at its center in radians. (Angles are measured counter-clockwise) Examples : curvedDoubleArrow = CurvedDoubleArrow(start_point = (0, 0, 0), end_point = (1, 2, 0), angle = TAU/2) curvedDoubleArrow = CurvedDoubleArrow(start_point = (-2, 3, 0), end_point = (1, 2, 0), angle = -TAU/12, color = BLUE) Returns ----- out : CurvedDoubleArrow object A CurvedDoubleArrow object satisfying the specified parameters ''' def __init__( self, start_point: Vect3, end_point: Vect3, **kwargs ): super().__init__(start_point, end_point, **kwargs) self.add_tip(at_start=True) class Circle(Arc): ''' Creates a circle. Parameters ----- radius : float Radius of the circle arc_center : array_like Center of the circle Examples : circle = Circle(radius=2, arc_center=(1,2,0)) circle = Circle(radius=3.14, arc_center=2 * LEFT + UP, color=DARK_BLUE) Returns ----- out : Circle object A Circle object satisfying the specified parameters ''' def __init__( self, start_angle: float = 0, stroke_color: ManimColor = RED, **kwargs ): super().__init__( start_angle, TAU, stroke_color=stroke_color, **kwargs ) def surround( self, mobject: Mobject, dim_to_match: int = 0, stretch: bool = False, buff: float = MED_SMALL_BUFF ) -> Self: self.replace(mobject, dim_to_match, stretch) self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0) self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1) return self def point_at_angle(self, angle: float) -> Vect3: start_angle = self.get_start_angle() return self.point_from_proportion( ((angle - start_angle) % TAU) / TAU ) def get_radius(self) -> float: return get_norm(self.get_start() - self.get_center()) class Dot(Circle): ''' Creates a dot. Dot is a filled white circle with no bounary and DEFAULT_DOT_RADIUS. Parameters ----- point : array_like Coordinates of center of the dot. Examples : dot = Dot(point=(1, 2, 0)) Returns ----- out : Dot object A Dot object satisfying the specified parameters ''' def __init__( self, point: Vect3 = ORIGIN, radius: float = DEFAULT_DOT_RADIUS, stroke_color: ManimColor = BLACK, stroke_width: float = 0.0, fill_opacity: float = 1.0, fill_color: ManimColor = DEFAULT_MOBJECT_COLOR, **kwargs ): super().__init__( arc_center=point, radius=radius, stroke_color=stroke_color, stroke_width=stroke_width, fill_opacity=fill_opacity, fill_color=fill_color, **kwargs ) class SmallDot(Dot): ''' Creates a small dot. Small dot is a filled white circle with no bounary and DEFAULT_SMALL_DOT_RADIUS. Parameters ----- point : array_like Coordinates of center of the small dot. Examples : smallDot = SmallDot(point=(1, 2, 0)) Returns ----- out : SmallDot object A SmallDot object satisfying the specified parameters ''' def __init__( self, point: Vect3 = ORIGIN, radius: float = DEFAULT_SMALL_DOT_RADIUS, **kwargs ): super().__init__(point, radius=radius, **kwargs) class Ellipse(Circle): ''' Creates an ellipse. Parameters ----- width : float Width of the ellipse height : float Height of the ellipse arc_center : array_like Coordinates of center of the ellipse Examples : ellipse = Ellipse(width=4, height=1, arc_center=(3, 3, 0)) ellipse = Ellipse(width=2, height=5, arc_center=ORIGIN, color=BLUE) Returns ----- out : Ellipse object An Ellipse object satisfying the specified parameters ''' def __init__( self, width: float = 2.0, height: float = 1.0, **kwargs ): super().__init__(**kwargs) self.set_width(width, stretch=True) self.set_height(height, stretch=True) class AnnularSector(VMobject): ''' Creates an annular sector. Parameters ----- inner_radius : float Inner radius of the annular sector outer_radius : float Outer radius of the annular sector start_angle : float Starting angle of the annular sector (Angles are measured counter-clockwise) angle : float Angle subtended at the center of the annular sector (Angles are measured counter-clockwise) arc_center : array_like Coordinates of center of the annular sector Examples : annularSector = AnnularSector(inner_radius=1, outer_radius=2, angle=TAU/2, start_angle=TAU*3/4, arc_center=(1,-2,0)) Returns ----- out : AnnularSector object An AnnularSector object satisfying the specified parameters ''' def __init__( self, angle: float = TAU / 4, start_angle: float = 0.0, inner_radius: float = 1.0, outer_radius: float = 2.0, arc_center: Vect3 = ORIGIN, fill_color: ManimColor = DEFAULT_LIGHT_COLOR, fill_opacity: float = 1.0, stroke_width: float = 0.0, **kwargs, ): super().__init__( fill_color=fill_color, fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs, ) # Initialize points inner_arc, outer_arc = [ Arc( start_angle=start_angle, angle=angle, radius=radius, arc_center=arc_center, ) for radius in (inner_radius, outer_radius) ] self.set_points(inner_arc.get_points()[::-1]) # Reverse self.add_line_to(outer_arc.get_points()[0]) self.add_subpath(outer_arc.get_points()) self.add_line_to(inner_arc.get_points()[-1]) class Sector(AnnularSector): ''' Creates a sector. Parameters ----- outer_radius : float Radius of the sector start_angle : float Starting angle of the sector in radians. (Angles are measured counter-clockwise) angle : float Angle subtended by the sector at its center in radians. (Angles are measured counter-clockwise) arc_center : array_like Coordinates of center of the sector Examples : sector = Sector(outer_radius=1, start_angle=TAU/3, angle=TAU/2, arc_center=[0,3,0]) sector = Sector(outer_radius=3, start_angle=TAU/4, angle=TAU/4, arc_center=ORIGIN, color=PINK) Returns ----- out : Sector object An Sector object satisfying the specified parameters ''' def __init__( self, angle: float = TAU / 4, radius: float = 1.0, **kwargs ): super().__init__( angle, inner_radius=0, outer_radius=radius, **kwargs ) class Annulus(VMobject): ''' Creates an annulus. Parameters ----- inner_radius : float Inner radius of the annulus outer_radius : float Outer radius of the annulus arc_center : array_like Coordinates of center of the annulus Examples : annulus = Annulus(inner_radius=2, outer_radius=3, arc_center=(1, -1, 0)) annulus = Annulus(inner_radius=2, outer_radius=3, stroke_width=20, stroke_color=RED, fill_color=BLUE, arc_center=ORIGIN) Returns ----- out : Annulus object An Annulus object satisfying the specified parameters ''' def __init__( self, inner_radius: float = 1.0, outer_radius: float = 2.0, fill_opacity: float = 1.0, stroke_width: float = 0.0, fill_color: ManimColor = DEFAULT_LIGHT_COLOR, center: Vect3 = ORIGIN, **kwargs, ): super().__init__( fill_color=fill_color, fill_opacity=fill_opacity, stroke_width=stroke_width, **kwargs, ) self.radius = outer_radius outer_path = outer_radius * quadratic_bezier_points_for_arc(TAU) inner_path = inner_radius * quadratic_bezier_points_for_arc(-TAU) self.add_subpath(outer_path) self.add_subpath(inner_path) self.shift(center) class Line(TipableVMobject): ''' Creates a line joining the points "start" and "end". Parameters ----- start : array_like Starting point of the line end : array_like Ending point of the line Examples : line = Line((0, 0, 0), (3, 0, 0)) line = Line((1, 2, 0), (-2, -3, 0), color=BLUE) Returns ----- out : Line object A Line object satisfying the specified parameters ''' def __init__( self, start: Vect3 | Mobject = LEFT, end: Vect3 | Mobject = RIGHT, buff: float = 0.0, path_arc: float = 0.0, **kwargs ): super().__init__(**kwargs) self.path_arc = path_arc self.buff = buff self.set_start_and_end_attrs(start, end) self.set_points_by_ends(self.start, self.end, buff, path_arc) def set_points_by_ends( self, start: Vect3, end: Vect3, buff: float = 0, path_arc: float = 0 ) -> Self: self.clear_points() self.start_new_path(start) self.add_arc_to(end, path_arc) # Apply buffer if buff > 0: length = self.get_arc_length() alpha = min(buff / length, 0.5) self.pointwise_become_partial(self, alpha, 1 - alpha) return self def set_path_arc(self, new_value: float) -> Self: self.path_arc = new_value self.init_points() return self def set_start_and_end_attrs(self, start: Vect3 | Mobject, end: Vect3 | Mobject): # If either start or end are Mobjects, this # gives their centers rough_start = self.pointify(start) rough_end = self.pointify(end) vect = normalize(rough_end - rough_start) # Now that we know the direction between them, # we can find the appropriate boundary point from # start and end, if they're mobjects self.start = self.pointify(start, vect) self.end = self.pointify(end, -vect) def pointify( self, mob_or_point: Mobject | Vect3, direction: Vect3 | None = None ) -> Vect3: """ Take an argument passed into Line (or subclass) and turn it into a 3d point. """ if isinstance(mob_or_point, Mobject): mob = mob_or_point if direction is None: return mob.get_center() else: return mob.get_continuous_bounding_box_point(direction) else: point = mob_or_point result = np.zeros(self.dim) result[:len(point)] = point return result def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self: curr_start, curr_end = self.get_start_and_end() if np.isclose(curr_start, curr_end).all(): # Handle null lines more gracefully self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) return self return super().put_start_and_end_on(start, end) def get_vector(self) -> Vect3: return self.get_end() - self.get_start() def get_unit_vector(self) -> Vect3: return normalize(self.get_vector()) def get_angle(self) -> float: return angle_of_vector(self.get_vector()) def get_projection(self, point: Vect3) -> Vect3: """ Return projection of a point onto the line """ unit_vect = self.get_unit_vector() start = self.get_start() return start + np.dot(point - start, unit_vect) * unit_vect def get_slope(self) -> float: return np.tan(self.get_angle()) def set_angle(self, angle: float, about_point: Optional[Vect3] = None) -> Self: if about_point is None: about_point = self.get_start() self.rotate( angle - self.get_angle(), about_point=about_point, ) return self def set_length(self, length: float, **kwargs): self.scale(length / self.get_length(), **kwargs) return self def get_arc_length(self) -> float: arc_len = get_norm(self.get_vector()) if self.path_arc > 0: arc_len *= self.path_arc / (2 * math.sin(self.path_arc / 2)) return arc_len class DashedLine(Line): ''' Creates a dashed line joining the points "start" and "end". Parameters ----- start : array_like Starting point of the dashed line end : array_like Ending point of the dashed line dash_length : float length of each dash Examples : line = DashedLine((0, 0, 0), (3, 0, 0)) line = DashedLine((1, 2, 3), (4, 5, 6), dash_length=0.01) Returns ----- out : DashedLine object A DashedLine object satisfying the specified parameters ''' def __init__( self, start: Vect3 = LEFT, end: Vect3 = RIGHT, dash_length: float = DEFAULT_DASH_LENGTH, positive_space_ratio: float = 0.5, **kwargs ): super().__init__(start, end, **kwargs) num_dashes = self.calculate_num_dashes(dash_length, positive_space_ratio) dashes = DashedVMobject( self, num_dashes=num_dashes, positive_space_ratio=positive_space_ratio ) self.clear_points() self.add(*dashes) def calculate_num_dashes(self, dash_length: float, positive_space_ratio: float) -> int: try: full_length = dash_length / positive_space_ratio return int(np.ceil(self.get_length() / full_length)) except ZeroDivisionError: return 1 def get_start(self) -> Vect3: if len(self.submobjects) > 0: return self.submobjects[0].get_start() else: return Line.get_start(self) def get_end(self) -> Vect3: if len(self.submobjects) > 0: return self.submobjects[-1].get_end() else: return Line.get_end(self) def get_start_and_end(self) -> Tuple[Vect3, Vect3]: return self.get_start(), self.get_end() def get_first_handle(self) -> Vect3: return self.submobjects[0].get_points()[1] def get_last_handle(self) -> Vect3: return self.submobjects[-1].get_points()[-2] class TangentLine(Line): ''' Creates a tangent line to the specified vectorized math object. Parameters ----- vmob : VMobject object Vectorized math object which the line will be tangent to alpha : float Point on the perimeter of the vectorized math object. It takes value between 0 and 1 both inclusive. length : float Length of the tangent line Examples : circle = Circle(arc_center=ORIGIN, radius=3, color=GREEN) tangentLine = TangentLine(vmob=circle, alpha=1/3, length=6, color=BLUE) Returns ----- out : TangentLine object A TangentLine object satisfying the specified parameters ''' def __init__( self, vmob: VMobject, alpha: float, length: float = 2, d_alpha: float = 1e-6, **kwargs ): a1 = clip(alpha - d_alpha, 0, 1) a2 = clip(alpha + d_alpha, 0, 1) super().__init__(vmob.pfp(a1), vmob.pfp(a2), **kwargs) self.scale(length / self.get_length()) class Elbow(VMobject): ''' Creates an elbow. Elbow is an L-shaped shaped object. Parameters ----- width : float Width of the elbow angle : float Angle of the elbow in radians with the horizontal. (Angles are measured counter-clockwise) Examples : line = Elbow(width=2, angle=TAU/16) Returns ----- out : Elbow object A Elbow object satisfying the specified parameters ''' def __init__( self, width: float = 0.2, angle: float = 0, **kwargs ): super().__init__(**kwargs) self.set_points_as_corners([UP, UR, RIGHT]) self.set_width(width, about_point=ORIGIN) self.rotate(angle, about_point=ORIGIN) class StrokeArrow(Line): def __init__( self, start: Vect3 | Mobject, end: Vect3 | Mobject, stroke_color: ManimColor = DEFAULT_LIGHT_COLOR, stroke_width: float = 5, buff: float = 0.25, tip_width_ratio: float = 5, tip_len_to_width: float = 0.0075, max_tip_length_to_length_ratio: float = 0.3, max_width_to_length_ratio: float = 8.0, **kwargs, ): self.tip_width_ratio = tip_width_ratio self.tip_len_to_width = tip_len_to_width self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio self.max_width_to_length_ratio = max_width_to_length_ratio self.n_tip_points = 3 self.original_stroke_width = stroke_width super().__init__( start, end, stroke_color=stroke_color, stroke_width=stroke_width, buff=buff, **kwargs ) def set_points_by_ends( self, start: Vect3, end: Vect3, buff: float = 0, path_arc: float = 0 ) -> Self: super().set_points_by_ends(start, end, buff, path_arc) self.insert_tip_anchor() self.create_tip_with_stroke_width() return self def insert_tip_anchor(self) -> Self: prev_end = self.get_end() arc_len = self.get_arc_length() tip_len = self.get_stroke_width() * self.tip_width_ratio * self.tip_len_to_width if tip_len >= self.max_tip_length_to_length_ratio * arc_len or arc_len == 0: alpha = self.max_tip_length_to_length_ratio else: alpha = tip_len / arc_len if self.path_arc > 0 and self.buff > 0: self.insert_n_curves(10) # Is this needed? self.pointwise_become_partial(self, 0.0, 1.0 - alpha) self.add_line_to(self.get_end()) self.add_line_to(prev_end) self.n_tip_points = 3 return self @Mobject.affects_data def create_tip_with_stroke_width(self) -> Self: if self.get_num_points() < 3: return self stroke_width = min( self.original_stroke_width, self.max_width_to_length_ratio * self.get_length(), ) tip_width = self.tip_width_ratio * stroke_width ntp = self.n_tip_points self.data['stroke_width'][:-ntp] = self.data['stroke_width'][0] self.data['stroke_width'][-ntp:, 0] = tip_width * np.linspace(1, 0, ntp) return self def reset_tip(self) -> Self: self.set_points_by_ends( self.get_start(), self.get_end(), path_arc=self.path_arc ) return self def set_stroke( self, color: ManimColor | Iterable[ManimColor] | None = None, width: float | Iterable[float] | None = None, *args, **kwargs ) -> Self: super().set_stroke(color=color, width=width, *args, **kwargs) self.original_stroke_width = self.get_stroke_width() if self.has_points(): self.reset_tip() return self def _handle_scale_side_effects(self, scale_factor: float) -> Self: if scale_factor != 1.0: self.reset_tip() return self class Arrow(Line): ''' Creates an arrow. Parameters ---------- start : array_like Starting point of the arrow end : array_like Ending point of the arrow buff : float, optional Buffer distance from the start and end points. Default is MED_SMALL_BUFF. path_arc : float, optional If set to a non-zero value, the arrow will be curved to subtend a circle by this angle. Default is 0 (straight arrow). thickness : float, optional How wide should the base of the arrow be. This affects the shaft width. Default is 3.0. tip_width_ratio : float, optional Ratio of the tip width to the shaft width. Default is 5. tip_angle : float, optional Angle of the arrow tip in radians. Default is PI/3 (60 degrees). max_tip_length_to_length_ratio : float, optional Maximum ratio of tip length to total arrow length. Prevents tips from being too large relative to the arrow. Default is 0.5. max_width_to_length_ratio : float, optional Maximum ratio of arrow width to total arrow length. Prevents arrows from being too wide relative to their length. Default is 0.1. **kwargs Additional keyword arguments passed to the parent Line class. Examples -------- >>> arrow = Arrow((0, 0, 0), (3, 0, 0)) >>> curved_arrow = Arrow(LEFT, RIGHT, path_arc=PI/4) >>> thick_arrow = Arrow(UP, DOWN, thickness=5.0, tip_width_ratio=3) Returns ------- Arrow An Arrow object satisfying the specified parameters. ''' tickness_multiplier = 0.015 def __init__( self, start: Vect3 | Mobject = LEFT, end: Vect3 | Mobject = LEFT, buff: float = MED_SMALL_BUFF, path_arc: float = 0, fill_color: ManimColor = DEFAULT_LIGHT_COLOR, fill_opacity: float = 1.0, stroke_width: float = 0.0, thickness: float = 3.0, tip_width_ratio: float = 5, tip_angle: float = PI / 3, max_tip_length_to_length_ratio: float = 0.5, max_width_to_length_ratio: float = 0.1, **kwargs, ): self.thickness = thickness self.tip_width_ratio = tip_width_ratio self.tip_angle = tip_angle self.max_tip_length_to_length_ratio = max_tip_length_to_length_ratio self.max_width_to_length_ratio = max_width_to_length_ratio super().__init__( start, end, fill_color=fill_color, fill_opacity=fill_opacity, stroke_width=stroke_width, buff=buff, path_arc=path_arc, **kwargs ) def get_key_dimensions(self, length): width = self.thickness * self.tickness_multiplier w_ratio = fdiv(self.max_width_to_length_ratio, fdiv(width, length)) if w_ratio < 1: width *= w_ratio tip_width = self.tip_width_ratio * width tip_length = tip_width / (2 * np.tan(self.tip_angle / 2)) t_ratio = fdiv(self.max_tip_length_to_length_ratio, fdiv(tip_length, length)) if t_ratio < 1: tip_length *= t_ratio tip_width *= t_ratio return width, tip_width, tip_length def set_points_by_ends( self, start: Vect3, end: Vect3, buff: float = 0, path_arc: float = 0 ) -> Self: vect = end - start length = max(get_norm(vect), 1e-8) # More systematic min? unit_vect = normalize(vect) # Find the right tip length and thickness width, tip_width, tip_length = self.get_key_dimensions(length - buff) # Adjust start and end based on buff if path_arc == 0: start = start + buff * unit_vect end = end - buff * unit_vect else: R = length / 2 / math.sin(path_arc / 2) midpoint = 0.5 * (start + end) center = midpoint + rotate_vector(0.5 * vect, PI / 2) / math.tan(path_arc / 2) sign = 1 start = center + rotate_vector(start - center, buff / R) end = center + rotate_vector(end - center, -buff / R) path_arc -= (2 * buff + tip_length) / R vect = end - start length = get_norm(vect) # Find points for the stem, imagining an arrow pointed to the left if path_arc == 0: points1 = (length - tip_length) * np.array([RIGHT, 0.5 * RIGHT, ORIGIN]) points1 += width * UP / 2 points2 = points1[::-1] + width * DOWN else: # Find arc points points1 = quadratic_bezier_points_for_arc(path_arc) points2 = np.array(points1[::-1]) points1 *= (R + width / 2) points2 *= (R - width / 2) rot_T = rotation_matrix_transpose(PI / 2 - path_arc, OUT) for points in points1, points2: points[:] = np.dot(points, rot_T) points += R * DOWN self.set_points(points1) # Tip self.add_line_to(tip_width * UP / 2) self.add_line_to(tip_length * LEFT) self.tip_index = len(self.get_points()) - 1 self.add_line_to(tip_width * DOWN / 2) self.add_line_to(points2[0]) # Close it out self.add_subpath(points2) self.add_line_to(points1[0]) # Reposition to match proper start and end self.rotate(angle_of_vector(vect) - self.get_angle()) self.rotate( PI / 2 - np.arccos(normalize(vect)[2]), axis=rotate_vector(self.get_unit_vector(), -PI / 2), ) self.shift(start - self.get_start()) return self def reset_points_around_ends(self) -> Self: self.set_points_by_ends( self.get_start().copy(), self.get_end().copy(), path_arc=self.path_arc ) return self def get_start(self) -> Vect3: points = self.get_points() return 0.5 * (points[0] + points[-3]) def get_end(self) -> Vect3: return self.get_points()[self.tip_index] def get_start_and_end(self): return (self.get_start(), self.get_end()) def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self: self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc) return self def scale(self, *args, **kwargs) -> Self: super().scale(*args, **kwargs) self.reset_points_around_ends() return self def set_thickness(self, thickness: float) -> Self: self.thickness = thickness self.reset_points_around_ends() return self def set_path_arc(self, path_arc: float) -> Self: self.path_arc = path_arc self.reset_points_around_ends() return self def set_perpendicular_to_camera(self, camera_frame): to_cam = camera_frame.get_implied_camera_location() - self.get_center() normal = self.get_unit_normal() axis = normalize(self.get_vector()) # Project to be perpendicular to axis trg_normal = to_cam - np.dot(to_cam, axis) * axis mat = rotation_between_vectors(normal, trg_normal) self.apply_matrix(mat, about_point=self.get_start()) return self class Vector(Arrow): ''' Creates a vector. Vector is an arrow with start point as ORIGIN Parameters ----- direction : array_like Coordinates of direction of the arrow Examples : arrow = Vector(direction=LEFT) Returns ----- out : Vector object A Vector object satisfying the specified parameters ''' def __init__( self, direction: Vect3 = RIGHT, buff: float = 0.0, **kwargs ): if len(direction) == 2: direction = np.hstack([direction, 0]) super().__init__(ORIGIN, direction, buff=buff, **kwargs) class CubicBezier(VMobject): ''' Creates a cubic Bézier curve. A cubic Bézier curve is defined by four control points: two anchor points (start and end) and two handle points that control the curvature. The curve starts at the first anchor point, is "pulled" toward the handle points, and ends at the second anchor point. Parameters ---------- a0 : array_like First anchor point (starting point of the curve). h0 : array_like First handle point (controls the initial direction and curvature from a0). h1 : array_like Second handle point (controls the final direction and curvature toward a1). a1 : array_like Second anchor point (ending point of the curve). **kwargs Additional keyword arguments passed to the parent VMobject class, such as stroke_color, stroke_width, fill_color, fill_opacity, etc. Returns ------- CubicBezier A CubicBezier object representing the specified cubic Bézier curve. ''' def __init__( self, a0: Vect3, h0: Vect3, h1: Vect3, a1: Vect3, **kwargs ): super().__init__(**kwargs) self.add_cubic_bezier_curve(a0, h0, h1, a1) class Polygon(VMobject): ''' Creates a polygon by joining the specified vertices. Parameters ----- *vertices : array_like Vertex of the polygon Examples : triangle = Polygon((-3,0,0), (3,0,0), (0,3,0)) Returns ----- out : Polygon object A Polygon object satisfying the specified parameters ''' def __init__( self, *vertices: Vect3, **kwargs ): super().__init__(**kwargs) self.set_points_as_corners([*vertices, vertices[0]]) def get_vertices(self) -> Vect3Array: return self.get_start_anchors() def round_corners(self, radius: Optional[float] = None) -> Self: if radius is None: verts = self.get_vertices() min_edge_length = min( get_norm(v1 - v2) for v1, v2 in zip(verts, verts[1:]) if not np.isclose(v1, v2).all() ) radius = 0.25 * min_edge_length vertices = self.get_vertices() arcs = [] for v1, v2, v3 in adjacent_n_tuples(vertices, 3): vect1 = normalize(v2 - v1) vect2 = normalize(v3 - v2) angle = angle_between_vectors(vect1, vect2) # Distance between vertex and start of the arc cut_off_length = radius * np.tan(angle / 2) # Negative radius gives concave curves sign = float(np.sign(radius * cross2d(vect1, vect2))) arc = ArcBetweenPoints( v2 - vect1 * cut_off_length, v2 + vect2 * cut_off_length, angle=sign * angle, n_components=2, ) arcs.append(arc) self.clear_points() # To ensure that we loop through starting with last arcs = [arcs[-1], *arcs[:-1]] for arc1, arc2 in adjacent_pairs(arcs): self.add_subpath(arc1.get_points()) self.add_line_to(arc2.get_start()) return self class Polyline(VMobject): def __init__( self, *vertices: Vect3, **kwargs ): super().__init__(**kwargs) self.set_points_as_corners(vertices) class RegularPolygon(Polygon): ''' Creates a regular polygon of edge length 1 at the center of the screen. Parameters ----- n : int Number of vertices of the regular polygon start_angle : float Starting angle of the regular polygon in radians. (Angles are measured counter-clockwise) Examples : pentagon = RegularPolygon(n=5, start_angle=30 * DEGREES) Returns ----- out : RegularPolygon object A RegularPolygon object satisfying the specified parameters ''' def __init__( self, n: int = 6, radius: float = 1.0, start_angle: float | None = None, **kwargs ): # Defaults to 0 for odd, 90 for even if start_angle is None: start_angle = (n % 2) * 90 * DEG start_vect = rotate_vector(radius * RIGHT, start_angle) vertices = compass_directions(n, start_vect) super().__init__(*vertices, **kwargs) class Triangle(RegularPolygon): ''' Creates a triangle of edge length 1 at the center of the screen. Parameters ----- start_angle : float Starting angle of the triangle in radians. (Angles are measured counter-clockwise) Examples : triangle = Triangle(start_angle=45 * DEGREES) Returns ----- out : Triangle object A Triangle object satisfying the specified parameters ''' def __init__(self, **kwargs): super().__init__(n=3, **kwargs) class ArrowTip(Triangle): def __init__( self, angle: float = 0, width: float = DEFAULT_ARROW_TIP_WIDTH, length: float = DEFAULT_ARROW_TIP_LENGTH, fill_opacity: float = 1.0, fill_color: ManimColor = DEFAULT_MOBJECT_COLOR, stroke_width: float = 0.0, tip_style: int = 0, # triangle=0, inner_smooth=1, dot=2 **kwargs ): super().__init__( start_angle=0, fill_opacity=fill_opacity, fill_color=fill_color, stroke_width=stroke_width, **kwargs ) self.set_height(width) self.set_width(length, stretch=True) if tip_style == 1: self.set_height(length * 0.9, stretch=True) self.data["point"][4] += np.array([0.6 * length, 0, 0]) elif tip_style == 2: h = length / 2 self.set_points(Dot().set_width(h).get_points()) self.rotate(angle) def get_base(self) -> Vect3: return self.point_from_proportion(0.5) def get_tip_point(self) -> Vect3: return self.get_points()[0] def get_vector(self) -> Vect3: return self.get_tip_point() - self.get_base() def get_angle(self) -> float: return angle_of_vector(self.get_vector()) def get_length(self) -> float: return get_norm(self.get_vector()) class Rectangle(Polygon): ''' Creates a rectangle at the center of the screen. Parameters ----- width : float Width of the rectangle height : float Height of the rectangle Examples : rectangle = Rectangle(width=3, height=4, color=BLUE) Returns ----- out : Rectangle object A Rectangle object satisfying the specified parameters ''' def __init__( self, width: float = 4.0, height: float = 2.0, **kwargs ): super().__init__(UR, UL, DL, DR, **kwargs) self.set_width(width, stretch=True) self.set_height(height, stretch=True) def surround(self, mobject, buff=SMALL_BUFF) -> Self: target_shape = np.array(mobject.get_shape()) + 2 * buff self.set_shape(*target_shape) self.move_to(mobject) return self class Square(Rectangle): ''' Creates a square at the center of the screen. Parameters ----- side_length : float Edge length of the square Examples : square = Square(side_length=5, color=PINK) Returns ----- out : Square object A Square object satisfying the specified parameters ''' def __init__(self, side_length: float = 2.0, **kwargs): super().__init__(side_length, side_length, **kwargs) class RoundedRectangle(Rectangle): ''' Creates a rectangle with round edges at the center of the screen. Parameters ----- width : float Width of the rounded rectangle height : float Height of the rounded rectangle corner_radius : float Corner radius of the rectangle Examples : rRectangle = RoundedRectangle(width=3, height=4, corner_radius=1, color=BLUE) Returns ----- out : RoundedRectangle object A RoundedRectangle object satisfying the specified parameters ''' def __init__( self, width: float = 4.0, height: float = 2.0, corner_radius: float = 0.5, **kwargs ): super().__init__(width, height, **kwargs) self.round_corners(corner_radius)