3b1b-manim/manimlib/for_3b1b_videos/common_scenes.py

297 lines
9.9 KiB
Python

import random
import string
from manimlib.animation.animation import Animation
from manimlib.animation.composition import LaggedStart
from manimlib.animation.creation import DrawBorderThenFill
from manimlib.animation.creation import FadeIn
from manimlib.animation.creation import FadeOut
from manimlib.animation.creation import Write
from manimlib.constants import *
from manimlib.continual_animation.continual_animation import ContinualMovement
from manimlib.for_3b1b_videos.pi_creature import Mortimer
from manimlib.for_3b1b_videos.pi_creature import Randolph
from manimlib.for_3b1b_videos.pi_creature_animations import Blink
from manimlib.for_3b1b_videos.pi_creature_scene import PiCreatureScene
from manimlib.mobject.geometry import DashedLine
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
from manimlib.mobject.geometry import Square
from manimlib.mobject.svg.drawings import Logo
from manimlib.mobject.svg.drawings import PatreonLogo
from manimlib.mobject.svg.tex_mobject import TextMobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.moving_camera_scene import MovingCameraScene
from manimlib.scene.scene import Scene
from manimlib.utils.space_ops import get_norm
from manimlib.utils.space_ops import normalize
class OpeningQuote(Scene):
CONFIG = {
"quote": [],
"quote_arg_separator": " ",
"highlighted_quote_terms": {},
"author": "",
"fade_in_kwargs": {
"submobject_mode": "lagged_start",
"rate_func": None,
"lag_factor": 4,
"run_time": 5,
},
"text_size": "\\Large",
"use_quotation_marks": True,
"top_buff": 1.0,
"author_buff": 1.0,
}
def construct(self):
self.quote = self.get_quote()
self.author = self.get_author(self.quote)
self.play(FadeIn(self.quote, **self.fade_in_kwargs))
self.wait(2)
self.play(Write(self.author, run_time=3))
self.wait()
def get_quote(self, max_width=FRAME_WIDTH - 1):
text_mobject_kwargs = {
"alignment": "",
"arg_separator": self.quote_arg_separator,
}
if isinstance(self.quote, str):
if self.use_quotation_marks:
quote = TextMobject("``%s''" %
self.quote.strip(), **text_mobject_kwargs)
else:
quote = TextMobject("%s" %
self.quote.strip(), **text_mobject_kwargs)
else:
if self.use_quotation_marks:
words = [self.text_size + " ``"] + list(self.quote) + ["''"]
else:
words = [self.text_size] + list(self.quote)
quote = TextMobject(*words, **text_mobject_kwargs)
# TODO, make less hacky
if self.quote_arg_separator == " ":
quote[0].shift(0.2 * RIGHT)
quote[-1].shift(0.2 * LEFT)
for term, color in self.highlighted_quote_terms:
quote.set_color_by_tex(term, color)
quote.to_edge(UP, buff=self.top_buff)
if quote.get_width() > max_width:
quote.set_width(max_width)
return quote
def get_author(self, quote):
author = TextMobject(self.text_size + " --" + self.author)
author.next_to(quote, DOWN, buff=self.author_buff)
author.set_color(YELLOW)
return author
class PatreonThanks(Scene):
CONFIG = {
"specific_patrons": [],
"max_patron_group_size": 20,
"patron_scale_val": 0.8,
}
def construct(self):
morty = Mortimer()
morty.next_to(ORIGIN, DOWN)
patreon_logo = PatreonLogo()
patreon_logo.to_edge(UP)
patrons = list(map(TextMobject, self.specific_patrons))
num_groups = float(len(patrons)) / self.max_patron_group_size
proportion_range = np.linspace(0, 1, num_groups + 1)
indices = (len(patrons) * proportion_range).astype('int')
patron_groups = [
VGroup(*patrons[i:j])
for i, j in zip(indices, indices[1:])
]
for i, group in enumerate(patron_groups):
left_group = VGroup(*group[:len(group) / 2])
right_group = VGroup(*group[len(group) / 2:])
for subgroup, vect in (left_group, LEFT), (right_group, RIGHT):
subgroup.arrange_submobjects(DOWN, aligned_edge=LEFT)
subgroup.scale(self.patron_scale_val)
subgroup.to_edge(vect)
last_group = None
for i, group in enumerate(patron_groups):
anims = []
if last_group is not None:
self.play(
FadeOut(last_group),
morty.look, UP + LEFT
)
else:
anims += [
DrawBorderThenFill(patreon_logo),
]
self.play(
LaggedStart(
FadeIn, group,
run_time=2,
),
morty.change, "gracious", group.get_corner(UP + LEFT),
*anims
)
self.play(morty.look_at, group.get_corner(DOWN + LEFT))
self.play(morty.look_at, group.get_corner(UP + RIGHT))
self.play(morty.look_at, group.get_corner(DOWN + RIGHT))
self.play(Blink(morty))
last_group = group
class PatreonEndScreen(PatreonThanks, PiCreatureScene):
CONFIG = {
"n_patron_columns": 3,
"max_patron_width": 3.5,
"run_time": 20,
"randomize_order": True,
"capitalize": True,
"name_y_spacing": 0.7,
"thanks_words": "Funded by the community, with special thanks to:",
}
def construct(self):
if self.randomize_order:
random.shuffle(self.specific_patrons)
if self.capitalize:
self.specific_patrons = [
" ".join(map(
lambda s: s.capitalize(),
patron.split(" ")
))
for patron in self.specific_patrons
]
# self.add_title()
self.scroll_through_patrons()
def create_pi_creatures(self):
title = self.title = TextMobject("Clicky Stuffs")
title.scale(1.5)
title.to_edge(UP, buff=MED_SMALL_BUFF)
randy, morty = self.pi_creatures = VGroup(Randolph(), Mortimer())
for pi, vect in (randy, LEFT), (morty, RIGHT):
pi.set_height(title.get_height())
pi.change_mode("thinking")
pi.look(DOWN)
pi.next_to(title, vect, buff=MED_LARGE_BUFF)
self.add_foreground_mobjects(title, randy, morty)
return self.pi_creatures
def scroll_through_patrons(self):
logo_box = Square(side_length=2.5)
logo_box.to_corner(DOWN + LEFT, buff=MED_LARGE_BUFF)
total_width = FRAME_X_RADIUS - logo_box.get_right()[0]
black_rect = Rectangle(
fill_color=BLACK,
fill_opacity=1,
stroke_width=3,
stroke_color=BLACK,
width=FRAME_WIDTH,
height=0.6 * FRAME_HEIGHT,
)
black_rect.to_edge(UP, buff=0)
line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT)
line.move_to(ORIGIN)
thanks = TextMobject(self.thanks_words)
thanks.scale(0.9)
thanks.next_to(black_rect.get_bottom(), UP, SMALL_BUFF)
thanks.set_color(YELLOW)
underline = Line(LEFT, RIGHT)
underline.match_width(thanks)
underline.scale(1.1)
underline.next_to(thanks, DOWN, SMALL_BUFF)
thanks.add(underline)
patrons = VGroup(*list(map(TextMobject, self.specific_patrons)))
patrons.scale(self.patron_scale_val)
for patron in patrons:
if patron.get_width() > self.max_patron_width:
patron.set_width(self.max_patron_width)
columns = VGroup(*[
VGroup(*patrons[i::self.n_patron_columns])
for i in range(self.n_patron_columns)
])
for column in columns:
for n, name in enumerate(column):
name.shift(n * self.name_y_spacing * DOWN)
columns.arrange_submobjects(
RIGHT, buff=LARGE_BUFF,
aligned_edge=UP,
)
if columns.get_width() > self.max_patron_width:
columns.set_width(total_width - 1)
thanks.to_edge(RIGHT)
columns.next_to(thanks, DOWN, 3 * LARGE_BUFF)
columns.generate_target()
columns.target.move_to(2 * DOWN, DOWN)
columns.target.align_to(
thanks, alignment_vect=RIGHT
)
vect = columns.target.get_center() - columns.get_center()
distance = get_norm(vect)
wait_time = 20
columns_shift = ContinualMovement(
columns,
direction=normalize(vect),
rate=(distance / wait_time)
)
self.add(columns_shift, black_rect, line, thanks)
self.wait(wait_time)
class LogoGenerationTemplate(MovingCameraScene):
def setup(self):
MovingCameraScene.setup(self)
frame = self.camera_frame
frame.shift(DOWN)
self.logo = Logo()
name = TextMobject("3Blue1Brown")
name.scale(2.5)
name.next_to(self.logo, DOWN, buff=MED_LARGE_BUFF)
name.set_sheen(-0.2, DR)
self.channel_name = name
def construct(self):
logo = self.logo
name = self.channel_name
self.play(
Write(name, run_time=3, lag_factor=2.5),
*self.get_logo_animations(logo)
)
self.wait()
def get_logo_animations(self, logo):
return [] # For subclasses
class ExternallyAnimatedScene(Scene):
def construct(self):
raise Exception("Don't actually run this class.")
class TODOStub(Scene):
CONFIG = {
"message": ""
}
def construct(self):
self.add(TextMobject("TODO: %s" % self.message))
self.wait()