3b1b-videos/_2024/puzzles/added_dimension.py
2024-11-11 07:40:05 -08:00

3235 lines
106 KiB
Python

from __future__ import annotations
from manim_imports_ext import *
def get_lozenge(side_length=1):
verts = [math.sqrt(3) * LEFT, UP, math.sqrt(3) * RIGHT, DOWN]
result = Polygon(*verts)
result.scale(side_length / get_norm(verts[0] - verts[1]))
return result
class ShowLozenge(InteractiveScene):
def construct(self):
# Add Lozenge
lozenge = get_lozenge()
lozenge.scale(4)
lozenge.set_stroke(TEAL)
arc1 = Arc(-30 * DEGREES, 60 * DEGREES, arc_center=lozenge.get_left(), radius=0.75)
arc2 = Arc(-150 * DEGREES, 120 * DEGREES, arc_center=lozenge.get_top(), radius=0.5)
VGroup(lozenge, arc1, arc2).stretch(0.95, 0) # Remove
# arc1_label = Tex(R"60^\circ")
arc1_label = Tex(R"70.5^\circ")
arc1_label.next_to(arc1, RIGHT, MED_SMALL_BUFF)
# arc2_label = Tex(R"120^\circ")
arc2_label = Tex(R"109.5^\circ")
arc2_label.next_to(arc2, DOWN, MED_SMALL_BUFF)
angle_labels = VGroup(
arc1, arc1_label,
arc2, arc2_label,
)
angle_labels.set_z_index(1)
self.play(
ShowCreation(lozenge, time_span=(1, 2.5)),
VShowPassingFlash(lozenge.copy().insert_n_curves(20).set_stroke(width=5), time_width=2),
run_time=3
)
self.play(
Write(arc1_label),
ShowCreation(arc1),
)
self.play(
Write(arc2_label),
ShowCreation(arc2),
)
self.add(angle_labels)
self.wait()
# Tile the plane
verts = lozenge.get_anchors()[:4]
v1 = verts[1] - verts[0]
v2 = verts[-1] - verts[0]
row = VGroup(lozenge.copy().shift(x * v1) for x in range(-10, 11))
rows = VGroup(row.copy().shift(y * v2) for y in range(-10, 11))
tiles = VGroup(*rows.family_members_with_points())
tiles.sort(lambda p: get_norm(p))
for mob in row, rows:
mob.set_fill(GREY, 1)
mob.set_stroke(WHITE, 2)
mob.shift(-tiles[0].get_center())
n_center_tiles = 501
self.play(
self.frame.animate.set_height(40),
lozenge.animate.set_fill(GREY, 1),
LaggedStart(
(TransformFromCopy(lozenge, tile, path_arc=30 * DEGREES) for tile in row),
lag_ratio=1.0 / len(row),
time_span=(1, 3),
),
run_time=4
)
self.play(
LaggedStart(
(TransformFromCopy(row, row2, path_arc=30 * DEGREES) for row2 in rows),
lag_ratio=1.0 / len(rows),
run_time=3,
),
)
self.clear()
self.add(rows, angle_labels)
# Squish it
self.play(FadeOut(angle_labels))
rows.save_state()
self.play(rows.animate.stretch(2, 0), run_time=2)
self.wait()
self.play(Restore(rows), run_time=2)
self.play(Write(angle_labels))
self.wait()
class CubesAsHexagonTiling(InteractiveScene):
n = 4
colors = [GREY, GREY, GREY]
def setup(self):
super().setup()
n = self.n
# Set up axes and camera angle
self.frame.set_field_of_view(1 * DEGREES)
self.frame.reorient(135, 55, 0)
self.axes = ThreeDAxes((-5, 5), (-5, 5), (-5, 5))
# Add base
self.base_cube = self.get_half_cube(
side_length=n,
shared_corner=[-1, -1, -1],
grid=True
)
self.add(self.base_cube)
self.add(Point())
# Block pattern
self.block_pattern = np.zeros((n, n, n))
self.cubes = VGroup()
def pre_populate(self):
for x in range(self.n**3 // 2):
new_cube = self.random_new_cube()
self.add_cube(new_cube)
def get_new_cube(self, x, y):
zero_indices = np.where(self.block_pattern[x, y, :] == 0)
if len(zero_indices) == 0:
print("Column full")
return
min_z = np.min(zero_indices)
min_y = np.min(np.where(self.block_pattern[x, :, min_z] == 0))
min_x = np.min(np.where(self.block_pattern[:, min_y, min_z] == 0))
return self.get_half_cube((min_x, min_y, min_z))
def random_new_cube(self):
empty_spaces = np.transpose(np.where(self.block_pattern[:, :, :] == 0))
x, y, z = random.choice(empty_spaces)
return self.get_new_cube(x, y)
def get_random_cube_from_stack(self):
filled_spaces = np.transpose(np.where(self.block_pattern[:, :, :] == 1))
x, y, z = random.choice(filled_spaces)
max_x = np.max(np.where(self.block_pattern[:, y, z] == 1))
max_y = np.max(np.where(self.block_pattern[max_x, :, z] == 1))
max_z = np.max(np.where(self.block_pattern[max_x, max_y, :] == 1))
for cube in self.cubes:
if all(cube.get_corner([-1, -1, -1]).astype(int) == (max_x, max_y, max_z)):
return cube
return self.cubes[-1]
def add_cube(self, cube):
self.cubes.add(cube)
cube.spacer = Mobject()
self.add(cube, cube.spacer)
self.refresh_block_pattern()
def remove_cube(self, cube):
self.cubes.remove(cube)
self.remove(cube, cube.spacer)
self.refresh_block_pattern()
def refresh_block_pattern(self):
self.block_pattern[:, :, :] = 0
for cube in self.cubes:
coords = cube.get_corner([-1, -1, -1]).astype(int)
self.block_pattern[*coords] = 1
def get_half_cube(self, coords=(0, 0, 0), side_length=1, colors=None, shared_corner=[1, 1, 1], grid=False):
if colors is None:
colors = self.colors
squares = Square(side_length).replicate(3)
if grid:
for square in squares:
grid = Square(side_length=1).get_grid(side_length, side_length, buff=0)
grid.move_to(square)
square.add(grid)
axes = [OUT, DOWN, LEFT]
for square, color, axis in zip(squares, colors, axes):
square.set_fill(color, 1)
square.set_stroke(color, 0)
square.rotate(90.1 * DEGREES, axis) # Why 0.1 ?
square.move_to(ORIGIN, shared_corner)
squares.move_to(coords, np.array([-1, -1, -1]))
squares.set_stroke(WHITE, 2)
return squares
def animate_in_with_rotation(self, cube, color=TEAL, run_time=2):
cube.save_state()
cube.rotate(-60 * DEGREES, axis=[1, 1, 1])
cube.set_fill(color)
blackness = cube.copy().set_color(BLACK)
spacer = Mobject()
self.play(FadeIn(cube))
self.add(blackness, spacer, cube)
self.play(Rotate(cube, 60 * DEGREES, axis=[1, 1, 1], run_time=run_time))
self.play(Restore(cube))
self.add_cube(cube)
self.remove(blackness, spacer)
def animate_out_with_rotation(self, cube, color=TEAL, run_time=2):
blackness = cube.copy().set_color(BLACK)
spacer = Mobject()
self.play(cube.animate.set_fill(color))
self.add(blackness, spacer, cube)
self.play(Rotate(cube, 60 * DEGREES, axis=[1, 1, 1], run_time=run_time))
self.remove(blackness, spacer)
self.play(FadeOut(cube))
self.remove_cube(cube)
class AmbientTilingChanges(CubesAsHexagonTiling):
n = 10
# n = 5
def construct(self):
# Hugely inefficient
self.pre_populate()
for x in range(10):
new_cube = self.random_new_cube()
self.animate_in_with_rotation(new_cube)
old_cube = self.get_random_cube_from_stack()
if old_cube is not new_cube:
self.animate_out_with_rotation(old_cube)
class RotationMove(InteractiveScene):
def construct(self):
# Add hex
lozenge = Polygon(math.sqrt(3) * LEFT, UP, math.sqrt(3) * RIGHT, DOWN)
lozenge.move_to(ORIGIN, DOWN)
hexagon = VGroup(
lozenge,
lozenge.copy().rotate(TAU / 3, about_point=ORIGIN),
lozenge.copy().rotate(2 * TAU / 3, about_point=ORIGIN),
)
hexagon.set_fill(TEAL_E, 1)
hexagon.set_stroke(WHITE, 3)
hexagon.set_height(3)
hexagon.move_to(3 * LEFT)
rot_hex = hexagon.copy()
hexagon.rotate(-60 * DEGREES)
rot_hex.move_to(3 * RIGHT)
arrow1 = Arrow(hexagon, rot_hex, thickness=5, path_arc=60 * DEGREES).shift(DOWN)
arrow2 = Arrow(rot_hex, hexagon, thickness=5, path_arc=60 * DEGREES).shift(UP)
self.add(hexagon, rot_hex, arrow1, arrow2)
self.wait()
self.play(LaggedStart(
VShowPassingFlash(arrow1.copy().set_stroke(YELLOW, 3)),
TransformFromCopy(hexagon, rot_hex, path_arc=60 * DEGREES),
lag_ratio=0.1,
run_time=2,
))
self.wait()
class AmbientTilingChangesHexagonBound(AmbientTilingChanges):
n = 4
class IntroduceHexagonFilling(InteractiveScene):
N = 15
tile_color = GREY_C
highlight_color = TEAL
drag_to_pan = False
def setup(self):
super().setup()
# Create hexagonal tiling
lozenge = get_lozenge()
lozenge.set_stroke(WHITE, 1)
lozenge.set_fill(self.tile_color, 1)
lozenge.move_to(ORIGIN, DOWN)
lozenges = VGroup(lozenge.copy().rotate(theta, about_point=ORIGIN) for theta in np.arange(0, TAU, TAU / 3))
tiling = VGroup()
for template in lozenges:
v1 = template.get_vertices()[0]
v2 = template.get_vertices()[2]
for x, y in it.product(*2 * [range(self.N)]):
tiling.add(template.copy().shift(x * v1 + y * v2))
self.add(tiling)
self.tiling = tiling
# Add hexagon
hexagon = RegularPolygon(6, radius=self.N, start_angle=90 * DEGREES)
hexagon.set_stroke(YELLOW, 3)
self.add(hexagon)
self.hexagon = hexagon
self.selected_set = VGroup()
def construct(self):
# Just play around
self.wait(10)
pass
def rotate_selection(self):
trip = self.selected_set
trip.target = trip.generate_target()
trip.target.rotate(TAU / 6)
trip.target.set_fill(self.tile_color)
self.add(trip)
self.play(MoveToTarget(trip, path_arc=TAU / 6))
self.selected_set.clear()
self.add(self.tiling)
for tile in self.tiling:
tile.refresh_bounding_box()
def fill_with_current_tiles(self):
# Populate
starter = self.tiling[0].copy()
starter.to_corner(UL)
random_order = VGroup(*self.tiling)
random_order.shuffle()
self.remove(self.tiling)
self.play(
LaggedStart(
(TransformFromCopy(starter, tile)
for tile in random_order),
lag_ratio=1.0 / len(self.tiling),
run_time=5
)
)
self.add(self.tiling)
def on_mouse_release(
self,
point: Vect3,
button: int,
mods: int
) -> None:
super().on_mouse_release(point, button, mods)
if len(self.selected_set) == 3:
return
mouse_center = self.mouse_point.get_center()
dists = [get_norm(tile.get_center() - mouse_center) for tile in self.tiling]
tile = self.tiling[np.argmin(dists)]
tile.set_fill(self.highlight_color)
if tile in self.selected_set:
tile.set_fill(self.tile_color)
self.selected_set.remove(tile)
else:
self.selected_set.add(tile)
def on_key_release(
self,
symbol: int,
modifiers: int
) -> None:
super().on_key_release(symbol, modifiers)
if chr(symbol) == "p":
self.fill_with_current_tiles()
if len(self.selected_set) != 3:
return
if chr(symbol) == "r":
self.rotate_selection()
class HexagonStack(CubesAsHexagonTiling):
n = 5
colors = [BLUE_B, BLUE_D, BLUE_E]
def construct(self):
# Add hexagonal stack
for x in range(self.n):
for y in range(self.n - x):
for z in range(self.n - x - y):
self.add_cube(self.get_new_cube(x, y))
self.remove(self.base_cube)
self.cubes.set_fill(BLUE_D)
self.wait()
class DrawHexagon(IntroduceHexagonFilling):
def construct(self):
self.remove(self.tiling)
# Test
tiles = self.tiling
tiles.add(*tiles.copy().rotate(60 * DEGREES))
tiles.set_fill(opacity=0)
tiles.set_stroke(WHITE, 0.5, 0.5)
frame = self.frame
hexagon = self.hexagon
brace = Brace(Line(ORIGIN, 4 * UP), RIGHT)
brace.next_to(hexagon, RIGHT, SMALL_BUFF)
brace_label = brace.get_text("4")
self.play(
ShowCreation(hexagon),
VShowPassingFlash(hexagon.copy().set_stroke(width=5).insert_n_curves(20), time_width=2),
run_time=2
)
self.wait()
self.play(
GrowFromCenter(brace),
Write(brace_label),
FadeIn(tiles),
)
self.wait()
class ShowAsThreeD(CubesAsHexagonTiling):
colors = [BLUE_B, BLUE_D, BLUE_E]
def construct(self):
# self.pre_populate()
# Color
grey_cubes = self.cubes.copy()
grey_cubes.set_fill(GREY)
full_grey = Group()
for cube in grey_cubes:
full_grey.add(cube, Point())
self.add(full_grey)
self.base_cube.save_state()
self.base_cube.set_fill(GREY)
self.wait()
self.play(
Restore(self.base_cube),
FadeOut(full_grey),
)
# Change perspective
self.pre_populate()
self.pre_populate()
self.play(
# self.frame.animate.reorient(118, 79, 0, (-1.01, 0.75, 1.54), 8.00)
self.frame.animate.reorient(118, 79, 0, (-1.01, 0.75, 1.54), 12.00).set_field_of_view(40 * DEGREES),
run_time=3
)
self.play(
# self.frame.animate.reorient(163, 83, 0, (1.82, -0.33, 1.89), 8.00),
self.frame.animate.reorient(163, 83, 0, (1.82, -0.33, 1.89), 12.00),
run_time=4
)
self.play(self.frame.animate.reorient(135, 55, 0, ORIGIN, 8).set_field_of_view(1 * DEGREES), run_time=3)
# Rotation is adding
new_cube = self.random_new_cube()
self.animate_in_with_rotation(new_cube)
self.wait()
self.play(
new_cube.animate.shift(5 * RIGHT + 3 * OUT),
run_time=6,
rate_func=there_and_back_with_pause
)
self.wait()
# Add or remove a few
for x in range(2):
new_cube = self.random_new_cube()
self.animate_in_with_rotation(new_cube, run_time=1)
old_cube = self.get_random_cube_from_stack()
if old_cube is not new_cube:
self.animate_out_with_rotation(old_cube, run_time=1)
# Remove all
while len(self.cubes) > 0:
cube = self.get_random_cube_from_stack()
self.play(FadeOut(cube, 0.25 * OUT, run_time=0.25))
self.remove_cube(cube)
self.wait()
for x in range(self.n**3):
cube = self.random_new_cube()
self.play(FadeIn(cube, shift=0.25 * IN, run_time=0.25))
self.add_cube(cube)
class Project3DCube(InteractiveScene):
def construct(self):
# Set axes
frame = self.frame
light_source = self.camera.light_source
frame.reorient(28, 68, 0, (0.99, 0.63, 0.66), 2.89)
light_source.move_to([3, 5, 7])
axes = ThreeDAxes(
(-3, 3), (-3, 3), (-3, 3),
axis_config=dict(tick_size=0.05)
)
axes.set_stroke(GREY_A, 1)
plane = NumberPlane((-3, 3), (-3, 3))
plane.axes.set_stroke(GREY_A, 1)
plane.background_lines.set_stroke(BLUE_E, 0.5)
plane.faded_lines.set_stroke(BLUE_E, 0.5, 0.25)
self.add(plane, axes)
# Add cube
vertices = np.array(list(it.product(*3 * [[0, 1]])))
vert_dots = DotCloud(vertices)
vert_dots.make_3d()
vert_dots.set_radius(0.025)
vert_dots.set_color(TEAL)
cube_shell = VGroup(
Line(vertices[i], vertices[j])
for i, p1 in enumerate(vertices)
for j, p2 in enumerate(vertices[i + 1:], start=i + 1)
if get_norm(p2 - p1) == 1
)
cube_shell.set_stroke(YELLOW, 1)
cube_shell.set_anti_alias_width(1)
cube_shell.set_width(1)
cube_shell.move_to(ORIGIN, [-1, -1, -1])
self.play(Write(cube_shell, lag_ratio=0.1, run_time=2))
self.wait()
# Show the coordinates
labels = VGroup()
for vert in vertices:
coords = vert.astype(int)
label = Tex(str(tuple(coords)), font_size=12)
label.next_to(vert, DR, buff=0.05)
label.rotate(45 * DEGREES, RIGHT, about_point=vert)
label.set_backstroke(BLACK, 2)
labels.add(label)
self.play(
LaggedStartMap(FadeIn, labels),
FadeIn(vert_dots),
frame.animate.reorient(10, 61, 0, (0.9, 0.51, 0.48), 2.44),
run_time=3,
)
self.wait(note="Talk through the coordinates")
# Show base and top square
edges = VGroup(*cube_shell)
edges.sort(lambda p: p[2])
self.play(
edges[4:].animate.set_stroke(width=0.5, opacity=0.25),
labels[1::2].animate.set_opacity(0.1)
)
self.wait()
self.play(
edges[8:].animate.set_stroke(width=2, opacity=1),
labels[1::2].animate.set_opacity(1),
edges[:4].animate.set_stroke(width=0.5, opacity=0.25),
labels[0::2].animate.set_opacity(0.1)
)
self.wait()
self.play(
edges.animate.set_stroke(width=1, opacity=1),
labels.animate.set_opacity(1)
)
self.play(FadeOut(labels))
# Orient to look down the corner
self.play(frame.animate.reorient(135.795, 55.795, 0, (-0.02, -0.08, 0.05), 3.61), run_time=4)
self.wait(2, note="Take a moment to look down the corner")
self.play(frame.animate.reorient(50, 68, 0, (-0.46, 0.29, 0.23), 3.45), run_time=4)
# Show the flat projection
diag_vect = Vector([1, 1, 1], thickness=2)
diag_vect.set_perpendicular_to_camera(frame)
diag_label = labels[-1].copy()
proj_mat = self.construct_proj_matrix()
perp_plane = Square3D().set_width(20)
perp_plane.set_color(GREY_E, 0.5)
perp_plane.apply_matrix(proj_mat)
proj_cube_shell = cube_shell.copy().apply_matrix(proj_mat)
proj_vert_dots = vert_dots.copy().apply_matrix(proj_mat)
self.play(
GrowArrow(diag_vect),
FadeIn(diag_label, shift=np.ones(3)),
cube_shell.animate.set_stroke(opacity=0.25),
)
self.wait()
self.play(
TransformFromCopy(cube_shell, proj_cube_shell),
TransformFromCopy(vert_dots, proj_vert_dots),
)
self.wait(10, note="Talk through projection")
frame.save_state()
self.play(
frame.animate.reorient(134.75, 54.47, 0, (-0.46, 0.29, 0.23), 3.45).set_field_of_view(1 * DEGREES),
run_time=4
)
self.wait()
self.play(Restore(frame, run_time=3))
self.wait()
# Project more cubes down
cube_grid = VGroup(
cube_shell.copy().shift(vect)
for vect in it.product(*3 * [[0, 1, 2]])
)
cube_grid.remove(cube_grid[0])
proj_cube_grid = cube_grid.copy().apply_matrix(proj_mat)
proj_cube_grid.set_stroke(YELLOW, 2, 0.5)
ghost_cube = cube_shell.copy().set_opacity(0)
self.play(
LaggedStart(
(TransformFromCopy(ghost_cube, new_cube)
for new_cube in cube_grid),
lag_ratio=0.05,
),
frame.animate.reorient(40, 72, 0, (1.25, 1.69, 0.99), 5.10),
run_time=5
)
self.wait()
self.play(
TransformFromCopy(cube_grid, proj_cube_grid),
frame.animate.reorient(60, 68, 0, (0.81, 1.09, 0.94), 5.36),
run_time=3
)
self.wait(note="Any commentary?")
self.play(
FadeOut(cube_grid),
FadeOut(proj_cube_grid),
FadeOut(diag_label),
FadeOut(diag_vect),
FadeOut(vert_dots),
FadeOut(proj_vert_dots),
frame.animate.reorient(42, 62, 0, (0.68, 0.48, 0.41), 2.34),
run_time=2,
)
# Show cube faces
cube = Cube()
cube.set_color(BLUE_E, 1)
cube.set_shading(0.75, 0.25, 0.5)
cube.replace(cube_shell)
cube.sort(lambda p: np.dot(p, np.ones(3)))
inner_faces = cube[:3]
outer_faces = cube[3:]
for mob in [cube_shell, proj_cube_shell, plane]: # No axes?
mob.apply_depth_test()
self.add(axes, cube, cube_shell, plane, proj_cube_shell)
self.play(
FadeIn(cube),
proj_cube_shell.animate.set_stroke(width=1, opacity=0.2),
)
self.wait(10, note="Note the outer faces")
self.add(axes, inner_faces, cube_shell, plane, proj_cube_shell)
self.play(
FadeOut(outer_faces),
inner_faces.animate.set_submobject_colors_by_gradient(RED, GREEN, BLUE),
)
self.wait(10, note="Gesture at inner faces")
inner_faces.save_state()
self.play(inner_faces.animate.apply_matrix(proj_mat), run_time=2)
self.play(inner_faces.animate.space_out_submobjects(1.2), rate_func=there_and_back, run_time=2)
self.wait(10)
# Shuffle faces around
inner_proj_state = inner_faces.copy()
self.wait()
self.play(Restore(inner_faces), run_time=2)
inner_faces.target = inner_faces.generate_target()
for face, vect in zip(inner_faces.target, [UP, RIGHT, OUT]):
face.shift(vect)
outer_state = inner_faces.target.copy()
self.play(MoveToTarget(inner_faces, lag_ratio=0.5, run_time=3))
self.wait()
self.play(inner_faces.animate.apply_matrix(proj_mat), run_time=2)
self.play(inner_faces.animate.space_out_submobjects(1.2), rate_func=there_and_back, run_time=2)
self.wait()
for u in [-1, 1]:
self.play(Rotate(inner_faces, u * PI / 3, axis=np.ones(3), run_time=2))
self.wait()
for group in inner_faces, inner_proj_state:
for i, mob in enumerate(group):
mob.shift(i * 0.0001 * IN)
self.play(Transform(inner_faces, inner_proj_state, lag_ratio=0.5, run_time=3))
self.wait()
self.play(Restore(inner_faces), run_time=2)
# Show coordinates for inner faces
bases = np.identity(3, dtype=int)
vects = VGroup(Vector(basis, thickness=2) for basis in bases)
coord_labels = VGroup(
Tex(str(tuple(basis)), font_size=16).next_to(basis, UR, buff=0.05).rotate(45 * DEGREES, RIGHT, about_point=basis)
for basis in bases
)
self.play(
axes.animate.set_stroke(width=0.5),
plane.axes.animate.set_stroke(width=0.5),
FadeOut(inner_faces),
LaggedStartMap(GrowArrow, vects),
run_time=2
)
self.wait()
self.play(
LaggedStartMap(FadeIn, coord_labels),
frame.animate.reorient(9, 63, 0, (1.03, 0.61, 0.56), 2.72),
run_time=2
)
self.wait()
# Emphasize pairs
vects_state = vects.copy()
labels_state = coord_labels.copy()
last_face = VectorizedPoint()
ordered_faces = Group(inner_faces[i] for i in [1, 0, 2])
ordered_faces.set_opacity(0.8)
ordered_faces.deactivate_depth_test()
for i in range(3):
vects_target = vects_state.copy()
labels_target = labels_state.copy()
vects_target[i].fade(0.8)
labels_target[i].fade(0.8)
self.add(ordered_faces[i], vects, coord_labels)
self.play(
Transform(vects, vects_target),
Transform(coord_labels, labels_target),
FadeIn(ordered_faces[i]),
FadeOut(last_face),
)
self.wait()
last_face = ordered_faces[i]
# Project all the vectors
proj_vects = VGroup(
Vector(np.dot(basis, proj_mat.T), thickness=3)
for basis in np.identity(3)
)
proj_coords = VGroup(
Tex(f"P{str(tuple(basis))}", font_size=16)
for basis in np.identity(3, dtype=int)
)
for label, vect in zip(proj_coords, proj_vects):
label.move_to(vect.get_end() + 0.25 * vect.get_vector())
label.rotate(45 * DEGREES, RIGHT)
label.rotate(45 * DEGREES, OUT)
vect.set_perpendicular_to_camera(self.frame)
proj_coords[1].shift(0.25 * UP)
faces = Group(ordered_faces[2], ordered_faces[0], ordered_faces[1])
self.add(faces, vects, coord_labels)
self.play(
Transform(vects, vects_state),
Transform(coord_labels, labels_state),
FadeIn(faces),
frame.animate.reorient(44, 55, 0, (1.03, 0.61, 0.56), 2.72),
run_time=2
)
self.play(
Transform(vects, proj_vects),
Transform(coord_labels, proj_coords),
faces.animate.apply_matrix(proj_mat),
)
self.play(frame.animate.reorient(56, 58, 0, (0.7, 0.32, 0.6), 2.72), run_time=5)
def add_coordinate_labels(self, axes):
coordinate_config = dict(font_size=12, buff=0.1)
axes.add_coordinate_labels(**coordinate_config)
axes.z_axis.add_numbers(
**coordinate_config,
excluding=[0],
direction=LEFT
)
for number in axes.z_axis.numbers:
number.scale(0.75, about_edge=RIGHT)
number.rotate(90 * DEGREES, RIGHT)
def construct_proj_matrix(self):
diag = normalize(np.ones(3))
id3 = np.identity(3)
return np.array([self.project(basis, diag) for basis in id3]).T
def gram_schmitt(self, vects):
for i in range(len(vects)):
for j in range(i):
vects[i] = self.project(vects[i], vects[j])
vects[i] = normalize(vects[i])
return vects
def project(self, vect, unit_norm):
"""
Project v1 onto the orthogonal subspace of norm
"""
return vect - np.dot(unit_norm, vect) * unit_norm
class Project4DCube(Project3DCube):
def construct(self):
# Get hypercube data
frame = self.frame
hypercube_points, edge_indices = self.get_hypercube_data()
# Prepare pre-projectiong
w_shift = 2 * RIGHT + UP + OUT
cube_verts = np.array(list(it.product(*3 * [[0, 1]])))
cube_shell = VGroup(
Line(cube_verts[i], cube_verts[j])
for i, p1 in enumerate(cube_verts)
for j, p2 in enumerate(cube_verts[i + 1:], start=i + 1)
if get_norm(p2 - p1) == 1
)
cube_shells = cube_shell.replicate(2)
cube_shells[1].shift(w_shift)
edge_connectors = VGroup(Line(v, v + w_shift) for v in cube_verts)
cube_shells[0].set_stroke(BLUE, 2)
cube_shells[1].set_stroke(YELLOW, 2)
edge_connectors.set_stroke(WHITE, 1)
coord_labels = VGroup()
for point in hypercube_points:
label = Tex(str(tuple(point)), font_size=12)
point_3d = point[:3] + point[3] * w_shift
label.next_to(point_3d, DR, buff=0.05)
label.rotate(45 * DEGREES, RIGHT, about_point=point_3d)
coord_labels.add(label)
coord_labels.set_backstroke(BLACK, 2)
low_labels = coord_labels[0::2]
high_labels = coord_labels[1::2]
low_labels.set_z_index(1)
high_labels.set_z_index(1)
for group in [low_labels, high_labels]:
group.generate_target()
for part in group.target:
part[-2].set_fill(RED)
# Show lists of coordinates
titles = VGroup(Text(f"{n}D Cube Vertices") for n in [3, 4])
coords3d = VGroup(Tex(str(tuple(coords))) for coords in it.product(*3 * [[0, 1]]))
coords4d = VGroup(Tex(str(tuple(coords))) for coords in it.product(*4 * [[0, 1]]))
coords3d.scale(0.75).arrange(DOWN, buff=MED_SMALL_BUFF)
coords4d.scale(0.75).arrange_in_grid(8, 2, v_buff=MED_SMALL_BUFF, h_buff=0.5)
for title, vect, coords in zip(titles, [LEFT, RIGHT], [coords3d, coords4d]):
title.move_to(vect * FRAME_WIDTH / 4).to_edge(UP)
title.add(Underline(title))
coords.set_backstroke(BLACK, 2)
coords.next_to(title, DOWN)
self.add(titles)
self.add(coords3d)
self.play(LaggedStartMap(FadeIn, coords4d, shift=0.1 * DOWN, lag_ratio=0.1, run_time=3))
self.wait()
label_group3d = VGroup(titles[0], coords3d)
label_group4d = VGroup(titles[1], coords4d)
VGroup(label_group3d, label_group4d).fix_in_frame()
# Show pre-projection
pre_low_labels = coords4d[0::2].copy()
pre_low_labels.unfix_from_frame()
pre_low_labels.set_backstroke(BLACK, 2)
label_group4d.target = label_group4d.generate_target()
label_group4d.target.scale(0.5).to_corner(UL)
label_group4d.target[1][1::2].set_opacity(0.2)
self.play(
Write(cube_shells[0]),
TransformFromCopy(pre_low_labels, low_labels),
frame.animate.reorient(11, 67, 0, (1.08, 0.47, 0.77), 3.22),
FadeOut(label_group3d, 3 * LEFT),
MoveToTarget(label_group4d),
run_time=3
)
self.wait(6, note="Pan somewhat")
self.play(
MoveToTarget(low_labels),
LaggedStart(
(FlashUnder(label[-3:], color=RED)
for label in low_labels),
lag_ratio=0.05,
)
)
self.wait()
self.play(
ShowCreation(edge_connectors, lag_ratio=0),
TransformFromCopy(*cube_shells),
TransformFromCopy(low_labels, high_labels),
label_group4d[1][1::2].animate.set_opacity(1),
run_time=3
)
self.wait()
self.play(
MoveToTarget(high_labels),
LaggedStart(
(FlashUnder(label[-3:], color=RED)
for label in high_labels),
lag_ratio=0.05,
)
)
self.wait(20, note="Pan and gesture")
# Put pre-projection in the corner
axes = ThreeDAxes((-3, 3), (-3, 3), (-3, 3))
axes.set_height(12)
pre_proj_points = np.array([
*hypercube_points[:8, 1:],
*(hypercube_points[:8, 1:] + w_shift),
])
pre_proj_frame = VGroup(
Line(pre_proj_points[i], pre_proj_points[j])
for i, j in edge_indices
)
pre_proj_frame.set_stroke(WHITE, 1)
pre_proj_frame.generate_target()
pre_proj_frame.target.fix_in_frame()
pre_proj_frame.target.set_height(1.0)
pre_proj_frame.target.rotate(60 * DEGREES, LEFT).rotate(45 * DEGREES, UP).rotate(15 * DEGREES, OUT)
pre_proj_frame.target.to_corner(UL, buff=LARGE_BUFF)
cloud = ThoughtBubble(Rectangle(2, 1.5))[0][3]
cloud.set_fill(GREY_E, 1)
cloud.to_corner(UL, buff=MED_SMALL_BUFF)
cloud.fix_in_frame()
cloud_label = Text("4D")
cloud_label.next_to(cloud, DOWN)
cloud_label.fix_in_frame()
pre_proj_frame.target.move_to(cloud)
arrow = Arrow(cloud.get_right(), UL, path_arc=-60 * DEGREES, thickness=5)
arrow.set_fill(border_width=0.5)
arrow.fix_in_frame()
arrow_label = TexText("Project along [1, 1, 1, 1]", font_size=24)
arrow_label.next_to(arrow.pfp(0.15), UR, buff=0.15)
arrow_label.fix_in_frame()
self.play(
FadeOut(label_group4d),
FadeOut(VGroup(cube_shells, edge_connectors, coord_labels)),
FadeIn(pre_proj_frame),
)
self.add(cloud, pre_proj_frame),
self.play(
FadeIn(cloud, time_span=(2, 3)),
Write(cloud_label, time_span=(2, 3)),
MoveToTarget(pre_proj_frame),
frame.animate.reorient(22, 76, 0, (-1.33, 0.51, 0.63), 7.64),
run_time=3,
)
self.wait()
self.play(
GrowArrow(arrow, path_arc=-30 * DEGREES),
Write(arrow_label),
Write(axes),
)
self.wait()
corner_group = VGroup(cloud, cloud_label, pre_proj_frame, arrow, arrow_label)
# Project down
proj_coords = self.project_along_diagonal(hypercube_points)
proj_points = axes.c2p(*proj_coords.T)
proj_frame = VGroup(
Line(proj_points[i], proj_points[j])
for i, j in edge_indices
)
proj_frame.set_stroke(YELLOW, 2)
self.add(Point(), pre_proj_frame)
self.play(Transform(pre_proj_frame.copy(), proj_frame.copy(), run_time=3, remover=True))
self.add(Point(), proj_frame)
self.wait()
# Show solid faces
inner_cells = self.get_rhombic_dodec(side_length=axes.x_axis.get_unit_size())
inner_cells.set_color(BLUE_E, 1)
axes.apply_depth_test()
self.play(
FadeOut(proj_frame),
FadeIn(inner_cells),
)
self.wait()
# Break up inner cells
space_factor = 1.5
ghost_cells = inner_cells.copy()
ghost_cells.deactivate_depth_test()
ghost_cells.set_opacity(0.1)
inner_cells.target = inner_cells.generate_target()
inner_cells.target.space_out_submobjects(space_factor)
for group in [inner_cells.target, ghost_cells]:
group.set_submobject_colors_by_gradient(RED_E, GREEN_E, BLUE_E, PINK)
self.play(
MoveToTarget(inner_cells),
FadeOut(corner_group),
run_time=2
)
self.wait()
self.play(
FadeOut(inner_cells),
FadeIn(ghost_cells, scale=0.8),
)
# Projected bases
proj_bases = self.construct_proj_matrix().T
proj_basis_vectors = VGroup(
Vector(axes.c2p(*basis))
for basis in proj_bases
)
proj_basis_labels = VGroup(
Tex(Rf"P{tuple(basis)}", font_size=24)
for basis in np.identity(4).astype(int)
)
for vect, label in zip(proj_basis_vectors, proj_basis_labels):
vect.set_perpendicular_to_camera(frame) # Always?
label.next_to(vect.get_end(), RIGHT, SMALL_BUFF)
label.rotate(45 * DEGREES, about_point=vect.get_end(), axis=RIGHT)
proj_basis_labels[0].shift(0.25 * DOWN)
self.play(
axes.animate.set_stroke(width=1),
LaggedStartMap(GrowArrow, proj_basis_vectors, suspend_mobject_updating=True),
FadeIn(proj_basis_labels),
)
self.wait()
self.play(
ghost_cells.animate.space_out_submobjects(space_factor).set_opacity(0.5),
run_time=2
)
# Iterate through triplets
ordered_cells = Group(ghost_cells[i] for i in [0, 1, 2, 3])
vect_groups = VGroup(
VGroup(vect, label)
for vect, label in zip(proj_basis_vectors, proj_basis_labels)
)
self.add(ordered_cells)
for i in range(4):
vect_groups.generate_target()
vect_groups.target.set_fill(opacity=1)
vect_groups.target[i].set_fill(opacity=0.1)
ordered_cells.generate_target()
ordered_cells.target.set_opacity(0.05)
ordered_cells.target[i].set_opacity(0.5)
self.play(
MoveToTarget(vect_groups),
MoveToTarget(ordered_cells),
)
self.wait()
self.play(
FadeOut(vect_groups),
FadeOut(ordered_cells),
FadeIn(inner_cells),
)
# Play more
self.wait(5)
self.play(inner_cells.animate.space_out_submobjects(1.0 / space_factor))
self.wait(10)
# Show inversion
self.play(FadeOut(inner_cells[1:]))
self.wait()
self.play(
inner_cells[0].animate.move_to(-inner_cells[0].get_center()),
rate_func=there_and_back_with_pause,
run_time=6,
)
self.wait()
self.play(FadeIn(inner_cells[1:]))
self.wait()
inner_cells.save_state()
self.play(
LaggedStart(
(cell.animate.move_to(-cell.get_center())
for cell in inner_cells),
group=inner_cells,
group_type=Group,
run_time=3,
lag_ratio=0.25
),
)
self.wait()
self.play(Restore(inner_cells))
self.wait()
# Tile space
N = 4
small_space_factor = 1.1
tiling = Group()
for i in range(4):
indices = list(range(4))
indices.remove(i)
bases = proj_bases[indices]
for coords in it.product(*3 * [list(range(N))]):
vect = axes.c2p(*np.dot(coords, bases))
new_cell = inner_cells[i].copy().shift(vect)
tiling.add(new_cell)
tiling.space_out_submobjects(small_space_factor)
tiling.sort(lambda p: get_norm(p))
colored_tiling = tiling.copy()
tiling.set_color(BLUE_E)
self.play(
FadeOut(inner_cells[:2]),
FadeOut(inner_cells[3:]),
axes.animate.set_stroke(width=0, opacity=0),
)
self.wait(15)
self.remove(inner_cells)
self.play(
LaggedStart(
(TransformFromCopy(inner_cells[2], cell)
for cell in tiling),
group_type=Group,
lag_ratio=0.05,
),
frame.animate.reorient(19, 65, 0, (1.39, 1.51, 0.57), 21.55),
run_time=8
)
self.wait(20)
self.play(frame.animate.reorient(36, 66, 0, (-1.32, 0.25, -0.7), 22.55), run_time=3)
self.play(frame.animate.increment_theta(PI), run_time=10)
self.play(Transform(tiling, colored_tiling))
self.wait()
def get_hypercube_data(self):
points = np.array(list(it.product(*4 * [[0, 1]])))
edge_indices = [
(i, j)
for i, p1 in enumerate(points)
for j, p2 in enumerate(points[i + 1:], start=i + 1)
if get_norm(p2 - p1) == 1
]
return points, edge_indices
def project_along_diagonal(self, points):
if not hasattr(self, "diag_4d_projection"):
self.diag_4d_projection = self.construct_proj_matrix()
return np.dot(points, self.diag_4d_projection.T)
def construct_proj_matrix(self):
diag = normalize(np.ones(4))
id4 = np.identity(4)
pre_basis = np.array([diag, id4[1] - id4[0], id4[2], id4[3]])
basis = self.gram_schmitt(pre_basis)
return basis[1:, :]
def get_rhombic_dodec(self, side_length=1):
cube = Cube()
cube.set_width(side_length)
cube.move_to(ORIGIN, -np.ones(3))
proj_bases = self.project_along_diagonal(np.identity(4))
cells = Group()
for i in range(4):
indices = list(range(4))
indices.remove(i)
mat = proj_bases[indices]
cells.add(cube.copy().apply_matrix(mat.T, about_point=ORIGIN))
cells.set_color(BLUE_E, 1)
return cells
class ShowRhombicDodecTesselation(Project4DCube):
def construct(self):
# Create tiling pattern
frame = self.frame
proj_bases = self.project_along_diagonal(np.identity(4))
dodec = self.get_rhombic_dodec()
N = 6
coords = [
coords
for coords in it.product(*4 * [list(range(N))])
if sum(coords) == 8
]
pieces = Group(
dodec.copy().shift(np.dot(coord, proj_bases))
for coord in coords
)
pieces.sort(lambda p: get_norm(p))
for piece in pieces:
piece.set_color(random_bright_color(hue_range=(0.5, 0.55), luminance_range=(0.25, 0.5)))
piece.save_state()
pieces.space_out_submobjects(1.25)
pieces.set_opacity(0)
frame.reorient(31, 77, 0).set_height(6)
self.play(
LaggedStart(
(Restore(piece)
for piece in pieces),
lag_ratio=0.5,
group_type=Group
),
frame.animate.reorient(-93, 68, 0, (0.07, 0.22, 0.17), 15),
run_time=12,
)
class CubeToHypercubeAnalogy(InteractiveScene):
def construct(self):
# Vertices
# Numbers of faces/cells
# Which specific cells touch the origin
pass
class AskStripQuestion(InteractiveScene):
def construct(self):
# Add circle
radius = 2.5
circle = Circle(radius=radius)
circle.set_stroke(YELLOW, 2)
radial_line = Line(circle.get_center(), circle.get_right())
radial_line.set_stroke(WHITE, 2)
radius_label = Integer(1)
radius_label.next_to(radial_line, UP, SMALL_BUFF)
self.play(
ShowCreation(radial_line),
FadeIn(radius_label, RIGHT)
)
self.play(
Rotate(radial_line, 2 * PI, about_point=circle.get_center()),
ShowCreation(circle),
run_time=2
)
self.wait()
# Show first strip
r0_tracker = ValueTracker(0.2)
r1_tracker = ValueTracker(0.8)
strip1 = always_redraw(lambda: self.get_strip(
circle,
r0_tracker.get_value(), r1_tracker.get_value(),
theta=TAU / 3,
color=TEAL,
include_arrow=True,
label=""
))
radius = radial_line.get_length()
width_label = DecimalNumber(0)
width_label.add_updater(lambda m: m.set_value(r1_tracker.get_value() - r0_tracker.get_value()))
width_label.add_updater(lambda m: m.set_height(min(0.33, 0.5 * strip1.submobjects[0].get_height())))
width_label.always.next_to(strip1.submobjects[0].get_center(), UR, SMALL_BUFF)
d_label = Tex(R"d_1")
d_label.move_to(width_label, DL)
strip1.suspend_updating()
self.animate_strip_in(strip1)
self.wait()
self.play(Write(width_label, suspend_mobject_updating=True))
strip1.resume_updating()
self.play(
r0_tracker.animate.set_value(0.49),
r1_tracker.animate.set_value(0.51),
run_time=4,
rate_func=there_and_back,
)
strip1.clear_updaters()
width_label.clear_updaters()
self.wait()
self.play(ReplacementTransform(width_label, d_label))
strip1.add(d_label)
# Add first couple strips
new_strips = VGroup(
self.get_strip(
circle, r0, r1, angle,
color=color,
include_arrow=True,
label=f"d_{n}",
)
for n, r0, r1, angle, color in [
(2, 0.5, 0.75, 2 * TAU / 3, GREEN),
(3, 0.1, 0.3, 0.8 * TAU, BLUE_D),
(4, 0.4, 0.7, 0.1 * TAU, BLUE_B),
]
)
for strip in new_strips:
self.animate_strip_in(strip)
# Cover in lots of strips
np.random.seed(0)
strips = VGroup(
self.get_strip(
circle,
*sorted(np.random.uniform(-1, 1, 2)),
TAU * np.random.uniform(0, TAU),
opacity=0.25
).set_stroke(width=1)
for n in range(10)
)
self.add(strips, strip1, new_strips, circle, radius_label)
self.play(FadeIn(strips, lag_ratio=0.5, run_time=3))
self.wait()
# Add together all the widths
frame = self.frame
arrows = VGroup(strip.submobjects[0] for strip in (strip1, *new_strips))
d_labels = VGroup(strip.submobjects[1] for strip in (strip1, *new_strips))
top_expr = Tex(R"d_1 + d_2 + d_3 + d_4 + \cdots + d_n")
top_expr.to_edge(UP, buff=0)
d_labels.target = VGroup(
top_expr[f"d_{n}"][0]
for n in range(1, 5)
)
self.play(
LaggedStart(
MoveToTarget(d_labels, lag_ratio=0.01),
Write(top_expr["+"]),
Write(top_expr[R"\cdots"]),
Write(top_expr[R"d_n"]),
lag_ratio=0.5
),
FadeOut(arrows),
frame.animate.move_to(UP).set_anim_args(run_time=2)
)
self.remove(d_labels)
self.add(top_expr)
self.wait()
# Compress sum
short_expr = Tex(R"\min\left( \sum_i d_i \right)")
short_expr.move_to(top_expr)
self.play(
LaggedStart(
ReplacementTransform(top_expr[re.compile("d_.")], short_expr["d_i"]),
ReplacementTransform(top_expr["+"], short_expr[R"\sum"]),
ReplacementTransform(top_expr[R"\cdots"], short_expr["i"][1]),
lag_ratio=0.25
)
)
self.wait()
self.play(LaggedStart(
Write(short_expr[R"\min\left("]),
Write(short_expr[R"\right)"]),
lag_ratio=0.5
))
self.wait()
# Show various alternate coverings
d_labels.set_opacity(0)
arrows.set_opacity(0)
curr_strips = VGroup(strip1, *new_strips, *strips)
og_strips = curr_strips
for _ in range(4):
self.play(FadeOut(curr_strips))
base_hue = random.random()
curr_strips = VGroup(
self.get_strip(
circle,
*sorted(np.random.uniform(-1, 1, 2)),
TAU * np.random.uniform(0, TAU),
color=random_bright_color(hue_range=(base_hue, base_hue + 0.2)),
opacity=0.25
).set_stroke(width=1)
for n in range(15)
)
self.play(ShowIncreasingSubsets(curr_strips))
self.play(FadeOut(curr_strips))
self.play(ShowIncreasingSubsets(og_strips))
# Show trivial covering
fat_strip = self.get_strip(circle, -1, 1, 0, RED_B)
fat_strip.rect.set_height(6, stretch=True)
fat_strip.pre_rect.move_to(fat_strip.rect, DOWN)
top_brace = Brace(fat_strip.rect, UP)
top_label = top_brace.get_text("2")
self.play(
FadeOut(og_strips),
short_expr.animate.next_to(circle, RIGHT, buff=LARGE_BUFF),
frame.animate.move_to(0.5 * UP)
)
self.play(Transform(fat_strip.pre_rect, fat_strip.rect))
self.play(GrowFromCenter(top_brace), Write(top_label))
self.wait()
# Subdivide trivial covering
subdivision = sorted([-1, 1, *np.random.uniform(-1, 1, 10)])
strips = VGroup(
self.get_strip(circle, r0, r1, theta=0, color=random_bright_color(hue_range=(0.3, 0.5)))
for r0, r1 in zip(subdivision, subdivision[1:])
)
self.play(
FadeOut(fat_strip.pre_rect),
FadeIn(strips, lag_ratio=0.5, run_time=2)
)
self.wait()
# Show suggestive fan covering
fan_covering = VGroup(
self.get_strip(circle, -0.4, 0.4, theta=theta)
for theta in np.arange(0, TAU, TAU / 3)
)
fan_covering.add(*(
self.get_strip(circle, 0.6, 0.9, theta=theta)
for theta in np.arange(TAU / 12, TAU, TAU / 3)
))
self.play(FadeOut(strips))
for strip in fan_covering:
self.animate_strip_in(strip)
self.wait()
def get_strip(self, circle, r0, r1, theta, color=None, opacity=0.5, include_arrow=False, label="", rect_length=10.0):
diam = circle.get_width()
width = (r1 - r0) * diam / 2
if color is None:
color = random_bright_color(luminance_range=(0.5, 0.7))
rect = Rectangle(width, rect_length)
rect.move_to(
interpolate(circle.get_center(), circle.get_right(), r0),
LEFT,
)
rect.set_fill(color, opacity)
rect.set_stroke(color, 1)
pre_rect = rect.copy().stretch(0, 1, about_edge=DOWN)
pre_rect.set_stroke(width=0)
VGroup(rect, pre_rect).rotate(theta, about_point=circle.get_center())
strip = Intersection(rect, circle)
strip.match_style(rect)
strip.rect = rect
strip.pre_rect = pre_rect
if include_arrow:
arrow = Tex(R"\longleftrightarrow")
arrow.set_width(width, stretch=True)
arrow.rotate(theta)
arrow.move_to(rect)
strip.add(arrow)
if len(label) > 0:
label = Tex(label, font_size=36)
label.move_to(rect.get_center())
vect = 0.25 * rotate_vector(UP, theta)
vect *= np.sign(vect[1])
label.shift(vect)
strip.add(label)
return strip
def animate_strip_in(self, strip):
self.play(Transform(strip.pre_rect, strip.rect))
self.play(LaggedStart(
FadeIn(strip),
FadeOut(strip.pre_rect),
lag_ratio=0.5,
run_time=1,
))
class StruggleWithStrips(AskStripQuestion):
def construct(self):
# Add circle
radius = 2.5
circle = Circle(radius=radius)
circle.set_stroke(YELLOW, 2)
radial_line = Line(circle.get_center(), circle.get_right())
radial_line.set_stroke(WHITE, 2)
radius_label = Integer(1)
radius_label.next_to(radial_line, UP, SMALL_BUFF)
self.add(circle, radial_line, radius_label)
# Show fan strategy
angles = [*np.arange(0, TAU, TAU / 3), *np.arange(TAU / 12, TAU, TAU / 3)]
widths = [*3 * [0.8], *3 * [0.25]]
strips = VGroup(
self.get_strip(circle, -0.4, 0.4, theta=theta, include_arrow=True)
for theta in angles[:3]
)
strips.add(*(
self.get_strip(circle, 0.7, 0.95, theta=theta, include_arrow=True)
for theta in angles[3:]
))
arrows = VGroup()
for strip in strips:
arrow = strip[0]
strip.remove(arrow)
arrows.add(arrow)
self.play(LaggedStart(
(TransformFromCopy(strip.pre_rect, strip.rect)
for strip in strips),
lag_ratio=0.1,
))
rects = VGroup(strip.rect for strip in strips)
self.play(
LaggedStartMap(FadeOut, rects),
LaggedStartMap(FadeIn, strips),
)
# Show the sum
sum_expr = Tex("0.00 + 0.00 + 0.00 + 0.00 + 0.00 + 0.00 = 0.00")
sum_expr.to_edge(UP)
decimals = sum_expr.make_number_changeable("0.00", replace_all=True)
width_terms = decimals[:6]
sum_term = decimals[6]
plusses = sum_expr["+"]
equals = sum_expr["="][0]
plusses.add_to_back(VectorizedPoint(sum_expr.get_left()))
sum_term.set_fill(RED)
last_arrow = VGroup()
for i in range(len(strips)):
width_term = width_terms[i]
width_term.set_value(widths[i])
width_term.save_state()
arrow = arrows[i]
width_term.next_to(
arrow.get_center(),
rotate_vector(UP, angles[i])
)
strips.target = strips.generate_target()
strips.target.set_opacity(0.2)
strips.target[i].set_fill(opacity=0.5)
strips.target[i].set_stroke(opacity=1)
self.play(
MoveToTarget(strips),
FadeIn(width_term),
FadeIn(arrow),
FadeOut(last_arrow),
)
self.play(
Restore(width_term),
FadeIn(plusses[i])
)
last_arrow = arrow
sum_term.set_value(sum(wt.get_value() for wt in width_terms))
self.play(
FadeOut(last_arrow),
strips.animate.set_fill(opacity=0.5).set_stroke(opacity=1),
Write(equals),
FadeIn(sum_term),
)
self.wait()
# Turn into parallel strips
np.random.seed(3)
subdivision = sorted([-1, 1, *np.random.uniform(-1, 1, 5)])
r_pairs = list(zip(subdivision, subdivision[1:]))
new_widths = [r1 - r0 for r0, r1 in r_pairs]
new_strips = VGroup(
self.get_strip(circle, r0, r1, theta=0)
for r0, r1 in r_pairs
)
new_strips.match_style(strips)
new_rects = VGroup(s.rect for s in new_strips)
self.play(
FadeOut(strips),
FadeIn(rects),
)
self.play(
# Transform(strips, new_strips),
ReplacementTransform(rects, new_rects),
*(
ChangeDecimalToValue(width_term, new_width)
for width_term, new_width in zip(width_terms, new_widths)
),
ChangeDecimalToValue(sum_term, 2.0),
run_time=2
)
self.play(FadeOut(new_rects), FadeIn(new_strips))
self.wait()
# Show sum of the area
width_sum = Tex(R"\sum_{\text{strip}} \textbf{Width}(\text{strip})")
area_sum = Tex(R"\sum_{\text{strip}} \textbf{Area}(\text{strip})")
area_sum_rhs = Tex(R"\ge \pi r^2 = \pi")
width_sum.to_corner(UR)
area_sum.to_corner(UL)
area_sum_rhs.next_to(area_sum[-1], RIGHT, MED_SMALL_BUFF)
width_brace = Brace(width_sum, DOWN)
width_annotation = width_brace.get_text("We want to\ncontrol this")
width_annotation.set_color(YELLOW)
self.play(FadeTransformPieces(sum_expr, width_sum))
self.play(GrowFromCenter(width_brace), Write(width_annotation))
self.wait()
self.play(Write(area_sum))
self.wait()
self.play(Write(area_sum_rhs))
self.wait()
# Add area and width label for strip
strip = new_strips[1]
area_label = TexText(R"Area = $0.00$")
area_dec = area_label.make_number_changeable("0.00")
area_dec.add_updater(lambda m: m.set_value(
get_norm(strip.get_area_vector()) / radius**2
))
area_label.add_updater(lambda m: m.next_to(strip, LEFT))
area_label.match_color(strip)
arrow = Tex(R"\leftrightarrow").stretch(2, 0)
arrow.match_width(strip)
arrow.always.move_to(strip)
width_label = VGroup(
Text("Width"),
Tex("=").rotate(90 * DEGREES),
DecimalNumber(1),
)
width_label.arrange(DOWN)
width_label.set_width(strip.get_width() * 0.8)
width_label[2].add_updater(lambda m: m.set_value(strip.get_width() / radius))
width_label.always.next_to(arrow, UP)
self.play(
FadeOut(new_strips[:1]),
FadeOut(new_strips[2:]),
FadeOut(radial_line),
FadeOut(radius_label),
)
self.play(Write(area_label))
self.wait()
self.play(
GrowFromCenter(arrow),
FadeIn(width_label),
)
self.wait()
# Show varying strip
r0 = subdivision[1]
delta_r = subdivision[2] - subdivision[1]
r0_tracker = ValueTracker(r0)
strip.add_updater(lambda m: m.match_points(self.get_strip(
circle,
r0_tracker.get_value(),
r0_tracker.get_value() + delta_r,
theta=0
)))
for value in [-1, 0.6, r0]:
self.play(r0_tracker.animate.set_value(value), run_time=4)
strip.clear_updaters()
self.play(
FadeOut(area_label),
FadeOut(width_label),
FadeOut(arrow),
FadeIn(new_strips[:1]),
FadeIn(new_strips[2:]),
)
# Show the dream of proportionality
width_label_group = VGroup(width_sum, width_brace, width_annotation)
circle_group = VGroup(circle, new_strips)
dream_sum = Tex(R"\sum_{\text{strip}} {k} \cdot \textbf{Width}(\text{strip})")
dream_sum[R"{k}"].set_color(YELLOW)
dream_sum.next_to(area_sum, DOWN, buff=2.0)
dream_sum.shift_onto_screen()
down_arrow = Arrow(area_sum, dream_sum, thickness=5)
arrow_words = Text("If only...")
arrow_words.next_to(down_arrow, RIGHT, SMALL_BUFF)
self.play(
circle_group.animate.shift(3 * RIGHT),
width_label_group.animate.scale(0.5, about_edge=UR),
)
self.wait()
self.play(
GrowArrow(down_arrow),
FadeIn(arrow_words, lag_ratio=0.1)
)
self.play(TransformMatchingStrings(area_sum.copy(), dream_sum))
self.wait()
def get_strip(self, *args, **kwargs):
kwargs["rect_length"] = kwargs.get("rect_length", 6.0)
return super().get_strip(*args, **kwargs)
class SphereStrips(InteractiveScene):
def construct(self):
# Axes
frame = self.frame
frame.set_height(3)
axes = ThreeDAxes((-2, 2), (-2, 2), (-2, 2))
axes.set_stroke(width=1)
plane = NumberPlane((-2, 2), (-2, 2))
plane.fade(0.5)
self.add(axes)
self.add(plane)
# Circle
circle = Circle()
circle.set_stroke(YELLOW, 3)
circle.set_fill(BLACK, 0.0)
self.add(circle)
# Sphere
sphere = ParametricSurface(
lambda u, v: [
np.sin(u) * np.cos(v),
np.sin(u) * np.sin(v),
np.cos(u)
],
u_range=(0, PI),
v_range=(0, 2 * PI)
)
sphere.set_opacity(0.5)
sphere.set_shading(0.5, 0.5, 0.5)
sphere.always_sort_to_camera(self.camera)
sphere.set_clip_plane(OUT, 1e-3)
# Show pre_strip
delta_x = 0.25
x0 = 0.5
strip = self.get_strip(x0, x0 + delta_x, 0)
pre_strip = strip.copy()
pre_strip.stretch(1e-3, 2)
pre_strip.set_z_index(1)
circle.set_clip_plane(UP, 10) # Why?
plane.set_clip_plane(UP, 10) # Why?
self.play(ShowCreation(pre_strip, run_time=2))
self.wait()
# Expand
pre_sphere = sphere.copy()
pre_sphere.stretch(0, 2)
pre_sphere.shift(1e-2 * IN)
pre_sphere.set_opacity(0)
strip.save_state()
strip.become(pre_strip)
sphere.save_state()
sphere.become(pre_sphere)
self.remove(pre_strip)
self.add(strip, sphere)
self.play(
frame.animate.reorient(-34, 59, 0),
run_time=2
)
self.wait()
self.add(pre_sphere, pre_strip)
self.play(
Restore(strip),
Restore(sphere),
run_time=3
)
self.play(
frame.animate.reorient(40, 59, 0),
run_time=7
)
self.wait()
# Note the area
brace = Brace(pre_strip, UP)
brace.add(brace.get_tex(R"d", font_size=24, buff=0.05))
brace.rotate(90 * DEGREES, RIGHT)
brace.next_to(strip, OUT, buff=0)
area_label = TexText(R"Area = $\pi d$")
area_label.to_corner(UR)
area_label.fix_in_frame()
self.play(
GrowFromCenter(brace, time_span=(1, 2)),
frame.animate.reorient(-2, 94, 0, (0.31, 0.11, 0.63), 2.35),
run_time=3,
)
self.wait()
self.play(
Write(area_label),
Transform(
brace[-1].copy(),
brace[-1].copy().scale(0.5).shift(1.5 * RIGHT + 0.25 * OUT).set_opacity(0),
remover=True
)
)
self.play(FlashAround(area_label, run_time=2))
self.wait()
# Move strip around
x0_tracker = ValueTracker(x0)
strip.add_updater(lambda m: m.become(self.get_strip(
x0_tracker.get_value(),
x0_tracker.get_value() + delta_x,
theta=0
)))
brace.add_updater(lambda m: m.next_to(strip, OUT, buff=0))
self.play(
x0_tracker.animate.set_value(-0.99),
frame.animate.reorient(-24, 82, 0, (0.4, 0.08, 0.63), 2.67),
run_time=5,
)
self.play(
x0_tracker.animate.set_value(0.5),
frame.animate.reorient(2, 76, 0, (0.4, 0.08, 0.63), 2.67),
run_time=5
)
strip.clear_updaters()
brace.clear_updaters()
self.play(FadeOut(brace), FadeOut(area_label))
# Reorient and add make full sphere
self.play(
frame.animate.reorient(29, 74, 0, ORIGIN, 3.00).set_anim_args(run_time=2),
sphere.animate.set_clip_plane(OUT, 1),
strip.animate.set_clip_plane(OUT, 1),
)
self.play(
frame.animate.reorient(7, 67, 0),
Rotate(strip, PI / 2, DOWN, about_point=ORIGIN),
run_time=2
)
self.wait()
# Add cylinder
clyinder = ParametricSurface(
lambda u, v: [np.cos(v), np.sin(v), u],
u_range=[-1, 1],
v_range=[0, TAU],
)
cylinder_mesh = SurfaceMesh(clyinder, resolution=(33, 51))
cylinder_mesh.set_stroke(WHITE, 1, 0.25)
cylinder_mesh.set_clip_plane(UP, 20)
cylinder_mesh.match_height(sphere)
self.play(ShowCreation(cylinder_mesh, lag_ratio=0.01))
self.wait()
# Project the strip
def clyinder_projection(points):
radii = np.apply_along_axis(np.linalg.norm, 1, points[:, :2])
return np.transpose([points[:, 0] / radii, points[:, 1] / radii, points[:, 2]])
def get_proj_strip(strip):
return strip.copy().apply_points_function(clyinder_projection).set_opacity(0.8)
proj_strip = get_proj_strip(strip)
proj_strip.save_state()
proj_strip.become(strip)
self.add(proj_strip, cylinder_mesh)
self.play(
frame.animate.reorient(-28, 62, 0).set_anim_args(run_time=4),
Restore(proj_strip, run_time=2),
)
self.wait()
# Vary the height of the strip
strip.add_updater(lambda m: m.become(
self.get_strip(
x0_tracker.get_value(),
x0_tracker.get_value() + delta_x,
theta=0,
).rotate(PI / 2, DOWN, about_point=ORIGIN)
))
proj_strip.add_updater(lambda m: m.match_z(strip))
sphere.set_clip_plane(UP, 20)
self.add(sphere, cylinder_mesh)
frame.add_ambient_rotation()
for value in [0.75, 0, 0.5]:
self.play(x0_tracker.animate.set_value(value), run_time=6)
frame.clear_updaters()
strip.clear_updaters()
proj_strip.clear_updaters()
# Go back to the hemisphere state
self.play(
FadeOut(cylinder_mesh),
FadeOut(proj_strip, 5 * OUT),
)
strip.set_clip_plane(OUT, 0)
sphere.set_clip_plane(OUT, 1)
self.play(
frame.animate.reorient(23, 68, 0),
sphere.animate.set_clip_plane(OUT, 0),
Rotate(strip, PI / 2, axis=UP, about_point=ORIGIN),
run_time=3
)
self.wait()
# Cover with more strips
strips = Group(
self.get_strip(
*sorted(np.random.random(2)),
theta=random.uniform(0, TAU),
color=random_bright_color(),
).shift(x * 1e-3 * OUT)
for x in range(1, 20)
)
strips.set_opacity(0.5)
self.play(
ShowCreation(strips, lag_ratio=0.9),
frame.animate.reorient(-17, 31, 0),
run_time=10
)
self.play(
frame.animate.reorient(-24, 64, 0),
run_time=8,
)
self.wait()
def get_strip(self, x0, x1, theta, color=BLUE):
strip = ParametricSurface(
lambda u, v: [
np.cos(u),
np.sin(u) * np.cos(v),
np.sin(u) * np.sin(v),
],
u_range=(math.acos(x1), math.acos(x0)),
v_range=(0, TAU),
)
strip.rotate(theta, OUT, about_point=ORIGIN)
strip.scale(1.001, about_point=ORIGIN)
strip.set_color(color)
strip.set_shading(0.5, 0.5, 0.5)
strip.set_clip_plane(OUT, 1e-3)
return strip
class MongesTheorem(InteractiveScene):
def construct(self):
# Add circles
circ1, circ2, circ3 = circles = self.get_initial_circles()
self.play(LaggedStartMap(ShowCreation, circles, lag_ratio=0.5, run_time=2))
self.wait()
# Add tangents
tangent_pairs = always_redraw(lambda: self.get_all_external_tangents(circles))
intersection_dots = always_redraw(lambda: self.get_all_intersection_dots(tangent_pairs))
dependents = Group(tangent_pairs, intersection_dots)
dependents.suspend_updating()
for tangents, dot, circle_pair in zip(tangent_pairs, intersection_dots, it.combinations(circles, 2)):
c1, c2 = (c.copy() for c in circle_pair)
self.play(*map(GrowFromCenter, tangents), run_time=1.5)
self.play(
LaggedStart(
c1.animate.scale(0, about_point=dot.get_center()),
c2.animate.scale(0, about_point=dot.get_center()),
FadeIn(dot),
lag_ratio=0.2,
)
)
self.remove(c1, c2)
self.wait()
# Manipuate the circles
dependents.resume_updating()
circles.save_state()
self.add(*dependents)
self.add(*circles)
self.wait(note="Play with circle positions. Be careful!")
# Show the line between them
monge_line = Line()
monge_line.f_always.put_start_and_end_on(
intersection_dots[0].get_center,
intersection_dots[2].get_center,
)
monge_line.always.set_length(100)
monge_line.set_stroke(WHITE, 3)
monge_line.suspend_updating()
self.play(GrowFromCenter(monge_line))
self.wait()
# Manipulate again
monge_line.resume_updating()
self.add(*circles)
self.wait(30, note="Play with circle positions. Be careful!")
self.play(Restore(circles), self.frame.animate.to_default_state(), run_time=3)
dependents.suspend_updating()
self.play(FadeOut(monge_line))
self.wait()
# Setup spheres and tangent groups
plane = NumberPlane((-8, 8), (-8, 8))
plane.background_lines.set_stroke(GREY, 1)
plane.faded_lines.set_stroke(GREY, 1, 0.25)
plane.axes.set_stroke(GREY, 1)
spheres = self.get_spheres(circles)
tangent_groups = always_redraw(lambda: self.get_tangent_groups(circles))
tangent_groups.suspend_updating()
# Show spheres
frame = self.frame
self.wait()
self.play(
frame.animate.reorient(-11, 69, 0),
FadeIn(plane),
FadeIn(spheres, lag_ratio=0.25),
run_time=4
)
self.wait()
# Reposition
self.play(self.frame.animate.reorient(-41, 72, 0), run_time=5)
# Show various external tangents
self.play(
frame.animate.reorient(-67, 76, 0),
LaggedStartMap(GrowFromCenter, tangent_groups[2], lag_ratio=0.1),
spheres[0].animate.set_opacity(0.05),
run_time=3
)
self.wait(10, note="Emphasize how it's formed")
self.play(
frame.animate.reorient(-105, 46, 0),
LaggedStartMap(GrowFromCenter, tangent_groups[1], lag_ratio=0.1),
spheres[0].animate.set_opacity(0.5),
spheres[1].animate.set_opacity(0.05),
run_time=3
)
self.wait()
self.play(
frame.animate.reorient(-175, 51, 0, (1.2, 0.92, -0.26)),
LaggedStartMap(GrowFromCenter, tangent_groups[0], lag_ratio=0.1),
spheres[1].animate.set_opacity(0.5),
spheres[2].animate.set_opacity(0.05),
run_time=3
)
self.wait()
self.play(
frame.animate.reorient(-70, 59, 0, (0.22, 0.32, -1.5), 9.17),
spheres[2].animate.set_opacity(0.5),
run_time=6,
)
self.wait()
# Show mutually tangent plane (Fudged, but it works)
xy_plane = Square3D(resolution=(100, 100)).rotate(PI)
xy_plane.set_color(BLUE_E, 0.35)
xy_plane.replace(plane)
inter_points = [dot.get_center() for dot in intersection_dots]
blue_tip = self.get_cone_tips(circles[2:], angle=84 * DEGREES)[0]
tangent_plane = self.get_plane_through_points([inter_points[2], inter_points[0], blue_tip])
plane_lines = VGroup(
tangent_groups[2][19].copy(),
tangent_groups[0][31].copy(),
tangent_groups[1][25].copy(),
)
plane_lines.set_stroke(width=4, opacity=1)
self.play(
frame.animate.reorient(-50, 74, 0, (0.22, 0.32, -1.5), 9.17),
ShowCreation(tangent_plane, time_span=(0, 2)),
run_time=6,
)
self.wait()
self.play(
frame.animate.reorient(-77, 63, 0, (0.22, 0.32, -1.5), 9.17),
FadeOut(tangent_groups),
run_time=4
)
for line in plane_lines:
self.play(ShowCreation(line, run_time=2))
self.wait()
self.add(xy_plane, tangent_plane, plane_lines)
self.play(ShowCreation(xy_plane, time_span=(0, 2)))
self.wait()
self.play(ShowCreation(monge_line, suspend_mobject_updating=True))
self.wait()
# Move circles to problem position
self.play(
FadeOut(xy_plane),
FadeOut(tangent_plane),
FadeOut(plane_lines),
self.frame.animate.to_default_state(),
run_time=2
)
dependents.resume_updating()
self.add(dependents)
self.play(
circles[1].animate.move_to(2 * LEFT),
circles[0].animate.move_to(0.2 * UP),
circles[2].animate.move_to(3 * RIGHT),
run_time=3
)
dependents.suspend_updating()
self.wait()
# Show the outside plane
angle = abs(tangent_pairs[2][0].get_angle())
partial_tangent_plane = xy_plane.copy()
pivot_point = intersection_dots[2].get_center()
partial_tangent_plane.rotate(angle, axis=DOWN, about_point=pivot_point)
partial_tangent_plane.set_height(5, stretch=True)
partial_tangent_plane.set_color(GREY_C, 0.5)
partial_tangent_plane.set_shading(0.25, 0.25, 0.25)
self.add(partial_tangent_plane)
self.play(ShowCreation(partial_tangent_plane))
self.wait()
self.play(self.frame.animate.reorient(27, 75, 0))
self.play(
Rotating(partial_tangent_plane, PI / 2, axis=RIGHT, about_point=pivot_point),
run_time=8,
rate_func=there_and_back,
)
self.wait()
self.play(
FadeOut(partial_tangent_plane),
self.frame.animate.to_default_state(),
run_time=3
)
dependents.resume_updating()
self.add(dependents)
self.play(circles[0].animate.move_to(2 * UP), run_time=3)
dependents.suspend_updating()
# Show the cones
cones = self.get_cones(circles)
def upadte_cone_positions(cones):
for cone, circle in zip(cones, circles):
cone.match_width(circle)
cone.move_to(circle, IN)
self.play(
self.frame.animate.reorient(-74, 72, 0, (-1.2, 0.14, -0.2), 8.00),
run_time=3
)
spheres.clear_updaters()
self.play(ReplacementTransform(spheres, cones, lag_ratio=0.5, run_time=2))
self.wait()
# Show the center of similarity
def get_tip_lines():
result = VGroup()
for i, j, k in [(2, 2, 2), (1, 2, 1), (0, 1, 0)]:
line = Line(intersection_dots[i].get_center(), cones[j].get_zenith())
line.match_color(tangent_pairs[k][0])
line.scale(2, about_point=line.get_start())
result.add(line)
return result
tip_lines = always_redraw(get_tip_lines)
tip_lines.suspend_updating()
self.play(ShowCreation(tip_lines[0]))
self.play(self.frame.animate.reorient(-1, 83, 0, (-1.2, 0.14, -0.2)), run_time=3)
self.wait()
cone_ghost = cones[2].copy().set_opacity(0.5)
cone_ghost.deactivate_depth_test()
self.add(cones, cone_ghost)
self.play(FadeIn(cone_ghost))
for x in range(2):
self.play(
cone_ghost.animate.scale(1e-2, about_point=intersection_dots[2].get_center()),
run_time=8,
rate_func=there_and_back
)
self.wait()
self.play(self.frame.animate.reorient(0, 7, 0, (-1.92, 0.22, 0.0)), run_time=3)
self.play(
FadeOut(cone_ghost),
self.frame.animate.reorient(-129, 75, 0, (-1.92, 0.22, 0.0)),
run_time=4
)
self.play(ShowCreation(tip_lines[1:], lag_ratio=0.5, run_time=2))
self.wait()
# Add plane
plane = always_redraw(lambda: self.get_plane_through_points([
intersection_dots[2].get_center(),
intersection_dots[0].get_center(),
cones[2].get_zenith()
]))
plane.suspend_updating()
self.play(
ShowCreation(plane),
self.frame.animate.reorient(-74, 66, 0, (-1.92, 0.22, 0.0)),
run_time=4
)
# Move the circles all about
dependents.add(tip_lines, plane)
dependents.resume_updating()
cones.add_updater(upadte_cone_positions)
self.add(cones, dependents)
self.play(circles[0].animate.move_to(0.2 * UP), run_time=3)
dependents.suspend_updating()
self.play(self.frame.animate.reorient(-173, 69, 0, (-1.36, 0.7, 1.01), 7.14), run_time=10)
self.wait(note="Reorient")
dependents.resume_updating()
self.play(
circles[0].animate.move_to(2 * UP),
self.frame.animate.reorient(-122, 54, 0, (-1.54, 0.75, 0.38), 8.65),
run_time=4
)
self.manipulate_circle_positions(circles)
dependents.suspend_updating()
def get_initial_circles(self):
centers = [[-3, 3, 0], [-6, -1.5, 0], [3, -1.5, 0]]
colors = [RED, GREEN, BLUE]
radii = [1, 2, 4]
circles = VGroup(
Circle(radius=radius).move_to(center).set_color(color)
for radius, center, color in zip(radii, centers, colors)
)
circles.scale(0.5)
circles.to_edge(RIGHT, buff=LARGE_BUFF)
return circles
def get_plane_through_points(self, points, color=GREY_B, opacity=0.5):
v1 = points[1] - points[0]
v2 = points[2] - points[0]
perp = normalize(cross(v2, v1))
vert_angle = math.acos(perp[2])
plane = Square3D(resolution=(100, 100))
plane.set_width(get_norm(v1))
plane.move_to(ORIGIN, DL)
plane.rotate(angle_of_vector(v1), about_point=ORIGIN)
plane.rotate(PI - vert_angle, axis=v1, about_point=ORIGIN)
plane.shift(points[0])
plane.scale(2, about_point=points[0])
plane.set_color(color, opacity=opacity)
return plane
def get_cones(self, circles, angle=90 * DEGREES):
cones = Group()
for circle in circles:
radius = circle.get_width() / 2
cone = Cone(radius=radius, height=radius / math.tan(angle / 2))
cone.move_to(circle, IN)
cone.set_color(circle.get_color())
cone.set_opacity(0.5)
cone.always_sort_to_camera(self.camera)
cones.add(cone)
return cones
def get_cone_tips(self, circles, angle=90 * DEGREES):
points = []
for circle in circles:
radius = circle.get_width() / 2
height = radius / math.tan(angle / 2)
point = circle.get_center() + height * OUT
points.append(point)
return points
def get_spheres(self, circles, opacity=0.5):
spheres = Group()
for circle in circles:
sphere = Sphere(radius=circle.get_radius())
sphere.set_color(circle.get_color(), opacity)
sphere.circle = circle
sphere.always_sort_to_camera(self.camera)
sphere.always.match_width(circle)
sphere.always.move_to(circle)
spheres.add(sphere)
return spheres
def get_tangent_groups(self, circles, n_lines=24):
tangent_groups = VGroup()
for circ1, circ2 in it.combinations(circles, 2):
tangent_pair = self.get_external_tangents(circ1, circ2)
point = self.get_intersection(*tangent_pair)
axis = circ2.get_center() - circ1.get_center()
group = VGroup()
for angle in np.arange(0, PI, PI / n_lines):
group.add(*tangent_pair.copy().rotate(angle, axis=axis, about_point=point))
for line in group:
line.shift(point - line.get_start())
group.set_stroke(width=1, opacity=0.5)
tangent_groups.add(group)
return tangent_groups
def get_all_intersection_dots(self, line_pairs):
return Group(
GlowDot(self.get_intersection(*pair))
for pair in line_pairs
)
def get_all_external_tangents(self, circles, **kwargs):
return VGroup(
self.get_external_tangents(circ1, circ2)
for circ1, circ2 in it.combinations(circles, 2)
)
def get_external_tangents(self, circle1, circle2, length=100, color=None):
c1 = circle1.get_center()
c2 = circle2.get_center()
r1 = circle1.get_radius()
r2 = circle2.get_radius()
if get_norm(c1 - c2) <= max(r1, r2):
return VectorizedPoint().replicate(2)
# Distance to intersection of external tangents
L1 = get_norm(c1 - c2) / (1 - r2 / r1)
intersection = c1 + L1 * normalize(c2 - c1)
theta = math.asin(r1 / L1)
line1 = Line(c1, c2)
line1.insert_n_curves(20)
line1.rotate(theta, about_point=intersection)
line1.set_length(length)
line2 = line1.copy().rotate(PI, axis=(c2 - c1), about_point=intersection)
result = VGroup(line1, line2)
if color is None:
color = interpolate_color(circle1.get_color(), circle2.get_color(), 0.5)
result.set_stroke(color, width=2)
return result
def get_intersection(self, line1, line2):
try:
return line_intersection(
line1.get_start_and_end(),
line2.get_start_and_end(),
)
except Exception:
return midpoint(line1.get_end(), line2.get_end())
def manipulate_circle_positions(self, circles):
circ1, circ2, circ3 = circles
# Example
self.play(circ2.animate.shift(LEFT), run_time=2)
self.play(circ2.animate.scale(0.75), run_time=2)
self.play(circ1.animate.scale(0.5).shift(0.2 * DOWN), run_time=2)
self.play(circ3.animate.scale(0.7).shift(0.2 * DOWN), run_time=4)
self.wait()
self.play(Restore(circles), run_time=3)
self.wait()
class GeneralCentersOfSimilarity(MongesTheorem):
def construct(self):
# Show centers of similarity
circles = self.get_initial_circles()
cos_dots = always_redraw(lambda: self.get_center_of_similarity_dots(circles))
similarity_lines = always_redraw(lambda: self.get_all_similarity_lines(circles))
theorem_line = Line().set_stroke(WHITE, 2).insert_n_curves(20)
theorem_line.add_updater(lambda m: m.put_start_and_end_on(
cos_dots[0].get_center(), cos_dots[2].get_center()
).scale(100))
labels = VGroup()
self.add(circles)
for lines, dot, pair in zip(similarity_lines, cos_dots, it.combinations(circles, 2)):
bigger = pair[0] if pair[0].get_width() > pair[1].get_width() else pair[1]
ghost = bigger.copy()
label = Text("Center of similarity", font_size=36)
label.next_to(dot, DL, SMALL_BUFF)
label.shift_onto_screen()
self.play(
ghost.animate.scale(0, about_point=dot.get_center()),
ShowCreation(lines, lag_ratio=0),
FadeIn(dot),
FadeIn(label)
)
self.remove(ghost)
self.wait()
labels.add(label)
theorem_line.update()
theorem_line.suspend_updating()
self.play(
FadeOut(labels),
GrowFromCenter(theorem_line)
)
theorem_line.resume_updating()
self.add(cos_dots)
self.add(similarity_lines)
self.add(theorem_line)
self.add(*circles)
# Play around
self.wait(20, note="Play!")
# Change shapes
pis = VGroup(
Tex(R"\pi")[0].match_style(circle).replace(circle)
for circle in circles
)
self.play(Transform(circles, pis, lag_ratio=0.5, run_time=3))
self.wait()
self.add(*circles)
# Play some more
self.wait(15, note="Play!")
# Show the cones
frame = self.frame
cones = VGroup(self.get_cone(shape) for shape in circles)
cones.set_stroke(opacity=0.75)
self.play(
LaggedStart(
(FadeIn(cone, lag_ratio=0.05)
for cone in cones),
lag_ratio=0.5,
),
frame.animate.reorient(16, 70, 0),
run_time=5
)
self.wait(5)
def get_center_of_similarity_dots(self, shapes):
return Group(
GlowDot(self.get_center_of_similarity(*pair))
for pair in it.combinations(shapes, 2)
)
def get_center_of_similarity(self, shape1, shape2):
w1 = shape1.get_width()
w2 = shape2.get_width()
c1 = shape1.get_center()
c2 = shape2.get_center()
vect = c2 - c1
dist = get_norm(vect)
# Desired ratio: (x - dist) / x = w2 / w1
# -------------> x - dist = x (w2 / w1)
# -------------> x (1 - w2 / w1) = dist
# -------------> result = c1 + x * (vect / dist)
return c1 + vect / (1.0 - w2 / w1)
def get_all_similarity_lines(self, shapes, **kwargs):
return VGroup(
self.get_similarity_lines(*pair, **kwargs)
for pair in it.combinations(shapes, 2)
)
def get_similarity_lines(self, shape1, shape2, n_lines=25):
point = self.get_center_of_similarity(shape1, shape2)
big = shape1 if shape1.get_width() > shape2.get_width() else shape2
color = interpolate_color(shape1.get_color(), shape2.get_color(), 0.5)
if big.get_num_points() == 0:
return VGroup()
result = VGroup(
Line(big.pfp(alpha), point)
for alpha in np.linspace(0, 1, n_lines)
)
result.set_stroke(color, 1, 0.5)
return result
def get_cone(self, shape):
top_z = 0.5 * shape.get_width()
return VGroup(
shape.copy().scale(a).set_z(z).set_stroke(width=1)
for a, z in zip(np.linspace(1, 0), np.linspace(0, top_z))
)
class SimilarDiagrams(MongesTheorem):
def construct(self):
# Test
circle1, circle2 = circles = VGroup(
Circle(radius=1).move_to(LEFT).set_color(GREEN),
Circle(radius=2).move_to(4 * RIGHT).set_color(BLUE),
)
lines = self.get_external_tangents(*circles)
int_point = self.get_intersection(*lines)
angle = lines[0].get_angle()
t_point1 = circle1.get_center() + rotate_vector(UP, -angle)
t_point2 = t_point1.copy()
t_point2[1] *= -1
t_points = [t_point1, t_point2]
radii = VGroup(
Line(circle1.get_center(), t_point)
for t_point in t_points
)
elbows = VGroup(
Elbow(width=0.1).rotate(PI - angle, about_point=ORIGIN).shift(t_point1),
Elbow(width=0.1).rotate(-1.5 * PI + angle, about_point=ORIGIN).shift(t_point2),
)
elbows.set_stroke(width=2)
tangents = VGroup(Line(t_point, int_point) for t_point in t_points)
tangents.set_color(TEAL)
self.add(circle1, radii, tangents, elbows)
self.wait()
self.add(radii.copy(), elbows.copy())
self.play(
TransformFromCopy(circle1, circle2),
VGroup(tangents, radii, elbows).animate.scale(2, about_point=int_point),
run_time=3
)
self.wait()
class AskAboutVolumeOfParallelpiped(InteractiveScene):
def construct(self):
# Axes and plane
frame = self.frame
axes = ThreeDAxes((-8, 8), (-4, 4), (-4, 4))
axes.set_stroke(WHITE, 2)
plane = NumberPlane()
frame.reorient(-43, 73, 0, (1.18, 0.21, 1.19), 5.96)
self.add(plane, axes)
# Tetrahedron
verts = [
(-2, 1, 1),
(1, 0, 3),
(3, 0, 0),
(2, -2, 0),
]
tetrahedron = VGroup(
Polygon(*subset)
for subset in it.combinations(verts, 3)
)
tetrahedron.set_stroke(WHITE, 1)
tetrahedron.set_fill(TEAL_E, 0.5)
tetrahedron.set_shading(0.5, 0.5, 0)
dots = DotCloud(verts, radius=0.05)
dots.make_3d()
dots.set_color(WHITE)
self.add(tetrahedron)
self.add(dots)
# Add vertex labels
labels = VGroup(
Tex(f"(x_{n}, y_{n}, z_{n})", font_size=36)
for n in range(1, 5)
)
vects = [OUT + LEFT, OUT, OUT + RIGHT, OUT + RIGHT]
for label, point, vect in zip(labels, dots.get_points(), vects):
label.rotate(89 * DEGREES, RIGHT)
label.next_to(point, vect, buff=SMALL_BUFF)
frame.reorient(-33, 84, 0, (0.34, 0.8, 1.42), 7.12)
frame.add_updater(lambda m: m.set_theta(-math.cos(7 * self.time * DEGREES) * 35 * DEGREES))
self.play(LaggedStartMap(FadeIn, labels, shift=0.5 * OUT, lag_ratio=0.5, run_time=3))
self.wait(30)
class TriangleAreaFormula(InteractiveScene):
def construct(self):
# Set up triangle
plane = NumberPlane(faded_line_ratio=1)
plane.add_coordinate_labels(font_size=16)
plane.background_lines.set_stroke(opacity=0.75)
plane.faded_lines.set_stroke(opacity=0.25)
verts = [
(1, 1, 0),
(2, -1, 0),
(3, 2, 0),
]
triangle = Polygon(*verts)
triangle.set_stroke(YELLOW, 3)
dots = Group(TrueDot(vert, radius=0.1) for vert in verts)
dots.set_color(WHITE)
self.add(plane)
self.add(triangle)
self.add(dots)
# Add labels
labels = VGroup(
Tex(Rf"(x_{n}, y_{n})", font_size=36)
for n in [1, 2, 3]
)
labels.set_backstroke(BLACK, 3)
for label, dot, vect in zip(labels, dots, [UP, DOWN, UP]):
label.next_to(dot, vect, SMALL_BUFF)
labels[0].shift(0.3 * LEFT)
self.frame.reorient(0, 0, 0, (2.02, 0.72, 0.0), 4.49),
self.play(
LaggedStartMap(FadeIn, labels, shift=0.25 * UP, lag_ratio=0.5, run_time=2),
self.frame.animate.to_default_state().set_anim_args(run_time=6),
)
self.wait()
self.play(triangle.animate.set_fill(YELLOW, 0.5))
self.wait()
# Set up 3D labels
labels_3d = VGroup(
Tex(Rf"(x_{n}, y_{n}, 1)", font_size=36)
for n in [1, 2, 3]
)
for label, dot, vect in zip(labels_3d, dots, [LEFT, UR, RIGHT]):
label.rotate(89 * DEGREES, RIGHT)
label.next_to(dot, vect + OUT, SMALL_BUFF)
label.shift(OUT)
# Move up to 3d
frame = self.frame
z_axis = NumberLine((-4, 4))
z_axis.rotate(PI / 2, DOWN)
z_axis.set_flat_stroke(False)
ghost_plane = plane.copy()
ghost_plane.fade(0.5)
ghost_plane.shift(OUT)
self.play(
frame.animate.reorient(-13, 75, 0, (-0.14, 0.7, 1.48), 9.34).set_anim_args(run_time=2),
FadeIn(z_axis),
)
self.play(
frame.animate.reorient(15, 81, 0, (-0.56, 0.95, 1.43), 9.34).set_anim_args(run_time=8),
TransformFromCopy(plane, ghost_plane),
triangle.animate.shift(OUT),
Transform(labels, labels_3d),
*(
dot.animate.shift(OUT).make_3d()
for dot in dots
),
)
self.wait()
# Show parallelpiped
cube = VCube(side_length=1)
cube.set_stroke(WHITE, 2)
cube.set_fill(WHITE, 0.1)
cube.deactivate_depth_test()
cube.move_to(ORIGIN, [-1, -1, -1])
cube.apply_matrix(np.transpose([
dot.get_center()
for dot in dots
]))
self.play(
frame.animate.reorient(-5, 63, 0, (-0.04, 0.69, 0.39), 7.73).set_anim_args(run_time=5),
Write(cube),
)
self.wait()
# Show tetrehedron
tetrahedron = VGroup(
triangle.copy(),
Polygon(ORIGIN, dots[0].get_center(), dots[1].get_center()),
Polygon(ORIGIN, dots[0].get_center(), dots[2].get_center()),
Polygon(ORIGIN, dots[1].get_center(), dots[2].get_center()),
)
tetrahedron.set_stroke(width=0)
tetrahedron.set_fill(YELLOW, 0.5)
self.play(
frame.animate.reorient(-4, 77, 0, (0.28, 0.78, 0.41), 7.73).set_anim_args(run_time=4),
Write(tetrahedron)
)
self.wait()
self.play(
frame.animate.reorient(-5, 64, 0, (0.22, 0.72, -0.04), 7.14),
run_time=8
)
self.wait()
class LogicForArea(InteractiveScene):
def construct(self):
# Test
det_tex = Tex(R"""
= \frac{1}{2}\det\left[\begin{array}{ccc}
x_1 & x_2 & x_3 \\
y_1 & y_2 & y_3 \\
1 & 1 & 1
\end{array}\right]
""")
equations = VGroup(
TexText(R"Volume(Tetra.) = $\frac{1}{3}$ Area(Tri.) $\times 1$"),
TexText(R"Volume(Tetra.) = $\frac{1}{6}$ Volume(Para.)"),
Tex(R"\Downarrow"),
TexText(R"Area(Tri.) = $\frac{1}{2}$ Volume(Para.)"),
)
for eq in [det_tex, *equations]:
eq["Tetra."].set_color(YELLOW)
eq["Tri."].set_color(YELLOW)
eq["Para."].set_color(YELLOW)
equations.arrange(DOWN, buff=LARGE_BUFF)
equations.to_corner(UL)
equations[2].scale(2)
det_tex.next_to(equations[-1]["="], DOWN, LARGE_BUFF, aligned_edge=LEFT)
self.frame.set_height(10)
self.add(det_tex)
self.add(equations)
class FourDDet(InteractiveScene):
def construct(self):
det_tex = Tex(R"""
\frac{1}{6}\det\left[\begin{array}{cccc}
x_1 & x_2 & x_3 & x_4 \\
y_1 & y_2 & y_3 & y_4 \\
z_1 & z_2 & z_3 & z_4 \\
1 & 1 & 1 & 1
\end{array}\right]
""")
group = VGroup(
Text("Volume", font_size=72),
Tex("=", font_size=72).rotate(90 * DEGREES),
det_tex
)
group.arrange(DOWN, buff=LARGE_BUFF)
self.add(group)
class RandomVectorStatistics(InteractiveScene):
def construct(self):
# Show 2d distribution
chart = self.get_random_angle_data_histogram(2, step_size=2)
label = VGroup(Integer(2, edge_to_fix=UR), Text("D"))
label.arrange(RIGHT, buff=0.05)
label.next_to(chart.get_corner(UL), DR)
label.shift(RIGHT)
self.add(label)
self.add(chart)
# Many random vectors
center = chart.get_center() + 0.5 * UP
for _ in range(0):
vects = VGroup(
Vector(
1.5 * rotate_vector(RIGHT, random.uniform(0, TAU)),
thickness=4
).set_fill(random_bright_color(), border_width=1)
for _ in range(2)
)
vects.shift(center)
angle = (vects[0].get_angle() - vects[1].get_angle()) % TAU
if angle > PI:
angle = TAU - angle
bar = chart.bars[int(angle / DEGREES) // 2].copy()
bar.set_color(YELLOW)
self.add(vects, bar)
self.wait(0.5)
self.remove(vects, bar)
# Animate an increase in charts
dim_tracker = ValueTracker(2)
def get_dim():
return int(dim_tracker.get_value())
label.add_updater(lambda m: m[0].set_value(get_dim()))
self.play(
dim_tracker.animate.set_value(1000).set_anim_args(rate_func=rush_into),
UpdateFromFunc(chart, lambda m: m.become(
self.get_random_angle_data_histogram(
get_dim(),
n_vects=50000000 // get_dim(),
# n_vects=500000 // get_dim(),
step_size=1 if get_dim() < 100 else 0.5
)
)),
run_time=20
)
def get_random_angle_data_histogram(self, dim, n_vects=1000000, step_size=1):
vects1, vects2 = all_vects = [np.random.normal(0, 1, (n_vects, dim)) for _ in range(2)]
for vects in all_vects:
norms = np.linalg.norm(vects, axis=1)
vects /= norms[:, np.newaxis]
angles = np.arccos((vects1 * vects2).sum(1)) / DEGREES
return self.get_histogram(angles, step_size=step_size)
def get_histogram(self, data, min_val=0, max_val=180, step_size=5, bar_color=BLUE_D):
bins = np.arange(min_val, max_val + 1, step_size)
bucket_counts, _ = np.histogram(data, bins=bins, range=(min_val, max_val))
bin_width = step_size / (max_val - min_val)
densities = (bucket_counts / bucket_counts.sum()) / (bin_width)
y_max = 16.0
if densities.max() > y_max:
densities *= (y_max / densities.max())**0.5
axes = Axes(
x_range=(min_val, max_val, 5),
y_range=(0, y_max, 2), # TODO
width=6, height=4
)
axes.x_axis.add_numbers(np.arange(min_val, max_val + 1, 45), font_size=24, unit_tex=R"^\circ")
x_unit = axes.x_axis.get_unit_size()
y_unit = axes.y_axis.get_unit_size()
bar_width = x_unit * step_size
bars = VGroup(
Rectangle(width=bar_width, height=density * y_unit).move_to(axes.c2p(x, 0), DL)
for x, density in zip(bins, densities)
)
bars.set_fill(bar_color, 1)
bars.set_stroke(WHITE, 0.5, 0.5)
chart = VGroup(axes, bars)
chart.axes = axes
chart.bars = bars
return chart
class ProbabilityQuestion(InteractiveScene):
N = 6
def construct(self):
# Number lines and trackers
trackers = Group(ValueTracker(np.random.uniform(-1, 1)) for x in range(self.N))
lines = VGroup(
self.get_uniform_random_indicator(tracker, n)
for n, tracker in enumerate(trackers, start=1)
)
lines.arrange(DOWN, buff=0.35)
lines.to_corner(DL)
self.add(lines)
# Add x_i distribution label
dist_label = TexText("$x_i$ uniform in [-1, 1]")
dist_label.match_x(lines).to_edge(UP)
self.add(dist_label)
# Add question label
# lhs = Tex(R"P\left(\sum_{i=0}^7 x_i^2 \le 1 \right) = ")
lhs = Tex(R"P\left(x_1^2 + x_2^2 + x_3^2 + x_4^2 + x_5^2 + x_6^2 \le 1 \right)")
rhs = Tex(R"\frac{\pi^3}{6} \cdot \frac{1}{2^6}")
eq = VGroup(lhs, Tex("=").rotate(PI / 2), rhs)
eq.arrange(DOWN)
eq.center().to_edge(RIGHT)
self.add(eq)
# Add brace
def get_sum():
return sum(t.get_value()**2 for t in trackers)
brace = Brace(lhs[R"x_1^2 + x_2^2 + x_3^2 + x_4^2 + x_5^2 + x_6^2"], UP)
brace.set_color(BLUE_D)
sum_value = DecimalNumber()
sum_value.set_color(BLUE_D)
sum_value.next_to(brace, UP)
sum_value.f_always.set_value(get_sum)
symbols = VGroup(Checkmark().set_color(GREEN), Exmark().set_color(RED))
symbols.set_height(0.5)
symbols.match_x(lhs["1"][-1]).align_to(sum_value, UP)
def update_symbols(syms):
if get_sum() < 1:
syms[0].set_opacity(1)
syms[1].set_opacity(0)
else:
syms[1].set_opacity(1)
syms[0].set_opacity(0)
symbols.add_updater(update_symbols)
self.add(brace, sum_value, symbols)
# Animate in 6D label
rect = SurroundingRectangle(rhs[R"\frac{\pi^3}{6}"])
rect.set_stroke(TEAL, 2)
label = Text("Volume of a \n 6D unit ball")
label.next_to(rect, DOWN)
label.set_color(TEAL)
self.play(ShowCreation(rect))
self.play(Write(label))
self.wait()
# Go over many random values
time_per_state = 0.2
total_time = 25
for _ in range(int(total_time / time_per_state)):
for tracker in trackers:
tracker.set_value(np.random.uniform(-1, 1))
self.wait(time_per_state)
def get_uniform_random_indicator(self, value_tracker, n):
line = NumberLine((-1, 1, 0.1), width=4, big_tick_spacing=1, tick_size=0.05)
line.set_stroke(width=2)
line.add_numbers(
np.arange(-1, 1.5, 0.5),
font_size=12,
num_decimal_places=1,
buff=0.15
)
tip = ArrowTip(angle=-90 * DEGREES)
tip.set_height(0.15)
tip.set_fill(YELLOW, 1)
tip.add_updater(lambda m: m.move_to(line.n2p(value_tracker.get_value()), DOWN))
tip.add_updater(lambda m: m.set_fill(self.value_to_color(value_tracker.get_value())))
x_label = Tex(Rf"x_{n}", font_size=30)
x_label.always.next_to(tip, UP, buff=0.05)
x_label.always.match_color(tip)
return VGroup(line, tip, x_label)
def value_to_color(self, value):
return interpolate_color_by_hsl(
GREY_B,
BLUE if value > 0 else RED,
abs(value)
)
class IntersectingCircles(InteractiveScene):
def construct(self):
# Add circles
circles = Circle(radius=2).replicate(4)
circles.set_stroke(BLUE_B, 2)
circles[3].set_stroke(YELLOW, 3)
circles.tri_intersection = ORIGIN # To change
circles.pair_intersections = np.zeros((3, 3)) # To change
vectors = [RIGHT, UL, DL]
vector_trackers = VGroup(VectorizedPoint(vect) for vect in vectors)
def update_circles(circles):
self.place_circles_by_vectors(
circles,
[vt.get_center() for vt in vector_trackers]
)
circles.add_updater(update_circles)
dots = GlowDots(circles.pair_intersections)
dots.set_color(WHITE)
dots.add_updater(lambda m: m.set_points(circles.pair_intersections))
circles[3].set_opacity(0)
self.play(LaggedStartMap(ShowCreation, circles, lag_ratio=0.7))
self.play(FadeIn(dots))
self.wait()
circles[3].set_stroke(opacity=1)
self.play(ShowCreation(circles[3]))
self.add(circles)
self.wait()
self.play(
vector_trackers[2].animate.move_to(LEFT + 0.5 * DOWN),
run_time=2
)
self.wait()
self.play(
vector_trackers[0].animate.move_to(RIGHT + 0.5 * DOWN),
run_time=2
)
self.wait()
self.play(
vector_trackers[0].animate.move_to(RIGHT),
run_time=2
)
self.wait()
self.play(circles[3].animate.set_opacity(0))
self.wait()
# Draw radial lines
centers = Dot(radius=0.05).replicate(3)
centers.set_color(WHITE)
def update_centers(centers):
for center, circle in zip(centers, circles):
center.move_to(circle)
centers.add_updater(update_centers)
radial_lines = self.get_radial_lines(circles, [vt.get_center() for vt in vector_trackers])
self.play(
LaggedStartMap(FadeOut, circles[:3].copy(), lag_ratio=0.5, scale=0),
FadeIn(centers, lag_ratio=0.5)
)
self.wait()
self.play(ShowCreation(radial_lines[:3], lag_ratio=0.75))
self.wait()
for i in range(3, 8, 2):
self.play(ShowCreation(radial_lines[i:i + 2], lag_ratio=0.5, run_time=1))
self.wait()
self.wait()
self.play(
LaggedStart(
(FadeTransform(radial_lines[i1].copy(), radial_lines[i2])
for i1, i2 in [(5, 9), (8, 10), (4, 11)]),
lag_ratio=0.75,
run_time=3
),
)
self.wait()
# Animate about
radial_lines.add_updater(lambda m: m.become(
self.get_radial_lines(circles, [vt.get_center() for vt in vector_trackers])
))
self.add(circles, centers, radial_lines)
self.play(
vector_trackers[1].animate.move_to(UP),
run_time=3
)
self.play(
vector_trackers[1].animate.move_to(UL),
run_time=3
)
circles[3].set_stroke(opacity=1)
self.play(ShowCreation(circles[3]))
self.wait()
def place_circles_by_vectors(self, circles, vectors):
radius = circles[0].get_radius()
radial_vectors = np.array([radius * normalize(vect) for vect in vectors])
for circle, radial_vector in zip(circles, radial_vectors):
circle.move_to(radial_vector)
circles[3].move_to(sum(radial_vectors)),
circles.tri_intersection = ORIGIN
circles.pair_intersections = np.array([
sum(pair) for pair in it.combinations(list(radial_vectors[:3]), 2)
])
return circles
def get_radial_lines(self, circles, vectors):
radius = circles[0].get_radius()
radial_vectors = np.array([radius * normalize(vect) for vect in vectors])
result = VGroup()
for vect in radial_vectors:
result.add(Line(ORIGIN, vect))
for v1 in radial_vectors:
for v2 in radial_vectors:
if np.all(v1 == v2):
continue
result.add(Line(v1, v1 + v2))
total_sum = sum(radial_vectors)
for vect in radial_vectors:
result.add(DashedLine(total_sum - vect, total_sum))
result.set_stroke(WHITE, 2)
result[-3:].set_stroke(RED, 2)
return result
class TriPod(MongesTheorem):
def construct(self):
# Initialize frame
frame = self.frame
self.set_floor_plane("xz")
axes = ThreeDAxes()
axes.set_stroke(width=1)
# Triangles
proj_point = 3 * UP
vects = [DOWN + 0.35 * LEFT + 0.5 * OUT, DOWN + 0.35 * RIGHT + 0.5 * OUT, DOWN + 0.5 * IN]
factors1 = [2.0, 1.0, 2.0]
factors2 = [4.5, 4.25, 4.0]
tri1, tri2 = tris = VGroup(
Polygon(*(proj_point + f * v for f, v in zip(factors, vects)))
for factors in [factors1, factors2]
)
tri1.set_stroke(BLUE, 3)
tri1.set_fill(BLUE, 0.5)
tri2.set_stroke(RED, 3)
tri2.set_fill(RED, 0.5)
self.add(tris)
# Labels
tri1_labels = VGroup(map(Tex, "ABC"))
tri2_labels = VGroup(map(Tex, ["A'", "B'", "C'"]))
tri_labels = VGroup(tri1_labels, tri2_labels)
tri_labels.scale(0.75)
for tri, labels in zip(tris, tri_labels):
center = tri.get_center()
for label, vert in zip(labels, tri.get_vertices()):
buff = normalize(vert - center)
label.move_to(vert + 0.45 * buff)
tri1_labels[2].scale(1.25).shift(0.15 * DR)
tri2_labels[2].scale(1.25).shift(0.2 * UR)
for labels in tri_labels:
self.play(LaggedStartMap(Write, labels, lag_ratio=0.5, run_time=2))
self.wait()
# Tripod legs
tripod_legs = VGroup(
Line(vert, proj_point)
for vert in tri2.get_vertices()
)
tripod_legs.set_stroke(WHITE, 1)
for line in tripod_legs:
line.scale(4)
self.play(
LaggedStartMap(GrowFromCenter, tripod_legs, lag_ratio=0.75, run_time=4),
VGroup(g[2] for g in tri_labels).animate.shift(0.1 * RIGHT).set_anim_args(time_span=(3, 4))
)
self.wait()
# Intersections
v1 = tri1.get_vertices()
v2 = tri2.get_vertices()
side_pairs = VGroup(
VGroup(
Line(v1[i], v1[j]),
Line(v2[i], v2[j]),
)
for (i, j) in [(1, 0), (0, 2), (1, 2)]
)
for pair, color in zip(side_pairs, [PINK, YELLOW, TEAL]):
pair.set_stroke(color, 2)
for line in pair:
line.scale(20, about_point=line.get_start())
side_pairs[1].set_stroke(width=(2, 20))
int_dots = Group()
for pair in side_pairs:
point = self.get_intersection(*pair)
dot = GlowDot(point)
int_dots.add(dot)
self.play(
ShowCreation(pair, lag_ratio=0),
)
self.wait()
# Add planes
planes = Group()
meshes = VGroup()
for tri in tris:
a, b, c = tri.get_vertices()
mat = np.array([
normalize(b - a),
normalize(c - a),
normalize(cross(b - a, c - a))
]).T
plane = Square3D(side_length=50)
plane.apply_matrix(mat)
plane.shift(a)
plane.set_color(GREY_C, 0.25)
plane.set_shading(1, 0.5, 0.5)
planes.add(plane)
mesh = SurfaceMesh(plane, resolution=(101, 101))
meshes.add(mesh)
meshes.add(meshes[0].copy().shift(0.03 * DR))
meshes.set_stroke(WHITE, 0.5, 0.25)
self.play(
FadeIn(planes),
FadeIn(meshes)
)
frame.clear_updaters()
frame.add_updater(lambda m, dt: m.increment_theta(math.sin(self.time / 10) * dt * 2 * DEGREES))
self.wait(10)