r"""Mobjects that are simple geometric shapes."""
from __future__ import annotations
__all__ = [
"Polygram",
"Polygon",
"RegularPolygram",
"RegularPolygon",
"Star",
"Triangle",
"Rectangle",
"Square",
"RoundedRectangle",
"Cutout",
]
from math import ceil
from typing import TYPE_CHECKING
import numpy as np
from manim.constants import *
from manim.mobject.geometry.arc import ArcBetweenPoints
from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
from manim.utils.color import BLUE, WHITE, ParsableManimColor
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices
if TYPE_CHECKING:
from typing_extensions import Self
from manim.typing import Point3D, Point3D_Array
from manim.utils.color import ParsableManimColor
[docs]class Polygram(VMobject, metaclass=ConvertToOpenGL):
"""A generalized :class:`Polygon`, allowing for disconnected sets of edges.
Parameters
----------
vertex_groups
The groups of vertices making up the :class:`Polygram`.
The first vertex in each group is repeated to close the shape.
Each point must be 3-dimensional: ``[x,y,z]``
color
The color of the :class:`Polygram`.
kwargs
Forwarded to the parent constructor.
Examples
--------
.. manim:: PolygramExample
import numpy as np
class PolygramExample(Scene):
def construct(self):
hexagram = Polygram(
[[0, 2, 0], [-np.sqrt(3), -1, 0], [np.sqrt(3), -1, 0]],
[[-np.sqrt(3), 1, 0], [0, -2, 0], [np.sqrt(3), 1, 0]],
)
self.add(hexagram)
dot = Dot()
self.play(MoveAlongPath(dot, hexagram), run_time=5, rate_func=linear)
self.remove(dot)
self.wait()
"""
def __init__(
self, *vertex_groups: Point3D, color: ParsableManimColor = BLUE, **kwargs
):
super().__init__(color=color, **kwargs)
for vertices in vertex_groups:
first_vertex, *vertices = vertices
first_vertex = np.array(first_vertex)
self.start_new_path(first_vertex)
self.add_points_as_corners(
[*(np.array(vertex) for vertex in vertices), first_vertex],
)
[docs] def get_vertices(self) -> Point3D_Array:
"""Gets the vertices of the :class:`Polygram`.
Returns
-------
:class:`numpy.ndarray`
The vertices of the :class:`Polygram`.
Examples
--------
::
>>> sq = Square()
>>> sq.get_vertices()
array([[ 1., 1., 0.],
[-1., 1., 0.],
[-1., -1., 0.],
[ 1., -1., 0.]])
"""
return self.get_start_anchors()
[docs] def get_vertex_groups(self) -> np.ndarray[Point3D_Array]:
"""Gets the vertex groups of the :class:`Polygram`.
Returns
-------
:class:`numpy.ndarray`
The vertex groups of the :class:`Polygram`.
Examples
--------
::
>>> poly = Polygram([ORIGIN, RIGHT, UP], [LEFT, LEFT + UP, 2 * LEFT])
>>> poly.get_vertex_groups()
array([[[ 0., 0., 0.],
[ 1., 0., 0.],
[ 0., 1., 0.]],
<BLANKLINE>
[[-1., 0., 0.],
[-1., 1., 0.],
[-2., 0., 0.]]])
"""
vertex_groups = []
group = []
for start, end in zip(self.get_start_anchors(), self.get_end_anchors()):
group.append(start)
if self.consider_points_equals(end, group[0]):
vertex_groups.append(group)
group = []
return np.array(vertex_groups)
[docs] def round_corners(
self,
radius: float | list[float] = 0.5,
evenly_distribute_anchors: bool = False,
components_per_rounded_corner: int = 2,
) -> Self:
"""Rounds off the corners of the :class:`Polygram`.
Parameters
----------
radius
The curvature of the corners of the :class:`Polygram`.
evenly_distribute_anchors
Break long line segments into proportionally-sized segments.
components_per_rounded_corner
The number of points used to represent the rounded corner curve.
.. seealso::
:class:`.~RoundedRectangle`
.. note::
If `radius` is supplied as a single value, then the same radius
will be applied to all corners. If `radius` is a list, then the
individual values will be applied sequentially, with the first
corner receiving `radius[0]`, the second corner receiving
`radius[1]`, etc. The radius list will be repeated as necessary.
The `components_per_rounded_corner` value is provided so that the
fidelity of the rounded corner may be fine-tuned as needed. 2 is
an appropriate value for most shapes, however a larger value may be
need if the rounded corner is particularly large. 2 is the minimum
number allowed, representing the start and end of the curve. 3 will
result in a start, middle, and end point, meaning 2 curves will be
generated.
The option to `evenly_distribute_anchors` is provided so that the
line segments (the part part of each line remaining after rounding
off the corners) can be subdivided to a density similar to that of
the average density of the rounded corners. This may be desirable
in situations in which an even distribution of curves is desired
for use in later transformation animations. Be aware, though, that
enabling this option can result in an an object containing
significantly more points than the original, especially when the
rounded corner curves are small.
Examples
--------
.. manim:: PolygramRoundCorners
:save_last_frame:
class PolygramRoundCorners(Scene):
def construct(self):
star = Star(outer_radius=2)
shapes = VGroup(star)
shapes.add(star.copy().round_corners(radius=0.1))
shapes.add(star.copy().round_corners(radius=0.25))
shapes.arrange(RIGHT)
self.add(shapes)
"""
if radius == 0:
return self
new_points = []
for vertices in self.get_vertex_groups():
arcs = []
# Repeat the radius list as necessary in order to provide a radius
# for each vertex.
if isinstance(radius, (int, float)):
radius_list = [radius] * len(vertices)
else:
radius_list = radius * ceil(len(vertices) / len(radius))
for currentRadius, (v1, v2, v3) in zip(
radius_list, adjacent_n_tuples(vertices, 3)
):
vect1 = v2 - v1
vect2 = v3 - v2
unit_vect1 = normalize(vect1)
unit_vect2 = normalize(vect2)
angle = angle_between_vectors(vect1, vect2)
# Negative radius gives concave curves
angle *= np.sign(currentRadius)
# Distance between vertex and start of the arc
cut_off_length = currentRadius * np.tan(angle / 2)
# Determines counterclockwise vs. clockwise
sign = np.sign(np.cross(vect1, vect2)[2])
arc = ArcBetweenPoints(
v2 - unit_vect1 * cut_off_length,
v2 + unit_vect2 * cut_off_length,
angle=sign * angle,
num_components=components_per_rounded_corner,
)
arcs.append(arc)
if evenly_distribute_anchors:
# Determine the average length of each curve
nonZeroLengthArcs = [arc for arc in arcs if len(arc.points) > 4]
if len(nonZeroLengthArcs):
totalArcLength = sum(
[arc.get_arc_length() for arc in nonZeroLengthArcs]
)
totalCurveCount = (
sum([len(arc.points) for arc in nonZeroLengthArcs]) / 4
)
averageLengthPerCurve = totalArcLength / totalCurveCount
else:
averageLengthPerCurve = 1
# To ensure that we loop through starting with last
arcs = [arcs[-1], *arcs[:-1]]
from manim.mobject.geometry.line import Line
for arc1, arc2 in adjacent_pairs(arcs):
new_points.extend(arc1.points)
line = Line(arc1.get_end(), arc2.get_start())
# Make sure anchors are evenly distributed, if necessary
if evenly_distribute_anchors:
line.insert_n_curves(
ceil(line.get_length() / averageLengthPerCurve)
)
new_points.extend(line.points)
self.set_points(new_points)
return self
[docs]class Polygon(Polygram):
"""A shape consisting of one closed loop of vertices.
Parameters
----------
vertices
The vertices of the :class:`Polygon`.
kwargs
Forwarded to the parent constructor.
Examples
--------
.. manim:: PolygonExample
:save_last_frame:
class PolygonExample(Scene):
def construct(self):
isosceles = Polygon([-5, 1.5, 0], [-2, 1.5, 0], [-3.5, -2, 0])
position_list = [
[4, 1, 0], # middle right
[4, -2.5, 0], # bottom right
[0, -2.5, 0], # bottom left
[0, 3, 0], # top left
[2, 1, 0], # middle
[4, 3, 0], # top right
]
square_and_triangles = Polygon(*position_list, color=PURPLE_B)
self.add(isosceles, square_and_triangles)
"""
def __init__(self, *vertices: Point3D, **kwargs) -> None:
super().__init__(vertices, **kwargs)
[docs]class RegularPolygram(Polygram):
"""A :class:`Polygram` with regularly spaced vertices.
Parameters
----------
num_vertices
The number of vertices.
density
The density of the :class:`RegularPolygram`.
Can be thought of as how many vertices to hop
to draw a line between them. Every ``density``-th
vertex is connected.
radius
The radius of the circle that the vertices are placed on.
start_angle
The angle the vertices start at; the rotation of
the :class:`RegularPolygram`.
kwargs
Forwarded to the parent constructor.
Examples
--------
.. manim:: RegularPolygramExample
:save_last_frame:
class RegularPolygramExample(Scene):
def construct(self):
pentagram = RegularPolygram(5, radius=2)
self.add(pentagram)
"""
def __init__(
self,
num_vertices: int,
*,
density: int = 2,
radius: float = 1,
start_angle: float | None = None,
**kwargs,
) -> None:
# Regular polygrams can be expressed by the number of their vertices
# and their density. This relation can be expressed as its Schläfli
# symbol: {num_vertices/density}.
#
# For instance, a pentagon can be expressed as {5/1} or just {5}.
# A pentagram, however, can be expressed as {5/2}.
# A hexagram *would* be expressed as {6/2}, except that 6 and 2
# are not coprime, and it can be simplified to 2{3}, which corresponds
# to the fact that a hexagram is actually made up of 2 triangles.
#
# See https://en.wikipedia.org/wiki/Polygram_(geometry)#Generalized_regular_polygons
# for more information.
num_gons = np.gcd(num_vertices, density)
num_vertices //= num_gons
density //= num_gons
# Utility function for generating the individual
# polygon vertices.
def gen_polygon_vertices(start_angle):
reg_vertices, start_angle = regular_vertices(
num_vertices,
radius=radius,
start_angle=start_angle,
)
vertices = []
i = 0
while True:
vertices.append(reg_vertices[i])
i += density
i %= num_vertices
if i == 0:
break
return vertices, start_angle
first_group, self.start_angle = gen_polygon_vertices(start_angle)
vertex_groups = [first_group]
for i in range(1, num_gons):
start_angle = self.start_angle + (i / num_gons) * TAU / num_vertices
group, _ = gen_polygon_vertices(start_angle)
vertex_groups.append(group)
super().__init__(*vertex_groups, **kwargs)
[docs]class RegularPolygon(RegularPolygram):
"""An n-sided regular :class:`Polygon`.
Parameters
----------
n
The number of sides of the :class:`RegularPolygon`.
kwargs
Forwarded to the parent constructor.
Examples
--------
.. manim:: RegularPolygonExample
:save_last_frame:
class RegularPolygonExample(Scene):
def construct(self):
poly_1 = RegularPolygon(n=6)
poly_2 = RegularPolygon(n=6, start_angle=30*DEGREES, color=GREEN)
poly_3 = RegularPolygon(n=10, color=RED)
poly_group = Group(poly_1, poly_2, poly_3).scale(1.5).arrange(buff=1)
self.add(poly_group)
"""
def __init__(self, n: int = 6, **kwargs) -> None:
super().__init__(n, density=1, **kwargs)
[docs]class Star(Polygon):
"""A regular polygram without the intersecting lines.
Parameters
----------
n
How many points on the :class:`Star`.
outer_radius
The radius of the circle that the outer vertices are placed on.
inner_radius
The radius of the circle that the inner vertices are placed on.
If unspecified, the inner radius will be
calculated such that the edges of the :class:`Star`
perfectly follow the edges of its :class:`RegularPolygram`
counterpart.
density
The density of the :class:`Star`. Only used if
``inner_radius`` is unspecified.
See :class:`RegularPolygram` for more information.
start_angle
The angle the vertices start at; the rotation of
the :class:`Star`.
kwargs
Forwardeds to the parent constructor.
Raises
------
:exc:`ValueError`
If ``inner_radius`` is unspecified and ``density``
is not in the range ``[1, n/2)``.
Examples
--------
.. manim:: StarExample
:save_as_gif:
class StarExample(Scene):
def construct(self):
pentagram = RegularPolygram(5, radius=2)
star = Star(outer_radius=2, color=RED)
self.add(pentagram)
self.play(Create(star), run_time=3)
self.play(FadeOut(star), run_time=2)
.. manim:: DifferentDensitiesExample
:save_last_frame:
class DifferentDensitiesExample(Scene):
def construct(self):
density_2 = Star(7, outer_radius=2, density=2, color=RED)
density_3 = Star(7, outer_radius=2, density=3, color=PURPLE)
self.add(VGroup(density_2, density_3).arrange(RIGHT))
"""
def __init__(
self,
n: int = 5,
*,
outer_radius: float = 1,
inner_radius: float | None = None,
density: int = 2,
start_angle: float | None = TAU / 4,
**kwargs,
) -> None:
inner_angle = TAU / (2 * n)
if inner_radius is None:
# See https://math.stackexchange.com/a/2136292 for an
# overview of how to calculate the inner radius of a
# perfect star.
if density <= 0 or density >= n / 2:
raise ValueError(
f"Incompatible density {density} for number of points {n}",
)
outer_angle = TAU * density / n
inverse_x = 1 - np.tan(inner_angle) * (
(np.cos(outer_angle) - 1) / np.sin(outer_angle)
)
inner_radius = outer_radius / (np.cos(inner_angle) * inverse_x)
outer_vertices, self.start_angle = regular_vertices(
n,
radius=outer_radius,
start_angle=start_angle,
)
inner_vertices, _ = regular_vertices(
n,
radius=inner_radius,
start_angle=self.start_angle + inner_angle,
)
vertices = []
for pair in zip(outer_vertices, inner_vertices):
vertices.extend(pair)
super().__init__(*vertices, **kwargs)
[docs]class Triangle(RegularPolygon):
"""An equilateral triangle.
Parameters
----------
kwargs
Additional arguments to be passed to :class:`RegularPolygon`
Examples
--------
.. manim:: TriangleExample
:save_last_frame:
class TriangleExample(Scene):
def construct(self):
triangle_1 = Triangle()
triangle_2 = Triangle().scale(2).rotate(60*DEGREES)
tri_group = Group(triangle_1, triangle_2).arrange(buff=1)
self.add(tri_group)
"""
def __init__(self, **kwargs) -> None:
super().__init__(n=3, **kwargs)
[docs]class Rectangle(Polygon):
"""A quadrilateral with two sets of parallel sides.
Parameters
----------
color
The color of the rectangle.
height
The vertical height of the rectangle.
width
The horizontal width of the rectangle.
grid_xstep
Space between vertical grid lines.
grid_ystep
Space between horizontal grid lines.
mark_paths_closed
No purpose.
close_new_points
No purpose.
kwargs
Additional arguments to be passed to :class:`Polygon`
Examples
----------
.. manim:: RectangleExample
:save_last_frame:
class RectangleExample(Scene):
def construct(self):
rect1 = Rectangle(width=4.0, height=2.0, grid_xstep=1.0, grid_ystep=0.5)
rect2 = Rectangle(width=1.0, height=4.0)
rects = Group(rect1,rect2).arrange(buff=1)
self.add(rects)
"""
def __init__(
self,
color: ParsableManimColor = WHITE,
height: float = 2.0,
width: float = 4.0,
grid_xstep: float | None = None,
grid_ystep: float | None = None,
mark_paths_closed: bool = True,
close_new_points: bool = True,
**kwargs,
):
super().__init__(UR, UL, DL, DR, color=color, **kwargs)
self.stretch_to_fit_width(width)
self.stretch_to_fit_height(height)
v = self.get_vertices()
if grid_xstep is not None:
from manim.mobject.geometry.line import Line
grid_xstep = abs(grid_xstep)
count = int(width / grid_xstep)
grid = VGroup(
*(
Line(
v[1] + i * grid_xstep * RIGHT,
v[1] + i * grid_xstep * RIGHT + height * DOWN,
color=color,
)
for i in range(1, count)
)
)
self.add(grid)
if grid_ystep is not None:
grid_ystep = abs(grid_ystep)
count = int(height / grid_ystep)
grid = VGroup(
*(
Line(
v[1] + i * grid_ystep * DOWN,
v[1] + i * grid_ystep * DOWN + width * RIGHT,
color=color,
)
for i in range(1, count)
)
)
self.add(grid)
[docs]class Square(Rectangle):
"""A rectangle with equal side lengths.
Parameters
----------
side_length
The length of the sides of the square.
kwargs
Additional arguments to be passed to :class:`Rectangle`.
Examples
--------
.. manim:: SquareExample
:save_last_frame:
class SquareExample(Scene):
def construct(self):
square_1 = Square(side_length=2.0).shift(DOWN)
square_2 = Square(side_length=1.0).next_to(square_1, direction=UP)
square_3 = Square(side_length=0.5).next_to(square_2, direction=UP)
self.add(square_1, square_2, square_3)
"""
def __init__(self, side_length: float = 2.0, **kwargs) -> None:
self.side_length = side_length
super().__init__(height=side_length, width=side_length, **kwargs)
[docs]class RoundedRectangle(Rectangle):
"""A rectangle with rounded corners.
Parameters
----------
corner_radius
The curvature of the corners of the rectangle.
kwargs
Additional arguments to be passed to :class:`Rectangle`
Examples
--------
.. manim:: RoundedRectangleExample
:save_last_frame:
class RoundedRectangleExample(Scene):
def construct(self):
rect_1 = RoundedRectangle(corner_radius=0.5)
rect_2 = RoundedRectangle(corner_radius=1.5, height=4.0, width=4.0)
rect_group = Group(rect_1, rect_2).arrange(buff=1)
self.add(rect_group)
"""
def __init__(self, corner_radius: float | list[float] = 0.5, **kwargs):
super().__init__(**kwargs)
self.corner_radius = corner_radius
self.round_corners(self.corner_radius)
[docs]class Cutout(VMobject, metaclass=ConvertToOpenGL):
"""A shape with smaller cutouts.
Parameters
----------
main_shape
The primary shape from which cutouts are made.
mobjects
The smaller shapes which are to be cut out of the ``main_shape``.
kwargs
Further keyword arguments that are passed to the constructor of
:class:`~.VMobject`.
.. warning::
Technically, this class behaves similar to a symmetric difference: if
parts of the ``mobjects`` are not located within the ``main_shape``,
these parts will be added to the resulting :class:`~.VMobject`.
Examples
--------
.. manim:: CutoutExample
class CutoutExample(Scene):
def construct(self):
s1 = Square().scale(2.5)
s2 = Triangle().shift(DOWN + RIGHT).scale(0.5)
s3 = Square().shift(UP + RIGHT).scale(0.5)
s4 = RegularPolygon(5).shift(DOWN + LEFT).scale(0.5)
s5 = RegularPolygon(6).shift(UP + LEFT).scale(0.5)
c = Cutout(s1, s2, s3, s4, s5, fill_opacity=1, color=BLUE, stroke_color=RED)
self.play(Write(c), run_time=4)
self.wait()
"""
def __init__(self, main_shape: VMobject, *mobjects: VMobject, **kwargs) -> None:
super().__init__(**kwargs)
self.append_points(main_shape.points)
if main_shape.get_direction() == "CW":
sub_direction = "CCW"
else:
sub_direction = "CW"
for mobject in mobjects:
self.append_points(mobject.force_direction(sub_direction).points)