Source code for manim.mobject.geometry.arc

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",
]

import itertools
import math
import warnings
from typing import TYPE_CHECKING, Sequence

import numpy as np
from colour import Color

from manim.constants import *
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VMobject
from manim.utils.color import *
from manim.utils.iterables import adjacent_pairs
from manim.utils.space_ops import (
    angle_of_vector,
    cartesian_to_spherical,
    line_intersection,
    perpendicular_bisector,
    rotate_vector,
)

if TYPE_CHECKING:
    from manim.mobject.mobject import Mobject
    from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex
    from manim.mobject.text.text_mobject import Text


[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=DEFAULT_ARROW_TIP_LENGTH, normal_vector=OUT, tip_style={}, **kwargs, ): self.tip_length = tip_length self.normal_vector = normal_vector self.tip_style = tip_style super().__init__(**kwargs) # Adding, Creating, Modifying tips
[docs] def add_tip(self, tip=None, tip_shape=None, tip_length=None, at_start=False): """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, at_start) else: self.position_tip(tip, at_start) self.reset_endpoints_based_on_tip(tip, at_start) self.asign_tip_attr(tip, at_start) self.add(tip) return self
[docs] def create_tip(self, tip_shape=None, tip_length=None, at_start=False): """Stylises the tip, positions it spatially, and returns the newly instantiated tip to the caller. """ tip = self.get_unpositioned_tip(tip_shape, tip_length) self.position_tip(tip, at_start) return tip
[docs] def get_unpositioned_tip(self, tip_shape=None, tip_length=None): """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 if tip_shape is None: tip_shape = ArrowTriangleFilledTip if tip_length is None: tip_length = self.get_default_tip_length() color = self.get_color() style = {"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, at_start=False): # 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.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, at_start): 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 asign_tip_attr(self, tip, at_start): if at_start: self.start_tip = tip else: self.tip = tip return self # Checking for tips def has_tip(self): return hasattr(self, "tip") and self.tip in self def has_start_tip(self): return hasattr(self, "start_tip") and self.start_tip in self # Getters def pop_tips(self): 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) self.put_start_and_end_on(start, end) return result
[docs] def get_tips(self): """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): """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): return self.tip_length def get_first_handle(self): return self.points[1] def get_last_handle(self): return self.points[-2]
[docs] def get_end(self): if self.has_tip(): return self.tip.get_start() else: return super().get_end()
[docs] def get_start(self): if self.has_start_tip(): return self.start_tip.get_start() else: return super().get_start()
def get_length(self): start, end = self.get_start_and_end() return 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 = 1.0, start_angle=0, angle=TAU / 4, num_components=9, arc_center=ORIGIN, **kwargs, ): if radius is None: # apparently None is passed by ArcBetweenPoints radius = 1.0 self.radius = radius self.num_components = num_components self.arc_center = arc_center self.start_angle = start_angle self.angle = angle self._failed_to_get_center = False super().__init__(**kwargs)
[docs] def generate_points(self): 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): 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, start_angle=0, n_components=8): 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): 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 handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1] handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:] self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:])
[docs] def get_arc_center(self, warning=True): """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 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") self._failed_to_get_center = True return np.array(ORIGIN)
def move_arc_center_to(self, point): self.shift(point - self.get_arc_center()) return self def stop_angle(self): return 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, end, angle=TAU / 4, radius=None, **kwargs): 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 - math.sqrt(radius**2 - halfdist**2) angle = math.acos((radius - arc_height) / radius) * sign super().__init__(radius=radius, angle=angle, **kwargs) if angle == 0: self.set_points_as_corners([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: self.radius = np.linalg.norm(np.array(start) - np.array(center)) else: self.radius = math.inf
[docs]class CurvedArrow(ArcBetweenPoints): def __init__(self, start_point, end_point, **kwargs): 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, end_point, **kwargs): 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: Color | str = RED, **kwargs, ): 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, ): """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): """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) """ start_angle = angle_of_vector(self.points[0] - self.get_center()) proportion = (angle - start_angle) / TAU proportion -= math.floor(proportion) return self.point_from_proportion(proportion)
[docs] @staticmethod def from_three_points( p1: Sequence[float], p2: Sequence[float], p3: Sequence[float], **kwargs ): """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([p1, p2]), perpendicular_bisector([p2, p3]), ) radius = 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: list | np.ndarray = ORIGIN, radius: float = DEFAULT_DOT_RADIUS, stroke_width: float = 0, fill_opacity: float = 1.0, color: Color | str = WHITE, **kwargs, ): 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=5, stroke_color=WHITE, fill_color=BLUE, **kwargs, ): 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 ``None`` (the default), the radius is calculated based on the size of the ``label``. 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, **kwargs, ) -> None: if isinstance(label, str): from manim import MathTex rendered_label = MathTex(label, color=BLACK) else: rendered_label = label if radius is None: radius = 0.1 + max(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): super().__init__(**kwargs) self.stretch_to_fit_width(width) self.stretch_to_fit_height(height)
[docs]class AnnularSector(Arc): """ 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=1, outer_radius=2, angle=TAU / 4, start_angle=0, fill_opacity=1, stroke_width=0, color=WHITE, **kwargs, ): 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): 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])
init_points = generate_points
[docs]class Sector(AnnularSector): """ Examples -------- .. manim:: ExampleSector :save_last_frame: class ExampleSector(Scene): def construct(self): sector = Sector(outer_radius=2, inner_radius=1) sector2 = Sector(outer_radius=2.5, inner_radius=0.8).move_to([-3, 0, 0]) sector.set_color(RED) sector2.set_color(PINK) self.add(sector, sector2) """ def __init__(self, outer_radius=1, inner_radius=0, **kwargs): super().__init__(inner_radius=inner_radius, outer_radius=outer_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 | None = 1, outer_radius: float | None = 2, fill_opacity=1, stroke_width=0, color=WHITE, mark_paths_closed=False, **kwargs, ): 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): 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)
init_points = generate_points
[docs]class CubicBezier(VMobject, metaclass=ConvertToOpenGL): """ 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, start_handle, end_handle, end_anchor, **kwargs): 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: list | np.ndarray, angle: float = PI / 4, radius: float | None = None, arc_config: list[dict] | None = None, **kwargs, ): 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 = 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) ] 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): 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 math.isnan(len_ratio) or math.isinf(len_ratio): continue line.insert_n_curves(int(arc1.get_num_curves() * len_ratio)) self.append_points(line.points)