3b1b-videos/_2021/some1_winners.py

792 lines
25 KiB
Python
Raw Normal View History

2021-11-01 13:21:00 -07:00
from manim_imports_ext import *
from manimlib.logger import log
import urllib.request
WINNERS = [
("That weird light at the bottom of a mug — ENVELOPES", "Paralogical", "fJWnA4j0_ho"),
("Hiding Images in Plain Sight: The Physics Of Magic Windows", "Matt Ferraro", "CatInCausticImage"),
("The Beauty of Bézier Curves", "Freya Holmér", "aVwxzDHniEw"),
("What Is The Most Complicated Lock Pattern?", "Dr. Zye", "PKjbBQ0PBCQ"),
("Pick's theorem: The wrong, amazing proof", "spacematt", "uh-yRNqLpOg"),
]
HONORABLE_MENTIONS = [
("Dirac's belt trick, Topology, and Spin ½ particles", "Noah Miller", "ACZC_XEyg9U"),
("Galois-Free Guarantee! | The Insolubility of the Quintic", "Carl Turner", "BSHv9Elk1MU"),
("The Two Envelope Problem - a Mystifying Probability Paradox", "Formant", "_NGPncypY68"),
("The Math Behind Font Rasterization | How it Works", "GamesWithGabe", "LaYPoMPRSlk"),
("What is a Spinor?", "Mia Hughes", "SpinorArticle"),
("Understanding e", "Veli Peltola", "e_comic"),
("Ancient Multiplication Trick", "Inigo Quilez", "CsMrHzp850M"),
("对称多项式基本定理自我探究", "凡人忆拾", "FiveIntsTheorem"),
("Lehmer Factor Stencils", "Proof of Concept", "QzohwKT6TNA"),
("What is the limit of a sequence of graphs?", "Spectral Collective", "7Gj9BH4IZ-4"),
("Steiner's Porism: proving a cool animation", "Joseph Newton", "fKAyaP8IzlE"),
("Wait, Probabilities can be Negative?!", "Steven G", "std9EBbtOC0"),
("This random graph fact will blow your mind", "Snarky Math", "3QjZ31lj974"),
("Why is pi here? Estimating π by Buffon's n̶e̶e̶d̶l̶e noodle!", "Mihai Nica", "e-RUyCs9B08"),
("Introduction to Waves", "Rob Schlub", "IntroToWaves"),
("ComplexFunctions", "Treena", "Treena"),
("I spent an entire summer to find this spiral", "Sort of School", "n-e9C8g5x68"),
("HACKENBUSH: a window to a new world of math", "Owen Maitzen", "ZYj4NkeGPdM"),
("The Tale of the Lights Puzzle", "Throw Math At It", "9aZsABF-Vj4"),
("The BEST Way to Find a Random Point in a Circle", "nubDotDev", "4y_nmpv-9lI"),
("Secrets of the Fibonacci Tiles", "Eric Severson", "Ct7oltmdJrM"),
("The Tale of Three Triangles", "Robin Truax", "5nuYD2M2AX8"),
("How Karatsuba's algorithm gave us new ways to multiply", "Nemean", "cCKOl5li6YM"),
("Can you change a sum by rearranging its numbers? --- The Riemann Series Theorem", "Morphocular", "U0w0f0PDdPA"),
("Neural manifolds - The Geometry of Behaviour", "Artem Kirsanov", "QHj9uVmwA_0"),
]
def get_youtube_slugs(file="some1_video_urls.txt"):
full_path = os.path.join(get_directories()["data"], file)
slugs = []
prefix = "https://youtu.be/"
with open(full_path, "r") as fp:
for url in fp.readlines():
if not url.startswith(prefix):
continue
slugs.append(url[len(prefix):-1])
return remove_list_redundancies(slugs)
def save_thumbnail_locally(slug, rewrite=False):
file = yt_slug_to_image_file(slug)
if os.path.exists(file) and not rewrite:
return file
suffixes = ["maxresdefault", "hqdefault", "mqdefault", "sddefault"]
urls = [
*(
f"https://img.youtube.com/vi/{slug}/{suffix}.jpg"
for suffix in suffixes
),
*(
f"https://i.ytimg.com/vi/{slug}/{suffix}.jpg"
for suffix in suffixes
)
]
for url in urls:
try:
urllib.request.urlretrieve(url, file)
return file
except urllib.request.HTTPError:
pass
log.warning(f"Thumbnail not found: {slug}")
def save_thumbnails_locally(slugs):
map(save_thumbnail_locally, slugs)
def yt_slug_to_image_file(slug):
return os.path.join(
get_raster_image_dir(),
"some1_thumbnails",
slug + ".jpg"
)
class Introduction(TeacherStudentsScene):
def construct(self):
# Kill background
self.clear()
morty = self.teacher
# Add title
this_summer = Text("This Summer", font_size=72)
this = this_summer.get_part_by_text("This")
summer = this_summer.get_part_by_text("Summer")
this_summer.to_edge(UP)
some = Text("Summer of Math Exposition", font_size=72)
some.to_edge(UP)
some.shift(0.7 * RIGHT)
this_summer.generate_target()
this_summer.target.shift(some[0].get_left() - summer[0].get_left())
logos = Group(
ImageMobject("Leios"),
Logo(),
)
for logo in logos:
logo.set_height(1.0)
logos.arrange(RIGHT, buff=2)
logos.next_to(this_summer.target, RIGHT, buff=1.5)
james_name = Text("James Schloss")
james_name.set_color(GREY_A)
james_name.next_to(logos[0], DOWN)
logos.generate_target()
logos.target.set_height(0.5)
logos.target.arrange(RIGHT, buff=0.25)
logos.target.replace(this_summer.target[:4], 0)
self.add(this)
self.wait(0.3)
self.add(summer)
self.wait(0.5)
self.play(
MoveToTarget(this_summer),
FadeIn(james_name, lag_ratio=0.1),
FadeIn(logos[0], LEFT),
)
self.play(
Write(logos[1], lag_ratio=0.01, stroke_width=0.05, run_time=1),
Animation(logos[1][-1].copy(), remover=True),
FadeOut(james_name),
)
self.wait()
self.play(
FadeIn(some[len("Summer"):], lag_ratio=0.1),
FadeOut(this),
MoveToTarget(logos, path_arc=-PI / 3, run_time=2)
)
self.remove(this_summer)
self.add(some)
self.wait()
title = Group(logos, some)
# Mention blog post
url = VGroup(
Text("Full details: "),
Text("https://3b1b.co/some1-results")
)
url[1].set_color(BLUE_C)
url.arrange(RIGHT)
url[0].shift(0.05 * UP)
url.to_edge(UP)
self.play(
FadeOut(title, UP),
FadeIn(url[0]),
ShowIncreasingSubsets(url[1], rate_func=linear, run_time=2)
)
self.wait()
# Key points
dots = Dot().get_grid(3, 1)
dots.arrange(DOWN, buff=1.5)
dots.align_to(url, LEFT)
dots.set_y(-0.5)
points = VGroup(
Text("Extremely open-ended"),
Text("Promise to feature 4 to 5 winners"),
Text("Brilliant offered $5k in cash prizes"),
)
colors = [GREEN_A, GREEN_B, GREEN_C]
for dot, point, color in zip(dots, points, colors):
point.next_to(dot, RIGHT)
point.set_color(color)
self.play(
LaggedStartMap(
FadeInFromPoint, dots,
lambda m: (m, url.get_bottom()),
),
)
self.wait()
for point in points:
self.play(Write(point))
if point is points[1]:
self.play(
morty.change("hooray").look(ORIGIN),
2021-11-01 13:21:00 -07:00
FadeOut(BackgroundRectangle(morty, buff=0.25, fill_opacity=1))
)
self.play(Blink(morty))
self.play(morty.change("tease", points[2]))
2021-11-01 13:21:00 -07:00
else:
self.wait()
# Discuss winner
winner_word = TexText("``Winners''", font_size=72)[0]
winner_word.move_to(self.hold_up_spot, DOWN)
pre_winner_word = points[1].get_part_by_text("winner").copy()
self.students.flip().flip()
self.add(self.background, pre_winner_word, *self.pi_creatures)
self.play(
LaggedStartMap(FadeOut, VGroup(
url, dots, points,
)),
FadeIn(self.background),
morty.change("hesitant", winner_word),
2021-11-01 13:21:00 -07:00
FadeTransform(pre_winner_word, winner_word[2:9]),
LaggedStartMap(FadeIn, self.students),
)
self.play(
Write(winner_word[:2]),
Write(winner_word[-2:]),
self.change_students("sassy", "raise_right_hand", "raise_left_hand"),
2021-11-01 13:21:00 -07:00
)
self.wait(4)
# Spirit of the event (start at 25)
salt = Text("Take with a grain of salt")
salt.to_edge(UP)
strange = Text("(what a strange phrase...)", font_size=20)
strange.set_color(GREY_B)
strange.next_to(salt, RIGHT)
arrow = Arrow(salt, winner_word)
self.play(
FadeIn(salt, lag_ratio=0.1),
ShowCreation(arrow),
self.change_students(
2021-11-01 13:21:00 -07:00
"pondering", "pondering", "erm",
look_at_arg=salt,
)
)
self.play(
Blink(self.students[2]),
FadeIn(strange, lag_ratio=0.1),
)
self.play(FadeOut(strange))
self.wait()
spirit = Text("The spirit of the event")
spirit.to_edge(UP)
bubble = ThoughtBubble(height=3, width=3)
bubble.pin_to(self.students[0])
bubble.shift(SMALL_BUFF * UR)
bubble.add_content(Tex(
r"|fg|_1 \leq |f|_p |g|_q",
tex_to_color_map={
"f": GREEN,
"g": TEAL,
}
))
bubble.add(bubble.content)
arrow.target = Arrow(spirit, bubble)
video = VideoIcon()
video.set_height(0.7)
video.next_to(self.students, UP, MED_LARGE_BUFF).to_edge(LEFT)
video.set_color(GREY_C)
video.set_gloss(1)
self.play(
FadeOut(salt, 0.5 * UP),
FadeIn(spirit, 0.5 * UP),
)
self.play(
MoveToTarget(arrow),
FadeIn(bubble),
self.students[0].change("thinking", bubble),
self.students[1].change("pondering", bubble),
self.students[2].change("pondering", bubble),
self.teacher.change("happy", bubble),
2021-11-01 13:21:00 -07:00
winner_word.animate.scale(0.3).set_opacity(0.5).to_corner(UR),
)
self.play(
self.students[0].change("raise_left_hand", video),
2021-11-01 13:21:00 -07:00
FadeIn(video, 0.5 * UP),
)
self.wait()
bubble_copies = bubble.replicate(2).scale(0.7)
for bc, student in zip(bubble_copies, self.students[1:]):
bc.move_to(student.get_corner(UR), DL)
self.play(
LaggedStart(*(
TransformFromCopy(bubble, bc)
for bc in bubble_copies
), lag_ratio=0.5),
LaggedStart(*(
student.change("thinking", UR)
2021-11-01 13:21:00 -07:00
for student in self.students[1:]
), lag_ratio=0.5),
run_time=2,
)
self.wait(3)
class ProsConsOfContext(Scene):
def construct(self):
title = Text("Framing as a contest", font_size=60)
title.to_edge(UP)
h_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH)
h_line.next_to(title, DOWN)
v_line = Line(h_line.get_center(), FRAME_HEIGHT * DOWN / 2)
pro_label = Text("Pros", font_size=60)
pro_label.set_color(GREEN)
pro_label.next_to(h_line, DOWN)
pro_label.set_x(-FRAME_WIDTH / 4)
con_label = Text("Cons", font_size=60)
con_label.set_color(RED)
con_label.next_to(h_line, DOWN)
con_label.set_x(FRAME_WIDTH / 4)
self.play(
LaggedStart(
ShowCreation(h_line),
ShowCreation(v_line),
lag_ratio=0.5,
),
LaggedStart(
FadeIn(title, 0.25 * UP),
FadeIn(pro_label),
FadeIn(con_label),
lag_ratio=0.5,
)
)
self.wait()
pros = VGroup(
Text("Clear deadline"),
Text("Extra push for quality"),
)
pros.set_color(GREEN_A)
pros.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
pros.next_to(pro_label, DOWN, LARGE_BUFF)
con = Text("Suggests that\nsuccess = winning")
con.set_color(RED_B)
con.match_x(con_label)
con.align_to(pros[0], UP)
for words in (*pros, con):
dot = Dot()
dot.next_to(words[0], LEFT)
words.add_to_back(dot)
self.play(Write(words, run_time=2))
self.wait()
self.embed()
class FiltrationProcess(Scene):
def construct(self):
total = TexText("$>1{,}200(!)$ submissions")
hundred = TexText("$\\sim 100$")
hundred.next_to(total, RIGHT, buff=4)
arrow = Arrow(total, hundred, stroke_width=5)
VGroup(total, arrow, hundred).center().to_edge(UP)
peer_words = Text("Peer review process", font_size=36)
peer_words.set_color(BLUE)
peer_words.next_to(arrow, DOWN, buff=SMALL_BUFF)
peer_subwords = TexText(
"(actually quite interesting\\\\see the blog post)",
font_size=24,
fill_color=GREY_B,
)
peer_subwords.next_to(peer_words, DOWN, MED_SMALL_BUFF)
self.play(FadeIn(total, lag_ratio=0.1))
self.wait()
self.play(
ShowCreation(arrow),
FadeIn(peer_words, lag_ratio=0.1)
)
self.play(
FadeIn(hundred),
FadeIn(peer_subwords, 0.25 * DOWN)
)
self.wait()
# Guest judges
guest_words = Text("Guest judges:")
guest_words.next_to(peer_subwords, DOWN, buff=LARGE_BUFF)
guest_words.add(Underline(guest_words))
guest_words.to_edge(RIGHT, buff=1.5)
guests = VGroup(
Text("Alex Kontorovich"),
Text("Henry Reich"),
Text("Nicky Case"),
Text("Tai-Danae Bradley"),
Text("Bukard Polster"),
Text("Mithuna Yoganathan"),
Text("Steven Strogatz"),
)
guests.scale(0.75)
guests.set_color(GREY_B)
guests.arrange(DOWN, aligned_edge=LEFT)
guests.next_to(guest_words, DOWN, aligned_edge=LEFT)
self.play(
FadeIn(guest_words),
LaggedStartMap(
FadeIn, guests,
shift=0.25 * DOWN,
lag_ratio=0.5,
run_time=4,
)
)
self.wait()
class AllVideosOrdered(Scene):
def construct(self):
n = 3
N = n**2
time_per_image = 1 / 5
buffer_size = 2 * N
slugs = get_youtube_slugs("some1_video_urls_ordered.txt")
log.info(f"Number of slugs: {len(slugs)}")
image_slots = ScreenRectangle().get_grid(n, n, buff=0.2)
image_slots.set_height(FRAME_HEIGHT)
image_slots.set_stroke(WHITE, 1)
images = buffer_size * [None]
self.add(image_slots)
def update_image(image, dt):
image.time += dt
image.set_opacity(clip(5 * image.time / (N * time_per_image), 0, 1))
k = 0
for slug in ProgressDisplay(slugs):
slot = image_slots[k % N]
save_thumbnail_locally(slug)
file = yt_slug_to_image_file(slug)
if not os.path.exists(file):
continue
if k > buffer_size:
old_image = images[k % buffer_size]
self.camera.release_texture(old_image.path)
self.remove(old_image)
image = ImageMobject(file)
image.replace(slot, stretch=True)
images[k % buffer_size] = image
image.time = 0
image.add_updater(update_image)
self.add(image)
self.wait(time_per_image)
k += 1
# Hack to keep OpenGL from tracking too many textures.
# Should be fixed more intelligently in Camera
if k % (buffer_size) == 0:
self.camera.n_textures = buffer_size + 2
class RevealingTiles(Scene):
def construct(self):
self.five_winners()
self.honorable_mentions()
def five_winners(self):
# title = Text("SoME1 Winners", font_size=72)
title = TexText("Summer of Math Exposition\\\\", "Winners", font_size=72)
title[1].scale(2, about_edge=UP)
title[1].set_color(YELLOW)
title.to_edge(UP, buff=0.2)
#
# title.to_edge(UP)
subtitle = Text("(in no particular order)", font_size=36)
subtitle.next_to(title, DOWN, buff=0.2)
subtitle.set_color(GREY_B)
self.add(title)
tiles = self.get_mystery_tile().replicate(5)
colors = color_gradient([BLUE_B, BLUE_D], 5)
for tile, color in zip(tiles, colors):
# tile[0].set_stroke(color, 1)
tile[0].set_stroke(YELLOW, 2)
tiles.set_height(2)
tiles.arrange_in_grid(2, 3, buff=MED_SMALL_BUFF)
tiles[3:].match_x(tiles[:3])
tiles.set_width(FRAME_WIDTH - 2)
# tiles.to_edge(DOWN, buff=1)
tiles.to_edge(DOWN, buff=0.25)
reorder = list(range(5))
random.seed(1)
random.shuffle(reorder)
# Animations
self.play(
Write(title),
LaggedStartMap(GrowFromCenter, tiles, lag_ratio=0.15)
)
self.wait()
self.play(
FadeIn(subtitle, lag_ratio=0.2),
LaggedStart(*(
ApplyMethod(
tile.move_to, tiles[reorder[i]],
path_arc=PI / 2
)
for i, tile in enumerate(tiles)
), lag_ratio=0.25, run_time=2)
)
self.wait()
#
for tile, triple in zip(tiles, WINNERS):
self.reveal_tile(tile, *triple)
self.title = title
self.subtitle = subtitle
self.tiles = tiles
def honorable_mentions(self):
tiles = self.tiles
new_title = TexText("Others you'll enjoy", font_size=72)
new_title.to_edge(UP, buff=MED_SMALL_BUFF)
tiles.generate_target()
new_tile = self.get_mystery_tile()
new_tile.replace(tiles[0])
new_tiles = Group(
*tiles.target,
*new_tile.replicate(len(HONORABLE_MENTIONS))
)
new_tiles.arrange_in_grid(5, 6, buff=0.25)
new_tiles.set_height(6.5)
new_tiles.next_to(new_title, DOWN)
self.play(
FadeTransform(self.title, new_title),
FadeOut(self.subtitle),
MoveToTarget(tiles),
LaggedStartMap(
FadeInFromPoint, new_tiles[5:],
lambda m: (m, ORIGIN),
lag_ratio=0.1,
run_time=5,
)
)
self.wait()
reorder_tail = list(range(5, len(new_tiles)))
random.shuffle(reorder_tail)
reorder = [*range(5), *reorder_tail]
centers = [nt.get_center().copy() for nt in new_tiles]
for i, tile in enumerate(new_tiles):
tile.move_to(centers[reorder[i]])
for tile, triple in zip(new_tiles[5:], HONORABLE_MENTIONS):
self.reveal_tile(tile, *triple)
def reveal_tile(self, tile, title, author, image_name):
# Flip tile
try:
image_path = get_full_raster_image_path(image_name)
except IOError:
# Try to redownload best YT slug
save_thumbnail_locally(image_name)
image_path = yt_slug_to_image_file(image_name)
# Trim to be 16x9
arr = np.array(Image.open(image_path))
h, w, _ = arr.shape
nh = w * 9 // 16
if nh < h:
trimmed_arr = arr[(h - nh) // 2:(h - nh) // 2 + nh, :, :]
Image.fromarray(trimmed_arr).save(image_path)
image = ImageMobject(image_path)
image.rotate(PI, RIGHT)
image.replace(tile, dim_to_match=1)
image.set_max_width(tile.get_width())
self.play(
Rotate(image, PI, RIGHT),
Rotate(tile, PI, RIGHT),
UpdateFromAlphaFunc(
tile, lambda m, a: m.set_opacity(1 if a < 0.5 else 0),
),
UpdateFromAlphaFunc(
image, lambda m, a: m.set_opacity(0 if a < 0.5 else 1),
),
)
tile.remove(tile[1])
tile[0].set_fill(BLACK, 1)
tile.add(image)
# Expand
full_rect = FullScreenRectangle()
full_rect.set_fill(GREY_E)
title = Text(title)
title.set_max_width(FRAME_WIDTH - 1)
title.to_edge(UP, buff=MED_SMALL_BUFF)
byline = Text(f"by {author}", font_size=30)
byline.set_color(GREY_B)
byline.next_to(title, DOWN, buff=0.2)
tile.save_state()
tile.generate_target()
tile.target.set_height(6)
tile.target.next_to(byline, DOWN)
tile.saved_state[0].set_stroke(opacity=1)
self.add(full_rect, tile)
self.play(
FadeIn(full_rect),
MoveToTarget(tile, run_time=2),
FadeInFromPoint(title, tile.get_top(), run_time=2),
)
self.play(FadeIn(byline, 0.25 * DOWN))
self.wait()
self.play(
Restore(tile),
FadeOut(full_rect),
FadeOut(title, 0.75 * UP),
FadeOut(byline, 1.0 * UP),
)
def get_mystery_tile(self):
rect = ScreenRectangle(height=1)
rect.set_stroke(BLUE_D, 1)
rect.set_fill(GREY_D, 1)
q_marks = Tex("???")
q_marks.flip().flip()
q_marks.set_fill(GREY_A)
q_marks.move_to(rect)
q_marks.shift(1e-3 * OUT)
return Group(rect, q_marks)
class AlmostTooGood(TeacherStudentsScene):
def construct(self):
self.pi_creatures.flip().flip()
self.teacher_says(
TexText("Almost \\emph{too} good"),
look_at_arg=self.students[2].eyes,
added_anims=[self.change_students("happy", "tease", "hesitant")],
2021-11-01 13:21:00 -07:00
)
self.wait(4)
class ViewRect(Scene):
views = 596
def construct(self):
rect = Rectangle(3.5, 1)
rect.set_height(0.25)
rect.set_stroke(RED, 3)
number = Integer(self.views, font_size=96)
number.set_fill(RED)
number.next_to(rect, DOWN, LARGE_BUFF)
number.shift(3 * RIGHT)
arrow = Arrow(rect, number)
arrow.set_color(RED)
self.play(
ShowCreation(rect)
)
number.set_value(0)
self.play(
ShowCreation(arrow),
# FadeInFromPoint(number, arrow.get_start())
ChangeDecimalToValue(number, self.views, run_time=2),
VFadeIn(number),
UpdateFromFunc(number, lambda m: m.set_fill(RED))
)
self.wait()
class ViewRect700k(ViewRect):
views = 795095
class SureSure(TeacherStudentsScene):
def construct(self):
self.students.flip().flip()
self.teacher_says(
"The point is \n not the winners",
look_at_arg=self.students[2].eyes,
bubble_kwargs={"height": 3, "width": 4}
)
self.play(PiCreatureSays(
self.students[0],
"Yeah, yeah, sure\n it isn't...",
bubble_kwargs={"height": 2, "width": 3}
))
self.change_student_modes("sassy", "angry", "hesitant")
self.play(self.teacher.change("guilty"))
2021-11-01 13:21:00 -07:00
self.wait(4)
class Narrative(Scene):
def construct(self):
question = Text("What makes a good piece of exposition?", font_size=60)
question.to_edge(UP)
question.add(Underline(question))
self.add(question)
properties = VGroup(
Text("Clarity"),
Text("Motivation"),
Text("Memorability"),
Text("Narrative"),
)
properties.scale(60 / 48)
properties.arrange(DOWN, buff=0.75, aligned_edge=LEFT)
properties.next_to(question, DOWN, LARGE_BUFF)
properties.set_fill(BLUE)
narrative = properties[-1]
for prop in properties:
dot = Dot()
dot.next_to(prop, LEFT)
self.play(
FadeIn(dot, scale=0.5),
FadeIn(prop, lag_ratio=0.1)
)
rect = FullScreenFadeRectangle()
rect.set_opacity(0.5)
self.add(rect, narrative)
self.play(
FadeIn(rect),
FlashAround(narrative, run_time=2, time_width=1),
narrative.animate.set_color(YELLOW),
)
self.wait()
class Traction(Scene):
def construct(self):
slug_count_pairs = [
("PKjbBQ0PBCQ", "$\\sim$800,000"),
("ACZC_XEyg9U", "$\\sim$100,000"),
("4y_nmpv-9lI", "$\\sim$140,000"),
("cCKOl5li6YM", "$\\sim$400,000"),
]
groups = Group()
for slug, count in slug_count_pairs:
rect = ScreenRectangle()
rect.set_height(3)
rect.set_stroke(BLUE, 2)
image = ImageMobject(yt_slug_to_image_file(slug))
image.replace(rect, 1)
text = TexText(f"{count} views")
text.next_to(image, DOWN, MED_SMALL_BUFF)
groups.add(Group(image, rect, text))
groups.arrange_in_grid(v_buff=MED_LARGE_BUFF, h_buff=LARGE_BUFF)
groups.set_height(FRAME_HEIGHT - 1)
for elem in groups:
self.play(
FadeIn(elem[:2]),
FadeIn(elem[-1], 0.5 * DOWN, scale=2)
)
self.wait()
class EndScreen(PatreonEndScreen):
pass