From d2a20b8944c8d3f624932149a8157743aa9f4bdc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 Jul 2018 12:29:35 -0700 Subject: [PATCH 01/23] Beginning lost lecture project --- active_projects/lost_lecture.py | 52 +++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 active_projects/lost_lecture.py diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py new file mode 100644 index 00000000..1d004350 --- /dev/null +++ b/active_projects/lost_lecture.py @@ -0,0 +1,52 @@ +from __future__ import absolute_import +from big_ol_pile_of_manim_imports import * + + +class ShowEmergingEllipse(Scene): + def construct(self): + circle = Circle(num_anchors=50, radius=3, color=BLUE) + e_point = 2 * RIGHT + e_dot = Dot(e_point, color=YELLOW) + lines = VGroup(*[ + Line(e_point, circle.point_from_proportion(a)) + for a in np.linspace(0, 1, 4 * 49) + ]) + lines.set_stroke(width=1) + for line in lines: + line.generate_target() + line.target.rotate(90 * DEGREES) + + fade_rect = FullScreenFadeRectangle() + line = lines[20] + line_dot = Dot(line.get_center(), color=YELLOW) + + words = TextMobject("Rotate $90^\\circ$ \\\\ about center") + words.add_to_back(words.copy().set_stroke(BLACK, 2)) + words.next_to(line_dot, RIGHT) + + self.play( + LaggedStart(ShowCreation, lines), + Animation(VGroup(e_dot, circle)) + ) + self.add(lines.copy().set_stroke(LIGHT_GREY, 0.5)) + self.add(e_dot, circle) + self.wait() + self.play(FadeIn(fade_rect), Animation(line)) + self.play( + GrowFromCenter(line_dot), + FadeInFromDown(words) + ) + self.add_foreground_mobjects(line.copy().set_stroke(LIGHT_GREY, 0.5)) + self.play(MoveToTarget(line, path_arc=90 * DEGREES)) + self.wait() + self.play( + FadeOut(fade_rect), + FadeOut(line_dot), + FadeOut(words), + Animation(line) + ) + self.play( + LaggedStart(MoveToTarget, lines, run_time=4), + Animation(VGroup(e_dot, circle)) + ) + self.wait() \ No newline at end of file From d1e7cd3c56e9e1ea7bf368f6619dd9165b810fee Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 Jul 2018 21:43:31 -0700 Subject: [PATCH 02/23] Opening animation to lost lecture --- active_projects/lost_lecture.py | 417 ++++++++++++++++++++++++++++++-- 1 file changed, 392 insertions(+), 25 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 1d004350..f7cd3e77 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -2,51 +2,418 @@ from __future__ import absolute_import from big_ol_pile_of_manim_imports import * +class Orbiting(ContinualAnimation): + CONFIG = { + "rate": 0.3, + } + + 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 + ContinualAnimation.__init__(self, planet, **kwargs) + + def update_mobject(self, dt): + # time = self.internal_time + rate = self.rate + + planet = self.planet + star = self.star + ellipse = self.ellipse + + rate *= 1 / np.linalg.norm( + planet.get_center() - star.get_center() + ) + self.proportion += rate * dt + self.proportion = self.proportion % 1 + planet.move_to(ellipse.point_from_proportion(self.proportion)) + +# Animations + + class ShowEmergingEllipse(Scene): + CONFIG = { + "circle_radius": 3, + "circle_color": BLUE, + "num_lines": 100, + "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 = Circle(num_anchors=50, radius=3, color=BLUE) - e_point = 2 * RIGHT + circle = self.get_circle() + e_point = self.get_eccentricity_point() e_dot = Dot(e_point, color=YELLOW) - lines = VGroup(*[ - Line(e_point, circle.point_from_proportion(a)) - for a in np.linspace(0, 1, 4 * 49) - ]) - lines.set_stroke(width=1) - for line in lines: - line.generate_target() - line.target.rotate(90 * DEGREES) + lines = self.get_lines() + ellipse = self.get_ellipse() fade_rect = FullScreenFadeRectangle() - line = lines[20] + + line = lines[len(lines) / 5] line_dot = Dot(line.get_center(), color=YELLOW) + line_dot.scale(0.5) - words = TextMobject("Rotate $90^\\circ$ \\\\ about center") - words.add_to_back(words.copy().set_stroke(BLACK, 2)) - words.next_to(line_dot, RIGHT) + ghost_line = self.get_ghost_lines(line) + ghost_lines = self.get_ghost_lines(lines) - self.play( - LaggedStart(ShowCreation, lines), - Animation(VGroup(e_dot, circle)) + rot_words = TextMobject("Rotate $90^\\circ$ \\\\ about center") + rot_words.next_to(line_dot, RIGHT) + + 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 ) - self.add(lines.copy().set_stroke(LIGHT_GREY, 0.5)) + elbow.shift(line.get_center()) + + 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)) self.play( + FadeIn(fade_rect), + Animation(line), GrowFromCenter(line_dot), - FadeInFromDown(words) + FadeInFromDown(rot_words), + ) + self.wait() + self.add(ghost_line) + self.play( + MoveToTarget(line, path_arc=90 * DEGREES), + Animation(rot_words), + ShowCreation(elbow) ) - self.add_foreground_mobjects(line.copy().set_stroke(LIGHT_GREY, 0.5)) - self.play(MoveToTarget(line, path_arc=90 * DEGREES)) self.wait() self.play( FadeOut(fade_rect), FadeOut(line_dot), - FadeOut(words), - Animation(line) + 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() \ No newline at end of file + self.wait() + self.play( + ShowCreation(ellipse), + Write(ellipse_words, run_time=1) + ) + 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_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 = np.linalg.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 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.scale_to_fit_width(0.3) + + feynman_cut = ImageMobject("FeynmanCut") + feynman_cut.scale_to_fit_height(4) + feynman_cut.next_to(ORIGIN, LEFT) + feynman_cut.shift(UP) + feynman_name = TextMobject("Richard Feynman") + feynman_name.next_to(feynman_cut, DOWN) + feynman_cut.save_state() + feynman_cut.shift(2 * DOWN) + feynman_rect = BackgroundRectangle( + feynman_cut, 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_cut.restore, + MaintainPositionRelativeTo(feynman_rect, feynman_cut), + 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.scale_to_fit_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.scale_to_fit_height(1) + bongo.set_color(WHITE) + bongo.next_to(safe, RIGHT, LARGE_BUFF) + + objects = VGroup(safe, bongo) + + feynman_smile = ImageMobject("Feynman_smile") + feynman_smile.match_width(objects) + 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_teacher") + feynman_teacher.scale_to_fit_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): + def construct(self): + self.add_title() + self.setup_orbits() + + def add_title(self): + pass + + def setup_orbits(self): + pass From aa6194cbbfb3f29ea4e1d4fe62edf5052198698c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 12 Jul 2018 15:27:36 -0700 Subject: [PATCH 03/23] Up to AskAboutEllipses in lost_lecture project --- active_projects/lost_lecture.py | 385 +++++++++++++++++++++++++++++++- 1 file changed, 381 insertions(+), 4 deletions(-) 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) + From 08b8a75d2fc185a7815de7a32b1384fa1186077f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 12 Jul 2018 19:53:30 -0700 Subject: [PATCH 04/23] Up to AskAboutInfiniteIntelligence in lost_lecture --- active_projects/lost_lecture.py | 183 ++++++++++++++++++++++++++++---- 1 file changed, 164 insertions(+), 19 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 8fda805c..b6d83036 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -58,6 +58,36 @@ class SunAnimation(ContinualAnimation): 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 @@ -223,16 +253,16 @@ class FeynmanAndOrbitingPlannetOnEllipseDiagram(ShowEmergingEllipse): comet = ImageMobject("earth") comet.scale_to_fit_width(0.3) - feynman_cut = ImageMobject("FeynmanCut") - feynman_cut.scale_to_fit_height(4) - feynman_cut.next_to(ORIGIN, LEFT) - feynman_cut.shift(UP) + feynman = ImageMobject("Feynman") + feynman.scale_to_fit_height(6) + feynman.next_to(ORIGIN, LEFT) + feynman.to_edge(UP) feynman_name = TextMobject("Richard Feynman") - feynman_name.next_to(feynman_cut, DOWN) - feynman_cut.save_state() - feynman_cut.shift(2 * DOWN) + feynman_name.next_to(feynman, DOWN) + feynman.save_state() + feynman.shift(2 * DOWN) feynman_rect = BackgroundRectangle( - feynman_cut, fill_opacity=1 + feynman, fill_opacity=1 ) group = VGroup(circle, ghost_lines, lines, e_dot, ellipse) @@ -242,8 +272,8 @@ class FeynmanAndOrbitingPlannetOnEllipseDiagram(ShowEmergingEllipse): self.add_foreground_mobjects(comet) self.wait() self.play( - feynman_cut.restore, - MaintainPositionRelativeTo(feynman_rect, feynman_cut), + feynman.restore, + MaintainPositionRelativeTo(feynman_rect, feynman), VFadeOut(feynman_rect), group.to_edge, RIGHT, ) @@ -300,7 +330,7 @@ class FeynmanFame(Scene): objects = VGroup(safe, bongo) - feynman_smile = ImageMobject("Feynman_smile") + feynman_smile = ImageMobject("Feynman_Los_Alamos") feynman_smile.match_width(objects) feynman_smile.next_to(objects, DOWN) @@ -335,7 +365,7 @@ class FeynmanFame(Scene): ) # As a teacher - feynman_teacher = ImageMobject("Feynman_teacher") + feynman_teacher = ImageMobject("Feynman_teaching") feynman_teacher.scale_to_fit_width(FRAME_WIDTH / 2 - 1) feynman_teacher.next_to(ORIGIN, RIGHT) @@ -651,7 +681,7 @@ class AskAboutEllipses(TheMotionOfPlanets): radius_in_denominator, lambda a: radial_line.get_length(), position_update_func=lambda mob: mob.move_to( - radius_in_denominator_ref, + radius_in_denominator_ref, LEFT ) ) @@ -776,21 +806,136 @@ class AskAboutEllipses(TheMotionOfPlanets): class FeynmanElementaryQuote(Scene): def construct(self): - quote = TextMobject( - """ + 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 [nothing] very little is requried + 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 = filter(lambda s: s, quote_text.split(" ")) + 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.scale_to_fit_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) + + +class LostLecturePicture(TODOStub): + CONFIG = {"camera_config": {"background_opacity": 1}} + + def construct(self): + picture = ImageMobject("Feynman_teaching") + picture.scale_to_fit_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]) ) - self.add(quote) +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( + "It's not too bad, \\\\ but stay focused", + added_anims=[self.get_student_changes(*["happy"] * 3)] + ) + self.wait() + self.look_at(self.screen) + self.wait(5) + + +class ShowEllipseDefiningProperty(Scene): + CONFIG = { + "ellipse_color": BLUE, + "a": 4.0, + "b": 3.0, + } + + def construct(self): + self.add_ellipse() + self.add_focal_lines() + self.add_distance_labels() + self.show_constant_sum() + self.label_foci() + self.label_focal_sum() + + def add_ellipse(self): + a = self.a + b = self.b + c = np.sqrt(a**2 - b**2) + ellipse = Circle(radius=a, color=self.ellipse_color) + ellipse.stretch(fdiv(b, a), dim=1) + self.foci = [c * LEFT, c * RIGHT] + self.ellipse = ellipse + self.add(ellipse) + + def add_focal_lines(self): + pass + + def add_distance_labels(self): + pass + + def show_constant_sum(self): + pass + + def label_foci(self): + pass + + def label_focal_sum(self): + pass From 393bfc68cedb0b9966c1644ab8f07106f144288a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 Jul 2018 10:31:29 -0700 Subject: [PATCH 05/23] Added Restore animation --- animation/transform.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/animation/transform.py b/animation/transform.py index 5682b120..45e18cf3 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -158,6 +158,11 @@ class ScaleInPlace(ApplyMethod): scale_factor, **kwargs) +class Restore(ApplyMethod): + def __init__(self, mobject, **kwargs): + ApplyMethod.__init__(self, mobject.restore, **kwargs) + + class ApplyFunction(Transform): CONFIG = { "submobject_mode": "all_at_once", From 3ebad5a4252fbb707a309c0ecc9afa09de71939f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 Jul 2018 10:31:56 -0700 Subject: [PATCH 06/23] Style fix to exponential_decay --- utils/rate_functions.py | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/utils/rate_functions.py b/utils/rate_functions.py index e8d2bf3a..5610319d 100644 --- a/utils/rate_functions.py +++ b/utils/rate_functions.py @@ -84,23 +84,8 @@ def squish_rate_func(func, a=0.4, b=0.6): def lingering(t): return squish_rate_func(lambda t: t, 0, 0.8)(t) -def exponential_decay(t, half_life = 0.1): -# The half-life should be rather small to minimize the cut-off error at the end - return 1 - np.exp(-t/half_life) - - - - - - - - - - - - - - - - +def exponential_decay(t, half_life=0.1): + # The half-life should be rather small to minimize + # the cut-off error at the end + return 1 - np.exp(-t / half_life) From a76bea6d725f112231286de02954ddc43d0aacee Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 Jul 2018 10:32:13 -0700 Subject: [PATCH 07/23] Added line_intersection function --- utils/space_ops.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/utils/space_ops.py b/utils/space_ops.py index 33e1e2c9..46573c39 100644 --- a/utils/space_ops.py +++ b/utils/space_ops.py @@ -118,3 +118,24 @@ def complex_func_to_R3_func(complex_func): def center_of_mass(points): points = [np.array(point).astype("float") for point in points] return sum(points) / len(points) + + +def line_intersection(line1, line2): + """ + return intersection point of two lines, + each defined with a pair of vectors determining + the end points + """ + x_diff = (line1[0][0] - line1[1][0], line2[0][0] - line2[1][0]) + y_diff = (line1[0][1] - line1[1][1], line2[0][1] - line2[1][1]) + + def det(a, b): + return a[0] * b[1] - a[1] * b[0] + + div = det(x_diff, y_diff) + if div == 0: + raise Exception("Lines do not intersect") + d = (det(*line1), det(*line2)) + x = det(d, x_diff) / div + y = det(d, y_diff) / div + return np.array([x, y, 0]) From 0f9e1796c76736c9bc1ce355becd5e72a522fd81 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 Jul 2018 10:32:39 -0700 Subject: [PATCH 08/23] Added (somewhat large) ProveEllipse scene --- active_projects/lost_lecture.py | 907 +++++++++++++++++++++++++++++++- 1 file changed, 890 insertions(+), 17 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index b6d83036..7f21ecb6 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -122,14 +122,7 @@ class ShowEmergingEllipse(Scene): rot_words = TextMobject("Rotate $90^\\circ$ \\\\ about center") rot_words.next_to(line_dot, RIGHT) - 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()) + elbow = self.get_elbow(line) eccentric_words = TextMobject("``Eccentric'' point") eccentric_words.next_to(circle.get_center(), DOWN) @@ -223,6 +216,17 @@ class ShowEmergingEllipse(Scene): 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() @@ -331,7 +335,7 @@ class FeynmanFame(Scene): objects = VGroup(safe, bongo) feynman_smile = ImageMobject("Feynman_Los_Alamos") - feynman_smile.match_width(objects) + feynman_smile.scale_to_fit_height(4) feynman_smile.next_to(objects, DOWN) VGroup(objects, feynman_smile).next_to(ORIGIN, RIGHT) @@ -902,40 +906,909 @@ class AskAboutInfiniteIntelligence(TeacherStudentsScene): 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.show_constant_sum() self.label_foci() self.label_focal_sum() def add_ellipse(self): a = self.a b = self.b - c = np.sqrt(a**2 - b**2) ellipse = Circle(radius=a, color=self.ellipse_color) ellipse.stretch(fdiv(b, a), dim=1) - self.foci = [c * LEFT, c * RIGHT] + ellipse.to_edge(LEFT, buff=LARGE_BUFF) self.ellipse = ellipse self.add(ellipse) def add_focal_lines(self): - pass + 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 = ContinualUpdateFromFunc( + 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): - pass + lines = self.focal_lines + colors = [YELLOW, PINK] - def show_constant_sum(self): - pass + 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): - pass + 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(*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.scale_to_fit_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 = ContinualUpdateFromFunc( + 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.scale_to_fit_width(max_width) + new_decimal.next_to(line, UP, SMALL_BUFF) + new_decimal.set_color(color) + VGroup(new_decimal, line).rotate( + -angle, about_point=ORIGIN + ) + label.submobjects = list(new_decimal.submobjects) + + distance_labels_animation = ContinualUpdateFromFunc( + 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): + def construct(self): + word = TextMobject("Geometry proof land") + word.rotate(-90 * DEGREES) + word.scale(0.25) + word.shift(3 * RIGHT) + word.apply_complex_function(np.exp) + word.rotate(90 * DEGREES) + word.scale_to_fit_width(9) + word.center() + word.to_edge(UP) + colors = [ + PINK, RED, YELLOW, GREEN, GREEN_A, BLUE, + MAROON_E, MAROON_B, YELLOW, BLUE, + ] + word.set_color_by_gradient(*colors) + + word_outlines = word.deepcopy() + word_outlines.set_fill(opacity=0) + word_outlines.set_stroke(WHITE, 1) + random.shuffle(colors) + word_outlines.set_color_by_gradient(*colors) + + 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.play(LaggedStart( + ShowCreationThenDestruction, word_outlines, + run_time=4 + )) + self.wait() + + +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 = map(Mobject.get_color, dots) + + def get_foci(): + return 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_bottom(), + circle.point_from_proportion(0.9), + ellipse.get_top(), + ellipse.point_from_proportion(0.7), + ] + for point in points: + self.play( + focal_sum_point.move_to, point + ) + self.wait() + self.remove(*to_add) + self.play(*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), + line.set_stroke, {"width": 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 = ContinualUpdateFromFunc( + 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 = ContinualUpdateFromFunc( + 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 = ContinualUpdateFromFunc( + 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(PINK, GREEN) + 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 = ContinualUpdateFromFunc( + 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(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) + 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.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(*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.scale_to_fit_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 = np.linalg.norm( + intersection - planet.get_center() + ) + if distance < 0.025: + line.set_color(BLUE) + self.add(line) + else: + line.set_color(WHITE) + + lines_update_animation = ContinualUpdateFromFunc( + lines, update_lines + ) + + self.add(orbit) + self.add(lines_update_animation) + self.add_foreground_mobjects(planet) + self.wait(12) + + +class EndOfGeometryProofiness(Scene): + def construct(self): pass From ab90c42ce12cfbc6b7cabe412b7a26bb77459aa3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 Jul 2018 10:45:29 -0700 Subject: [PATCH 09/23] Added FadeOutAndShift animation --- animation/creation.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/animation/creation.py b/animation/creation.py index 7172c0c8..b50d2af0 100644 --- a/animation/creation.py +++ b/animation/creation.py @@ -144,9 +144,14 @@ class FadeIn(Transform): class FadeInAndShiftFromDirection(Transform): - def __init__(self, mobject, direction=DOWN, **kwargs): + CONFIG = { + "direction": DOWN, + } + + def __init__(self, mobject, direction=None, **kwargs): digest_config(self, kwargs) target = mobject.copy() + direction = direction or self.direction mobject.shift(direction) mobject.fade(1) Transform.__init__(self, mobject, target, **kwargs) @@ -161,6 +166,23 @@ class FadeInFromDown(FadeInAndShiftFromDirection): } +class FadeOutAndShift(FadeOut): + CONFIG = { + "direction": DOWN, + } + + def __init__(self, mobject, direction=None, **kwargs): + FadeOut.__init__(self, mobject, **kwargs) + direction = direction or self.direction + self.target_mobject.shift(direction) + + +class FadeOutAndShiftDown(FadeOutAndShift): + CONFIG = { + "direction": DOWN, + } + + class VFadeIn(Animation): """ VFadeIn and VFadeOut only work for VMobjects, but they can be applied From 60a6b0aaa3b6eb01eaec2d5ef60f3aa302a9a3e1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 16 Jul 2018 12:37:45 -0700 Subject: [PATCH 10/23] Beginning KeplersSecondLaw scene --- active_projects/lost_lecture.py | 278 ++++++++++++++++++++++++++++---- 1 file changed, 250 insertions(+), 28 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 7f21ecb6..38de6dbb 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from big_ol_pile_of_manim_imports import * +COBALT = "#0047AB" class Orbiting(ContinualAnimation): CONFIG = { @@ -576,7 +577,12 @@ class TheMotionOfPlanets(Scene): class AskAboutEllipses(TheMotionOfPlanets): CONFIG = { "camera_config": {"background_opacity": 1}, + "sun_center": ORIGIN, "animate_sun": True, + "a": 3.5, + "b": 2.0, + "ellipse_color": WHITE, + "ellipse_stroke_width": 1, } def construct(self): @@ -595,6 +601,7 @@ class AskAboutEllipses(TheMotionOfPlanets): def add_sun(self): sun = ImageMobject("sun", height=0.5) + sun.move_to(self.sun_center) self.sun = sun self.add(sun) if self.animate_sun: @@ -792,12 +799,15 @@ class AskAboutEllipses(TheMotionOfPlanets): # Helpers def get_ellipse(self): - a = 7.0 - b = 4.0 + a = self.a + b = self.b c = np.sqrt(a**2 - b**2) - ellipse = Circle(radius=a / 2) - ellipse.set_stroke(WHITE, 1) - ellipse.stretch(b / a, dim=1) + 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 / 2 ) @@ -1141,6 +1151,9 @@ class ShowEllipseDefiningProperty(Scene): new_decimal.scale_to_fit_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 ) @@ -1187,25 +1200,19 @@ class ShowEllipseDefiningProperty(Scene): class GeometryProofLand(Scene): - def construct(self): - word = TextMobject("Geometry proof land") - word.rotate(-90 * DEGREES) - word.scale(0.25) - word.shift(3 * RIGHT) - word.apply_complex_function(np.exp) - word.rotate(90 * DEGREES) - word.scale_to_fit_width(9) - word.center() - word.to_edge(UP) - colors = [ + CONFIG = { + "colors": [ PINK, RED, YELLOW, GREEN, GREEN_A, BLUE, MAROON_E, MAROON_B, YELLOW, BLUE, - ] - word.set_color_by_gradient(*colors) + ], + } + 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) @@ -1230,6 +1237,19 @@ class GeometryProofLand(Scene): )) self.wait() + def get_geometry_proof_land_word(self): + word = TextMobject("Geometry proof land") + word.rotate(-90 * DEGREES) + word.scale(0.25) + word.shift(3 * RIGHT) + word.apply_complex_function(np.exp) + word.rotate(90 * DEGREES) + word.scale_to_fit_width(9) + word.center() + word.to_edge(UP) + word.set_color_by_gradient(*self.colors) + return word + class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): CONFIG = { @@ -1374,10 +1394,10 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): self.add(*to_add) points = [ - ellipse.get_bottom(), - circle.point_from_proportion(0.9), ellipse.get_top(), - ellipse.point_from_proportion(0.7), + circle.point_from_proportion(0.2), + ellipse.point_from_proportion(0.2), + ellipse.point_from_proportion(0.4), ] for point in points: self.play( @@ -1555,7 +1575,7 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): self.get_eccentricity_point() ] ]) - triangles.set_color_by_gradient(PINK, GREEN) + triangles.set_color_by_gradient(RED_C, COBALT) triangles.set_stroke(WHITE, 2) # Add even more distant label updates @@ -1609,7 +1629,7 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): ) elbow_update_animation = UpdateFromFunc( elbow, - lambda e: Transform(e, self.get_elbow(line)).update(1) + lambda e: Transform(e, self.get_elbow(ghost_line)).update(1) ) P_dot_movement_updates = [ @@ -1638,7 +1658,7 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): # Why is this needed?!? self.add(*self.focal_sum_things_to_add) - self.wait(0) + self.wait(0.01) self.remove(*self.focal_sum_things_to_add) # Show label @@ -1659,6 +1679,7 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): FadeInFromDown(Q_label), GrowFromCenter(Q_dot) ) + self.wait() self.add_foreground_mobjects(Q_dot) self.add(Q_label_animation) self.play( @@ -1794,10 +1815,10 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): intersection - planet.get_center() ) if distance < 0.025: - line.set_color(BLUE) + line.set_stroke(BLUE, 3) self.add(line) else: - line.set_color(WHITE) + line.set_stroke(WHITE, 1) lines_update_animation = ContinualUpdateFromFunc( lines, update_lines @@ -1809,6 +1830,207 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): self.wait(12) -class EndOfGeometryProofiness(Scene): +class EndOfGeometryProofiness(GeometryProofLand): def construct(self): - pass + 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": 3 * RIGHT + DOWN, + "animate_sun": False, + "a": 5.0, + "b": 2.5, + "ellipse_stroke_width": 2, + "area_color": COBALT, + "area_opacity": 0.75, + "arc_color": YELLOW, + "arc_stroke_width": 3, + } + + def construct(self): + self.add_title() + self.add_sun() + self.add_foreground_mobjects(self.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 + + def show_several_sweeps(self): + n_sweeps = 3 + shown_areas = VGroup() + for x in range(n_sweeps): + self.wait() + area = self.show_area_sweep() + shown_areas.add(area) + self.wait(2) + self.play(FadeOut(shown_areas)) + + + def contrast_close_to_far(self): + orbit = self.orbit + sun_point = self.sun.get_center() + + start_prop = 0.8 + self.wait_until_proportion(start_prop) + area = self.show_area_sweep() + end_prop = max(0.9, orbit.proportion) + arc = self.get_arc(start_prop, end_prop) + radius = Line(sun_point, arc.points[0]) + radius.set_color(PINK) + + + radius_words = self.get_radius_words( + radius, "Short" + ) + + arc_words = TextMobject("Long arc") + angle = 9 * DEGREES + arc_words.rotate(angle) + arc_words.scale(0.1) + vect = rotate_vector(RIGHT, angle) + arc_words.next_to(vect, vect) + arc_words.apply_complex_function(np.exp) + arc_words.scale(2) + arc_words.next_to( + arc.point_from_proportion(0.5), + rotate_vector(vect, 90 * DEGREES), + buff=-MED_SMALL_BUFF, + ) + 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.4 + self.wait_until_proportion(start_prop) + area = self.show_area_sweep() + end_prop = max(0.45, orbit.proportion) + arc = self.get_arc(start_prop, end_prop) + radius = Line(sun_point, arc.points[0]) + radius.set_color(PINK) + radius_words = self.get_radius_words(radius, "Long") + + # 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): + sun_point = self.sun.get_center() + 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 < prop: + self.wait(0.2) + + def get_radius_words(self, radius, adjective): + radius_words = TextMobject( + "%s radius" % adjective, + ) + radius_words.scale_to_fit_width( + 0.8 * radius.get_length() + ) + radius_words.match_color(radius) + radius_words.next_to(ORIGIN, DOWN, SMALL_BUFF) + radius_words.rotate(radius.get_angle(), about_point=ORIGIN) + radius_words.shift(radius.get_center()) + return radius_words From c9b71afb85d22aca5e1c0b858e870d1e7a3c5e05 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 16 Jul 2018 19:42:06 -0700 Subject: [PATCH 11/23] Added PI constant --- constants.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/constants.py b/constants.py index 5a4e29bc..0298b618 100644 --- a/constants.py +++ b/constants.py @@ -106,7 +106,8 @@ BOTTOM = FRAME_Y_RADIUS * DOWN LEFT_SIDE = FRAME_X_RADIUS * LEFT RIGHT_SIDE = FRAME_X_RADIUS * RIGHT -TAU = 2 * np.pi +PI = np.pi +TAU = 2 * PI DEGREES = TAU / 360 ANIMATIONS_DIR = os.path.join(MEDIA_DIR, "animations") From 61d5f6f75908e77f4acb9864e98e25a2a59dbbe0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 16 Jul 2018 19:42:26 -0700 Subject: [PATCH 12/23] Up to AngularMomentumArgument --- active_projects/lost_lecture.py | 478 ++++++++++++++++++++++++++++---- 1 file changed, 430 insertions(+), 48 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 38de6dbb..3cdc24fb 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -1,8 +1,12 @@ from __future__ import absolute_import from big_ol_pile_of_manim_imports import * +from active_projects.div_curl import VectorField +from active_projects.div_curl import get_force_field_func + COBALT = "#0047AB" + class Orbiting(ContinualAnimation): CONFIG = { "rate": 0.3, @@ -577,12 +581,14 @@ class TheMotionOfPlanets(Scene): 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): @@ -600,17 +606,20 @@ class AskAboutEllipses(TheMotionOfPlanets): self.title = title def add_sun(self): - sun = ImageMobject("sun", height=0.5) + sun = ImageMobject("sun", height=self.sun_height) sun.move_to(self.sun_center) self.sun = sun self.add(sun) if self.animate_sun: - self.add(SunAnimation(sun)) + sun_animation = SunAnimation(sun) + self.add(sun_animation) + self.add_foreground_mobjects( + sun_animation.mobject + ) def add_orbit(self): sun = self.sun - comet = ImageMobject("comet") - comet.scale_to_fit_height(0.2) + comet = self.get_comet() ellipse = self.get_ellipse() orbit = Orbiting(comet, sun, ellipse) @@ -798,6 +807,11 @@ class AskAboutEllipses(TheMotionOfPlanets): self.wait(6) # Helpers + def get_comet(self): + comet = ImageMobject("comet") + comet.scale_to_fit_height(self.comet_height) + return comet + def get_ellipse(self): a = self.a b = self.b @@ -809,7 +823,7 @@ class AskAboutEllipses(TheMotionOfPlanets): ) ellipse.stretch(fdiv(b, a), dim=1) ellipse.move_to( - self.sun.get_center() + c * LEFT / 2 + self.sun.get_center() + c * LEFT, ) self.focus_points = [ self.sun.get_center(), @@ -1848,75 +1862,75 @@ class EndOfGeometryProofiness(GeometryProofLand): class KeplersSecondLaw(AskAboutEllipses): CONFIG = { - "sun_center": 3 * RIGHT + DOWN, - "animate_sun": False, + "sun_center": 4 * RIGHT + 0.75 * DOWN, + "animate_sun": True, "a": 5.0, - "b": 2.5, + "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_foreground_mobjects(self.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 = 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): - n_sweeps = 3 shown_areas = VGroup() - for x in range(n_sweeps): + for x in range(self.n_sample_sweeps): self.wait() area = self.show_area_sweep() shown_areas.add(area) - self.wait(2) - self.play(FadeOut(shown_areas)) - + 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.8 + start_prop = 0.9 self.wait_until_proportion(start_prop) - area = self.show_area_sweep() - end_prop = max(0.9, orbit.proportion) + 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(PINK) + radius.set_color(WHITE) - - radius_words = self.get_radius_words( - radius, "Short" - ) + radius_words = self.get_radius_words(radius, "Short") + radius_words.next_to(radius.get_center(), LEFT, SMALL_BUFF) arc_words = TextMobject("Long arc") - angle = 9 * DEGREES - arc_words.rotate(angle) - arc_words.scale(0.1) - vect = rotate_vector(RIGHT, angle) - arc_words.next_to(vect, vect) + 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(2) + arc_words.scale(0.8) arc_words.next_to( - arc.point_from_proportion(0.5), - rotate_vector(vect, 90 * DEGREES), - buff=-MED_SMALL_BUFF, + arc, RIGHT, buff=-SMALL_BUFF ) arc_words.match_color(arc) @@ -1934,14 +1948,30 @@ class KeplersSecondLaw(AskAboutEllipses): # Show narrow arc # (Code repetition...uck) - start_prop = 0.4 + start_prop = 0.475 self.wait_until_proportion(start_prop) - area = self.show_area_sweep() - end_prop = max(0.45, orbit.proportion) - arc = self.get_arc(start_prop, end_prop) - radius = Line(sun_point, arc.points[0]) - radius.set_color(PINK) - radius_words = self.get_radius_words(radius, "Long") + 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): @@ -1986,7 +2016,6 @@ class KeplersSecondLaw(AskAboutEllipses): return result def get_arc(self, prop1, prop2): - sun_point = self.sun.get_center() ellipse = self.get_ellipse() prop1 = prop1 % 1.0 prop2 = prop2 % 1.0 @@ -2019,18 +2048,371 @@ class KeplersSecondLaw(AskAboutEllipses): if self.skip_animations: self.orbit.proportion = prop else: - while self.orbit.proportion < prop: - self.wait(0.2) + while (self.orbit.proportion % 1) < prop: + self.wait(self.frame_duration) def get_radius_words(self, radius, adjective): radius_words = TextMobject( "%s radius" % adjective, ) - radius_words.scale_to_fit_width( - 0.8 * radius.get_length() - ) + min_width = 0.8 * radius.get_length() + if radius_words.get_width() > min_width: + radius_words.scale_to_fit_width(min_width) radius_words.match_color(radius) - radius_words.next_to(ORIGIN, DOWN, SMALL_BUFF) - radius_words.rotate(radius.get_angle(), about_point=ORIGIN) + 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 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: -np.linalg.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): + def construct(self): + pass From f4022d6c750d6cb54dc9f9612695ed9f99939a70 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 16 Jul 2018 22:22:43 -0700 Subject: [PATCH 13/23] Fixed small bug with FadeInAndShiftFromDirection --- animation/creation.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/animation/creation.py b/animation/creation.py index b50d2af0..0801ebe1 100644 --- a/animation/creation.py +++ b/animation/creation.py @@ -151,7 +151,8 @@ class FadeInAndShiftFromDirection(Transform): def __init__(self, mobject, direction=None, **kwargs): digest_config(self, kwargs) target = mobject.copy() - direction = direction or self.direction + if direction is None: + direction = self.direction mobject.shift(direction) mobject.fade(1) Transform.__init__(self, mobject, target, **kwargs) @@ -173,7 +174,8 @@ class FadeOutAndShift(FadeOut): def __init__(self, mobject, direction=None, **kwargs): FadeOut.__init__(self, mobject, **kwargs) - direction = direction or self.direction + if direction is None: + direction = self.direction self.target_mobject.shift(direction) From a443ddc8f288c970b7bf3ebe2426a33bca2f860d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 16 Jul 2018 22:22:59 -0700 Subject: [PATCH 14/23] Scenes up to IntroduceShapeOfVelocities --- active_projects/lost_lecture.py | 332 ++++++++++++++++++++++++++++++-- 1 file changed, 314 insertions(+), 18 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 3cdc24fb..6069c567 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -706,23 +706,8 @@ class AskAboutEllipses(TheMotionOfPlanets): ) # 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 + force_arrow, force_arrow_animation = self.get_force_arrow_and_update( + self.comet ) inverse_square_law_words = TextMobject( @@ -831,6 +816,43 @@ class AskAboutEllipses(TheMotionOfPlanets): ] 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 = ContinualUpdateFromFunc( + 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 = ContinualUpdateFromFunc( + line, lambda l: l.put_start_and_end_on( + self.sun.get_center(), + comet.get_center(), + ) + ) + return line, line_update + class FeynmanElementaryQuote(Scene): def construct(self): @@ -928,6 +950,47 @@ class AskAboutInfiniteIntelligence(TeacherStudentsScene): 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(*filter( + lambda m: m is not item, + items + )) + new_item = item.copy() + new_item.scale(scale_factor, about_edge=LEFT) + new_item.set_fill(WHITE, 1) + self.play( + Transform(item, new_item), + *map(MoveToTarget, other_items) + ) + self.wait() + + class ShowEllipseDefiningProperty(Scene): CONFIG = { "camera_config": {"background_opacity": 1}, @@ -2414,5 +2477,238 @@ class HistoryOfAngularMomentum(TeacherStudentsScene): class FeynmanRecountingNewton(Scene): + CONFIG = { + "camera_config": {"background_opacity": 1}, + } + def construct(self): - pass + feynman_teaching = ImageMobject("Feynman_teaching") + feynman_teaching.scale_to_fit_width(FRAME_WIDTH) + + newton = ImageMobject("Newton") + principia = ImageMobject("Principia_equal_area") + images = [newton, principia] + for image in images: + image.scale_to_fit_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": False, + "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=6): + return 1 * (np.exp((1.0 / c) * (z) + 1) - np.exp(1)) + + ellipse = self.ellipse + ellipse.save_state() + ellipse.generate_target() + ellipse.target.apply_complex_function(func) + ellipse.target.replace(ellipse, dim_to_match=0) + + 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 = self.get_velocity_vector(0) + + def update_moving_vector(vector): + new_vector = self.get_velocity_vector( + self.orbit.proportion, + # scalar=4.0, + ) + Transform(vector, new_vector).update(1) + + moving_vector_animation = ContinualUpdateFromFunc( + moving_vector, update_moving_vector + ) + + 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) + + 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 = np.linalg.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 From 0b290a19b958aa299c8db3092029570b270121ce Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jul 2018 12:44:18 -0700 Subject: [PATCH 15/23] Added Line.get_unit_vector --- mobject/geometry.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mobject/geometry.py b/mobject/geometry.py index d250db56..37879d6a 100644 --- a/mobject/geometry.py +++ b/mobject/geometry.py @@ -368,6 +368,14 @@ class Line(VMobject): def get_vector(self): return self.get_end() - self.get_start() + def get_unit_vector(self): + vect = self.get_vector() + norm = np.linalg.norm(vect) + if norm == 0: + # TODO, is this the behavior I want? + return np.array(ORIGIN) + return vect / norm + def get_start(self): return np.array(self.points[0]) From 7a8ff97cd78f6e7225333d836a80551c3a471176 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jul 2018 12:44:33 -0700 Subject: [PATCH 16/23] Halfway through ShowEqualAngleSlices --- active_projects/lost_lecture.py | 315 ++++++++++++++++++++++++++++++-- 1 file changed, 304 insertions(+), 11 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 6069c567..82481ec5 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -9,7 +9,7 @@ COBALT = "#0047AB" class Orbiting(ContinualAnimation): CONFIG = { - "rate": 0.3, + "rate": 7.5, } def __init__(self, planet, star, ellipse, **kwargs): @@ -24,18 +24,28 @@ class Orbiting(ContinualAnimation): def update_mobject(self, dt): # time = self.internal_time - rate = self.rate planet = self.planet star = self.star ellipse = self.ellipse - rate *= 1 / np.linalg.norm( - planet.get_center() - star.get_center() + rate = self.rate + radius_vector = planet.get_center() - star.get_center() + rate *= 1.0 / np.linalg.norm(radius_vector) + + prop = self.proportion + d_prop = 0.001 + ds = np.linalg.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) ) - self.proportion += rate * dt - self.proportion = self.proportion % 1 - planet.move_to(ellipse.point_from_proportion(self.proportion)) class SunAnimation(ContinualAnimation): @@ -812,7 +822,7 @@ class AskAboutEllipses(TheMotionOfPlanets): ) self.focus_points = [ self.sun.get_center(), - self.sun.get_center() + c * LEFT, + self.sun.get_center() + 2 * c * LEFT, ] return ellipse @@ -2515,7 +2525,7 @@ class FeynmanRecountingNewton(Scene): class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): CONFIG = { - "animate_sun": False, + "animate_sun": True, "sun_center": 2 * RIGHT, "a": 4.0, "b": 3.5, @@ -2535,14 +2545,15 @@ class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): self.add_foreground_mobjects(self.comet) def warp_orbit(self): - def func(z, c=6): + 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=0) + ellipse.target.replace(ellipse, dim_to_match=1) self.wait(5) self.play(MoveToTarget(ellipse, run_time=2)) @@ -2646,6 +2657,7 @@ class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): circle = Circle(color=YELLOW) circle.replace(vector_targets) + circle.scale(1.04) velocity_space = TextMobject("Velocity space") velocity_space.next_to(circle, UP) @@ -2712,3 +2724,284 @@ class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): vector.copy().set_stroke(BLACK, 5) ) return vector + + +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": False, + "theta": 30 * DEGREES, + } + + def construct(self): + self.setup_orbit() + self.show_equal_angle_slices() + self.label_points_P_theta() + self.ask_about_time_per_slice() + self.areas_are_proportional_to_radius_squared() + self.show_inverse_square_law() + self.show_change_in_velocity() + self.directly_compare_velocity_vectors() + self.show_equal_angle_changes() + + 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 / np.linalg.norm(v1), + v2 / np.linalg.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), + *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 label_points_P_theta(self): + sun_center = self.sun.get_center() + lines = self.lines + thick_lines = lines.copy().set_stroke(YELLOW, 4) + + frame_scale_factor = 1.2 + frame = self.camera_frame + + P_labels = VGroup() + dots = VGroup() + for line in lines: + dot = Dot(line.get_end()) + dot.scale(0.5) + dot.set_color(YELLOW) + dots.add(dot) + + angle = angle_of_vector(line.get_end() - sun_center) + angle = angle % TAU + angle_degrees = angle * (360 / TAU) + P_label = TexMobject( + "P_{%d^\\circ}" % int(np.round(angle_degrees, -1)) + ) + P_label.next_to( + dot, line.get_unit_vector(), + buff=SMALL_BUFF, + submobject_to_align=P_label[:2], + ) + P_labels.add(P_label) + + self.play( + LaggedStart(GrowFromCenter, dots), + LaggedStart(Write, P_labels), + Animation(self.comet), + frame.scale, frame_scale_factor + ) + self.wait(2) + self.play(*map(FadeOut, filter( + lambda pl: pl is not P_labels[1], + P_labels + ))) + self.play(ShowCreationThenDestruction(thick_lines[1])) + self.wait() + for i in 1, 2, 3: + self.play( + FadeOut(P_labels[i]), + FadeIn(P_labels[i + 1]), + ) + self.play(ShowCreationThenDestruction(thick_lines[i + 1])) + self.wait() + self.play( + FadeOut(P_labels[4]), + # frame.scale, 1.0 / frame_scale_factor + ) + + self.dots = dots + self.P_labels = P_labels + + def ask_about_time_per_slice(self): + wedge1 = self.wedges[0] + wedge2 = self.wedges[6] + arc1 = self.arcs[0] + arc2 = self.arcs[6] + 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() + 0.5 * DOWN, + color=WHITE, + ) + arrow2 = Arrow( + words2.get_right(), + wedge2.get_center() + 0.5 * UL, + color=WHITE + ) + + foreground = VGroup( + self.ellipse, self.angle_arcs, self.lines, + self.dots, comet, + ) + self.play( + Write(words1), + wedge1.set_fill, {"opacity": 1}, + GrowArrow(arrow1), + Animation(foreground), + ) + 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() + + def areas_are_proportional_to_radius_squared(self): + pass + + def show_inverse_square_law(self): + pass + + def show_change_in_velocity(self): + pass + + def directly_compare_velocity_vectors(self): + pass + + def show_equal_angle_changes(self): + pass From 8f6e055870c8e438d2640784a9652497982de58c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jul 2018 16:16:42 -0700 Subject: [PATCH 17/23] Further ShowEqualAngleSlices progress --- active_projects/lost_lecture.py | 277 +++++++++++++++++++++++++++++--- 1 file changed, 252 insertions(+), 25 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 82481ec5..325967f8 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -2597,18 +2597,7 @@ class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): for alpha in alphas ]) - moving_vector = self.get_velocity_vector(0) - - def update_moving_vector(vector): - new_vector = self.get_velocity_vector( - self.orbit.proportion, - # scalar=4.0, - ) - Transform(vector, new_vector).update(1) - - moving_vector_animation = ContinualUpdateFromFunc( - moving_vector, update_moving_vector - ) + moving_vector, moving_vector_animation = self.get_velocity_vector_and_update() self.play(LaggedStart( ShowCreation, vectors, @@ -2725,6 +2714,20 @@ class IntroduceShapeOfVelocities(AskAboutEllipses, MovingCameraScene): ) 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 = ContinualUpdateFromFunc( + moving_vector, update_moving_vector + ) + return moving_vector, moving_vector_animation + class AskWhy(TeacherStudentsScene): def construct(self): @@ -2752,6 +2755,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): CONFIG = { "animate_sun": False, "theta": 30 * DEGREES, + # "theta": 15 * DEGREES, } def construct(self): @@ -2761,7 +2765,6 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): self.ask_about_time_per_slice() self.areas_are_proportional_to_radius_squared() self.show_inverse_square_law() - self.show_change_in_velocity() self.directly_compare_velocity_vectors() self.show_equal_angle_changes() @@ -2906,7 +2909,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): angle = angle % TAU angle_degrees = angle * (360 / TAU) P_label = TexMobject( - "P_{%d^\\circ}" % int(np.round(angle_degrees, -1)) + "P_{%d^\\circ}" % int(np.round(angle_degrees)) ) P_label.next_to( dot, line.get_unit_vector(), @@ -2945,13 +2948,11 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): def ask_about_time_per_slice(self): wedge1 = self.wedges[0] - wedge2 = self.wedges[6] + wedge2 = self.wedges[len(self.wedges) / 2] arc1 = self.arcs[0] - arc2 = self.arcs[6] + arc2 = self.arcs[len(self.arcs) / 2] comet = self.comet - frame = self.camera_frame - words1 = TextMobject( "Time spent \\\\ traversing \\\\ this slice?" ) @@ -2962,12 +2963,12 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): arrow1 = Arrow( words1.get_bottom(), - wedge1.get_center() + 0.5 * DOWN, + wedge1.get_center() + wedge1.get_height() * DOWN / 2, color=WHITE, ) arrow2 = Arrow( words2.get_right(), - wedge2.get_center() + 0.5 * UL, + wedge2.get_center() + wedge2.get_height() * UL / 4, color=WHITE ) @@ -2991,17 +2992,243 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): 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): - pass + 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.dots), + 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): - pass + 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] - def show_change_in_velocity(self): - pass + 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(CircleThenFadeAround(prop_exp[:-2])) + self.play( + FadeOut(delta_t_numerator), + MoveToTarget(moving_R_squared), + randy.change, "happy", delta_v_expression + ) + 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): - pass + ellipse = self.ellipse + lines = self.lines + expressions = self.force_expressions + vectors = VGroup(*[ + self.get_velocity_vector(line.prop) + for line in lines + ]) + + root_point = ellipse.get_left() + 3 * LEFT + DOWN + root_dot = Dot(root_point) + + for vector in vectors: + 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) + ) + + self.play(LaggedStart(GrowArrow, vectors)) + self.play( + LaggedStart(MoveToTarget, vectors), + GrowFromCenter(root_dot), + expressions.scale, 0.5, {"about_edge": UL} + ) def show_equal_angle_changes(self): pass + + +class ShowMoreFinelyChoppedOrbit(ShowEqualAngleSlices): + def construct(self): + pass From 599f6a3f5a77dd06f4179dc7466d6b6bf5c94125 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jul 2018 17:37:09 -0700 Subject: [PATCH 18/23] Finished ShowEqualAngleSlices --- active_projects/lost_lecture.py | 135 ++++++++++++++++++++++++++++---- 1 file changed, 121 insertions(+), 14 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 325967f8..912eacb3 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -2753,20 +2753,18 @@ class FeynmanConfusedByNewton(Scene): class ShowEqualAngleSlices(IntroduceShapeOfVelocities): CONFIG = { - "animate_sun": False, + "animate_sun": True, "theta": 30 * DEGREES, - # "theta": 15 * DEGREES, } def construct(self): self.setup_orbit() self.show_equal_angle_slices() - self.label_points_P_theta() + # self.label_points_P_theta() self.ask_about_time_per_slice() self.areas_are_proportional_to_radius_squared() self.show_inverse_square_law() self.directly_compare_velocity_vectors() - self.show_equal_angle_changes() def setup_orbit(self): IntroduceShapeOfVelocities.setup_orbit(self) @@ -2938,10 +2936,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): ) self.play(ShowCreationThenDestruction(thick_lines[i + 1])) self.wait() - self.play( - FadeOut(P_labels[4]), - # frame.scale, 1.0 / frame_scale_factor - ) + self.play(FadeOut(P_labels[4])) self.dots = dots self.P_labels = P_labels @@ -3176,10 +3171,11 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): ) self.play(CircleThenFadeAround(prop_exp[:-2])) self.play( - FadeOut(delta_t_numerator), + 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)) @@ -3200,11 +3196,15 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): 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(), @@ -3218,17 +3218,124 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): vector.target.copy().set_stroke(BLACK, 5) ) - self.play(LaggedStart(GrowArrow, vectors)) + 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(CircleThenFadeAround(v1)) self.play( - LaggedStart(MoveToTarget, vectors), + MoveToTarget(v1), GrowFromCenter(root_dot), expressions.scale, 0.5, {"about_edge": UL} ) + self.wait() + self.play( + ReplacementTransform( + v1.saved_state.copy(), v2, + path_arc=self.theta + ) + ) + self.play(MoveToTarget(v2), Animation(root_dot)) + self.wait() + self.play(GrowArrow(diff_vect)) + self.wait() - def show_equal_angle_changes(self): - pass + 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.wait(2) + self.play(FadeIn(polygon)) + self.wait(5) + self.play(FadeOut(polygon)) + self.wait(15) + self.play(FadeIn(polygon)) -class ShowMoreFinelyChoppedOrbit(ShowEqualAngleSlices): +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(Scene): def construct(self): pass From daf5bfe29ce802e0c79fddc1e0656a2dcd9e303c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 17 Jul 2018 21:57:36 -0700 Subject: [PATCH 19/23] UseVelocityDiagramToDeduceCurve --- active_projects/lost_lecture.py | 606 ++++++++++++++++++++++++++++---- 1 file changed, 545 insertions(+), 61 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 912eacb3..8d9b7b02 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -920,6 +920,31 @@ class FeynmanElementaryQuote(Scene): 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.scale_to_fit_height(3) + images.arrange_submobjects(RIGHT, buff=LARGE_BUFF) + images.to_edge(DOWN, buff=LARGE_BUFF) + crosses = VGroup(*map(Cross, images)) + crosses.set_stroke("RED", 10) + + for image, cross in zip(images, crosses): + image.add(SurroundingRectangle( + image, + stroke_width=3, + stroke_color=WHITE, + buff=0 + )) + cross.scale(1.1) + self.play(FadeInFromDown(image)) + self.play(ShowCreation(cross)) + self.wait() class LostLecturePicture(TODOStub): @@ -2760,7 +2785,6 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): def construct(self): self.setup_orbit() self.show_equal_angle_slices() - # self.label_points_P_theta() self.ask_about_time_per_slice() self.areas_are_proportional_to_radius_squared() self.show_inverse_square_law() @@ -2887,66 +2911,13 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): self.angle_arcs = angle_arcs self.thetas = thetas - def label_points_P_theta(self): - sun_center = self.sun.get_center() - lines = self.lines - thick_lines = lines.copy().set_stroke(YELLOW, 4) - - frame_scale_factor = 1.2 - frame = self.camera_frame - - P_labels = VGroup() - dots = VGroup() - for line in lines: - dot = Dot(line.get_end()) - dot.scale(0.5) - dot.set_color(YELLOW) - dots.add(dot) - - angle = angle_of_vector(line.get_end() - sun_center) - angle = angle % TAU - angle_degrees = angle * (360 / TAU) - P_label = TexMobject( - "P_{%d^\\circ}" % int(np.round(angle_degrees)) - ) - P_label.next_to( - dot, line.get_unit_vector(), - buff=SMALL_BUFF, - submobject_to_align=P_label[:2], - ) - P_labels.add(P_label) - - self.play( - LaggedStart(GrowFromCenter, dots), - LaggedStart(Write, P_labels), - Animation(self.comet), - frame.scale, frame_scale_factor - ) - self.wait(2) - self.play(*map(FadeOut, filter( - lambda pl: pl is not P_labels[1], - P_labels - ))) - self.play(ShowCreationThenDestruction(thick_lines[1])) - self.wait() - for i in 1, 2, 3: - self.play( - FadeOut(P_labels[i]), - FadeIn(P_labels[i + 1]), - ) - self.play(ShowCreationThenDestruction(thick_lines[i + 1])) - self.wait() - self.play(FadeOut(P_labels[4])) - - self.dots = dots - self.P_labels = P_labels - 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?" @@ -2968,14 +2939,15 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): ) foreground = VGroup( - self.ellipse, self.angle_arcs, self.lines, - self.dots, comet, + 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( @@ -3043,7 +3015,6 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): scaling_group.scale, 0.5, {"about_point": sun_center}, Animation(self.area_question_arrows), - Animation(self.dots), Animation(self.comet), rate_func=there_and_back, run_time=4, @@ -3265,7 +3236,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): self.wait() self.play( ReplacementTransform( - v1.saved_state.copy(), v2, + v1.saved_state.copy(), v2.saved_state, path_arc=self.theta ) ) @@ -3295,6 +3266,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): LaggedStart(ShowCreation, external_angle_arcs), Animation(difference_vectors), ) + self.add_foreground_mobjects(difference_vectors) self.wait(2) self.play(FadeIn(polygon)) self.wait(5) @@ -3336,6 +3308,518 @@ class IKnowThisIsTricky(TeacherStudentsScene): self.wait(3) -class PonderOverOffCenterDiagram(Scene): +class PonderOverOffCenterDiagram(PiCreatureScene): def construct(self): - pass + 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 UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): + CONFIG = { + "animate_sun": False, + "theta": 15 * DEGREES, + } + + 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(*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 = 4 + 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( + VectorizedPoint(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, + submobject_mode="lagged_start", + lag_ratio=0.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(*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 + + 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.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 = np.linalg.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=1), + MoveToTarget(highlighted_line), + run_time=2, + 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): + little_ellipse = self.little_ellipse + + # 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 From b5796e4ed935d84a8161c61460ae23ea0c9c173c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Jul 2018 00:23:06 -0700 Subject: [PATCH 20/23] Finishing changes to UseVelocityDiagramToDeduceCurve --- active_projects/lost_lecture.py | 164 ++++++++++++++++++++++++++++++-- 1 file changed, 158 insertions(+), 6 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 8d9b7b02..98b0c98f 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -258,6 +258,60 @@ class ShowEmergingEllipse(Scene): return result +class ShowFullStory(Scene): + def construct(self): + directory = os.path.join( + MEDIA_DIR, + "animations/active_projects/lost_lecture/images" + ) + scene_names = [ + "ShowEmergingEllipse", + "ShowFullStory", + "FeynmanAndOrbitingPlannetOnEllipseDiagram", + "FeynmanFameStart", + "TheMotionOfPlanets", + "FeynmanElementaryQuote", + "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.scale_to_fit_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() @@ -3377,7 +3431,7 @@ class PonderOverOffCenterDiagram(PiCreatureScene): class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): CONFIG = { - "animate_sun": False, + "animate_sun": True, "theta": 15 * DEGREES, } @@ -3518,7 +3572,7 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): circle_wedge = self.get_wedge(circle_arc, circle.get_center()) circle_wedge.set_fill(PINK) circle_wedge_start = self.get_wedge( - VectorizedPoint(circle.get_top()), + Line(circle.get_top(), circle.get_top()), circle.get_center() ).match_style(circle_wedge) @@ -3650,8 +3704,6 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): lines.copy(), tangent_lines, run_time=3, - submobject_mode="lagged_start", - lag_ratio=0.3 ) ) self.wait() @@ -3705,6 +3757,8 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): 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)) @@ -3727,6 +3781,10 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): 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() @@ -3780,9 +3838,12 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): self.add(ghost_lines) self.play( - LaggedStart(MoveToTarget, lines, lag_ratio=1), + LaggedStart( + MoveToTarget, lines, + lag_ratio=0.1, + run_time=8, + ), MoveToTarget(highlighted_line), - run_time=2, path_arc=-angle, ) self.play(ShowCreation(little_ellipse)) @@ -3808,7 +3869,69 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): 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}, + *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): @@ -3823,3 +3946,32 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): wedge.set_stroke(width=0) wedge.set_fill(COBALT, opacity=opacity) return wedge + + +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): + def construct(self): + self.teacher_says( + "Pat yourself \\\\ on the back!", + target_mode="hooray" + ) + self.change_all_student_modes("happy") + self.wait(3) From 943998ff3cc51d35fd60eadd4ddfacf4aff09086 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Jul 2018 16:33:45 -0700 Subject: [PATCH 21/23] A few tweaks to lost_lecture while editing it all together --- active_projects/lost_lecture.py | 64 +++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 7 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 98b0c98f..632804c0 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -267,10 +267,10 @@ class ShowFullStory(Scene): scene_names = [ "ShowEmergingEllipse", "ShowFullStory", - "FeynmanAndOrbitingPlannetOnEllipseDiagram", "FeynmanFameStart", "TheMotionOfPlanets", "FeynmanElementaryQuote", + "DrawingEllipse", "ShowEllipseDefiningProperty", "ProveEllipse", "KeplersSecondLaw", @@ -918,6 +918,19 @@ class AskAboutEllipses(TheMotionOfPlanets): 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 = """ @@ -985,19 +998,32 @@ class FeynmanElementaryQuote(Scene): image.scale_to_fit_height(3) images.arrange_submobjects(RIGHT, buff=LARGE_BUFF) images.to_edge(DOWN, buff=LARGE_BUFF) + images[1].move_to(images[0]) crosses = VGroup(*map(Cross, images)) crosses.set_stroke("RED", 10) for image, cross in zip(images, crosses): - image.add(SurroundingRectangle( + image.rect = SurroundingRectangle( image, stroke_width=3, stroke_color=WHITE, buff=0 - )) + ) cross.scale(1.1) - self.play(FadeInFromDown(image)) - self.play(ShowCreation(cross)) + 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() @@ -1080,6 +1106,28 @@ class TableOfContents(Scene): 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.scale_to_fit_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}, @@ -1629,7 +1677,8 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): self.play(LaggedStart(Restore, lines)) self.play( FadeOut(to_fade), - line.set_stroke, {"width": 3}, + ghost_line.set_stroke, YELLOW, 3, + line.set_stroke, WHITE, 3, ReplacementTransform(ep_dot.copy(), P_dot), ) self.play(FadeInFromDown(P_label)) @@ -3338,7 +3387,8 @@ class ShowEqualAngleSlices15DegreeSlices(ShowEqualAngleSlices): class ShowEqualAngleSlices5DegreeSlices(ShowEqualAngleSlices): CONFIG = { - "animate_sun": True, + # "animate_sun": True, + "animate_sun": False, "theta": 5 * DEGREES, } From 340cb14a28fac18c23848746b0246d5c4495a8b6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 18 Jul 2018 22:19:00 -0700 Subject: [PATCH 22/23] Preliminary end to lost_lecture project --- active_projects/lost_lecture.py | 188 +++++++++++++++++++++++++++++++- 1 file changed, 185 insertions(+), 3 deletions(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 632804c0..829e4f8d 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -106,6 +106,37 @@ class ShowWord(Animation): # 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.scale_to_fit_height(FRAME_HEIGHT) + self.add(gradient) + + morty = self.pi_creatures + henry = ImageMobject("Henry_As_Stick") + henry.scale_to_fit_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, @@ -642,6 +673,16 @@ class TheMotionOfPlanets(Scene): 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}, @@ -680,6 +721,8 @@ class AskAboutEllipses(TheMotionOfPlanets): self.add_foreground_mobjects( sun_animation.mobject ) + else: + self.add_foreground_mobjects(sun) def add_orbit(self): sun = self.sun @@ -2045,6 +2088,27 @@ class ProveEllipse(ShowEmergingEllipse, ShowEllipseDefiningProperty): 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() @@ -2133,6 +2197,7 @@ class KeplersSecondLaw(AskAboutEllipses): arc_words.next_to( arc, RIGHT, buff=-SMALL_BUFF ) + arc_words.shift(4 * SMALL_BUFF * DOWN) arc_words.match_color(arc) # Show stubby arc @@ -2270,6 +2335,56 @@ class KeplersSecondLaw(AskAboutEllipses): 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 = ContinualUpdateFromFunc( + 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.scale_to_fit_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, @@ -3387,8 +3502,7 @@ class ShowEqualAngleSlices15DegreeSlices(ShowEqualAngleSlices): class ShowEqualAngleSlices5DegreeSlices(ShowEqualAngleSlices): CONFIG = { - # "animate_sun": True, - "animate_sun": False, + "animate_sun": True, "theta": 5 * DEGREES, } @@ -3479,10 +3593,20 @@ class PonderOverOffCenterDiagram(PiCreatureScene): 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): @@ -3577,7 +3701,7 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): comet = self.comet sun_center = self.sun.get_center() - index = 4 + index = self.index angle = fdiv(index, len(lines)) * TAU thick_line = lines[index].copy() thick_line.set_stroke(RED, 3) @@ -3998,6 +4122,30 @@ class UseVelocityDiagramToDeduceCurve(ShowEqualAngleSlices): 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: -np.linalg.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 @@ -4018,6 +4166,10 @@ class TryToRememberProof(PiCreatureScene): class PatYourselfOnTheBack(TeacherStudentsScene): + CONFIG = { + "camera_config": {"background_opacity": 1}, + } + def construct(self): self.teacher_says( "Pat yourself \\\\ on the back!", @@ -4025,3 +4177,33 @@ class PatYourselfOnTheBack(TeacherStudentsScene): ) 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.scale_to_fit_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) From 743298f49e3c77932b0dc335d1cb5ada251fda23 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 25 Jul 2018 10:22:25 -0700 Subject: [PATCH 23/23] FLL Thumbnail --- active_projects/lost_lecture.py | 77 ++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/active_projects/lost_lecture.py b/active_projects/lost_lecture.py index 829e4f8d..20b75062 100644 --- a/active_projects/lost_lecture.py +++ b/active_projects/lost_lecture.py @@ -1100,7 +1100,7 @@ class AskAboutInfiniteIntelligence(TeacherStudentsScene): ) self.wait() self.teacher_says( - "It's not too bad, \\\\ but stay focused", + "Stay focused, \\\\ go full screen, \\\\ and you'll be fine.", added_anims=[self.get_student_changes(*["happy"] * 3)] ) self.wait() @@ -4207,3 +4207,78 @@ class PatYourselfOnTheBack(TeacherStudentsScene): FadeInFromDown(chess) ) self.wait(2) + + +class Thumbnail(ShowEmergingEllipse): + CONFIG = { + "num_lines": 50, + } + + def construct(self): + background = ImageMobject("Feynman_teaching") + background.scale_to_fit_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.scale_to_fit_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)