Files
manim/manimlib/mobject/geometry.py

1537 lines
47 KiB
Python

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)