mirror of
https://github.com/3b1b/videos.git
synced 2025-08-31 21:58:59 +00:00
3154 lines
107 KiB
Python
3154 lines
107 KiB
Python
from __future__ import annotations
|
|
|
|
from manim_imports_ext import *
|
|
|
|
from _2024.inscribed_rect.helpers import *
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from typing import Callable
|
|
from manimlib.typing import Vect3, Vect4
|
|
|
|
|
|
class LoopScene(InteractiveScene):
|
|
def get_dot_group(
|
|
self,
|
|
vect_tracker: ValueTracker,
|
|
loop_func: Callable[[float], Vect3],
|
|
colors=None,
|
|
radius: float = 0.05,
|
|
glow_factor: float = 1.5,
|
|
):
|
|
n = len(vect_tracker.get_value())
|
|
if colors is None:
|
|
colors = [random_bright_color() for _ in range(n)]
|
|
|
|
dots = Group(
|
|
get_special_dot(color, radius=radius, glow_factor=glow_factor)
|
|
for _, color in zip(range(n), it.cycle(colors))
|
|
)
|
|
|
|
def update_dots(dots):
|
|
for dot, value in zip(dots, vect_tracker.get_value()):
|
|
dot.move_to(loop_func(value))
|
|
|
|
dots.add_updater(update_dots)
|
|
return dots
|
|
|
|
def get_movable_pair(
|
|
self,
|
|
uv_tracker: ValueTracker,
|
|
loop_func: Callable[[float], Vect3],
|
|
colors=[YELLOW, PINK],
|
|
radius: float = 0.05,
|
|
glow_factor: float = 1.5,
|
|
):
|
|
return self.get_dot_group(uv_tracker, loop_func, colors, radius, glow_factor)
|
|
|
|
def get_movable_quad(
|
|
self,
|
|
abcd_tracker: ValueTracker,
|
|
loop_func: Callable[[float], Vect3],
|
|
colors=[YELLOW, MAROON_B, PINK, RED],
|
|
radius: float = 0.05,
|
|
glow_factor: float = 1.5,
|
|
):
|
|
return self.get_dot_group(abcd_tracker, loop_func, colors, radius, glow_factor)
|
|
|
|
def get_connecting_line(self, dot_pair, stroke_color=TEAL_B, stroke_width=2):
|
|
d1, d2 = dot_pair
|
|
if stroke_color is None:
|
|
stroke_color = average_color(d1.get_color(), d2.get_color())
|
|
|
|
line = Line().set_stroke(stroke_color, stroke_width)
|
|
line.f_always.put_start_and_end_on(d1.get_center, d2.get_center)
|
|
return line
|
|
|
|
def get_midpoint_dot(self, dot_pair, color=TEAL_B):
|
|
dot = dot_pair[0].copy()
|
|
dot.f_always.move_to(dot_pair.get_center)
|
|
dot.set_color(color)
|
|
return dot
|
|
|
|
def get_dot_polygon(self, dots, stroke_color=BLUE, stroke_width=3):
|
|
polygon = Polygon(LEFT, RIGHT)
|
|
polygon.set_stroke(stroke_color, stroke_width)
|
|
polygon.add_updater(lambda m: m.set_points_as_corners(
|
|
[*(d.get_center() for d in dots), dots[0].get_center()]
|
|
))
|
|
return polygon
|
|
|
|
def get_dot_labels(self, dots, label_texs, direction=UL, buff=0):
|
|
result = VGroup()
|
|
for dot, tex in zip(dots, label_texs):
|
|
label = Tex(tex)
|
|
label.match_color(dot[0])
|
|
label.set_backstroke(BLACK, 3)
|
|
label.always.next_to(dot[0], direction, buff=buff)
|
|
result.add(label)
|
|
return result
|
|
|
|
|
|
class StateThePuzzle(LoopScene):
|
|
def construct(self):
|
|
# Show the loop
|
|
loop = get_example_loop(4)
|
|
loop.set_height(7)
|
|
loop.move_to(2 * RIGHT)
|
|
curve_words = Text("Closed\nContinuous\nCurve", alignment="LEFT", font_size=72)
|
|
curve_words.to_edge(LEFT)
|
|
|
|
self.play(
|
|
ShowCreation(loop),
|
|
Write(curve_words, time_span=(3, 5)),
|
|
run_time=9
|
|
)
|
|
|
|
# Show four points going to a square
|
|
loop.insert_n_curves(50)
|
|
loop_func = loop.quick_point_from_proportion
|
|
quad_tracker = ValueTracker([0, 0, 0, 0])
|
|
dots = self.get_movable_quad(quad_tracker, loop_func, colors=color_gradient([RED, PINK], 4), radius=0.075)
|
|
square_params = find_rectangle(loop_func, target_angle=90 * DEG)
|
|
|
|
polygon = self.get_dot_polygon(dots, stroke_color=YELLOW, stroke_width=5)
|
|
inscribed_words = TexText(R"``Inscribed\\Square''", font_size=72)
|
|
inscribed_words.to_edge(LEFT)
|
|
|
|
self.add(dots)
|
|
self.play(quad_tracker.animate.set_value(square_params), run_time=3)
|
|
polygon.update()
|
|
self.add(polygon, dots)
|
|
self.play(ShowCreation(polygon, suspend_mobject_updating=True))
|
|
self.play(
|
|
Write(inscribed_words),
|
|
FadeOut(curve_words, LEFT)
|
|
)
|
|
self.wait()
|
|
|
|
# Alternate squares
|
|
new_square_params = [
|
|
[0.519, 0.308, 0.277, 0.177],
|
|
[0.444, 0.105, 0.877, 0.650],
|
|
[0.037, 0.739, 0.468, 0.372],
|
|
]
|
|
dots.suspend_updating()
|
|
for new_params in new_square_params:
|
|
new_dots = dots.copy()
|
|
new_dots.set_opacity(0)
|
|
for dot, p in zip(new_dots, new_params):
|
|
dot.move_to(loop_func(p))
|
|
|
|
dots.set_opacity(0)
|
|
self.play(Transform(dots, new_dots), run_time=2)
|
|
dots.set_opacity(1)
|
|
self.wait()
|
|
|
|
# Ask question
|
|
title = Text("Open Question", font_size=60)
|
|
title.add(Underline(title))
|
|
title.set_color(BLUE)
|
|
question = Text("Do all closed\ncontinuous curves\nhave an inscribed\nsquare?", alignment="LEFT")
|
|
question.next_to(title, DOWN)
|
|
question.align_to(title[0], LEFT)
|
|
question_group = VGroup(title, question)
|
|
question_group.to_corner(UL, buff=MED_SMALL_BUFF)
|
|
|
|
self.play(
|
|
FadeIn(question_group, UP),
|
|
FadeOut(inscribed_words, LEFT)
|
|
)
|
|
self.wait()
|
|
|
|
# Ambiently animate to various different loops
|
|
def true_find_square(loop_func, trg_angle=90 * DEG, cost_tol=1e-2, max_tries=8):
|
|
ic = np.arange(0, 1, 0.25)
|
|
min_params = ic
|
|
min_cost = np.inf
|
|
for x in range(max_tries):
|
|
params, cost = find_rectangle(loop_func, target_angle=trg_angle, n_refinements=3, return_cost=True)
|
|
if cost < min_cost:
|
|
min_params = params
|
|
min_cost = cost
|
|
ic = np.random.random(4)
|
|
return min_params
|
|
|
|
new_loops = [
|
|
get_example_loop(1),
|
|
get_example_loop(2),
|
|
Tex(R"\pi").family_members_with_points()[0],
|
|
Tex(R"\epsilon").family_members_with_points()[0],
|
|
get_example_loop(1),
|
|
]
|
|
og_loop = loop.copy()
|
|
for new_loop in new_loops:
|
|
new_loop.insert_n_curves(50)
|
|
new_loop.match_style(loop)
|
|
new_loop.match_height(loop)
|
|
new_loop.move_to(loop)
|
|
|
|
dots.resume_updating()
|
|
self.add(dots, polygon)
|
|
for new_loop in new_loops:
|
|
self.remove(dots, polygon)
|
|
self.play(
|
|
Transform(loop, new_loop),
|
|
# UpdateFromFunc(
|
|
# quad_tracker,
|
|
# lambda m: m.set_value(true_find_square(loop_func))
|
|
# ),
|
|
run_time=1
|
|
)
|
|
self.add(dots, polygon)
|
|
for _ in range(5):
|
|
quad_tracker.set_value(find_rectangle(loop_func, np.random.random(4), target_angle=90 * DEG))
|
|
self.wait(0.5)
|
|
|
|
# Change question to rectangle
|
|
square_word = question["square"]
|
|
q_mark = question["?"]
|
|
rect_word = Text("rectangle")
|
|
rect_word.move_to(square_word, LEFT)
|
|
rect_word.set_color(BLUE)
|
|
red_line = Line(LEFT, RIGHT)
|
|
red_line.replace(square_word, 0)
|
|
red_line.set_stroke(RED, 8)
|
|
|
|
self.play(
|
|
FadeOut(title, UP),
|
|
ShowCreation(red_line)
|
|
)
|
|
self.play(
|
|
VGroup(square_word, red_line).animate.shift(0.75 * DOWN),
|
|
Write(rect_word),
|
|
q_mark.animate.next_to(rect_word, RIGHT, SMALL_BUFF, aligned_edge=UP),
|
|
)
|
|
self.wait()
|
|
|
|
# Transition dots to rectangle
|
|
rect_params = find_rectangle(loop_func, target_angle=45 * DEG)
|
|
self.play(quad_tracker.animate.set_value(rect_params), run_time=4)
|
|
self.wait()
|
|
|
|
dots.suspend_updating()
|
|
new_dots = dots.copy()
|
|
for dot, p in zip(new_dots, rect_params):
|
|
dot.move_to(loop_func(p))
|
|
self.play(
|
|
Transform(dots, new_dots),
|
|
run_time=4
|
|
)
|
|
self.wait()
|
|
dots.resume_updating()
|
|
|
|
# More ambient transitioning
|
|
for new_loop in [og_loop, *new_loops[1:3]]:
|
|
self.play(
|
|
Transform(loop, new_loop),
|
|
UpdateFromFunc(
|
|
quad_tracker,
|
|
lambda m: m.set_value(true_find_square(loop_func, 60 * DEG))
|
|
),
|
|
run_time=5
|
|
)
|
|
self.wait()
|
|
|
|
|
|
class ReframeToPairsOfPoints(LoopScene):
|
|
def construct(self):
|
|
# Add loop and dots
|
|
loop = SVGMobject("xmas_tree").family_members_with_points()[0]
|
|
loop.set_stroke(WHITE, 3)
|
|
loop.set_fill(opacity=0)
|
|
loop.insert_n_curves(100)
|
|
loop.set_height(7)
|
|
loop.to_edge(RIGHT)
|
|
loop_func = loop.quick_point_from_proportion
|
|
|
|
quad_tracker = ValueTracker(np.arange(0, 1, 0.25))
|
|
dots = self.get_movable_quad(quad_tracker, loop_func, radius=0.075)
|
|
labels = self.get_dot_labels(dots, "ABCD")
|
|
labels.set_backstroke(BLACK, 3)
|
|
rect_params = find_rectangle(loop_func, target_angle=55 * DEG)
|
|
quad_tracker.set_value(rect_params + np.random.uniform(0.2, 0.2, 4))
|
|
|
|
self.add(quad_tracker, loop, dots, labels)
|
|
|
|
# Add words
|
|
question1 = VGroup(
|
|
Text("Find four points"),
|
|
Tex("(A, B, C, D)"),
|
|
Text("That form a rectangle"),
|
|
)
|
|
question2 = VGroup(
|
|
Text("Find two pairs of points", t2s={"pairs": ITALIC}),
|
|
Tex(
|
|
R"\{\{A, C\}, \{B, D\}\}",
|
|
t2c={"A": YELLOW, "B": RED, "C": YELLOW, "D": RED},
|
|
),
|
|
Text("With the same midpoint\nand distance apart"),
|
|
)
|
|
for question in question1, question2:
|
|
question.arrange(DOWN, buff=0.35)
|
|
question.to_corner(UL)
|
|
|
|
for char, label in zip("ABCD", labels):
|
|
question1[1][char].match_style(label)
|
|
|
|
self.add(question1)
|
|
|
|
# Move to rectangle
|
|
polygon = self.get_dot_polygon(dots)
|
|
|
|
self.play(quad_tracker.animate.set_value(rect_params), run_time=8)
|
|
polygon.update()
|
|
self.play(
|
|
ShowCreation(polygon, suspend_mobject_updating=True),
|
|
loop.animate.set_stroke(width=2, opacity=0.5),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Switch question
|
|
line1 = self.get_connecting_line(dots[0::2]).set_stroke(YELLOW)
|
|
line2 = self.get_connecting_line(dots[1::2]).set_stroke(RED)
|
|
lines = VGroup(line1, line2)
|
|
lines.update().suspend_updating()
|
|
|
|
self.play(
|
|
FadeOut(polygon),
|
|
FadeOut(question1[2], DOWN),
|
|
TransformMatchingStrings(question1[0], question2[0], key_map={"four": "two pairs of"}, mismatch_animation=FadeTransformPieces),
|
|
TransformMatchingTex(question1[1], question2[1]),
|
|
dots[2].animate.set_color(YELLOW),
|
|
dots[1].animate.set_color(RED),
|
|
labels[2].animate.set_fill(YELLOW),
|
|
labels[1].animate.set_fill(RED),
|
|
)
|
|
self.wait()
|
|
self.play(LaggedStartMap(ShowCreation, lines, lag_ratio=0.5))
|
|
self.wait()
|
|
|
|
# Show the midpoint
|
|
mid_dot1 = self.get_midpoint_dot(dots[0::2])
|
|
mid_dot2 = self.get_midpoint_dot(dots[1::2])
|
|
mid_dot1.update()
|
|
mid_dot2.update()
|
|
arrow = Vector(RIGHT)
|
|
arrow.match_color(mid_dot1[0])
|
|
arrow.next_to(mid_dot1, LEFT, SMALL_BUFF)
|
|
|
|
self.play(
|
|
FadeIn(mid_dot1),
|
|
GrowArrow(arrow),
|
|
FadeIn(question2[2]["With the same midpoint"], lag_ratio=0.1)
|
|
)
|
|
self.play(
|
|
FlashAround(question2[2]["midpoint"], color=TEAL),
|
|
question2[2]["midpoint"].animate.set_color(TEAL),
|
|
)
|
|
self.wait()
|
|
|
|
# Show the distance apart
|
|
frame = self.frame
|
|
new_lines = lines.copy()
|
|
new_lines.clear_updaters()
|
|
for line in new_lines:
|
|
line.rotate(PI / 2 - line.get_angle())
|
|
new_lines.arrange(RIGHT, buff=0.5)
|
|
new_lines.next_to(loop, LEFT, buff=0.5)
|
|
|
|
self.play(
|
|
TransformFromCopy(lines, new_lines),
|
|
Write(question2[2]["and distance apart"]),
|
|
run_time=2
|
|
)
|
|
self.play(
|
|
question2[2]["distance apart"].animate.set_color(YELLOW),
|
|
FlashUnder(question2[2]["distance apart"]),
|
|
run_time=1
|
|
)
|
|
self.wait()
|
|
|
|
# Clear the loop and such
|
|
dots.clear_updaters()
|
|
dots[0].f_always.move_to(line1.get_start)
|
|
dots[2].f_always.move_to(line1.get_end)
|
|
dots[1].f_always.move_to(line2.get_start)
|
|
dots[3].f_always.move_to(line2.get_end)
|
|
|
|
self.play(
|
|
LaggedStartMap(FadeOut, Group(mid_dot1, arrow, loop, new_lines)),
|
|
question2[2].animate.set_opacity(0.25),
|
|
line1.animate.scale(0.35).rotate(45 * DEG).shift(UP),
|
|
line2.animate.scale(0.90).rotate(-30 * DEG).shift(DOWN),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Match the midpoints
|
|
for dot in mid_dot1, mid_dot2:
|
|
dot.set_color(WHITE)
|
|
dot.scale(0.5)
|
|
|
|
target_midpoint = midpoint(line1.get_center(), line2.get_center())
|
|
|
|
self.play(
|
|
*map(FadeIn, [mid_dot1, mid_dot2]),
|
|
question2[2]["With the same midpoint"].animate.set_fill(opacity=1),
|
|
)
|
|
self.play(
|
|
line1.animate.move_to(target_midpoint),
|
|
line2.animate.move_to(target_midpoint),
|
|
)
|
|
self.wait()
|
|
|
|
# Match the distance
|
|
self.play(
|
|
line1.animate.set_length(line2.get_length()),
|
|
question2[2]["and distance apart"].animate.set_opacity(1),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Show various rectangles
|
|
polygon.update()
|
|
self.play(ShowCreation(polygon, suspend_mobject_updating=True))
|
|
for line in [line2, line1, line2]:
|
|
self.play(Rotate(line, 100 * DEG), run_time=4)
|
|
self.wait()
|
|
|
|
|
|
class ShowTheSurface(LoopScene):
|
|
def construct(self):
|
|
# Axes and plane
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
frame.set_height(6)
|
|
|
|
# Curve
|
|
loop = get_example_loop()
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.play(ShowCreation(loop, run_time=2))
|
|
|
|
# Pair of points
|
|
uv_tracker = ValueTracker([0, 0.5])
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.075)
|
|
connecting_line = self.get_connecting_line(dots)
|
|
midpoint_dot = self.get_midpoint_dot(dots)
|
|
|
|
self.add(uv_tracker)
|
|
self.add(dots)
|
|
self.add(connecting_line)
|
|
|
|
self.play(
|
|
UpdateFromFunc(uv_tracker, lambda m: m.set_value(np.random.random(2))),
|
|
)
|
|
self.add(dots, connecting_line)
|
|
self.play(uv_tracker.animate.set_value([0.8, 0.5]), run_time=3)
|
|
|
|
# Add coordinates
|
|
coords = Tex("(x, y)", font_size=36)
|
|
coords.set_backstroke(BLACK, 3)
|
|
coords.set_fill(WHITE, 1)
|
|
|
|
midpoint_dot.update()
|
|
coords.always.next_to(midpoint_dot, UR, buff=-0.1)
|
|
|
|
self.play(
|
|
Write(coords, suspend_mobject_updating=True),
|
|
FadeIn(midpoint_dot, scale=0.5)
|
|
)
|
|
self.wait()
|
|
self.play(Write(plane, lag_ratio=0.01, run_time=2, stroke_width=1))
|
|
self.wait()
|
|
|
|
# Show the distance
|
|
brace = Brace(Line(LEFT, RIGHT).set_width(connecting_line.get_length()), DOWN, buff=SMALL_BUFF)
|
|
brace.rotate(connecting_line.get_angle(), about_point=ORIGIN)
|
|
brace.shift(connecting_line.get_center())
|
|
brace.set_color(GREY_B)
|
|
d_label = Tex("d", font_size=36)
|
|
d_label.move_to(brace.get_center() + 0.4 * normalize(brace.get_center() - midpoint_dot.get_center()))
|
|
|
|
self.play(GrowFromCenter(brace), Write(d_label))
|
|
|
|
# 3d coords into the corner
|
|
coords_3d = Tex("(x, y, d)", font_size=36)
|
|
coords_3d.next_to(self.frame.get_corner(UR), DL, MED_SMALL_BUFF)
|
|
|
|
self.play(
|
|
TransformFromCopy(coords[:4], coords_3d[:4]),
|
|
TransformFromCopy(d_label, coords_3d[4:6]),
|
|
TransformFromCopy(coords[4:], coords_3d[6:]),
|
|
run_time=2
|
|
)
|
|
|
|
# Into 3d
|
|
z_line = self.get_z_line(dots)
|
|
top_dot = self.get_top_dot(z_line)
|
|
top_dot.update()
|
|
top_dot_coords = coords_3d.copy()
|
|
top_dot_coords.unfix_from_frame()
|
|
top_dot_coords.rotate(90 * DEG, RIGHT)
|
|
top_dot_coords.scale(0.75)
|
|
top_dot_coords.next_to(top_dot, OUT + RIGHT, buff=-0.05)
|
|
|
|
self.play(
|
|
frame.animate.reorient(5, 79, 0, (0.4, 0.01, 1.41), 5.07),
|
|
FadeIn(axes),
|
|
ReplacementTransform(coords_3d, top_dot_coords),
|
|
TransformFromCopy(midpoint_dot, top_dot, suspend_mobject_updating=True),
|
|
run_time=3
|
|
)
|
|
self.play(
|
|
frame.animate.reorient(-21, 84, 0, (0.4, 0.01, 1.41), 5.07),
|
|
run_time=5
|
|
)
|
|
self.play(
|
|
frame.animate.reorient(0, 18, 0, (0.41, -0.13, 1.34), 4.76),
|
|
run_time=2
|
|
)
|
|
self.play(FlashAround(coords, run_time=2, time_width=1.5))
|
|
self.play(
|
|
TransformFromCopy(connecting_line, z_line, suspend_mobject_updating=True, time_span=(4, 6)),
|
|
frame.animate.reorient(-38, 88, 0, (1.19, -0.11, 1.48), 3.94),
|
|
run_time=6,
|
|
)
|
|
self.play(frame.animate.reorient(-4, 88, 0, (1.19, -0.11, 1.48), 3.94), run_time=5)
|
|
self.wait()
|
|
|
|
# Show another pair of points
|
|
uv_tracker2 = ValueTracker([0.2, 0.4])
|
|
dots2 = self.get_movable_pair(uv_tracker2, loop_func, colors=[RED, MAROON_B])
|
|
connecting_line2 = self.get_connecting_line(dots2)
|
|
z_line2 = self.get_z_line(dots2)
|
|
top_dot2 = self.get_top_dot(z_line2)
|
|
|
|
dot_group1 = Group(dots, connecting_line, midpoint_dot, z_line, top_dot)
|
|
dot_group2 = Group(dots2, connecting_line2, z_line2, top_dot2)
|
|
|
|
self.play(
|
|
frame.animate.reorient(12, 78, 0, (-0.48, 0.08, 1.15), 5.27),
|
|
LaggedStartMap(FadeIn, dot_group2),
|
|
LaggedStartMap(FadeOut, Group(d_label, brace, coords, top_dot_coords)),
|
|
run_time=4
|
|
)
|
|
self.play(uv_tracker2.animate.set_value([0.1, 0.2]), run_time=8)
|
|
self.wait()
|
|
nudge = 0.01
|
|
for _ in range(3):
|
|
self.play(
|
|
uv_tracker.animate.increment_value(np.random.uniform(-nudge, nudge, 2)),
|
|
uv_tracker2.animate.increment_value(np.random.uniform(-nudge, nudge, 2)),
|
|
run_time=2,
|
|
rate_func=lambda t: wiggle(t, 7),
|
|
)
|
|
self.wait()
|
|
|
|
# Show pair collision
|
|
# ic = np.random.random(4)
|
|
# print(list(ic.round(2)))
|
|
rect_params = find_rectangle(
|
|
loop_func,
|
|
initial_condition=[0.54, 0.59, 0.73, 0.31],
|
|
n_refinements=5,
|
|
target_angle=64 * DEG
|
|
)
|
|
self.play(
|
|
frame.animate.reorient(-18, 67, 0, (0.11, 0.07, 0.75), 4.63),
|
|
uv_tracker.animate.set_value(rect_params[0::2]),
|
|
uv_tracker2.animate.set_value(rect_params[1::2]),
|
|
run_time=12
|
|
)
|
|
self.wait()
|
|
|
|
# Show the rectangle
|
|
rect_points = [loop_func(x) for x in rect_params]
|
|
rect = Polygon(*rect_points)
|
|
rect.set_stroke(YELLOW, 5)
|
|
z_group = Group(z_line, z_line2, top_dot, top_dot2)
|
|
self.play(
|
|
FadeOut(z_group),
|
|
FadeOut(axes),
|
|
frame.animate.reorient(0, 0, 0, ORIGIN, 4.75),
|
|
run_time=4
|
|
)
|
|
self.play(
|
|
ShowCreation(rect),
|
|
loop.animate.set_stroke(width=2)
|
|
)
|
|
self.wait(2)
|
|
self.play(
|
|
FadeOut(rect),
|
|
FadeIn(z_group),
|
|
FadeIn(axes),
|
|
frame.animate.reorient(22, 85, 0, (-0.33, 0.45, 1.52), 6.78),
|
|
run_time=3
|
|
)
|
|
|
|
# Set them both in motion
|
|
self.set_uv_tracker_in_motion(uv_tracker, velocity=(-0.05, 0.1))
|
|
self.set_uv_tracker_in_motion(uv_tracker2, velocity=(-0.025, 0.07))
|
|
frame.add_ambient_rotation()
|
|
|
|
for dot in [top_dot, top_dot2]:
|
|
tail = TracingTail(dot, time_traced=5, stroke_color=BLUE, stroke_width=(0, 5))
|
|
traced_path = TracedPath(dot.get_center, stroke_color=BLUE, stroke_width=1)
|
|
dot.paths = VGroup(traced_path, tail)
|
|
self.add(dot.paths)
|
|
self.wait(30)
|
|
|
|
# Surface
|
|
surface, mesh, surface_func = self.get_surface_info(loop_func, surface_resolution=(301, 301))
|
|
|
|
top_dot.paths.clear_updaters()
|
|
top_dot2.paths.clear_updaters()
|
|
self.play(
|
|
FadeIn(mesh),
|
|
FadeIn(surface),
|
|
FadeOut(top_dot.paths),
|
|
FadeOut(top_dot2.paths),
|
|
frame.animate.reorient(-29, 81, 0, (0.14, 0.39, 2.1), 6.47).set_anim_args(run_time=8),
|
|
)
|
|
self.wait(15)
|
|
|
|
# Remove dot groups
|
|
self.play(
|
|
FadeOut(dot_group1),
|
|
FadeOut(dot_group2),
|
|
)
|
|
uv_tracker.clear_updaters()
|
|
uv_tracker2.clear_updaters()
|
|
self.wait()
|
|
|
|
# Show surface cross sections
|
|
z_tracker = ValueTracker(surface.get_z(OUT))
|
|
top_mesh = mesh.copy()
|
|
top_mesh.set_stroke(width=0.5, opacity=0.1)
|
|
|
|
cross_plane = Square3D()
|
|
cross_plane.set_color(WHITE, 0.1)
|
|
cross_plane.replace(plane)
|
|
cross_plane.f_always.set_z(z_tracker.get_value)
|
|
|
|
surface.f_always.set_clip_plane(lambda: IN, z_tracker.get_value)
|
|
top_mesh.f_always.set_clip_plane(lambda: OUT, lambda: -z_tracker.get_value())
|
|
|
|
self.play(
|
|
surface.animate.set_opacity(1),
|
|
mesh.animate.set_stroke(width=0, opacity=0),
|
|
)
|
|
self.add(top_mesh)
|
|
self.play(FadeIn(cross_plane))
|
|
self.play(
|
|
z_tracker.animate.set_value(0.25),
|
|
surface.animate.set_color(BLUE_E, 1),
|
|
run_time=8
|
|
)
|
|
self.wait(3)
|
|
|
|
# Add dots and show the intersection point
|
|
target_z = surface_func(*rect_params[0::2])[2]
|
|
uv_tracker.set_value(rect_params[0::2] + np.random.random(2) * 0.2)
|
|
uv_tracker2.set_value(rect_params[1::2] + np.random.random(2) * -0.2)
|
|
|
|
self.play(
|
|
FadeIn(dot_group1),
|
|
FadeIn(dot_group2),
|
|
surface.animate.set_opacity(0.75)
|
|
)
|
|
frame.clear_updaters()
|
|
self.play(
|
|
uv_tracker.animate.set_value(rect_params[0::2]),
|
|
uv_tracker2.animate.set_value(rect_params[1::2]),
|
|
FadeOut(z_line2, time_span=(2.5, 3)),
|
|
FadeOut(top_dot2, time_span=(2.5, 3)),
|
|
frame.animate.reorient(-3, 49, 0, (0.76, 0.62, 0.49), 6.46),
|
|
run_time=3
|
|
)
|
|
|
|
# Show the rectangle again
|
|
abcd_tracker = ValueTracker(rect_params)
|
|
uv_tracker.f_always.set_value(lambda: abcd_tracker.get_value()[0::2])
|
|
uv_tracker2.f_always.set_value(lambda: abcd_tracker.get_value()[1::2])
|
|
|
|
rect = Rectangle()
|
|
rect.f_always.set_points_as_corners(lambda: list(map(loop_func, abcd_tracker.get_value())))
|
|
rect.always.close_path()
|
|
|
|
self.add(abcd_tracker)
|
|
self.play(
|
|
ShowCreation(rect, suspend_mobject_updating=True),
|
|
)
|
|
self.wait()
|
|
|
|
# Raise the cross section up to point
|
|
self.play(
|
|
z_tracker.animate.set_value(target_z),
|
|
frame.animate.reorient(-3, 47, 0, (0.27, 0.98, 0.85), 3.80),
|
|
top_mesh.animate.set_stroke(opacity=0.01),
|
|
run_time=7,
|
|
)
|
|
# self.wait(15) # Comment on the intersection
|
|
self.wait()
|
|
|
|
# Animate changing rectangle
|
|
traced_path = TracingTail(top_dot, stroke_color=WHITE, time_traced=5)
|
|
|
|
self.add(traced_path)
|
|
self.play(
|
|
frame.animate.reorient(-3, 48, 0, (0.15, 0.76, 0.6), 5.26),
|
|
run_time=4,
|
|
)
|
|
z_tracker.f_always.set_value(top_dot.get_z)
|
|
self.animate_to_rectangle_with_angle(abcd_tracker, loop_func, 80 * DEGREES, n_samples=10, param_range_per_step=0.02)
|
|
z_tracker.clear_updaters()
|
|
self.play(frame.animate.reorient(0, 44, 0, (0.02, 0.63, 0.47), 7.67), run_time=5)
|
|
self.remove(traced_path)
|
|
|
|
# Multiple self intersection points
|
|
for _ in range(5):
|
|
new_rect_params = find_rectangle(
|
|
loop_func,
|
|
initial_condition=np.random.random(4),
|
|
target_angle=np.random.uniform(30 * DEG, 90 * DEG),
|
|
)
|
|
new_z = get_dist(*map(loop_func, new_rect_params[::2]))
|
|
self.play(
|
|
abcd_tracker.animate.set_value(new_rect_params),
|
|
z_tracker.animate.set_value(new_z),
|
|
)
|
|
self.wait()
|
|
|
|
# Raise the ceiling in full
|
|
self.play(
|
|
frame.animate.reorient(-1, 48, 0, (0.29, 1.22, 1.26), 8.99),
|
|
z_tracker.animate.set_value(surface.get_z(OUT)),
|
|
FadeOut(rect),
|
|
FadeOut(dot_group2),
|
|
run_time=10,
|
|
)
|
|
self.wait()
|
|
self.remove(abcd_tracker)
|
|
uv_tracker.clear_updaters()
|
|
self.play(
|
|
z_tracker.animate.set_value(0.25),
|
|
top_mesh.animate.set_stroke(opacity=0.1),
|
|
frame.animate.reorient(-7, 78, 0, (0.31, 1.16, 0.94), 7.21),
|
|
run_time=6
|
|
)
|
|
|
|
# Point out what happens near the edge
|
|
glow_tracker = ValueTracker(dots[0][0].get_glow_factor())
|
|
radius_tracker = ValueTracker(dots[0][0].get_radius())
|
|
for dot in [*dots, top_dot]:
|
|
for part in dot:
|
|
part.f_always.set_glow_factor(glow_tracker.get_value)
|
|
part.f_always.set_radius(radius_tracker.get_value)
|
|
v = uv_tracker.get_value()[1]
|
|
self.play(
|
|
uv_tracker.animate.set_value([v + 0.01, v]),
|
|
glow_tracker.animate.set_value(0.25),
|
|
radius_tracker.animate.set_value(0.025),
|
|
frame.animate.reorient(-7, 78, 0, (0.1, 1.02, 0.18), 3.80),
|
|
FadeOut(midpoint_dot),
|
|
run_time=5
|
|
)
|
|
self.play(uv_tracker.animate.set_value([0.26, 0.25]), run_time=6)
|
|
self.wait()
|
|
self.play(
|
|
uv_tracker.animate.set_value([0.25, 0.25]),
|
|
frame.animate.reorient(-7, 78, 0, (-0.15, 1.0, -0.07), 2.80),
|
|
z_tracker.animate.set_value(0.01),
|
|
run_time=7
|
|
)
|
|
self.play(frame.animate.reorient(2, 70, 0, (0.05, 1.06, 0.05), 4.29), run_time=10)
|
|
self.wait()
|
|
|
|
def get_axes_and_plane(
|
|
self,
|
|
x_range=(-3, 3),
|
|
y_range=(-3, 3),
|
|
z_range=(0, 5),
|
|
depth=4,
|
|
):
|
|
axes = ThreeDAxes(x_range, y_range, z_range)
|
|
axes.set_depth(depth, stretch=True, about_edge=IN)
|
|
axes.set_stroke(GREY_B, 1)
|
|
|
|
plane = NumberPlane(x_range, y_range)
|
|
plane.background_lines.set_stroke(BLUE, 1, 0.75)
|
|
plane.faded_lines.set_stroke(BLUE, 0.5, 0.25)
|
|
plane.axes.match_style(axes)
|
|
plane.set_z_index(-1)
|
|
return axes, plane
|
|
|
|
def get_z_line(self, dot_pair, stroke_color=TEAL_B, stroke_width=2):
|
|
z_line = Line(IN, OUT).set_stroke(stroke_color, stroke_width)
|
|
|
|
def update_z_line(z_line):
|
|
point1 = dot_pair[0].get_center()
|
|
point2 = dot_pair[1].get_center()
|
|
midpoint = mid(point1, point2)
|
|
top = midpoint + get_norm(point1 - point2) * OUT
|
|
z_line.put_start_and_end_on(midpoint, top)
|
|
|
|
z_line.add_updater(update_z_line)
|
|
z_line.update()
|
|
return z_line
|
|
|
|
def get_top_dot(self, z_line, color=BLUE, radius=0.05, glow_factor=1.0):
|
|
top_dot = Group(
|
|
TrueDot(radius=radius).make_3d(),
|
|
GlowDot(radius=radius * 2, glow_factor=glow_factor)
|
|
)
|
|
top_dot.set_color(color)
|
|
top_dot.f_always.move_to(z_line.get_end)
|
|
return top_dot
|
|
|
|
def set_uv_tracker_in_motion(self, uv_tracker, velocity=(-0.05, 0.1)):
|
|
velocity = np.array(velocity)
|
|
|
|
def update_uv_tracker(uv_tracker, dt):
|
|
new_value = uv_tracker.get_value() + dt * velocity
|
|
uv_tracker.set_value(new_value % 1)
|
|
|
|
uv_tracker.add_updater(update_uv_tracker)
|
|
return uv_tracker
|
|
|
|
def animate_to_rectangle_with_angle(
|
|
self, abcd_tracker, loop_func, target_angle,
|
|
n_samples=5,
|
|
run_time=5,
|
|
param_range_per_step=0.1,
|
|
n_samples_per_range=10,
|
|
n_refinements=3,
|
|
):
|
|
# Find the sample points
|
|
points = list(map(loop_func, abcd_tracker.get_value()))
|
|
curr_angle = abs(angle_between_vectors(points[2] - points[0], points[3] - points[1]))
|
|
if curr_angle > PI / 2:
|
|
curr_angle = PI - curr_angle
|
|
|
|
rectangle_range = [abcd_tracker.get_value()]
|
|
for angle in np.linspace(curr_angle, target_angle, n_samples + 1)[1:]:
|
|
rectangle_range.append(find_rectangle(
|
|
loop_func=loop_func,
|
|
initial_condition=rectangle_range[-1],
|
|
target_angle=angle,
|
|
initial_param_range=param_range_per_step,
|
|
n_samples_per_range=n_samples_per_range,
|
|
n_refinements=n_refinements,
|
|
))
|
|
rectangle_range = np.array(rectangle_range)
|
|
|
|
self.play(
|
|
UpdateFromAlphaFunc(abcd_tracker, lambda m, a: m.set_value(smooth_index(rectangle_range, a))),
|
|
run_time=run_time
|
|
)
|
|
|
|
def get_surface_info(
|
|
self,
|
|
loop_func: Callable[[float], Vect3],
|
|
surface_color=BLUE,
|
|
surface_opacity=0.25,
|
|
surface_resolution=(101, 101),
|
|
mesh_color=WHITE,
|
|
mesh_stroke_width=0.5,
|
|
mesh_stroke_opacity=0.1,
|
|
mesh_resolution=(101, 101),
|
|
):
|
|
surface_func = get_surface_func(loop_func)
|
|
|
|
surface = ParametricSurface(
|
|
get_half_parametric_func(surface_func),
|
|
resolution=surface_resolution,
|
|
)
|
|
surface.set_color(surface_color, surface_opacity)
|
|
surface.always_sort_to_camera(self.camera)
|
|
|
|
full_surface = ParametricSurface(surface_func)
|
|
mesh = SurfaceMesh(full_surface, resolution=mesh_resolution, normal_nudge=0)
|
|
mesh.set_stroke(WHITE, mesh_stroke_width, mesh_stroke_opacity)
|
|
mesh.deactivate_depth_test()
|
|
|
|
return surface, mesh, surface_func
|
|
|
|
|
|
class ChangeTheSurface(ShowTheSurface):
|
|
def construct(self):
|
|
# Axes and plane
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
frame.reorient(-45, 83, 0, (0.08, 0.63, 2.3), 8.55)
|
|
frame.add_ambient_rotation()
|
|
self.add(axes, plane)
|
|
|
|
# Show loops
|
|
example_loops = VGroup(
|
|
get_example_loop(1),
|
|
get_example_loop(2),
|
|
# get_example_loop(3),
|
|
SVGMobject("gingerbread_outline")[0]
|
|
)
|
|
for loop in example_loops:
|
|
loop.set_height(5)
|
|
loop.set_stroke(WHITE, 4).set_fill(opacity=0)
|
|
loop = example_loops[0].copy()
|
|
|
|
surface = self.get_surface(loop)
|
|
|
|
def update_surface(surface):
|
|
surface.become(self.get_surface(loop))
|
|
surface.always_sort_to_camera(self.camera)
|
|
|
|
self.add(loop)
|
|
self.add(surface)
|
|
|
|
for next_loop in example_loops[1:]:
|
|
self.play(
|
|
Transform(loop, next_loop),
|
|
UpdateFromFunc(surface, update_surface),
|
|
run_time=8
|
|
)
|
|
self.wait(2)
|
|
|
|
# Circle and ellipse
|
|
circle = Circle(radius=2)
|
|
circle.set_stroke(WHITE, 4)
|
|
|
|
self.play(
|
|
Transform(loop, circle),
|
|
UpdateFromFunc(surface, update_surface),
|
|
run_time=5
|
|
)
|
|
loop.become(circle)
|
|
|
|
surface.always_sort_to_camera(self.camera)
|
|
|
|
# Show all the recangles
|
|
x_tracker = ValueTracker(0.125)
|
|
get_x = x_tracker.get_value
|
|
loop_func = loop.pfp
|
|
|
|
uv_tracker1 = ValueTracker([0, 0])
|
|
uv_tracker2 = ValueTracker([0, 0])
|
|
uv_tracker1.add_updater(lambda m: m.set_value([get_x(), get_x() + 0.5]))
|
|
uv_tracker2.add_updater(lambda m: m.set_value([0.5 - get_x(), 1.0 - get_x()]))
|
|
|
|
dots1 = self.get_movable_pair(uv_tracker1, loop_func)
|
|
dots2 = self.get_movable_pair(uv_tracker2, loop_func, colors=[RED, MAROON_B])
|
|
line1 = self.get_connecting_line(dots1)
|
|
line2 = self.get_connecting_line(dots2)
|
|
z_line = self.get_z_line(dots1)
|
|
top_dot = self.get_top_dot(z_line)
|
|
rect = Rectangle()
|
|
rect.set_stroke(YELLOW, 3)
|
|
rect.f_always.set_points_as_corners(lambda: list(map(loop_func, [
|
|
get_x(), 0.5 - get_x(), get_x() + 0.5, 1.0 - get_x(), get_x()
|
|
])))
|
|
|
|
rect_group = Group(dots1, dots2, line1, line2, z_line, top_dot, rect)
|
|
|
|
self.add(uv_tracker1, uv_tracker2)
|
|
self.play(
|
|
FadeIn(rect_group),
|
|
frame.animate.reorient(-14, 31, 0, (-0.08, -0.56, 1.19), 8.04),
|
|
run_time=3
|
|
)
|
|
for value in [0.24, 0.01, 0.125]:
|
|
self.play(x_tracker.animate.set_value(value), run_time=6)
|
|
self.wait()
|
|
|
|
# Squish into an ellipse
|
|
ellipse = circle.copy().stretch(0.5, 1)
|
|
self.play(
|
|
frame.animate.reorient(-11, 67, 0, (-0.08, -0.56, 1.19), 8.04),
|
|
run_time=2
|
|
)
|
|
self.play(
|
|
Transform(loop, ellipse),
|
|
UpdateFromFunc(surface, update_surface),
|
|
run_time=5
|
|
)
|
|
self.play(frame.animate.reorient(156, 77, 0, (-0.08, -0.56, 1.19), 8.04), run_time=10)
|
|
self.wait(4)
|
|
|
|
# Move the coordinates again
|
|
self.play(frame.animate.reorient(174, 34, 0, (-0.08, -0.56, 1.19), 8.04), run_time=3)
|
|
for value in [0.24, 0.01, 0.125]:
|
|
self.play(x_tracker.animate.set_value(value), run_time=6)
|
|
|
|
def get_surface(self, loop, surface_resolution=(301, 301), color=BLUE, opacity=0.5):
|
|
# return Square3D().set_z(10)
|
|
surface_func = get_surface_func(loop.quick_point_from_proportion)
|
|
surface = ParametricSurface(
|
|
get_half_parametric_func(surface_func),
|
|
resolution=surface_resolution,
|
|
)
|
|
surface.sort_faces_back_to_front(self.camera.get_location())
|
|
surface.set_color(color, opacity)
|
|
return surface
|
|
|
|
|
|
class ParameterizeTheLoop(InteractiveScene):
|
|
def construct(self):
|
|
# Set up the loop
|
|
loop = get_example_loop(width=5)
|
|
loop.insert_n_curves(20)
|
|
loop.to_edge(LEFT, buff=LARGE_BUFF)
|
|
|
|
x_tracker = ValueTracker()
|
|
get_x = x_tracker.get_value
|
|
loop_x_group = self.get_loop_coord_group(loop, get_x)
|
|
|
|
self.add(loop)
|
|
self.add(loop_x_group)
|
|
|
|
# Set up the unit interval
|
|
interval = UnitInterval(width=6)
|
|
interval.to_edge(RIGHT)
|
|
interval.add_numbers()
|
|
|
|
x_tip = ArrowTip(angle=-90 * DEG)
|
|
x_tip.set_height(0.2)
|
|
x_tip.set_color(YELLOW)
|
|
x_tip.f_always.move_to(lambda: interval.n2p(get_x()), lambda: DOWN)
|
|
int_x_label = DecimalNumber(font_size=24)
|
|
int_x_label.set_color(YELLOW)
|
|
int_x_label.always.next_to(x_tip, UP, buff=0.15)
|
|
int_x_label.f_always.set_value(get_x)
|
|
|
|
int_x_group = VGroup(x_tip, int_x_label)
|
|
self.add(interval)
|
|
self.add(int_x_group)
|
|
|
|
# Animate changing x
|
|
self.play(x_tracker.animate.set_value(1), run_time=12, rate_func=there_and_back)
|
|
x_tracker.set_value(0)
|
|
|
|
# Snip the loop
|
|
snipped_loop = loop.copy()
|
|
sl_points = np.array(loop.get_points()) # Snipped loop points
|
|
sl_points[0] += 0.25 * LEFT
|
|
sl_points[-1] += 0.25 * UP
|
|
snipped_loop.set_points(sl_points)
|
|
|
|
scissors = SVGMobject("scissors")
|
|
scissors.set_color(GREY_B)
|
|
scissors.rotate(45 * DEG)
|
|
scissors.set_height(0.75)
|
|
scissors_shift = np.array([0.7, -0.5, 0])
|
|
scissors.move_to(loop.get_start() + scissors_shift)
|
|
|
|
self.play(
|
|
FadeOut(loop_x_group),
|
|
FadeOut(int_x_group),
|
|
FadeIn(scissors)
|
|
)
|
|
moving_loop = loop.copy()
|
|
loop.set_stroke(opacity=0.25)
|
|
self.play(
|
|
Transform(moving_loop, snipped_loop, time_span=(0.5, 1.5)),
|
|
scissors.animate.shift(-2 * scissors_shift),
|
|
run_time=2
|
|
)
|
|
self.play(FadeOut(scissors))
|
|
|
|
# Map it onto the unit interval
|
|
line = Line(interval.n2p(0), interval.n2p(1))
|
|
line.match_style(moving_loop)
|
|
self.play(Transform(moving_loop, line, run_time=3, path_arc=-30 * DEG))
|
|
self.wait()
|
|
|
|
# Show coordinate moving around
|
|
self.play(
|
|
FadeIn(loop_x_group),
|
|
FadeIn(int_x_group),
|
|
loop.animate.set_stroke(opacity=1),
|
|
FadeOut(moving_loop),
|
|
)
|
|
self.play(x_tracker.animate.set_value(1), run_time=5)
|
|
self.wait()
|
|
for value in [0, 1, 0]:
|
|
x_tracker.set_value(value)
|
|
self.wait()
|
|
|
|
# Glue 0 to 1
|
|
circular_interval = Circle(radius=TAU / interval.get_length())
|
|
circular_interval.rotate(PI / 2)
|
|
circular_interval.match_style(interval)
|
|
circle_ticks = VGroup(
|
|
Line(1.1 * point, 0.9 * point)
|
|
for a in np.linspace(0, 1, 11)
|
|
for point in [circular_interval.pfp(a)]
|
|
)
|
|
circle_numbers = interval.numbers.copy()
|
|
for tick, number in zip(circle_ticks, circle_numbers):
|
|
number.move_to(1.3 * tick.get_center())
|
|
circle_numbers[-1].move_to(0.7 * circle_ticks[-1].get_center())
|
|
|
|
circular_interval.add(circle_ticks, circle_numbers)
|
|
circular_interval.move_to(interval)
|
|
|
|
interval.save_state()
|
|
self.play(
|
|
FadeOut(int_x_group),
|
|
Transform(interval, circular_interval, run_time=3),
|
|
)
|
|
self.play(FlashAround(VectorizedPoint(interval.get_start()), run_time=2))
|
|
self.wait()
|
|
self.play(Restore(interval, run_time=3))
|
|
|
|
# Add a second point
|
|
y_tracker = ValueTracker(0)
|
|
get_y = y_tracker.get_value
|
|
loop_y_group = self.get_loop_coord_group(loop, get_y, color=PINK, label_direction=UR)
|
|
loop_y_group.update()
|
|
|
|
self.add(loop_y_group)
|
|
self.play(
|
|
# FadeIn(loop_y_group, time_span=(0, 1)),
|
|
x_tracker.animate.set_value(0.15),
|
|
y_tracker.animate.set_value(0.25),
|
|
run_time=2,
|
|
)
|
|
|
|
# Add a second axis
|
|
x_axis = interval
|
|
y_axis = UnitInterval()
|
|
y_axis.set_width(interval.get_length())
|
|
y_axis.rotate(90 * DEG)
|
|
y_axis.add_numbers(direction=LEFT)
|
|
|
|
y_axis.move_to(x_axis.n2p(0))
|
|
y_axis.shift(0.25 * LEFT)
|
|
|
|
int_y_group = int_x_group.copy()
|
|
int_y_group.clear_updaters()
|
|
int_y_group.set_color(PINK)
|
|
y_tip, y_dec = int_y_group
|
|
y_tip.rotate(-90 * DEG)
|
|
y_tip.f_always.move_to(lambda: y_axis.n2p(get_y()), lambda: LEFT)
|
|
y_dec.f_always.set_value(get_y)
|
|
y_dec.always.next_to(y_tip, RIGHT, SMALL_BUFF)
|
|
|
|
int_y_group.update()
|
|
int_y_group.suspend_updating()
|
|
self.play(
|
|
FadeIn(y_axis),
|
|
VFadeIn(int_x_group),
|
|
x_axis.animate.shift(y_axis.n2p(0) - x_axis.n2p(0)),
|
|
FadeTransformPieces(loop_y_group.copy(), int_y_group),
|
|
run_time=2
|
|
)
|
|
int_y_group.resume_updating()
|
|
self.wait()
|
|
self.play(y_tracker.animate.set_value(0.84), run_time=3)
|
|
self.play(x_tracker.animate.set_value(0.75), run_time=3)
|
|
self.play(y_tracker.animate.set_value(0.65), run_time=3)
|
|
|
|
# Show in the unit square
|
|
axes = Axes((0, 1), (0, 1), width=x_axis.get_length(), height=y_axis.get_length())
|
|
axes.shift(x_axis.n2p(0) - axes.c2p(0, 0))
|
|
int_y_group.clear_updaters()
|
|
int_x_group.clear_updaters()
|
|
x_tip, x_dec = int_x_group
|
|
|
|
square = Square()
|
|
square.set_z_index(-1)
|
|
square.set_stroke(GREY, 1)
|
|
square.set_fill(GREY_E, 0.5)
|
|
square.set_width(x_axis.get_length())
|
|
square.move_to(x_axis.n2p(0), DL)
|
|
|
|
square_point = get_special_dot(color=BLUE)
|
|
square_point.f_always.move_to(lambda: axes.c2p(get_x(), get_y()))
|
|
|
|
v_line = Line(DOWN, UP).set_stroke(WHITE, 1)
|
|
h_line = Line(LEFT, RIGHT).set_stroke(WHITE, 1)
|
|
v_line.add_updater(lambda m: m.put_start_and_end_on(
|
|
axes.c2p(get_x(), 0), axes.c2p(get_x(), get_y())
|
|
))
|
|
h_line.add_updater(lambda m: m.put_start_and_end_on(
|
|
axes.c2p(0, get_y()), axes.c2p(get_x(), get_y())
|
|
))
|
|
coord_lines = VGroup(v_line, h_line)
|
|
|
|
coord_label = Tex(R"(0.00, 0.00)", font_size=24)
|
|
coord_standins = coord_label.make_number_changeable("0.00", replace_all=True)
|
|
coord_label.always.next_to(square_point, UR, buff=-0.1)
|
|
coord_standins.set_opacity(0)
|
|
|
|
coord_lines.update()
|
|
coord_lines.suspend_updating()
|
|
self.play(
|
|
FadeIn(square),
|
|
ShowCreation(v_line),
|
|
ShowCreation(h_line),
|
|
y_tip.animate.flip(UP, about_edge=LEFT),
|
|
x_tip.animate.flip(RIGHT, about_edge=DOWN),
|
|
x_dec.animate.move_to(coord_label[1]),
|
|
y_dec.animate.move_to(coord_label[3]),
|
|
FadeIn(coord_label),
|
|
FadeIn(square_point, scale=0.5),
|
|
)
|
|
coord_lines.resume_updating()
|
|
|
|
x_tip.f_always.match_x(lambda: x_axis.n2p(get_x()))
|
|
y_tip.f_always.match_y(lambda: y_axis.n2p(get_y()))
|
|
x_dec.f_always.set_value(get_x)
|
|
y_dec.f_always.set_value(get_y)
|
|
|
|
coord_label.replace_submobject(1, x_dec)
|
|
coord_label.replace_submobject(3, y_dec)
|
|
|
|
# Move coordinates
|
|
xy_tracker = ValueTracker(np.array([get_x(), get_y()]))
|
|
x_tracker.f_always.set_value(lambda: xy_tracker.get_value()[0])
|
|
y_tracker.f_always.set_value(lambda: xy_tracker.get_value()[1])
|
|
self.add(x_tracker, y_tracker)
|
|
self.play(xy_tracker.animate.set_value([0.50, get_y()]), run_time=4)
|
|
self.play(xy_tracker.animate.set_value([get_x(), 0.20]), run_time=4)
|
|
self.play(xy_tracker.animate.set_value([0.05, get_y()]), run_time=4)
|
|
np.random.seed(0)
|
|
for _ in range(3):
|
|
self.play(xy_tracker.animate.set_value(np.random.random(2)), run_time=4)
|
|
|
|
# Highlight the x=0 and x=1 lines
|
|
x_line_color = BLUE
|
|
frame = self.frame
|
|
left_edge = Line(DOWN, UP)
|
|
left_edge.set_stroke(x_line_color, 5)
|
|
left_edge.match_height(square)
|
|
left_edge.move_to(square, LEFT)
|
|
right_edge = left_edge.copy()
|
|
right_edge.move_to(square, RIGHT)
|
|
|
|
left_tips = ArrowTip(angle=90 * DEG).get_grid(3, 1, buff=1.0)
|
|
left_tips.move_to(left_edge)
|
|
left_tips.set_color(x_line_color)
|
|
right_tips = left_tips.copy()
|
|
right_tips.move_to(right_edge)
|
|
|
|
self.play(xy_tracker.animate.set_value([0, 0]), run_time=2)
|
|
self.play(
|
|
frame.animate.set_height(9),
|
|
ShowCreation(left_edge),
|
|
xy_tracker.animate.set_value([0, 1]),
|
|
run_time=12
|
|
)
|
|
xy_tracker.set_value([1, 0])
|
|
self.play(
|
|
ShowCreation(right_edge),
|
|
xy_tracker.animate.set_value([1, 1]),
|
|
run_time=8
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
Write(left_tips),
|
|
Write(right_tips),
|
|
)
|
|
|
|
v_arrows = VGroup(VGroup(left_edge, left_tips), VGroup(right_edge, right_tips))
|
|
|
|
# Highlight y=0 and y=1 lines
|
|
y_line_color = GREEN_SCREEN
|
|
bottom_edge = Line(LEFT, RIGHT)
|
|
bottom_edge.set_stroke(y_line_color, 5)
|
|
bottom_edge.match_width(square)
|
|
bottom_edge.move_to(square, DOWN)
|
|
top_edge = bottom_edge.copy()
|
|
top_edge.move_to(square, UP)
|
|
|
|
bottom_tips = ArrowTip().get_grid(1, 3, buff=1.0)
|
|
bottom_tips.move_to(bottom_edge)
|
|
bottom_tips.set_color(y_line_color)
|
|
top_tips = bottom_tips.copy()
|
|
top_tips.move_to(top_edge)
|
|
|
|
xy_tracker.set_value([0, 0])
|
|
self.play(
|
|
xy_tracker.animate.set_value([1, 0]),
|
|
ShowCreation(bottom_edge),
|
|
Write(bottom_tips, time_span=(2, 4)),
|
|
run_time=4
|
|
)
|
|
xy_tracker.set_value([0, 1])
|
|
self.play(
|
|
xy_tracker.animate.set_value([1, 1]),
|
|
ShowCreation(top_edge),
|
|
Write(top_tips, time_span=(2, 4)),
|
|
run_time=4
|
|
)
|
|
|
|
h_arrows = VGroup(VGroup(bottom_edge, bottom_tips), VGroup(top_edge, top_tips))
|
|
|
|
# Fold into a torus
|
|
def half_torus_func(u, v):
|
|
return torus_func(u, 0.5 * v)
|
|
|
|
surfaces = Group(
|
|
TexturedSurface(ParametricSurface(func), "TorusTexture")
|
|
for func in [square_func, tube_func, half_torus_func, torus_func]
|
|
)
|
|
for surface in surfaces:
|
|
surface.set_shading(0.25, 0.25, 0)
|
|
surface.set_opacity(0.75)
|
|
|
|
target_z = 5
|
|
square3d, tube, half_torus, torus = surfaces
|
|
square3d.replace(square)
|
|
|
|
surface = square3d.copy()
|
|
surface.replace(square)
|
|
surface.set_z(target_z)
|
|
|
|
tube.set_width(surface.get_width() / PI)
|
|
tube.match_height(surface, stretch=True)
|
|
tube.move_to(surface, IN)
|
|
|
|
torus.match_depth(tube)
|
|
torus.move_to(tube)
|
|
half_torus.match_width(torus)
|
|
half_torus.move_to(torus, UP)
|
|
|
|
cover_rect = SurroundingRectangle(Group(loop, loop_y_group))
|
|
cover_rect.set_fill(BLACK, 1).set_stroke(width=0)
|
|
|
|
self.add(surface)
|
|
self.play(
|
|
FadeIn(surface, shift=target_z * OUT),
|
|
FadeIn(cover_rect),
|
|
frame.animate.reorient(-13, 61, 0, (1.52, 1.67, 1.97), 15.41),
|
|
run_time=3,
|
|
)
|
|
self.play(Transform(surface, tube), run_time=3)
|
|
self.wait()
|
|
self.play(Transform(surface, half_torus, path_arc=PI / 2), run_time=3)
|
|
self.play(Transform(surface, torus, path_arc=PI / 2), run_time=3)
|
|
self.wait()
|
|
self.remove(surface)
|
|
self.add(torus)
|
|
|
|
# Put torus in position above the loop
|
|
torus_point = TrueDot(color=BLUE)
|
|
torus_point.f_always.move_to(lambda: torus.uv_to_point(get_x(), get_y()))
|
|
torus_point.apply_depth_test()
|
|
|
|
self.play(
|
|
FadeOut(cover_rect),
|
|
loop.animate.set_height(6).next_to(y_axis, LEFT, buff=1.5),
|
|
frame.animate.reorient(0, 0, 0, (0.44, 1.84, 0.0), 13.21),
|
|
torus.animate.set_height(7).rotate(50 * DEG, LEFT).move_to(6 * UP),
|
|
torus.animate.set_height(7).rotate(50 * DEG, LEFT).move_to(6 * UP).match_x(square),
|
|
v_arrows.animate.set_opacity(0.25),
|
|
h_arrows.animate.set_opacity(0.25),
|
|
coord_label.animate.scale(1.5),
|
|
run_time=3
|
|
)
|
|
|
|
torus_mesh = SurfaceMesh(torus, resolution=(21, 21))
|
|
torus_mesh.set_stroke(WHITE, 0.5, 0.5)
|
|
self.add(torus_point, torus_mesh, torus)
|
|
self.play(
|
|
Write(torus_mesh, lag_ratio=0.01, stroke_width=0.5, run_time=1),
|
|
FadeIn(torus_point),
|
|
)
|
|
|
|
target_xys = [
|
|
[0.13, 0.25],
|
|
[0.13, 0.65],
|
|
[0.13, 0.35],
|
|
[0.97, 0.35],
|
|
[0.10, 0.35],
|
|
]
|
|
for xy in target_xys:
|
|
self.play(xy_tracker.animate.set_value(xy), run_time=4)
|
|
|
|
# Wiggle the points
|
|
for _ in range(3):
|
|
self.play(
|
|
xy_tracker.animate.increment_value(0.02 * np.random.uniform(-1, 1, 2)),
|
|
run_time=2,
|
|
rate_func=lambda t: wiggle(t, 7)
|
|
)
|
|
self.wait()
|
|
|
|
# Fade back to square
|
|
self.play(
|
|
frame.animate.reorient(0, 0, 0, (-0.57, 0.46, 0.0), 10),
|
|
FadeOut(torus, UP),
|
|
FadeOut(torus_mesh, UP),
|
|
FadeOut(torus_point, UP),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Show (x, y) -> (y, x) pairs
|
|
coord_ghosts = Group()
|
|
double_arrows = VGroup()
|
|
|
|
def get_coord_ghost():
|
|
result = Group(square_point, coord_label).copy()
|
|
result.clear_updaters()
|
|
result.fade(0.25)
|
|
coord_ghosts.add(result)
|
|
return result
|
|
|
|
def get_double_arrow():
|
|
point1 = axes.c2p(get_x(), get_y())
|
|
point2 = axes.c2p(get_y(), get_x())
|
|
vect = normalize(point2 - point1)
|
|
result = VGroup(
|
|
Arrow(point1, point2).shift(0.1 * vect),
|
|
Arrow(point2, point1).shift(-0.1 * vect),
|
|
)
|
|
result.set_stroke(GREY_C)
|
|
double_arrows.add(result)
|
|
return result
|
|
|
|
def show_reflection():
|
|
x_dot = loop_x_group[0]
|
|
y_dot = loop_y_group[0]
|
|
loop_x_group.suspend_updating()
|
|
loop_y_group.suspend_updating()
|
|
|
|
self.add(get_coord_ghost())
|
|
self.play(
|
|
GrowFromPoint(get_double_arrow(), square_point.get_center()),
|
|
xy_tracker.animate.set_value([get_y(), get_x()]),
|
|
Swap(x_dot, y_dot),
|
|
run_time=1
|
|
)
|
|
self.play(Swap(x_dot, y_dot))
|
|
self.wait()
|
|
self.add(get_coord_ghost())
|
|
|
|
loop_x_group.resume_updating().update()
|
|
loop_y_group.resume_updating().update()
|
|
self.add(loop_x_group, loop_y_group)
|
|
loop_x_group.update()
|
|
loop_y_group.update()
|
|
|
|
for xy in [[0.1, 0.9], [0.8, 0.95]]:
|
|
show_reflection()
|
|
self.play(xy_tracker.animate.set_value(xy))
|
|
show_reflection()
|
|
|
|
# Show fold line
|
|
fold_line = Line(axes.c2p(0, 0), axes.c2p(1, 1))
|
|
fold_line.set_stroke(Color("red"), 2)
|
|
|
|
self.play(
|
|
ShowCreation(fold_line),
|
|
*map(FadeOut, [v_line, h_line, coord_label, square_point, x_tip, y_tip]),
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
FadeOut(coord_ghosts),
|
|
FadeOut(double_arrows),
|
|
v_arrows.animate.set_opacity(1),
|
|
h_arrows.animate.set_opacity(1),
|
|
)
|
|
|
|
# Fold the square
|
|
ul_triangle = Polygon(DL, UL, UR)
|
|
dr_triangle = Polygon(DL, DR, UR)
|
|
for triangle in [ul_triangle, dr_triangle]:
|
|
triangle.replace(square)
|
|
triangle.match_style(square)
|
|
triangle.set_z_index(-1)
|
|
triangle.set_shading(0.25, 0, 0)
|
|
self.add(triangle)
|
|
self.remove(square)
|
|
|
|
self.play(
|
|
Rotate(ul_triangle, PI, about_point=square.get_center(), axis=UR),
|
|
Rotate(v_arrows[0], PI, about_point=square.get_center(), axis=UR),
|
|
Rotate(h_arrows[1], PI, about_point=square.get_center(), axis=UR),
|
|
run_time=2
|
|
)
|
|
self.remove(v_arrows)
|
|
self.play(h_arrows.animate.set_color(PURPLE))
|
|
|
|
folded_square = Group(dr_triangle, h_arrows, fold_line).copy()
|
|
|
|
# Note the diagonal line again
|
|
self.play(
|
|
FadeIn(square_point),
|
|
FadeIn(coord_label),
|
|
)
|
|
self.play(xy_tracker.animate.set_value([0.9, 0.9]), run_time=2)
|
|
self.play(xy_tracker.animate.set_value([0.1, 0.1]), run_time=8)
|
|
self.wait()
|
|
|
|
# Comment on the tricky points (Probably edit out the actual transitions)
|
|
self.play(xy_tracker.animate.set_value([0.1, 0]), run_time=2)
|
|
self.play(FlashAround(coord_label))
|
|
self.wait()
|
|
self.play(xy_tracker.animate.set_value([1, 0]), run_time=2)
|
|
self.play(xy_tracker.animate.set_value([1, 0.1]))
|
|
self.play(FlashAround(coord_label))
|
|
self.wait()
|
|
self.play(xy_tracker.animate.set_value([0.9, 0]))
|
|
self.play(FlashAround(coord_label))
|
|
self.wait()
|
|
self.play(xy_tracker.animate.set_value([1, 0]))
|
|
self.play(xy_tracker.animate.set_value([1, 0.9]), run_time=2)
|
|
self.play(FlashAround(coord_label))
|
|
|
|
# Fade out axes and such
|
|
self.play(
|
|
LaggedStartMap(FadeOut, Group(
|
|
loop_x_group[1], loop_y_group[1], coord_label, square_point,
|
|
x_axis, y_axis,
|
|
))
|
|
)
|
|
self.wait()
|
|
|
|
# Show the new cut
|
|
cut_line = Line(square.get_center(), square.get_corner(DR))
|
|
cut_line.set_color(YELLOW)
|
|
cut_tips = bottom_tips.copy().set_color(YELLOW)
|
|
cut_tips.rotate(-45 * DEGREES)
|
|
cut_tips.move_to(cut_line)
|
|
cut_arrow1 = VGroup(cut_line, cut_tips)
|
|
cut_arrow2 = cut_arrow1.copy()
|
|
|
|
d_tri = Polygon(LEFT, UP, RIGHT)
|
|
d_tri.match_width(square).move_to(square, DOWN)
|
|
r_tri = Polygon(DOWN, LEFT, UP)
|
|
r_tri.match_height(square).move_to(square, RIGHT)
|
|
for tri in d_tri, r_tri:
|
|
tri.set_fill(GREY_D, 0.75)
|
|
tri.set_stroke(width=0)
|
|
|
|
fold_line1, fold_line2 = fold_line.replicate(2)
|
|
fold_line1.put_start_and_end_on(square.get_corner(DL), square.get_center())
|
|
fold_line2.put_start_and_end_on(square.get_center(), square.get_corner(UR))
|
|
|
|
piece1 = VGroup(d_tri, h_arrows[0], fold_line1).copy()
|
|
piece2 = VGroup(r_tri, h_arrows[1], fold_line2).copy()
|
|
pieces = VGroup(piece1, piece2)
|
|
to_remove = VGroup(h_arrows, fold_line)
|
|
old_tris = VGroup(ul_triangle, dr_triangle)
|
|
|
|
self.add(pieces, old_tris, fold_line)
|
|
self.play(
|
|
ShowCreation(cut_line),
|
|
Write(cut_tips, time_span=(0.5, 1.5)),
|
|
FadeIn(pieces),
|
|
FadeOut(old_tris),
|
|
run_time=1.5,
|
|
)
|
|
self.remove(to_remove)
|
|
piece1.add(cut_arrow1)
|
|
piece2.add(cut_arrow2)
|
|
self.add(pieces)
|
|
self.play(VGroup(piece1, piece2).animate.space_out_submobjects(1.5).move_to(square))
|
|
self.wait()
|
|
|
|
# Rearrange pieces
|
|
pieces.target = pieces.generate_target()
|
|
pieces.target[0].rotate(90 * DEGREES)
|
|
pieces.target[1].flip()
|
|
pieces.target.arrange(RIGHT, buff=0.5)
|
|
pieces.target.move_to(square)
|
|
|
|
self.play(MoveToTarget(pieces), run_time=2)
|
|
self.wait()
|
|
self.play(
|
|
piece1.animate.shift(square.get_center() - piece1[0].get_right()),
|
|
piece2.animate.shift(square.get_center() - piece2[0].get_left()),
|
|
)
|
|
self.play(
|
|
piece1[1].animate.set_opacity(0),
|
|
piece2[1].animate.set_opacity(0),
|
|
)
|
|
self.wait()
|
|
|
|
# Fold into a Mobius strip
|
|
custom_squish = bezier([0, 0.05, 0.95, 1])
|
|
|
|
def smoothed_mobius_func(u, v):
|
|
return mobius_strip_func(u, custom_squish(v))
|
|
|
|
def get_partial_strip(upper_theta=1.0):
|
|
result = ParametricSurface(lambda u, v: mobius_strip_func(
|
|
u, custom_squish(v) * upper_theta / TAU
|
|
))
|
|
result.scale(2, about_point=ORIGIN)
|
|
result.shift((0, 4, 4))
|
|
return result
|
|
|
|
surfaces = Group(
|
|
TexturedSurface(plain_surface, "MobiusStripTexture")
|
|
for plain_surface in [
|
|
ParametricSurface(square_func),
|
|
get_partial_strip(2.0),
|
|
]
|
|
)
|
|
for surface in surfaces:
|
|
surface.set_shading(0.25, 0.25, 0)
|
|
surface.set_opacity(0.75)
|
|
|
|
target_z = 4
|
|
square3d, partial_strip = surfaces
|
|
square3d.rotate(45 * DEG)
|
|
square3d.replace(pieces)
|
|
square3d.set_z(target_z)
|
|
surface = square3d.copy()
|
|
|
|
cover_rect.surround(loop, buff=0.2)
|
|
|
|
self.play(
|
|
FadeIn(cover_rect),
|
|
FadeIn(surface, shift=target_z * OUT),
|
|
frame.animate.reorient(2, 51, 0, (-0.35, 3.04, 0.42), 15.36),
|
|
run_time=3
|
|
)
|
|
self.play(
|
|
Transform(surface, partial_strip, run_time=2),
|
|
)
|
|
self.play(
|
|
UpdateFromAlphaFunc(surface, lambda m, a: m.set_points(
|
|
get_partial_strip(interpolate(2, TAU, smooth(a))).get_points()
|
|
)),
|
|
run_time=5
|
|
)
|
|
self.play(
|
|
frame.animate.reorient(0, 42, 0, (-0.11, 2.48, 0.87), 13.94),
|
|
Rotate(surface, PI, axis=RIGHT),
|
|
run_time=8
|
|
)
|
|
mobius_strip = surface
|
|
|
|
# Reintroduce coordiante plane
|
|
self.play(
|
|
frame.animate.reorient(0, 0, 0, (-1.02, 3.21, 0.0), 14.55),
|
|
mobius_strip.animate.rotate(40 * DEG, LEFT).move_to(7.5 * UP),
|
|
loop.animate.scale(1.25, about_edge=RIGHT),
|
|
FadeOut(pieces),
|
|
FadeIn(x_axis),
|
|
FadeIn(y_axis),
|
|
FadeIn(folded_square),
|
|
FadeIn(square_point),
|
|
FadeIn(coord_label),
|
|
FadeIn(loop_x_group[1]),
|
|
FadeIn(loop_y_group[1]),
|
|
)
|
|
|
|
# Show a point on the mobius strip
|
|
strip_dot = TrueDot(color=BLUE)
|
|
|
|
def update_strip_dot(dot):
|
|
u, v = torus_uv_to_mobius_uv(get_x(), get_y())
|
|
strip_dot.move_to(mobius_strip.uv_to_point(u, v))
|
|
|
|
strip_dot.add_updater(update_strip_dot)
|
|
|
|
self.play(FadeIn(strip_dot))
|
|
|
|
xy_values = [
|
|
[0.5, 0.25],
|
|
[0.9, 0.1],
|
|
[0.8, 0.7],
|
|
[0.53, 0.12],
|
|
[0.0, 0.0],
|
|
]
|
|
for xy in xy_values:
|
|
self.play(xy_tracker.animate.set_value(xy), run_time=3)
|
|
|
|
self.play(xy_tracker.animate.set_value([0.99, 0.99]), run_time=8)
|
|
self.play(xy_tracker.animate.set_value([0, 0]), run_time=8)
|
|
|
|
# Map from torus to strip
|
|
torus_group = Group(torus_mesh, torus)
|
|
torus_group.next_to(square, UP, buff=2)
|
|
|
|
torus_fold_line = ParametricCurve(lambda t: torus.uv_to_point(t, t), t_range=(0, 1, 0.01))
|
|
torus_fold_line.set_stroke(RED, 1, 1)
|
|
|
|
self.play(
|
|
mobius_strip.animate.shift(7 * LEFT),
|
|
GrowFromCenter(torus_group),
|
|
FadeIn(torus_point),
|
|
)
|
|
self.play(
|
|
ShowCreation(torus_fold_line),
|
|
xy_tracker.animate.set_value([0.99, 0.99]),
|
|
run_time=5
|
|
)
|
|
self.wait()
|
|
|
|
# Animate torus squish
|
|
squished_torus = TexturedSurface(
|
|
ParametricSurface(lambda u, v: mobius_strip_func(*torus_uv_to_mobius_uv(u, v))),
|
|
"TorusTexture",
|
|
)
|
|
squished_torus.replace(torus)
|
|
squished_torus.rotate(40 * DEG, LEFT)
|
|
squished_torus.set_opacity(0)
|
|
|
|
squished_torus_mesh = SurfaceMesh(squished_torus, resolution=(21, 21))
|
|
squished_torus_mesh.match_style(torus_mesh)
|
|
squished_torus_mesh.set_stroke(opacity=0.35)
|
|
squished_torus_mesh.make_jagged()
|
|
|
|
new_fold_line = ParametricCurve(lambda t: squished_torus.uv_to_point(t, t), t_range=(0, 0.99, 0.01))
|
|
|
|
self.play(
|
|
Transform(torus, squished_torus),
|
|
Transform(torus_mesh, squished_torus_mesh),
|
|
torus_fold_line.animate.set_points(new_fold_line.get_points()),
|
|
run_time=5,
|
|
)
|
|
self.wait()
|
|
|
|
def get_loop_coord_group(self, loop, get_x, color=YELLOW, font_size=36, dot_to_num_buff=0.075, label_direction=UL):
|
|
loop_dot = get_special_dot(color=color)
|
|
loop_dot.f_always.move_to(lambda: loop.pfp(get_x()))
|
|
|
|
loop_x_label = DecimalNumber(font_size=font_size)
|
|
loop_x_label.match_color(loop_dot[1])
|
|
loop_x_label.set_backstroke(BLACK, 3)
|
|
loop_x_label.always.next_to(loop_dot[0], label_direction, buff=dot_to_num_buff)
|
|
loop_x_label.f_always.set_value(get_x)
|
|
|
|
return Group(loop_dot, loop_x_label)
|
|
|
|
|
|
class DiscussOrderOfPoints(LoopScene):
|
|
def construct(self):
|
|
# Add loops
|
|
loop = get_example_loop(2)
|
|
loop.set_height(6)
|
|
loop.to_edge(DOWN, buff=0.25)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
# Dots
|
|
uv_tracker = ValueTracker([0.8, 0.4])
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.1)
|
|
line = self.get_connecting_line(dots)
|
|
mid_dot = self.get_midpoint_dot(dots)
|
|
mid_dot.update()
|
|
|
|
A_label = Tex("A")
|
|
B_label = Tex("B")
|
|
labels = VGroup(A_label, B_label)
|
|
for dot, label in zip(dots, labels):
|
|
label.next_to(dot, UL, buff=-0.1)
|
|
|
|
self.add(dots)
|
|
self.add(labels)
|
|
|
|
dots.clear_updaters()
|
|
|
|
# Add question
|
|
question = TexText(R"Is $(A, B)$ distinct from $(B, A)$?", font_size=60)
|
|
question.to_edge(UP)
|
|
self.play(Write(question))
|
|
self.wait()
|
|
|
|
# Swap points
|
|
for _ in range(2):
|
|
self.play(
|
|
Swap(*dots),
|
|
Swap(*labels),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Show the same midpoint
|
|
midpoint_word = Text("Same midpoint", font_size=36)
|
|
midpoint_word.next_to(mid_dot, LEFT, buff=0)
|
|
midpoint_word.set_color(TEAL)
|
|
|
|
dist_word = Text("Same distance", font_size=36)
|
|
dist_word.set_color(TEAL)
|
|
dist_word.next_to(ORIGIN, DOWN, SMALL_BUFF)
|
|
dist_word.rotate(line.get_angle(), about_point=ORIGIN)
|
|
dist_word.shift(mid_dot.get_center())
|
|
|
|
self.play(
|
|
GrowFromCenter(line, suspend_mobject_updating=True),
|
|
FadeIn(mid_dot),
|
|
FadeIn(midpoint_word, lag_ratio=0.1),
|
|
)
|
|
self.play(Swap(*dots))
|
|
self.play(
|
|
TransformMatchingStrings(
|
|
midpoint_word, dist_word,
|
|
key_map={"midpoint": "distance"},
|
|
run_time=1
|
|
)
|
|
)
|
|
self.play(Swap(*dots))
|
|
self.play(FadeOut(dist_word))
|
|
self.wait()
|
|
|
|
# Answer
|
|
answer = Text("It shouldn't be!")
|
|
answer.next_to(question, DOWN)
|
|
answer.to_edge(RIGHT)
|
|
answer.set_color(RED)
|
|
|
|
self.play(FadeIn(answer, lag_ratio=0.1))
|
|
self.wait()
|
|
|
|
# Show trivial rectangle
|
|
frame = self.frame
|
|
angle = 60 * DEG
|
|
question.fix_in_frame()
|
|
answer.fix_in_frame()
|
|
pair_group1 = Group(dots, line)
|
|
pair_group1.clear_updaters()
|
|
pair_group2 = pair_group1.copy()
|
|
dots2 = pair_group2[0]
|
|
dots2[0].set_color(PINK)
|
|
dots2[1].set_color(YELLOW)
|
|
pair_group2.rotate(angle, about_point=mid_dot.get_center())
|
|
|
|
rect = self.get_dot_polygon(
|
|
list(it.chain(*zip(dots, dots2)))
|
|
)
|
|
rect.update()
|
|
|
|
self.play(
|
|
FadeOut(loop),
|
|
FadeIn(pair_group2),
|
|
VFadeIn(rect),
|
|
frame.animate.move_to(mid_dot.get_center() + 0.5 * UP).set_height(6)
|
|
)
|
|
self.play(
|
|
Rotate(pair_group2, -angle, about_point=mid_dot.get_center()),
|
|
run_time=5
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
Rotate(pair_group2, -angle, about_point=mid_dot.get_center()),
|
|
run_time=5
|
|
)
|
|
|
|
|
|
class MapTheStripOntoTheSurface(ShowTheSurface):
|
|
def construct(self):
|
|
# Setup loop and surface
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
self.add(axes, plane)
|
|
|
|
loop = SVGMobject("gingerbread_outline")[0]
|
|
loop.insert_n_curves(50)
|
|
loop.set_height(5)
|
|
loop.set_stroke(WHITE, 3)
|
|
loop.set_fill(opacity=0)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
surface, mesh, surface_func = self.get_surface_info(loop_func)
|
|
surface.set_opacity(0.1)
|
|
|
|
# Setup a pair of points wandering the surface for a bit
|
|
uv_tracker = ValueTracker([0, 0.5])
|
|
self.set_uv_tracker_in_motion(uv_tracker)
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.075)
|
|
connecting_line = self.get_connecting_line(dots)
|
|
z_line = self.get_z_line(dots)
|
|
top_dot = self.get_top_dot(z_line)
|
|
pair_group = Group(connecting_line, z_line, top_dot)
|
|
pair_group.update()
|
|
|
|
self.add(uv_tracker, dots)
|
|
self.play(
|
|
frame.animate.reorient(-38, 82, 0, (-0.02, 0.06, 1.73), 8.03),
|
|
FadeIn(pair_group),
|
|
FadeIn(surface),
|
|
Write(mesh, stroke_width=1, lag_ratio=0.001, time_span=(2, 4)),
|
|
run_time=4,
|
|
)
|
|
pair_group.add(dots)
|
|
self.add(pair_group)
|
|
frame.add_ambient_rotation()
|
|
self.wait(20)
|
|
|
|
# Show Mobius strip
|
|
frame.clear_updaters()
|
|
|
|
strip = ParametricSurface(mobius_strip_func)
|
|
strip.set_shading(0.25, 0.25, 0)
|
|
strip.set_color(BLUE, 0.35)
|
|
edge_parts = VGroup(
|
|
ParametricCurve(lambda t: mobius_strip_func(0, t)),
|
|
ParametricCurve(lambda t: mobius_strip_func(1, t)),
|
|
)
|
|
edge = edge_parts[0].copy().append_vectorized_mobject(edge_parts[1])
|
|
edge.make_smooth()
|
|
edge.set_stroke(Color("red"), 0)
|
|
|
|
def uv_to_strip_point(u, v):
|
|
return strip.uv_to_point(*torus_uv_to_mobius_uv(u, v))
|
|
|
|
strip_point = get_special_dot(color=TEAL)
|
|
strip_point.f_always.move_to(
|
|
lambda: uv_to_strip_point(*uv_tracker.get_value())
|
|
)
|
|
|
|
strip_group = Group(strip, edge)
|
|
strip_group.set_height(6)
|
|
strip_group.next_to(surface, LEFT, buff=3)
|
|
strip_group.rotate(10 * DEGREES, RIGHT)
|
|
|
|
self.play(
|
|
frame.animate.reorient(0, 68, 0, (-3.7, -0.17, 1.95), 9.56),
|
|
FadeIn(strip_group),
|
|
FadeIn(strip_point),
|
|
run_time=3
|
|
)
|
|
self.wait(20)
|
|
|
|
# Show the full mapping
|
|
uv_tracker.clear_updaters()
|
|
pre_surface = ParametricSurface(
|
|
get_half_parametric_func(uv_to_strip_point)
|
|
)
|
|
pre_surface.match_style(strip)
|
|
pre_mesh = SurfaceMesh(
|
|
ParametricSurface(uv_to_strip_point),
|
|
resolution=mesh.resolution,
|
|
normal_nudge=0
|
|
)
|
|
pre_mesh.set_stroke(WHITE, 0.25, 0.1)
|
|
|
|
moving_surface = surface.copy()
|
|
moving_mesh = mesh.copy()
|
|
|
|
self.play(FadeOut(strip_point), FadeOut(pair_group))
|
|
self.remove(surface, mesh)
|
|
self.play(
|
|
Transform(moving_surface, pre_surface),
|
|
Transform(moving_mesh, pre_mesh),
|
|
frame.animate.reorient(-25, 71, 0, (-4.86, 0.35, 1.64), 9.84),
|
|
run_time=10
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
ReplacementTransform(moving_surface, surface),
|
|
ReplacementTransform(moving_mesh, mesh),
|
|
frame.animate.reorient(36, 68, 0, (-3.94, 0.76, 0.7), 10.14),
|
|
run_time=10
|
|
)
|
|
self.play(
|
|
frame.animate.reorient(0, 60, 0, (-3.7, 0.73, 0.64), 10.44),
|
|
FadeIn(strip_point),
|
|
FadeIn(pair_group),
|
|
run_time=5,
|
|
)
|
|
|
|
# Trace the edge
|
|
u, v = uv_tracker.get_value()
|
|
self.play(
|
|
uv_tracker.animate.set_value([int(2 * u), int(2 * v)]),
|
|
loop.animate.set_stroke(opacity=0.2),
|
|
run_time=2
|
|
)
|
|
uv_tracker.set_value([0, 0])
|
|
edge.set_stroke(width=3)
|
|
loop_copy = loop.copy().set_stroke(opacity=1)
|
|
self.play(
|
|
ShowCreation(edge),
|
|
ShowCreation(loop_copy),
|
|
uv_tracker.animate.set_value([0.999, 0.999]),
|
|
run_time=5,
|
|
)
|
|
self.remove(loop_copy)
|
|
loop.set_stroke(opacity=1)
|
|
|
|
# Show the mapping with the edge
|
|
edge_image = loop.copy().match_style(edge)
|
|
moving_edge = edge.copy()
|
|
|
|
moving_group = Group(moving_surface, moving_mesh, moving_edge)
|
|
pre_group = Group(pre_surface, pre_mesh, edge)
|
|
post_group = Group(surface, mesh, edge_image)
|
|
|
|
moving_group.become(pre_group)
|
|
|
|
self.play(
|
|
FadeIn(moving_surface),
|
|
FadeIn(moving_mesh),
|
|
FadeOut(strip_point),
|
|
FadeOut(pair_group),
|
|
)
|
|
self.play(
|
|
Transform(moving_group, post_group),
|
|
frame.animate.reorient(18, 65, 0, (-3.85, 0.76, 0.85), 10.44),
|
|
run_time=10
|
|
)
|
|
self.wait()
|
|
|
|
# Show another back and forth, but with colliding points
|
|
rect_params = find_rectangle(loop_func)
|
|
pre_collision_dots, post_collision_dots = coll_dot_groups = Group(
|
|
get_special_dot(color, radius=0.075)
|
|
for color in [YELLOW, GREEN_SCREEN]
|
|
).replicate(2)
|
|
coll_dot_groups.deactivate_depth_test()
|
|
for func, dot_group in zip([uv_to_strip_point, surface_func], coll_dot_groups):
|
|
for dot, uv in zip(dot_group, [rect_params[0::2], rect_params[1::2]]):
|
|
dot.move_to(func(*uv))
|
|
|
|
moving_dots = pre_collision_dots.copy()
|
|
|
|
self.play(
|
|
Transform(moving_group, pre_group),
|
|
frame.animate.reorient(-32, 70, 0, (-3.85, 0.76, 0.85), 10.44),
|
|
run_time=5
|
|
)
|
|
self.play(FadeIn(pre_collision_dots))
|
|
self.wait()
|
|
self.play(
|
|
Transform(moving_group, post_group),
|
|
Transform(moving_dots, post_collision_dots),
|
|
frame.animate.reorient(1, 59, 0, (-4.08, 0.88, 1.17), 9.71),
|
|
run_time=5
|
|
)
|
|
|
|
# Show show rectangle for this collision
|
|
uv_tracker1 = uv_tracker
|
|
uv_tracker1.set_value(rect_params[0::2])
|
|
pair_group1 = pair_group
|
|
|
|
uv_tracker2 = ValueTracker(rect_params[1::2])
|
|
dots2 = self.get_movable_pair(uv_tracker2, loop_func, colors=[RED, MAROON_B])
|
|
connecting_line2 = self.get_connecting_line(dots2)
|
|
pair_group2 = Group(dots2, connecting_line2)
|
|
|
|
rectangle = self.get_dot_polygon(Group(dots[0], dots2[0], dots[1], dots2[1]))
|
|
rectangle.set_stroke(YELLOW, 5)
|
|
dots.update()
|
|
dots2.update()
|
|
rectangle.update()
|
|
rectangle.suspend_updating()
|
|
|
|
self.play(
|
|
FadeOut(moving_group),
|
|
FadeIn(pair_group1),
|
|
FadeIn(pair_group2),
|
|
mesh.animate.set_stroke(opacity=0.05),
|
|
frame.animate.reorient(0, 38, 0, (-2.92, 0.03, 1.12), 6.65),
|
|
run_time=3,
|
|
)
|
|
self.play(
|
|
ShowCreation(rectangle, time_span=(0, 2)),
|
|
frame.animate.reorient(36, 68, 0, (-2.89, -0.15, 0.74), 8.11),
|
|
run_time=8
|
|
)
|
|
|
|
|
|
class AmbientRectangleSearch(ShowTheSurface):
|
|
def construct(self):
|
|
# Setup loop
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
self.add(plane)
|
|
|
|
loop = get_example_loop(2)
|
|
loop.insert_n_curves(50)
|
|
loop.set_height(5)
|
|
loop.set_stroke(WHITE, 3)
|
|
loop.set_fill(opacity=0)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
# Dots
|
|
abcd_tracker = ValueTracker(np.random.random(4))
|
|
dots = self.get_movable_quad(abcd_tracker, loop_func, radius=0.1)
|
|
polygon = self.get_dot_polygon(dots)
|
|
polygon.set_stroke(YELLOW, 2)
|
|
|
|
self.add(dots, polygon)
|
|
|
|
# Various rectangles
|
|
for _ in range(20):
|
|
rect_params = find_rectangle(
|
|
loop_func,
|
|
initial_condition=np.random.random(4),
|
|
target_angle=np.random.uniform(0, 90 * DEG),
|
|
)
|
|
self.play(abcd_tracker.animate.set_value(rect_params), run_time=2)
|
|
self.wait()
|
|
|
|
|
|
class GenericLoopPair(ShowTheSurface):
|
|
def construct(self):
|
|
loop = Circle(radius=2.5)
|
|
loop.set_stroke(WHITE, 3)
|
|
loop.set_fill(opacity=0)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
uv_tracker = ValueTracker([0, 0.5])
|
|
# self.set_uv_tracker_in_motion(uv_tracker, velocity=(0.1 * PI / 2, -0.1))
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.075, colors=[YELLOW, YELLOW])
|
|
connecting_line = self.get_connecting_line(dots)
|
|
connecting_line.set_stroke(YELLOW)
|
|
pair_group = Group(dots, connecting_line)
|
|
|
|
self.add(uv_tracker, pair_group)
|
|
|
|
# Wait
|
|
for _ in range(20):
|
|
self.play(uv_tracker.animate.set_value(np.random.random(2)), run_time=2)
|
|
self.wait(0.5)
|
|
|
|
|
|
class SudaneseBand(InteractiveScene):
|
|
def construct(self):
|
|
# Tranform mobius to Sudanese
|
|
frame = self.frame
|
|
sudanese_band = self.get_full_surface(sudanese_band_func)
|
|
strip = self.get_full_surface(alt_mobius_strip_func)
|
|
strip.set_height(6)
|
|
og_strip = strip.copy()
|
|
sudanese_band.set_height(6)
|
|
|
|
frame.reorient(28, 75, 0, ORIGIN, 8)
|
|
self.add(strip)
|
|
self.play(frame.animate.increment_theta(180 * DEG), run_time=12)
|
|
self.play(
|
|
frame.animate.reorient(99, 102, 0),
|
|
Transform(strip, sudanese_band, time_span=(0, 6)),
|
|
run_time=10
|
|
)
|
|
self.wait(30) # Examine
|
|
|
|
# Back to strip
|
|
self.play(
|
|
Transform(strip, og_strip),
|
|
run_time=6
|
|
)
|
|
|
|
def get_full_surface(self, surface_func, resolution=(101, 101)):
|
|
surface = ParametricSurface(surface_func, resolution=resolution)
|
|
surface.set_color(BLUE_E, 1)
|
|
surface.set_shading(0.25, 0.25, 0)
|
|
|
|
mesh = VGroup(
|
|
*SurfaceMesh(surface, resolution=(21, 21), normal_nudge=1e-3),
|
|
*SurfaceMesh(surface, resolution=(21, 21), normal_nudge=-1e-3),
|
|
)
|
|
mesh.set_stroke(WHITE, 0.5, 0.2)
|
|
|
|
edge = VGroup(
|
|
ParametricCurve(lambda t: surface_func(0, t), (0, 1, 0.01)),
|
|
ParametricCurve(lambda t: surface_func(1, t), (0, 1, 0.01)),
|
|
)
|
|
edge.set_stroke(Color("red"), 2)
|
|
edge.apply_depth_test()
|
|
|
|
return Group(surface, mesh, edge)
|
|
|
|
|
|
class ShowSurfaceReflection(ShowTheSurface):
|
|
def construct(self):
|
|
# Setup loop and surface
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
self.add(axes, plane)
|
|
|
|
loop = get_example_loop(2)
|
|
loop.insert_n_curves(50)
|
|
loop.set_height(5)
|
|
loop.set_stroke(WHITE, 3)
|
|
loop.set_fill(opacity=0)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
surface, mesh, surface_func = self.get_surface_info(loop_func)
|
|
surface.set_opacity(0.1)
|
|
surface_group = Group(surface, mesh)
|
|
|
|
self.add(surface_group)
|
|
frame.reorient(-36, 82, 0, (0.09, 1.17, 2.23), 11.39)
|
|
self.play(frame.animate.reorient(36, 85, 0, (-0.68, 0.78, 2.39), 11.39), run_time=6)
|
|
|
|
# Show the reflection
|
|
reflection = surface_group.copy()
|
|
reflection.stretch(-1, 2, about_point=axes.c2p(0, 0, 0))
|
|
ghost_surface = surface_group.copy()
|
|
ghost_surface.set_opacity(0)
|
|
|
|
self.play(
|
|
ReplacementTransform(ghost_surface, reflection),
|
|
frame.animate.reorient(33, 81, 0, (-0.15, 0.61, 0.29), 14.60),
|
|
run_time=5
|
|
)
|
|
self.add(reflection, surface_group)
|
|
|
|
# Emphasize the ege
|
|
frame.add_ambient_rotation(2 * DEG)
|
|
low_edge = loop.copy()
|
|
low_edge.set_color(RED)
|
|
shift_vect = 0.25 * OUT
|
|
|
|
self.play(
|
|
surface_group.animate.shift(shift_vect),
|
|
loop.animate.set_stroke(Color("red")).shift(shift_vect),
|
|
low_edge.animate.shift(-shift_vect),
|
|
reflection.animate.shift(-shift_vect),
|
|
FadeOut(axes),
|
|
FadeOut(plane),
|
|
run_time=2,
|
|
)
|
|
self.wait(1)
|
|
self.play(
|
|
FadeOut(loop, shift=-shift_vect),
|
|
FadeOut(low_edge, shift=shift_vect),
|
|
surface_group.animate.shift(-shift_vect),
|
|
reflection.animate.shift(shift_vect),
|
|
run_time=2
|
|
)
|
|
self.wait(12)
|
|
|
|
|
|
class ConstructKleinBottle(InteractiveScene):
|
|
def construct(self):
|
|
# Add arrow diagram
|
|
square = Square()
|
|
square.set_fill(GREY_E, 1).set_stroke(BLACK, width=0)
|
|
square.set_height(4)
|
|
|
|
dr_tri = Polygon(DL, DR, UR)
|
|
dr_tri.match_style(square)
|
|
dr_tri.replace(square)
|
|
|
|
mobius_diagram = VGroup(
|
|
dr_tri,
|
|
self.get_tri_arrow(square.get_corner(DL), square.get_corner(DR)),
|
|
self.get_tri_arrow(square.get_corner(DR), square.get_corner(UR)),
|
|
Line(square.get_corner(DL), square.get_corner(UR)).set_stroke(Color("red"), 3)
|
|
)
|
|
|
|
mobius_label = Text("Möbius Strip", font_size=60)
|
|
mobius_label.next_to(mobius_diagram, UR)
|
|
mobius_label.shift(DOWN + 0.25 * RIGHT)
|
|
mobius_arrow = Arrow(
|
|
mobius_label.get_bottom(),
|
|
mobius_diagram.get_center() + 0.5 * DR,
|
|
path_arc=-90 * DEG,
|
|
thickness=5
|
|
)
|
|
mobius_arrow.set_z_index(1)
|
|
|
|
self.add(mobius_diagram)
|
|
self.play(
|
|
FadeIn(mobius_label),
|
|
FadeIn(mobius_arrow),
|
|
)
|
|
self.wait()
|
|
|
|
# Show a reflection
|
|
reflection = mobius_diagram.copy()
|
|
reflection.flip(UR, about_point=square.get_center())
|
|
reflection.shift(UL)
|
|
reflection[1:3].set_color(PINK)
|
|
|
|
reflection_label = Text("Reflected\nMöbius Strip", font_size=60)
|
|
reflection_label.next_to(reflection, LEFT, aligned_edge=DOWN)
|
|
reflection_arrow = Arrow(
|
|
reflection_label.get_top(),
|
|
reflection.get_center() + 0.5 * LEFT + 0.25 * UP,
|
|
path_arc=-90 * DEG,
|
|
thickness=5
|
|
)
|
|
|
|
self.play(
|
|
LaggedStart(
|
|
TransformMatchingStrings(mobius_label.copy(), reflection_label, run_time=1),
|
|
TransformFromCopy(mobius_diagram, reflection),
|
|
TransformFromCopy(mobius_arrow, reflection_arrow),
|
|
lag_ratio=0.1
|
|
),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Glue along boundary
|
|
glue_label = Text("Glue the boundaries", font_size=36)
|
|
glue_label.next_to(ORIGIN, DOWN, SMALL_BUFF)
|
|
glue_label.rotate(45 * DEG, about_point=ORIGIN)
|
|
glue_label.shift(square.get_center())
|
|
|
|
self.play(
|
|
LaggedStart(
|
|
FadeOut(reflection_arrow),
|
|
FadeOut(mobius_arrow),
|
|
reflection.animate.shift(DR),
|
|
reflection_label.animate.scale(0.75).next_to(square, LEFT, MED_SMALL_BUFF),
|
|
mobius_label.animate.scale(0.75).next_to(square, RIGHT, MED_SMALL_BUFF),
|
|
),
|
|
FadeIn(glue_label, lag_ratio=0.1),
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
LaggedStartMap(FadeOut, VGroup(glue_label, reflection_label, mobius_label)),
|
|
mobius_diagram[-1].animate.set_stroke(width=0),
|
|
reflection[-1].animate.set_stroke(width=0),
|
|
)
|
|
|
|
# Cut along diagonal
|
|
teal_arrows = mobius_diagram[1:3]
|
|
pink_arrows = reflection[1:3]
|
|
yellow_arrows = self.get_tri_arrow(square.get_corner(UL), square.get_corner(DR), color=YELLOW).replicate(2)
|
|
ur_tri = Polygon(DR, UR, UL)
|
|
dl_tri = Polygon(DR, DL, UL)
|
|
for tri in [ur_tri, dl_tri]:
|
|
tri.match_style(square)
|
|
tri.replace(square)
|
|
|
|
ur_group = VGroup(ur_tri, teal_arrows[1], pink_arrows[1])
|
|
dl_group = VGroup(dl_tri, teal_arrows[0], pink_arrows[0])
|
|
|
|
self.remove(mobius_diagram, reflection)
|
|
self.add(ur_group)
|
|
self.add(dl_group)
|
|
|
|
self.play(*(Write(arrow, stroke_color=YELLOW) for arrow in yellow_arrows))
|
|
ur_group.add(yellow_arrows[0])
|
|
dl_group.add(yellow_arrows[1])
|
|
self.play(VGroup(ur_group, dl_group).animate.space_out_submobjects(3))
|
|
self.wait()
|
|
|
|
# Flip and glue
|
|
frame = self.frame
|
|
self.play(
|
|
dl_group.animate.next_to(ORIGIN, UP, 0.5),
|
|
ur_group.animate.flip(UR).next_to(ORIGIN, DOWN, 0.5),
|
|
frame.animate.set_height(10),
|
|
run_time=2,
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
ur_group.animate.shift(-ur_tri.get_top()),
|
|
dl_group.animate.shift(-dl_tri.get_bottom()),
|
|
)
|
|
self.play(teal_arrows.animate.set_stroke(width=0).set_fill(opacity=0))
|
|
|
|
# Shear back into square
|
|
pre_square = square.copy()
|
|
pre_square.apply_matrix(np.matrix([[1, -1], [0, 1]]).T)
|
|
pre_square.move_to(VGroup(dl_tri, dr_tri), UP)
|
|
|
|
trg_yellow_arrows = VGroup(
|
|
self.get_tri_arrow(square.get_corner(DR), square.get_corner(DL), color=YELLOW).flip(RIGHT),
|
|
self.get_tri_arrow(square.get_corner(UL), square.get_corner(UR), color=YELLOW),
|
|
)
|
|
|
|
self.remove(ur_tri, dl_tri)
|
|
self.add(pre_square, pink_arrows, yellow_arrows)
|
|
self.play(
|
|
Transform(pre_square, square),
|
|
Transform(yellow_arrows, trg_yellow_arrows),
|
|
pink_arrows[0].animate.move_to(square.get_left()),
|
|
pink_arrows[1].animate.move_to(square.get_right()),
|
|
frame.animate.set_height(8),
|
|
run_time=2
|
|
)
|
|
self.wait()
|
|
|
|
# Fold into half tube
|
|
klein_func = self.get_kelin_bottle_func()
|
|
near_smooth = bezier([0, 0.1, 0.9, 1])
|
|
surfaces = Group(
|
|
TexturedSurface(ParametricSurface(func), "KleinBottleTexture")
|
|
for func in [
|
|
square_func,
|
|
tube_func,
|
|
lambda u, v: torus_func(u, 0.5 * v),
|
|
lambda u, v: klein_func(u, 0.5 * near_smooth(v)),
|
|
]
|
|
)
|
|
for surface in surfaces:
|
|
surface.set_opacity(0.9)
|
|
surface.set_shading(0.3, 0.2, 0)
|
|
square3d, tube, half_torus, half_klein = surfaces
|
|
square3d.replace(square)
|
|
square3d.shift(4 * OUT)
|
|
moving_surface = square3d.copy()
|
|
|
|
tube.set_width(square.get_width() / PI)
|
|
tube.set_height(square.get_height(), stretch=True)
|
|
tube.move_to(square3d)
|
|
|
|
half_torus.match_depth(tube)
|
|
half_torus.move_to(tube)
|
|
|
|
self.play(
|
|
FadeIn(moving_surface, shift=square3d.get_z() * OUT),
|
|
frame.animate.reorient(0, 56, 0, (0.07, 0.52, 2.39), 11.25),
|
|
run_time=3,
|
|
)
|
|
self.play(Transform(moving_surface, tube), run_time=4)
|
|
self.wait()
|
|
self.play(Transform(moving_surface, half_torus, path_arc=PI / 2), run_time=4)
|
|
self.wait()
|
|
self.play(Transform(moving_surface, half_klein), run_time=4)
|
|
|
|
# Transition to full Klein Bottle
|
|
klein_diagram = VGroup(pre_square, pink_arrows, yellow_arrows)
|
|
v_upper_bound = 0.85
|
|
self.play(
|
|
UpdateFromAlphaFunc(moving_surface, lambda m, a: m.match_points(
|
|
ParametricSurface(lambda u, v: klein_func(u, interpolate(0.5, 1, a) * near_smooth(v)))
|
|
).set_opacity(interpolate(0.9, 0.75, a))),
|
|
klein_diagram.animate.set_x(-5),
|
|
frame.animate.reorient(0, 46, 0, (-0.71, -0.11, 1.71), 10.87),
|
|
run_time=8
|
|
)
|
|
self.wait()
|
|
self.play(
|
|
klein_diagram.animate.next_to(moving_surface, LEFT, buff=2),
|
|
frame.animate.reorient(0, 0, 0, (-3.2, 0.03, 0.0), 12.58),
|
|
run_time=4
|
|
)
|
|
self.wait()
|
|
|
|
def get_tri_arrow(self, start, end, color=TEAL, stroke_width=3, tip_width=0.35):
|
|
line = Line(start, end)
|
|
line.set_stroke(color, stroke_width)
|
|
tips = ArrowTip().replicate(3)
|
|
tips.set_fill(color)
|
|
tips.set_width(tip_width)
|
|
tips.rotate(line.get_angle())
|
|
for alpha, tip in zip(np.linspace(0.2, 0.8, 3), tips):
|
|
tip.move_to(line.pfp(alpha))
|
|
|
|
return VGroup(line, tips)
|
|
|
|
def get_kelin_bottle_func(self, width=4, z=4):
|
|
# Test kelin func
|
|
ref_svg = SVGMobject("KleinReference")[0]
|
|
ref_svg.make_smooth(approx=False)
|
|
ref_svg.add_line_to(ref_svg.get_start())
|
|
ref_svg.set_stroke(WHITE, 3)
|
|
ref_svg.set_width(width)
|
|
ref_svg.rotate(PI)
|
|
ref_svg.set_z(4)
|
|
ref_svg.insert_n_curves(100)
|
|
|
|
# curve_func = get_quick_loop_func(ref_svg)
|
|
curve_func = ref_svg.quick_point_from_proportion
|
|
radius_func = bezier([1, 1, 0.5, 0.3, 0.3, 0.3, 1.0])
|
|
tan_alpha_func = bezier([1, 1, 0, 0, 0, 0, 1, 1])
|
|
v_alpha_func = squish_rate_func(smooth, 0.25, 0.75)
|
|
|
|
def pre_klein_func(u, v):
|
|
dv = 1e-2
|
|
c_point = curve_func(v)
|
|
c_prime = normalize((curve_func(v + dv) - curve_func(v - dv)) / (2 * dv))
|
|
tangent_alpha = tan_alpha_func(v)
|
|
# tangent = interpolate(c_prime, UP if v < 0.5 else DOWN, tangent_alpha)
|
|
tangent = interpolate(c_prime, interpolate(UP, DOWN, v_alpha_func(v)), tangent_alpha)
|
|
|
|
perp = normalize(cross(tangent, OUT))
|
|
radius = radius_func(v)
|
|
|
|
return c_point + radius * (math.cos(TAU * u) * OUT - math.sin(TAU * u) * perp)
|
|
|
|
v_upper_bound = 0.85
|
|
|
|
def true_kelin_func(u, v):
|
|
if v <= v_upper_bound:
|
|
return pre_klein_func(u, v)
|
|
else:
|
|
alpha = inverse_interpolate(v_upper_bound, 1, v)
|
|
return interpolate(pre_klein_func(u, v_upper_bound), pre_klein_func(1 - u, 0), alpha)
|
|
|
|
return true_kelin_func
|
|
|
|
|
|
class PuzzleOverMobiusDiagram(ConstructKleinBottle):
|
|
def construct(self):
|
|
# Test
|
|
triangle = Polygon(DL, DR, UR)
|
|
triangle.set_fill(GREY_E, 1)
|
|
triangle.set_stroke(WHITE, 0)
|
|
triangle.set_height(4)
|
|
triangle.to_edge(UP, buff=1)
|
|
|
|
low_arrow = self.get_tri_arrow(triangle.get_corner(DL), triangle.get_corner(DR), stroke_width=4)
|
|
right_arrow = self.get_tri_arrow(triangle.get_corner(DR), triangle.get_corner(UR), stroke_width=4)
|
|
edge = Line(triangle.get_corner(DL), triangle.get_corner(UR))
|
|
edge.set_stroke(Color("red"), 3)
|
|
|
|
self.add(triangle, edge, low_arrow, right_arrow)
|
|
self.wait()
|
|
|
|
low_arrow.save_state()
|
|
self.play(
|
|
triangle.animate.set_opacity(0.25),
|
|
edge.animate.set_opacity(0.1),
|
|
low_arrow.animate.rotate(90 * DEG).next_to(right_arrow, RIGHT)
|
|
)
|
|
self.wait()
|
|
self.play(low_arrow.animate.move_to(right_arrow))
|
|
self.play(low_arrow.animate.next_to(right_arrow, RIGHT))
|
|
self.play(
|
|
Restore(low_arrow),
|
|
triangle.animate.set_fill(opacity=1),
|
|
edge.animate.set_stroke(opacity=1),
|
|
)
|
|
|
|
# Show the dots
|
|
dots = GlowDot(color=BLUE, radius=0.25, glow_factor=1.5).replicate(2)
|
|
top_dot = dots[0].copy()
|
|
top_dot.move_to(right_arrow.get_top())
|
|
for _ in range(3):
|
|
dots[0].move_to(low_arrow.get_left())
|
|
dots[1].move_to(right_arrow.get_bottom())
|
|
self.add(top_dot)
|
|
self.wait(1 / 30)
|
|
self.remove(top_dot)
|
|
self.play(
|
|
dots[0].animate.move_to(low_arrow.get_right()),
|
|
dots[1].animate.move_to(right_arrow.get_top()),
|
|
run_time=3,
|
|
rate_func=linear
|
|
)
|
|
|
|
|
|
class ShowAngleInformation(ShowTheSurface):
|
|
def construct(self):
|
|
# Setup (fairly heavily copied from above)
|
|
frame = self.frame
|
|
axes, plane = self.get_axes_and_plane()
|
|
plane.fade(0.5)
|
|
frame.set_height(6)
|
|
frame.set_x(1)
|
|
self.add(plane)
|
|
|
|
loop = get_example_loop()
|
|
loop.set_height(5.5).center()
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
square_params = find_rectangle(loop_func, target_angle=90 * DEG)
|
|
uv_tracker = ValueTracker(square_params[0::2])
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.05)
|
|
connecting_line = self.get_connecting_line(dots)
|
|
connecting_line.update().suspend_updating()
|
|
midpoint_dot = self.get_midpoint_dot(dots)
|
|
midpoint_dot.update()
|
|
pair_group = Group(dots, connecting_line, midpoint_dot)
|
|
|
|
self.add(pair_group)
|
|
|
|
# Corner coordinates
|
|
corner_coords = Tex("(x, y, d)")
|
|
corner_coords.next_to(frame.get_corner(UR), DL)
|
|
self.add(corner_coords)
|
|
|
|
# Add coordinates
|
|
coords = Tex("(x, y)")
|
|
coords.set_backstroke(BLACK, 5)
|
|
coords.set_fill(WHITE, 1)
|
|
|
|
coords.next_to(midpoint_dot, DR, buff=-0.1)
|
|
self.play(
|
|
Write(coords),
|
|
FlashAround(corner_coords["x, y"], time_width=1.5),
|
|
run_time=1.5
|
|
)
|
|
|
|
# Show the distance
|
|
brace = Brace(Line(LEFT, RIGHT).set_width(connecting_line.get_length()), DOWN, buff=SMALL_BUFF)
|
|
brace.rotate(connecting_line.get_angle() + PI, about_point=ORIGIN)
|
|
brace.shift(connecting_line.get_center())
|
|
brace.set_fill(GREY, 1)
|
|
d_label = Tex("d")
|
|
d_label.set_backstroke(BLACK, 5)
|
|
d_label.move_to(brace.get_center() + 0.5 * normalize(brace.get_center() - midpoint_dot.get_center()))
|
|
|
|
self.play(
|
|
GrowFromCenter(brace),
|
|
Write(d_label),
|
|
FlashAround(corner_coords["d"], time_width=1.5, run_time=1.5)
|
|
)
|
|
self.wait()
|
|
|
|
# Show the angle
|
|
cross_point = connecting_line.pfp(inverse_interpolate(
|
|
connecting_line.get_y(DOWN), connecting_line.get_y(UP), -1
|
|
))
|
|
h_line = Line(LEFT, RIGHT)
|
|
h_line.set_width(4)
|
|
h_line.move_to(cross_point + RIGHT)
|
|
h_line.set_stroke(WHITE, 2)
|
|
arc = Arc(0, connecting_line.get_angle(), radius=0.45, arc_center=cross_point)
|
|
theta = Tex(R"\theta")
|
|
theta.next_to(arc.pfp(0.5), RIGHT, SMALL_BUFF).shift(SMALL_BUFF * UP)
|
|
|
|
self.play(
|
|
ShowCreation(h_line),
|
|
ShowCreation(arc),
|
|
Write(theta),
|
|
)
|
|
|
|
# New corner coords
|
|
new_corner_coords = Tex(R"(x, y, d, \theta)")
|
|
new_corner_coords.move_to(corner_coords, RIGHT)
|
|
self.play(
|
|
*(
|
|
ReplacementTransform(corner_coords[substr], new_corner_coords[substr])
|
|
for substr in ["(x, y, d", ")"]
|
|
),
|
|
TransformFromCopy(theta, new_corner_coords[R"\theta"]),
|
|
Write(new_corner_coords[R","][-1]),
|
|
)
|
|
self.add(new_corner_coords)
|
|
self.wait()
|
|
|
|
# Show the other pair
|
|
uv_tracker2 = ValueTracker(square_params[1::2])
|
|
dots2 = self.get_movable_pair(uv_tracker2, loop_func, radius=0.05, colors=[RED, MAROON_B])
|
|
connecting_line2 = self.get_connecting_line(dots2)
|
|
pair_group2 = Group(dots2, connecting_line2)
|
|
midpoint = midpoint_dot.get_center()
|
|
|
|
self.play(
|
|
ReplacementTransform(
|
|
connecting_line.copy().clear_updaters(),
|
|
connecting_line2,
|
|
path_arc=-90 * DEG,
|
|
suspend_mobject_updating=True
|
|
),
|
|
Rotate(brace, -90 * DEG, about_point=midpoint),
|
|
d_label.animate.rotate(-90 * DEGREES, about_point=midpoint).rotate(90 * DEG),
|
|
coords.animate.next_to(midpoint_dot, DL, buff=-0.1),
|
|
loop.animate.set_stroke(opacity=0.5)
|
|
)
|
|
self.play(FadeIn(dots2))
|
|
self.wait()
|
|
|
|
# Show the right angle
|
|
cross_point2 = connecting_line2.pfp(inverse_interpolate(
|
|
connecting_line2.get_start()[1], connecting_line2.get_end()[1], 0
|
|
))
|
|
h_line2 = Line(cross_point2, cross_point2 + 2 * RIGHT)
|
|
h_line2.match_style(h_line)
|
|
arc2 = Arc(0, PI + connecting_line2.get_angle(), radius=0.45, arc_center=cross_point2)
|
|
theta_plus = Tex(R"\theta + 90^\circ", font_size=42)
|
|
theta_plus.next_to(arc2.pfp(0.3), UR, buff=SMALL_BUFF)
|
|
|
|
elbow = Elbow()
|
|
elbow.rotate(connecting_line2.get_angle() + 90 * DEG, about_point=ORIGIN)
|
|
elbow.shift(midpoint_dot.get_center())
|
|
perp_label = Tex(R"90^\circ", font_size=36)
|
|
perp_label.next_to(elbow.pfp(0.5), UP, SMALL_BUFF)
|
|
|
|
self.play(
|
|
FadeOut(brace),
|
|
FadeOut(d_label),
|
|
ShowCreation(elbow),
|
|
Write(perp_label),
|
|
)
|
|
|
|
# New pair corner label
|
|
shift_vect = 0.5 * RIGHT
|
|
low_corner_coords = Tex(R"(x, y, d, \theta + 90^\circ)")
|
|
low_corner_coords.next_to(new_corner_coords, DOWN, aligned_edge=RIGHT)
|
|
low_corner_coords.shift(shift_vect)
|
|
|
|
self.play(
|
|
TransformMatchingStrings(new_corner_coords.copy(), low_corner_coords),
|
|
new_corner_coords.animate.shift(shift_vect),
|
|
frame.animate.shift(shift_vect),
|
|
)
|
|
self.wait()
|
|
|
|
# Show the square
|
|
square = self.get_dot_polygon(
|
|
list(it.chain(*zip(dots, dots2)))
|
|
)
|
|
square.update().suspend_updating()
|
|
square.set_stroke(YELLOW, 3)
|
|
self.play(ShowCreation(square))
|
|
self.wait()
|
|
|
|
# Comment on four dimension
|
|
|
|
|
|
class RectanglesOfAllAspectRatios(LoopScene):
|
|
def construct(self):
|
|
# Loop
|
|
loop = self.get_loop()
|
|
loop.set_height(6.5)
|
|
loop.to_corner(DR)
|
|
loop.set_stroke(WHITE, 1).set_fill(opacity=0)
|
|
loop_func = get_quick_loop_func(loop)
|
|
self.add(loop)
|
|
|
|
# Dots
|
|
angle_tracker = ValueTracker(90 * DEG)
|
|
get_angle = angle_tracker.get_value
|
|
square_params = find_rectangle(
|
|
loop_func,
|
|
initial_condition=np.arange(0.5, 1, 0.1),
|
|
target_angle=angle_tracker.get_value()
|
|
)
|
|
abcd_tracker = ValueTracker(square_params)
|
|
dots = self.get_movable_quad(abcd_tracker, loop_func, radius=0.075)
|
|
rect = self.get_dot_polygon(dots)
|
|
rect.set_stroke(YELLOW)
|
|
|
|
rect_group = Group(abcd_tracker, dots, rect)
|
|
|
|
self.add(rect_group)
|
|
|
|
# Show aspect ratio
|
|
def get_aspect_ratio():
|
|
phi = get_angle() / 2
|
|
return math.cot(phi)
|
|
|
|
sample_rect = Square(side_length=2)
|
|
sample_rect_center = 4 * LEFT + DOWN
|
|
sample_rect_size_factor = 2.5
|
|
sample_rect.set_stroke(YELLOW)
|
|
|
|
def update_sample_rect(rect):
|
|
phi = get_angle() / 2
|
|
sf = sample_rect_size_factor
|
|
rect.set_shape(sf * math.cos(phi), sf * math.sin(phi))
|
|
rect.move_to(sample_rect_center)
|
|
|
|
sample_rect.add_updater(update_sample_rect)
|
|
|
|
aspect_ratio_label = TexText("Aspect ratio = 1:1")
|
|
aspect_ratio_label.next_to(sample_rect, UP).shift(MED_SMALL_BUFF * LEFT)
|
|
ar_label = aspect_ratio_label["1:1"][0]
|
|
|
|
self.add(aspect_ratio_label[:-3])
|
|
self.add(ar_label)
|
|
self.add(sample_rect)
|
|
self.wait()
|
|
|
|
# Change angle
|
|
def true_find_rect(loop_func, angle, max_tries=10, max_cost=1e-2):
|
|
for _ in range(max_tries):
|
|
result, cost = find_rectangle(
|
|
loop_func,
|
|
initial_condition=np.random.random(4),
|
|
target_angle=angle,
|
|
n_refinements=4,
|
|
return_cost=True
|
|
)
|
|
if cost < max_cost:
|
|
return result
|
|
return result
|
|
|
|
aspect_ratio_pairs = self.get_aspect_ratio_pairs()
|
|
for tex, ratio in aspect_ratio_pairs:
|
|
label = Tex(tex)
|
|
label.move_to(ar_label, LEFT)
|
|
if ratio < 1:
|
|
ratio = 1.0 / ratio
|
|
angle_tracker.set_value(2 * math.atan(1.0 / ratio))
|
|
abcd_tracker.set_value(true_find_rect(loop_func, get_angle()))
|
|
ar_label.set_submobjects(list(label))
|
|
self.wait(1 / 3)
|
|
|
|
def get_loop(self):
|
|
return get_example_loop(4)
|
|
|
|
def get_aspect_ratio_pairs(self):
|
|
return [
|
|
("16 : 9", 16 / 9),
|
|
("4 : 3", 4 / 3),
|
|
("10 : 1", 10 / 1),
|
|
(R"\sqrt{2} : 1", math.sqrt(2)),
|
|
(R"5 : 2", 5 / 2),
|
|
(R"\pi : 1", math.pi),
|
|
(R"\varphi : 1", (1 + math.sqrt(5)) / 2), # Golden ratio
|
|
(R"e : 1", math.e),
|
|
("21 : 9", 21 / 9), # Ultrawide
|
|
(R"\sqrt{3} : 1", math.sqrt(3)),
|
|
("3 : 2", 3 / 2), # Classic photography
|
|
("1 : 1", 1 / 1), # Square
|
|
(R"2\pi : 1", 2 * math.pi),
|
|
("7 : 5", 7 / 5),
|
|
(R"\ln(10) : 1", math.log(10)),
|
|
("8 : 5", 8 / 5),
|
|
("12 : 5", 12 / 5),
|
|
(R"\sqrt{5} : 1", math.sqrt(5)),
|
|
("9 : 16", 9 / 16), # Portrait 16:9
|
|
("2 : 3", 2 / 3), # Portrait photography
|
|
(R"\pi : 2", math.pi / 2),
|
|
("7 : 3", 7 / 3),
|
|
(R"2\sqrt{2} : 1", 2 * math.sqrt(2)),
|
|
("15 : 4", 15 / 4),
|
|
(R"\sqrt{7} : 1", math.sqrt(7)),
|
|
("11 : 4", 11 / 4),
|
|
(R"e : 2", math.e / 2),
|
|
("13 : 5", 13 / 5),
|
|
("8 : 3", 8 / 3),
|
|
(R"\pi^2 : 9", math.pi ** 2 / 9)
|
|
]
|
|
|
|
|
|
class AllAspectRatioXMaxTree(RectanglesOfAllAspectRatios):
|
|
def get_loop(self):
|
|
return SVGMobject("xmas_tree").family_members_with_points()[0]
|
|
|
|
def get_aspect_ratio_pairs(self):
|
|
return [
|
|
(R"\sqrt{10} : 1", math.sqrt(10)),
|
|
(R"\pi : \sqrt{2}", math.pi / math.sqrt(2)),
|
|
("17 : 10", 17 / 10),
|
|
(R"\ln(2) : 1", math.log(2)),
|
|
("14 : 3", 14 / 3),
|
|
(R"3\sqrt{3} : 2", 3 * math.sqrt(3) / 2),
|
|
("19 : 6", 19 / 6),
|
|
(R"e^2 : 10", math.e ** 2 / 10),
|
|
("25 : 16", 25 / 16),
|
|
(R"\varphi^2 : 2", ((1 + math.sqrt(5)) / 2) ** 2 / 2),
|
|
("23 : 9", 23 / 9),
|
|
(R"\sqrt{6} : 1", math.sqrt(6)),
|
|
("18 : 5", 18 / 5),
|
|
(R"2\pi : 3", 2 * math.pi / 3),
|
|
("16 : 10", 16 / 10),
|
|
(R"\sqrt{13} : 2", math.sqrt(13) / 2),
|
|
("22 : 7", 22 / 7), # Common π approximation ratio
|
|
(R"e : \sqrt{5}", math.e / math.sqrt(5)),
|
|
("20 : 11", 20 / 11),
|
|
(R"\ln(5) : 1", math.log(5)),
|
|
("27 : 16", 27 / 16),
|
|
(R"3\varphi : 4", 3 * ((1 + math.sqrt(5)) / 2) / 4),
|
|
("24 : 11", 24 / 11),
|
|
(R"\sqrt{15} : 2", math.sqrt(15) / 2),
|
|
("21 : 13", 21 / 13),
|
|
(R"5\pi : 8", 5 * math.pi / 8),
|
|
("26 : 15", 26 / 15),
|
|
(R"\sqrt{17} : 3", math.sqrt(17) / 3),
|
|
("29 : 12", 29 / 12),
|
|
(R"e\pi : 10", math.e * math.pi / 10)
|
|
]
|
|
|
|
|
|
class AllAspectRatioPi(RectanglesOfAllAspectRatios):
|
|
def get_loop(self):
|
|
return Tex(R"\pi").family_members_with_points()[0]
|
|
|
|
|
|
class TrackTheAngle(ShowTheSurface):
|
|
initial_uv = [0.55, 0.75]
|
|
second_uv = [0.6, 0.66]
|
|
limiting_uv = [0.659, 0.66]
|
|
|
|
def construct(self):
|
|
# Initial setup
|
|
frame = self.frame
|
|
frame.set_height(6.5)
|
|
frame.set_x(1.5)
|
|
loop, pair_group = self.setup_loop()
|
|
uv_tracker, dots, connecting_line = pair_group
|
|
uv_tracker.set_value(self.initial_uv)
|
|
pair_group.update()
|
|
|
|
self.remove(pair_group)
|
|
|
|
# Show smoothness
|
|
x_tracker = ValueTracker(0.5)
|
|
get_x = x_tracker.get_value
|
|
dx = 1e-3
|
|
line = Line(LEFT, RIGHT)
|
|
line.set_stroke(TEAL, 4)
|
|
line.f_always.put_start_and_end_on(
|
|
lambda: loop.quick_point_from_proportion(get_x()),
|
|
lambda: loop.quick_point_from_proportion(get_x() + dx)
|
|
)
|
|
line.always.set_length(3)
|
|
|
|
self.add(line)
|
|
self.play(x_tracker.animate.set_value(0.9), run_time=10)
|
|
|
|
# Temp
|
|
x_tracker.set_value(0)
|
|
self.play(x_tracker.animate.set_value(0.99), run_time=25, rate_func=linear)
|
|
|
|
# Angle label
|
|
angle_label = self.get_angle_label(connecting_line)
|
|
theta_label = always_redraw(lambda: self.get_theta_label(connecting_line))
|
|
|
|
self.play(
|
|
VFadeIn(theta_label, suspend_mobject_updating=True),
|
|
VFadeIn(angle_label, suspend_mobject_updating=True),
|
|
FadeOut(line),
|
|
FadeIn(pair_group),
|
|
loop.animate.set_stroke(WHITE, 1, 0.5)
|
|
)
|
|
|
|
# Move points
|
|
self.play(
|
|
uv_tracker.animate.set_value(self.second_uv),
|
|
run_time=4
|
|
)
|
|
self.play(
|
|
uv_tracker.animate.set_value(self.limiting_uv),
|
|
run_time=15,
|
|
rate_func=bezier([0, 0, 1, 1, 1, 1, 1, 1, 1]),
|
|
)
|
|
|
|
def setup_loop(self):
|
|
# Setup (fairly heavily copied from above)
|
|
axes, plane = self.get_axes_and_plane()
|
|
plane.fade(0.5)
|
|
self.add(plane)
|
|
|
|
loop = self.get_loop()
|
|
loop.set_height(5.5).center()
|
|
loop_func = loop.quick_point_from_proportion
|
|
self.add(loop)
|
|
|
|
uv_tracker = ValueTracker([0, 0.5])
|
|
dots = self.get_movable_pair(uv_tracker, loop_func, radius=0.075)
|
|
connecting_line = self.get_connecting_line(dots)
|
|
connecting_line.always.set_length(20)
|
|
pair_group = Group(uv_tracker, dots, connecting_line)
|
|
pair_group.update()
|
|
|
|
self.add(pair_group)
|
|
|
|
return loop, pair_group
|
|
|
|
def get_loop(self):
|
|
return get_example_loop(4)
|
|
|
|
def get_angle_label(self, line):
|
|
label = Tex(R"\theta = 100.0^\circ")
|
|
label.next_to(self.frame.get_corner(UR), DL)
|
|
angle = label.make_number_changeable("100.0", edge_to_fix=RIGHT)
|
|
angle.f_always.set_value(lambda: (line.get_angle() % PI) / DEG)
|
|
return label
|
|
|
|
def get_theta_label(self, line):
|
|
h_line = Line(ORIGIN, RIGHT)
|
|
line_center = line.get_center()
|
|
h_line.move_to(line_center, LEFT)
|
|
angle = line.get_angle() % PI
|
|
arc = Arc(0, angle, radius=0.25, arc_center=line_center)
|
|
theta = Tex(R"\theta", font_size=36)
|
|
try:
|
|
arc_center = arc.pfp(0.5)
|
|
except Exception:
|
|
arc_center = arc.get_center()
|
|
theta.move_to(arc_center + 0.8 * (arc_center - line_center))
|
|
return VGroup(h_line, arc, theta)
|
|
|
|
|
|
class TrackTheAngleForFractal(TrackTheAngle):
|
|
def get_loop(self, n_iters=7):
|
|
triangle = Triangle()
|
|
triangle.set_height(4)
|
|
a, b, c = triangle.get_vertices()
|
|
snowflake = VMobject().set_points_as_corners(np.vstack([
|
|
self.get_koch_line_points(a, c, n_iters),
|
|
self.get_koch_line_points(c, b, n_iters),
|
|
self.get_koch_line_points(b, a, n_iters),
|
|
]))
|
|
snowflake.set_stroke(WHITE, 1)
|
|
|
|
return snowflake
|
|
|
|
def get_koch_line_points(self, start, end, n_iters=7):
|
|
"""
|
|
Return points for a Koch snowflake portion,
|
|
not including the end
|
|
"""
|
|
a, b, c, d = np.linspace(start, end, 4)
|
|
tip = b + rotate_vector(c - b, 60 * DEG)
|
|
if n_iters == 0:
|
|
return np.array([a, b, tip, c])
|
|
return np.vstack([
|
|
self.get_koch_line_points(a, b, n_iters - 1),
|
|
self.get_koch_line_points(b, tip, n_iters - 1),
|
|
self.get_koch_line_points(tip, c, n_iters - 1),
|
|
self.get_koch_line_points(c, d, n_iters - 1),
|
|
])
|
|
|
|
|
|
class KochZoom(TrackTheAngleForFractal):
|
|
def construct(self):
|
|
snowflake = self.get_loop(9)
|
|
self.add(snowflake)
|
|
self.play(
|
|
self.frame.animate.reorient(0, 0, 0, (-1.12, 1.0, 0.0), 0.23),
|
|
rate_func=bezier([0, 0, 1, 1, 1, 1, 1, 1]),
|
|
run_time=10
|
|
)
|
|
|
|
|
|
class MobiusStripsAndKleinBottlesIn4D(ConstructKleinBottle):
|
|
def construct(self):
|
|
# Add surfaces
|
|
four_d = Text("A certain\n4D space")
|
|
cloud = ThoughtBubble(four_d, bulge_radius=0.15)[0][-1]
|
|
cloud.add(four_d)
|
|
cloud.set_width(4)
|
|
cloud.to_edge(RIGHT).shift(2 * DOWN)
|
|
self.add(cloud)
|
|
|
|
strip = ParametricSurface(mobius_strip_func)
|
|
strip.rotate(45 * DEG, LEFT)
|
|
strip.set_width(3)
|
|
|
|
bottle = ParametricSurface(self.get_kelin_bottle_func())
|
|
bottle.rotate(30 * DEG, LEFT)
|
|
bottle.set_height(3)
|
|
bottle.always_sort_to_camera(self.camera)
|
|
|
|
surfaces = Group(strip, bottle)
|
|
surfaces.arrange(RIGHT, buff=1.0)
|
|
surfaces.set_width(5)
|
|
surfaces.to_corner(UR)
|
|
|
|
for surface in surfaces:
|
|
surface.set_color(BLUE_D, 0.5)
|
|
surface.set_shading(0.4, 0.3, 0)
|
|
mesh = SurfaceMesh(surface, resolution=(11, 51))
|
|
mesh.set_stroke(WHITE, 0.5, 0.2)
|
|
mesh.deactivate_depth_test()
|
|
|
|
arrow = Arrow(surface, cloud.get_top(), thickness=5)
|
|
|
|
self.play(
|
|
FadeIn(surface),
|
|
Write(mesh, stroke_width=0.5, run_time=2),
|
|
GrowArrow(arrow),
|
|
)
|
|
|
|
|
|
class MusicalIntervalsAsPairs(InteractiveScene):
|
|
def construct(self):
|
|
# Add piano
|
|
piano = Piano()[:39]
|
|
piano.center()
|
|
piano.set_width(FRAME_WIDTH)
|
|
piano.set_shading(0.2, 0.1, 0)
|
|
|
|
keys = piano[15:27]
|
|
key_labels = VGroup(map(Tex, [
|
|
R"C",
|
|
R"C^{\#}",
|
|
R"D",
|
|
R"D^{\#}",
|
|
R"E",
|
|
R"F",
|
|
R"F^{\#}",
|
|
R"G",
|
|
R"G^{\#}",
|
|
R"A",
|
|
R"A^{\#}",
|
|
R"B",
|
|
]))
|
|
key_labels.scale(0.6)
|
|
key_labels.set_stroke(WHITE, 1)
|
|
for key, label in zip(keys, key_labels):
|
|
label.next_to(key.get_bottom(), UP, buff=0.1)
|
|
|
|
self.add(piano)
|
|
|
|
# Highlight random key pairs
|
|
random.seed(0)
|
|
indices = list(range(12))
|
|
keys.save_state()
|
|
for _ in range(24):
|
|
i, j = random.sample(indices, 2)
|
|
keys[i].set_color(TEAL)
|
|
keys[j].set_color(TEAL)
|
|
self.add(key_labels[i])
|
|
self.add(key_labels[j])
|
|
self.wait(0.5)
|
|
# self.play_notes(i, j, 0.5) # Only used for screen recording
|
|
self.remove(key_labels)
|
|
keys.restore()
|
|
|
|
# Show the circle
|
|
circle = Circle(radius=3)
|
|
circle.set_stroke(WHITE, 3)
|
|
circle.flip(axis=UR)
|
|
|
|
key_labels.target = key_labels.generate_target()
|
|
dots = Group()
|
|
for label, alpha in zip(key_labels.target, np.arange(0, 1, 1 / 12)):
|
|
point = circle.pfp(alpha)
|
|
label.move_to(1.1 * point)
|
|
dots.add(GlowDot(point, color=TEAL, radius=0.3))
|
|
|
|
self.play(
|
|
FadeOut(piano[:15]),
|
|
keys.animate.set_opacity(0.5),
|
|
FadeOut(piano[27:]),
|
|
VFadeIn(key_labels),
|
|
)
|
|
self.remove(key_labels)
|
|
self.play(
|
|
ShowCreation(circle),
|
|
LaggedStart(
|
|
(FadeTransform(Group(key), dot)
|
|
for key, dot in zip(keys, dots)),
|
|
lag_ratio=0.1,
|
|
group_type=Group,
|
|
),
|
|
MoveToTarget(key_labels, lag_ratio=0.01),
|
|
run_time=2
|
|
)
|
|
|
|
# Show the random connections again
|
|
random.seed(0)
|
|
line = Line().set_stroke(TEAL, 3)
|
|
self.add(line)
|
|
|
|
for _ in range(24):
|
|
i, j = random.sample(indices, 2)
|
|
line.put_start_and_end_on(
|
|
dots[i].get_center(),
|
|
dots[j].get_center(),
|
|
)
|
|
self.wait(1 / 3)
|
|
|
|
def play_notes(self, i, j, duration=0.5, sample_rate=44100):
|
|
"""
|
|
Play two notes simultaneously, specified as half steps above middle C.
|
|
|
|
Parameters:
|
|
i (int): Half steps above middle C for first note
|
|
j (int): Half steps above middle C for second note
|
|
duration (float): Length of time to play in seconds
|
|
sample_rate (int): Number of samples per second
|
|
"""
|
|
import sounddevice as sd
|
|
# Middle C is 261.63 Hz
|
|
base_freq = 261.63
|
|
|
|
# Calculate frequencies using equal temperament formula
|
|
freq1 = base_freq * (2 ** (i / 12))
|
|
freq2 = base_freq * (2 ** (j / 12))
|
|
|
|
# Generate time array
|
|
t = np.linspace(0, duration, int(sample_rate * duration), False)
|
|
|
|
# Generate sine waves for each note
|
|
note1 = np.sin(2 * np.pi * freq1 * t)
|
|
note2 = np.sin(2 * np.pi * freq2 * t)
|
|
|
|
# Combine notes and normalize
|
|
combined = (note1 + note2) / 2
|
|
|
|
# Play the sound
|
|
sd.play(combined, sample_rate)
|
|
sd.wait() # Wait until the sound has finished playing
|