diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index f7cd3e77..8fda805c 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -13,6 +13,8 @@ class Orbiting(ContinualAnimation): 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): @@ -30,6 +32,32 @@ class Orbiting(ContinualAnimation): self.proportion = self.proportion % 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 + ) + # Animations @@ -37,7 +65,7 @@ class ShowEmergingEllipse(Scene): CONFIG = { "circle_radius": 3, "circle_color": BLUE, - "num_lines": 100, + "num_lines": 150, "lines_stroke_width": 1, "eccentricity_vector": 2 * RIGHT, "ghost_lines_stroke_color": LIGHT_GREY, @@ -128,7 +156,7 @@ class ShowEmergingEllipse(Scene): self.wait() self.play( ShowCreation(ellipse), - Write(ellipse_words, run_time=1) + FadeInFromDown(ellipse_words) ) self.wait() @@ -408,12 +436,361 @@ class FeynmanLecturesScreenCaptureFrame(Scene): 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): - pass + 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): - pass + sun = ImageMobject("sun") + sun.scale_to_fit_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.scale_to_fit_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.scale_to_fit_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 AskAboutEllipses(TheMotionOfPlanets): + CONFIG = { + "camera_config": {"background_opacity": 1}, + "animate_sun": True, + } + + 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=0.5) + self.sun = sun + self.add(sun) + if self.animate_sun: + self.add(SunAnimation(sun)) + + def add_orbit(self): + sun = self.sun + comet = ImageMobject("comet") + comet.scale_to_fit_height(0.2) + 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 = ContinualUpdateFromFunc( + 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.scale_to_fit_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 = ContinualUpdateFromFunc( + 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, + ) + ) + + # Force arrow + force_arrow = Arrow(LEFT, RIGHT, color=YELLOW) + + def update_force_arrow(arrow): + radius = radial_line.get_length() + # target_length = 1 / radius**2 + target_length = 1 / 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 = ContinualUpdateFromFunc( + force_arrow, update_force_arrow + ) + + 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.scale_to_fit_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_ellipse(self): + a = 7.0 + b = 4.0 + c = np.sqrt(a**2 - b**2) + ellipse = Circle(radius=a / 2) + ellipse.set_stroke(WHITE, 1) + ellipse.stretch(b / a, dim=1) + ellipse.move_to( + self.sun.get_center() + c * LEFT / 2 + ) + self.focus_points = [ + self.sun.get_center(), + self.sun.get_center() + c * LEFT, + ] + return ellipse + + +class FeynmanElementaryQuote(Scene): + def construct(self): + quote = TextMobject( + """ + I am going to give what I will call an + \\emph{elementary} demonstration. But elementary + does not mean easy to understand. Elementary + means that [nothing] very little is requried + to know ahead of time in order to understand it, + except to have an infinite amount of intelligence. + """, + tex_to_color_map={ + "\\emph{elementary}": BLUE, + "elementary": BLUE, + "Elementary": BLUE, + } + ) + + self.add(quote) +