3b1b-manim/old_projects/lost_lecture.py
2019-01-15 12:20:43 -08:00

4286 lines
129 KiB
Python

from big_ol_pile_of_manim_imports import *
from old_projects.div_curl import VectorField
from old_projects.div_curl import get_force_field_func
COBALT = "#0047AB"
class Orbiting(ContinualAnimation):
CONFIG = {
"rate": 7.5,
}
def __init__(self, planet, star, ellipse, **kwargs):
self.planet = planet
self.star = star
self.ellipse = ellipse
# Proportion of the way around the ellipse
self.proportion = 0
planet.move_to(ellipse.point_from_proportion(0))
ContinualAnimation.__init__(self, planet, **kwargs)
def update_mobject(self, dt):
# time = self.internal_time
planet = self.planet
star = self.star
ellipse = self.ellipse
rate = self.rate
radius_vector = planet.get_center() - star.get_center()
rate *= 1.0 / get_norm(radius_vector)
prop = self.proportion
d_prop = 0.001
ds = get_norm(op.add(
ellipse.point_from_proportion((prop + d_prop) % 1),
-ellipse.point_from_proportion(prop),
))
delta_prop = (d_prop / ds) * rate * dt
self.proportion = (self.proportion + delta_prop) % 1
planet.move_to(
ellipse.point_from_proportion(self.proportion)
)
class SunAnimation(ContinualAnimation):
CONFIG = {
"rate": 0.2,
"angle": 60 * DEGREES,
}
def __init__(self, sun, **kwargs):
self.sun = sun
self.rotated_sun = sun.deepcopy()
self.rotated_sun.rotate(60 * DEGREES)
ContinualAnimation.__init__(
self, Group(sun, self.rotated_sun), **kwargs
)
def update_mobject(self, dt):
time = self.internal_time
a = (np.sin(self.rate * time * TAU) + 1) / 2.0
self.rotated_sun.rotate(-self.angle)
self.rotated_sun.move_to(self.sun)
self.rotated_sun.rotate(self.angle)
self.rotated_sun.pixel_array = np.array(
a * self.sun.pixel_array,
dtype=self.sun.pixel_array.dtype
)
class ShowWord(Animation):
CONFIG = {
"time_per_char": 0.06,
"rate_func": None,
}
def __init__(self, word, **kwargs):
assert(isinstance(word, SingleStringTexMobject))
digest_config(self, kwargs)
run_time = kwargs.pop(
"run_time",
self.time_per_char * len(word)
)
self.stroke_width = word.get_stroke_width()
Animation.__init__(self, word, run_time=run_time, **kwargs)
def update_mobject(self, alpha):
word = self.mobject
stroke_width = self.stroke_width
count = int(alpha * len(word))
remainder = (alpha * len(word)) % 1
word[:count].set_fill(opacity=1)
word[:count].set_stroke(width=stroke_width)
if count < len(word):
word[count].set_fill(opacity=remainder)
word[count].set_stroke(width=remainder * stroke_width)
word[count + 1:].set_fill(opacity=0)
word[count + 1:].set_stroke(width=0)
# Animations
class TakeOver(PiCreatureScene):
CONFIG = {
"default_pi_creature_kwargs": {
"color": GREY_BROWN,
"flip_at_start": True,
},
"default_pi_creature_start_corner": DR,
}
def construct(self):
gradient = ImageMobject("white_black_gradient")
gradient.set_height(FRAME_HEIGHT)
self.add(gradient)
morty = self.pi_creatures
henry = ImageMobject("Henry_As_Stick")
henry.set_height(4)
henry.to_edge(LEFT)
henry.to_edge(DOWN)
self.add(morty, henry)
self.pi_creature_says(
"Muahaha! All \\\\ mine now.",
bubble_kwargs={"fill_opacity": 0.5},
bubble_creation_class=FadeIn,
target_mode="conniving",
added_anims=[henry.rotate, 5 * DEGREES]
)
self.wait(2)
class ShowEmergingEllipse(Scene):
CONFIG = {
"circle_radius": 3,
"circle_color": BLUE,
"num_lines": 150,
"lines_stroke_width": 1,
"eccentricity_vector": 2 * RIGHT,
"ghost_lines_stroke_color": LIGHT_GREY,
"ghost_lines_stroke_width": 0.5,
"ellipse_color": PINK,
}
def construct(self):
circle = self.get_circle()
e_point = self.get_eccentricity_point()
e_dot = Dot(e_point, color=YELLOW)
lines = self.get_lines()
ellipse = self.get_ellipse()
fade_rect = FullScreenFadeRectangle()
line = lines[len(lines) // 5]
line_dot = Dot(line.get_center(), color=YELLOW)
line_dot.scale(0.5)
ghost_line = self.get_ghost_lines(line)
ghost_lines = self.get_ghost_lines(lines)
rot_words = TextMobject("Rotate $90^\\circ$ \\\\ about center")
rot_words.next_to(line_dot, RIGHT)
elbow = self.get_elbow(line)
eccentric_words = TextMobject("``Eccentric'' point")
eccentric_words.next_to(circle.get_center(), DOWN)
ellipse_words = TextMobject("Perfect ellipse")
ellipse_words.next_to(ellipse, UP, SMALL_BUFF)
for text in rot_words, ellipse_words:
text.add_to_back(text.copy().set_stroke(BLACK, 5))
shuffled_lines = VGroup(*lines)
random.shuffle(shuffled_lines.submobjects)
self.play(ShowCreation(circle))
self.play(
FadeInAndShiftFromDirection(e_dot, LEFT),
Write(eccentric_words, run_time=1)
)
self.wait()
self.play(
LaggedStart(ShowCreation, shuffled_lines),
Animation(VGroup(e_dot, circle)),
FadeOut(eccentric_words)
)
self.add(ghost_lines)
self.add(e_dot, circle)
self.wait()
self.play(
FadeIn(fade_rect),
Animation(line),
GrowFromCenter(line_dot),
FadeInFromDown(rot_words),
)
self.wait()
self.add(ghost_line)
self.play(
MoveToTarget(line, path_arc=90 * DEGREES),
Animation(rot_words),
ShowCreation(elbow)
)
self.wait()
self.play(
FadeOut(fade_rect),
FadeOut(line_dot),
FadeOut(rot_words),
FadeOut(elbow),
Animation(line),
Animation(ghost_line)
)
self.play(
LaggedStart(MoveToTarget, lines, run_time=4),
Animation(VGroup(e_dot, circle))
)
self.wait()
self.play(
ShowCreation(ellipse),
FadeInFromDown(ellipse_words)
)
self.wait()
def get_circle(self):
circle = self.circle = Circle(
radius=self.circle_radius,
color=self.circle_color
)
return circle
def get_eccentricity_point(self):
return self.circle.get_center() + self.eccentricity_vector
def get_lines(self):
center = self.circle.get_center()
radius = self.circle.get_width() / 2
e_point = self.get_eccentricity_point()
lines = VGroup(*[
Line(
e_point,
center + rotate_vector(radius * RIGHT, angle)
)
for angle in np.linspace(0, TAU, self.num_lines)
])
lines.set_stroke(width=self.lines_stroke_width)
for line in lines:
line.generate_target()
line.target.rotate(90 * DEGREES)
return lines
def get_ghost_lines(self, lines):
return lines.copy().set_stroke(
color=self.ghost_lines_stroke_color,
width=self.ghost_lines_stroke_width
)
def get_elbow(self, line):
elbow = VGroup(Line(UP, UL), Line(UL, LEFT))
elbow.set_stroke(width=1)
elbow.scale(0.2, about_point=ORIGIN)
elbow.rotate(
line.get_angle() - 90 * DEGREES,
about_point=ORIGIN
)
elbow.shift(line.get_center())
return elbow
def get_ellipse(self):
center = self.circle.get_center()
e_point = self.get_eccentricity_point()
radius = self.circle.get_width() / 2
# Ellipse parameters
a = radius / 2
c = get_norm(e_point - center) / 2
b = np.sqrt(a**2 - c**2)
result = Circle(radius=b, color=self.ellipse_color)
result.stretch(a / b, 0)
result.move_to(Line(center, e_point))
return result
class ShowFullStory(Scene):
def construct(self):
directory = os.path.join(
MEDIA_DIR,
"animations/active_projects/lost_lecture/images"
)
scene_names = [
"ShowEmergingEllipse",
"ShowFullStory",
"FeynmanFameStart",
"TheMotionOfPlanets",
"FeynmanElementaryQuote",
"DrawingEllipse",
"ShowEllipseDefiningProperty",
"ProveEllipse",
"KeplersSecondLaw",
"AngularMomentumArgument",
"HistoryOfAngularMomentum",
"FeynmanRecountingNewton",
"IntroduceShapeOfVelocities",
"ShowEqualAngleSlices",
"PonderOverOffCenterDiagram",
"UseVelocityDiagramToDeduceCurve",
]
images = Group(*[
ImageMobject(os.path.join(directory, name + ".png"))
for name in scene_names
])
for image in images:
image.add(
SurroundingRectangle(image, buff=0, color=WHITE)
)
images.arrange_submobjects_in_grid(n_rows=4)
images.scale(
1.01 * FRAME_WIDTH / images[0].get_width()
)
images.shift(-images[0].get_center())
self.play(
images.set_width, FRAME_WIDTH - 1,
images.center,
run_time=3,
)
self.wait()
self.play(
images.shift, -images[2].get_center(),
images.scale, FRAME_WIDTH / images[2].get_width(),
{"about_point": ORIGIN},
run_time=3,
)
self.wait()
class FeynmanAndOrbitingPlannetOnEllipseDiagram(ShowEmergingEllipse):
def construct(self):
circle = self.get_circle()
lines = self.get_lines()
ghost_lines = self.get_ghost_lines(lines)
for line in lines:
MoveToTarget(line).update(1)
ellipse = self.get_ellipse()
e_dot = Dot(self.get_eccentricity_point())
e_dot.set_color(YELLOW)
comet = ImageMobject("earth")
comet.set_width(0.3)
feynman = ImageMobject("Feynman")
feynman.set_height(6)
feynman.next_to(ORIGIN, LEFT)
feynman.to_edge(UP)
feynman_name = TextMobject("Richard Feynman")
feynman_name.next_to(feynman, DOWN)
feynman.save_state()
feynman.shift(2 * DOWN)
feynman_rect = BackgroundRectangle(
feynman, fill_opacity=1
)
group = VGroup(circle, ghost_lines, lines, e_dot, ellipse)
self.add(group)
self.add(Orbiting(comet, e_dot, ellipse))
self.add_foreground_mobjects(comet)
self.wait()
self.play(
feynman.restore,
MaintainPositionRelativeTo(feynman_rect, feynman),
VFadeOut(feynman_rect),
group.to_edge, RIGHT,
)
self.play(Write(feynman_name))
self.wait()
self.wait(10)
class FeynmanFame(Scene):
def construct(self):
books = VGroup(
ImageMobject("Feynman_QED_cover"),
ImageMobject("Surely_Youre_Joking_cover"),
ImageMobject("Feynman_Lectures_cover"),
)
for book in books:
book.set_height(6)
book.move_to(FRAME_WIDTH * LEFT / 4)
feynman_diagram = self.get_feynman_diagram()
feynman_diagram.next_to(ORIGIN, RIGHT)
fd_parts = VGroup(*reversed(feynman_diagram.family_members_with_points()))
# As a physicist
self.play(self.get_book_intro(books[0]))
self.play(LaggedStart(
Write, feynman_diagram,
run_time=4
))
self.wait()
self.play(
self.get_book_intro(books[1]),
self.get_book_outro(books[0]),
LaggedStart(
ApplyMethod, fd_parts,
lambda m: (m.scale, 0),
run_time=1
),
)
self.remove(feynman_diagram)
self.wait()
# As a public figure
safe = SVGMobject(file_name="safe", height=2)
safe_rect = SurroundingRectangle(safe, buff=0)
safe_rect.set_stroke(width=0)
safe_rect.set_fill(DARK_GREY, 1)
safe.add_to_back(safe_rect)
bongo = SVGMobject(file_name="bongo")
bongo.set_height(1)
bongo.set_color(WHITE)
bongo.next_to(safe, RIGHT, LARGE_BUFF)
objects = VGroup(safe, bongo)
feynman_smile = ImageMobject("Feynman_Los_Alamos")
feynman_smile.set_height(4)
feynman_smile.next_to(objects, DOWN)
VGroup(objects, feynman_smile).next_to(ORIGIN, RIGHT)
joke = TextMobject(
"``Science is the belief \\\\ in the ignorance of \\\\ experts.''"
)
joke.move_to(objects)
self.play(LaggedStart(
DrawBorderThenFill, objects,
lag_ratio=0.75
))
self.play(self.get_book_intro(feynman_smile))
self.wait()
self.play(
objects.shift, 2 * UP,
VFadeOut(objects)
)
self.play(Write(joke))
self.wait(2)
self.play(
self.get_book_intro(books[2]),
self.get_book_outro(books[1]),
LaggedStart(FadeOut, joke, run_time=1),
ApplyMethod(
feynman_smile.shift, FRAME_HEIGHT * DOWN,
remover=True
)
)
# As a teacher
feynman_teacher = ImageMobject("Feynman_teaching")
feynman_teacher.set_width(FRAME_WIDTH / 2 - 1)
feynman_teacher.next_to(ORIGIN, RIGHT)
self.play(self.get_book_intro(feynman_teacher))
self.wait(3)
def get_book_animation(self, book,
initial_shift,
animated_shift,
opacity_func
):
rect = BackgroundRectangle(book, fill_opacity=1)
book.shift(initial_shift)
return AnimationGroup(
ApplyMethod(book.shift, animated_shift),
UpdateFromAlphaFunc(
rect, lambda r, a: r.move_to(book).set_fill(
opacity=opacity_func(a)
),
remover=True
)
)
def get_book_intro(self, book):
return self.get_book_animation(
book, 2 * DOWN, 2 * UP, lambda a: 1 - a
)
def get_book_outro(self, book):
return ApplyMethod(book.shift, FRAME_HEIGHT * UP, remover=True)
def get_feynman_diagram(self):
x_min = -1.5
x_max = 1.5
arrow = Arrow(LEFT, RIGHT, buff=0, use_rectangular_stem=False)
arrow.tip.move_to(arrow.get_center())
arrows = VGroup(*[
arrow.copy().rotate(angle).next_to(point, vect, buff=0)
for (angle, point, vect) in [
(-45 * DEGREES, x_min * RIGHT, UL),
(-135 * DEGREES, x_min * RIGHT, DL),
(-135 * DEGREES, x_max * RIGHT, UR),
(-45 * DEGREES, x_max * RIGHT, DR),
]
])
labels = VGroup(*[
TexMobject(tex)
for tex in ["e^-", "e^+", "\\text{\\=q}", "q"]
])
vects = [UR, DR, UL, DL]
for arrow, label, vect in zip(arrows, labels, vects):
label.next_to(arrow.get_center(), vect, buff=SMALL_BUFF)
wave = FunctionGraph(
lambda x: 0.2 * np.sin(2 * TAU * x),
x_min=x_min,
x_max=x_max,
)
wave_label = TexMobject("\\gamma")
wave_label.next_to(wave, UP, SMALL_BUFF)
labels.add(wave_label)
squiggle = ParametricFunction(
lambda t: np.array([
t + 0.5 * np.sin(TAU * t),
0.5 * np.cos(TAU * t),
0,
]),
t_min=0,
t_max=4,
)
squiggle.scale(0.25)
squiggle.set_color(BLUE)
squiggle.rotate(-30 * DEGREES)
squiggle.next_to(
arrows[2].point_from_proportion(0.75),
DR, buff=0
)
squiggle_label = TexMobject("g")
squiggle_label.next_to(squiggle, UR, buff=-MED_SMALL_BUFF)
labels.add(squiggle_label)
return VGroup(arrows, wave, squiggle, labels)
class FeynmanLecturesScreenCaptureFrame(Scene):
def construct(self):
url = TextMobject("http://www.feynmanlectures.caltech.edu/")
url.to_edge(UP)
screen_rect = ScreenRectangle(height=6)
screen_rect.next_to(url, DOWN)
self.add(url)
self.play(ShowCreation(screen_rect))
self.wait()
class TheMotionOfPlanets(Scene):
CONFIG = {
"camera_config": {"background_opacity": 1},
"random_seed": 2,
}
def construct(self):
self.add_title()
self.setup_orbits()
def add_title(self):
title = TextMobject("``The motion of planets around the sun''")
title.set_color(YELLOW)
title.to_edge(UP)
title.add_to_back(title.copy().set_stroke(BLACK, 5))
self.add(title)
self.title = title
def setup_orbits(self):
sun = ImageMobject("sun")
sun.set_height(0.7)
planets, ellipses, orbits = self.get_planets_ellipses_and_orbits(sun)
archivist_words = TextMobject(
"Judith Goodstein (Caltech archivist)"
)
archivist_words.to_corner(UL)
archivist_words.shift(1.5 * DOWN)
archivist_words.add_background_rectangle()
alt_name = TextMobject("David Goodstein (Caltech physicist)")
alt_name.next_to(archivist_words, DOWN, aligned_edge=LEFT)
alt_name.add_background_rectangle()
book = ImageMobject("Lost_Lecture_cover")
book.set_height(4)
book.next_to(alt_name, DOWN)
self.add(SunAnimation(sun))
self.add(ellipses, planets)
self.add(self.title)
self.add(*orbits)
self.add_foreground_mobjects(planets)
self.wait(10)
self.play(
VGroup(ellipses, sun).shift, 3 * RIGHT,
FadeInFromDown(archivist_words),
Animation(self.title)
)
self.add_foreground_mobjects(archivist_words)
self.wait(3)
self.play(FadeInFromDown(alt_name))
self.add_foreground_mobjects(alt_name)
self.wait()
self.play(FadeInFromDown(book))
self.wait(15)
def get_planets_ellipses_and_orbits(self, sun):
planets = VGroup(
ImageMobject("mercury"),
ImageMobject("venus"),
ImageMobject("earth"),
ImageMobject("mars"),
ImageMobject("comet")
)
sizes = [0.383, 0.95, 1.0, 0.532, 0.3]
orbit_radii = [0.254, 0.475, 0.656, 1.0, 3.0]
orbit_eccentricies = [0.206, 0.006, 0.0167, 0.0934, 0.967]
for planet, size in zip(planets, sizes):
planet.set_height(0.5)
planet.scale(size)
ellipses = VGroup(*[
Circle(radius=r, color=WHITE, stroke_width=1)
for r in orbit_radii
])
for circle, ec in zip(ellipses, orbit_eccentricies):
a = circle.get_height() / 2
c = ec * a
b = np.sqrt(a**2 - c**2)
circle.stretch(b / a, 1)
c = np.sqrt(a**2 - b**2)
circle.shift(c * RIGHT)
for circle in ellipses:
circle.rotate(
TAU * np.random.random(),
about_point=ORIGIN
)
ellipses.scale(3.5, about_point=ORIGIN)
orbits = [
Orbiting(
planet, sun, circle,
rate=0.25 * r**(2 / 3)
)
for planet, circle, r in zip(planets, ellipses, orbit_radii)
]
orbits[-1].proportion = 0.15
orbits[-1].rate = 0.5
return planets, ellipses, orbits
class TeacherHoldingUp(TeacherStudentsScene):
def construct(self):
self.play(
self.teacher.change, "raise_right_hand"
)
self.change_all_student_modes("pondering")
self.look_at(ORIGIN)
self.wait(5)
class AskAboutEllipses(TheMotionOfPlanets):
CONFIG = {
"camera_config": {"background_opacity": 1},
"sun_height": 0.5,
"sun_center": ORIGIN,
"animate_sun": True,
"a": 3.5,
"b": 2.0,
"ellipse_color": WHITE,
"ellipse_stroke_width": 1,
"comet_height": 0.2,
}
def construct(self):
self.add_title()
self.add_sun()
self.add_orbit()
self.add_focus_lines()
self.add_force_labels()
self.comment_on_imperfections()
self.set_up_differential_equations()
def add_title(self):
title = Title("Why are orbits ellipses?")
self.add(title)
self.title = title
def add_sun(self):
sun = ImageMobject("sun", height=self.sun_height)
sun.move_to(self.sun_center)
self.sun = sun
self.add(sun)
if self.animate_sun:
sun_animation = SunAnimation(sun)
self.add(sun_animation)
self.add_foreground_mobjects(
sun_animation.mobject
)
else:
self.add_foreground_mobjects(sun)
def add_orbit(self):
sun = self.sun
comet = self.get_comet()
ellipse = self.get_ellipse()
orbit = Orbiting(comet, sun, ellipse)
self.add(ellipse)
self.add(orbit)
self.ellipse = ellipse
self.comet = comet
self.orbit = orbit
def add_focus_lines(self):
f1, f2 = self.focus_points
comet = self.comet
lines = VGroup(Line(LEFT, RIGHT), Line(LEFT, RIGHT))
lines.set_stroke(LIGHT_GREY, 1)
def update_lines(lines):
l1, l2 = lines
P = comet.get_center()
l1.put_start_and_end_on(f1, P)
l2.put_start_and_end_on(f2, P)
return lines
animation = ContinualUpdate(
lines, update_lines
)
self.add(animation)
self.wait(8)
self.focus_lines = lines
self.focus_lines_animation = animation
def add_force_labels(self):
radial_line = self.focus_lines[0]
# Radial line measurement
radius_measurement_kwargs = {
"num_decimal_places": 3,
"color": BLUE,
}
radius_measurement = DecimalNumber(1, **radius_measurement_kwargs)
def update_radial_measurement(measurement):
angle = -radial_line.get_angle() + np.pi
radial_line.rotate(angle, about_point=ORIGIN)
new_decimal = DecimalNumber(
radial_line.get_length(),
**radius_measurement_kwargs
)
max_width = 0.6 * radial_line.get_width()
if new_decimal.get_width() > max_width:
new_decimal.set_width(max_width)
new_decimal.next_to(radial_line, UP, SMALL_BUFF)
VGroup(new_decimal, radial_line).rotate(
-angle, about_point=ORIGIN
)
Transform(measurement, new_decimal).update(1)
radius_measurement_animation = ContinualUpdate(
radius_measurement, update_radial_measurement
)
# Force equation
force_equation = TexMobject(
"F = {GMm \\over (0.000)^2}",
tex_to_color_map={
"F": YELLOW,
"0.000": BLACK,
}
)
force_equation.next_to(self.title, DOWN)
force_equation.to_edge(RIGHT)
radius_in_denominator_ref = force_equation.get_part_by_tex("0.000")
radius_in_denominator = DecimalNumber(
0, **radius_measurement_kwargs
)
radius_in_denominator.scale(0.95)
update_radius_in_denominator = ContinualChangingDecimal(
radius_in_denominator,
lambda a: radial_line.get_length(),
position_update_func=lambda mob: mob.move_to(
radius_in_denominator_ref, LEFT
)
)
# Force arrow
force_arrow, force_arrow_animation = self.get_force_arrow_and_update(
self.comet
)
inverse_square_law_words = TextMobject(
"``Inverse square law''"
)
inverse_square_law_words.next_to(force_equation, DOWN, MED_LARGE_BUFF)
inverse_square_law_words.to_edge(RIGHT)
force_equation.next_to(inverse_square_law_words, UP, MED_LARGE_BUFF)
def v_fade_in(mobject):
return UpdateFromAlphaFunc(
mobject,
lambda mob, alpha: mob.set_fill(opacity=alpha)
)
self.add(update_radius_in_denominator)
self.add(radius_measurement_animation)
self.play(
FadeIn(force_equation),
v_fade_in(radius_in_denominator),
v_fade_in(radius_measurement)
)
self.add(force_arrow_animation)
self.play(v_fade_in(force_arrow))
self.wait(8)
self.play(Write(inverse_square_law_words))
self.wait(9)
self.force_equation = force_equation
self.inverse_square_law_words = inverse_square_law_words
self.force_arrow = force_arrow
self.radius_measurement = radius_measurement
def comment_on_imperfections(self):
planets, ellipses, orbits = self.get_planets_ellipses_and_orbits(self.sun)
orbits.pop(-1)
ellipses.submobjects.pop(-1)
planets.submobjects.pop(-1)
scale_factor = 20
center = self.sun.get_center()
ellipses.save_state()
ellipses.scale(scale_factor, about_point=center)
self.add(*orbits)
self.play(ellipses.restore, Animation(planets))
self.wait(7)
self.play(
ellipses.scale, scale_factor, {"about_point": center},
Animation(planets)
)
self.remove(*orbits)
self.remove(planets, ellipses)
self.wait(2)
def set_up_differential_equations(self):
d_dt = TexMobject("{d \\over dt}")
in_vect = Matrix(np.array([
"x(t)",
"y(t)",
"\\dot{x}(t)",
"\\dot{y}(t)",
]))
equals = TexMobject("=")
out_vect = Matrix(np.array([
"\\dot{x}(t)",
"\\dot{y}(t)",
"-x(t) / (x(t)^2 + y(t)^2)^{3/2}",
"-y(t) / (x(t)^2 + y(t)^2)^{3/2}",
]), element_alignment_corner=ORIGIN)
equation = VGroup(d_dt, in_vect, equals, out_vect)
equation.arrange_submobjects(RIGHT, buff=SMALL_BUFF)
equation.set_width(6)
equation.to_corner(DR, buff=MED_LARGE_BUFF)
cross = Cross(equation)
self.play(Write(equation))
self.wait(6)
self.play(ShowCreation(cross))
self.wait(6)
# Helpers
def get_comet(self):
comet = ImageMobject("comet")
comet.set_height(self.comet_height)
return comet
def get_ellipse(self):
a = self.a
b = self.b
c = np.sqrt(a**2 - b**2)
ellipse = Circle(radius=a)
ellipse.set_stroke(
self.ellipse_color,
self.ellipse_stroke_width,
)
ellipse.stretch(fdiv(b, a), dim=1)
ellipse.move_to(
self.sun.get_center() + c * LEFT,
)
self.focus_points = [
self.sun.get_center(),
self.sun.get_center() + 2 * c * LEFT,
]
return ellipse
def get_force_arrow_and_update(self, comet, scale_factor=1):
force_arrow = Arrow(LEFT, RIGHT, color=YELLOW)
sun = self.sun
def update_force_arrow(arrow):
radial_line = Line(
sun.get_center(), comet.get_center()
)
radius = radial_line.get_length()
# target_length = 1 / radius**2
target_length = scale_factor / radius # Lies!
arrow.scale(
target_length / arrow.get_length()
)
arrow.rotate(
np.pi + radial_line.get_angle() - arrow.get_angle()
)
arrow.shift(
radial_line.get_end() - arrow.get_start()
)
force_arrow_animation = ContinualUpdate(
force_arrow, update_force_arrow
)
return force_arrow, force_arrow_animation
def get_radial_line_and_update(self, comet):
line = Line(LEFT, RIGHT)
line.set_stroke(LIGHT_GREY, 1)
line_update = ContinualUpdate(
line, lambda l: l.put_start_and_end_on(
self.sun.get_center(),
comet.get_center(),
)
)
return line, line_update
class FeynmanSaysItBest(TeacherStudentsScene):
def construct(self):
self.teacher_says(
"Feynman says \\\\ it best",
added_anims=[
self.get_student_changes(
"hooray", "happy", "erm"
)
]
)
self.wait(3)
class FeynmanElementaryQuote(Scene):
def construct(self):
quote_text = """
\\large
I am going to give what I will call an
\\emph{elementary} demonstration. But elementary
does not mean easy to understand. Elementary
means that very little is required
to know ahead of time in order to understand it,
except to have an infinite amount of intelligence.
"""
quote_parts = [s for s in quote_text.split(" ") if s]
quote = TextMobject(
*quote_parts,
tex_to_color_map={
"\\emph{elementary}": BLUE,
"elementary": BLUE,
"Elementary": BLUE,
"infinite": YELLOW,
"amount": YELLOW,
"of": YELLOW,
"intelligence": YELLOW,
"very": RED,
"little": RED,
},
alignment=""
)
quote[-1].shift(2 * SMALL_BUFF * LEFT)
quote.set_width(FRAME_WIDTH - 1)
quote.to_edge(UP)
quote.get_part_by_tex("of").set_color(WHITE)
nothing = TextMobject("nothing")
nothing.scale(0.9)
very = quote.get_part_by_tex("very")
nothing.shift(very[0].get_left() - nothing[0].get_left())
nothing.set_color(RED)
for word in quote:
if word is very:
self.add_foreground_mobjects(nothing)
self.play(ShowWord(nothing))
self.wait(0.2)
nothing.sort_submobjects(lambda p: -p[0])
self.play(LaggedStart(
FadeOut, nothing,
run_time=1
))
self.remove_foreground_mobject(nothing)
back_word = word.copy().set_stroke(BLACK, 5)
self.add_foreground_mobjects(back_word, word)
self.play(
ShowWord(back_word),
ShowWord(word),
)
self.wait(0.005 * len(word)**1.5)
self.wait()
# Show thumbnails
images = Group(
ImageMobject("Calculus_Thumbnail"),
ImageMobject("Fourier_Thumbnail"),
)
for image in images:
image.set_height(3)
images.arrange_submobjects(RIGHT, buff=LARGE_BUFF)
images.to_edge(DOWN, buff=LARGE_BUFF)
images[1].move_to(images[0])
crosses = VGroup(*list(map(Cross, images)))
crosses.set_stroke("RED", 10)
for image, cross in zip(images, crosses):
image.rect = SurroundingRectangle(
image,
stroke_width=3,
stroke_color=WHITE,
buff=0
)
cross.scale(1.1)
self.play(
FadeInFromDown(images[0]),
FadeInFromDown(images[0].rect)
)
self.play(ShowCreation(crosses[0]))
self.wait()
self.play(
FadeOutAndShiftDown(images[0]),
FadeOutAndShiftDown(images[0].rect),
FadeOutAndShiftDown(crosses[0]),
FadeInFromDown(images[1]),
FadeInFromDown(images[1].rect),
)
self.play(ShowCreation(crosses[1]))
self.wait()
class LostLecturePicture(TODOStub):
CONFIG = {"camera_config": {"background_opacity": 1}}
def construct(self):
picture = ImageMobject("Feynman_teaching")
picture.set_height(FRAME_WIDTH)
picture.to_corner(UL, buff=0)
picture.fade(0.5)
self.play(
picture.to_corner, DR, {"buff": 0},
picture.shift, 1.5 * DOWN,
path_arc=60 * DEGREES,
run_time=20,
rate_func=bezier([0, 0, 1, 1])
)
class AskAboutInfiniteIntelligence(TeacherStudentsScene):
def construct(self):
self.student_says(
"Infinite intelligence?",
target_mode="confused"
)
self.play(
self.get_student_changes("horrified", "confused", "sad"),
self.teacher.change, "happy",
)
self.wait()
self.teacher_says(
"Stay focused, \\\\ go full screen, \\\\ and you'll be fine.",
added_anims=[self.get_student_changes(*["happy"] * 3)]
)
self.wait()
self.look_at(self.screen)
self.wait(5)
class TableOfContents(Scene):
def construct(self):
items = VGroup(
TextMobject("How the ellipse will arise"),
TextMobject("Kepler's 2nd law"),
TextMobject("The shape of velocities"),
)
items.arrange_submobjects(
DOWN, buff=LARGE_BUFF, aligned_edge=LEFT
)
items.to_edge(LEFT, buff=1.5)
for item in items:
item.add(Dot().next_to(item, LEFT))
item.generate_target()
item.target.set_fill(GREY, opacity=0.5)
title = Title("The plan")
scale_factor = 1.2
self.add(title)
self.play(LaggedStart(
FadeIn, items,
run_time=1,
lag_ratio=0.7,
))
self.wait()
for item in items:
other_items = VGroup(*[m for m in items if m is not item])
new_item = item.copy()
new_item.scale(scale_factor, about_edge=LEFT)
new_item.set_fill(WHITE, 1)
self.play(
Transform(item, new_item),
*list(map(MoveToTarget, other_items))
)
self.wait()
class DrawEllipseOverlay(Scene):
def construct(self):
ellipse = Circle()
ellipse.stretch_to_fit_width(7.0)
ellipse.stretch_to_fit_height(3.8)
ellipse.shift(1.05 * UP + 0.48 * LEFT)
ellipse.set_stroke(RED, 8)
image = ImageMobject(
os.path.join(
get_image_output_directory(self.__class__),
"HeldUpEllipse.jpg"
)
)
image.set_height(FRAME_HEIGHT)
# self.add(image)
self.play(ShowCreation(ellipse))
self.wait()
self.play(FadeOut(ellipse))
class ShowEllipseDefiningProperty(Scene):
CONFIG = {
"camera_config": {"background_opacity": 1},
"ellipse_color": BLUE,
"a": 4.0,
"b": 3.0,
"distance_labels_scale_factor": 1.0,
}
def construct(self):
self.add_ellipse()
self.add_focal_lines()
self.add_distance_labels()
self.label_foci()
self.label_focal_sum()
def add_ellipse(self):
a = self.a
b = self.b
ellipse = Circle(radius=a, color=self.ellipse_color)
ellipse.stretch(fdiv(b, a), dim=1)
ellipse.to_edge(LEFT, buff=LARGE_BUFF)
self.ellipse = ellipse
self.add(ellipse)
def add_focal_lines(self):
push_pins = VGroup(*[
SVGMobject(
file_name="push_pin",
color=LIGHT_GREY,
fill_opacity=0.8,
height=0.5,
).move_to(point, DR).shift(0.05 * RIGHT)
for point in self.get_foci()
])
dot = Dot()
dot.scale(0.5)
position_tracker = ValueTracker(0.125)
dot_update = ContinualUpdate(
dot,
lambda d: d.move_to(
self.ellipse.point_from_proportion(
position_tracker.get_value() % 1
)
)
)
position_tracker_wander = ContinualMovement(
position_tracker, rate=0.05,
)
lines, lines_update_animation = self.get_focal_lines_and_update(
self.get_foci, dot
)
self.add_foreground_mobjects(push_pins, dot)
self.add(dot_update)
self.play(LaggedStart(
FadeInAndShiftFromDirection, push_pins,
lambda m: (m, 2 * UP + LEFT),
run_time=1,
lag_ratio=0.75
))
self.play(ShowCreation(lines))
self.add(lines_update_animation)
self.add(position_tracker_wander)
self.wait(2)
self.position_tracker = position_tracker
self.focal_lines = lines
def add_distance_labels(self):
lines = self.focal_lines
colors = [YELLOW, PINK]
distance_labels, distance_labels_animation = \
self.get_distance_labels_and_update(lines, colors)
sum_expression, numbers, number_updates = \
self.get_sum_expression_and_update(
lines, colors, lambda mob: mob.to_corner(UR)
)
sum_expression_fading_rect = BackgroundRectangle(
sum_expression, fill_opacity=1
)
sum_rect = SurroundingRectangle(numbers[-1])
constant_words = TextMobject("Stays constant")
constant_words.next_to(sum_rect, DOWN, aligned_edge=RIGHT)
VGroup(sum_rect, constant_words).set_color(BLUE)
self.add(distance_labels_animation)
self.add(*number_updates)
self.add(sum_expression)
self.add_foreground_mobjects(sum_expression_fading_rect)
self.play(
VFadeIn(distance_labels),
FadeOut(sum_expression_fading_rect),
)
self.remove_foreground_mobject(sum_expression_fading_rect)
self.wait(7)
self.play(
ShowCreation(sum_rect),
Write(constant_words)
)
self.wait(7)
self.play(FadeOut(sum_rect), FadeOut(constant_words))
self.sum_expression = sum_expression
self.sum_rect = sum_rect
def label_foci(self):
foci = self.get_foci()
focus_words = VGroup(*[
TextMobject("Focus").next_to(focus, DOWN)
for focus in foci
])
foci_word = TextMobject("Foci")
foci_word.move_to(focus_words)
foci_word.shift(MED_SMALL_BUFF * UP)
connecting_lines = VGroup(*[
Arrow(
foci_word.get_edge_center(-edge),
focus_word.get_edge_center(edge),
buff=MED_SMALL_BUFF,
stroke_width=2,
)
for focus_word, edge in zip(focus_words, [LEFT, RIGHT])
])
translation = TextMobject(
"``Foco'' $\\rightarrow$ Fireplace"
)
translation.to_edge(RIGHT)
translation.shift(UP)
sun = ImageMobject("sun", height=0.5)
sun.move_to(foci[0])
sun_animation = SunAnimation(sun)
self.play(FadeInFromDown(focus_words))
self.wait(2)
self.play(
ReplacementTransform(focus_words.copy(), foci_word),
)
self.play(*list(map(ShowCreation, connecting_lines)))
for word in list(focus_words) + [foci_word]:
word.add_background_rectangle()
self.add_foreground_mobjects(word)
self.wait(4)
self.play(Write(translation))
self.wait(2)
self.play(GrowFromCenter(sun))
self.add(sun_animation)
self.wait(8)
def label_focal_sum(self):
sum_rect = self.sum_rect
focal_sum = TextMobject("``Focal sum''")
focal_sum.scale(1.5)
focal_sum.next_to(sum_rect, DOWN, aligned_edge=RIGHT)
VGroup(sum_rect, focal_sum).set_color(RED)
footnote = TextMobject(
"""
\\Large
*This happens to equal the longest distance
across the ellipse, so perhaps the more standard
terminology would be ``major axis'', but I want
some terminology that conveys the idea of adding
two distances to the foci.
""",
alignment="",
)
footnote.set_width(5)
footnote.to_corner(DR)
footnote.set_stroke(WHITE, 0.5)
self.play(FadeInFromDown(focal_sum))
self.play(Write(sum_rect))
self.wait()
self.play(FadeIn(footnote))
self.wait(2)
self.play(FadeOut(footnote))
self.wait(8)
# Helpers
def get_foci(self):
ellipse = self.ellipse
a = ellipse.get_width() / 2
b = ellipse.get_height() / 2
c = np.sqrt(a**2 - b**2)
center = ellipse.get_center()
return [
center + c * RIGHT,
center + c * LEFT,
]
def get_focal_lines_and_update(self, get_foci, focal_sum_point):
lines = VGroup(Line(LEFT, RIGHT), Line(LEFT, RIGHT))
lines.set_stroke(width=2)
def update_lines(lines):
foci = get_foci()
for line, focus in zip(lines, foci):
line.put_start_and_end_on(
focus, focal_sum_point.get_center()
)
lines[1].rotate(np.pi)
lines_update_animation = ContinualUpdate(
lines, update_lines
)
return lines, lines_update_animation
def get_distance_labels_and_update(self, lines, colors):
distance_labels = VGroup(
DecimalNumber(0), DecimalNumber(0),
)
for label in distance_labels:
label.scale(self.distance_labels_scale_factor)
def update_distance_labels(labels):
for label, line, color in zip(labels, lines, colors):
angle = -line.get_angle()
if np.abs(angle) > 90 * DEGREES:
angle = 180 * DEGREES + angle
line.rotate(angle, about_point=ORIGIN)
new_decimal = DecimalNumber(line.get_length())
new_decimal.scale(
self.distance_labels_scale_factor
)
max_width = 0.6 * line.get_width()
if new_decimal.get_width() > max_width:
new_decimal.set_width(max_width)
new_decimal.next_to(line, UP, SMALL_BUFF)
new_decimal.set_color(color)
new_decimal.add_to_back(
new_decimal.copy().set_stroke(BLACK, 5)
)
VGroup(new_decimal, line).rotate(
-angle, about_point=ORIGIN
)
label.submobjects = list(new_decimal.submobjects)
distance_labels_animation = ContinualUpdate(
distance_labels, update_distance_labels
)
return distance_labels, distance_labels_animation
def get_sum_expression_and_update(self, lines, colors, sum_position_func):
sum_expression = TexMobject("0.00", "+", "0.00", "=", "0.00")
sum_position_func(sum_expression)
number_refs = sum_expression.get_parts_by_tex("0.00")
number_refs.set_fill(opacity=0)
numbers = VGroup(*[DecimalNumber(0) for ref in number_refs])
for number, color in zip(numbers, colors):
number.set_color(color)
# Not the most elegant...
number_updates = [
ContinualChangingDecimal(
numbers[0], lambda a: lines[0].get_length(),
position_update_func=lambda m: m.move_to(
number_refs[1], LEFT
)
),
ContinualChangingDecimal(
numbers[1], lambda a: lines[1].get_length(),
position_update_func=lambda m: m.move_to(
number_refs[0], LEFT
)
),
ContinualChangingDecimal(
numbers[2], lambda a: sum(map(Line.get_length, lines)),
position_update_func=lambda m: m.move_to(
number_refs[2], LEFT
)
),
]
return sum_expression, numbers, number_updates
class GeometryProofLand(Scene):
CONFIG = {
"colors": [
PINK, RED, YELLOW, GREEN, GREEN_A, BLUE,
MAROON_E, MAROON_B, YELLOW, BLUE,
],
"text": "Geometry proof land",
}
def construct(self):
word = self.get_geometry_proof_land_word()
word_outlines = word.deepcopy()
word_outlines.set_fill(opacity=0)
word_outlines.set_stroke(WHITE, 1)
colors = list(self.colors)
random.shuffle(colors)
word_outlines.set_color_by_gradient(*colors)
word_outlines.set_stroke(width=5)
circles = VGroup()
for letter in word:
circle = Circle()
# circle = letter.copy()
circle.replace(letter, dim_to_match=1)
circle.scale(3)
circle.set_stroke(WHITE, 0)
circle.set_fill(letter.get_color(), 0)
circles.add(circle)
circle.target = letter
self.play(
LaggedStart(MoveToTarget, circles),
run_time=2
)
self.add(word_outlines, circles)
self.play(LaggedStart(
FadeIn, word_outlines,
run_time=3,
rate_func=there_and_back,
), Animation(circles))
self.wait()
def get_geometry_proof_land_word(self):
word = TextMobject(self.text)
word.rotate(-90 * DEGREES)
word.scale(0.25)
word.shift(3 * RIGHT)
word.apply_complex_function(np.exp)
word.rotate(90 * DEGREES)
word.set_width(9)
word.center()
word.to_edge(UP)
word.set_color_by_gradient(*self.colors)
word.set_background_stroke(width=0)
return word
class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty):
CONFIG = {
"eccentricity_vector": 1.5 * RIGHT,
"ellipse_color": PINK,
"distance_labels_scale_factor": 0.7,
}
def construct(self):
self.setup_ellipse()
self.hypothesize_foci()
self.setup_and_show_focal_sum()
self.show_circle_radius()
self.limit_to_just_one_line()
self.look_at_perpendicular_bisector()
self.show_orbiting_planet()
def setup_ellipse(self):
circle = self.circle = self.get_circle()
circle.to_edge(LEFT)
ep = self.get_eccentricity_point()
ep_dot = self.ep_dot = Dot(ep, color=YELLOW)
lines = self.lines = self.get_lines()
for line in lines:
line.save_state()
ghost_lines = self.ghost_lines = self.get_ghost_lines(lines)
ellipse = self.ellipse = self.get_ellipse()
self.add(ghost_lines, circle, lines, ep_dot)
self.play(
LaggedStart(MoveToTarget, lines),
Animation(ep_dot),
)
self.play(ShowCreation(ellipse))
self.wait()
def hypothesize_foci(self):
circle = self.circle
ghost_lines = self.ghost_lines
ghost_lines_copy = ghost_lines.copy().set_stroke(YELLOW, 3)
center = circle.get_center()
center_dot = Dot(center, color=RED)
# ep = self.get_eccentricity_point()
ep_dot = self.ep_dot
dots = VGroup(center_dot, ep_dot)
center_label = TextMobject("Circle center")
ep_label = TextMobject("Eccentric point")
labels = VGroup(center_label, ep_label)
vects = [UL, DR]
arrows = VGroup()
for label, dot, vect in zip(labels, dots, vects):
label.next_to(dot, vect, MED_LARGE_BUFF)
label.match_color(dot)
label.add_to_back(
label.copy().set_stroke(BLACK, 5)
)
arrow = Arrow(
label.get_corner(-vect),
dot.get_corner(vect),
buff=SMALL_BUFF
)
arrow.match_color(dot)
arrow.add_to_back(arrow.copy().set_stroke(BLACK, 5))
arrows.add(arrow)
labels_target = labels.copy()
labels_target.arrange_submobjects(
DOWN, aligned_edge=LEFT
)
guess_start = TextMobject("Guess: Foci = ")
brace = Brace(labels_target, LEFT)
full_guess = VGroup(guess_start, brace, labels_target)
full_guess.arrange_submobjects(RIGHT)
full_guess.to_corner(UR)
self.play(
FadeInFromDown(labels[1]),
GrowArrow(arrows[1]),
)
self.play(LaggedStart(
ShowPassingFlash, ghost_lines_copy
))
self.wait()
self.play(ReplacementTransform(circle.copy(), center_dot))
self.add_foreground_mobjects(dots)
self.play(
FadeInFromDown(labels[0]),
GrowArrow(arrows[0]),
)
self.wait()
self.play(
Write(guess_start),
GrowFromCenter(brace),
run_time=1
)
self.play(
ReplacementTransform(labels.copy(), labels_target)
)
self.wait()
self.play(FadeOut(labels), FadeOut(arrows))
self.center_dot = center_dot
def setup_and_show_focal_sum(self):
circle = self.circle
ellipse = self.ellipse
focal_sum_point = VectorizedPoint()
focal_sum_point.move_to(circle.get_top())
dots = [self.ep_dot, self.center_dot]
colors = list(map(Mobject.get_color, dots))
def get_foci():
return list(map(Mobject.get_center, dots))
focal_lines, focal_lines_update_animation = \
self.get_focal_lines_and_update(get_foci, focal_sum_point)
distance_labels, distance_labels_update_animation = \
self.get_distance_labels_and_update(focal_lines, colors)
sum_expression, numbers, number_updates = \
self.get_sum_expression_and_update(
focal_lines, colors,
lambda mob: mob.to_edge(RIGHT).shift(UP)
)
to_add = self.focal_sum_things_to_add = [
focal_lines_update_animation,
distance_labels_update_animation,
sum_expression,
] + list(number_updates)
self.play(
ShowCreation(focal_lines),
Write(distance_labels),
FadeIn(sum_expression),
Write(numbers),
run_time=1
)
self.wait()
self.add(*to_add)
points = [
ellipse.get_top(),
circle.point_from_proportion(0.2),
ellipse.point_from_proportion(0.2),
ellipse.point_from_proportion(0.4),
]
for point in points:
self.play(
focal_sum_point.move_to, point
)
self.wait()
self.remove(*to_add)
self.play(*list(map(FadeOut, [
focal_lines, distance_labels,
sum_expression, numbers
])))
self.set_variables_as_attrs(
focal_lines, focal_lines_update_animation,
focal_sum_point,
distance_labels, distance_labels_update_animation,
sum_expression,
numbers, number_updates
)
def show_circle_radius(self):
circle = self.circle
center = circle.get_center()
point = circle.get_right()
color = GREEN
radius = Line(center, point, color=color)
radius_measurement = DecimalNumber(radius.get_length())
radius_measurement.set_color(color)
radius_measurement.next_to(radius, UP, SMALL_BUFF)
radius_measurement.add_to_back(
radius_measurement.copy().set_stroke(BLACK, 5)
)
group = VGroup(radius, radius_measurement)
group.rotate(30 * DEGREES, about_point=center)
self.play(ShowCreation(radius))
self.play(Write(radius_measurement))
self.wait()
self.play(Rotating(
group,
rate_func=smooth,
run_time=7,
about_point=center
))
self.play(FadeOut(group))
def limit_to_just_one_line(self):
lines = self.lines
ghost_lines = self.ghost_lines
ep_dot = self.ep_dot
index = int(0.2 * len(lines))
line = lines[index]
ghost_line = ghost_lines[index]
to_fade = VGroup(*list(lines) + list(ghost_lines))
to_fade.remove(line, ghost_line)
P_dot = Dot(line.saved_state.get_end())
P_label = TexMobject("P")
P_label.next_to(P_dot, UP, SMALL_BUFF)
self.add_foreground_mobjects(self.ellipse)
self.play(LaggedStart(Restore, lines))
self.play(
FadeOut(to_fade),
ghost_line.set_stroke, YELLOW, 3,
line.set_stroke, WHITE, 3,
ReplacementTransform(ep_dot.copy(), P_dot),
)
self.play(FadeInFromDown(P_label))
self.wait()
for l in lines:
l.generate_target()
l.target.rotate(
90 * DEGREES,
about_point=l.get_center()
)
self.set_variables_as_attrs(
line, ghost_line,
P_dot, P_label
)
def look_at_perpendicular_bisector(self):
# Alright, this method's gonna blow up. Let's go!
circle = self.circle
ellipse = self.ellipse
ellipse.save_state()
lines = self.lines
line = self.line
ghost_lines = self.ghost_lines
ghost_line = self.ghost_line
P_dot = self.P_dot
P_label = self.P_label
elbow = self.get_elbow(line)
self.play(
MoveToTarget(line, path_arc=90 * DEGREES),
ShowCreation(elbow)
)
# Perpendicular bisector label
label = TextMobject("``Perpendicular bisector''")
label.scale(0.75)
label.set_color(YELLOW)
label.next_to(ORIGIN, UP, MED_SMALL_BUFF)
label.add_background_rectangle()
angle = line.get_angle() + np.pi
label.rotate(angle, about_point=ORIGIN)
label.shift(line.get_center())
# Dot defining Q point
Q_dot = Dot(color=GREEN)
Q_dot.move_to(self.focal_sum_point)
focal_sum_point_animation = NormalAnimationAsContinualAnimation(
MaintainPositionRelativeTo(
self.focal_sum_point, Q_dot
)
)
self.add(focal_sum_point_animation)
Q_dot.move_to(line.point_from_proportion(0.9))
Q_dot.save_state()
Q_label = TexMobject("Q")
Q_label.scale(0.7)
Q_label.match_color(Q_dot)
Q_label.add_to_back(Q_label.copy().set_stroke(BLACK, 5))
Q_label.next_to(Q_dot, UL, buff=0)
Q_label_animation = NormalAnimationAsContinualAnimation(
MaintainPositionRelativeTo(Q_label, Q_dot)
)
# Pretty hacky...
def distance_label_shift_update(label):
line = self.focal_lines[0]
if line.get_end()[0] > line.get_start()[0]:
vect = label.get_center() - line.get_center()
label.shift(-2 * vect)
distance_label_shift_update_animation = ContinualUpdate(
self.distance_labels[0],
distance_label_shift_update
)
self.focal_sum_things_to_add.append(
distance_label_shift_update_animation
)
# Define QP line
QP_line = Line(LEFT, RIGHT)
QP_line.match_style(self.focal_lines)
QP_line_update = ContinualUpdate(
QP_line, lambda l: l.put_start_and_end_on(
Q_dot.get_center(), P_dot.get_center(),
)
)
QE_line = Line(LEFT, RIGHT)
QE_line.set_stroke(YELLOW, 3)
QE_line_update = ContinualUpdate(
QE_line, lambda l: l.put_start_and_end_on(
Q_dot.get_center(),
self.get_eccentricity_point()
)
)
# Define similar triangles
triangles = VGroup(*[
Polygon(
Q_dot.get_center(),
line.get_center(),
end_point,
fill_opacity=1,
)
for end_point in [
P_dot.get_center(),
self.get_eccentricity_point()
]
])
triangles.set_color_by_gradient(RED_C, COBALT)
triangles.set_stroke(WHITE, 2)
# Add even more distant label updates
def distance_label_rotate_update(label):
QE_line_update.update(0)
angle = QP_line.get_angle() - QE_line.get_angle()
label.rotate(angle, about_point=Q_dot.get_center())
return label
distance_label_rotate_update_animation = ContinualUpdate(
self.distance_labels[0],
distance_label_rotate_update
)
# Hook up line to P to P_dot
radial_line = DashedLine(ORIGIN, 3 * RIGHT)
radial_line_update = UpdateFromFunc(
radial_line, lambda l: l.put_start_and_end_on(
circle.get_center(),
P_dot.get_center()
)
)
def put_dot_at_intersection(dot):
point = line_intersection(
line.get_start_and_end(),
radial_line.get_start_and_end()
)
dot.move_to(point)
return dot
keep_Q_dot_at_intersection = UpdateFromFunc(
Q_dot, put_dot_at_intersection
)
Q_dot.restore()
ghost_line_update_animation = UpdateFromFunc(
ghost_line, lambda l: l.put_start_and_end_on(
self.get_eccentricity_point(),
P_dot.get_center()
)
)
def update_perp_bisector(line):
line.scale(ghost_line.get_length() / line.get_length())
line.rotate(ghost_line.get_angle() - line.get_angle())
line.rotate(90 * DEGREES)
line.move_to(ghost_line)
perp_bisector_update_animation = UpdateFromFunc(
line, update_perp_bisector
)
elbow_update_animation = UpdateFromFunc(
elbow,
lambda e: Transform(e, self.get_elbow(ghost_line)).update(1)
)
P_dot_movement_updates = [
radial_line_update,
keep_Q_dot_at_intersection,
MaintainPositionRelativeTo(
P_label, P_dot
),
ghost_line_update_animation,
perp_bisector_update_animation,
elbow_update_animation,
]
# Comment for tangency
sum_rect = SurroundingRectangle(
self.numbers[-1]
)
tangency_comment = TextMobject(
"Always $\\ge$ radius"
)
tangency_comment.next_to(
sum_rect, DOWN,
aligned_edge=RIGHT
)
VGroup(sum_rect, tangency_comment).set_color(GREEN)
# Why is this needed?!?
self.add(*self.focal_sum_things_to_add)
self.wait(0.01)
self.remove(*self.focal_sum_things_to_add)
# Show label
self.play(Write(label))
self.wait()
# Show Q_dot moving about a little
self.play(
FadeOut(label),
FadeIn(self.focal_lines),
FadeIn(self.distance_labels),
FadeIn(self.sum_expression),
FadeIn(self.numbers),
ellipse.set_stroke, {"width": 0.5},
)
self.add(*self.focal_sum_things_to_add)
self.play(
FadeInFromDown(Q_label),
GrowFromCenter(Q_dot)
)
self.wait()
self.add_foreground_mobjects(Q_dot)
self.add(Q_label_animation)
self.play(
Q_dot.move_to, line.point_from_proportion(0.05),
rate_func=there_and_back,
run_time=4
)
self.wait()
# Show similar triangles
self.play(
FadeIn(triangles[0]),
ShowCreation(QP_line),
Animation(elbow),
)
self.add(QP_line_update)
for i in range(3):
self.play(
FadeIn(triangles[(i + 1) % 2]),
FadeOut(triangles[i % 2]),
Animation(self.distance_labels),
Animation(elbow)
)
self.play(
FadeOut(triangles[1]),
Animation(self.distance_labels)
)
# Move first distance label
# (boy, this got messy...hopefully no one ever has
# to read this.)
angle = QP_line.get_angle() - QE_line.get_angle()
Q_point = Q_dot.get_center()
for x in range(2):
self.play(ShowCreationThenDestruction(QE_line))
distance_label_copy = self.distance_labels[0].copy()
self.play(
ApplyFunction(
distance_label_rotate_update,
distance_label_copy,
path_arc=angle
),
Rotate(QE_line, angle, about_point=Q_point)
)
self.play(FadeOut(QE_line))
self.remove(distance_label_copy)
self.add(distance_label_rotate_update_animation)
self.focal_sum_things_to_add.append(
distance_label_rotate_update_animation
)
self.wait()
self.play(
Q_dot.move_to, line.point_from_proportion(0),
run_time=4,
rate_func=there_and_back
)
# Trace out ellipse
self.play(ShowCreation(radial_line))
self.wait()
self.play(
ApplyFunction(put_dot_at_intersection, Q_dot),
run_time=3,
)
self.wait()
self.play(
Rotating(
P_dot,
about_point=circle.get_center(),
rate_func=bezier([0, 0, 1, 1]),
run_time=10,
),
ellipse.restore,
*P_dot_movement_updates
)
self.wait()
# Talk through tangency
self.play(
ShowCreation(sum_rect),
Write(tangency_comment),
)
points = [line.get_end(), line.get_start(), Q_dot.get_center()]
run_times = [1, 3, 2]
for point, run_time in zip(points, run_times):
self.play(Q_dot.move_to, point, run_time=run_time)
self.wait()
self.remove(*self.focal_sum_things_to_add)
self.play(*list(map(FadeOut, [
radial_line,
QP_line,
P_dot, P_label,
Q_dot, Q_label,
elbow,
self.distance_labels,
self.numbers,
self.sum_expression,
sum_rect,
tangency_comment,
])))
self.wait()
# Show all lines
lines.remove(line)
ghost_lines.remove(ghost_line)
for line in lines:
line.generate_target()
line.target.rotate(90 * DEGREES)
self.play(
LaggedStart(FadeIn, ghost_lines),
LaggedStart(FadeIn, lines),
)
self.play(LaggedStart(MoveToTarget, lines))
self.wait()
def show_orbiting_planet(self):
ellipse = self.ellipse
ep_dot = self.ep_dot
planet = ImageMobject("earth")
planet.set_height(0.25)
orbit = Orbiting(planet, ep_dot, ellipse)
lines = self.lines
def update_lines(lines):
for gl, line in zip(self.ghost_lines, lines):
intersection = line_intersection(
[self.circle.get_center(), gl.get_end()],
line.get_start_and_end()
)
distance = get_norm(
intersection - planet.get_center()
)
if distance < 0.025:
line.set_stroke(BLUE, 3)
self.add(line)
else:
line.set_stroke(WHITE, 1)
lines_update_animation = ContinualUpdate(
lines, update_lines
)
self.add(orbit)
self.add(lines_update_animation)
self.add_foreground_mobjects(planet)
self.wait(12)
class Enthusiast(Scene):
def construct(self):
randy = Randolph(color=BLUE_C)
randy.flip()
self.play(randy.change, "surprised")
self.play(Blink(randy))
self.wait()
class SimpleThinking(Scene):
def construct(self):
randy = Randolph(color=BLUE_C)
randy.flip()
self.play(randy.change, "thinking", 3 * UP)
self.play(Blink(randy))
self.wait()
self.play(randy.change, "hooray", 3 * UP)
self.play(Blink(randy))
self.wait()
class EndOfGeometryProofiness(GeometryProofLand):
def construct(self):
geometry_word = self.get_geometry_proof_land_word()
orbital_mechanics = TextMobject("Orbital Mechanics")
orbital_mechanics.scale(1.5)
orbital_mechanics.to_edge(UP)
underline = Line(LEFT, RIGHT)
underline.match_width(orbital_mechanics)
underline.next_to(orbital_mechanics, DOWN, SMALL_BUFF)
self.play(LaggedStart(FadeOutAndShiftDown, geometry_word))
self.play(FadeInFromDown(orbital_mechanics))
self.play(ShowCreation(underline))
self.wait()
class KeplersSecondLaw(AskAboutEllipses):
CONFIG = {
"sun_center": 4 * RIGHT + 0.75 * DOWN,
"animate_sun": True,
"a": 5.0,
"b": 3.0,
"ellipse_stroke_width": 2,
"area_color": COBALT,
"area_opacity": 0.75,
"arc_color": YELLOW,
"arc_stroke_width": 3,
"n_sample_sweeps": 5,
"fade_sample_areas": True,
}
def construct(self):
self.add_title()
self.add_sun()
self.add_orbit()
self.add_foreground_mobjects(self.comet)
self.show_several_sweeps()
self.contrast_close_to_far()
def add_title(self):
title = TextMobject("Kepler's 2nd law:")
title.scale(1.0)
title.to_edge(UP)
self.add(title)
self.title = title
subtitle = TextMobject(
"Orbits sweep a constant area per unit time"
)
subtitle.next_to(title, DOWN, buff=0.2)
subtitle.set_color(BLUE)
self.add(subtitle)
def show_several_sweeps(self):
shown_areas = VGroup()
for x in range(self.n_sample_sweeps):
self.wait()
area = self.show_area_sweep()
shown_areas.add(area)
self.wait()
if self.fade_sample_areas:
self.play(FadeOut(shown_areas))
def contrast_close_to_far(self):
orbit = self.orbit
sun_point = self.sun.get_center()
start_prop = 0.9
self.wait_until_proportion(start_prop)
self.show_area_sweep()
end_prop = orbit.proportion
arc = self.get_arc(start_prop, end_prop)
radius = Line(sun_point, arc.points[0])
radius.set_color(WHITE)
radius_words = self.get_radius_words(radius, "Short")
radius_words.next_to(radius.get_center(), LEFT, SMALL_BUFF)
arc_words = TextMobject("Long arc")
arc_words.rotate(90 * DEGREES)
arc_words.scale(0.5)
arc_words.next_to(RIGHT, RIGHT)
arc_words.apply_complex_function(np.exp)
arc_words.scale(0.8)
arc_words.next_to(
arc, RIGHT, buff=-SMALL_BUFF
)
arc_words.shift(4 * SMALL_BUFF * DOWN)
arc_words.match_color(arc)
# Show stubby arc
# self.remove(orbit)
# self.add(self.comet)
self.play(
ShowCreation(radius),
Write(radius_words),
)
self.play(
ShowCreation(arc),
Write(arc_words)
)
# Show narrow arc
# (Code repetition...uck)
start_prop = 0.475
self.wait_until_proportion(start_prop)
self.show_area_sweep()
end_prop = orbit.proportion
short_arc = self.get_arc(start_prop, end_prop)
long_radius = Line(sun_point, short_arc.points[0])
long_radius.set_color(WHITE)
long_radius_words = self.get_radius_words(long_radius, "Long")
short_arc_words = TextMobject("Short arc")
short_arc_words.scale(0.5)
short_arc_words.rotate(90 * DEGREES)
short_arc_words.next_to(short_arc, LEFT, SMALL_BUFF)
short_arc_words.match_color(short_arc)
self.play(
ShowCreation(long_radius),
Write(long_radius_words),
)
self.play(
ShowCreation(short_arc),
Write(short_arc_words)
)
self.wait(15)
# Helpers
def show_area_sweep(self, time=1.0):
orbit = self.orbit
start_prop = orbit.proportion
area = self.get_area(start_prop, start_prop)
area_update = UpdateFromFunc(
area,
lambda a: Transform(
a, self.get_area(start_prop, orbit.proportion)
).update(1)
)
self.play(area_update, run_time=time)
return area
def get_area(self, prop1, prop2):
"""
Return a mobject illustrating the area swept
out between a point prop1 of the way along
the ellipse, and prop2 of the way.
"""
sun_point = self.sun.get_center()
arc = self.get_arc(prop1, prop2)
# Add lines from start
result = VMobject()
result.append_vectorized_mobject(
Line(sun_point, arc.points[0])
)
result.append_vectorized_mobject(arc)
result.append_vectorized_mobject(
Line(arc.points[-1], sun_point)
)
result.set_stroke(WHITE, width=0)
result.set_fill(
self.area_color,
self.area_opacity,
)
return result
def get_arc(self, prop1, prop2):
ellipse = self.get_ellipse()
prop1 = prop1 % 1.0
prop2 = prop2 % 1.0
if prop2 > prop1:
arc = VMobject().pointwise_become_partial(
ellipse, prop1, prop2
)
elif prop1 > prop2:
arc, arc_extension = [
VMobject().pointwise_become_partial(
ellipse, p1, p2
)
for p1, p2 in [(prop1, 1.0), (0.0, prop2)]
]
arc.append_vectorized_mobject(arc_extension)
else:
arc = VectorizedPoint(
ellipse.point_from_proportion(prop1)
)
arc.set_stroke(
self.arc_color,
self.arc_stroke_width,
)
return arc
def wait_until_proportion(self, prop):
if self.skip_animations:
self.orbit.proportion = prop
else:
while (self.orbit.proportion % 1) < prop:
self.wait(self.frame_duration)
def get_radius_words(self, radius, adjective):
radius_words = TextMobject(
"%s radius" % adjective,
)
min_width = 0.8 * radius.get_length()
if radius_words.get_width() > min_width:
radius_words.set_width(min_width)
radius_words.match_color(radius)
radius_words.next_to(ORIGIN, UP, SMALL_BUFF)
angle = radius.get_angle()
angle = ((angle + PI) % TAU) - PI
if np.abs(angle) > PI / 2:
angle += PI
radius_words.rotate(angle, about_point=ORIGIN)
radius_words.shift(radius.get_center())
return radius_words
class NonEllipticalKeplersLaw(KeplersSecondLaw):
CONFIG = {
"animate_sun": True,
"sun_center": 2 * RIGHT,
"n_sample_sweeps": 10,
}
def construct(self):
self.add_sun()
self.add_orbit()
self.show_several_sweeps()
def add_orbit(self):
sun = self.sun
comet = ImageMobject("comet", height=0.5)
orbit_shape = self.get_ellipse()
orbit = self.orbit = Orbiting(comet, sun, orbit_shape)
self.add(orbit_shape)
self.add(orbit)
arrow, arrow_update = self.get_force_arrow_and_update(
comet
)
alt_arrow_update = ContinualUpdate(
arrow, lambda a: a.scale(
1.0 / a.get_length(),
about_point=a.get_start()
)
)
self.add(arrow_update, alt_arrow_update)
self.add_foreground_mobjects(comet, arrow)
self.ellipse = orbit_shape
def get_ellipse(self):
orbit_shape = ParametricFunction(
lambda t: (1 + 0.2 * np.sin(5 * TAU * t)) * np.array([
np.cos(TAU * t),
np.sin(TAU * t),
0
])
)
orbit_shape.set_height(7)
orbit_shape.stretch(1.5, 0)
orbit_shape.shift(LEFT)
orbit_shape.set_stroke(LIGHT_GREY, 1)
return orbit_shape
class AngularMomentumArgument(KeplersSecondLaw):
CONFIG = {
"animate_sun": False,
"sun_center": 4 * RIGHT + DOWN,
"comet_start_point": 4 * LEFT,
"comet_end_point": 5 * LEFT + DOWN,
"comet_height": 0.3,
}
def construct(self):
self.add_sun()
self.show_small_sweep()
self.show_sweep_dimensions()
self.show_conservation_of_angular_momentum()
def show_small_sweep(self):
sun_center = self.sun_center
comet_start = self.comet_start_point
comet_end = self.comet_end_point
triangle = Polygon(
sun_center, comet_start, comet_end,
fill_opacity=1,
fill_color=COBALT,
stroke_width=0,
)
triangle.save_state()
alt_triangle = Polygon(
sun_center,
interpolate(comet_start, comet_end, 0.9),
comet_end
)
alt_triangle.match_style(triangle)
comet = self.get_comet()
comet.move_to(comet_start)
velocity_vector = Arrow(
comet_start, comet_end,
color=WHITE,
buff=0
)
velocity_vector_label = TexMobject("\\vec{\\textbf{v}}")
velocity_vector_label.next_to(
velocity_vector.get_center(), UL,
buff=SMALL_BUFF
)
small_time_label = TextMobject(
"Small", "time", "$\\Delta t$",
)
small_time_label.to_edge(UP)
small = small_time_label.get_part_by_tex("Small")
small_rect = SurroundingRectangle(small)
self.add_foreground_mobjects(comet)
self.play(
ShowCreation(
triangle,
rate_func=lambda t: interpolate(1.0 / 3, 2.0 / 3, t)
),
MaintainPositionRelativeTo(
velocity_vector, comet
),
MaintainPositionRelativeTo(
velocity_vector_label,
velocity_vector,
),
ApplyMethod(
comet.move_to, comet_end,
rate_func=None,
),
run_time=2,
)
self.play(Write(small_time_label), run_time=2)
self.wait()
self.play(
Transform(triangle, alt_triangle),
ShowCreation(small_rect),
small.set_color, YELLOW,
)
self.wait()
self.play(
Restore(triangle),
FadeOut(small_rect),
small.set_color, WHITE,
)
self.wait()
self.triangle = triangle
self.comet = comet
self.delta_t = small_time_label.get_part_by_tex(
"$\\Delta t$"
)
self.velocity_vector = velocity_vector
self.small_time_label = small_time_label
def show_sweep_dimensions(self):
triangle = self.triangle
# velocity_vector = self.velocity_vector
delta_t = self.delta_t
comet = self.comet
triangle_points = triangle.get_anchors()[:3]
top = triangle_points[1]
area_label = TexMobject(
"\\text{Area}", "=", "\\frac{1}{2}",
"\\text{Base}", "\\times", "\\text{Height}",
)
area_label.set_color_by_tex_to_color_map({
"Base": PINK,
"Height": YELLOW,
})
area_label.to_edge(UP)
equals = area_label.get_part_by_tex("=")
area_expression = TexMobject(
"=", "\\frac{1}{2}", "R", "\\times",
"\\vec{\\textbf{v}}_\\perp",
"\\Delta t",
)
area_expression.set_color_by_tex_to_color_map({
"R": PINK,
"textbf{v}": YELLOW,
})
area_expression.next_to(area_label, DOWN)
area_expression.align_to(equals, LEFT)
self.R_v_perp = VGroup(*area_expression[-4:-1])
self.R_v_perp_rect = SurroundingRectangle(
self.R_v_perp,
stroke_color=BLUE,
fill_color=BLACK,
fill_opacity=1,
)
base = Line(triangle_points[2], triangle_points[0])
base.set_stroke(PINK, 3)
base_point = line_intersection(
base.get_start_and_end(),
[top, top + DOWN]
)
height = Line(top, base_point)
height.set_stroke(YELLOW, 3)
radius_label = TextMobject("Radius")
radius_label.next_to(base, DOWN, SMALL_BUFF)
radius_label.match_color(base)
R_term = area_expression.get_part_by_tex("R")
R_term.save_state()
R_term.move_to(radius_label[0])
R_term.set_fill(opacity=0.5)
v_perp = Arrow(*height.get_start_and_end(), buff=0)
v_perp.set_color(YELLOW)
v_perp.shift(comet.get_center() - v_perp.get_start())
v_perp_label = TexMobject(
"\\vec{\\textbf{v}}_\\perp"
)
v_perp_label.set_color(YELLOW)
v_perp_label.next_to(v_perp, RIGHT, buff=SMALL_BUFF)
v_perp_delta_t = VGroup(v_perp_label.copy(), delta_t.copy())
v_perp_delta_t.generate_target()
v_perp_delta_t.target.arrange_submobjects(RIGHT, buff=SMALL_BUFF)
v_perp_delta_t.target.next_to(height, RIGHT, SMALL_BUFF)
self.small_time_label.add(v_perp_delta_t[1])
self.play(
FadeInFromDown(area_label),
self.small_time_label.scale, 0.5,
self.small_time_label.to_corner, UL,
)
self.wait()
self.play(
ShowCreation(base),
Write(radius_label),
)
self.wait()
self.play(ShowCreation(height))
self.wait()
self.play(
GrowArrow(v_perp),
Write(v_perp_label, run_time=1),
)
self.wait()
self.play(MoveToTarget(v_perp_delta_t))
self.wait()
self.play(*[
ReplacementTransform(
area_label.get_part_by_tex(tex).copy(),
area_expression.get_part_by_tex(tex),
)
for tex in ("=", "\\frac{1}{2}", "\\times")
])
self.play(Restore(R_term))
self.play(ReplacementTransform(
v_perp_delta_t.copy(),
VGroup(*area_expression[-2:])
))
self.wait()
def show_conservation_of_angular_momentum(self):
R_v_perp = self.R_v_perp
R_v_perp_rect = self.R_v_perp_rect
sun_center = self.sun_center
comet = self.comet
comet.save_state()
vector_field = VectorField(
get_force_field_func((sun_center, -1))
)
vector_field.set_fill(opacity=0.8)
vector_field.sort_submobjects(
lambda p: -get_norm(p - sun_center)
)
stays_constant = TextMobject("Stays constant")
stays_constant.next_to(
R_v_perp_rect, DR, buff=MED_LARGE_BUFF
)
stays_constant.match_color(R_v_perp_rect)
stays_constant_arrow = Arrow(
stays_constant.get_left(),
R_v_perp_rect.get_bottom(),
color=R_v_perp_rect.get_color()
)
sun_dot = Dot(sun_center, fill_opacity=0.25)
big_dot = Dot(fill_opacity=0, radius=FRAME_WIDTH)
R_v_perp.save_state()
R_v_perp.generate_target()
R_v_perp.target.to_edge(LEFT, buff=MED_LARGE_BUFF)
lp, rp = parens = TexMobject("()")
lp.next_to(R_v_perp.target, LEFT)
rp.next_to(R_v_perp.target, RIGHT)
self.play(Transform(
big_dot, sun_dot,
run_time=1,
remover=True
))
self.wait()
self.play(
DrawBorderThenFill(R_v_perp_rect),
Animation(R_v_perp),
Write(stays_constant, run_time=1),
GrowArrow(stays_constant_arrow),
)
self.wait()
foreground = VGroup(*self.get_mobjects())
self.play(
LaggedStart(GrowArrow, vector_field),
Animation(foreground)
)
for x in range(3):
self.play(
LaggedStart(
ApplyFunction, vector_field,
lambda mob: (lambda m: m.scale(1.1).set_fill(opacity=1), mob),
rate_func=there_and_back,
run_time=1
),
Animation(foreground)
)
self.play(
FadeIn(parens),
MoveToTarget(R_v_perp),
)
self.play(
comet.scale, 2,
comet.next_to, parens, RIGHT, {"buff": SMALL_BUFF}
)
self.wait()
self.play(
FadeOut(parens),
R_v_perp.restore,
comet.restore,
)
self.wait(3)
class KeplersSecondLawImage(KeplersSecondLaw):
CONFIG = {
"animate_sun": False,
"n_sample_sweeps": 8,
"fade_sample_areas": False,
}
def construct(self):
self.add_sun()
self.add_foreground_mobjects(self.sun)
self.add_orbit()
self.add_foreground_mobjects(self.comet)
self.show_several_sweeps()
class HistoryOfAngularMomentum(TeacherStudentsScene):
CONFIG = {
"camera_config": {"fill_opacity": 1}
}
def construct(self):
am = VGroup(TextMobject("Angular momentum"))
k2l = TextMobject("Kepler's 2nd law")
arrow = Arrow(ORIGIN, RIGHT)
group = VGroup(am, arrow, k2l)
group.arrange_submobjects(RIGHT)
group.next_to(self.hold_up_spot, UL)
k2l_image = ImageMobject("Kepler2ndLaw")
k2l_image.match_width(k2l)
k2l_image.next_to(k2l, UP)
k2l.add(k2l_image)
angular_momentum_formula = TexMobject(
"R", "\\times", "m", "\\vec{\\textbf{v}}_\\perp",
)
angular_momentum_formula.set_color_by_tex_to_color_map({
"R": PINK,
"v": YELLOW,
})
angular_momentum_formula.next_to(am, UP)
am.add(angular_momentum_formula)
self.play(
self.teacher.change, "raise_right_hand",
FadeInFromDown(group),
self.get_student_changes(*3 * ["pondering"])
)
self.wait()
self.play(
am.next_to, arrow, RIGHT,
{"index_of_submobject_to_align": 0},
k2l.next_to, arrow, LEFT,
{"index_of_submobject_to_align": 0},
path_arc=90 * DEGREES,
run_time=2
)
self.wait(3)
class FeynmanRecountingNewton(Scene):
CONFIG = {
"camera_config": {"background_opacity": 1},
}
def construct(self):
feynman_teaching = ImageMobject("Feynman_teaching")
feynman_teaching.set_width(FRAME_WIDTH)
newton = ImageMobject("Newton")
principia = ImageMobject("Principia_equal_area")
images = [newton, principia]
for image in images:
image.set_height(5)
newton.to_corner(UL)
principia.next_to(newton, RIGHT)
for image in images:
image.rect = SurroundingRectangle(
image, color=WHITE, buff=0,
)
self.play(FadeInFromDown(feynman_teaching, run_time=2))
self.wait()
self.play(
FadeInFromDown(newton),
FadeInFromDown(newton.rect),
)
self.wait()
self.play(*[
FadeInAndShiftFromDirection(
mob, direction=3 * LEFT
)
for mob in (principia, principia.rect)
])
self.wait()
class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene):
CONFIG = {
"animate_sun": True,
"sun_center": 2 * RIGHT,
"a": 4.0,
"b": 3.5,
"num_vectors": 25,
}
def construct(self):
self.setup_orbit()
self.warp_orbit()
self.reference_inverse_square_law()
self.show_velocity_vectors()
self.collect_velocity_vectors()
def setup_orbit(self):
self.add_sun()
self.add_orbit()
self.add_foreground_mobjects(self.comet)
def warp_orbit(self):
def func(z, c=3.5):
return 1 * (np.exp((1.0 / c) * (z) + 1) - np.exp(1))
ellipse = self.ellipse
ellipse.save_state()
ellipse.generate_target()
ellipse.target.stretch(0.7, 1)
ellipse.target.apply_complex_function(func)
ellipse.target.replace(ellipse, dim_to_match=1)
self.wait(5)
self.play(MoveToTarget(ellipse, run_time=2))
self.wait(5)
def reference_inverse_square_law(self):
ellipse = self.ellipse
force_equation = TexMobject(
"F", "=", "{G", "M", "m", "\\over", "R^2}"
)
force_equation.move_to(ellipse)
force_equation.set_color_by_tex("F", YELLOW)
force_arrow, force_arrow_update = self.get_force_arrow_and_update(
self.comet, scale_factor=3,
)
radial_line, radial_line_update = self.get_radial_line_and_update(
self.comet
)
self.add(radial_line_update)
self.add(force_arrow_update)
self.play(
Restore(ellipse),
Write(force_equation),
UpdateFromAlphaFunc(
force_arrow,
lambda m, a: m.set_fill(opacity=a)
),
UpdateFromAlphaFunc(
radial_line,
lambda m, a: m.set_stroke(width=a)
),
)
self.wait(10)
def show_velocity_vectors(self):
alphas = np.linspace(0, 1, self.num_vectors, endpoint=False)
vectors = VGroup(*[
self.get_velocity_vector(alpha)
for alpha in alphas
])
moving_vector, moving_vector_animation = self.get_velocity_vector_and_update()
self.play(LaggedStart(
ShowCreation, vectors,
lag_ratio=0.2,
run_time=3,
))
self.wait(5)
self.add(moving_vector_animation)
self.play(
FadeOut(vectors),
VFadeIn(moving_vector)
)
self.wait(10)
vectors.set_fill(opacity=0.5)
self.play(
LaggedStart(ShowCreation, vectors),
Animation(moving_vector)
)
self.wait(5)
self.velocity_vectors = vectors
self.moving_vector = moving_vector
def collect_velocity_vectors(self):
vectors = self.velocity_vectors.copy()
frame = self.camera_frame
ellipse = self.ellipse
frame_shift = 2.5 * LEFT
root_point = ellipse.get_left() + 3 * LEFT + 1 * UP
vector_targets = VGroup()
for vector in vectors:
vector.target = Arrow(
root_point,
root_point + vector.get_vector(),
buff=0,
rectangular_stem_width=0.025,
tip_length=0.2,
color=vector.get_color(),
)
vector.target.add_to_back(
vector.target.copy().set_stroke(BLACK, 5)
)
vector_targets.add(vector.target)
circle = Circle(color=YELLOW)
circle.replace(vector_targets)
circle.scale(1.04)
velocity_space = TextMobject("Velocity space")
velocity_space.next_to(circle, UP)
rect = SurroundingRectangle(
VGroup(circle, velocity_space),
buff=MED_LARGE_BUFF,
color=WHITE,
)
self.play(
ApplyMethod(
frame.shift, frame_shift,
run_time=2,
),
LaggedStart(
MoveToTarget, vectors,
run_time=4,
),
FadeInFromDown(velocity_space),
FadeInFromDown(rect),
)
self.wait(2)
self.play(
ShowCreation(circle),
Animation(vectors)
)
self.wait(24)
# Helpers
def get_velocity_vector(self, alpha, d_alpha=0.01, scalar=3.0):
norm = get_norm
ellipse = self.ellipse
sun_center = self.sun.get_center()
min_length = 0.1 * scalar
max_length = 0.5 * scalar
p1, p2 = [
ellipse.point_from_proportion(a)
for a in (alpha, alpha + d_alpha)
]
vector = Arrow(
p1, p2, buff=0
)
radius_vector = p1 - sun_center
curr_v_perp = norm(np.cross(
vector.get_vector(),
radius_vector / norm(radius_vector)
))
vector.scale(
scalar / (norm(curr_v_perp) * norm(radius_vector)),
about_point=vector.get_start()
)
vector.set_color(
interpolate_color(
BLUE, RED, inverse_interpolate(
min_length, max_length,
vector.get_length()
)
)
)
vector.add_to_back(
vector.copy().set_stroke(BLACK, 5)
)
return vector
def get_velocity_vector_and_update(self):
moving_vector = self.get_velocity_vector(0)
def update_moving_vector(vector):
new_vector = self.get_velocity_vector(
self.orbit.proportion,
)
Transform(vector, new_vector).update(1)
moving_vector_animation = ContinualUpdate(
moving_vector, update_moving_vector
)
return moving_vector, moving_vector_animation
class AskWhy(TeacherStudentsScene):
def construct(self):
self.student_says(
"Um...why?",
target_mode="confused",
student_index=2,
bubble_kwargs={"direction": LEFT},
)
self.play(
self.teacher.change, "happy",
self.get_student_changes(
"raise_left_hand", "sassy", "confused"
)
)
self.wait(5)
class FeynmanConfusedByNewton(Scene):
def construct(self):
pass
class ShowEqualAngleSlices(IntroduceShapeOfVelocities):
CONFIG = {
"animate_sun": True,
"theta": 30 * DEGREES,
}
def construct(self):
self.setup_orbit()
self.show_equal_angle_slices()
self.ask_about_time_per_slice()
self.areas_are_proportional_to_radius_squared()
self.show_inverse_square_law()
self.directly_compare_velocity_vectors()
def setup_orbit(self):
IntroduceShapeOfVelocities.setup_orbit(self)
self.remove(self.orbit)
self.add(self.comet)
def show_equal_angle_slices(self):
sun_center = self.sun.get_center()
ellipse = self.ellipse
def get_cos_angle_diff(v1, v2):
return np.dot(
v1 / get_norm(v1),
v2 / get_norm(v2),
)
lines = VGroup()
angle_arcs = VGroup()
thetas = VGroup()
angles = np.arange(0, TAU, self.theta)
for angle in angles:
prop = angle / TAU
vect = rotate_vector(RIGHT, angle)
end_point = ellipse.point_from_proportion(prop)
curr_cos = get_cos_angle_diff(
end_point - sun_center, vect
)
coss_diff = (1 - curr_cos)
while abs(coss_diff) > 0.00001:
d_prop = 0.001
alt_end = ellipse.point_from_proportion(
(prop + d_prop) % 1
)
alt_cos = get_cos_angle_diff(alt_end - sun_center, vect)
d_cos = (alt_cos - curr_cos)
delta_prop = (coss_diff / d_cos) * d_prop
prop += delta_prop
end_point = ellipse.point_from_proportion(prop)
curr_cos = get_cos_angle_diff(end_point - sun_center, vect)
coss_diff = 1 - curr_cos
line = Line(sun_center, end_point)
line.prop = prop
lines.add(line)
angle_arc = AnnularSector(
angle=self.theta,
inner_radius=1,
outer_radius=1.05,
)
angle_arc.rotate(angle, about_point=ORIGIN)
angle_arc.scale(0.5, about_point=ORIGIN)
angle_arc.shift(sun_center)
angle_arc.mid_angle = angle + self.theta / 2
angle_arcs.add(angle_arc)
theta = TexMobject("\\theta")
theta.scale(0.6)
vect = rotate_vector(RIGHT, angle_arc.mid_angle)
theta.move_to(
angle_arc.get_center() + 0.2 * vect
)
thetas.add(theta)
arcs = VGroup()
wedges = VGroup()
for l1, l2 in adjacent_pairs(lines):
arc = VMobject()
arc.pointwise_become_partial(
ellipse, l1.prop, (l2.prop or 1.0)
)
arcs.add(arc)
wedge = VMobject()
wedge.append_vectorized_mobject(
Line(sun_center, arc.points[0])
)
wedge.append_vectorized_mobject(arc)
wedge.append_vectorized_mobject(
Line(arc.points[-1], sun_center)
)
wedges.add(wedge)
lines.set_stroke(LIGHT_GREY, 2)
angle_arcs.set_color_by_gradient(
YELLOW, BLUE, RED, PINK, YELLOW
)
arcs.set_color_by_gradient(BLUE, YELLOW)
wedges.set_stroke(width=0)
wedges.set_fill(opacity=1)
wedges.set_color_by_gradient(BLUE, COBALT, BLUE_E, BLUE)
kwargs = {
"run_time": 6,
"lag_ratio": 0.2,
"rate_func": there_and_back,
}
faders = VGroup(wedges, angle_arcs, thetas)
faders.set_fill(opacity=0.4)
thetas.set_fill(opacity=0)
self.play(
FadeIn(faders),
*list(map(ShowCreation, lines))
)
self.play(*[
LaggedStart(
ApplyMethod, fader,
lambda m: (m.set_fill, {"opacity": 1}),
**kwargs
)
for fader in faders
] + [Animation(lines)])
self.wait()
self.lines = lines
self.wedges = wedges
self.arcs = arcs
self.angle_arcs = angle_arcs
self.thetas = thetas
def ask_about_time_per_slice(self):
wedge1 = self.wedges[0]
wedge2 = self.wedges[len(self.wedges) / 2]
arc1 = self.arcs[0]
arc2 = self.arcs[len(self.arcs) / 2]
comet = self.comet
frame = self.camera_frame
words1 = TextMobject(
"Time spent \\\\ traversing \\\\ this slice?"
)
words2 = TextMobject("How about \\\\ this one?")
words1.to_corner(UR)
words2.next_to(wedge2, LEFT, MED_LARGE_BUFF)
arrow1 = Arrow(
words1.get_bottom(),
wedge1.get_center() + wedge1.get_height() * DOWN / 2,
color=WHITE,
)
arrow2 = Arrow(
words2.get_right(),
wedge2.get_center() + wedge2.get_height() * UL / 4,
color=WHITE
)
foreground = VGroup(
self.ellipse, self.angle_arcs,
self.lines, comet,
)
self.play(
Write(words1),
wedge1.set_fill, {"opacity": 1},
GrowArrow(arrow1),
Animation(foreground),
frame.scale, 1.2,
)
self.play(MoveAlongPath(comet, arc1, rate_func=None))
self.play(
Write(words2),
wedge2.set_fill, {"opacity": 1},
Write(arrow2),
Animation(foreground),
)
self.play(MoveAlongPath(comet, arc2, rate_func=None, run_time=3))
self.wait()
self.area_questions = VGroup(words1, words2)
self.area_question_arrows = VGroup(arrow1, arrow2)
self.highlighted_wedges = VGroup(wedge1, wedge2)
def areas_are_proportional_to_radius_squared(self):
wedges = self.highlighted_wedges
wedge = wedges[1]
frame = self.camera_frame
ellipse = self.ellipse
sun_center = self.sun.get_center()
line = self.lines[len(self.lines) / 2]
thick_line = line.copy().set_stroke(PINK, 4)
radius_word = TextMobject("Radius")
radius_word.next_to(thick_line, UP, SMALL_BUFF)
radius_word.match_color(thick_line)
arc = self.arcs[len(self.arcs) / 2]
thick_arc = arc.copy().set_stroke(RED, 4)
scaling_group = VGroup(
wedge, thick_line, radius_word, thick_arc
)
expression = TextMobject(
"Time", "$\\propto$",
"Area", "$\\propto$", "$(\\text{Radius})^2$"
)
expression.next_to(ellipse, UP, LARGE_BUFF)
prop_to_brace = Brace(expression[1], DOWN, buff=SMALL_BUFF)
prop_to_words = TextMobject("(proportional to)")
prop_to_words.scale(0.7)
prop_to_words.next_to(prop_to_brace, DOWN, SMALL_BUFF)
VGroup(prop_to_words, prop_to_brace).set_color(GREEN)
self.play(
Write(expression[:3]),
frame.shift, 0.5 * UP,
FadeInFromDown(prop_to_words),
GrowFromCenter(prop_to_brace),
)
self.wait(2)
self.play(
ShowCreation(thick_line),
FadeInFromDown(radius_word)
)
self.wait()
self.play(ShowCreationThenDestruction(thick_arc))
self.play(ShowCreation(thick_arc))
self.wait()
self.play(Write(expression[3:]))
self.play(
scaling_group.scale, 0.5,
{"about_point": sun_center},
Animation(self.area_question_arrows),
Animation(self.comet),
rate_func=there_and_back,
run_time=4,
)
self.wait()
expression.add(prop_to_brace, prop_to_words)
self.proportionality_expression = expression
def show_inverse_square_law(self):
prop_exp = self.proportionality_expression
comet = self.comet
frame = self.camera_frame
ellipse = self.ellipse
orbit = self.orbit
next_line = self.lines[(len(self.lines) / 2) + 1]
arc = self.arcs[len(self.arcs) / 2]
force_expression = TexMobject(
"ma", "=", "\\text{Force}",
"\\propto", "\\frac{1}{(\\text{Radius})^2}"
)
force_expression.next_to(ellipse, LEFT, MED_LARGE_BUFF)
force_expression.align_to(prop_exp, UP)
force_expression.set_color_by_tex("Force", YELLOW)
acceleration_expression = TexMobject(
"a", "=", "{\\Delta v",
"\\over", "\\Delta t}",
"\\propto", "{1 \\over (\\text{Radius})^2}"
)
acceleration_expression.next_to(
force_expression, DOWN, buff=0.75,
aligned_edge=LEFT
)
delta_v_expression = TexMobject(
"\\Delta v}", "\\propto",
"{\\Delta t", "\\over", "(\\text{Radius})^2}"
)
delta_v_expression.next_to(
acceleration_expression, DOWN, buff=0.75,
aligned_edge=LEFT
)
delta_t_numerator = delta_v_expression.get_part_by_tex(
"\\Delta t"
)
moving_R_squared = prop_exp.get_part_by_tex("Radius").copy()
moving_R_squared.generate_target()
moving_R_squared.target.move_to(delta_t_numerator, DOWN)
moving_R_squared.target.set_color(GREEN)
randy = Randolph()
randy.next_to(force_expression, DOWN, LARGE_BUFF)
force_vector, force_vector_update = self.get_force_arrow_and_update(
comet, scale_factor=3,
)
moving_vector, moving_vector_animation = self.get_velocity_vector_and_update()
self.play(
FadeOut(self.area_questions),
FadeOut(self.area_question_arrows),
FadeInFromDown(force_expression),
frame.shift, 2 * LEFT,
)
self.remove(*self.area_questions)
self.play(
randy.change, "confused", force_expression,
VFadeIn(randy)
)
self.wait(2)
self.play(
randy.change, "pondering", force_expression[0],
ShowPassingFlashAround(force_expression[:2]),
)
self.play(Blink(randy))
self.play(
FadeInFromDown(acceleration_expression),
randy.change, "hooray", force_expression,
randy.shift, 2 * DOWN,
)
self.wait(2)
self.play(Blink(randy))
self.play(randy.change, "thinking")
self.wait()
self.play(
comet.move_to, arc.points[0],
path_arc=90 * DEGREES
)
force_vector_update.update(0)
self.play(
Blink(randy),
GrowArrow(force_vector)
)
self.add(force_vector_update)
self.add_foreground_mobjects(comet)
# Slightly hacky orbit treatment here...
orbit.proportion = 0.5
moving_vector_animation.update(0)
start_velocity_vector = moving_vector.copy()
self.play(
GrowArrow(start_velocity_vector),
randy.look_at, moving_vector
)
self.add(moving_vector_animation)
self.add(orbit)
while orbit.proportion < next_line.prop:
self.wait(self.frame_duration)
self.remove(orbit)
self.add_foreground_mobjects(comet)
self.wait(2)
self.play(
randy.change, "pondering", force_expression,
randy.shift, 2 * DOWN,
FadeInFromDown(delta_v_expression)
)
self.play(Blink(randy))
self.wait(2)
self.play(
delta_t_numerator.scale, 1.5, {"about_edge": DOWN},
delta_t_numerator.set_color, YELLOW
)
self.play(ShowCreationThenFadeAround(prop_exp[:-2]))
self.play(
delta_t_numerator.fade, 1,
MoveToTarget(moving_R_squared),
randy.change, "happy", delta_v_expression
)
delta_v_expression.add(moving_R_squared)
self.wait()
self.play(FadeOut(randy))
self.start_velocity_vector = start_velocity_vector
self.end_velocity_vector = moving_vector.copy()
self.moving_vector = moving_vector
self.force_expressions = VGroup(
force_expression,
acceleration_expression,
delta_v_expression,
)
def directly_compare_velocity_vectors(self):
ellipse = self.ellipse
lines = self.lines
expressions = self.force_expressions
vectors = VGroup(*[
self.get_velocity_vector(line.prop)
for line in lines
])
index = len(vectors) / 2
v1 = vectors[index]
v2 = vectors[index + 1]
root_point = ellipse.get_left() + 3 * LEFT + DOWN
root_dot = Dot(root_point)
for vector in vectors:
vector.save_state()
vector.target = Arrow(
*vector.get_start_and_end(),
color=vector.get_color(),
buff=0
)
vector.target.scale(2)
vector.target.shift(
root_point - vector.target.get_start()
)
vector.target.add_to_back(
vector.target.copy().set_stroke(BLACK, 5)
)
difference_vectors = VGroup()
external_angle_lines = VGroup()
external_angle_arcs = VGroup()
for vect1, vect2 in adjacent_pairs(vectors):
diff_vect = Arrow(
vect1.target.get_end(),
vect2.target.get_end(),
buff=0,
color=YELLOW,
rectangular_stem_width=0.025,
tip_length=0.15
)
diff_vect.add_to_back(
diff_vect.copy().set_stroke(BLACK, 2)
)
difference_vectors.add(diff_vect)
line = Line(
diff_vect.get_start(),
diff_vect.get_start() + 2 * diff_vect.get_vector(),
)
external_angle_lines.add(line)
arc = Arc(self.theta, stroke_width=2)
arc.rotate(line.get_angle(), about_point=ORIGIN)
arc.scale(0.4, about_point=ORIGIN)
arc.shift(line.get_center())
external_angle_arcs.add(arc)
external_angle_lines.set_stroke(LIGHT_GREY, 2)
diff_vect = difference_vectors[index]
polygon = Polygon(*[
vect.target.get_end()
for vect in vectors
])
polygon.set_fill(BLUE_E, opacity=0.8)
polygon.set_stroke(WHITE, 3)
self.play(ShowCreationThenFadeAround(v1))
self.play(
MoveToTarget(v1),
GrowFromCenter(root_dot),
expressions.scale, 0.5, {"about_edge": UL}
)
self.wait()
self.play(
ReplacementTransform(
v1.saved_state.copy(), v2.saved_state,
path_arc=self.theta
)
)
self.play(MoveToTarget(v2), Animation(root_dot))
self.wait()
self.play(GrowArrow(diff_vect))
self.wait()
n = len(vectors)
for i in range(n - 1):
v1 = vectors[(i + index + 1) % n]
v2 = vectors[(i + index + 2) % n]
diff_vect = difference_vectors[(i + index + 1) % n]
# TODO, v2.saved_state is on screen untracked
self.play(ReplacementTransform(
v1.saved_state.copy(), v2.saved_state,
path_arc=self.theta
))
self.play(
MoveToTarget(v2),
GrowArrow(diff_vect)
)
self.add(self.orbit)
self.wait()
self.play(
LaggedStart(ShowCreation, external_angle_lines),
LaggedStart(ShowCreation, external_angle_arcs),
Animation(difference_vectors),
)
self.add_foreground_mobjects(difference_vectors)
self.wait(2)
self.play(FadeIn(polygon))
self.wait(5)
self.play(FadeOut(polygon))
self.wait(15)
self.play(FadeIn(polygon))
class ShowEqualAngleSlices15DegreeSlices(ShowEqualAngleSlices):
CONFIG = {
"animate_sun": True,
"theta": 15 * DEGREES,
}
class ShowEqualAngleSlices5DegreeSlices(ShowEqualAngleSlices):
CONFIG = {
"animate_sun": True,
"theta": 5 * DEGREES,
}
class IKnowThisIsTricky(TeacherStudentsScene):
def construct(self):
self.teacher_says(
"All you need is \\\\ infinite intelligence",
bubble_kwargs={
"width": 4,
"height": 3,
},
added_anims=[
self.get_student_changes(
*3 * ["horrified"],
look_at_arg=self.screen
)
]
)
self.look_at(self.screen)
self.wait(3)
class PonderOverOffCenterDiagram(PiCreatureScene):
def construct(self):
randy, morty = self.pi_creatures
velocity_diagram = self.get_velocity_diagram()
bubble = randy.get_bubble()
rect = SurroundingRectangle(
velocity_diagram,
buff=MED_LARGE_BUFF,
color=LIGHT_GREY
)
rect.stretch(1.2, 1, about_edge=DOWN)
words = TextMobject("Velocity space")
words.next_to(rect.get_top(), DOWN)
self.play(
LaggedStart(GrowFromCenter, velocity_diagram),
randy.change, "pondering",
morty.change, "confused",
)
self.wait(2)
self.play(ShowCreation(bubble))
self.wait(2)
self.play(
FadeOut(bubble),
randy.change, "confused",
morty.change, "pondering",
ShowCreation(rect)
)
self.play(Write(words))
self.wait(2)
def create_pi_creatures(self):
randy = Randolph(height=2.5)
randy.to_corner(DL)
morty = Mortimer(height=2.5)
morty.to_corner(DR)
return randy, morty
def get_velocity_diagram(self):
circle = Circle(color=WHITE, radius=2)
circle.rotate(90 * DEGREES)
circle.to_edge(DOWN, buff=LARGE_BUFF)
root_point = interpolate(
circle.get_center(),
circle.get_bottom(),
0.5,
)
dot = Dot(root_point)
vectors = VGroup()
for a in np.arange(0, 1, 1.0 / 24):
end_point = circle.point_from_proportion(a)
vector = Arrow(root_point, end_point, buff=0)
vector.set_color(interpolate_color(
BLUE, RED,
inverse_interpolate(
1, 3, vector.get_length(),
)
))
vector.add_to_back(vector.copy().set_stroke(BLACK, 5))
vectors.add(vector)
vectors.add_to_back(circle)
vectors.add(dot)
return vectors
class OneMoreTrick(TeacherStudentsScene):
def construct(self):
for student in self.students:
student.change("tired")
self.teacher_says("Just one more \\\\ tricky bit!")
self.change_all_student_modes("hooray")
self.wait(3)
class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices):
CONFIG = {
"animate_sun": True,
"theta": 15 * DEGREES,
"index": 6,
}
def construct(self):
self.setup_orbit()
self.setup_velocity_diagram()
self.show_theta_degrees()
self.match_velocity_vector_to_tangency()
self.replace_vectors_with_lines()
self.not_that_velocity_vector_is_theta()
self.ask_about_curve()
self.show_90_degree_rotation()
self.show_geometry_of_rotated_diagram()
def setup_orbit(self):
ShowEqualAngleSlices.setup_orbit(self)
self.force_skipping()
self.show_equal_angle_slices()
self.revert_to_original_skipping_status()
orbit_word = self.orbit_word = TextMobject("Orbit")
orbit_word.scale(1.5)
orbit_word.next_to(self.ellipse, UP, LARGE_BUFF)
self.add(orbit_word)
def setup_velocity_diagram(self):
ellipse = self.ellipse
root_point = ellipse.get_left() + 4 * LEFT + DOWN
frame = self.camera_frame
root_dot = Dot(root_point)
vectors = VGroup()
original_vectors = VGroup()
for line in self.lines:
vector = self.get_velocity_vector(line.prop)
vector.save_state()
original_vectors.add(vector.copy())
vector.target = self.get_velocity_vector(
line.prop, scalar=8.0
)
vector.target.shift(
root_point - vector.target.get_start()
)
vectors.add(vector)
circle = Circle()
circle.rotate(92 * DEGREES)
circle.replace(VGroup(*[v.target for v in vectors]))
circle.set_stroke(WHITE, 2)
circle.shift(
(root_point[0] - circle.get_center()[0]) * RIGHT
)
circle.shift(0.035 * LEFT) # ?!?
velocities_word = TextMobject("Velocities")
velocities_word.scale(1.5)
velocities_word.next_to(circle, UP)
velocities_word.align_to(self.orbit_word, DOWN)
frame.scale(1.2)
frame.shift(3 * LEFT + 0.5 * UP)
self.play(ApplyWave(ellipse))
self.play(*list(map(GrowArrow, vectors)))
self.play(
LaggedStart(
MoveToTarget, vectors,
lag_ratio=1,
run_time=2
),
GrowFromCenter(root_dot),
FadeInFromDown(velocities_word),
)
self.add_foreground_mobjects(root_dot)
self.play(
ShowCreation(circle),
Animation(vectors),
)
self.wait()
self.vectors = vectors
self.original_vectors = original_vectors
self.velocity_circle = circle
self.root_dot = root_dot
self.circle = circle
def show_theta_degrees(self):
lines = self.lines
ellipse = self.ellipse
circle = self.circle
vectors = self.vectors
comet = self.comet
sun_center = self.sun.get_center()
index = self.index
angle = fdiv(index, len(lines)) * TAU
thick_line = lines[index].copy()
thick_line.set_stroke(RED, 3)
horizontal = lines[0].copy()
horizontal.set_stroke(WHITE, 3)
ellipse_arc = VMobject()
ellipse_arc.pointwise_become_partial(
ellipse, 0, thick_line.prop
)
ellipse_arc.set_stroke(YELLOW, 3)
ellipse_wedge = self.get_wedge(ellipse_arc, sun_center)
ellipse_wedge_start = self.get_wedge(
VectorizedPoint(ellipse.get_right()), sun_center
)
ellipse_angle_arc = Arc(
self.theta * index,
radius=0.5
)
ellipse_angle_arc.shift(sun_center)
ellipse_theta = TexMobject("\\theta")
ellipse_theta.next_to(ellipse_angle_arc, RIGHT, MED_SMALL_BUFF)
ellipse_theta.shift(2 * SMALL_BUFF * UL)
vector = vectors[index].deepcopy()
vector.set_fill(YELLOW)
vector.save_state()
Transform(vector, vectors[0]).update(1)
vector.set_fill(YELLOW)
circle_arc = VMobject()
circle_arc.pointwise_become_partial(
circle, 0, fdiv(index, len(vectors))
)
circle_arc.set_stroke(RED, 4)
circle_theta = TexMobject("\\theta")
circle_theta.scale(1.5)
circle_theta.next_to(circle_arc, UP, SMALL_BUFF)
circle_theta.shift(SMALL_BUFF * DL)
circle_wedge = self.get_wedge(circle_arc, circle.get_center())
circle_wedge.set_fill(PINK)
circle_wedge_start = self.get_wedge(
Line(circle.get_top(), circle.get_top()),
circle.get_center()
).match_style(circle_wedge)
circle_center_dot = Dot(circle.get_center())
# circle_center_dot.set_color(BLUE)
self.play(FocusOn(comet))
self.play(
ReplacementTransform(
ellipse_wedge_start, ellipse_wedge,
path_arc=angle,
),
FadeIn(ellipse_arc),
ShowCreation(ellipse_angle_arc),
Write(ellipse_theta),
ReplacementTransform(
lines[0].copy(), thick_line,
path_arc=angle
),
MoveAlongPath(comet, ellipse_arc),
run_time=2
)
self.wait()
self.play(
ReplacementTransform(
circle_wedge_start, circle_wedge,
path_arc=angle
),
ShowCreation(circle_arc),
Write(circle_theta),
Restore(vector, path_arc=angle),
GrowFromCenter(circle_center_dot),
FadeIn(horizontal),
run_time=2
)
self.wait()
self.set_variables_as_attrs(
index,
ellipse_wedge, ellipse_arc,
ellipse_angle_arc, ellipse_theta,
thick_line, horizontal,
circle_wedge, circle_arc,
circle_theta, circle_center_dot,
highlighted_vector=vector
)
def match_velocity_vector_to_tangency(self):
vector = self.highlighted_vector
comet = self.comet
original_vector = self.original_vectors[self.index].copy()
original_vector.set_fill(YELLOW)
tangent_line = Line(
*original_vector.get_start_and_end()
)
tangent_line.set_stroke(LIGHT_GREY, 3)
tangent_line.scale(5)
tangent_line.move_to(comet)
self.play(
ReplacementTransform(
vector.copy(), original_vector,
run_time=2
),
Animation(comet),
)
self.wait()
self.play(
ShowCreation(tangent_line),
Animation(original_vector),
Animation(comet),
)
self.wait()
self.set_variables_as_attrs(
example_tangent_line=tangent_line,
example_tangent_vector=original_vector,
)
def replace_vectors_with_lines(self):
vectors = self.vectors
original_vectors = self.original_vectors
root_dot = self.root_dot
highlighted_vector = self.highlighted_vector
lines = VGroup()
tangent_lines = VGroup()
for vect, o_vect in zip(vectors, original_vectors):
line = Line(*vect.get_start_and_end())
t_line = Line(*o_vect.get_start_and_end())
t_line.scale(5)
t_line.move_to(o_vect.get_start())
lines.add(line)
tangent_lines.add(t_line)
vect.generate_target()
vect.target.scale(0, about_point=root_dot.get_center())
lines.set_stroke(GREEN, 2)
tangent_lines.set_stroke(GREEN, 2)
highlighted_line = Line(
*highlighted_vector.get_start_and_end(),
stroke_color=YELLOW,
stroke_width=4
)
self.play(
LaggedStart(MoveToTarget, vectors),
highlighted_vector.scale, 0,
{"about_point": root_dot.get_center()},
Animation(highlighted_vector),
Animation(self.circle_wedge),
Animation(self.circle_arc),
Animation(self.circle),
Animation(self.circle_center_dot),
)
self.remove(vectors, highlighted_vector)
self.play(
LaggedStart(ShowCreation, lines),
ShowCreation(highlighted_line),
Animation(highlighted_vector),
)
self.wait()
self.play(
ReplacementTransform(
lines.copy(),
tangent_lines,
run_time=3,
)
)
self.wait()
self.play(FadeOut(tangent_lines))
self.eccentric_lines = lines
self.highlighted_line = highlighted_line
def not_that_velocity_vector_is_theta(self):
vector = self.example_tangent_vector
v_line = Line(DOWN, UP)
v_line.move_to(vector.get_start(), DOWN)
angle = vector.get_angle() - 90 * DEGREES
arc = Arc(angle, radius=0.5)
arc.rotate(90 * DEGREES, about_point=ORIGIN)
arc.shift(vector.get_start())
theta_q = TexMobject("\\theta ?")
theta_q.next_to(arc, UP)
theta_q.shift(SMALL_BUFF * LEFT)
cross = Cross(theta_q)
self.play(ShowCreation(v_line))
self.play(
ShowCreation(arc),
FadeInFromDown(theta_q),
)
self.wait()
self.play(ShowCreation(cross))
self.wait()
self.play(*list(map(FadeOut, [v_line, arc, theta_q, cross])))
self.wait()
self.play(
ReplacementTransform(
self.ellipse_theta.copy(), self.circle_theta,
),
ReplacementTransform(
self.ellipse_angle_arc.copy(), self.circle_arc,
),
run_time=2,
)
self.wait()
self.play(
ReplacementTransform(
self.circle.copy(),
self.circle_center_dot,
)
)
self.wait()
def ask_about_curve(self):
ellipse = self.ellipse
circle = self.circle
line = self.highlighted_line.copy()
vector = self.example_tangent_vector
morty = Mortimer(height=2.5)
morty.move_to(ellipse.get_corner(UL))
morty.shift(MED_SMALL_BUFF * LEFT)
self.play(FadeIn(morty))
self.play(
morty.change, "confused", ellipse,
ShowCreationThenDestruction(
ellipse.copy().set_stroke(BLUE, 3),
run_time=2
)
)
self.play(
Blink(morty),
ApplyWave(ellipse),
)
self.play(morty.look_at, circle)
self.play(morty.change, "pondering", circle)
self.play(Blink(morty))
self.play(morty.look_at, ellipse)
self.play(morty.change, "maybe", ellipse)
self.play(
line.move_to, vector, run_time=2
)
self.play(FadeOut(line))
self.wait()
self.play(morty.look_at, circle)
self.wait()
self.play(FadeOut(morty))
def show_90_degree_rotation(self):
circle = self.circle
circle_setup = VGroup(
circle, self.eccentric_lines,
self.circle_wedge,
self.circle_arc,
self.highlighted_line,
self.circle_center_dot,
self.root_dot,
self.circle_theta,
)
circle_setup.generate_target()
angle = -90 * DEGREES
circle_setup.target.rotate(
angle,
about_point=circle.get_center()
)
circle_setup.target[-1].rotate(-angle)
circle_setup.target[2].set_fill(opacity=0)
circle_setup.target[2].set_stroke(WHITE, 4)
self.play(MoveToTarget(circle_setup, path_arc=angle))
self.wait()
lines = self.eccentric_lines
highlighted_line = self.highlighted_line
ghost_lines = lines.copy()
ghost_lines.set_stroke(width=1)
ghost_lines[self.index].set_stroke(YELLOW, 4)
for mob in list(lines) + [highlighted_line]:
mob.generate_target()
mob.save_state()
mob.target.rotate(-angle)
foci = [
self.root_dot.get_center(),
circle.get_center(),
]
a = circle.get_width() / 4
c = get_norm(foci[1] - foci[0]) / 2
b = np.sqrt(a**2 - c**2)
little_ellipse = Circle(radius=a)
little_ellipse.stretch(b / a, 1)
little_ellipse.move_to(center_of_mass(foci))
little_ellipse.set_stroke(PINK, 4)
self.add(ghost_lines)
self.play(
LaggedStart(
MoveToTarget, lines,
lag_ratio=0.1,
run_time=8,
),
MoveToTarget(highlighted_line),
path_arc=-angle,
)
self.play(ShowCreation(little_ellipse))
self.wait(2)
self.play(
little_ellipse.replace, self.ellipse,
run_time=4,
rate_func=there_and_back_with_pause
)
self.wait(2)
self.play(*[
Restore(
mob,
path_arc=angle,
run_time=4,
rate_func=there_and_back_with_pause
)
for mob in list(lines) + [highlighted_line]
] + [Animation(little_ellipse)])
self.ghost_lines = ghost_lines
self.little_ellipse = little_ellipse
def show_geometry_of_rotated_diagram(self):
ellipse = self.ellipse
little_ellipse = self.little_ellipse
circle = self.circle
perp_line = self.highlighted_line.copy()
perp_line.rotate(PI)
circle_arc = self.circle_arc
arc_copy = circle_arc.copy()
center = circle.get_center()
velocity_vector = self.example_tangent_vector
e_line = perp_line.copy().rotate(90 * DEGREES)
c_line = Line(center, e_line.get_end())
c_line.set_stroke(WHITE, 4)
# lines = [c_line, e_line, perp_line]
tangency_point = line_intersection(
perp_line.get_start_and_end(),
c_line.get_start_and_end(),
)
tangency_point_dot = Dot(tangency_point)
tangency_point_dot.set_color(BLUE)
tangency_point_dot.save_state()
tangency_point_dot.scale(5)
tangency_point_dot.set_fill(opacity=0)
def indicate(line):
red_copy = line.copy().set_stroke(RED, 5)
return ShowPassingFlash(red_copy, run_time=2)
self.play(
self.ghost_lines.set_stroke, {"width": 0.5},
self.eccentric_lines.set_stroke, {"width": 0.5},
*list(map(WiggleOutThenIn, [e_line, c_line]))
)
for x in range(3):
self.play(
indicate(e_line),
indicate(c_line),
)
self.play(WiggleOutThenIn(perp_line))
for x in range(2):
self.play(indicate(perp_line))
self.play(Restore(tangency_point_dot))
self.add_foreground_mobjects(tangency_point_dot)
self.wait(2)
self.play(
arc_copy.scale, 0.15, {"about_point": center},
run_time=2
)
self.wait(2)
self.play(
perp_line.move_to, velocity_vector,
run_time=4,
rate_func=there_and_back_with_pause
)
self.wait()
self.play(
little_ellipse.replace, ellipse,
run_time=4,
rate_func=there_and_back_with_pause
)
self.wait()
# Helpers
def get_wedge(self, arc, center_point, opacity=0.8):
wedge = VMobject()
wedge.append_vectorized_mobject(
Line(center_point, arc.points[0])
)
wedge.append_vectorized_mobject(arc)
wedge.append_vectorized_mobject(
Line(arc.points[-1], center_point)
)
wedge.set_stroke(width=0)
wedge.set_fill(COBALT, opacity=opacity)
return wedge
class ShowSunVectorField(Scene):
def construct(self):
sun_center = IntroduceShapeOfVelocities.CONFIG["sun_center"]
vector_field = VectorField(
get_force_field_func((sun_center, -1))
)
vector_field.set_fill(opacity=0.8)
vector_field.sort_submobjects(
lambda p: -get_norm(p - sun_center)
)
for vector in vector_field:
vector.generate_target()
vector.target.set_fill(opacity=1)
vector.target.set_stroke(YELLOW, 0.5)
for x in range(3):
self.play(LaggedStart(
MoveToTarget, vector_field,
rate_func=there_and_back,
lag_ratio=0.5,
))
class TryToRememberProof(PiCreatureScene):
def construct(self):
randy = self.pi_creature
words = TextMobject("Oh god...how \\\\ did it go?")
words.next_to(randy, UP)
words.shift_onto_screen()
self.play(
randy.change, "telepath",
FadeInFromDown(words)
)
self.look_at(ORIGIN)
self.wait(2)
self.play(randy.change, "concerned_musician")
self.look_at(ORIGIN)
self.wait(3)
class PatYourselfOnTheBack(TeacherStudentsScene):
CONFIG = {
"camera_config": {"background_opacity": 1},
}
def construct(self):
self.teacher_says(
"Pat yourself \\\\ on the back!",
target_mode="hooray"
)
self.change_all_student_modes("happy")
self.wait(3)
self.play(
RemovePiCreatureBubble(
self.teacher,
target_mode="raise_right_hand"
),
self.get_student_changes(
*3 * ["pondering"],
look_at_arg=self.screen
)
)
self.look_at(UP)
self.wait(8)
self.change_student_modes(*3 * ["thinking"])
self.look_at(UP)
self.wait(12)
self.teacher_says("I just love this!")
feynman = ImageMobject("Feynman", height=4)
feynman.to_corner(UL)
chess = ImageMobject("ChessGameOfTheCentury")
chess.set_height(4)
chess.next_to(feynman)
self.play(FadeInFromDown(feynman))
self.wait()
self.play(
RemovePiCreatureBubble(self.teacher, target_mode="happy"),
FadeInFromDown(chess)
)
self.wait(2)
class Thumbnail(ShowEmergingEllipse):
CONFIG = {
"num_lines": 50,
}
def construct(self):
background = ImageMobject("Feynman_teaching")
background.set_width(FRAME_WIDTH)
background.scale(1.05)
background.to_corner(UR, buff=0)
background.shift(2 * UP)
self.add(background)
circle = self.get_circle()
circle.set_stroke(width=6)
circle.set_height(6.5)
circle.to_corner(UL)
circle.set_fill(BLACK, 0.9)
lines = self.get_lines()
lines.set_stroke(YELLOW, 5)
lines.set_color_by_gradient(YELLOW, RED)
ghost_lines = self.get_ghost_lines(lines)
for line in lines:
line.rotate(90 * DEGREES)
ellipse = self.get_ellipse()
ellipse.set_stroke(BLUE, 6)
sun = ImageMobject("sun", height=0.5)
sun.move_to(self.get_eccentricity_point())
circle_group = VGroup(circle, ghost_lines, lines, ellipse, sun)
self.add(circle_group)
l1 = Line(
circle.point_from_proportion(0.175),
6.25 * RIGHT + 0.75 * DOWN
)
l2 = Line(
circle.point_from_proportion(0.75),
6.25 * RIGHT + 2.5 * DOWN
)
l2a = VMobject().pointwise_become_partial(l2, 0, 0.56)
l2b = VMobject().pointwise_become_partial(l2, 0.715, 1)
expand_lines = VGroup(l1, l2a, l2b)
expand_lines.set_stroke("RED", 5)
self.add(expand_lines)
self.add(circle_group)
small_group = circle_group.copy()
small_group.scale(0.2)
small_group.stretch(1.35, 1)
small_group.move_to(6.2 * RIGHT + 1.6 * DOWN)
for mob in small_group:
if isinstance(mob, VMobject) and mob.get_stroke_width() > 1:
mob.set_stroke(width=1)
small_group[0].set_fill(opacity=0.25)
self.add(small_group)
title = TextMobject(
"Feynman's \\\\", "Lost \\\\", "Lecture",
alignment=""
)
title.scale(2.4)
for part in title:
part.add_to_back(
part.copy().set_stroke(BLACK, 12).set_fill(BLACK, 1)
)
title.to_corner(UR)
title[2].to_edge(RIGHT)
title[1].shift(0.9 * RIGHT)
title.shift(0.5 * LEFT)
self.add(title)