drawings.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. from __future__ import annotations
  2. import numpy as np
  3. import itertools as it
  4. import random
  5. from manimlib.animation.composition import AnimationGroup
  6. from manimlib.animation.rotation import Rotating
  7. from manimlib.constants import BLACK
  8. from manimlib.constants import BLUE_A
  9. from manimlib.constants import BLUE_B
  10. from manimlib.constants import BLUE_C
  11. from manimlib.constants import BLUE_D
  12. from manimlib.constants import DOWN
  13. from manimlib.constants import DOWN
  14. from manimlib.constants import FRAME_WIDTH
  15. from manimlib.constants import GREEN
  16. from manimlib.constants import GREEN_SCREEN
  17. from manimlib.constants import GREEN_E
  18. from manimlib.constants import GREY
  19. from manimlib.constants import GREY_A
  20. from manimlib.constants import GREY_B
  21. from manimlib.constants import GREY_E
  22. from manimlib.constants import LEFT
  23. from manimlib.constants import LEFT
  24. from manimlib.constants import MED_LARGE_BUFF
  25. from manimlib.constants import MED_SMALL_BUFF
  26. from manimlib.constants import LARGE_BUFF
  27. from manimlib.constants import ORIGIN
  28. from manimlib.constants import OUT
  29. from manimlib.constants import PI
  30. from manimlib.constants import RED
  31. from manimlib.constants import RED_E
  32. from manimlib.constants import RIGHT
  33. from manimlib.constants import SMALL_BUFF
  34. from manimlib.constants import SMALL_BUFF
  35. from manimlib.constants import UP
  36. from manimlib.constants import UL
  37. from manimlib.constants import UR
  38. from manimlib.constants import DL
  39. from manimlib.constants import DR
  40. from manimlib.constants import WHITE
  41. from manimlib.constants import YELLOW
  42. from manimlib.constants import TAU
  43. from manimlib.mobject.boolean_ops import Difference
  44. from manimlib.mobject.boolean_ops import Union
  45. from manimlib.mobject.geometry import Arc
  46. from manimlib.mobject.geometry import Circle
  47. from manimlib.mobject.geometry import Dot
  48. from manimlib.mobject.geometry import Line
  49. from manimlib.mobject.geometry import Polygon
  50. from manimlib.mobject.geometry import Rectangle
  51. from manimlib.mobject.geometry import Square
  52. from manimlib.mobject.geometry import AnnularSector
  53. from manimlib.mobject.mobject import Mobject
  54. from manimlib.mobject.numbers import Integer
  55. from manimlib.mobject.shape_matchers import SurroundingRectangle
  56. from manimlib.mobject.svg.svg_mobject import SVGMobject
  57. from manimlib.mobject.svg.tex_mobject import Tex
  58. from manimlib.mobject.svg.tex_mobject import TexText
  59. from manimlib.mobject.svg.special_tex import TexTextFromPresetString
  60. from manimlib.mobject.three_dimensions import Prismify
  61. from manimlib.mobject.three_dimensions import VCube
  62. from manimlib.mobject.types.vectorized_mobject import VGroup
  63. from manimlib.mobject.types.vectorized_mobject import VMobject
  64. from manimlib.mobject.svg.text_mobject import Text
  65. from manimlib.utils.bezier import interpolate
  66. from manimlib.utils.iterables import adjacent_pairs
  67. from manimlib.utils.rate_functions import linear
  68. from manimlib.utils.space_ops import angle_of_vector
  69. from manimlib.utils.space_ops import compass_directions
  70. from manimlib.utils.space_ops import get_norm
  71. from manimlib.utils.space_ops import midpoint
  72. from manimlib.utils.space_ops import rotate_vector
  73. from typing import TYPE_CHECKING
  74. if TYPE_CHECKING:
  75. from typing import Tuple, Sequence, Callable
  76. from manimlib.typing import ManimColor, Vect3
  77. class Checkmark(TexTextFromPresetString):
  78. tex: str = R"\ding{51}"
  79. default_color: ManimColor = GREEN
  80. class Exmark(TexTextFromPresetString):
  81. tex: str = R"\ding{55}"
  82. default_color: ManimColor = RED
  83. class Lightbulb(SVGMobject):
  84. file_name = "lightbulb"
  85. def __init__(
  86. self,
  87. height: float = 1.0,
  88. color: ManimColor = YELLOW,
  89. stroke_width: float = 3.0,
  90. fill_opacity: float = 0.0,
  91. **kwargs
  92. ):
  93. super().__init__(
  94. height=height,
  95. color=color,
  96. stroke_width=stroke_width,
  97. fill_opacity=fill_opacity,
  98. **kwargs
  99. )
  100. self.insert_n_curves(25)
  101. class Speedometer(VMobject):
  102. def __init__(
  103. self,
  104. arc_angle: float = 4 * PI / 3,
  105. num_ticks: int = 8,
  106. tick_length: float = 0.2,
  107. needle_width: float = 0.1,
  108. needle_height: float = 0.8,
  109. needle_color: ManimColor = YELLOW,
  110. **kwargs,
  111. ):
  112. super().__init__(**kwargs)
  113. self.arc_angle = arc_angle
  114. self.num_ticks = num_ticks
  115. self.tick_length = tick_length
  116. self.needle_width = needle_width
  117. self.needle_height = needle_height
  118. self.needle_color = needle_color
  119. start_angle = PI / 2 + arc_angle / 2
  120. end_angle = PI / 2 - arc_angle / 2
  121. self.arc = Arc(
  122. start_angle=start_angle,
  123. angle=-self.arc_angle
  124. )
  125. self.add(self.arc)
  126. tick_angle_range = np.linspace(start_angle, end_angle, num_ticks)
  127. for index, angle in enumerate(tick_angle_range):
  128. vect = rotate_vector(RIGHT, angle)
  129. tick = Line((1 - tick_length) * vect, vect)
  130. label = Integer(10 * index)
  131. label.set_height(tick_length)
  132. label.shift((1 + tick_length) * vect)
  133. self.add(tick, label)
  134. needle = Polygon(
  135. LEFT, UP, RIGHT,
  136. stroke_width=0,
  137. fill_opacity=1,
  138. fill_color=self.needle_color
  139. )
  140. needle.stretch_to_fit_width(needle_width)
  141. needle.stretch_to_fit_height(needle_height)
  142. needle.rotate(start_angle - np.pi / 2, about_point=ORIGIN)
  143. self.add(needle)
  144. self.needle = needle
  145. self.center_offset = self.get_center()
  146. def get_center(self):
  147. result = VMobject.get_center(self)
  148. if hasattr(self, "center_offset"):
  149. result -= self.center_offset
  150. return result
  151. def get_needle_tip(self):
  152. return self.needle.get_anchors()[1]
  153. def get_needle_angle(self):
  154. return angle_of_vector(
  155. self.get_needle_tip() - self.get_center()
  156. )
  157. def rotate_needle(self, angle):
  158. self.needle.rotate(angle, about_point=self.arc.get_arc_center())
  159. return self
  160. def move_needle_to_velocity(self, velocity):
  161. max_velocity = 10 * (self.num_ticks - 1)
  162. proportion = float(velocity) / max_velocity
  163. start_angle = np.pi / 2 + self.arc_angle / 2
  164. target_angle = start_angle - self.arc_angle * proportion
  165. self.rotate_needle(target_angle - self.get_needle_angle())
  166. return self
  167. class Laptop(VGroup):
  168. def __init__(
  169. self,
  170. width: float = 3,
  171. body_dimensions: Tuple[float, float, float] = (4.0, 3.0, 0.05),
  172. screen_thickness: float = 0.01,
  173. keyboard_width_to_body_width: float = 0.9,
  174. keyboard_height_to_body_height: float = 0.5,
  175. screen_width_to_screen_plate_width: float = 0.9,
  176. key_color_kwargs: dict = dict(
  177. stroke_width=0,
  178. fill_color=BLACK,
  179. fill_opacity=1,
  180. ),
  181. fill_opacity: float = 1.0,
  182. stroke_width: float = 0.0,
  183. body_color: ManimColor = GREY_B,
  184. shaded_body_color: ManimColor = GREY,
  185. open_angle: float = np.pi / 4,
  186. **kwargs
  187. ):
  188. super().__init__(**kwargs)
  189. body = VCube(side_length=1)
  190. for dim, scale_factor in enumerate(body_dimensions):
  191. body.stretch(scale_factor, dim=dim)
  192. body.set_width(width)
  193. body.set_fill(shaded_body_color, opacity=1)
  194. body.sort(lambda p: p[2])
  195. body[-1].set_fill(body_color)
  196. screen_plate = body.copy()
  197. keyboard = VGroup(*[
  198. VGroup(*[
  199. Square(**key_color_kwargs)
  200. for x in range(12 - y % 2)
  201. ]).arrange(RIGHT, buff=SMALL_BUFF)
  202. for y in range(4)
  203. ]).arrange(DOWN, buff=MED_SMALL_BUFF)
  204. keyboard.stretch_to_fit_width(
  205. keyboard_width_to_body_width * body.get_width(),
  206. )
  207. keyboard.stretch_to_fit_height(
  208. keyboard_height_to_body_height * body.get_height(),
  209. )
  210. keyboard.next_to(body, OUT, buff=0.1 * SMALL_BUFF)
  211. keyboard.shift(MED_SMALL_BUFF * UP)
  212. body.add(keyboard)
  213. screen_plate.stretch(screen_thickness /
  214. body_dimensions[2], dim=2)
  215. screen = Rectangle(
  216. stroke_width=0,
  217. fill_color=BLACK,
  218. fill_opacity=1,
  219. )
  220. screen.replace(screen_plate, stretch=True)
  221. screen.scale(screen_width_to_screen_plate_width)
  222. screen.next_to(screen_plate, OUT, buff=0.1 * SMALL_BUFF)
  223. screen_plate.add(screen)
  224. screen_plate.next_to(body, UP, buff=0)
  225. screen_plate.rotate(
  226. open_angle, RIGHT,
  227. about_point=screen_plate.get_bottom()
  228. )
  229. self.screen_plate = screen_plate
  230. self.screen = screen
  231. axis = Line(
  232. body.get_corner(UP + LEFT + OUT),
  233. body.get_corner(UP + RIGHT + OUT),
  234. color=BLACK,
  235. stroke_width=2
  236. )
  237. self.axis = axis
  238. self.add(body, screen_plate, axis)
  239. class VideoIcon(SVGMobject):
  240. file_name: str = "video_icon"
  241. def __init__(
  242. self,
  243. width: float = 1.2,
  244. color=BLUE_A,
  245. **kwargs
  246. ):
  247. super().__init__(color=color, **kwargs)
  248. self.set_width(width)
  249. class VideoSeries(VGroup):
  250. def __init__(
  251. self,
  252. num_videos: int = 11,
  253. gradient_colors: Sequence[ManimColor] = [BLUE_B, BLUE_D],
  254. width: float = FRAME_WIDTH - MED_LARGE_BUFF,
  255. **kwargs
  256. ):
  257. super().__init__(
  258. *(VideoIcon() for x in range(num_videos)),
  259. **kwargs
  260. )
  261. self.arrange(RIGHT)
  262. self.set_width(width)
  263. self.set_color_by_gradient(*gradient_colors)
  264. class Clock(VGroup):
  265. def __init__(
  266. self,
  267. stroke_color: ManimColor = WHITE,
  268. stroke_width: float = 3.0,
  269. hour_hand_height: float = 0.3,
  270. minute_hand_height: float = 0.6,
  271. tick_length: float = 0.1,
  272. **kwargs,
  273. ):
  274. style = dict(stroke_color=stroke_color, stroke_width=stroke_width)
  275. circle = Circle(**style)
  276. ticks = []
  277. for x, point in enumerate(compass_directions(12, UP)):
  278. length = tick_length
  279. if x % 3 == 0:
  280. length *= 2
  281. ticks.append(Line(point, (1 - length) * point, **style))
  282. self.hour_hand = Line(ORIGIN, hour_hand_height * UP, **style)
  283. self.minute_hand = Line(ORIGIN, minute_hand_height * UP, **style)
  284. super().__init__(
  285. circle, self.hour_hand, self.minute_hand,
  286. *ticks
  287. )
  288. class ClockPassesTime(AnimationGroup):
  289. def __init__(
  290. self,
  291. clock: Clock,
  292. run_time: float = 5.0,
  293. hours_passed: float = 12.0,
  294. rate_func: Callable[[float], float] = linear,
  295. **kwargs
  296. ):
  297. rot_kwargs = dict(
  298. axis=OUT,
  299. about_point=clock.get_center()
  300. )
  301. hour_radians = -hours_passed * 2 * PI / 12
  302. super().__init__(
  303. Rotating(
  304. clock.hour_hand,
  305. angle=hour_radians,
  306. **rot_kwargs
  307. ),
  308. Rotating(
  309. clock.minute_hand,
  310. angle=12 * hour_radians,
  311. **rot_kwargs
  312. ),
  313. **kwargs
  314. )
  315. class Bubble(VGroup):
  316. file_name: str = "Bubbles_speech.svg"
  317. bubble_center_adjustment_factor = 0.125
  318. def __init__(
  319. self,
  320. content: str | VMobject | None = None,
  321. buff: float = 1.0,
  322. filler_shape: Tuple[float, float] = (3.0, 2.0),
  323. pin_point: Vect3 | None = None,
  324. direction: Vect3 = LEFT,
  325. add_content: bool = True,
  326. fill_color: ManimColor = BLACK,
  327. fill_opacity: float = 0.8,
  328. stroke_color: ManimColor = WHITE,
  329. stroke_width: float = 3.0,
  330. **kwargs
  331. ):
  332. super().__init__(**kwargs)
  333. self.direction = direction
  334. if content is None:
  335. content = Rectangle(*filler_shape)
  336. content.set_fill(opacity=0)
  337. content.set_stroke(width=0)
  338. elif isinstance(content, str):
  339. content = Text(content)
  340. self.content = content
  341. self.body = self.get_body(content, direction, buff)
  342. self.body.set_fill(fill_color, fill_opacity)
  343. self.body.set_stroke(stroke_color, stroke_width)
  344. self.add(self.body)
  345. if add_content:
  346. self.add(self.content)
  347. if pin_point is not None:
  348. self.pin_to(pin_point)
  349. def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
  350. body = SVGMobject(self.file_name)
  351. if direction[0] > 0:
  352. body.flip()
  353. # Resize
  354. width = content.get_width()
  355. height = content.get_height()
  356. target_width = width + min(buff, height)
  357. target_height = 1.35 * (height + buff) # Magic number?
  358. body.set_shape(target_width, target_height)
  359. body.move_to(content)
  360. body.shift(self.bubble_center_adjustment_factor * body.get_height() * DOWN)
  361. return body
  362. def get_tip(self):
  363. return self.get_corner(DOWN + self.direction)
  364. def get_bubble_center(self):
  365. factor = self.bubble_center_adjustment_factor
  366. return self.get_center() + factor * self.get_height() * UP
  367. def move_tip_to(self, point):
  368. self.shift(point - self.get_tip())
  369. return self
  370. def flip(self, axis=UP, only_body=True, **kwargs):
  371. super().flip(axis=axis, **kwargs)
  372. if only_body:
  373. # Flip in place, don't use kwargs
  374. self.content.flip(axis=axis)
  375. if abs(axis[1]) > 0:
  376. self.direction = -np.array(self.direction)
  377. return self
  378. def pin_to(self, mobject, auto_flip=False):
  379. mob_center = mobject.get_center()
  380. want_to_flip = np.sign(mob_center[0]) != np.sign(self.direction[0])
  381. if want_to_flip and auto_flip:
  382. self.flip()
  383. boundary_point = mobject.get_bounding_box_point(UP - self.direction)
  384. vector_from_center = 1.0 * (boundary_point - mob_center)
  385. self.move_tip_to(mob_center + vector_from_center)
  386. return self
  387. def position_mobject_inside(self, mobject, buff=MED_LARGE_BUFF):
  388. mobject.set_max_width(self.body.get_width() - 2 * buff)
  389. mobject.set_max_height(self.body.get_height() / 1.5 - 2 * buff)
  390. mobject.shift(self.get_bubble_center() - mobject.get_center())
  391. return mobject
  392. def add_content(self, mobject):
  393. self.position_mobject_inside(mobject)
  394. self.content = mobject
  395. return self.content
  396. def write(self, text):
  397. self.add_content(Text(text))
  398. return self
  399. def resize_to_content(self, buff=1.0): # TODO
  400. self.body.match_points(self.get_body(
  401. self.content, self.direction, buff
  402. ))
  403. def clear(self):
  404. self.remove(self.content)
  405. return self
  406. class SpeechBubble(Bubble):
  407. def __init__(
  408. self,
  409. content: str | VMobject | None = None,
  410. buff: float = MED_SMALL_BUFF,
  411. filler_shape: Tuple[float, float] = (2.0, 1.0),
  412. stem_height_to_bubble_height: float = 0.5,
  413. stem_top_x_props: Tuple[float, float] = (0.2, 0.3),
  414. **kwargs
  415. ):
  416. self.stem_height_to_bubble_height = stem_height_to_bubble_height
  417. self.stem_top_x_props = stem_top_x_props
  418. super().__init__(content, buff, filler_shape, **kwargs)
  419. def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
  420. rect = SurroundingRectangle(content, buff=buff)
  421. rect.round_corners()
  422. lp = rect.get_corner(DL)
  423. rp = rect.get_corner(DR)
  424. stem_height = self.stem_height_to_bubble_height * rect.get_height()
  425. low_prop, high_prop = self.stem_top_x_props
  426. triangle = Polygon(
  427. interpolate(lp, rp, low_prop),
  428. interpolate(lp, rp, high_prop),
  429. lp + stem_height * DOWN,
  430. )
  431. result = Union(rect, triangle)
  432. result.insert_n_curves(20)
  433. if direction[0] > 0:
  434. result.flip()
  435. return result
  436. class ThoughtBubble(Bubble):
  437. def __init__(
  438. self,
  439. content: str | VMobject | None = None,
  440. buff: float = SMALL_BUFF,
  441. filler_shape: Tuple[float, float] = (2.0, 1.0),
  442. bulge_radius: float = 0.35,
  443. bulge_overlap: float = 0.25,
  444. noise_factor: float = 0.1,
  445. circle_radii: list[float] = [0.1, 0.15, 0.2],
  446. **kwargs
  447. ):
  448. self.bulge_radius = bulge_radius
  449. self.bulge_overlap = bulge_overlap
  450. self.noise_factor = noise_factor
  451. self.circle_radii = circle_radii
  452. super().__init__(content, buff, filler_shape, **kwargs)
  453. def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
  454. rect = SurroundingRectangle(content, buff)
  455. perimeter = rect.get_arc_length()
  456. radius = self.bulge_radius
  457. step = (1 - self.bulge_overlap) * (2 * radius)
  458. nf = self.noise_factor
  459. corners = [rect.get_corner(v) for v in [DL, UL, UR, DR]]
  460. points = []
  461. for c1, c2 in adjacent_pairs(corners):
  462. n_alphas = int(get_norm(c1 - c2) / step) + 1
  463. for alpha in np.linspace(0, 1, n_alphas):
  464. points.append(interpolate(
  465. c1, c2, alpha + nf * (step / n_alphas) * (random.random() - 0.5)
  466. ))
  467. cloud = Union(rect, *(
  468. # Add bulges
  469. Circle(radius=radius * (1 + nf * random.random())).move_to(point)
  470. for point in points
  471. ))
  472. cloud.set_stroke(WHITE, 2)
  473. circles = VGroup(Circle(radius=radius) for radius in self.circle_radii)
  474. circ_buff = 0.25 * self.circle_radii[0]
  475. circles.arrange(UR, buff=circ_buff)
  476. circles[1].shift(circ_buff * DR)
  477. circles.next_to(cloud, DOWN, 4 * circ_buff, aligned_edge=LEFT)
  478. circles.set_stroke(WHITE, 2)
  479. result = VGroup(*circles, cloud)
  480. if direction[0] > 0:
  481. result.flip()
  482. return result
  483. class OldSpeechBubble(Bubble):
  484. file_name: str = "Bubbles_speech.svg"
  485. class DoubleSpeechBubble(Bubble):
  486. file_name: str = "Bubbles_double_speech.svg"
  487. class OldThoughtBubble(Bubble):
  488. file_name: str = "Bubbles_thought.svg"
  489. def get_body(self, content: VMobject, direction: Vect3, buff: float) -> VMobject:
  490. body = super().get_body(content, direction, buff)
  491. body.sort(lambda p: p[1])
  492. return body
  493. def make_green_screen(self):
  494. self.body[-1].set_fill(GREEN_SCREEN, opacity=1)
  495. return self
  496. class VectorizedEarth(SVGMobject):
  497. file_name: str = "earth"
  498. def __init__(
  499. self,
  500. height: float = 2.0,
  501. **kwargs
  502. ):
  503. super().__init__(height=height, **kwargs)
  504. self.insert_n_curves(20)
  505. circle = Circle(
  506. stroke_width=3,
  507. stroke_color=GREEN,
  508. fill_opacity=1,
  509. fill_color=BLUE_C,
  510. )
  511. circle.replace(self)
  512. self.add_to_back(circle)
  513. class Piano(VGroup):
  514. def __init__(
  515. self,
  516. n_white_keys = 52,
  517. black_pattern = [0, 2, 3, 5, 6],
  518. white_keys_per_octave = 7,
  519. white_key_dims = (0.15, 1.0),
  520. black_key_dims = (0.1, 0.66),
  521. key_buff = 0.02,
  522. white_key_color = WHITE,
  523. black_key_color = GREY_E,
  524. total_width = 13,
  525. **kwargs
  526. ):
  527. self.n_white_keys = n_white_keys
  528. self.black_pattern = black_pattern
  529. self.white_keys_per_octave = white_keys_per_octave
  530. self.white_key_dims = white_key_dims
  531. self.black_key_dims = black_key_dims
  532. self.key_buff = key_buff
  533. self.white_key_color = white_key_color
  534. self.black_key_color = black_key_color
  535. self.total_width = total_width
  536. super().__init__(**kwargs)
  537. self.add_white_keys()
  538. self.add_black_keys()
  539. self.sort_keys()
  540. self[:-1].reverse_points()
  541. self.set_width(self.total_width)
  542. def add_white_keys(self):
  543. key = Rectangle(*self.white_key_dims)
  544. key.set_fill(self.white_key_color, 1)
  545. key.set_stroke(width=0)
  546. self.white_keys = key.get_grid(1, self.n_white_keys, buff=self.key_buff)
  547. self.add(*self.white_keys)
  548. def add_black_keys(self):
  549. key = Rectangle(*self.black_key_dims)
  550. key.set_fill(self.black_key_color, 1)
  551. key.set_stroke(width=0)
  552. self.black_keys = VGroup()
  553. for i in range(len(self.white_keys) - 1):
  554. if i % self.white_keys_per_octave not in self.black_pattern:
  555. continue
  556. wk1 = self.white_keys[i]
  557. wk2 = self.white_keys[i + 1]
  558. bk = key.copy()
  559. bk.move_to(midpoint(wk1.get_top(), wk2.get_top()), UP)
  560. big_bk = bk.copy()
  561. big_bk.stretch((bk.get_width() + self.key_buff) / bk.get_width(), 0)
  562. big_bk.stretch((bk.get_height() + self.key_buff) / bk.get_height(), 1)
  563. big_bk.move_to(bk, UP)
  564. for wk in wk1, wk2:
  565. wk.become(Difference(wk, big_bk).match_style(wk))
  566. self.black_keys.add(bk)
  567. self.add(*self.black_keys)
  568. def sort_keys(self):
  569. self.sort(lambda p: p[0])
  570. class Piano3D(VGroup):
  571. def __init__(
  572. self,
  573. shading: Tuple[float, float, float] = (1.0, 0.2, 0.2),
  574. stroke_width: float = 0.25,
  575. stroke_color: ManimColor = BLACK,
  576. key_depth: float = 0.1,
  577. black_key_shift: float = 0.05,
  578. piano_2d_config: dict = dict(
  579. white_key_color=GREY_A,
  580. key_buff=0.001
  581. ),
  582. **kwargs
  583. ):
  584. piano_2d = Piano(**piano_2d_config)
  585. super().__init__(*(
  586. Prismify(key, key_depth)
  587. for key in piano_2d
  588. ))
  589. self.set_stroke(stroke_color, stroke_width)
  590. self.set_shading(*shading)
  591. self.apply_depth_test()
  592. # Elevate black keys
  593. for i, key in enumerate(self):
  594. if piano_2d[i] in piano_2d.black_keys:
  595. key.shift(black_key_shift * OUT)
  596. key.set_color(BLACK)
  597. class DieFace(VGroup):
  598. def __init__(
  599. self,
  600. value: int,
  601. side_length: float = 1.0,
  602. corner_radius: float = 0.15,
  603. stroke_color: ManimColor = WHITE,
  604. stroke_width: float = 2.0,
  605. fill_color: ManimColor = GREY_E,
  606. dot_radius: float = 0.08,
  607. dot_color: ManimColor = WHITE,
  608. dot_coalesce_factor: float = 0.5
  609. ):
  610. dot = Dot(radius=dot_radius, fill_color=dot_color)
  611. square = Square(
  612. side_length=side_length,
  613. stroke_color=stroke_color,
  614. stroke_width=stroke_width,
  615. fill_color=fill_color,
  616. fill_opacity=1.0,
  617. )
  618. square.round_corners(corner_radius)
  619. if not (1 <= value <= 6):
  620. raise Exception("DieFace only accepts integer inputs between 1 and 6")
  621. edge_group = [
  622. (ORIGIN,),
  623. (UL, DR),
  624. (UL, ORIGIN, DR),
  625. (UL, UR, DL, DR),
  626. (UL, UR, ORIGIN, DL, DR),
  627. (UL, UR, LEFT, RIGHT, DL, DR),
  628. ][value - 1]
  629. arrangement = VGroup(*(
  630. dot.copy().move_to(square.get_bounding_box_point(vect))
  631. for vect in edge_group
  632. ))
  633. arrangement.space_out_submobjects(dot_coalesce_factor)
  634. super().__init__(square, arrangement)
  635. self.dots = arrangement
  636. self.value = value
  637. self.index = value
  638. class Dartboard(VGroup):
  639. radius = 3
  640. n_sectors = 20
  641. def __init__(self, **kwargs):
  642. super().__init__(**kwargs)
  643. n_sectors = self.n_sectors
  644. angle = TAU / n_sectors
  645. segments = VGroup(*[
  646. VGroup(*[
  647. AnnularSector(
  648. inner_radius=in_r,
  649. outer_radius=out_r,
  650. start_angle=n * angle,
  651. angle=angle,
  652. fill_color=color,
  653. )
  654. for n, color in zip(
  655. range(n_sectors),
  656. it.cycle(colors)
  657. )
  658. ])
  659. for colors, in_r, out_r in [
  660. ([GREY_B, GREY_E], 0, 1),
  661. ([GREEN_E, RED_E], 0.5, 0.55),
  662. ([GREEN_E, RED_E], 0.95, 1),
  663. ]
  664. ])
  665. segments.rotate(-angle / 2)
  666. bullseyes = VGroup(*[
  667. Circle(radius=r)
  668. for r in [0.07, 0.035]
  669. ])
  670. bullseyes.set_fill(opacity=1)
  671. bullseyes.set_stroke(width=0)
  672. bullseyes[0].set_color(GREEN_E)
  673. bullseyes[1].set_color(RED_E)
  674. self.bullseye = bullseyes[1]
  675. self.add(*segments, *bullseyes)
  676. self.scale(self.radius)