123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776 |
- from __future__ import annotations
- import numpy as np
- import itertools as it
- import random
- from manimlib.animation.composition import AnimationGroup
- from manimlib.animation.rotation import Rotating
- from manimlib.constants import BLACK
- from manimlib.constants import BLUE_A
- from manimlib.constants import BLUE_B
- from manimlib.constants import BLUE_C
- from manimlib.constants import BLUE_D
- from manimlib.constants import DOWN
- from manimlib.constants import DOWN
- from manimlib.constants import FRAME_WIDTH
- from manimlib.constants import GREEN
- from manimlib.constants import GREEN_SCREEN
- from manimlib.constants import GREEN_E
- from manimlib.constants import GREY
- from manimlib.constants import GREY_A
- from manimlib.constants import GREY_B
- from manimlib.constants import GREY_E
- from manimlib.constants import LEFT
- from manimlib.constants import LEFT
- from manimlib.constants import MED_LARGE_BUFF
- from manimlib.constants import MED_SMALL_BUFF
- from manimlib.constants import LARGE_BUFF
- from manimlib.constants import ORIGIN
- from manimlib.constants import OUT
- from manimlib.constants import PI
- from manimlib.constants import RED
- from manimlib.constants import RED_E
- from manimlib.constants import RIGHT
- from manimlib.constants import SMALL_BUFF
- from manimlib.constants import SMALL_BUFF
- from manimlib.constants import UP
- from manimlib.constants import UL
- from manimlib.constants import UR
- from manimlib.constants import DL
- from manimlib.constants import DR
- from manimlib.constants import WHITE
- from manimlib.constants import YELLOW
- from manimlib.constants import TAU
- from manimlib.mobject.boolean_ops import Difference
- from manimlib.mobject.boolean_ops import Union
- from manimlib.mobject.geometry import Arc
- from manimlib.mobject.geometry import Circle
- from manimlib.mobject.geometry import Dot
- from manimlib.mobject.geometry import Line
- from manimlib.mobject.geometry import Polygon
- from manimlib.mobject.geometry import Rectangle
- from manimlib.mobject.geometry import Square
- from manimlib.mobject.geometry import AnnularSector
- from manimlib.mobject.mobject import Mobject
- from manimlib.mobject.numbers import Integer
- from manimlib.mobject.shape_matchers import SurroundingRectangle
- from manimlib.mobject.svg.svg_mobject import SVGMobject
- from manimlib.mobject.svg.tex_mobject import Tex
- from manimlib.mobject.svg.tex_mobject import TexText
- from manimlib.mobject.svg.special_tex import TexTextFromPresetString
- from manimlib.mobject.three_dimensions import Prismify
- from manimlib.mobject.three_dimensions import VCube
- from manimlib.mobject.types.vectorized_mobject import VGroup
- from manimlib.mobject.types.vectorized_mobject import VMobject
- from manimlib.mobject.svg.text_mobject import Text
- from manimlib.utils.bezier import interpolate
- from manimlib.utils.iterables import adjacent_pairs
- from manimlib.utils.rate_functions import linear
- from manimlib.utils.space_ops import angle_of_vector
- from manimlib.utils.space_ops import compass_directions
- from manimlib.utils.space_ops import get_norm
- from manimlib.utils.space_ops import midpoint
- from manimlib.utils.space_ops import rotate_vector
- from typing import TYPE_CHECKING
- if TYPE_CHECKING:
- from typing import Tuple, Sequence, Callable
- from manimlib.typing import ManimColor, Vect3
- class Checkmark(TexTextFromPresetString):
- tex: str = R"\ding{51}"
- default_color: ManimColor = GREEN
- class Exmark(TexTextFromPresetString):
- tex: str = R"\ding{55}"
- default_color: ManimColor = RED
- class Lightbulb(SVGMobject):
- file_name = "lightbulb"
- def __init__(
- self,
- height: float = 1.0,
- color: ManimColor = YELLOW,
- stroke_width: float = 3.0,
- fill_opacity: float = 0.0,
- **kwargs
- ):
- super().__init__(
- height=height,
- color=color,
- stroke_width=stroke_width,
- fill_opacity=fill_opacity,
- **kwargs
- )
- self.insert_n_curves(25)
- class Speedometer(VMobject):
- def __init__(
- self,
- arc_angle: float = 4 * PI / 3,
- num_ticks: int = 8,
- tick_length: float = 0.2,
- needle_width: float = 0.1,
- needle_height: float = 0.8,
- needle_color: ManimColor = YELLOW,
- **kwargs,
- ):
- super().__init__(**kwargs)
- self.arc_angle = arc_angle
- self.num_ticks = num_ticks
- self.tick_length = tick_length
- self.needle_width = needle_width
- self.needle_height = needle_height
- self.needle_color = needle_color
- start_angle = PI / 2 + arc_angle / 2
- end_angle = PI / 2 - arc_angle / 2
- self.arc = Arc(
- start_angle=start_angle,
- angle=-self.arc_angle
- )
- self.add(self.arc)
- tick_angle_range = np.linspace(start_angle, end_angle, num_ticks)
- for index, angle in enumerate(tick_angle_range):
- vect = rotate_vector(RIGHT, angle)
- tick = Line((1 - tick_length) * vect, vect)
- label = Integer(10 * index)
- label.set_height(tick_length)
- label.shift((1 + tick_length) * vect)
- self.add(tick, label)
- needle = Polygon(
- LEFT, UP, RIGHT,
- stroke_width=0,
- fill_opacity=1,
- fill_color=self.needle_color
- )
- needle.stretch_to_fit_width(needle_width)
- needle.stretch_to_fit_height(needle_height)
- needle.rotate(start_angle - np.pi / 2, about_point=ORIGIN)
- self.add(needle)
- self.needle = needle
- self.center_offset = self.get_center()
- def get_center(self):
- result = VMobject.get_center(self)
- if hasattr(self, "center_offset"):
- result -= self.center_offset
- return result
- def get_needle_tip(self):
- return self.needle.get_anchors()[1]
- def get_needle_angle(self):
- return angle_of_vector(
- self.get_needle_tip() - self.get_center()
- )
- def rotate_needle(self, angle):
- self.needle.rotate(angle, about_point=self.arc.get_arc_center())
- return self
- def move_needle_to_velocity(self, velocity):
- max_velocity = 10 * (self.num_ticks - 1)
- proportion = float(velocity) / max_velocity
- start_angle = np.pi / 2 + self.arc_angle / 2
- target_angle = start_angle - self.arc_angle * proportion
- self.rotate_needle(target_angle - self.get_needle_angle())
- return self
- class Laptop(VGroup):
- def __init__(
- self,
- width: float = 3,
- body_dimensions: Tuple[float, float, float] = (4.0, 3.0, 0.05),
- screen_thickness: float = 0.01,
- keyboard_width_to_body_width: float = 0.9,
- keyboard_height_to_body_height: float = 0.5,
- screen_width_to_screen_plate_width: float = 0.9,
- key_color_kwargs: dict = dict(
- stroke_width=0,
- fill_color=BLACK,
- fill_opacity=1,
- ),
- fill_opacity: float = 1.0,
- stroke_width: float = 0.0,
- body_color: ManimColor = GREY_B,
- shaded_body_color: ManimColor = GREY,
- open_angle: float = np.pi / 4,
- **kwargs
- ):
- super().__init__(**kwargs)
- body = VCube(side_length=1)
- for dim, scale_factor in enumerate(body_dimensions):
- body.stretch(scale_factor, dim=dim)
- body.set_width(width)
- body.set_fill(shaded_body_color, opacity=1)
- body.sort(lambda p: p[2])
- body[-1].set_fill(body_color)
- screen_plate = body.copy()
- keyboard = VGroup(*[
- VGroup(*[
- Square(**key_color_kwargs)
- for x in range(12 - y % 2)
- ]).arrange(RIGHT, buff=SMALL_BUFF)
- for y in range(4)
- ]).arrange(DOWN, buff=MED_SMALL_BUFF)
- keyboard.stretch_to_fit_width(
- keyboard_width_to_body_width * body.get_width(),
- )
- keyboard.stretch_to_fit_height(
- keyboard_height_to_body_height * body.get_height(),
- )
- keyboard.next_to(body, OUT, buff=0.1 * SMALL_BUFF)
- keyboard.shift(MED_SMALL_BUFF * UP)
- body.add(keyboard)
- screen_plate.stretch(screen_thickness /
- body_dimensions[2], dim=2)
- screen = Rectangle(
- stroke_width=0,
- fill_color=BLACK,
- fill_opacity=1,
- )
- screen.replace(screen_plate, stretch=True)
- screen.scale(screen_width_to_screen_plate_width)
- screen.next_to(screen_plate, OUT, buff=0.1 * SMALL_BUFF)
- screen_plate.add(screen)
- screen_plate.next_to(body, UP, buff=0)
- screen_plate.rotate(
- open_angle, RIGHT,
- about_point=screen_plate.get_bottom()
- )
- self.screen_plate = screen_plate
- self.screen = screen
- axis = Line(
- body.get_corner(UP + LEFT + OUT),
- body.get_corner(UP + RIGHT + OUT),
- color=BLACK,
- stroke_width=2
- )
- self.axis = axis
- self.add(body, screen_plate, axis)
- class VideoIcon(SVGMobject):
- file_name: str = "video_icon"
- def __init__(
- self,
- width: float = 1.2,
- color=BLUE_A,
- **kwargs
- ):
- super().__init__(color=color, **kwargs)
- self.set_width(width)
- class VideoSeries(VGroup):
- def __init__(
- self,
- num_videos: int = 11,
- gradient_colors: Sequence[ManimColor] = [BLUE_B, BLUE_D],
- width: float = FRAME_WIDTH - MED_LARGE_BUFF,
- **kwargs
- ):
- super().__init__(
- *(VideoIcon() for x in range(num_videos)),
- **kwargs
- )
- self.arrange(RIGHT)
- self.set_width(width)
- self.set_color_by_gradient(*gradient_colors)
- class Clock(VGroup):
- def __init__(
- self,
- stroke_color: ManimColor = WHITE,
- stroke_width: float = 3.0,
- hour_hand_height: float = 0.3,
- minute_hand_height: float = 0.6,
- tick_length: float = 0.1,
- **kwargs,
- ):
- style = dict(stroke_color=stroke_color, stroke_width=stroke_width)
- circle = Circle(**style)
- ticks = []
- for x, point in enumerate(compass_directions(12, UP)):
- length = tick_length
- if x % 3 == 0:
- length *= 2
- ticks.append(Line(point, (1 - length) * point, **style))
- self.hour_hand = Line(ORIGIN, hour_hand_height * UP, **style)
- self.minute_hand = Line(ORIGIN, minute_hand_height * UP, **style)
- super().__init__(
- circle, self.hour_hand, self.minute_hand,
- *ticks
- )
- class ClockPassesTime(AnimationGroup):
- def __init__(
- self,
- clock: Clock,
- run_time: float = 5.0,
- hours_passed: float = 12.0,
- rate_func: Callable[[float], float] = linear,
- **kwargs
- ):
- rot_kwargs = dict(
- axis=OUT,
- about_point=clock.get_center()
- )
- hour_radians = -hours_passed * 2 * PI / 12
- super().__init__(
- Rotating(
- clock.hour_hand,
- angle=hour_radians,
- **rot_kwargs
- ),
- Rotating(
- clock.minute_hand,
- angle=12 * hour_radians,
- **rot_kwargs
- ),
- **kwargs
- )
- class Bubble(VGroup):
- file_name: str = "Bubbles_speech.svg"
- bubble_center_adjustment_factor = 0.125
- def __init__(
- self,
- content: str | VMobject | None = None,
- buff: float = 1.0,
- filler_shape: Tuple[float, float] = (3.0, 2.0),
- pin_point: Vect3 | None = None,
- direction: Vect3 = LEFT,
- add_content: bool = True,
- fill_color: ManimColor = BLACK,
- fill_opacity: float = 0.8,
- stroke_color: ManimColor = WHITE,
- stroke_width: float = 3.0,
- **kwargs
- ):
- super().__init__(**kwargs)
- self.direction = direction
- if content is None:
- content = Rectangle(*filler_shape)
- content.set_fill(opacity=0)
- content.set_stroke(width=0)
- elif isinstance(content, str):
- content = Text(content)
- self.content = content
- self.body = self.get_body(content, direction, buff)
- self.body.set_fill(fill_color, fill_opacity)
- self.body.set_stroke(stroke_color, stroke_width)
- self.add(self.body)
- if add_content:
- self.add(self.content)
- if pin_point is not None:
- self.pin_to(pin_point)
- def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
- body = SVGMobject(self.file_name)
- if direction[0] > 0:
- body.flip()
- # Resize
- width = content.get_width()
- height = content.get_height()
- target_width = width + min(buff, height)
- target_height = 1.35 * (height + buff) # Magic number?
- body.set_shape(target_width, target_height)
- body.move_to(content)
- body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN)
- return body
- def get_tip(self):
- return self.get_corner(DOWN + self.direction)
- def get_bubble_center(self):
- factor = self.bubble_center_adjustment_factor
- return self.get_center() + factor * self.get_height() * UP
- def move_tip_to(self, point):
- self.shift(point - self.get_tip())
- return self
- def flip(self, axis=UP, only_body=True, **kwargs):
- super().flip(axis=axis, **kwargs)
- if only_body:
- # Flip in place, don't use kwargs
- self.content.flip(axis=axis)
- if abs(axis[1]) > 0:
- self.direction = -np.array(self.direction)
- return self
- def pin_to(self, mobject, auto_flip=False):
- mob_center = mobject.get_center()
- want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0])
- if want_to_flip and auto_flip:
- self.flip()
- boundary_point = mobject.get_bounding_box_point(UP - self.direction)
- vector_from_center = 1.0 * (boundary_point - mob_center)
- self.move_tip_to(mob_center + vector_from_center)
- return self
- def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF):
- mobject.set_max_width(self.body.get_width() - 2 * buff)
- mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff)
- mobject.shift(self.get_bubble_center() - mobject.get_center())
- return mobject
- def add_content(self, mobject):
- self.position_mobject_inside(mobject)
- self.content = mobject
- return self.content
- def write(self, text):
- self.add_content(Text(text))
- return self
- def resize_to_content(self, buff=1.0): # TODO
- self.body.match_points(self.get_body(
- self.content, self.direction, buff
- ))
- def clear(self):
- self.remove(self.content)
- return self
- class SpeechBubble(Bubble):
- def __init__(
- self,
- content: str | VMobject | None = None,
- buff: float = MED_SMALL_BUFF,
- filler_shape: Tuple[float, float] = (2.0, 1.0),
- stem_height_to_bubble_height: float = 0.5,
- stem_top_x_props: Tuple[float, float] = (0.2, 0.3),
- **kwargs
- ):
- self.stem_height_to_bubble_height = stem_height_to_bubble_height
- self.stem_top_x_props = stem_top_x_props
- super().__init__(content, buff, filler_shape, **kwargs)
- def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
- rect = SurroundingRectangle(content, buff=buff)
- rect.round_corners()
- lp = rect.get_corner(DL)
- rp = rect.get_corner(DR)
- stem_height = self.stem_height_to_bubble_height * rect.get_height()
- low_prop, high_prop = self.stem_top_x_props
- triangle = Polygon(
- interpolate(lp, rp, low_prop),
- interpolate(lp, rp, high_prop),
- lp + stem_height * DOWN,
- )
- result = Union(rect, triangle)
- result.insert_n_curves(20)
- if direction[0] > 0:
- result.flip()
- return result
- class ThoughtBubble(Bubble):
- def __init__(
- self,
- content: str | VMobject | None = None,
- buff: float = SMALL_BUFF,
- filler_shape: Tuple[float, float] = (2.0, 1.0),
- bulge_radius: float = 0.35,
- bulge_overlap: float = 0.25,
- noise_factor: float = 0.1,
- circle_radii: list[float] = [0.1, 0.15, 0.2],
- **kwargs
- ):
- self.bulge_radius = bulge_radius
- self.bulge_overlap = bulge_overlap
- self.noise_factor = noise_factor
- self.circle_radii = circle_radii
- super().__init__(content, buff, filler_shape, **kwargs)
- def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
- rect = SurroundingRectangle(content, buff)
- perimeter = rect.get_arc_length()
- radius = self.bulge_radius
- step = (1 - self.bulge_overlap) * (2 * radius)
- nf = self.noise_factor
- corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]]
- points = []
- for c1, c2 in adjacent_pairs(corners):
- n_alphas = int(get_norm(c1 - c2) / step) + 1
- for alpha in np.linspace(0, 1, n_alphas):
- points.append(interpolate(
- c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5)
- ))
- cloud = Union(rect, *(
- # Add bulges
- Circle(radius=radius * (1 + nf * random.random())).move_to(point)
- for point in points
- ))
- cloud.set_stroke(WHITE, 2)
- circles = VGroup(Circle(radius=radius) for radius in self.circle_radii)
- circ_buff = 0.25 * self.circle_radii[0]
- circles.arrange(UR, buff=circ_buff)
- circles[1].shift(circ_buff * DR)
- circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT)
- circles.set_stroke(WHITE, 2)
- result = VGroup(*circles, cloud)
- if direction[0] > 0:
- result.flip()
- return result
- class OldSpeechBubble(Bubble):
- file_name: str = "Bubbles_speech.svg"
- class DoubleSpeechBubble(Bubble):
- file_name: str = "Bubbles_double_speech.svg"
- class OldThoughtBubble(Bubble):
- file_name: str = "Bubbles_thought.svg"
- def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
- body = super().get_body(content, direction, buff)
- body.sort(lambda p: p[1])
- return body
- def make_green_screen(self):
- self.body[-1].set_fill(GREEN_SCREEN, opacity=1)
- return self
- class VectorizedEarth(SVGMobject):
- file_name: str = "earth"
- def __init__(
- self,
- height: float = 2.0,
- **kwargs
- ):
- super().__init__(height=height, **kwargs)
- self.insert_n_curves(20)
- circle = Circle(
- stroke_width=3,
- stroke_color=GREEN,
- fill_opacity=1,
- fill_color=BLUE_C,
- )
- circle.replace(self)
- self.add_to_back(circle)
- class Piano(VGroup):
- def __init__(
- self,
- n_white_keys = 52,
- black_pattern = [0, 2, 3, 5, 6],
- white_keys_per_octave = 7,
- white_key_dims = (0.15, 1.0),
- black_key_dims = (0.1, 0.66),
- key_buff = 0.02,
- white_key_color = WHITE,
- black_key_color = GREY_E,
- total_width = 13,
- **kwargs
- ):
- self.n_white_keys = n_white_keys
- self.black_pattern = black_pattern
- self.white_keys_per_octave = white_keys_per_octave
- self.white_key_dims = white_key_dims
- self.black_key_dims = black_key_dims
- self.key_buff = key_buff
- self.white_key_color = white_key_color
- self.black_key_color = black_key_color
- self.total_width = total_width
- super().__init__(**kwargs)
- self.add_white_keys()
- self.add_black_keys()
- self.sort_keys()
- self[:-1].reverse_points()
- self.set_width(self.total_width)
- def add_white_keys(self):
- key = Rectangle(*self.white_key_dims)
- key.set_fill(self.white_key_color, 1)
- key.set_stroke(width=0)
- self.white_keys = key.get_grid(1, self.n_white_keys, buff=self.key_buff)
- self.add(*self.white_keys)
- def add_black_keys(self):
- key = Rectangle(*self.black_key_dims)
- key.set_fill(self.black_key_color, 1)
- key.set_stroke(width=0)
- self.black_keys = VGroup()
- for i in range(len(self.white_keys) - 1):
- if i % self.white_keys_per_octave not in self.black_pattern:
- continue
- wk1 = self.white_keys[i]
- wk2 = self.white_keys[i + 1]
- bk = key.copy()
- bk.move_to(midpoint(wk1.get_top(), wk2.get_top()), UP)
- big_bk = bk.copy()
- big_bk.stretch((bk.get_width() + self.key_buff) / bk.get_width(), 0)
- big_bk.stretch((bk.get_height() + self.key_buff) / bk.get_height(), 1)
- big_bk.move_to(bk, UP)
- for wk in wk1, wk2:
- wk.become(Difference(wk, big_bk).match_style(wk))
- self.black_keys.add(bk)
- self.add(*self.black_keys)
- def sort_keys(self):
- self.sort(lambda p: p[0])
- class Piano3D(VGroup):
- def __init__(
- self,
- shading: Tuple[float, float, float] = (1.0, 0.2, 0.2),
- stroke_width: float = 0.25,
- stroke_color: ManimColor = BLACK,
- key_depth: float = 0.1,
- black_key_shift: float = 0.05,
- piano_2d_config: dict = dict(
- white_key_color=GREY_A,
- key_buff=0.001
- ),
- **kwargs
- ):
- piano_2d = Piano(**piano_2d_config)
- super().__init__(*(
- Prismify(key, key_depth)
- for key in piano_2d
- ))
- self.set_stroke(stroke_color, stroke_width)
- self.set_shading(*shading)
- self.apply_depth_test()
- # Elevate black keys
- for i, key in enumerate(self):
- if piano_2d[i] in piano_2d.black_keys:
- key.shift(black_key_shift * OUT)
- key.set_color(BLACK)
- class DieFace(VGroup):
- def __init__(
- self,
- value: int,
- side_length: float = 1.0,
- corner_radius: float = 0.15,
- stroke_color: ManimColor = WHITE,
- stroke_width: float = 2.0,
- fill_color: ManimColor = GREY_E,
- dot_radius: float = 0.08,
- dot_color: ManimColor = WHITE,
- dot_coalesce_factor: float = 0.5
- ):
- dot = Dot(radius=dot_radius, fill_color=dot_color)
- square = Square(
- side_length=side_length,
- stroke_color=stroke_color,
- stroke_width=stroke_width,
- fill_color=fill_color,
- fill_opacity=1.0,
- )
- square.round_corners(corner_radius)
- if not (1 <= value <= 6):
- raise Exception("DieFace only accepts integer inputs between 1 and 6")
- edge_group = [
- (ORIGIN,),
- (UL, DR),
- (UL, ORIGIN, DR),
- (UL, UR, DL, DR),
- (UL, UR, ORIGIN, DL, DR),
- (UL, UR, LEFT, RIGHT, DL, DR),
- ][value - 1]
- arrangement = VGroup(*(
- dot.copy().move_to(square.get_bounding_box_point(vect))
- for vect in edge_group
- ))
- arrangement.space_out_submobjects(dot_coalesce_factor)
- super().__init__(square, arrangement)
- self.dots = arrangement
- self.value = value
- self.index = value
- class Dartboard(VGroup):
- radius = 3
- n_sectors = 20
- def __init__(self, **kwargs):
- super().__init__(**kwargs)
- n_sectors = self.n_sectors
- angle = TAU / n_sectors
- segments = VGroup(*[
- VGroup(*[
- AnnularSector(
- inner_radius=in_r,
- outer_radius=out_r,
- start_angle=n * angle,
- angle=angle,
- fill_color=color,
- )
- for n, color in zip(
- range(n_sectors),
- it.cycle(colors)
- )
- ])
- for colors, in_r, out_r in [
- ([GREY_B, GREY_E], 0, 1),
- ([GREEN_E, RED_E], 0.5, 0.55),
- ([GREEN_E, RED_E], 0.95, 1),
- ]
- ])
- segments.rotate(-angle / 2)
- bullseyes = VGroup(*[
- Circle(radius=r)
- for r in [0.07, 0.035]
- ])
- bullseyes.set_fill(opacity=1)
- bullseyes.set_stroke(width=0)
- bullseyes[0].set_color(GREEN_E)
- bullseyes[1].set_color(RED_E)
- self.bullseye = bullseyes[1]
- self.add(*segments, *bullseyes)
- self.scale(self.radius)
|