mirror of
https://github.com/3b1b/manim.git
synced 2025-07-27 03:52:20 +08:00

* Only use -no-pdf for xelatex rendering * Instead of tracking du and dv points on surface, track points off the surface in the normal direction This means that surface shading will not necessarily work well for arbitrary transformations of the surface. But the existing solution was flimsy anyway, and caused annoying issues with singularity points. * Have density of anchor points on arcs depend on arc length * Allow for specifying true normals and orientation of Sphere * Change miter threshold on stroke shader * Add get_start_and_end to DashedLine * Add min_total_width option to DecimalNumber * Have BackgroundRectangle.set_style absorb (and ignore) added configuration Note, this feels suboptimal * Add LineBrace * Update font_size adjustment in Tex * Add scale_factor parameter to BulletedList.fade_all_but * Minor import tweaks * Add play_sound * Small if -> elif update * Always use Group for FadeTransform * Use time_spanned_alpha in ChangingDecimal * Change priority of number_config vs. self.decimal_number_config in NumberLine init * Fix clock animation * Allow sample_coords to be passed into VectorField
775 lines
24 KiB
Python
775 lines
24 KiB
Python
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 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.numbers import Integer
|
|
from manimlib.mobject.shape_matchers import SurroundingRectangle
|
|
from manimlib.mobject.svg.svg_mobject import SVGMobject
|
|
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
|
|
),
|
|
group=clock,
|
|
run_time=run_time,
|
|
**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)
|