Source code for manim.mobject.geometry.polygram

r"""Mobjects that are simple geometric shapes."""

from __future__ import annotations

__all__ = [
    "Polygram",
    "Polygon",
    "RegularPolygram",
    "RegularPolygon",
    "Star",
    "Triangle",
    "Rectangle",
    "Square",
    "RoundedRectangle",
    "Cutout",
]

from typing import Iterable, Sequence

import numpy as np
from colour import Color

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 *
from manim.utils.iterables import adjacent_n_tuples, adjacent_pairs
from manim.utils.space_ops import angle_between_vectors, normalize, regular_vertices


[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: Iterable[Sequence[float]], color=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) -> np.ndarray: """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: """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 = 0.5): """Rounds off the corners of the :class:`Polygram`. Parameters ---------- radius The curvature of the corners of the :class:`Polygram`. .. seealso:: :class:`.~RoundedRectangle` 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 = [] for v1, v2, v3 in 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(radius) # Distance between vertex and start of the arc cut_off_length = radius * 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, ) arcs.append(arc) # 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 len_ratio = line.get_length() / arc1.get_arc_length() line.insert_n_curves(int(arc1.get_num_curves() * len_ratio)) 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: Sequence[float], **kwargs): 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, ): # 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): 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, ): 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): 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: Color = 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): 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 = 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): 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)