r"""Mobjects that are curved.
Examples
--------
.. manim:: UsefulAnnotations
:save_last_frame:
class UsefulAnnotations(Scene):
def construct(self):
m0 = Dot()
m1 = AnnotationDot()
m2 = LabeledDot("ii")
m3 = LabeledDot(MathTex(r"\alpha").set_color(ORANGE))
m4 = CurvedArrow(2*LEFT, 2*RIGHT, radius= -5)
m5 = CurvedArrow(2*LEFT, 2*RIGHT, radius= 8)
m6 = CurvedDoubleArrow(ORIGIN, 2*RIGHT)
self.add(m0, m1, m2, m3, m4, m5, m6)
for i, mobj in enumerate(self.mobjects):
mobj.shift(DOWN * (i-3))
"""
from __future__ import annotations
__all__ = [
"TipableVMobject",
"Arc",
"ArcBetweenPoints",
"CurvedArrow",
"CurvedDoubleArrow",
"Circle",
"Dot",
"AnnotationDot",
"LabeledDot",
"Ellipse",
"AnnularSector",
"Sector",
"Annulus",
"CubicBezier",
"ArcPolygon",
"ArcPolygonFromArcs",
"TangentialArc",
]
import itertools
import warnings
from typing import TYPE_CHECKING, Any, Self, cast
import numpy as np
from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor
from manim.utils.iterables import adjacent_pairs
from manim.utils.space_ops import (
angle_between_vectors,
angle_of_vector,
cartesian_to_spherical,
line_intersection,
perpendicular_bisector,
rotate_vector,
)
if TYPE_CHECKING:
from collections.abc import Iterable
import manim.mobject.geometry.tips as tips
from manim.mobject.geometry.line import Line
from manim.mobject.mobject import Mobject
from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
from manim.mobject.text.text_mobject import Text
from manim.typing import (
Point3D,
Point3DLike,
QuadraticSpline,
Vector3DLike,
)
[docs]
class TipableVMobject(VMobject, metaclass=ConvertToOpenGL):
"""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
"""
def __init__(
self,
tip_length: float = DEFAULT_ARROW_TIP_LENGTH,
normal_vector: Vector3DLike = OUT,
tip_style: dict | None = None,
**kwargs: Any,
) -> None:
self.tip_length: float = tip_length
self.normal_vector = normal_vector
self.tip_style: dict = tip_style if tip_style is not None else {}
super().__init__(**kwargs)
# Adding, Creating, Modifying tips
[docs]
def add_tip(
self,
tip: tips.ArrowTip | None = None,
tip_shape: type[tips.ArrowTip] | None = None,
tip_length: float | None = None,
tip_width: float | None = None,
at_start: bool = False,
) -> 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.
"""
if tip is None:
tip = self.create_tip(tip_shape, tip_length, tip_width, at_start)
else:
self.position_tip(tip, at_start)
self.reset_endpoints_based_on_tip(tip, at_start)
self.assign_tip_attr(tip, at_start)
self.add(tip)
return self
[docs]
def create_tip(
self,
tip_shape: type[tips.ArrowTip] | None = None,
tip_length: float | None = None,
tip_width: float | None = None,
at_start: bool = False,
) -> tips.ArrowTip:
"""Stylises the tip, positions it spatially, and returns
the newly instantiated tip to the caller.
"""
tip = self.get_unpositioned_tip(tip_shape, tip_length, tip_width)
self.position_tip(tip, at_start)
return tip
[docs]
def get_unpositioned_tip(
self,
tip_shape: type[tips.ArrowTip] | None = None,
tip_length: float | None = None,
tip_width: float | None = None,
) -> tips.ArrowTip | tips.ArrowTriangleFilledTip:
"""Returns a tip that has been stylistically configured,
but has not yet been given a position in space.
"""
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
style: dict[str, Any] = {}
if tip_shape is None:
tip_shape = ArrowTriangleFilledTip
if tip_shape is ArrowTriangleFilledTip:
if tip_width is None:
tip_width = self.get_default_tip_length()
style.update({"width": tip_width})
if tip_length is None:
tip_length = self.get_default_tip_length()
color = self.get_color()
style.update({"fill_color": color, "stroke_color": color})
style.update(self.tip_style)
tip = tip_shape(length=tip_length, **style)
return tip
def position_tip(self, tip: tips.ArrowTip, at_start: bool = False) -> tips.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()
angles = cartesian_to_spherical(handle - anchor)
tip.rotate(
angles[1] - PI - tip.tip_angle,
) # Rotates the tip along the azimuthal
if not hasattr(self, "_init_positioning_axis"):
axis = np.array(
[
np.sin(angles[1]),
-np.cos(angles[1]),
0,
]
) # Obtains the perpendicular of the tip
tip.rotate(
-angles[2] + PI / 2,
axis=axis,
) # Rotates the tip along the vertical wrt the axis
self._init_positioning_axis = axis
tip.shift(anchor - tip.tip_point)
return tip
def reset_endpoints_based_on_tip(self, tip: tips.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:
self.put_start_and_end_on(tip.base, self.get_end())
else:
self.put_start_and_end_on(self.get_start(), tip.base)
return self
def assign_tip_attr(self, tip: tips.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 = self.get_group_class()()
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)
if result.submobjects:
self.put_start_and_end_on(start, end)
return result
[docs]
def get_tips(self) -> VGroup:
"""Returns a VGroup (collection of VMobjects) containing
the TipableVMObject instance's tips.
"""
result = self.get_group_class()()
if hasattr(self, "tip"):
result.add(self.tip)
if hasattr(self, "start_tip"):
result.add(self.start_tip)
return result
[docs]
def get_tip(self) -> VMobject:
"""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:
tip: VMobject = tips[0]
return tip
def get_default_tip_length(self) -> float:
return self.tip_length
def get_first_handle(self) -> Point3D:
# Type inference of extracting an element from a list, is not
# supported by numpy, see this numpy issue
# https://github.com/numpy/numpy/issues/16544
first_handle: Point3D = self.points[1]
return first_handle
def get_last_handle(self) -> Point3D:
# Type inference of extracting an element from a list, is not
# supported by numpy, see this numpy issue
# https://github.com/numpy/numpy/issues/16544
last_handle: Point3D = self.points[-2]
return last_handle
[docs]
def get_end(self) -> Point3D:
if self.has_tip():
return self.tip.get_start()
else:
return super().get_end()
[docs]
def get_start(self) -> Point3D:
if self.has_start_tip():
return self.start_tip.get_start()
else:
return super().get_start()
def get_length(self) -> float:
start, end = self.get_start_and_end()
return float(np.linalg.norm(start - end))
[docs]
class Arc(TipableVMobject):
"""A circular arc.
Examples
--------
A simple arc of angle Pi.
.. manim:: ArcExample
:save_last_frame:
class ArcExample(Scene):
def construct(self):
self.add(Arc(angle=PI))
"""
def __init__(
self,
radius: float | None = 1.0,
start_angle: float = 0,
angle: float = TAU / 4,
num_components: int = 9,
arc_center: Point3DLike = ORIGIN,
**kwargs: Any,
):
if radius is None: # apparently None is passed by ArcBetweenPoints
radius = 1.0
self.radius = radius
self.num_components = num_components
self.arc_center: Point3D = np.asarray(arc_center)
self.start_angle = start_angle
self.angle = angle
self._failed_to_get_center: bool = False
super().__init__(**kwargs)
[docs]
def generate_points(self) -> None:
self._set_pre_positioned_points()
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
# Points are set a bit differently when rendering via OpenGL.
# TODO: refactor Arc so that only one strategy for setting points
# has to be used.
def init_points(self) -> None:
self.set_points(
Arc._create_quadratic_bezier_points(
angle=self.angle,
start_angle=self.start_angle,
n_components=self.num_components,
),
)
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
@staticmethod
def _create_quadratic_bezier_points(
angle: float, start_angle: float = 0, n_components: int = 8
) -> QuadraticSpline:
samples = np.array(
[
[np.cos(a), np.sin(a), 0]
for a in np.linspace(
start_angle,
start_angle + angle,
2 * n_components + 1,
)
],
)
theta = angle / n_components
samples[1::2] /= np.cos(theta / 2)
points = np.zeros((3 * n_components, 3))
points[0::3] = samples[0:-1:2]
points[1::3] = samples[1::2]
points[2::3] = samples[2::2]
return points
def _set_pre_positioned_points(self) -> None:
anchors = np.array(
[
np.cos(a) * RIGHT + np.sin(a) * UP
for a in np.linspace(
self.start_angle,
self.start_angle + self.angle,
self.num_components,
)
],
)
# Figure out which control points will give the
# Appropriate tangent lines to the circle
d_theta = self.angle / (self.num_components - 1.0)
tangent_vectors = np.zeros(anchors.shape)
# Rotate all 90 degrees, via (x, y) -> (-y, x)
tangent_vectors[:, 1] = anchors[:, 0]
tangent_vectors[:, 0] = -anchors[:, 1]
# Use tangent vectors to deduce anchors
factor = 4 / 3 * np.tan(d_theta / 4)
handles1 = anchors[:-1] + factor * tangent_vectors[:-1]
handles2 = anchors[1:] - factor * tangent_vectors[1:]
self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:])
[docs]
def get_arc_center(self, warning: bool = True) -> Point3D:
"""Looks at the normals to the first two
anchors, and finds their intersection points
"""
# First two anchors and handles
a1, h1, h2, a2 = self.points[:4]
if np.all(a1 == a2):
# For a1 and a2 to lie at the same point arc radius
# must be zero. Thus arc_center will also lie at
# that point.
return np.copy(a1)
# Tangent vectors
t1 = h1 - a1
t2 = h2 - a2
# Normals
n1 = rotate_vector(t1, TAU / 4)
n2 = rotate_vector(t2, TAU / 4)
try:
return line_intersection(line1=(a1, a1 + n1), line2=(a2, a2 + n2))
except Exception:
if warning:
warnings.warn(
"Can't find Arc center, using ORIGIN instead", stacklevel=1
)
self._failed_to_get_center = True
return np.array(ORIGIN)
def move_arc_center_to(self, point: Point3DLike) -> Self:
self.shift(point - self.get_arc_center())
return self
def stop_angle(self) -> float:
return cast(
float,
angle_of_vector(self.points[-1] - self.get_arc_center()) % TAU,
)
[docs]
class ArcBetweenPoints(Arc):
"""Inherits from Arc and additionally takes 2 points between which the arc is spanned.
Example
-------
.. manim:: ArcBetweenPointsExample
class ArcBetweenPointsExample(Scene):
def construct(self):
circle = Circle(radius=2, stroke_color=GREY)
dot_1 = Dot(color=GREEN).move_to([2, 0, 0]).scale(0.5)
dot_1_text = Tex("(2,0)").scale(0.5).next_to(dot_1, RIGHT).set_color(BLUE)
dot_2 = Dot(color=GREEN).move_to([0, 2, 0]).scale(0.5)
dot_2_text = Tex("(0,2)").scale(0.5).next_to(dot_2, UP).set_color(BLUE)
arc= ArcBetweenPoints(start=2 * RIGHT, end=2 * UP, stroke_color=YELLOW)
self.add(circle, dot_1, dot_2, dot_1_text, dot_2_text)
self.play(Create(arc))
"""
def __init__(
self,
start: Point3DLike,
end: Point3DLike,
angle: float = TAU / 4,
radius: float | None = None,
**kwargs: Any,
) -> None:
if radius is not None:
self.radius = radius
if radius < 0:
sign = -2
radius *= -1
else:
sign = 2
halfdist = np.linalg.norm(np.array(start) - np.array(end)) / 2
if radius < halfdist:
raise ValueError(
"""ArcBetweenPoints called with a radius that is
smaller than half the distance between the points.""",
)
arc_height = radius - np.sqrt(radius**2 - halfdist**2)
angle = np.arccos((radius - arc_height) / radius) * sign
super().__init__(radius=radius, angle=angle, **kwargs)
if angle == 0:
self.set_points_as_corners(np.array([LEFT, RIGHT]))
self.put_start_and_end_on(start, end)
if radius is None:
center = self.get_arc_center(warning=False)
if not self._failed_to_get_center:
# np.linalg.norm returns floating[Any] which is not compatible with float
self.radius = cast(
float, np.linalg.norm(np.array(start) - np.array(center))
)
else:
self.radius = np.inf
[docs]
class TangentialArc(ArcBetweenPoints):
"""
Construct an arc that is tangent to two intersecting lines.
You can choose any of the 4 possible corner arcs via the `corner` tuple.
corner = (s1, s2) where each si is ±1 to control direction along each line.
Examples
--------
.. manim:: TangentialArcExample
:save_last_frame:
class TangentialArcExample(Scene):
def construct(self):
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT)
line1.rotate(angle=31 * DEGREES, about_point=ORIGIN)
line2 = DashedLine(start=3 * UP, end=3 * DOWN)
line2.rotate(angle=12 * DEGREES, about_point=ORIGIN)
arc = TangentialArc(line1, line2, radius=2.25, corner=(1, 1), color=TEAL)
self.add(arc, line1, line2)
The following example shows all four possible corner configurations:
.. manim:: TangentialArcCorners
:save_last_frame:
class TangentialArcCorners(Scene):
def construct(self):
# Create two intersecting lines
line1 = DashedLine(start=3 * LEFT, end=3 * RIGHT, color=GREY)
line2 = DashedLine(start=3 * UP, end=3 * DOWN, color=GREY)
# All four corner configurations with different colors
arc_pp = TangentialArc(line1, line2, radius=1.5, corner=(1, 1), color=RED)
arc_pn = TangentialArc(line1, line2, radius=1.5, corner=(1, -1), color=GREEN)
arc_np = TangentialArc(line1, line2, radius=1.5, corner=(-1, 1), color=BLUE)
arc_nn = TangentialArc(line1, line2, radius=1.5, corner=(-1, -1), color=YELLOW)
# Labels for each arc
label_pp = Text("(1,1)", font_size=24, color=RED).next_to(arc_pp, UR, buff=0.1)
label_pn = Text("(1,-1)", font_size=24, color=GREEN).next_to(arc_pn, DR, buff=0.1)
label_np = Text("(-1,1)", font_size=24, color=BLUE).next_to(arc_np, UL, buff=0.1)
label_nn = Text("(-1,-1)", font_size=24, color=YELLOW).next_to(arc_nn, DL, buff=0.1)
self.add(line1, line2, arc_pp, arc_pn, arc_np, arc_nn)
self.add(label_pp, label_pn, label_np, label_nn)
"""
def __init__(
self,
line1: Line,
line2: Line,
radius: float,
corner: Any = (1, 1),
**kwargs: Any,
):
self.line1 = line1
self.line2 = line2
intersection_point = line_intersection(
[line1.get_start(), line1.get_end()], [line2.get_start(), line2.get_end()]
)
s1, s2 = corner
# Get unit vector for specified directions
unit_vector1 = s1 * line1.get_unit_vector()
unit_vector2 = s2 * line2.get_unit_vector()
corner_angle = angle_between_vectors(unit_vector1, unit_vector2)
tangent_point_distance = radius / np.tan(corner_angle / 2)
# tangent points
tangent_point1 = intersection_point + tangent_point_distance * unit_vector1
tangent_point2 = intersection_point + tangent_point_distance * unit_vector2
cross_product = (
unit_vector1[0] * unit_vector2[1] - unit_vector1[1] * unit_vector2[0]
)
# Determine start and end points based on orientation
if cross_product < 0:
# Counterclockwise orientation - standard order
start_point = tangent_point1
end_point = tangent_point2
else:
# Clockwise orientation - reverse the points
start_point = tangent_point2
end_point = tangent_point1
super().__init__(start=start_point, end=end_point, radius=radius, **kwargs)
[docs]
class CurvedArrow(ArcBetweenPoints):
def __init__(
self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any
) -> None:
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip)
super().__init__(start_point, end_point, **kwargs)
self.add_tip(tip_shape=tip_shape)
[docs]
class CurvedDoubleArrow(CurvedArrow):
def __init__(
self, start_point: Point3DLike, end_point: Point3DLike, **kwargs: Any
) -> None:
if "tip_shape_end" in kwargs:
kwargs["tip_shape"] = kwargs.pop("tip_shape_end")
from manim.mobject.geometry.tips import ArrowTriangleFilledTip
tip_shape_start = kwargs.pop("tip_shape_start", ArrowTriangleFilledTip)
super().__init__(start_point, end_point, **kwargs)
self.add_tip(at_start=True, tip_shape=tip_shape_start)
[docs]
class Circle(Arc):
"""A circle.
Parameters
----------
color
The color of the shape.
kwargs
Additional arguments to be passed to :class:`Arc`
Examples
--------
.. manim:: CircleExample
:save_last_frame:
class CircleExample(Scene):
def construct(self):
circle_1 = Circle(radius=1.0)
circle_2 = Circle(radius=1.5, color=GREEN)
circle_3 = Circle(radius=1.0, color=BLUE_B, fill_opacity=1)
circle_group = Group(circle_1, circle_2, circle_3).arrange(buff=1)
self.add(circle_group)
"""
def __init__(
self,
radius: float | None = None,
color: ParsableManimColor = RED,
**kwargs: Any,
) -> None:
super().__init__(
radius=radius,
start_angle=0,
angle=TAU,
color=color,
**kwargs,
)
[docs]
def surround(
self,
mobject: Mobject,
dim_to_match: int = 0,
stretch: bool = False,
buffer_factor: float = 1.2,
) -> Self:
"""Modifies a circle so that it surrounds a given mobject.
Parameters
----------
mobject
The mobject that the circle will be surrounding.
dim_to_match
buffer_factor
Scales the circle with respect to the mobject. A `buffer_factor` < 1 makes the circle smaller than the mobject.
stretch
Stretches the circle to fit more tightly around the mobject. Note: Does not work with :class:`Line`
Examples
--------
.. manim:: CircleSurround
:save_last_frame:
class CircleSurround(Scene):
def construct(self):
triangle1 = Triangle()
circle1 = Circle().surround(triangle1)
group1 = Group(triangle1,circle1) # treat the two mobjects as one
line2 = Line()
circle2 = Circle().surround(line2, buffer_factor=2.0)
group2 = Group(line2,circle2)
# buffer_factor < 1, so the circle is smaller than the square
square3 = Square()
circle3 = Circle().surround(square3, buffer_factor=0.5)
group3 = Group(square3, circle3)
group = Group(group1, group2, group3).arrange(buff=1)
self.add(group)
"""
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle single-dimension stretching
# Something goes wrong here when surrounding lines?
# TODO: Figure out and fix
self.replace(mobject, dim_to_match, stretch)
self.width = np.sqrt(mobject.width**2 + mobject.height**2)
return self.scale(buffer_factor)
[docs]
def point_at_angle(self, angle: float) -> Point3D:
"""Returns the position of a point on the circle.
Parameters
----------
angle
The angle of the point along the circle in radians.
Returns
-------
:class:`numpy.ndarray`
The location of the point along the circle's circumference.
Examples
--------
.. manim:: PointAtAngleExample
:save_last_frame:
class PointAtAngleExample(Scene):
def construct(self):
circle = Circle(radius=2.0)
p1 = circle.point_at_angle(PI/2)
p2 = circle.point_at_angle(270*DEGREES)
s1 = Square(side_length=0.25).move_to(p1)
s2 = Square(side_length=0.25).move_to(p2)
self.add(circle, s1, s2)
"""
proportion = angle / TAU
proportion -= np.floor(proportion)
return self.point_from_proportion(proportion)
[docs]
@staticmethod
def from_three_points(
p1: Point3DLike, p2: Point3DLike, p3: Point3DLike, **kwargs: Any
) -> Circle:
"""Returns a circle passing through the specified
three points.
Example
-------
.. manim:: CircleFromPointsExample
:save_last_frame:
class CircleFromPointsExample(Scene):
def construct(self):
circle = Circle.from_three_points(LEFT, LEFT + UP, UP * 2, color=RED)
dots = VGroup(
Dot(LEFT),
Dot(LEFT + UP),
Dot(UP * 2),
)
self.add(NumberPlane(), circle, dots)
"""
center = line_intersection(
perpendicular_bisector([np.asarray(p1), np.asarray(p2)]),
perpendicular_bisector([np.asarray(p2), np.asarray(p3)]),
)
# np.linalg.norm returns floating[Any] which is not compatible with float
radius = cast(float, np.linalg.norm(p1 - center))
return Circle(radius=radius, **kwargs).shift(center)
[docs]
class Dot(Circle):
"""A circle with a very small radius.
Parameters
----------
point
The location of the dot.
radius
The radius of the dot.
stroke_width
The thickness of the outline of the dot.
fill_opacity
The opacity of the dot's fill_colour
color
The color of the dot.
kwargs
Additional arguments to be passed to :class:`Circle`
Examples
--------
.. manim:: DotExample
:save_last_frame:
class DotExample(Scene):
def construct(self):
dot1 = Dot(point=LEFT, radius=0.08)
dot2 = Dot(point=ORIGIN)
dot3 = Dot(point=RIGHT)
self.add(dot1,dot2,dot3)
"""
def __init__(
self,
point: Point3DLike = ORIGIN,
radius: float = DEFAULT_DOT_RADIUS,
stroke_width: float = 0,
fill_opacity: float = 1.0,
color: ParsableManimColor = WHITE,
**kwargs: Any,
) -> None:
super().__init__(
arc_center=point,
radius=radius,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
color=color,
**kwargs,
)
[docs]
class AnnotationDot(Dot):
"""A dot with bigger radius and bold stroke to annotate scenes."""
def __init__(
self,
radius: float = DEFAULT_DOT_RADIUS * 1.3,
stroke_width: float = 5,
stroke_color: ParsableManimColor = WHITE,
fill_color: ParsableManimColor = BLUE,
**kwargs: Any,
) -> None:
super().__init__(
radius=radius,
stroke_width=stroke_width,
stroke_color=stroke_color,
fill_color=fill_color,
**kwargs,
)
[docs]
class LabeledDot(Dot):
"""A :class:`Dot` containing a label in its center.
Parameters
----------
label
The label of the :class:`Dot`. This is rendered as :class:`~.MathTex`
by default (i.e., when passing a :class:`str`), but other classes
representing rendered strings like :class:`~.Text` or :class:`~.Tex`
can be passed as well.
radius
The radius of the :class:`Dot`. If provided, the ``buff`` is ignored.
If ``None`` (the default), the radius is calculated based on the size
of the ``label`` and the ``buff``.
Examples
--------
.. manim:: SeveralLabeledDots
:save_last_frame:
class SeveralLabeledDots(Scene):
def construct(self):
sq = Square(fill_color=RED, fill_opacity=1)
self.add(sq)
dot1 = LabeledDot(Tex("42", color=RED))
dot2 = LabeledDot(MathTex("a", color=GREEN))
dot3 = LabeledDot(Text("ii", color=BLUE))
dot4 = LabeledDot("3")
dot1.next_to(sq, UL)
dot2.next_to(sq, UR)
dot3.next_to(sq, DL)
dot4.next_to(sq, DR)
self.add(dot1, dot2, dot3, dot4)
"""
def __init__(
self,
label: str | SingleStringMathTex | Text | Tex,
radius: float | None = None,
buff: float = SMALL_BUFF,
**kwargs: Any,
) -> None:
if isinstance(label, str):
from manim import MathTex
rendered_label: VMobject = MathTex(label, color=BLACK)
else:
rendered_label = label
if radius is None:
radius = buff + float(
np.linalg.norm([rendered_label.width, rendered_label.height]) / 2
)
super().__init__(radius=radius, **kwargs)
rendered_label.move_to(self.get_center())
self.add(rendered_label)
[docs]
class Ellipse(Circle):
"""A circular shape; oval, circle.
Parameters
----------
width
The horizontal width of the ellipse.
height
The vertical height of the ellipse.
kwargs
Additional arguments to be passed to :class:`Circle`.
Examples
--------
.. manim:: EllipseExample
:save_last_frame:
class EllipseExample(Scene):
def construct(self):
ellipse_1 = Ellipse(width=2.0, height=4.0, color=BLUE_B)
ellipse_2 = Ellipse(width=4.0, height=1.0, color=BLUE_D)
ellipse_group = Group(ellipse_1,ellipse_2).arrange(buff=1)
self.add(ellipse_group)
"""
def __init__(self, width: float = 2, height: float = 1, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.stretch_to_fit_width(width)
self.stretch_to_fit_height(height)
[docs]
class AnnularSector(Arc):
"""A sector of an annulus.
Parameters
----------
inner_radius
The inside radius of the Annular Sector.
outer_radius
The outside radius of the Annular Sector.
angle
The clockwise angle of the Annular Sector.
start_angle
The starting clockwise angle of the Annular Sector.
fill_opacity
The opacity of the color filled in the Annular Sector.
stroke_width
The stroke width of the Annular Sector.
color
The color filled into the Annular Sector.
Examples
--------
.. manim:: AnnularSectorExample
:save_last_frame:
class AnnularSectorExample(Scene):
def construct(self):
# Changes background color to clearly visualize changes in fill_opacity.
self.camera.background_color = WHITE
# The default parameter start_angle is 0, so the AnnularSector starts from the +x-axis.
s1 = AnnularSector(color=YELLOW).move_to(2 * UL)
# Different inner_radius and outer_radius than the default.
s2 = AnnularSector(inner_radius=1.5, outer_radius=2, angle=45 * DEGREES, color=RED).move_to(2 * UR)
# fill_opacity is typically a number > 0 and <= 1. If fill_opacity=0, the AnnularSector is transparent.
s3 = AnnularSector(inner_radius=1, outer_radius=1.5, angle=PI, fill_opacity=0.25, color=BLUE).move_to(2 * DL)
# With a negative value for the angle, the AnnularSector is drawn clockwise from the start value.
s4 = AnnularSector(inner_radius=1, outer_radius=1.5, angle=-3 * PI / 2, color=GREEN).move_to(2 * DR)
self.add(s1, s2, s3, s4)
"""
def __init__(
self,
inner_radius: float = 1,
outer_radius: float = 2,
angle: float = TAU / 4,
start_angle: float = 0,
fill_opacity: float = 1,
stroke_width: float = 0,
color: ParsableManimColor = WHITE,
**kwargs: Any,
) -> None:
self.inner_radius = inner_radius
self.outer_radius = outer_radius
super().__init__(
start_angle=start_angle,
angle=angle,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
color=color,
**kwargs,
)
[docs]
def generate_points(self) -> None:
inner_arc, outer_arc = (
Arc(
start_angle=self.start_angle,
angle=self.angle,
radius=radius,
arc_center=self.arc_center,
)
for radius in (self.inner_radius, self.outer_radius)
)
outer_arc.reverse_points()
self.append_points(inner_arc.points)
self.add_line_to(outer_arc.points[0])
self.append_points(outer_arc.points)
self.add_line_to(inner_arc.points[0])
def init_points(self) -> None:
self.generate_points()
[docs]
class Sector(AnnularSector):
"""A sector of a circle.
Examples
--------
.. manim:: ExampleSector
:save_last_frame:
class ExampleSector(Scene):
def construct(self):
sector = Sector(radius=2)
sector2 = Sector(radius=2.5, angle=60*DEGREES).move_to([-3, 0, 0])
sector.set_color(RED)
sector2.set_color(PINK)
self.add(sector, sector2)
"""
def __init__(self, radius: float = 1, **kwargs: Any) -> None:
super().__init__(inner_radius=0, outer_radius=radius, **kwargs)
[docs]
class Annulus(Circle):
"""Region between two concentric :class:`Circles <.Circle>`.
Parameters
----------
inner_radius
The radius of the inner :class:`Circle`.
outer_radius
The radius of the outer :class:`Circle`.
kwargs
Additional arguments to be passed to :class:`Annulus`
Examples
--------
.. manim:: AnnulusExample
:save_last_frame:
class AnnulusExample(Scene):
def construct(self):
annulus_1 = Annulus(inner_radius=0.5, outer_radius=1).shift(UP)
annulus_2 = Annulus(inner_radius=0.3, outer_radius=0.6, color=RED).next_to(annulus_1, DOWN)
self.add(annulus_1, annulus_2)
"""
def __init__(
self,
inner_radius: float = 1,
outer_radius: float = 2,
fill_opacity: float = 1,
stroke_width: float = 0,
color: ParsableManimColor = WHITE,
mark_paths_closed: bool = False,
**kwargs: Any,
) -> None:
self.mark_paths_closed = mark_paths_closed # is this even used?
self.inner_radius = inner_radius
self.outer_radius = outer_radius
super().__init__(
fill_opacity=fill_opacity, stroke_width=stroke_width, color=color, **kwargs
)
[docs]
def generate_points(self) -> None:
self.radius = self.outer_radius
outer_circle = Circle(radius=self.outer_radius)
inner_circle = Circle(radius=self.inner_radius)
inner_circle.reverse_points()
self.append_points(outer_circle.points)
self.append_points(inner_circle.points)
self.shift(self.arc_center)
def init_points(self) -> None:
self.generate_points()
[docs]
class CubicBezier(VMobject, metaclass=ConvertToOpenGL):
"""A cubic Bézier curve.
Example
-------
.. manim:: BezierSplineExample
:save_last_frame:
class BezierSplineExample(Scene):
def construct(self):
p1 = np.array([-3, 1, 0])
p1b = p1 + [1, 0, 0]
d1 = Dot(point=p1).set_color(BLUE)
l1 = Line(p1, p1b)
p2 = np.array([3, -1, 0])
p2b = p2 - [1, 0, 0]
d2 = Dot(point=p2).set_color(RED)
l2 = Line(p2, p2b)
bezier = CubicBezier(p1b, p1b + 3 * RIGHT, p2b - 3 * RIGHT, p2b)
self.add(l1, d1, l2, d2, bezier)
"""
def __init__(
self,
start_anchor: Point3DLike,
start_handle: Point3DLike,
end_handle: Point3DLike,
end_anchor: Point3DLike,
**kwargs: Any,
) -> None:
super().__init__(**kwargs)
self.add_cubic_bezier_curve(start_anchor, start_handle, end_handle, end_anchor)
[docs]
class ArcPolygon(VMobject, metaclass=ConvertToOpenGL):
"""A generalized polygon allowing for points to be connected with arcs.
This version tries to stick close to the way :class:`Polygon` is used. Points
can be passed to it directly which are used to generate the according arcs
(using :class:`ArcBetweenPoints`). An angle or radius can be passed to it to
use across all arcs, but to configure arcs individually an ``arc_config`` list
has to be passed with the syntax explained below.
Parameters
----------
vertices
A list of vertices, start and end points for the arc segments.
angle
The angle used for constructing the arcs. If no other parameters
are set, this angle is used to construct all arcs.
radius
The circle radius used to construct the arcs. If specified,
overrides the specified ``angle``.
arc_config
When passing a ``dict``, its content will be passed as keyword
arguments to :class:`~.ArcBetweenPoints`. Otherwise, a list
of dictionaries containing values that are passed as keyword
arguments for every individual arc can be passed.
kwargs
Further keyword arguments that are passed to the constructor of
:class:`~.VMobject`.
Attributes
----------
arcs : :class:`list`
The arcs created from the input parameters::
>>> from manim import ArcPolygon
>>> ap = ArcPolygon([0, 0, 0], [2, 0, 0], [0, 2, 0])
>>> ap.arcs
[ArcBetweenPoints, ArcBetweenPoints, ArcBetweenPoints]
.. tip::
Two instances of :class:`ArcPolygon` can be transformed properly into one
another as well. Be advised that any arc initialized with ``angle=0``
will actually be a straight line, so if a straight section should seamlessly
transform into an arced section or vice versa, initialize the straight section
with a negligible angle instead (such as ``angle=0.0001``).
.. note::
There is an alternative version (:class:`ArcPolygonFromArcs`) that is instantiated
with pre-defined arcs.
See Also
--------
:class:`ArcPolygonFromArcs`
Examples
--------
.. manim:: SeveralArcPolygons
class SeveralArcPolygons(Scene):
def construct(self):
a = [0, 0, 0]
b = [2, 0, 0]
c = [0, 2, 0]
ap1 = ArcPolygon(a, b, c, radius=2)
ap2 = ArcPolygon(a, b, c, angle=45*DEGREES)
ap3 = ArcPolygon(a, b, c, arc_config={'radius': 1.7, 'color': RED})
ap4 = ArcPolygon(a, b, c, color=RED, fill_opacity=1,
arc_config=[{'radius': 1.7, 'color': RED},
{'angle': 20*DEGREES, 'color': BLUE},
{'radius': 1}])
ap_group = VGroup(ap1, ap2, ap3, ap4).arrange()
self.play(*[Create(ap) for ap in [ap1, ap2, ap3, ap4]])
self.wait()
For further examples see :class:`ArcPolygonFromArcs`.
"""
def __init__(
self,
*vertices: Point3DLike,
angle: float = PI / 4,
radius: float | None = None,
arc_config: list[dict] | None = None,
**kwargs: Any,
) -> None:
n = len(vertices)
point_pairs = [(vertices[k], vertices[(k + 1) % n]) for k in range(n)]
if not arc_config:
if radius:
all_arc_configs: Iterable[dict] = itertools.repeat(
{"radius": radius}, len(point_pairs)
)
else:
all_arc_configs = itertools.repeat({"angle": angle}, len(point_pairs))
elif isinstance(arc_config, dict):
all_arc_configs = itertools.repeat(arc_config, len(point_pairs))
else:
assert len(arc_config) == n
all_arc_configs = arc_config
arcs = [
ArcBetweenPoints(*pair, **conf)
for (pair, conf) in zip(point_pairs, all_arc_configs, strict=True)
]
super().__init__(**kwargs)
# Adding the arcs like this makes ArcPolygon double as a VGroup.
# Also makes changes to the ArcPolygon, such as scaling, affect
# the arcs, so that their new values are usable.
self.add(*arcs)
for arc in arcs:
self.append_points(arc.points)
# This enables the use of ArcPolygon.arcs as a convenience
# because ArcPolygon[0] returns itself, not the first Arc.
self.arcs = arcs
[docs]
class ArcPolygonFromArcs(VMobject, metaclass=ConvertToOpenGL):
"""A generalized polygon allowing for points to be connected with arcs.
This version takes in pre-defined arcs to generate the arcpolygon and introduces
little new syntax. However unlike :class:`Polygon` it can't be created with points
directly.
For proper appearance the passed arcs should connect seamlessly:
``[a,b][b,c][c,a]``
If there are any gaps between the arcs, those will be filled in
with straight lines, which can be used deliberately for any straight
sections. Arcs can also be passed as straight lines such as an arc
initialized with ``angle=0``.
Parameters
----------
arcs
These are the arcs from which the arcpolygon is assembled.
kwargs
Keyword arguments that are passed to the constructor of
:class:`~.VMobject`. Affects how the ArcPolygon itself is drawn,
but doesn't affect passed arcs.
Attributes
----------
arcs
The arcs used to initialize the ArcPolygonFromArcs::
>>> from manim import ArcPolygonFromArcs, Arc, ArcBetweenPoints
>>> ap = ArcPolygonFromArcs(Arc(), ArcBetweenPoints([1,0,0], [0,1,0]), Arc())
>>> ap.arcs
[Arc, ArcBetweenPoints, Arc]
.. tip::
Two instances of :class:`ArcPolygon` can be transformed properly into
one another as well. Be advised that any arc initialized with ``angle=0``
will actually be a straight line, so if a straight section should seamlessly
transform into an arced section or vice versa, initialize the straight
section with a negligible angle instead (such as ``angle=0.0001``).
.. note::
There is an alternative version (:class:`ArcPolygon`) that can be instantiated
with points.
.. seealso::
:class:`ArcPolygon`
Examples
--------
One example of an arcpolygon is the Reuleaux triangle.
Instead of 3 straight lines connecting the outer points,
a Reuleaux triangle has 3 arcs connecting those points,
making a shape with constant width.
Passed arcs are stored as submobjects in the arcpolygon.
This means that the arcs are changed along with the arcpolygon,
for example when it's shifted, and these arcs can be manipulated
after the arcpolygon has been initialized.
Also both the arcs contained in an :class:`~.ArcPolygonFromArcs`, as well as the
arcpolygon itself are drawn, which affects draw time in :class:`~.Create`
for example. In most cases the arcs themselves don't
need to be drawn, in which case they can be passed as invisible.
.. manim:: ArcPolygonExample
class ArcPolygonExample(Scene):
def construct(self):
arc_conf = {"stroke_width": 0}
poly_conf = {"stroke_width": 10, "stroke_color": BLUE,
"fill_opacity": 1, "color": PURPLE}
a = [-1, 0, 0]
b = [1, 0, 0]
c = [0, np.sqrt(3), 0]
arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf)
arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf)
arc2 = ArcBetweenPoints(c, a, radius=2, **arc_conf)
reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf)
self.play(FadeIn(reuleaux_tri))
self.wait(2)
The arcpolygon itself can also be hidden so that instead only the contained
arcs are drawn. This can be used to easily debug arcs or to highlight them.
.. manim:: ArcPolygonExample2
class ArcPolygonExample2(Scene):
def construct(self):
arc_conf = {"stroke_width": 3, "stroke_color": BLUE,
"fill_opacity": 0.5, "color": GREEN}
poly_conf = {"color": None}
a = [-1, 0, 0]
b = [1, 0, 0]
c = [0, np.sqrt(3), 0]
arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf)
arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf)
arc2 = ArcBetweenPoints(c, a, radius=2, stroke_color=RED)
reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf)
self.play(FadeIn(reuleaux_tri))
self.wait(2)
"""
def __init__(self, *arcs: Arc | ArcBetweenPoints, **kwargs: Any) -> None:
if not all(isinstance(m, (Arc, ArcBetweenPoints)) for m in arcs):
raise ValueError(
"All ArcPolygon submobjects must be of type Arc/ArcBetweenPoints",
)
super().__init__(**kwargs)
# Adding the arcs like this makes ArcPolygonFromArcs double as a VGroup.
# Also makes changes to the ArcPolygonFromArcs, such as scaling, affect
# the arcs, so that their new values are usable.
self.add(*arcs)
# This enables the use of ArcPolygonFromArcs.arcs as a convenience
# because ArcPolygonFromArcs[0] returns itself, not the first Arc.
self.arcs = [*arcs]
from .line import Line
for arc1, arc2 in adjacent_pairs(arcs):
self.append_points(arc1.points)
line = Line(arc1.get_end(), arc2.get_start())
len_ratio = line.get_length() / arc1.get_arc_length()
if np.isnan(len_ratio) or np.isinf(len_ratio):
continue
line.insert_n_curves(int(arc1.get_num_curves() * len_ratio))
self.append_points(line.points)