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

376 lines
12 KiB
Python

import random
from manimlib.animation.composition import OldLaggedStart
from manimlib.animation.creation import DrawBorderThenFill
from manimlib.animation.creation import Write
from manimlib.animation.fading import FadeIn
from manimlib.animation.fading import FadeOut
from manimlib.constants import *
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.mobject.mobject_update_utils import always_shift
from manimlib.scene.moving_camera_scene import MovingCameraScene
from manimlib.scene.scene import Scene
from manimlib.utils.rate_functions import linear
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": {
"lag_ratio": 0.5,
"rate_func": linear,
"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(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(
OldLaggedStart(
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(
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
always_shift(
columns,
direction=normalize(vect),
rate=(distance / wait_time)
)
self.add(columns, 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),
*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()
class Banner(Scene):
CONFIG = {
"camera_config": {
"pixel_height": 1440,
"pixel_width": 2560,
},
"pi_height": 1.25,
"pi_bottom": 0.25 * DOWN,
"use_date": False,
"date": "Sunday, February 3rd",
"message_scale_val": 0.9,
"add_supporter_note": False,
"pre_date_text": "Next video on ",
}
def __init__(self, **kwargs):
# Force these dimensions
self.camera_config = {
"pixel_height": 1440,
"pixel_width": 2560,
}
Scene.__init__(self, **kwargs)
def construct(self):
pis = self.get_pis()
pis.set_height(self.pi_height)
pis.arrange(RIGHT, aligned_edge=DOWN)
pis.move_to(self.pi_bottom, DOWN)
self.add(pis)
if self.use_date:
message = self.get_date_message()
else:
message = self.get_probabalistic_message()
message.scale(self.message_scale_val)
message.next_to(pis, DOWN)
self.add(message)
if self.add_supporter_note:
note = self.get_supporter_note()
note.scale(0.5)
message.shift((MED_SMALL_BUFF - SMALL_BUFF) * UP)
note.next_to(message, DOWN, SMALL_BUFF)
self.add(note)
yellow_parts = [sm for sm in message if sm.get_color() == YELLOW]
for pi in pis:
if yellow_parts:
pi.look_at(yellow_parts[-1])
else:
pi.look_at(message)
def get_pis(self):
return VGroup(
Randolph(color=BLUE_E, mode="pondering"),
Randolph(color=BLUE_D, mode="hooray"),
Randolph(color=BLUE_C, mode="sassy"),
Mortimer(color=GREY_BROWN, mode="thinking")
)
def get_probabalistic_message(self):
return TextMobject(
"New video every", "Sunday",
"(with probability 0.3)",
tex_to_color_map={"Sunday": YELLOW},
)
def get_date_message(self):
return TextMobject(
self.pre_date_text,
self.date,
tex_to_color_map={self.date: YELLOW},
)
def get_supporter_note(self):
return TextMobject(
"(Available to supporters for review now)",
color="#F96854",
)