diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 5aff7457..efabdcbf 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -1,4 +1,7 @@ +from operator import le +from os import DirEntry from manimlib.animation.animation import Animation +from manimlib.animation.composition import AnimationGroup from manimlib.animation.rotation import Rotating from manimlib.constants import * from manimlib.mobject.boolean_ops import Difference @@ -13,7 +16,7 @@ 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.tex_mobject import TexTextFromPresetString -from manimlib.mobject.three_dimensions import Cube +from manimlib.mobject.three_dimensions import VCube from manimlib.mobject.three_dimensions import Prismify from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject @@ -23,6 +26,12 @@ from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import complex_to_R3 from manimlib.utils.space_ops import midpoint from manimlib.utils.space_ops import rotate_vector +from manimlib.utils.space_ops import compass_directions + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from typing import Tuple, Sequence, Callable + from manimlib.constants import ManimColor, np_vector class Checkmark(TexTextFromPresetString): tex: str = R"\ding{51}" @@ -34,45 +43,61 @@ class Exmark(TexTextFromPresetString): default_color: str = RED - class Lightbulb(SVGMobject): - CONFIG = { - "height": 1, - "stroke_color": YELLOW, - "stroke_width": 3, - "fill_color": YELLOW, - "fill_opacity": 0, - } + file_name = "lightbulb" - def __init__(self, **kwargs): - super().__init__("lightbulb", **kwargs) + 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): - CONFIG = { - "arc_angle": 4 * np.pi / 3, - "num_ticks": 8, - "tick_length": 0.2, - "needle_width": 0.1, - "needle_height": 0.8, - "needle_color": YELLOW, - } + def __init__( + self, + arc_angle: float = 4 * np.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) - def init_points(self): - start_angle = np.pi / 2 + self.arc_angle / 2 - end_angle = np.pi / 2 - self.arc_angle / 2 - self.add(Arc( + 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 = np.pi / 2 + arc_angle / 2 + end_angle = np.pi / 2 - arc_angle / 2 + self.arc = Arc( start_angle=start_angle, angle=-self.arc_angle - )) - tick_angle_range = np.linspace(start_angle, end_angle, self.num_ticks) + ) + 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 - self.tick_length) * vect, vect) + tick = Line((1 - tick_length) * vect, vect) label = Tex(str(10 * index)) - label.set_height(self.tick_length) - label.shift((1 + self.tick_length) * vect) + label.set_height(tick_length) + label.shift((1 + tick_length) * vect) self.add(tick, label) needle = Polygon( @@ -81,8 +106,8 @@ class Speedometer(VMobject): fill_opacity=1, fill_color=self.needle_color ) - needle.stretch_to_fit_width(self.needle_width) - needle.stretch_to_fit_height(self.needle_height) + 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 @@ -104,7 +129,7 @@ class Speedometer(VMobject): ) def rotate_needle(self, angle): - self.needle.rotate(angle, about_point=self.get_center()) + self.needle.rotate(angle, about_point=self.arc.get_arc_center()) return self def move_needle_to_velocity(self, velocity): @@ -117,66 +142,67 @@ class Speedometer(VMobject): class Laptop(VGroup): - CONFIG = { - "width": 3, - "body_dimensions": [4, 3, 0.05], - "screen_thickness": 0.01, - "keyboard_width_to_body_width": 0.9, - "keyboard_height_to_body_height": 0.5, - "screen_width_to_screen_plate_width": 0.9, - "key_color_kwargs": { - "stroke_width": 0, - "fill_color": BLACK, - "fill_opacity": 1, - }, - "fill_opacity": 1, - "stroke_width": 0, - "body_color": GREY_B, - "shaded_body_color": GREY, - "open_angle": np.pi / 4, - } - - def __init__(self, **kwargs): + 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 = Cube(side_length=1) - for dim, scale_factor in enumerate(self.body_dimensions): + + body = VCube(side_length=1) + for dim, scale_factor in enumerate(body_dimensions): body.stretch(scale_factor, dim=dim) - body.set_width(self.width) - body.set_fill(self.shaded_body_color, opacity=1) + body.set_width(width) + body.set_fill(shaded_body_color, opacity=1) body.sort(lambda p: p[2]) - body[-1].set_fill(self.body_color) + body[-1].set_fill(body_color) screen_plate = body.copy() keyboard = VGroup(*[ VGroup(*[ - Square(**self.key_color_kwargs) + 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( - self.keyboard_width_to_body_width * body.get_width(), + keyboard_width_to_body_width * body.get_width(), ) keyboard.stretch_to_fit_height( - self.keyboard_height_to_body_height * body.get_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(self.screen_thickness / - self.body_dimensions[2], dim=2) + 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(self.screen_width_to_screen_plate_width) + 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( - self.open_angle, RIGHT, + open_angle, RIGHT, about_point=screen_plate.get_bottom() ) self.screen_plate = screen_plate @@ -196,131 +222,131 @@ class Laptop(VGroup): class VideoIcon(SVGMobject): - CONFIG = { - "width": FRAME_WIDTH / 12., - } + file_name: str = "video_icon" - def __init__(self, **kwargs): - super().__init__(file_name="video_icon", **kwargs) - self.center() - self.set_width(self.width) - self.set_stroke(color=WHITE, width=0) - self.set_fill(color=WHITE, opacity=1) + def __init__( + self, + width: float = 1.2, + color=BLUE_A, + **kwargs + ): + super().__init__(color=color, **kwargs) + self.set_width(width) class VideoSeries(VGroup): - CONFIG = { - "num_videos": 11, - "gradient_colors": [BLUE_B, BLUE_D], - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - videos = [VideoIcon() for x in range(self.num_videos)] - VGroup.__init__(self, *videos, **kwargs) - self.arrange() - self.set_width(FRAME_WIDTH - MED_LARGE_BUFF) - self.set_color_by_gradient(*self.gradient_colors) + 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): - CONFIG = {} - - def __init__(self, **kwargs): - circle = Circle(color=WHITE) + 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 in range(12): - alpha = x / 12. - point = complex_to_R3( - np.exp(2 * np.pi * alpha * complex(0, 1)) - ) - length = 0.2 if x % 3 == 0 else 0.1 - ticks.append( - Line(point, (1 - length) * point) - ) - self.hour_hand = Line(ORIGIN, 0.3 * UP) - self.minute_hand = Line(ORIGIN, 0.6 * UP) - # for hand in self.hour_hand, self.minute_hand: - # #Balance out where the center is - # hand.add(VectorizedPoint(-hand.get_end())) + 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) - VGroup.__init__( - self, circle, - self.hour_hand, self.minute_hand, + super().__init__( + circle, self.hour_hand, self.minute_hand, *ticks ) -class ClockPassesTime(Animation): - CONFIG = { - "run_time": 5, - "hours_passed": 12, - "rate_func": linear, - } - - def __init__(self, clock, **kwargs): - digest_config(self, kwargs) - assert(isinstance(clock, Clock)) - rot_kwargs = { - "axis": OUT, - "about_point": clock.get_center() - } - hour_radians = -self.hours_passed * 2 * np.pi / 12 - self.hour_rotation = Rotating( - clock.hour_hand, - angle=hour_radians, - **rot_kwargs +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() ) - self.hour_rotation.begin() - self.minute_rotation = Rotating( - clock.minute_hand, - angle=12 * hour_radians, - **rot_kwargs + 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 ) - self.minute_rotation.begin() - Animation.__init__(self, clock, **kwargs) - - def interpolate_mobject(self, alpha): - for rotation in self.hour_rotation, self.minute_rotation: - rotation.interpolate_mobject(alpha) class Bubble(SVGMobject): - CONFIG = { - "direction": LEFT, - "center_point": ORIGIN, - "content_scale_factor": 0.7, - "height": 5, - "width": 8, - "max_height": None, - "max_width": None, - "bubble_center_adjustment_factor": 1. / 8, - "file_name": None, - "fill_color": BLACK, - "fill_opacity": 0.8, - "stroke_color": WHITE, - "stroke_width": 3, - } + file_name: str = "Bubbles_speech.svg" + + def __init__( + self, + direction: np_vector = LEFT, + center_point: np_vector = ORIGIN, + content_scale_factor: float = 0.7, + height: float = 4.0, + width: float = 8.0, + max_height: float | None = None, + max_width: float | None = None, + bubble_center_adjustment_factor: float = 0.125, + fill_color: ManimColor = BLACK, + fill_opacity: float = 0.8, + stroke_color: ManimColor = WHITE, + stroke_width: float = 3.0, + **kwargs + ): + self.direction = direction + self.bubble_center_adjustment_factor = bubble_center_adjustment_factor + + super().__init__( + fill_color=fill_color, + fill_opacity=fill_opacity, + stroke_color=stroke_color, + stroke_width=stroke_width, + **kwargs + ) - def __init__(self, **kwargs): - digest_config(self, kwargs) - if self.file_name is None: - raise Exception("Must invoke Bubble subclass") - SVGMobject.__init__(self, self.file_name, **kwargs) self.center() - self.set_height(self.height, stretch=True) - self.set_width(self.width, stretch=True) - if self.max_height: - self.set_max_height(self.max_height) - if self.max_width: - self.set_max_width(self.max_width) - if self.direction[0] > 0: + self.set_height(height, stretch=True) + self.set_width(width, stretch=True) + if max_height: + self.set_max_height(max_height) + if max_width: + self.set_max_width(max_width) + if direction[0] > 0: self.flip() - if "direction" in kwargs: - self.direction = kwargs["direction"] - self.direction_was_specified = True - else: - self.direction_was_specified = False + self.content = Mobject() self.refresh_triangulation() @@ -350,8 +376,7 @@ class Bubble(SVGMobject): def pin_to(self, mobject): mob_center = mobject.get_center() want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0]) - can_flip = not self.direction_was_specified - if want_to_flip and can_flip: + if want_to_flip: self.flip() boundary_point = mobject.get_bounding_box_point(UP - self.direction) vector_from_center = 1.0 * (boundary_point - mob_center) @@ -389,23 +414,15 @@ class Bubble(SVGMobject): class SpeechBubble(Bubble): - CONFIG = { - "file_name": "Bubbles_speech.svg", - "height": 4 - } + file_name: str = "Bubbles_speech.svg" class DoubleSpeechBubble(Bubble): - CONFIG = { - "file_name": "Bubbles_double_speech.svg", - "height": 4 - } + file_name: str = "Bubbles_double_speech.svg" class ThoughtBubble(Bubble): - CONFIG = { - "file_name": "Bubbles_thought.svg", - } + file_name: str = "Bubbles_thought.svg" def __init__(self, **kwargs): Bubble.__init__(self, **kwargs) @@ -419,14 +436,15 @@ class ThoughtBubble(Bubble): class VectorizedEarth(SVGMobject): - CONFIG = { - "file_name": "earth", - "height": 1.5, - "fill_color": BLACK, - } + file_name: str = "earth" - def __init__(self, **kwargs): - SVGMobject.__init__(self, **kwargs) + 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, @@ -490,29 +508,28 @@ class Piano(VGroup): class Piano3D(VGroup): - CONFIG = { - "depth_test": True, - "reflectiveness": 1.0, - "stroke_width": 0.25, - "stroke_color": BLACK, - "key_depth": 0.1, - "black_key_shift": 0.05, - } - piano_2d_config = { - "white_key_color": GREY_A, - "key_buff": 0.001 - } - - def __init__(self, **kwargs): - digest_config(self, kwargs) - piano_2d = Piano(**self.piano_2d_config) + def __init__( + self, + reflectiveness: float = 1.0, + 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, self.key_depth) + Prismify(key, key_depth) for key in piano_2d )) - self.set_stroke(self.stroke_color, self.stroke_width) + self.set_stroke(stroke_color, stroke_width) self.apply_depth_test() + # Elevate black keys for i, key in enumerate(self): if piano_2d[i] in piano_2d.black_keys: - key.shift(self.black_key_shift * OUT) + key.shift(black_key_shift * OUT)