mirror of
https://github.com/3b1b/manim.git
synced 2025-08-21 05:44:04 +00:00
554 lines
16 KiB
Python
554 lines
16 KiB
Python
from manimlib.imports import *
|
|
|
|
|
|
def boolian_linear_combo(bools):
|
|
return reduce(op.xor, [b * n for n, b in enumerate(bools)], 0)
|
|
|
|
|
|
def string_to_bools(message):
|
|
# For easter eggs on the board
|
|
bits = bin(int.from_bytes(message.encode(), 'big'))[2:]
|
|
bits = (len(message) * 8 - len(bits)) * '0' + bits
|
|
return [bool(int(b)) for b in bits]
|
|
|
|
|
|
def layer_mobject(mobject, nudge=1e-6):
|
|
for i, sm in enumerate(mobject.family_members_with_points()):
|
|
sm.shift(i * nudge * OUT)
|
|
|
|
|
|
class Chessboard(SGroup):
|
|
CONFIG = {
|
|
"shape": (8, 8),
|
|
"height": 7,
|
|
"depth": 0.25,
|
|
"colors": [LIGHT_GREY, DARKER_GREY],
|
|
"gloss": 0.2,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
nr, nc = self.shape
|
|
cube = Cube(square_resolution=(3, 3))
|
|
# Replace top square with something slightly higher res
|
|
top_square = Square3D(resolution=(5, 5))
|
|
top_square.replace(cube[0])
|
|
cube.replace_submobject(0, top_square)
|
|
self.add(*[cube.copy() for x in range(nc * nr)])
|
|
self.arrange_in_grid(buff=0)
|
|
self.set_height(self.height)
|
|
self.set_depth(self.depth, stretch=True)
|
|
for i, j in it.product(range(nr), range(nc)):
|
|
color = self.colors[(i + j) % 2]
|
|
self[i * nc + j].set_color(color)
|
|
self.center()
|
|
self.set_gloss(self.gloss)
|
|
|
|
|
|
class Coin(Group):
|
|
CONFIG = {
|
|
"disk_resolution": (4, 51),
|
|
"height": 1,
|
|
"depth": 0.1,
|
|
"color": GOLD_D,
|
|
"tails_color": RED,
|
|
"include_labels": True,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
res = self.disk_resolution
|
|
self.top = Disk3D(resolution=res, gloss=0.2)
|
|
self.bottom = self.top.copy()
|
|
self.top.shift(OUT)
|
|
self.bottom.shift(IN)
|
|
self.edge = Cylinder(height=2, resolution=(res[1], 2))
|
|
self.add(self.top, self.bottom, self.edge)
|
|
self.rotate(90 * DEGREES, OUT)
|
|
self.set_color(self.color)
|
|
self.bottom.set_color(RED)
|
|
|
|
if self.include_labels:
|
|
labels = VGroup(
|
|
TextMobject("H"),
|
|
TextMobject("T"),
|
|
)
|
|
for label, vect in zip(labels, [OUT, IN]):
|
|
label.shift(1.02 * vect)
|
|
label.set_height(0.8)
|
|
labels[1].flip(RIGHT)
|
|
labels.apply_depth_test()
|
|
else:
|
|
labels = Group(Mobject(), Mobject())
|
|
self.add(*labels)
|
|
self.labels = labels
|
|
|
|
self.set_height(self.height)
|
|
self.set_depth(self.depth, stretch=True)
|
|
|
|
def is_heads(self):
|
|
return self.top.get_center()[2] > self.bottom.get_center()[2]
|
|
|
|
def flip(self, axis=RIGHT):
|
|
super().flip(axis)
|
|
|
|
|
|
class CoinsOnBoard(Group):
|
|
CONFIG = {
|
|
"proportion_of_square_height": 0.7,
|
|
"coin_config": {},
|
|
}
|
|
|
|
def __init__(self, chessboard, **kwargs):
|
|
super().__init__(**kwargs)
|
|
prop = self.proportion_of_square_height
|
|
for cube in chessboard:
|
|
coin = Coin(**self.coin_config)
|
|
coin.set_height(prop * cube.get_height())
|
|
coin.next_to(cube, OUT, buff=0)
|
|
self.add(coin)
|
|
|
|
def flip_at_random(self, p=0.5):
|
|
for coin in self:
|
|
if random.random() < p:
|
|
coin.flip()
|
|
return self
|
|
|
|
def flip_by_message(self, message):
|
|
heads = string_to_bools(message)
|
|
for coin, head in zip(self, heads):
|
|
if not head:
|
|
coin.flip()
|
|
return self
|
|
|
|
|
|
class Key(SVGMobject):
|
|
CONFIG = {
|
|
"file_name": "key",
|
|
"fill_color": GOLD,
|
|
"fill_opacity": 1,
|
|
"stroke_color": GOLD,
|
|
"stroke_width": 0,
|
|
"gloss": 0.5,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.rotate(PI / 2, OUT)
|
|
|
|
|
|
class FlipCoin(Animation):
|
|
CONFIG = {
|
|
"axis": RIGHT,
|
|
"run_time": 1,
|
|
"shift_dir": OUT,
|
|
}
|
|
|
|
def __init__(self, coin, **kwargs):
|
|
super().__init__(coin, **kwargs)
|
|
self.shift_vect = coin.get_height() * self.shift_dir / 2
|
|
|
|
def interpolate_mobject(self, alpha):
|
|
coin = self.mobject
|
|
for sm, start_sm in self.families:
|
|
sm.points[:] = start_sm.points
|
|
coin.rotate(alpha * PI, axis=self.axis)
|
|
coin.shift(4 * alpha * (1 - alpha) * self.shift_vect)
|
|
return coin
|
|
|
|
|
|
# Scenes
|
|
class IntroducePuzzle(Scene):
|
|
CONFIG = {
|
|
"camera_class": ThreeDCamera,
|
|
}
|
|
|
|
def construct(self):
|
|
# Setup
|
|
frame = self.camera.frame
|
|
|
|
chessboard = Chessboard()
|
|
chessboard.move_to(ORIGIN, OUT)
|
|
|
|
grid = NumberPlane(
|
|
x_range=(0, 8), y_range=(0, 8),
|
|
faded_line_ratio=0
|
|
)
|
|
grid.match_height(chessboard)
|
|
grid.next_to(chessboard, OUT, 1e-8)
|
|
low_grid = grid.copy()
|
|
low_grid.next_to(chessboard, IN, 1e-8)
|
|
grid.add(low_grid)
|
|
grid.set_stroke(GREY, width=2)
|
|
grid.set_gloss(0.5)
|
|
grid.prepare_for_nonlinear_transform(0)
|
|
|
|
coins = CoinsOnBoard(chessboard)
|
|
coins.set_gloss(0.2)
|
|
coins_to_flip = Group()
|
|
head_bools = string_to_bools('3b1b :)')
|
|
for coin, heads in zip(coins, head_bools):
|
|
if not heads:
|
|
coins_to_flip.add(coin)
|
|
coins_to_flip.shuffle()
|
|
|
|
count_label = VGroup(
|
|
Integer(0, edge_to_fix=RIGHT),
|
|
TextMobject("Coins")
|
|
)
|
|
count_label.arrange(RIGHT, aligned_edge=DOWN)
|
|
count_label.to_corner(UL)
|
|
count_label.fix_in_frame()
|
|
|
|
# Draw board and coins
|
|
frame.set_rotation(-25 * DEGREES, 70 * DEGREES, 0)
|
|
self.play(
|
|
FadeIn(chessboard),
|
|
ShowCreationThenDestruction(grid, lag_ratio=0.01),
|
|
frame.set_theta, 0,
|
|
frame.set_phi, 45 * DEGREES,
|
|
run_time=3,
|
|
)
|
|
self.wait()
|
|
|
|
self.add(count_label)
|
|
self.play(
|
|
ShowIncreasingSubsets(coins),
|
|
UpdateFromFunc(count_label[0], lambda m, c=coins: m.set_value(len(c))),
|
|
rate_func=bezier([0, 0, 1, 1]),
|
|
run_time=2,
|
|
)
|
|
self.wait()
|
|
self.play(LaggedStartMap(FlipCoin, coins_to_flip, run_time=6, lag_ratio=0.1))
|
|
self.add(coins)
|
|
coins.unlock_shader_data()
|
|
self.wait()
|
|
|
|
# Show key
|
|
key = Key()
|
|
key.rotate(PI / 4, RIGHT)
|
|
key.move_to(3 * OUT)
|
|
key.scale(0.8)
|
|
key.to_edge(LEFT, buff=1)
|
|
|
|
k = boolian_linear_combo(head_bools) ^ 63 # To make the flip below the actual solution
|
|
key_cube = Cube(resolution=(6, 6))
|
|
key_cube.match_color(chessboard[k])
|
|
key_cube.replace(chessboard[k], stretch=True)
|
|
chessboard.replace_submobject(k, key_cube)
|
|
key_square = key_cube[0]
|
|
chessboard.generate_target()
|
|
chessboard.save_state()
|
|
for i, cube in enumerate(chessboard.target):
|
|
if i == k:
|
|
cube[0].set_color(MAROON_E)
|
|
else:
|
|
cube.set_color(interpolate_color(cube.get_color(), BLACK, 0.8))
|
|
|
|
key.generate_target()
|
|
key.target.rotate(PI / 4, LEFT)
|
|
key.target.set_width(0.7 * key_square.get_width())
|
|
key.target.next_to(key_square, IN, buff=SMALL_BUFF)
|
|
|
|
self.play(FadeIn(key, LEFT))
|
|
self.wait()
|
|
self.play(
|
|
FadeOut(coins, lag_ratio=0.01),
|
|
MoveToTarget(chessboard),
|
|
)
|
|
ks_top = key_square.get_top()
|
|
self.play(
|
|
Rotate(key_square, PI / 2, axis=LEFT, about_point=ks_top),
|
|
MoveToTarget(key),
|
|
frame.set_phi, 60 * DEGREES,
|
|
run_time=2,
|
|
)
|
|
self.play(
|
|
Rotate(key_square, PI / 2, axis=RIGHT, about_point=ks_top),
|
|
run_time=2,
|
|
)
|
|
chessboard.unlock_shader_data()
|
|
chessboard.saved_state[k][0].match_color(key_square)
|
|
self.play(
|
|
chessboard.restore,
|
|
FadeIn(coins),
|
|
frame.set_phi, 20 * DEGREES,
|
|
frame.move_to, 2 * LEFT,
|
|
run_time=3
|
|
)
|
|
|
|
# State goal
|
|
goal = TextMobject(
|
|
"Communicate where\\\\the key is",
|
|
" by turning\\\\over one coin.",
|
|
alignment=""
|
|
)
|
|
goal.next_to(count_label, DOWN, LARGE_BUFF, LEFT)
|
|
goal.fix_in_frame()
|
|
goal[1].set_color(YELLOW)
|
|
|
|
self.play(FadeIn(goal[0]))
|
|
self.wait()
|
|
self.play(FadeIn(goal[1]))
|
|
self.wait()
|
|
|
|
coin = coins[63]
|
|
rect = SurroundingRectangle(coin, color=TEAL)
|
|
|
|
self.play(FadeInFromLarge(rect))
|
|
self.play(FlipCoin(coin), FadeOut(rect))
|
|
|
|
|
|
class FromCoinToSquareMaps(Scene):
|
|
CONFIG = {
|
|
"camera_class": ThreeDCamera,
|
|
"messages": [
|
|
"Please, ",
|
|
"go watch",
|
|
"Stand-up",
|
|
"Maths on",
|
|
"YouTube."
|
|
],
|
|
"flip_lag_ratio": 0.05,
|
|
}
|
|
|
|
def construct(self):
|
|
messages = self.messages
|
|
|
|
board1 = Chessboard()
|
|
board1.set_width(5.5)
|
|
board1.to_corner(DL)
|
|
|
|
board2 = board1.copy()
|
|
board2.to_corner(DR)
|
|
|
|
coins = CoinsOnBoard(board1)
|
|
bools = string_to_bools(messages[0])
|
|
for coin, head in zip(coins, bools):
|
|
if not head:
|
|
coin.flip(RIGHT)
|
|
|
|
for cube in board2:
|
|
cube.original_color = cube.get_color()
|
|
|
|
arrow = Arrow(board1.get_right(), board2.get_left())
|
|
arrow.tip.set_stroke(width=0)
|
|
|
|
title1 = TextMobject("Pattern of coins")
|
|
title2 = TextMobject("Individual square")
|
|
|
|
for title, board in [(title1, board1), (title2, board2)]:
|
|
title.scale(0.5 / title[0][0].get_height())
|
|
title.next_to(board, UP, MED_LARGE_BUFF)
|
|
|
|
title2.align_to(title1, UP)
|
|
|
|
def get_special_square(coins=coins, board=board2):
|
|
bools = [coin.is_heads() for coin in coins]
|
|
return board[boolian_linear_combo(bools)]
|
|
|
|
self.add(board1)
|
|
self.add(title1)
|
|
self.add(coins)
|
|
|
|
self.play(
|
|
GrowArrow(arrow),
|
|
FadeIn(board2, 2 * LEFT)
|
|
)
|
|
square = get_special_square()
|
|
rect = SurroundingRectangle(square, buff=0)
|
|
rect.set_color(PINK)
|
|
rect.next_to(square, OUT, buff=0.01)
|
|
self.play(
|
|
square.set_color, MAROON_C,
|
|
ShowCreation(rect),
|
|
FadeIn(title2)
|
|
)
|
|
|
|
for message in messages[1:]:
|
|
new_bools = string_to_bools(message)
|
|
coins_to_flip = Group()
|
|
for coin, to_heads in zip(coins, new_bools):
|
|
if coin.is_heads() ^ to_heads:
|
|
coins_to_flip.add(coin)
|
|
coins_to_flip.shuffle()
|
|
self.play(LaggedStartMap(
|
|
FlipCoin, coins_to_flip,
|
|
lag_ratio=self.flip_lag_ratio,
|
|
run_time=1,
|
|
))
|
|
|
|
new_square = get_special_square()
|
|
self.play(
|
|
square.set_color, square.original_color,
|
|
new_square.set_color, MAROON_C,
|
|
rect.move_to, new_square, OUT,
|
|
rect.shift, 0.01 * OUT,
|
|
)
|
|
square = new_square
|
|
self.wait()
|
|
|
|
|
|
class FromCoinToSquareMapsSingleFlips(FromCoinToSquareMaps):
|
|
CONFIG = {
|
|
"messages": [
|
|
"FlipBits",
|
|
"BlipBits",
|
|
"ClipBits",
|
|
"ChipBits",
|
|
"ChipBats",
|
|
"ChipRats",
|
|
]
|
|
}
|
|
|
|
|
|
class DiagramOfProgression(ThreeDScene):
|
|
def construct(self):
|
|
# Setup panels
|
|
P1_COLOR = BLUE_C
|
|
P2_COLOR = RED
|
|
|
|
rect = Rectangle(4, 2)
|
|
rect.set_fill(GREY_E, 1)
|
|
panels = Group()
|
|
for x in range(4):
|
|
panels.add(Group(rect.copy()))
|
|
panels.arrange_in_grid(buff=1)
|
|
panels[::2].shift(0.5 * LEFT)
|
|
panels.set_width(FRAME_WIDTH - 2)
|
|
panels.center().to_edge(DOWN)
|
|
|
|
chessboard = Chessboard()
|
|
chessboard.set_height(0.9 * panels[0].get_height())
|
|
coins = CoinsOnBoard(
|
|
chessboard,
|
|
coin_config={
|
|
"disk_resolution": (2, 25),
|
|
"include_labels": False,
|
|
}
|
|
)
|
|
coins.flip_by_message("Tau > Pi")
|
|
|
|
for panel in panels[1:]:
|
|
cb = chessboard.copy()
|
|
co = coins.copy()
|
|
cb.next_to(panel.get_right(), LEFT)
|
|
co.next_to(cb, OUT, 0)
|
|
panel.chessboard = cb
|
|
panel.coins = co
|
|
panel.add(cb, co)
|
|
|
|
kw = {
|
|
"tex_to_color_map": {
|
|
"Prisoner 1": P1_COLOR,
|
|
"Prisoner 2": P2_COLOR,
|
|
}
|
|
}
|
|
titles = VGroup(
|
|
TextMobject("Prisoners conspire", **kw),
|
|
TextMobject("Prisoner 1 sees key", **kw),
|
|
TextMobject("Prisoner 1 flips coin", **kw),
|
|
TextMobject("Prisoner 2 guesses key square", **kw),
|
|
)
|
|
|
|
for panel, title in zip(panels, titles):
|
|
title.next_to(panel, UP)
|
|
panel.title = title
|
|
panel.add(title)
|
|
|
|
# Darken first chessboard
|
|
for coin in panels[1].coins:
|
|
coin.remove(coin.edge)
|
|
if coin.is_heads():
|
|
coin.remove(coin.bottom)
|
|
coin.remove(coin.labels[1])
|
|
else:
|
|
coin.remove(coin.top)
|
|
coin.remove(coin.labels[0])
|
|
coin.set_opacity(0.25)
|
|
|
|
# Add characters
|
|
prisoner1 = PiCreature(color=P1_COLOR)
|
|
prisoner2 = PiCreature(color=P2_COLOR)
|
|
pis = VGroup(prisoner1, prisoner2)
|
|
pis.arrange(RIGHT, buff=1)
|
|
pis.set_height(1.5)
|
|
|
|
p0_pis = pis.copy()
|
|
p0_pis.set_height(2.0, about_edge=DOWN)
|
|
p0_pis[1].flip()
|
|
p0_pis.next_to(panels[0].get_bottom(), UP, SMALL_BUFF)
|
|
p0_pis[0].change("pondering", p0_pis[1].eyes)
|
|
p0_pis[1].change("speaking", p0_pis[0].eyes)
|
|
panels[0].add(p0_pis)
|
|
|
|
p1_pi = pis[0].copy()
|
|
p1_pi.next_to(panels[1].get_corner(DL), UR, SMALL_BUFF)
|
|
p1_pi.change("happy")
|
|
key = Key()
|
|
key.set_height(0.5)
|
|
key.next_to(p1_pi, UP)
|
|
key.set_color(YELLOW)
|
|
key_cube = panels[1].chessboard[18]
|
|
key_square = Square()
|
|
key_square.replace(key_cube)
|
|
key_square.set_stroke(width=3)
|
|
key_square.match_color(key)
|
|
p1_pi.look_at(key_square)
|
|
key_arrow = Arrow(
|
|
key.get_right() + SMALL_BUFF * UP,
|
|
key_square.get_corner(UL),
|
|
path_arc=-45 * DEGREES,
|
|
buff=SMALL_BUFF
|
|
)
|
|
key_arrow.tip.set_stroke(width=0)
|
|
key_arrow.match_color(key)
|
|
panels[1].add(p1_pi, key)
|
|
|
|
p2_pi = pis[0].copy()
|
|
p2_pi.next_to(panels[2].get_corner(DL), UR, SMALL_BUFF)
|
|
p2_pi.change("tease")
|
|
flip_coin = panels[2].coins[38]
|
|
panels[3].coins[38].flip()
|
|
flip_square = Square()
|
|
flip_square.replace(panels[2].chessboard[38])
|
|
flip_square.set_stroke(BLUE, 5)
|
|
for coin in panels[2].coins:
|
|
if coin is not flip_coin:
|
|
coin.remove(coin.edge)
|
|
if coin.is_heads():
|
|
coin.remove(coin.bottom)
|
|
coin.remove(coin.labels[1])
|
|
else:
|
|
coin.remove(coin.top)
|
|
coin.remove(coin.labels[0])
|
|
coin.set_opacity(0.25)
|
|
panels[2].add(p2_pi)
|
|
|
|
p3_pi = pis[1].copy()
|
|
p3_pi.next_to(panels[3].get_corner(DL), UR, SMALL_BUFF)
|
|
p3_pi.shift(MED_LARGE_BUFF * RIGHT)
|
|
p3_pi.change("confused")
|
|
panels[3].add(p3_pi)
|
|
|
|
# Animate each panel in
|
|
self.play(FadeIn(panels[1], DOWN))
|
|
self.play(
|
|
ShowCreation(key_arrow),
|
|
FadeInFromLarge(key_square),
|
|
)
|
|
self.wait()
|
|
|
|
self.play(FadeIn(panels[2], UP))
|
|
self.play(
|
|
ShowCreation(flip_square),
|
|
FlipCoin(flip_coin),
|
|
p2_pi.look_at, flip_coin,
|
|
)
|
|
self.wait()
|
|
|
|
self.play(FadeIn(panels[3], LEFT))
|
|
self.wait()
|
|
|
|
self.play(FadeIn(panels[0]))
|
|
self.wait()
|