mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00

* Only use -no-pdf for xelatex rendering * Instead of tracking du and dv points on surface, track points off the surface in the normal direction This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points. * Have density of anchor points on arcs depend on arc length * Allow for specifying true normals and orientation of Sphere * Change miter threshold on stroke shader * Add get_start_and_end to DashedLine * Add min_total_width option to DecimalNumber * Have BackgroundRectangle.set_style absorb (and ignore) added configuration Note, this feels suboptimal * Add LineBrace * Update font_size adjustment in Tex
1081 lines
33 KiB
Python
1081 lines
33 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 GREY_A, RED, WHITE, BLACK
|
|
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):
|
|
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):
|
|
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):
|
|
def __init__(
|
|
self,
|
|
start_point: Vect3,
|
|
end_point: Vect3,
|
|
**kwargs
|
|
):
|
|
super().__init__(start_point, end_point, **kwargs)
|
|
self.add_tip()
|
|
|
|
|
|
class CurvedDoubleArrow(CurvedArrow):
|
|
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):
|
|
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):
|
|
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 = WHITE,
|
|
**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):
|
|
def __init__(
|
|
self,
|
|
point: Vect3 = ORIGIN,
|
|
radius: float = DEFAULT_SMALL_DOT_RADIUS,
|
|
**kwargs
|
|
):
|
|
super().__init__(point, radius=radius, **kwargs)
|
|
|
|
|
|
class Ellipse(Circle):
|
|
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):
|
|
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 = GREY_A,
|
|
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):
|
|
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):
|
|
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 = GREY_A,
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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 = GREY_A,
|
|
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):
|
|
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 = GREY_A,
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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):
|
|
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 = WHITE,
|
|
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):
|
|
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):
|
|
def __init__(self, side_length: float = 2.0, **kwargs):
|
|
super().__init__(side_length, side_length, **kwargs)
|
|
|
|
|
|
class RoundedRectangle(Rectangle):
|
|
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)
|