diff --git a/active_projects/shadows.py b/active_projects/shadows.py new file mode 100644 index 00000000..cef60ae9 --- /dev/null +++ b/active_projects/shadows.py @@ -0,0 +1,411 @@ +from big_ol_pile_of_manim_imports import * + + +# Helpers +def get_shadow(mobject, opacity=0.5): + result = mobject.deepcopy() + result.apply_function(lambda p: [p[0], p[1], 0]) + color = interpolate_color( + mobject.get_fill_color(), BLACK, + mobject.get_fill_opacity() + ) + # color = BLACK + result.set_fill(color, opacity=opacity) + result.set_stroke(BLACK, 0.5, opacity=opacity) + result.set_shade_in_3d(False) + return result + + +def get_boundary_points(shadow, n_points=20): + points = shadow.get_points_defining_boundary() + return np.array([ + points[np.argmax(np.dot(points, vect.T))] + for vect in compass_directions(n_points) + ]) + + +def get_area(planar_mobject): + boundary = get_boundary_points(planar_mobject, 100) + xs = boundary[:, 0] + ys = boundary[:, 1] + dxs = np.append(xs[-1], xs[:-1]) - xs + dys = np.append(ys[-1], ys[:-1]) - ys + return abs(sum([ + 0.5 * (x * dy - y * dx) + for x, dx, y, dy in zip(xs, dxs, ys, dys) + ])) + + +def get_xy_plane_projection_point(p1, p2): + """ + Draw a line from source to p1 to p2. Where does it + intersect the xy plane? + """ + vect = p2 - p1 + return p1 - (p1[2] / vect[2]) * vect + + +# Scenes + + +class ShowShadows(ThreeDScene): + CONFIG = { + "object_center": [0, 0, 3], + "area_label_center": [0, -1.5, 0], + "surface_area": 6.0, + "num_reorientations": 10, + "camera_config": { + "light_source_start_point": 10 * OUT, + "frame_center": [0, 0, 0.5], + } + } + + def setup(self): + self.add_plane() + self.setup_orientation_trackers() + self.setup_object_and_shadow() + self.add_shadow_area_label() + self.add_surface_area_label() + + def add_plane(self): + plane = self.plane = Rectangle( + width=FRAME_WIDTH, + height=24.2, + stroke_width=0, + fill_color=WHITE, + fill_opacity=0.35, + ) + plane.set_sheen(0.2, DR) + grid = NumberPlane( + color=LIGHT_GREY, + secondary_color=DARK_GREY, + y_radius=int(plane.get_height() / 2), + stroke_width=1, + secondary_line_ratio=0, + ) + plane.add(grid) + plane.add(VectorizedPoint(10 * IN)) + plane.set_shade_in_3d(True, z_index_as_group=True) + self.add(plane) + + def setup_orientation_trackers(self): + # Euler angles + self.alpha_tracker = ValueTracker(0) + self.beta_tracker = ValueTracker(0) + self.gamma_tracker = ValueTracker(0) + + def setup_object_and_shadow(self): + self.obj3d = updating_mobject_from_func(self.get_reoriented_object) + self.shadow = updating_mobject_from_func(lambda: get_shadow(self.obj3d)) + + def add_shadow_area_label(self): + text = TextMobject("Shadow area: ") + decimal = DecimalNumber(0) + label = VGroup(text, decimal) + label.arrange_submobjects(RIGHT) + label.scale(1.5) + label.move_to(self.area_label_center - decimal.get_center()) + self.shadow_area_label = label + self.shadow_area_decimal = decimal + + # def update_decimal(decimal): + # # decimal.set_value(get_area(self.shadow)) + # self.add_fixed_in_frame_mobjects(decimal) + + # decimal.add_updater(update_decimal) + continual_update = ContinualChangingDecimal( + decimal, + lambda a: get_area(self.shadow), + position_update_func=lambda d: self.add_fixed_in_frame_mobjects(d) + ) + + # self.add_fixed_orientation_mobjects(label) + self.add_fixed_in_frame_mobjects(label) + self.add(label) + self.add(continual_update) + + def add_surface_area_label(self): + text = TextMobject("Surface area: ") + decimal = DecimalNumber(self.surface_area) + label = VGroup(text, decimal) + label.arrange_submobjects(RIGHT) + label.scale(1.25) + label.set_fill(YELLOW) + label.set_background_stroke(width=3) + label.next_to(self.obj3d, RIGHT, LARGE_BUFF) + label.shift(MED_LARGE_BUFF * IN) + self.surface_area_label = label + self.add_fixed_orientation_mobjects(label) + + def construct(self): + # Show creation + obj3d = self.obj3d.copy() + obj3d.clear_updaters() + temp_shadow = updating_mobject_from_func(lambda: get_shadow(obj3d)) + self.add(temp_shadow) + self.move_camera( + phi=60 * DEGREES, + theta=-120 * DEGREES, + added_anims=[ + LaggedStart(DrawBorderThenFill, obj3d) + ], + run_time=2 + ) + self.begin_ambient_camera_rotation(0.01) + self.remove(obj3d, temp_shadow) + + average_label = self.get_average_label() + # Reorient + self.add(self.obj3d, self.shadow) + for n in range(self.num_reorientations): + self.randomly_reorient() + if n == 3: + self.add_fixed_in_frame_mobjects(average_label) + self.play(Write(average_label, run_time=2)) + else: + self.wait() + + def randomly_reorient(self, run_time=3): + a, b, c = TAU * np.random.random(3) + self.play( + self.alpha_tracker.set_value, a, + self.beta_tracker.set_value, b, + self.gamma_tracker.set_value, c, + run_time=run_time + ) + + # + def get_object(self): + cube = Cube() + cube.set_height(1) + # cube.set_width(2, stretch=True) + cube.set_stroke(WHITE, 0.5) + return cube + + def get_reoriented_object(self): + obj3d = self.get_object() + angles = [ + self.alpha_tracker.get_value(), + self.beta_tracker.get_value(), + self.gamma_tracker.get_value(), + ] + vects = [OUT, RIGHT, OUT] + + center = self.object_center + obj3d.move_to(center) + for angle, vect in zip(angles, vects): + obj3d.rotate(angle, vect, about_point=center) + return obj3d + + def get_average_label(self): + rect = SurroundingRectangle( + self.shadow_area_decimal, + buff=SMALL_BUFF, + color=RED, + ) + words = TextMobject( + "Average", "=", + "$\\frac{\\text{Surface area}}{4}$" + ) + words.scale(1.5) + words[0].match_color(rect) + words[2].set_color(self.surface_area_label[0].get_fill_color()) + words.set_background_stroke(width=3) + words.next_to( + rect, DOWN, + index_of_submobject_to_align=0, + ) + # words.shift(MED_LARGE_BUFF * LEFT) + return VGroup(rect, words) + + +class ShowInfinitelyFarLightSource(ShowShadows): + CONFIG = { + "num_reorientations": 1, + "camera_center": [0, 0, 1], + } + + def construct(self): + self.force_skipping() + ShowShadows.construct(self) + self.revert_to_original_skipping_status() + + self.add_light_source_based_shadow_updater() + self.add_light() + self.move_light_around() + self.show_vertical_lines() + + def add_light(self): + light = self.light = self.get_light() + light_source = self.camera.light_source + light.move_to(light_source) + light_source.add_updater(lambda m: m.move_to(light)) + self.add(light_source) + self.add_fixed_orientation_mobjects(light) + + def move_light_around(self): + light = self.light + self.add_foreground_mobjects(self.shadow_area_label) + self.play( + light.move_to, 5 * OUT + DOWN, + run_time=3 + ) + self.play(Rotating( + light, angle=TAU, about_point=5 * OUT, + rate_func=smooth, run_time=3 + )) + self.play( + light.move_to, 30 * OUT, + run_time=3, + ) + self.remove(light) + + def show_vertical_lines(self): + lines = self.get_vertical_lines() + obj3d = self.obj3d + shadow = self.shadow + target_obj3d = obj3d.copy() + target_obj3d.become(shadow) + target_obj3d.match_style(obj3d) + target_obj3d.set_shade_in_3d(False) + source_obj3d = obj3d.copy() + source_obj3d.set_shade_in_3d(False) + source_obj3d.fade(1) + + self.play(LaggedStart(ShowCreation, lines)) + self.wait() + self.add(source_obj3d, lines) + self.play( + ReplacementTransform(source_obj3d, target_obj3d), + run_time=2 + ) + self.add(target_obj3d, lines) + self.play(FadeOut(target_obj3d),) + self.wait() + lines.add_updater(lambda m: m.become(self.get_vertical_lines())) + for x in range(5): + self.randomly_reorient() + + def add_light_source_based_shadow_updater(self): + shadow = self.shadow + light_source = self.camera.light_source + obj3d = self.obj3d + center = obj3d.get_center() + + def update(shadow): + lsp = light_source.get_center() + proj_center = get_xy_plane_projection_point(lsp, center) + c_to_lsp = lsp - center + unit_c_to_lsp = normalize(c_to_lsp) + rotation = rotation_matrix( + angle=np.arccos(np.dot(unit_c_to_lsp, OUT)), + axis=normalize(np.cross(unit_c_to_lsp, OUT)) + ) + new_shadow = get_shadow( + self.obj3d.copy().apply_matrix(rotation) + ) + shadow.become(new_shadow) + shadow.scale(get_norm(lsp) / get_norm(c_to_lsp)) + shadow.move_to(proj_center) + return shadow + shadow.add_updater(update) + + def get_light(self): + n_rings = 40 + radii = np.linspace(0, 2, n_rings) + rings = VGroup(*[ + Annulus(inner_radius=r1, outer_radius=r2) + for r1, r2 in zip(radii, radii[1:]) + ]) + opacities = np.linspace(1, 0, n_rings)**1.5 + for opacity, ring in zip(opacities, rings): + ring.set_fill(YELLOW, opacity) + ring.set_stroke(YELLOW, width=0.1, opacity=opacity) + return rings + + def get_vertical_lines(self): + shadow = self.shadow + points = get_boundary_points(shadow, 10) + # half_points = [(p1 + p2) / 2 for p1, p2 in adjacent_pairs(points)] + # points = np.append(points, half_points, axis=0) + light_source = self.light.get_center() + lines = VGroup(*[ + DashedLine(light_source, point) + for point in points + ]) + lines.set_shade_in_3d(True) + for line in lines: + line.remove(*line[:int(0.8 * len(line))]) + line[-10:].set_shade_in_3d(False) + line.set_stroke(YELLOW, 1) + return lines + + +class CylinderShadows(ShowShadows): + CONFIG = { + "surface_area": 2 * PI + 2 * PI * 2, + "area_label_center": [0, -2, 0], + } + + def get_object(self): + height = 2 + cylinder = ParametricSurface( + lambda u, v: np.array([ + np.cos(TAU * v), + np.sin(TAU * v), + height * (1 - u) + ]), + resolution=(6, 32) + ) + # circle = Circle(radius=1) + circle = ParametricSurface( + lambda u, v: np.array([ + (v + 0.01) * np.cos(TAU * u), + (v + 0.01) * np.sin(TAU * u), + 0, + ]), + resolution=(16, 8) + ) + # circle.set_fill(GREEN, opacity=0.5) + for surface in cylinder, circle: + surface.set_fill_by_checkerboard(GREEN, GREEN_E, opacity=1.0) + # surface.set_fill(GREEN, opacity=0.5) + cylinder.add(circle) + cylinder.add(circle.copy().flip().move_to(height * OUT)) + cylinder.set_shade_in_3d(True) + cylinder.set_stroke(width=0) + cylinder.scale(1.003) + return cylinder + + +class PrismShadows(ShowShadows): + CONFIG = { + "surface_area": 3 * np.sqrt(3) / 2 + 3 * (np.sqrt(3) * 2), + "object_center": [0, 0, 3], + "area_label_center": [0, -2.25, 0], + } + + def get_object(self): + height = 2 + prism = VGroup() + triangle = RegularPolygon(3) + verts = triangle.get_anchors()[:3] + rects = [ + Polygon(v1, v2, v2 + height * OUT, v1 + height * OUT) + for v1, v2 in adjacent_pairs(verts) + ] + prism.add(triangle, *rects) + prism.add(triangle.copy().shift(height * OUT)) + triangle.reverse_points() + prism.set_shade_in_3d(True) + prism.set_fill(PINK, 0.8) + prism.set_stroke(WHITE, 1) + return prism + + +class TheseFourPiAreSquare(PiCreatureScene): + def construct(self): + pass + + def create_pi_creatures(self): + pass diff --git a/big_ol_pile_of_manim_imports.py b/big_ol_pile_of_manim_imports.py index b9aefb48..ba8ee046 100644 --- a/big_ol_pile_of_manim_imports.py +++ b/big_ol_pile_of_manim_imports.py @@ -56,6 +56,7 @@ from mobject.three_dimensions import * from mobject.types.image_mobject import * from mobject.types.point_cloud_mobject import * from mobject.types.vectorized_mobject import * +from mobject.updater import * from mobject.value_tracker import * from for_3b1b_videos.common_scenes import * diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index 2ef4874e..48b3d28f 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -23,7 +23,6 @@ from utils.simple_functions import clip_in_place class ThreeDCamera(Camera): CONFIG = { - "sun_vect": 5 * UP + LEFT, "shading_factor": 0.2, "distance": 20.0, "default_distance": 5.0, @@ -167,7 +166,7 @@ class ThreeDCamera(Camera): distance = self.get_distance() rot_matrix = self.get_rotation_matrix() - points -= frame_center + points = points - frame_center points = np.dot(points, rot_matrix.T) zs = points[:, 2] for i in 0, 1: @@ -184,7 +183,7 @@ class ThreeDCamera(Camera): factor[(distance - zs) < 0] = 10**6 # clip_in_place(factor, 0, 10**6) points[:, i] *= factor - points += frame_center + points = points + frame_center return points def project_point(self, point): diff --git a/constants.py b/constants.py index af3fc0ef..0a6ae11c 100644 --- a/constants.py +++ b/constants.py @@ -114,11 +114,11 @@ PI = np.pi TAU = 2 * PI DEGREES = TAU / 360 -ANIMATIONS_DIR = os.path.join(MEDIA_DIR, "animations") +VIDEO_DIR = os.path.join(MEDIA_DIR, "videos") RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images") SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images") # TODO, staged scenes should really go into a subdirectory of a given scenes directory -STAGED_SCENES_DIR = os.path.join(ANIMATIONS_DIR, "staged_scenes") +STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes") ### THIS_DIR = os.path.dirname(os.path.realpath(__file__)) FILE_DIR = os.path.join(THIS_DIR, "files") @@ -128,7 +128,7 @@ TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing? MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") -for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, ANIMATIONS_DIR, TEX_DIR, +for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR, TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR, STAGED_SCENES_DIR]: if not os.path.exists(folder): @@ -136,8 +136,10 @@ for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, ANIMATIONS_DIR, TEX_DI TEX_USE_CTEX = False TEX_TEXT_TO_REPLACE = "YourTextHere" -TEMPLATE_TEX_FILE = os.path.join(THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX - else "ctex_template.tex") +TEMPLATE_TEX_FILE = os.path.join( + THIS_DIR, "tex_template.tex" if not TEX_USE_CTEX + else "ctex_template.tex" +) with open(TEMPLATE_TEX_FILE, "r") as infile: TEMPLATE_TEXT_FILE_BODY = infile.read() TEMPLATE_TEX_FILE_BODY = TEMPLATE_TEXT_FILE_BODY.replace( diff --git a/extract_scene.py b/extract_scene.py index 68356986..35e1a0b2 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -260,7 +260,7 @@ def main(): scene_names_to_classes = dict(inspect.getmembers(module, is_scene)) # config["output_directory"] = os.path.join( - # ANIMATIONS_DIR, + # VIDEO_DIR, # config["file"].replace(".py", "") # ) diff --git a/for_3b1b_videos/common_scenes.py b/for_3b1b_videos/common_scenes.py index bdd84192..820b7d2d 100644 --- a/for_3b1b_videos/common_scenes.py +++ b/for_3b1b_videos/common_scenes.py @@ -227,16 +227,16 @@ class PatreonEndScreen(PatreonThanks): RIGHT, buff=LARGE_BUFF, aligned_edge=UP, ) - columns.set_width(total_width - 1) - columns.next_to(black_rect, DOWN, 3 * LARGE_BUFF) - columns.to_edge(RIGHT) + if columns.get_width() > self.max_patron_width: + columns.set_width(total_width - 1) - thanks.align_to(columns, alignment_vect=RIGHT) + thanks.to_edge(RIGHT) + columns.next_to(thanks, DOWN, 3 * LARGE_BUFF) self.add(columns, black_rect, line, thanks) self.play( columns.move_to, 2 * DOWN, DOWN, - columns.to_edge, RIGHT, + columns.align_to, thanks, {"alignment_vect": RIGHT}, rate_func=None, run_time=self.run_time, ) diff --git a/mobject/matrix.py b/mobject/matrix.py index 7fb10d5f..aa5659cf 100644 --- a/mobject/matrix.py +++ b/mobject/matrix.py @@ -88,7 +88,7 @@ class Matrix(VMobject): def matrix_to_mob_matrix(self, matrix): return np.vectorize(self.element_to_mobject)( - matrix + matrix, **self.element_to_mobject_config ) def organize_mob_matrix(self, matrix): diff --git a/mobject/mobject.py b/mobject/mobject.py index 9c5b814a..1ef46e9e 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -113,7 +113,7 @@ class Mobject(Container): def save_image(self, name=None): self.get_image().save( - os.path.join(ANIMATIONS_DIR, (name or str(self)) + ".png") + os.path.join(VIDEO_DIR, (name or str(self)) + ".png") ) def copy(self): @@ -186,6 +186,7 @@ class Mobject(Container): def clear_updaters(self): self.updaters = [] + return self # Transforming operations @@ -703,7 +704,7 @@ class Mobject(Container): # Getters def get_points_defining_boundary(self): - return self.points + return self.get_all_points() def get_num_points(self): return len(self.points) @@ -743,7 +744,8 @@ class Mobject(Container): def get_boundary_point(self, direction): all_points = self.get_points_defining_boundary() - return all_points[np.argmax(np.dot(all_points, direction))] + index = np.argmax(np.dot(all_points, np.array(direction).T)) + return all_points[index] def get_z_index_reference_point(self): # TODO, better place to define default z_index_group? diff --git a/mobject/types/vectorized_mobject.py b/mobject/types/vectorized_mobject.py index 44114c95..e22d559d 100644 --- a/mobject/types/vectorized_mobject.py +++ b/mobject/types/vectorized_mobject.py @@ -1,5 +1,5 @@ - +import itertools as it from colour import Color from mobject.mobject import Mobject @@ -313,6 +313,7 @@ class VMobject(Mobject): submob.shade_in_3d = value if z_index_as_group: submob.z_index_group = self + return self # Drawing def start_at(self, point): @@ -482,7 +483,9 @@ class VMobject(Mobject): return self.points[::3] def get_points_defining_boundary(self): - return self.get_anchors() + return np.array(list(it.chain(*[ + sm.get_anchors() for sm in self.get_family() + ]))) # Alignment def align_points(self, vmobject): diff --git a/mobject/updater.py b/mobject/updater.py new file mode 100644 index 00000000..e3e4e7b3 --- /dev/null +++ b/mobject/updater.py @@ -0,0 +1,4 @@ +def updating_mobject_from_func(func): + mob = func() + mob.add_updater(lambda m: mob.become(func())) + return mob diff --git a/old_projects/borsuk_addition.py b/old_projects/borsuk_addition.py new file mode 100644 index 00000000..51509bd4 --- /dev/null +++ b/old_projects/borsuk_addition.py @@ -0,0 +1,1236 @@ +from big_ol_pile_of_manim_imports import * +from old_projects.lost_lecture import GeometryProofLand +from old_projects.quaternions import SpecialThreeDScene +from old_projects.uncertainty import Flash + + +class Introduction(TeacherStudentsScene): + CONFIG = { + "random_seed": 2, + } + + def construct(self): + self.play( + Animation(VectorizedPoint(self.hold_up_spot)), + self.teacher.change, "raise_right_hand", + ) + self.change_student_modes( + "angry", "sassy", "pleading" + ) + self.wait() + + movements = [] + for student in self.students: + student.center_tracker = VectorizedPoint() + student.center_tracker.move_to(student) + student.center_tracker.save_state() + student.add_updater( + lambda m: m.move_to(m.center_tracker) + ) + movement = ContinualMovement( + student.center_tracker, + direction=DOWN + 3 * LEFT, + rate=1.5 * random.random() + ) + movements.append(movement) + self.add(*movements) + self.change_student_modes( + "pondering", "sad", "concerned_musician", + look_at_arg=10 * LEFT + 2 * DOWN + ) + self.teacher_says( + "Wait, wait, wait!", + target_mode="surprised" + ) + self.remove(*movements) + self.play( + self.get_student_changes(*3 * ["hesitant"]), + *[ + Restore(student.center_tracker) + for student in self.students + ] + ) + + +class StudentsWatching(TeacherStudentsScene): + def construct(self): + self.play( + self.teacher.change, "raise_right_hand", + self.get_student_changes( + *3 * ["thinking"], + look_at_arg=self.screen + ), + VFadeIn(self.pi_creatures, run_time=2) + ) + self.wait(5) + + +class UnexpectedConnection(Scene): + def construct(self): + primes = TexMobject( + "2,", "3,", "5,", "7,", "11,", "13,", "17,", "\\dots" + ) + primes.move_to(2.5 * UP) + + circle = Circle( + color=YELLOW, + stroke_width=1, + radius=1.5, + ) + circle.shift(1.5 * DOWN) + center = circle.get_center() + center_dot = Dot(center) + radius = Line(center, circle.get_right()) + radius.set_stroke(WHITE, 3) + + arrow = DoubleArrow(primes, circle) + arrow.tip[1].shift(SMALL_BUFF * UP) + arrow.save_state() + arrow.rotate(90 * DEGREES) + arrow.scale(1.5) + arrow.fade(1) + + formula = TexMobject( + "\\frac{\\pi^2}{6} = \\prod_{p \\text{ prime}}" + "\\frac{1}{1 - p^{-2}}" + ) + formula.next_to(arrow.get_center(), RIGHT) + + def get_arc(): + angle = radius.get_angle() + return Arc( + start_angle=0, + angle=angle, + radius=circle.radius, + stroke_color=YELLOW, + stroke_width=5 + ).shift(center) + + arc = updating_mobject_from_func(get_arc) + + decimal = DecimalNumber(0) + decimal.add_updater( + lambda d: d.move_to(interpolate( + radius.get_start(), + radius.get_end(), + 1.5, + )), + ) + decimal.add_updater( + lambda d: d.set_value(radius.get_angle()) + ) + pi = TexMobject("\\pi") + pi.scale(2) + pi.next_to(circle, LEFT) + + self.add(circle, radius, center_dot, decimal, arc) + self.play( + Rotate(radius, PI - 1e-7, about_point=center), + LaggedStart(FadeInFromDown, primes), + run_time=4 + ) + self.remove(decimal) + self.add(pi) + self.wait() + self.play( + Restore(arrow), + FadeInFrom(formula, LEFT) + ) + self.wait() + + +class MapOfVideo(MovingCameraScene): + def construct(self): + images = Group( + ImageMobject("NecklaceThumbnail"), + ImageMobject("BorsukUlamThumbnail"), + ImageMobject("TopologyProofThumbnail"), + ImageMobject("ContinuousNecklaceThumbnail"), + ImageMobject("NecklaceSphereAssociationThumbnail") + ) + for image in images: + rect = SurroundingRectangle(image, buff=0) + rect.set_stroke(WHITE, 3) + image.add(rect) + + image_line = Group(*images[:2], *images[3:]) + image_line.arrange_submobjects(RIGHT, buff=LARGE_BUFF) + images[2].next_to(image_line, DOWN, buff=1.5) + images.set_width(FRAME_WIDTH - 1) + images.to_edge(UP, buff=LARGE_BUFF) + + arrows = VGroup( + Arrow(images[0], images[1], buff=SMALL_BUFF), + Arrow( + images[1].get_corner(DR) + 0.5 * LEFT, + images[2].get_top() + 0.5 * LEFT, + ), + Arrow( + images[2].get_top() + 0.5 * RIGHT, + images[3].get_corner(DL) + 0.5 * RIGHT, + ), + Arrow(images[3], images[4], buff=SMALL_BUFF), + ) + + self.play(LaggedStart(FadeInFromDown, images, run_time=4)) + self.play(LaggedStart(GrowArrow, arrows)) + self.wait() + group = Group(images, arrows) + for image in images: + group.save_state() + group.generate_target() + group.target.shift(-image.get_center()) + group.target.scale( + FRAME_WIDTH / image.get_width(), + about_point=ORIGIN, + ) + + self.play(MoveToTarget(group, run_time=3)) + self.wait() + self.play(Restore(group, run_time=3)) + + def get_curved_arrow(self, *points): + line = VMobject() + line.set_points(points) + tip = Arrow(points[-2], points[-1], buff=SMALL_BUFF).tip + line.pointwise_become_partial(line, 0, 0.9) + line.add(tip) + return line + + +class MathIsDeep(PiCreatureScene): + def construct(self): + words = TextMobject( + "Math", "is", "deep" + ) + words.scale(2) + words.to_edge(UP) + math = words[0].copy() + math[1].remove(math[1][1]) + math.set_fill(opacity=0) + math.set_stroke(width=0, background=True) + numbers = [13, 1, 20, 8] + num_mobs = VGroup(*[Integer(d) for d in numbers]) + num_mobs.arrange_submobjects(RIGHT, buff=MED_LARGE_BUFF) + num_mobs.next_to(math, DOWN, buff=1.5) + num_mobs.set_color(YELLOW) + top_arrows = VGroup(*[ + Arrow(c.get_bottom(), n.get_top()) + for c, n in zip(math, num_mobs) + ]) + n_sum = Integer(sum(numbers)) + n_sum.scale(1.5) + n_sum.next_to(num_mobs, DOWN, buff=1.5) + low_arrows = VGroup(*[ + Arrow(n.get_bottom(), n_sum.get_top()) + for n in num_mobs + ]) + VGroup(top_arrows, low_arrows).set_color(WHITE) + + n_sum_border = n_sum.deepcopy() + n_sum_border.set_fill(opacity=0) + n_sum_border.set_stroke(YELLOW, width=1) + n_sum_border.set_stroke(width=0, background=True) + + # pre_num_mobs = num_mobs.copy() + # for pn, letter in zip(pre_num_mobs, math): + # pn.fade(1) + # pn.set_color(RED) + # pn.move_to(letter) + # num_mobs[1].add_subpath(num_mobs[1].points) + + self.play( + LaggedStart( + FadeInFromLarge, words, + scale_factor=1.5, + run_time=0.6, + lag_ratio=0.6, + ), + self.pi_creature.change, "pondering" + ) + self.play( + TransformFromCopy(math, num_mobs), + *map(GrowArrow, top_arrows), + ) + self.wait() + self.play( + TransformFromCopy(num_mobs, VGroup(n_sum)), + self.pi_creature.change, "thinking", + *map(GrowArrow, low_arrows), + ) + self.play(LaggedStart(ShowCreationThenDestruction, n_sum_border)) + self.play(Blink(self.pi_creature)) + self.wait() + + +class MinimizeSharding(Scene): + def construct(self): + piece_groups = VGroup(*[ + VGroup(*[ + self.get_piece() + for x in range(3) + ]).arrange_submobjects(RIGHT, buff=SMALL_BUFF) + for y in range(4) + ]).arrange_submobjects(RIGHT, buff=SMALL_BUFF) + + self.add(piece_groups) + self.play(*[ + ApplyMethod(mob.space_out_submobjects, 0.7) + for mob in piece_groups + ]) + self.wait() + group1 = piece_groups[:2] + group2 = piece_groups[2:] + self.play( + group1.arrange_submobjects, DOWN, + group1.next_to, ORIGIN, LEFT, LARGE_BUFF, + group2.arrange_submobjects, DOWN, + group2.next_to, ORIGIN, RIGHT, LARGE_BUFF, + ) + self.wait() + + def get_piece(self): + jagged_spots = [ + ORIGIN, 2 * UP + RIGHT, 4 * UP + LEFT, 6 * UP, + ] + corners = list(it.chain( + jagged_spots, + [6 * UP + 10 * RIGHT], + [ + p + 10 * RIGHT + for p in reversed(jagged_spots) + ], + [ORIGIN] + )) + piece = VMobject().set_points_as_corners(corners) + piece.set_width(1) + piece.center() + piece.set_stroke(WHITE, width=0.5) + piece.set_fill(BLUE, opacity=1) + return piece + + +class Antipodes(Scene): + def construct(self): + word = TextMobject("``Antipodes''") + word.set_width(FRAME_WIDTH - 1) + word.set_color(MAROON_B) + self.play(Write(word)) + self.wait() + + +class TopologyWordBreak(Scene): + def construct(self): + word = TextMobject("Topology") + word.scale(2) + colors = [BLUE, YELLOW, RED] + classes = VGroup(*[VGroup() for x in range(3)]) + for letter in word: + genus = len(letter.submobjects) + letter.target_color = colors[genus] + letter.generate_target() + letter.target.set_color(colors[genus]) + classes[genus].add(letter.target) + signs = VGroup() + for group in classes: + new_group = VGroup() + for elem in group[:-1]: + new_group.add(elem) + sign = TexMobject("\\simeq") + new_group.add(sign) + signs.add(sign) + new_group.add(group[-1]) + group.submobjects = list(new_group.submobjects) + group.arrange_submobjects(RIGHT) + + word[2].target.shift(0.1 * DOWN) + word[7].target.shift(0.1 * DOWN) + + classes.arrange_submobjects(DOWN, buff=LARGE_BUFF, aligned_edge=LEFT) + classes.shift(2 * RIGHT) + + genus_labels = VGroup(*[ + TextMobject("Genus %d:" % d).scale(1.5).next_to( + classes[d], LEFT, MED_LARGE_BUFF + ) + for d in range(3) + ]) + genus_labels.shift(SMALL_BUFF * UP) + + self.play(Write(word)) + self.play(LaggedStart( + ApplyMethod, word, + lambda m: (m.set_color, m.target_color), + run_time=1 + )) + self.play( + LaggedStart(MoveToTarget, word), + LaggedStart(FadeIn, signs), + LaggedStart(FadeInFromDown, genus_labels), + ) + self.wait(3) + + +class TopologyProofLand(GeometryProofLand): + CONFIG = { + "text": "Topology proof land" + } + + +class GreenLine(Scene): + def construct(self): + self.add(Line(LEFT, RIGHT, color=GREEN)) + + +class Thief(Scene): + def construct(self): + self.play(Write(TextMobject("Thief"))) + self.wait() + + +class FunctionGInSymbols(Scene): + def construct(self): + p_tex = "\\vec{\\textbf{p}}" + neg_p_tex = "-\\vec{\\textbf{p}}" + + def color_tex(tex_mob): + pairs = [ + (p_tex, YELLOW), + (neg_p_tex, RED), + ("{g}", GREEN), + ] + for tex, color in pairs: + tex_mob.set_color_by_tex( + tex, color, substring=False + ) + + f_of_p = TexMobject("f", "(", p_tex, ")") + f_of_p.shift(2.5 * LEFT + 2.5 * UP) + f_of_neg_p = TexMobject("f", "(", neg_p_tex, ")") + g_of_p = TexMobject("g", "(", p_tex, ")") + g_of_p[0].set_color(YELLOW) + for mob in f_of_p, f_of_neg_p, g_of_p: + color_tex(mob) + dec_rhs = DecimalMatrix([[-0.9], [0.5]]) + dec_rhs.next_to(f_of_p, RIGHT) + minus = TexMobject("-") + equals = TexMobject("=") + equals.next_to(f_of_p, RIGHT) + zero_zero = IntegerMatrix([[0], [0]]) + + for matrix in dec_rhs, zero_zero: + matrix.space_out_submobjects(0.8) + matrix.brackets.scale(0.9) + matrix.next_to(equals, RIGHT) + + f_of_neg_p.next_to(equals, RIGHT) + + f = f_of_p.get_part_by_tex("f") + p = f_of_p.get_part_by_tex(p_tex) + f_brace = Brace(f, UP, buff=SMALL_BUFF) + f_brace.add(f_brace.get_text("Continuous function")) + p_brace = Brace(p, DOWN, buff=SMALL_BUFF) + p_brace.add(p_brace.get_text("Sphere point").match_color(p)) + + f_of_p.save_state() + f_of_p.space_out_submobjects(2) + f_of_p.scale(2) + f_of_p.fade(1) + + self.play(f_of_p.restore) + self.play(GrowFromCenter(f_brace)) + self.wait() + self.play(GrowFromCenter(p_brace)) + self.wait() + self.play( + FadeInFromDown(equals), + Write(dec_rhs), + FadeOut(f_brace), + FadeOut(p_brace), + ) + self.wait(2) + self.play(WiggleOutThenIn(f)) + self.wait() + self.play( + FadeOutAndShift(dec_rhs, DOWN), + FadeInFromDown(f_of_neg_p) + ) + self.wait() + + # Rearrange + f_of_neg_p.generate_target() + f_of_p.generate_target() + group = VGroup(f_of_p.target, minus, f_of_neg_p.target) + group.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + group.next_to(equals, LEFT) + + self.play( + MoveToTarget(f_of_p, path_arc=PI), + MoveToTarget(f_of_neg_p, path_arc=-PI), + FadeInFromLarge(minus), + FadeInFrom(zero_zero, LEFT) + ) + self.wait() + + # Define g + def_eq = TexMobject(":=") + def_eq.next_to(f_of_p, LEFT) + g_of_p.next_to(def_eq, LEFT) + rect = SurroundingRectangle(VGroup(g_of_p, f_of_neg_p)) + rect.set_stroke(width=1) + seeking_text = TexMobject( + "\\text{Looking for }", p_tex, "\\text{ where}" + ) + color_tex(seeking_text) + seeking_text.next_to(zero_zero, DOWN, MED_LARGE_BUFF) + seeking_text.to_edge(LEFT) + g_equals_zero = VGroup( + g_of_p.copy(), equals, zero_zero + ) + g_equals_zero.generate_target() + g_equals_zero.target.arrange_submobjects(RIGHT, SMALL_BUFF) + g_equals_zero.target.next_to(seeking_text, DOWN) + + self.play( + FadeInFromLarge(g_of_p), + FadeInFrom(def_eq, LEFT) + ) + self.play( + FadeInFromDown(seeking_text), + MoveToTarget(g_equals_zero) + ) + self.play(ShowCreation(rect)) + self.wait() + self.play(FadeOut(rect)) + + # Show g is odd + g_of_neg_p = TexMobject("{g}", "(", neg_p_tex, ")") + eq2 = TexMobject("=") + rhs = TexMobject( + "f", "(", neg_p_tex, ")", "-", + "f", "(", p_tex, ")", "=", + "-", "{g}", "(", p_tex, ")", + ) + for mob in g_of_neg_p, rhs: + color_tex(mob) + g_of_neg_p.next_to(g_of_p, DOWN, aligned_edge=LEFT, buff=LARGE_BUFF) + eq2.next_to(g_of_neg_p, RIGHT, SMALL_BUFF) + rhs.next_to(eq2, RIGHT, SMALL_BUFF) + neg_g_of_p = rhs[-5:] + neg_g_of_p.save_state() + neg_g_of_p.next_to(eq2, RIGHT, SMALL_BUFF) + + self.play( + FadeIn(g_of_neg_p), + FadeIn(eq2), + FadeIn(neg_g_of_p), + VGroup(seeking_text, g_equals_zero).shift, 1.5 * DOWN + ) + self.wait() + self.play(CircleThenFadeAround(g_of_neg_p[2])) + self.wait() + self.play(CircleThenFadeAround(neg_g_of_p)) + self.wait() + self.play(neg_g_of_p.restore) + rects = VGroup(*map(SurroundingRectangle, [f_of_p, f_of_neg_p])) + self.play(LaggedStart( + ShowCreationThenDestruction, rects, + lag_ratio=0.8 + )) + self.play( + TransformFromCopy(f_of_p, rhs[5:9]), + TransformFromCopy(f_of_neg_p, rhs[:4]), + FadeIn(rhs[4]), + FadeIn(rhs[-6]), + ) + self.wait() + + +class FunctionGInputSpace(SpecialThreeDScene): + def setup(self): + self.init_tracked_point() + + sphere = self.get_sphere() + sphere.set_fill(BLUE_E, opacity=0.5) + self.sphere = sphere + + self.set_camera_orientation( + phi=70 * DEGREES, + theta=-120 * DEGREES, + ) + self.begin_ambient_camera_rotation(rate=0.02) + + self.init_dot() + + self.add(ThreeDAxes()) + + def construct(self): + self.show_input_dot() + self.show_start_path() + self.show_antipodal_point() + self.show_equator() + self.deform_towards_north_pole() + + def show_input_dot(self): + sphere = self.sphere + dot = self.dot + point_mob = self.tracked_point + start_point = self.get_start_point() + + arrow = Arrow( + start_point + (LEFT + OUT + UP), start_point, + color=BLUE, + buff=MED_LARGE_BUFF, + ) + arrow.rotate(90 * DEGREES, axis=arrow.get_vector()) + arrow.add_to_back(arrow.copy().set_stroke(BLACK, 5)) + + p_label = self.p_label = TexMobject("\\vec{\\textbf{p}}") + p_label.set_color(YELLOW) + p_label.next_to(arrow.get_start(), OUT, buff=0.3) + p_label.set_shade_in_3d(True) + + self.play(Write(sphere, run_time=3)) + self.add(dot) + self.add_fixed_orientation_mobjects(p_label) + self.play( + point_mob.move_to, start_point, + GrowArrow(arrow), + FadeInFrom(p_label, IN) + ) + self.wait() + self.play( + arrow.scale, 0, {"about_point": arrow.get_end()}, + p_label.next_to, dot, OUT + LEFT, SMALL_BUFF + ) + p_label.add_updater(lambda p: p.next_to(dot, OUT + LEFT, SMALL_BUFF)) + self.wait(4) + + def show_start_path(self): + path = self.get_start_path() + self.draw_path(path, uncreate=True) + self.wait() + + def show_antipodal_point(self): + path = self.get_antipodal_path() + end_dot = updating_mobject_from_func( + lambda: self.get_dot( + path[-1].point_from_proportion(1) + ).set_color(RED) + ) + + neg_p = TexMobject("-\\vec{\\textbf{p}}") + neg_p.add_updater( + lambda p: p.next_to(end_dot, UP + RIGHT + IN) + ) + neg_p.set_color(RED) + neg_p.set_shade_in_3d(True) + + self.move_camera( + phi=100 * DEGREES, + theta=30 * DEGREES, + added_anims=[ShowCreation(path)], + run_time=4, + ) + self.wait() + self.add_fixed_orientation_mobjects(neg_p) + self.play( + FadeInFromLarge(end_dot), + Write(neg_p) + ) + self.wait(4) + self.move_camera( + phi=70 * DEGREES, + theta=-120 * DEGREES, + run_time=2 + ) + self.wait(7) + # Flip + self.move_camera( + phi=100 * DEGREES, + theta=30 * DEGREES, + run_time=2, + ) + self.wait(7) + self.move_camera( + phi=70 * DEGREES, + theta=-120 * DEGREES, + added_anims=[ + FadeOut(end_dot), + FadeOut(neg_p), + FadeOut(path), + ], + run_time=2, + ) + + def show_equator(self): + point_mob = self.tracked_point + equator = self.get_lat_line() + + self.play(point_mob.move_to, equator[0].point_from_proportion(0)) + self.play(ShowCreation(equator, run_time=4)) + for x in range(2): + self.play( + Rotate(point_mob, PI, about_point=ORIGIN, axis=OUT), + run_time=4 + ) + self.wait(3) + self.play( + FadeOut(self.dot), + FadeOut(self.p_label), + ) + + self.equator = equator + + def deform_towards_north_pole(self): + equator = self.equator + + self.play(UpdateFromAlphaFunc( + equator, + lambda m, a: m.become(self.get_lat_line(a * PI / 2)), + run_time=16 + )) + self.wait() + + # + def init_tracked_point(self): + self.tracked_point = VectorizedPoint([0, 0, 2]) + self.tracked_point.add_updater( + lambda p: p.move_to(2 * normalize(p.get_center())) + ) + self.add(self.tracked_point) + + def init_dot(self): + self.dot = updating_mobject_from_func( + lambda: self.get_dot(self.tracked_point.get_center()) + ) + + def get_start_path(self): + path = ParametricFunction( + lambda t: np.array([ + -np.sin(TAU * t + TAU / 4), + np.cos(2 * TAU * t + TAU / 4), + 0 + ]), + color=RED + ) + path.scale(0.5) + path.shift(0.5 * OUT) + path.rotate(60 * DEGREES, RIGHT, about_point=ORIGIN) + path.shift( + self.get_start_point() - path.point_from_proportion(0) + ) + path.apply_function(lambda p: 2 * normalize(p)) + return path + + def get_antipodal_path(self): + start = self.get_start_point() + path = ParametricFunction( + lambda t: 2.03 * np.array([ + 0, + np.sin(PI * t), + np.cos(PI * t), + ]), + color=YELLOW + ) + path.apply_matrix(z_to_vector(start)) + + dashed_path = DashedMobject(path) + dashed_path.set_shade_in_3d(True) + + return dashed_path + + def get_lat_line(self, lat=0): + equator = ParametricFunction(lambda t: 2.03 * np.array([ + np.cos(lat) * np.sin(TAU * t), + np.cos(lat) * (-np.cos(TAU * t)), + np.sin(lat) + ])) + equator.rotate(-90 * DEGREES) + dashed_equator = DashedMobject( + equator, + dashes_num=40, + color=RED, + ) + dashed_equator.set_shade_in_3d(True) + return dashed_equator + + def draw_path(self, path, + run_time=4, + dot_follow=True, + uncreate=False, + added_anims=None + ): + added_anims = added_anims or [] + point_mob = self.tracked_point + anims = [ShowCreation(path)] + if dot_follow: + anims.append(UpdateFromFunc( + point_mob, + lambda p: p.move_to(path.point_from_proportion(1)) + )) + self.add(path, self.dot) + self.play(*anims, run_time=run_time) + + if uncreate: + self.wait() + self.play( + Uncreate(path), + run_time=run_time + ) + + def modify_path(self, path): + return path + + def get_start_point(self): + return 2 * normalize([-1, -1, 1]) + + def get_dot(self, point): + dot = Dot(color=WHITE) + dot.shift(2.05 * OUT) + dot.apply_matrix(z_to_vector(normalize(point))) + dot.set_shade_in_3d(True) + return dot + + +class FunctionGOutputSpace(FunctionGInputSpace): + def construct(self): + self.show_input_dot() + self.show_start_path() + self.show_antipodal_point() + self.show_equator() + self.deform_towards_north_pole() + + def setup(self): + axes = self.axes = Axes( + x_min=-2.5, + x_max=2.5, + y_min=-2.5, + y_max=2.5, + number_line_config={'unit_size': 1.5} + ) + for axis in axes: + numbers = list(range(-2, 3)) + numbers.remove(0) + axis.add_numbers(*numbers) + + self.init_tracked_point() + self.init_dot() + + def show_input_dot(self): + axes = self.axes + dot = self.dot + point_mob = self.tracked_point + + point_mob.move_to(self.get_start_point()) + self.add(dot) + self.continual_update(0) + self.remove(dot) + + p_tex = "\\vec{\\textbf{p}}" + fp_label = self.fp_label = TexMobject("f(", p_tex, ")") + fp_label.set_color_by_tex(p_tex, YELLOW) + + self.play(Write(axes, run_time=3)) + self.wait(3) + dc = dot.copy() + self.play( + FadeInFrom(dc, 2 * UP, remover=True), + UpdateFromFunc(fp_label, lambda fp: fp.next_to(dc, UL, SMALL_BUFF)) + ) + self.add(dot) + fp_label.add_updater( + lambda fp: fp.next_to(dot, UL, SMALL_BUFF) + ) + self.wait(2) + + def draw_path(self, path, + run_time=4, + dot_follow=True, + uncreate=False, + added_anims=None + ): + added_anims = added_anims or [] + point_mob = self.tracked_point + shadow_path = path.deepcopy().fade(1) + flat_path = self.modify_path(path) + anims = [ + ShowCreation(flat_path), + ShowCreation(shadow_path), + ] + if dot_follow: + anims.append(UpdateFromFunc( + point_mob, + lambda p: p.move_to(shadow_path.point_from_proportion(1)) + )) + self.add(flat_path, self.dot) + self.play(*anims, run_time=run_time) + + if uncreate: + self.wait() + self.remove(shadow_path) + self.play( + Uncreate(flat_path), + run_time=run_time + ) + + def show_antipodal_point(self): + dot = self.dot + pre_path = VMobject().set_points_smoothly([ + ORIGIN, DOWN, DOWN + 2 * RIGHT, + 3 * RIGHT + 0.5 * UP, 0.5 * RIGHT, ORIGIN + ]) + pre_path.rotate(-45 * DEGREES, about_point=ORIGIN) + pre_path.shift(dot.get_center()) + path = DashedMobject(pre_path) + + fp_label = self.fp_label + equals = TexMobject("=") + equals.next_to(fp_label, RIGHT, SMALL_BUFF) + f_neg_p = TexMobject("f(", "-\\vec{\\textbf{p}}", ")") + f_neg_p[1].set_color(RED) + f_neg_p.next_to(equals, RIGHT) + + gp_label = TexMobject("g", "(", "\\vec{\\textbf{p}}", ")") + gp_label[0].set_color(GREEN) + gp_label[2].set_color(YELLOW) + gp_label.add_updater(lambda m: m.next_to(dot, UL, SMALL_BUFF)) + self.gp_label = gp_label + # gp_label.next_to(Dot(ORIGIN), UL, SMALL_BUFF) + + self.play(ShowCreation(path, run_time=4)) + self.wait() + self.play( + Write(equals), + Write(f_neg_p), + ) + self.wait(6) + self.play( + FadeOut(VGroup(path, equals, f_neg_p)) + ) + dot.clear_updaters() + self.add(fp_label, gp_label) + gp_label.set_background_stroke(width=0) + self.play( + dot.move_to, ORIGIN, + VFadeOut(fp_label), + VFadeIn(gp_label), + ) + self.wait(4) + self.play( + dot.move_to, self.odd_func(self.get_start_point()) + ) + # Flip, 2 second for flip, 7 seconds after + path = self.get_antipodal_path() + path.apply_function(self.odd_func) + end_dot = Dot(color=RED) + end_dot.move_to(path[-1].point_from_proportion(1)) + g_neg_p = TexMobject( + "g", "(", "-\\vec{\\textbf{p}}", ")" + ) + g_neg_p[0].set_color(GREEN) + g_neg_p[2].set_color(RED) + g_neg_p.next_to(end_dot, UR, SMALL_BUFF) + reflection_line = DashedLine( + dot.get_center(), end_dot.get_center(), + stroke_width=0, + ) + vector = Vector(dot.get_center()) + + self.play(ShowCreation(path, run_time=1)) + self.wait() + self.play( + ShowCreationThenDestruction(reflection_line, run_time=2), + TransformFromCopy(dot, end_dot), + ReplacementTransform( + gp_label.deepcopy().clear_updaters(), g_neg_p + ), + ) + self.wait() + self.play(FadeIn(vector)) + self.play(Rotate(vector, angle=PI, about_point=ORIGIN)) + self.play(FadeOut(vector)) + self.play( + FadeOut(end_dot), + FadeOut(g_neg_p), + FadeOut(path), + ) + + def show_equator(self): + dot = self.dot + point_mob = self.tracked_point + equator = self.get_lat_line() + flat_eq = equator.deepcopy().apply_function(self.odd_func) + equator.fade(1) + + equator_start = equator[0].point_from_proportion(0) + + # To address + self.play( + point_mob.move_to, equator_start, + dot.move_to, self.odd_func(equator_start) + ) + dot.add_updater(lambda m: m.move_to( + self.odd_func(point_mob.get_center()) + )) + self.play( + ShowCreation(equator), + ShowCreation(flat_eq), + run_time=4, + ) + for x in range(2): + self.play( + Rotate(point_mob, PI, about_point=ORIGIN, axis=OUT), + run_time=4 + ) + self.wait(3) + self.play( + FadeOut(self.dot), + FadeOut(self.gp_label), + ) + + self.equator = equator + self.flat_eq = flat_eq + + def deform_towards_north_pole(self): + equator = self.equator + flat_eq = self.flat_eq + + self.play( + UpdateFromAlphaFunc( + equator, + lambda m, a: m.become(self.get_lat_line(a * PI / 2)).set_stroke(width=0), + run_time=16 + ), + UpdateFromFunc( + flat_eq, + lambda m: m.become( + equator.deepcopy().apply_function(self.odd_func).set_stroke( + color=RED, width=3 + ) + ) + ) + ) + self.wait() + # + + def func(self, point): + x, y, z = point + return 0.5 * self.axes.coords_to_point( + 2 * x + 0.5 * y + z, + 2 * y - 0.5 * np.sin(PI * x) + z**2 + 1 - x, + ) + + def odd_func(self, point): + return (self.func(point) - self.func(-point)) / 2 + + def get_dot(self, point): + return Dot(self.func(point)) + + def modify_path(self, path): + path.apply_function(self.func) + return path + + +class RotationOfEquatorGraphInOuputSpace(FunctionGOutputSpace): + def construct(self): + self.add(self.axes) + equator = self.get_lat_line(0) + equator.remove(*equator[len(equator) // 2:]) + + flat_eq = equator.copy().apply_function(self.odd_func) + vector = Vector(flat_eq[0].point_from_proportion(0)) + vector_copy = vector.copy().fade(0.5) + + self.add(flat_eq) + self.add(flat_eq.copy()) + self.wait() + self.play(FadeIn(vector)) + self.add(vector_copy) + self.play( + Rotate( + VGroup(flat_eq, vector), + PI, about_point=ORIGIN, run_time=5 + )) + self.play(FadeOut(vector), FadeOut(vector_copy)) + + +class WriteInputSpace(Scene): + def construct(self): + self.play(Write(TextMobject("Input space"))) + self.wait() + + +class WriteOutputSpace(Scene): + def construct(self): + self.play(Write(TextMobject("Output space"))) + self.wait() + + +class LineScene(Scene): + def construct(self): + self.add(DashedLine(5 * LEFT, 5 * RIGHT, color=WHITE)) + + +class ShowFlash(Scene): + def construct(self): + dot = Dot(ORIGIN, color=YELLOW) + dot.set_stroke(width=0) + dot.set_fill(opacity=0) + self.play(Flash(dot, flash_radius=0.8, line_length=0.6, run_time=2)) + self.wait() + + +class WaitForIt(Scene): + def construct(self): + words = TextMobject("Wait for it", "$\\dots$", arg_separator="") + words.scale(2) + self.add(words[0]) + self.play(Write(words[1], run_time=3)) + self.wait() + + +class DrawSphere(SpecialThreeDScene): + def construct(self): + sphere = self.get_sphere() + sphere.shift(IN) + question = TextMobject("What \\emph{is} a sphere?") + question.set_width(FRAME_WIDTH - 3) + question.to_edge(UP) + self.move_camera(phi=70 * DEGREES, run_time=0) + self.begin_ambient_camera_rotation() + self.add_fixed_in_frame_mobjects(question) + self.play( + Write(sphere), + FadeInFromDown(question) + ) + self.wait(4) + + +class DivisionOfUnity(Scene): + def construct(self): + factor = 2 + line = Line(factor * LEFT, factor * RIGHT) + lower_brace = Brace(line, DOWN) + lower_brace.add(lower_brace.get_text("1")) + v_lines = VGroup(*[ + DashedLine(0.2 * UP, 0.2 * DOWN).shift(factor * v) + for v in [LEFT, 0.3 * LEFT, 0.1 * RIGHT, RIGHT] + ]) + upper_braces = VGroup(*[ + Brace(VGroup(vl1, vl2), UP) + for vl1, vl2 in zip(v_lines[:-1], v_lines[1:]) + ]) + colors = color_gradient([GREEN, BLUE], 3) + for i, color, brace in zip(it.count(1), colors, upper_braces): + label = brace.get_tex("x_%d^2" % i) + label.set_color(color) + brace.add(label) + + self.add(line, lower_brace) + self.play(LaggedStart( + ShowCreation, v_lines[1:3], + lag_ratio=0.8, + run_time=1 + )) + self.play(LaggedStart( + GrowFromCenter, upper_braces + )) + self.wait() + + +class ThreeDSpace(ThreeDScene): + def construct(self): + axes = ThreeDAxes() + self.add(axes) + self.set_camera_orientation(phi=70 * DEGREES, theta=-130 * DEGREES) + self.begin_ambient_camera_rotation() + + density = 1 + radius = 3 + lines = VGroup(*[ + VGroup(*[ + Line( + radius * IN, radius * OUT, + stroke_color=WHITE, + stroke_width=1, + stroke_opacity=0.5, + ).shift(x * RIGHT + y * UP) + for x in np.arange(-radius, radius + density, density) + for y in np.arange(-radius, radius + density, density) + ]).rotate(n * 120 * DEGREES, axis=[1, 1, 1]) + for n in range(3) + ]) + + self.play(Write(lines)) + self.wait(30) + + +class NecklaceSphereConnectionTitle(Scene): + def construct(self): + text = TextMobject("Necklace Sphere Association") + text.set_width(FRAME_WIDTH - 1) + self.add(text) + + +class BorsukEndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "Ali Yahya", + "Meshal Alshammari", + "Crypticswarm", + "Ankit Agarwal", + "Yu Jun", + "Shelby Doolittle", + "Dave Nicponski", + "Damion Kistler", + "Juan Benet", + "Othman Alikhan", + "Justin Helps", + "Markus Persson", + "Dan Buchoff", + "Derek Dai", + "Joseph John Cox", + "Luc Ritchie", + "Guido Gambardella", + "Jerry Ling", + "Mark Govea", + "Vecht ", + "Jonathan Eppele", + "Shimin Kuang", + "Rish Kundalia", + "Achille Brighton", + "Kirk Werklund", + "Ripta Pasay", + "Felipe Diniz", + ], + "n_patron_columns": 2, + } + + +class Thumbnail(SpecialThreeDScene): + def construct(self): + sphere = ParametricSurface( + func=lambda u, v: 2 * np.array([ + np.cos(v) * np.sin(u) + 0.2 * np.cos(3 * u), + np.sin(v) * np.sin(u), + np.cos(u) + 0.2 * np.sin(4 * v) - 0.3 * np.cos(3 * u) + ]), + resolution=(24, 48), + u_min=0.001, + u_max=PI - 0.001, + v_min=0, + v_max=TAU, + ) + sphere.rotate(70 * DEGREES, DOWN) + self.set_camera_orientation( + phi=80 * DEGREES, + theta=-90 * DEGREES, + ) + # randy = Randolph(mode="telepath") + # eyes = VGroup(randy.eyes, randy.pupils) + # eyes.scale(3.5) + # eyes.rotate(90 * DEGREES, RIGHT) + # eyes.next_to(sphere, OUT, buff=0) + + self.add(sphere) diff --git a/old_projects/counting_in_binary.py b/old_projects/counting_in_binary.py index 7d54f5f7..0ccdab43 100644 --- a/old_projects/counting_in_binary.py +++ b/old_projects/counting_in_binary.py @@ -10,7 +10,7 @@ from big_ol_pile_of_manim_imports import * from script_wrapper import command_line_create_scene MOVIE_PREFIX = "counting_in_binary/" -BASE_HAND_FILE = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, "Base.mp4") +BASE_HAND_FILE = os.path.join(VIDEO_DIR, MOVIE_PREFIX, "Base.mp4") FORCED_FRAME_DURATION = 0.02 TIME_RANGE = (0, 42) INITIAL_PADDING = 27 @@ -84,7 +84,7 @@ class Hand(ImageMobject): def __init__(self, num, small = False, **kwargs): Mobject2D.__init__(self, **kwargs) path = os.path.join( - ANIMATIONS_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%num + VIDEO_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%num ) invert = False if not self.read_in_cached_attrs(path, invert): @@ -160,7 +160,7 @@ class SaveEachNumber(OverHand): OverHand.construct(self) for count in COUNT_TO_FRAME_NUM: path = os.path.join( - ANIMATIONS_DIR, MOVIE_PREFIX, "images", + VIDEO_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%count ) Image.fromarray(self.frames[COUNT_TO_FRAME_NUM[count]]).save(path) diff --git a/old_projects/lost_lecture.py b/old_projects/lost_lecture.py index 2f13b2df..599230c8 100644 --- a/old_projects/lost_lecture.py +++ b/old_projects/lost_lecture.py @@ -1459,6 +1459,7 @@ class GeometryProofLand(Scene): PINK, RED, YELLOW, GREEN, GREEN_A, BLUE, MAROON_E, MAROON_B, YELLOW, BLUE, ], + "text": "Geometry proof land", } def construct(self): @@ -1469,6 +1470,7 @@ class GeometryProofLand(Scene): colors = list(self.colors) random.shuffle(colors) word_outlines.set_color_by_gradient(*colors) + word_outlines.set_stroke(width=5) circles = VGroup() for letter in word: @@ -1485,14 +1487,16 @@ class GeometryProofLand(Scene): LaggedStart(MoveToTarget, circles), run_time=2 ) + self.add(word_outlines, circles) self.play(LaggedStart( - ShowCreationThenDestruction, word_outlines, - run_time=4 - )) + FadeIn, word_outlines, + run_time=3, + rate_func=there_and_back, + ), Animation(circles)) self.wait() def get_geometry_proof_land_word(self): - word = TextMobject("Geometry proof land") + word = TextMobject(self.text) word.rotate(-90 * DEGREES) word.scale(0.25) word.shift(3 * RIGHT) @@ -1502,6 +1506,7 @@ class GeometryProofLand(Scene): word.center() word.to_edge(UP) word.set_color_by_gradient(*self.colors) + word.set_background_stroke(width=0) return word diff --git a/old_projects/name_animation.py b/old_projects/name_animation.py index b1880ffe..61f6672b 100644 --- a/old_projects/name_animation.py +++ b/old_projects/name_animation.py @@ -99,7 +99,7 @@ if __name__ == "__main__": animated_name=name, write_to_movie=True, output_directory=os.path.join( - ANIMATIONS_DIR, + VIDEO_DIR, "active_projects", "name_animations", ), diff --git a/old_projects/playground_counting_in_binary.py b/old_projects/playground_counting_in_binary.py index 070de121..0848d5b1 100644 --- a/old_projects/playground_counting_in_binary.py +++ b/old_projects/playground_counting_in_binary.py @@ -57,7 +57,7 @@ class Hand(ImageMobject): def __init__(self, num, **kwargs): Mobject2D.__init__(self, **kwargs) path = os.path.join( - ANIMATIONS_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%num + VIDEO_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%num ) invert = False if self.read_in_cached_attrs(path, invert): @@ -83,14 +83,14 @@ class EdgeDetection(SceneFromVideo): return "-".join([filename.split(".")[0], str(t1), str(t2)]) def construct(self, filename, t1, t2): - path = os.path.join(ANIMATIONS_DIR, filename) + path = os.path.join(VIDEO_DIR, filename) SceneFromVideo.construct(self, path) self.apply_gaussian_blur() self.apply_edge_detection(t1, t2) class BufferedCounting(SceneFromVideo): def construct(self): - path = os.path.join(ANIMATIONS_DIR, "CountingInBinary.m4v") + path = os.path.join(VIDEO_DIR, "CountingInBinary.m4v") time_range = (3, 42) SceneFromVideo.construct(self, path, time_range = time_range) self.buffer_pixels(spreads = (3, 2)) @@ -130,7 +130,7 @@ class ClearLeftSide(SceneFromVideo): return scenename def construct(self, scenename): - path = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, scenename + ".mp4") + path = os.path.join(VIDEO_DIR, MOVIE_PREFIX, scenename + ".mp4") SceneFromVideo.construct(self, path) self.set_color_region_over_time_range( Region(lambda x, y : x < -1, shape = self.shape) @@ -148,7 +148,7 @@ class DraggedPixels(SceneFromVideo): return args[0] def construct(self, video): - path = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, video+".mp4") + path = os.path.join(VIDEO_DIR, MOVIE_PREFIX, video+".mp4") SceneFromVideo.construct(self, path) self.drag_pixels() @@ -165,11 +165,11 @@ class DraggedPixels(SceneFromVideo): class SaveEachNumber(SceneFromVideo): def construct(self): - path = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, "ClearLeftSideBufferedCounting.mp4") + path = os.path.join(VIDEO_DIR, MOVIE_PREFIX, "ClearLeftSideBufferedCounting.mp4") SceneFromVideo.construct(self, path) for count in COUNT_TO_FRAME_NUM: path = os.path.join( - ANIMATIONS_DIR, MOVIE_PREFIX, "images", + VIDEO_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%count ) Image.fromarray(self.frames[COUNT_TO_FRAME_NUM[count]]).save(path) @@ -184,7 +184,7 @@ class ShowCounting(SceneFromVideo): return filename def construct(self, filename): - path = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, filename + ".mp4") + path = os.path.join(VIDEO_DIR, MOVIE_PREFIX, filename + ".mp4") SceneFromVideo.construct(self, path) total_time = len(self.frames)*self.frame_duration for count in range(32): @@ -208,7 +208,7 @@ class ShowFrameNum(SceneFromVideo): return filename def construct(self, filename): - path = os.path.join(ANIMATIONS_DIR, MOVIE_PREFIX, filename+".mp4") + path = os.path.join(VIDEO_DIR, MOVIE_PREFIX, filename+".mp4") SceneFromVideo.construct(self, path) for frame, count in zip(self.frames, it.count()): print(count + "of" + len(self.frames)) diff --git a/active_projects/quat3d.py b/old_projects/quat3d.py similarity index 96% rename from active_projects/quat3d.py rename to old_projects/quat3d.py index 18d228fe..a0e4e5aa 100644 --- a/active_projects/quat3d.py +++ b/old_projects/quat3d.py @@ -1,5 +1,5 @@ from big_ol_pile_of_manim_imports import * -from active_projects.quaternions import * +from old_projects.quaternions import * W_COLOR = YELLOW I_COLOR = GREEN @@ -148,6 +148,25 @@ class Gimbal(VGroup): # Scenes +class ButFirst(TeacherStudentsScene): + def construct(self): + for student in self.students: + student.change("surprised") + + self.teacher_says("But first!") + self.change_all_student_modes("happy") + self.play(RemovePiCreatureBubble( + self.teacher, + target_mode="raise_right_hand" + )) + self.change_student_modes( + *["pondering"] * 3, + look_at_arg=self.screen, + ) + self.play(self.teacher.look_at, self.screen) + self.wait(4) + + class Introduction(QuaternionHistory): CONFIG = { "names_and_quotes": [ @@ -767,6 +786,16 @@ class EulerAnglesAndGimbal(ShowSeveralQuaternionRotations): return line +class InterpolationFail(Scene): + def construct(self): + words = TextMobject( + "Sometimes interpolating 3d\\\\" + "orientations is tricky..." + ) + words.to_edge(UP) + self.add(words) + + class QuaternionInterpolation(ShowSeveralQuaternionRotations): def construct(self): self.add_q_tracker() @@ -1354,3 +1383,27 @@ class ExpandOutFullProduct(TeacherStudentsScene): look_at_arg=words ) self.wait(5) + + +class Link(Scene): + def construct(self): + word = TextMobject("eater.net/quaternions") + word.add_background_rectangle() + rect = SurroundingRectangle(word) + rect.set_color(BLUE) + arrow = Vector(UR, color=GREEN) + arrow.next_to(rect, UP) + arrow.align_to(rect, RIGHT) + short_arrow = arrow.copy().scale(0.8, about_edge=DL) + + self.add(word) + self.play( + ShowCreation(rect), + GrowArrow(arrow), + ) + for x in range(10): + self.play(Transform( + arrow, short_arrow, + rate_func=there_and_back, + run_time=2 + )) diff --git a/active_projects/quaternions.py b/old_projects/quaternions.py similarity index 99% rename from active_projects/quaternions.py rename to old_projects/quaternions.py index 8447337d..43f6fe22 100644 --- a/active_projects/quaternions.py +++ b/old_projects/quaternions.py @@ -97,12 +97,6 @@ def stereo_project(mobject, axis=0, r=1, outer_r=10, **kwargs): return mobject -def updating_mobject_from_func(func): - mob = func() - mob.add_updater(lambda m: mob.become(func())) - return mob - - class Linus(VGroup): CONFIG = { "body_config": { diff --git a/old_projects/turbulence.py b/old_projects/turbulence.py new file mode 100644 index 00000000..c574b7de --- /dev/null +++ b/old_projects/turbulence.py @@ -0,0 +1,1786 @@ +from big_ol_pile_of_manim_imports import * +from old_projects.div_curl import PureAirfoilFlow +from old_projects.div_curl import VectorFieldSubmobjectFlow +from old_projects.div_curl import VectorFieldPointFlow +from old_projects.div_curl import four_swirls_function +from old_projects.lost_lecture import ShowWord + + +class CreationDestructionMobject(VMobject): + CONFIG = { + "start_time": 0, + "frequency": 0.25, + "max_ratio_shown": 0.3, + "use_copy": True, + } + + def __init__(self, template, **kwargs): + VMobject.__init__(self, **kwargs) + if self.use_copy: + self.ghost_mob = template.copy().fade(1) + self.add(self.ghost_mob) + else: + self.ghost_mob = template + # Don't add + self.shown_mob = template.deepcopy() + self.shown_mob.clear_updaters() + self.add(self.shown_mob) + self.total_time = self.start_time + + def update(mob, dt): + mob.total_time += dt + period = 1.0 / mob.frequency + unsmooth_alpha = (mob.total_time % period) / period + alpha = bezier([0, 0, 1, 1])(unsmooth_alpha) + mrs = mob.max_ratio_shown + mob.shown_mob.pointwise_become_partial( + mob.ghost_mob, + max(interpolate(-mrs, 1, alpha), 0), + min(interpolate(0, 1 + mrs, alpha), 1), + ) + + self.add_updater(update) + + +class Eddy(VMobject): + CONFIG = { + "cd_mob_config": { + "frequency": 0.2, + "max_ratio_shown": 0.3 + }, + "n_spirils": 5, + "n_layers": 20, + "radius": 1, + "colors": [BLUE_A, BLUE_E], + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + lines = self.get_lines() + # self.add(lines) + self.add(*[ + CreationDestructionMobject(line, **self.cd_mob_config) + for line in lines + ]) + self.randomize_times() + + def randomize_times(self): + for submob in self.submobjects: + if hasattr(submob, "total_time"): + T = 1.0 / submob.frequency + submob.total_time = T * random.random() + + def get_lines(self): + a = 0.2 + return VGroup(*[ + self.get_line(r=self.radius * (1 - a + 2 * a * random.random())) + for x in range(self.n_layers) + ]) + + def get_line(self, r): + return ParametricFunction( + lambda t: r * (t + 1)**(-1) * np.array([ + np.cos(TAU * t), + np.sin(TAU * t), + 0, + ]), + t_min=0.1 * random.random(), + t_max=self.n_spirils, + stroke_width=1, + color=interpolate_color(*self.colors, random.random()) + ) + + +class Chaos(Eddy): + CONFIG = { + "n_lines": 12, + "height": 1, + "width": 2, + "n_midpoints": 4, + "cd_mob_config": { + "use_copy": False, + "frequency": 1, + "max_ratio_shown": 0.8 + } + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + rect = Rectangle(height=self.height, width=self.width) + rect.move_to(ORIGIN, DL) + rect.fade(1) + self.rect = rect + self.add(rect) + + lines = self.get_lines() + self.add(*[ + CreationDestructionMobject(line, **self.cd_mob_config) + for line in lines + ]) + self.randomize_times() + lines.fade(1) + self.add(lines) + + def get_lines(self): + return VGroup(*[ + self.get_line(y) + for y in np.linspace(0, self.height, self.n_lines) + ]) + + def get_line(self, y): + frequencies = [0] + list(2 + 2 * np.random.random(self.n_midpoints)) + [0] + rect = self.rect + line = Line( + y * UP, y * UP + self.width * RIGHT, + stroke_width=1 + ) + line.insert_n_anchor_points(self.n_midpoints) + line.total_time = random.random() + delta_h = self.height / (self.n_lines - 1) + + def update(line, dt): + x0, y0 = rect.get_corner(DL)[:2] + x1, y1 = rect.get_corner(UR)[:2] + line.total_time += dt + xs = np.linspace(x0, x1, self.n_midpoints + 2) + new_anchors = [ + np.array([ + x + 1.0 * delta_h * np.cos(f * line.total_time), + y0 + y + 1.0 * delta_h * np.cos(f * line.total_time), + 0 + ]) + for (x, f) in zip(xs, frequencies) + ] + line.set_points_smoothly(new_anchors) + + line.add_updater(update) + return line + + +class DoublePendulum(VMobject): + CONFIG = { + "start_angles": [3 * PI / 7, 3 * PI / 4], + "color1": BLUE, + "color2": RED, + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + line1 = Line(ORIGIN, UP) + dot1 = Dot(color=self.color1) + dot1.add_updater(lambda d: d.move_to(line1.get_end())) + line2 = Line(UP, 2 * UP) + dot2 = Dot(color=self.color2) + dot2.add_updater(lambda d: d.move_to(line2.get_end())) + self.add(line1, line2, dot1, dot2) + + # Largely copied from https://scipython.com/blog/the-double-pendulum/ + # Pendulum rod lengths (m), bob masses (kg). + L1, L2 = 1, 1 + m1, m2 = 1, 1 + # The gravitational acceleration (m.s-2). + g = 9.81 + + self.state_vect = np.array([ + self.start_angles[0], 0, + self.start_angles[1], 0, + ]) + self.state_vect += np.random.random(4) * 1e-7 + + def update(group, dt): + for x in range(2): + line1, line2 = group.submobjects[:2] + theta1, z1, theta2, z2 = group.state_vect + + c, s = np.cos(theta1 - theta2), np.sin(theta1 - theta2) + + theta1dot = z1 + z1dot = (m2 * g * np.sin(theta2) * c - m2 * s * (L1 * (z1**2) * c + L2 * z2**2) - + (m1 + m2) * g * np.sin(theta1)) / L1 / (m1 + m2 * s**2) + theta2dot = z2 + z2dot = ((m1 + m2) * (L1 * (z1**2) * s - g * np.sin(theta2) + g * np.sin(theta1) * c) + + m2 * L2 * (z2**2) * s * c) / L2 / (m1 + m2 * s**2) + + group.state_vect += 0.5 * dt * np.array([ + theta1dot, z1dot, theta2dot, z2dot, + ]) + group.state_vect[1::2] *= 0.9999 + + p1 = L1 * np.sin(theta1) * RIGHT - L1 * np.cos(theta1) * UP + p2 = p1 + L2 * np.sin(theta2) * RIGHT - L2 * np.cos(theta2) * UP + + line1.put_start_and_end_on(ORIGIN, p1) + line2.put_start_and_end_on(p1, p2) + + self.add_updater(update) + + +class DoublePendulums(VGroup): + def __init__(self, **kwargs): + colors = [BLUE, RED, YELLOW, PINK, MAROON_B, PURPLE, GREEN] + VGroup.__init__( + self, + *[ + DoublePendulum( + color1=random.choice(colors), + color2=random.choice(colors), + ) + for x in range(5) + ], + **kwargs, + ) + + +class Diffusion(VMobject): + CONFIG = { + "height": 1.5, + "n_dots": 1000, + "colors": [RED, BLUE] + } + + def __init__(self, **kwargs): + VMobject.__init__(self, **kwargs) + self.add_dots() + self.add_invisible_circles() + + def add_dots(self): + dots = VGroup(*[Dot() for x in range(self.n_dots)]) + dots.arrange_submobjects_in_grid(buff=SMALL_BUFF) + dots.center() + dots.set_height(self.height) + dots.sort_submobjects(lambda p: p[0]) + dots[:len(dots) // 2].set_color(self.colors[0]) + dots[len(dots) // 2:].set_color(self.colors[1]) + dots.set_fill(opacity=0.8) + self.dots = dots + self.add(dots) + + def add_invisible_circles(self): + circles = VGroup() + for dot in self.dots: + point = dot.get_center() + radius = get_norm(point) + circle = Circle(radius=radius) + circle.rotate(angle_of_vector(point)) + circle.fade(1) + circles.add(circle) + self.add_updater_to_dot(dot, circle) + self.add(circles) + + def add_updater_to_dot(self, dot, circle): + dot.total_time = 0 + radius = get_norm(dot.get_center()) + freq = 0.1 + 0.05 * random.random() + 0.05 / radius + + def update(dot, dt): + dot.total_time += dt + prop = (freq * dot.total_time) % 1 + dot.move_to(circle.point_from_proportion(prop)) + + dot.add_updater(update) + + +class NavierStokesEquations(TexMobject): + CONFIG = { + "tex_to_color_map": { + "\\rho": YELLOW, + "\\mu": GREEN, + "\\textbf{v}": BLUE, + "p{}": RED, + }, + "width": 10, + } + + def __init__(self, **kwargs): + v_tex = "\\textbf{v}" + TexMobject.__init__( + self, + "\\rho", + "\\left(" + "{\\partial", v_tex, "\\over", + "\\partial", "t}", + "+", + v_tex, "\\cdot", "\\nabla", v_tex, + "\\right)", + "=", + "-", "\\nabla", "p{}", "+", + "\\mu", "\\nabla^2", v_tex, "+", + # "\\frac{1}{3}", "\\mu", "\\nabla", + # "(", "\\nabla", "\\cdot", v_tex, ")", "+", + "\\textbf{F}", + "\\qquad\\qquad", + "\\nabla", "\\cdot", v_tex, "=", "0", + **kwargs + ) + self.set_width(self.width) + + def get_labels(self): + parts = self.get_parts() + words = [ + "Analogous to \\\\ mass $\\times$ acceleration", + "Pressure\\\\forces", + "Viscous\\\\forces", + "External\\\\forces", + ] + + result = VGroup() + braces = VGroup() + word_mobs = VGroup() + for i, part, word in zip(it.count(), parts, words): + brace = Brace(part, DOWN, buff=SMALL_BUFF) + word_mob = brace.get_text(word) + word_mob.scale(0.7, about_edge=UP) + word_mobs.add(word_mob) + braces.add(brace) + result.add(VGroup(brace, word_mob)) + word_mobs[1:].arrange_submobjects(RIGHT, buff=MED_SMALL_BUFF) + word_mobs[1:].next_to(braces[2], DOWN, SMALL_BUFF) + word_mobs[1].set_color(RED) + word_mobs[2].set_color(GREEN) + return result + + def get_parts(self): + return VGroup( + self[:12], + self[13:16], + self[17:20], + self[21:22], + ) + + +class Test(Scene): + def construct(self): + self.add(DoublePendulums()) + self.wait(30) + +# Scenes + + +class EddyReference(Scene): + CONFIG = { + "radius": 1, + "label": "Eddy", + "label": "", + } + + def construct(self): + eddy = Eddy(radius=self.radius) + new_eddy = eddy.get_lines() + for line in new_eddy: + line.set_stroke( + width=(3 + 3 * random.random()) + ) + label = TextMobject(self.label) + label.next_to(new_eddy, UP) + + self.play( + LaggedStart(ShowCreationThenDestruction, new_eddy), + FadeIn( + label, + rate_func=there_and_back_with_pause, + ), + run_time=3 + ) + + +class EddyReferenceWithLabel(EddyReference): + CONFIG = { + "label": "Eddy" + } + + +class EddyLabels(Scene): + def construct(self): + labels = VGroup( + TextMobject("Large eddy"), + TextMobject("Medium eddy"), + TextMobject("Small eddy"), + ) + for label in labels: + self.play(FadeIn( + label, + rate_func=there_and_back_with_pause, + run_time=3 + )) + + +class LargeEddyReference(EddyReference): + CONFIG = { + "radius": 2, + "label": "" + } + + +class MediumEddyReference(EddyReference): + CONFIG = { + "radius": 0.8, + "label": "Medium eddy" + } + + +class SmallEddyReference(EddyReference): + CONFIG = { + "radius": 0.25, + "label": "Small eddy" + } + + +class SomeTurbulenceEquations(PiCreatureScene): + def construct(self): + randy, morty = self.pi_creatures + navier_stokes = NavierStokesEquations() + line = Line(randy.get_right(), morty.get_left()) + navier_stokes.replace(line, dim_to_match=0) + navier_stokes.scale(1.2) + + distribution = TexMobject( + "E(k) \\propto k^{-5/3}", + tex_to_color_map={ + "k": GREEN, + "-5/3": YELLOW, + } + ) + distribution.next_to(morty, UL) + brace = Brace(distribution, DOWN, buff=SMALL_BUFF) + brace_words = brace.get_text("Explained soon...") + brace_group = VGroup(brace, brace_words) + + self.play( + Write(navier_stokes), + randy.change, "confused", navier_stokes, + morty.change, "confused", navier_stokes, + ) + self.wait(3) + self.play( + morty.change, "raise_right_hand", distribution, + randy.look_at, distribution, + FadeInFromDown(distribution), + navier_stokes.fade, 0.5, + ) + self.play(GrowFromCenter(brace_group)) + self.play(randy.change, "pondering", distribution) + self.wait(3) + dist_group = VGroup(distribution, brace_group) + self.play( + LaggedStart(FadeOut, VGroup(randy, morty, navier_stokes)), + dist_group.scale, 1.5, + dist_group.center, + dist_group.to_edge, UP, + ) + self.wait() + + def create_pi_creatures(self): + randy, morty = Randolph(), Mortimer() + randy.to_corner(DL) + morty.to_corner(DR) + return (randy, morty) + + +class JokeRingEquation(Scene): + def construct(self): + items = VGroup( + TextMobject("Container with a lip"), + TextMobject("Fill with smoke (or fog)"), + TextMobject("Hold awkwardly"), + ) + line = Line(LEFT, RIGHT).set_width(items.get_width() + 1) + items.add(line) + items.add(TextMobject("Vortex ring")) + items.arrange_submobjects(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT) + line.shift(LEFT) + plus = TexMobject("+") + plus.next_to(line.get_left(), UR, SMALL_BUFF) + line.add(plus) + items.to_edge(RIGHT) + + point = 3.8 * LEFT + 0.2 * UP + arrow1 = Arrow( + items[0].get_left(), point + 0.8 * UP + 0.3 * RIGHT, + use_rectangular_stem=False, + path_arc=90 * DEGREES, + ) + arrow1.pointwise_become_partial(arrow1, 0, 0.99) + + arrow2 = Arrow( + items[1].get_left(), point, + ) + arrows = VGroup(arrow1, arrow2) + + for i in 0, 1: + self.play( + FadeInFromDown(items[i]), + ShowCreation(arrows[i]) + ) + self.wait() + self.play(LaggedStart(FadeIn, items[2:])) + self.wait() + self.play(FadeOut(arrows)) + self.wait() + + +class VideoOnPhysicsGirlWrapper(Scene): + def construct(self): + rect = ScreenRectangle(height=6) + title = TextMobject("Video on Physics Girl") + title.scale(1.5) + title.to_edge(UP) + rect.next_to(title, DOWN) + + self.add(title) + self.play(ShowCreation(rect)) + self.wait() + + +class LightBouncingOffFogParticle(Scene): + def construct(self): + words = TextMobject( + "Light bouncing\\\\", + "off fog particles" + ) + arrow = Vector(UP + 0.5 * RIGHT) + arrow.next_to(words, UP) + arrow.set_color(WHITE) + + self.add(words) + self.play(GrowArrow(arrow)) + self.wait() + + +class NightHawkInLightWrapper(Scene): + def construct(self): + title = TextMobject("NightHawkInLight") + title.scale(1.5) + title.to_edge(UP) + rect = ScreenRectangle(height=6) + rect.next_to(title, DOWN) + self.add(title) + self.play(ShowCreation(rect)) + self.wait() + + +class CarefulWithLasers(TeacherStudentsScene): + def construct(self): + morty = self.teacher + randy = self.students[1] + randy2 = self.students[2] + # randy.change('hooray') + laser = VGroup( + Rectangle( + height=0.1, + width=0.3, + fill_color=LIGHT_GREY, + fill_opacity=1, + stroke_color=DARK_GREY, + stroke_width=1, + ), + Line(ORIGIN, 10 * RIGHT, color=GREEN_SCREEN) + ) + laser.arrange_submobjects(RIGHT, buff=0) + laser.rotate(45 * DEGREES) + laser.shift(randy.get_corner(UR) - laser[0].get_center() + 0.1 * DR) + + laser.time = 0 + + def update_laser(laser, dt): + laser.time += dt + laser.rotate( + 0.5 * dt * np.sin(laser.time), + about_point=laser[0].get_center() + ) + laser.add_updater(update_laser) + + self.play(LaggedStart(FadeInFromDown, self.pi_creatures, run_time=1)) + self.add(self.pi_creatures, laser) + for pi in self.pi_creatures: + pi.add_updater(lambda p: p.look_at(laser[1])) + self.play( + ShowCreation(laser), + self.get_student_changes( + "surprised", "hooray", "horrified", + look_at_arg=laser + ) + ) + self.teacher_says( + "Careful with \\\\ the laser!", + target_mode="angry" + ) + self.wait(2.2) + morty.save_state() + randy2.save_state() + self.play( + morty.blink, randy2.blink, + run_time=0.3 + ) + self.wait(2) + self.play( + morty.restore, randy2.restore, + run_time=0.3 + ) + self.wait(2) + + +class SetAsideTurbulence(PiCreatureScene): + def construct(self): + self.pi_creature_says( + "Forget vortex rings", + target_mode="speaking" + ) + self.wait() + self.pi_creature_says( + "look at that\\\\ turbulence!", + target_mode="surprised" + ) + self.wait() + + def create_pi_creature(self): + morty = Mortimer() + morty.to_corner(DR) + return morty + + +class WavingRodLabel(Scene): + def construct(self): + words = TextMobject( + "(Waving a small flag \\\\ through the air)" + ) + self.play(Write(words)) + self.wait() + + +class SeekOrderWords(Scene): + def construct(self): + words = TextMobject("Seek order amidst chaos") + words.scale(1.5) + self.play(Write(words)) + self.wait() + + +class LongEddy(Scene): + def construct(self): + self.add(Eddy()) + self.wait(30) + + +class LongDoublePendulum(Scene): + def construct(self): + self.add(DoublePendulums()) + self.wait(30) + + +class LongDiffusion(Scene): + def construct(self): + self.add(Diffusion()) + self.wait(30) + + +class AskAboutTurbulence(TeacherStudentsScene): + def construct(self): + self.pi_creatures_ask() + self.divide_by_qualitative_quantitative() + self.three_qualitative_descriptors() + self.rigorous_definition() + + def pi_creatures_ask(self): + morty = self.teacher + randy = self.students[1] + morty.change("surprised") + + words = TextMobject("Wait,", "what", "exactly \\\\", "is turbulence?") + question = TextMobject("What", "is turbulence?") + question.to_edge(UP, buff=MED_SMALL_BUFF) + h_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH - 1) + h_line.next_to(question, DOWN, buff=MED_LARGE_BUFF) + + self.student_says( + words, + target_mode='raise_left_hand', + added_anims=[morty.change, 'pondering'] + ) + self.change_student_modes( + "erm", "raise_left_hand", "confused", + ) + self.wait(3) + self.play( + morty.change, "raise_right_hand", + FadeOut(randy.bubble), + ReplacementTransform(VGroup(words[1], words[3]), question), + FadeOut(VGroup(words[0], words[2])), + self.get_student_changes( + *3 * ["pondering"], + look_at_arg=question + ) + ) + self.play( + ShowCreation(h_line), + LaggedStart( + FadeOutAndShiftDown, self.pi_creatures, + run_time=1, + lag_ratio=0.8 + ) + ) + self.wait() + + self.question = question + self.h_line = h_line + + def divide_by_qualitative_quantitative(self): + v_line = Line( + self.h_line.get_center(), + FRAME_HEIGHT * DOWN / 2, + ) + words = VGroup( + TextMobject("Features", color=YELLOW), + TextMobject("Rigorous definition", color=BLUE), + ) + words.next_to(self.h_line, DOWN) + words[0].shift(FRAME_WIDTH * LEFT / 4) + words[1].shift(FRAME_WIDTH * RIGHT / 4) + self.play( + ShowCreation(v_line), + LaggedStart(FadeInFromDown, words) + ) + self.wait() + + self.words = words + + def three_qualitative_descriptors(self): + words = VGroup( + TextMobject("- Eddies"), + TextMobject("- Chaos"), + TextMobject("- Diffusion"), + ) + words.arrange_submobjects( + DOWN, buff=1.25, + aligned_edge=LEFT + ) + words.to_edge(LEFT) + words.shift(MED_LARGE_BUFF * DOWN) + + # objects = VGroup( + # Eddy(), + # DoublePendulum(), + # Diffusion(), + # ) + + # for word, obj in zip(words, objects): + for word in words: + # obj.next_to(word, RIGHT) + self.play( + FadeInFromDown(word), + # VFadeIn(obj) + ) + self.wait(3) + + def rigorous_definition(self): + randy = Randolph() + randy.move_to(FRAME_WIDTH * RIGHT / 4) + randy.change("pondering", self.words[1]) + + self.play(FadeIn(randy)) + self.play(Blink(randy)) + self.wait() + self.play(randy.change, "shruggie") + for x in range(2): + self.play(Blink(randy)) + self.wait() + self.play(randy.look, LEFT) + self.wait(2) + self.play(randy.look, UP) + self.play(Blink(randy)) + self.wait() + + +class BumpyPlaneRide(Scene): + def construct(self): + plane = SVGMobject(file_name="plane2") + self.add(plane) + + total_time = 0 + while total_time < 10: + point = 2 * np.append(np.random.random(2), 2) + DL + point *= 0.2 + time = 0.2 * random.random() + total_time += time + arc = PI * random.random() - PI / 2 + self.play( + plane.move_to, point, + run_time=time, + path_arc=arc + ) + + +class PureAirfoilFlowCopy(PureAirfoilFlow): + def modify_vector_field(self, vector_field): + PureAirfoilFlow.modify_vector_field(self, vector_field) + vector_field.set_fill(opacity=0.1) + vector_field.set_stroke(opacity=0.1) + + +class LaminarFlowLabel(Scene): + def construct(self): + words = TextMobject("Laminar flow") + words.scale(1.5) + words.to_edge(UP) + subwords = TextMobject( + "`Lamina', in Latin, means \\\\" + "``a thin sheet of material''", + tex_to_color_map={"Lamina": YELLOW}, + arg_separator="", + ) + subwords.next_to(words, DOWN, MED_LARGE_BUFF) + VGroup(words, subwords).set_background_stroke(width=4) + self.play(Write(words)) + self.wait() + self.play(FadeInFromDown(subwords)) + self.wait() + + +class HighCurlFieldBreakingLayers(Scene): + CONFIG = { + "flow_anim": VectorFieldSubmobjectFlow, + } + + def construct(self): + lines = VGroup(*[ + self.get_line() + for x in range(20) + ]) + lines.arrange_submobjects(DOWN, buff=MED_SMALL_BUFF) + lines[0::2].set_color(BLUE) + lines[1::2].set_color(RED) + all_dots = VGroup(*it.chain(*lines)) + + def func(p): + vect = four_swirls_function(p) + norm = get_norm(vect) + if norm > 2: + vect *= 4.0 / get_norm(vect)**2 + return vect + + self.add(lines) + self.add(self.flow_anim(all_dots, func)) + self.wait(16) + + def get_line(self): + line = VGroup(*[Dot() for x in range(100)]) + line.set_height(0.1) + line.arrange_submobjects(RIGHT, buff=0) + line.set_width(10) + return line + + +class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers): + CONFIG = { + "flow_anim": VectorFieldPointFlow + } + + def get_line(self): + line = Line(LEFT, RIGHT) + line.insert_n_anchor_points(500) + line.set_width(5) + return line + + +class VorticitySynonyms(Scene): + def construct(self): + words = VGroup( + TextMobject("High", "vorticity"), + TexMobject( + "\\text{a.k.a} \\,", + "|\\nabla \\times \\vec{\\textbf{v}}| > 0" + ), + TextMobject("a.k.a", "high", "swirly-swirly", "factor"), + ) + words[0].set_color_by_tex("vorticity", BLUE) + words[1].set_color_by_tex("nabla", BLUE) + words[2].set_color_by_tex("swirly", BLUE) + words.arrange_submobjects( + DOWN, + aligned_edge=LEFT, + buff=MED_LARGE_BUFF + ) + + for word in words: + word.add_background_rectangle() + self.play(FadeInFromDown(word)) + self.wait() + + +class VorticityDoesNotImplyTurbulence(TeacherStudentsScene): + def construct(self): + t_to_v = TextMobject( + "Turbulence", "$\\Rightarrow$", "Vorticity", + ) + v_to_t = TextMobject( + "Vorticity", "$\\Rightarrow$", "Turbulence", + ) + for words in t_to_v, v_to_t: + words.move_to(self.hold_up_spot, DR) + words.set_color_by_tex_to_color_map({ + "Vorticity": BLUE, + "Turbulence": GREEN, + }) + v_to_t.submobjects.reverse() + cross = Cross(v_to_t[1]) + + morty = self.teacher + self.play( + morty.change, "raise_right_hand", + FadeInFromDown(t_to_v) + ) + self.wait() + self.play(t_to_v.shift, 2 * UP,) + self.play( + TransformFromCopy(t_to_v, v_to_t, path_arc=PI / 2), + self.get_student_changes( + "erm", "confused", "sassy", + run_time=1 + ), + ShowCreation(cross, run_time=2), + ) + self.add(cross) + self.wait(4) + + +class SurroundingRectangleSnippet(Scene): + def construct(self): + rect = Rectangle() + rect.set_color(YELLOW) + rect.set_stroke(width=5) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + + +class FeynmanOnTurbulence(Scene): + def construct(self): + feynman = ImageMobject("Feynman_Woods", height=4) + name = TextMobject("Richard Feynman") + name.next_to(feynman, DOWN) + quote = TextMobject( + "``", "Turbulence", "is the most\\\\" + "important", "unsolved problem\\\\", + "of classical physics.''", + tex_to_color_map={ + "Turbulence": BLUE, + "unsolved problem\\\\": YELLOW, + }, + ) + quote[0].shift(SMALL_BUFF * RIGHT) + quote.next_to(feynman, RIGHT) + Group(feynman, name, quote).center() + + self.play( + FadeInFrom(feynman, UP), + FadeInFrom(name, DOWN), + Write(quote, run_time=4, lag_factor=5) + ) + self.wait() + + +class ShowNavierStokesEquations(Scene): + def construct(self): + self.introduce_equations() + self.ask_about_evolution() + self.ask_about_reasonable() + self.ask_about_blowup() + self.show_money() + + def introduce_equations(self): + name = TextMobject("Navier-Stokes equations (incompressible)") + equations = NavierStokesEquations() + name.to_edge(UP) + equations.next_to(name, DOWN, MED_LARGE_BUFF) + labels = equations.get_labels() + parts = equations.get_parts() + newtons_second = TextMobject( + "Newton's 2nd law \\\\ $ma = F$" + ) + newtons_second.next_to(parts, DOWN) + variables = TexMobject( + "&\\textbf{v}", "\\text{ is velocity}\\\\", + "&\\rho", "\\text{ is density}\\\\", + "&p{}", "\\text{ is pressure}\\\\", + "&\\mu", "\\text{ is viscosity}\\\\", + tex_to_color_map=NavierStokesEquations.CONFIG["tex_to_color_map"] + ) + variables.to_corner(DL) + + self.play(FadeInFromDown(equations)) + self.play(Write(name)) + self.play(LaggedStart( + FadeInFrom, variables, + lambda m: (m, RIGHT), + )) + self.wait() + self.play(Write(newtons_second)) + self.wait() + self.play( + FadeInFromDown(labels[0]), + newtons_second.next_to, variables, RIGHT, LARGE_BUFF + ) + self.play(CircleThenFadeAround(parts[0])) + self.wait() + self.play(LaggedStart(FadeInFrom, labels[1:])) + self.wait(3) + self.play(LaggedStart( + FadeOut, VGroup(*it.chain(labels, variables, newtons_second)) + )) + + self.equations = equations + + def ask_about_evolution(self): + words = TextMobject( + "Given a start state...", + "...how does it evolve?" + ) + words.arrange_submobjects(RIGHT, buff=2) + + words.next_to(self.equations, DOWN, LARGE_BUFF) + + self.play(Write(words[0])) + self.wait() + self.play(Write(words[1])) + self.wait(2) + self.play(FadeOut(words)) + + def ask_about_reasonable(self): + question = TextMobject( + "Do ``reasonable'' \\\\" + "solutions always\\\\" + "exist?" + ) + self.play(FadeInFromDown(question)) + self.wait() + + self.reasonable_question = question + + def ask_about_blowup(self): + axes, graph = self.get_axes_and_graph() + question = TextMobject("Is this possible?") + question.set_color(YELLOW) + question.move_to(axes.get_corner(UR), LEFT) + question.align_to(axes, UP) + q_arrow = Arrow( + question.get_bottom(), + graph.point_from_proportion(0.8), + buff=SMALL_BUFF, + use_rectangular_stem=False, + path_arc=-60 * DEGREES + ) + q_arrow.set_stroke(WHITE, 3) + morty = Mortimer() + morty.to_corner(DR) + morty.change('confused', graph) + + self.play( + Write(axes, run_time=1), + self.reasonable_question.to_edge, LEFT, + self.reasonable_question.shift, DOWN, + ) + self.play( + Write(question), + ShowCreation(graph), + FadeIn(morty), + ) + self.add(q_arrow, morty) + self.play(ShowCreation(q_arrow), Blink(morty)) + self.wait() + self.play(morty.look_at, question) + self.wait() + self.play(morty.change, "maybe", graph) + self.wait(2) + to_fade = VGroup(question, q_arrow, axes, graph) + self.play( + LaggedStart(FadeOut, to_fade), + morty.change, "pondering" + ) + self.wait(2) + self.play(Blink(morty)) + self.wait(2) + + self.morty = morty + + def show_money(self): + # Million dollar problem + problem = TextMobject( + "Navier-Stokes existence \\\\ and smoothness problems" + ) + money = TextMobject("\\$1{,}000{,}000") + money.set_color(GREEN) + money.next_to(problem, DOWN) + pi1 = Randolph() + pi2 = self.morty + pi1.to_corner(DL) + pis = VGroup(pi1, pi2) + for pi in pis: + pi.change("pondering") + pi.money_eyes = VGroup() + for eye in pi.eyes: + cash = TexMobject("\\$") + cash.set_color(GREEN) + cash.replace(eye, dim_to_match=1) + pi.money_eyes.add(cash) + + self.play( + ReplacementTransform( + self.reasonable_question, + problem, + ), + pi2.look_at, problem, + pi1.look_at, problem, + VFadeIn(pi1), + ) + self.wait() + self.play(FadeInFromLarge(money)) + self.play( + pi1.change, "hooray", + pi2.change, "hooray", + ) + self.play( + ReplacementTransform(pi1.pupils, pi1.money_eyes), + ReplacementTransform(pi2.pupils, pi2.money_eyes), + ) + self.wait() + + # Helpers + def get_axes_and_graph(self): + axes = Axes( + x_min=-1, + x_max=5, + y_min=-1, + y_max=5, + ) + time = TextMobject("Time") + time.next_to(axes.x_axis, RIGHT) + ke = TextMobject("Kinetic energy") + ke.next_to(axes.y_axis, UP) + axes.add(time, ke) + axes.set_height(4) + axes.center() + axes.to_edge(DOWN) + v_line = DashedLine( + axes.coords_to_point(4, 0), + axes.coords_to_point(4, 5), + ) + axes.add(v_line) + graph = axes.get_graph( + lambda x: -1.0 / (x - 4), + x_min=0.01, + x_max=3.8, + ) + graph.set_color(BLUE) + return axes, graph + + +class NewtonsSecond(Scene): + def construct(self): + square = Square( + stroke_color=WHITE, + fill_color=LIGHT_GREY, + fill_opacity=0.5, + side_length=1 + ) + label = TexMobject("m") + label.scale(1.5) + label.move_to(square) + square.add(label) + square.save_state() + arrows = VGroup( + Vector(0.5 * UP).next_to(square, UP, buff=0), + Vector(RIGHT).next_to(square, RIGHT, buff=0), + ) + + self.play( + square.shift, 4 * RIGHT + 2 * UP, + rate_func=lambda t: t**2, + run_time=2 + ) + self.wait() + square.restore() + self.play( + LaggedStart(GrowArrow, arrows) + ) + square.add(arrows) + self.play( + square.shift, 4 * RIGHT + 2 * UP, + rate_func=lambda t: t**2, + run_time=2 + ) + self.wait() + + +class CandleLabel(Scene): + def construct(self): + word = TextMobject("Candle") + arrow = Vector(DR, color=WHITE) + arrow.move_to(word.get_bottom() + SMALL_BUFF * DOWN, UL) + self.play( + FadeInFromDown(word), + GrowArrow(arrow) + ) + self.wait() + + +class FiguresOfFluidDynamics(Scene): + def construct(self): + names = [ + "Leonhard Euler", + "George Stokes", + "Hermann von Helmholtz", + "Lewis Richardson", + "Geoffrey Taylor", + "Andrey Kolmogorov", + ] + images = Group(*[ + ImageMobject(name.replace(" ", "_"), height=3) + for name in names + ]) + images.arrange_submobjects(RIGHT, buff=MED_SMALL_BUFF) + image_groups = Group() + for image, name in zip(images, names): + name_mob = TextMobject(name) + name_mob.scale(0.6) + name_mob.next_to(image, DOWN) + image_groups.add(Group(image, name_mob)) + image_groups.arrange_submobjects_in_grid(2, 3) + image_groups.set_height(FRAME_HEIGHT - 1) + + self.play(LaggedStart( + FadeInFromDown, image_groups, + lag_ratio=0.5, + run_time=3 + )) + self.wait() + to_fade = image_groups[:-1] + to_fade.generate_target() + to_fade.target.space_out_submobjects(3) + to_fade.target.shift(3 * UL) + to_fade.target.fade(1) + self.play( + MoveToTarget(to_fade, remover=True), + image_groups[-1].set_height, 5, + image_groups[-1].center, + ) + self.wait() + + +class KineticEnergyBreakdown(Scene): + def construct(self): + title = TextMobject("Kinetic energy breakdown") + title.to_edge(UP) + h_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) + h_line.next_to(title, DOWN) + v_line = Line(h_line.get_center(), FRAME_HEIGHT * DOWN / 2) + lc_title = TextMobject("Simpler physics") + lc_title.set_color(YELLOW) + rc_title = TextMobject("Turbulence physics") + rc_title.set_color(GREEN) + for word, vect in (lc_title, LEFT), (rc_title, RIGHT): + word.next_to(h_line, DOWN) + word.shift(FRAME_WIDTH * vect / 4) + + left_items = VGroup( + TextMobject("- Big moving things"), + TextMobject("- Heat"), + ) + left_items.arrange_submobjects(DOWN, aligned_edge=LEFT) + left_items.next_to(lc_title, DOWN, MED_LARGE_BUFF) + left_items.to_edge(LEFT) + + self.play( + Write(VGroup(*it.chain( + title, h_line, v_line, lc_title, rc_title + ))) + ) + self.wait() + for item in left_items: + self.play(FadeInFrom(item)) + self.wait() + + +class MovingCar(Scene): + def construct(self): + car = Car() + x = 3 + car.move_to(x * LEFT) + self.play(MoveCar(car, x * RIGHT, run_time=4)) + + +class Heat(Scene): + def construct(self): + box = Square( + side_length=2, + stroke_color=WHITE, + ) + balls = VGroup(*[ + self.get_ball(box) + for x in range(20) + ]) + + self.add(box, balls) + self.wait(20) + + def get_ball(self, box): + speed_factor = random.random() + ball = Dot( + radius=0.05, + color=interpolate_color(BLUE, RED, speed_factor) + ) + speed = 2 + 3 * speed_factor + direction = rotate_vector(RIGHT, TAU * random.random()) + ball.velocity = speed * direction + x0, y0, z0 = box.get_corner(DL) + x1, y1, z1 = box.get_corner(UR) + ball.move_to(np.array([ + interpolate(x0, x1, random.random()), + interpolate(y0, y1, random.random()), + 0 + ])) + + def update(ball, dt): + ball.shift(ball.velocity * dt) + if ball.get_left()[0] < box.get_left()[0]: + ball.velocity[0] = abs(ball.velocity[0]) + if ball.get_right()[0] > box.get_right()[0]: + ball.velocity[0] = -abs(ball.velocity[0]) + if ball.get_bottom()[1] < box.get_bottom()[1]: + ball.velocity[1] = abs(ball.velocity[1]) + if ball.get_top()[1] > box.get_top()[1]: + ball.velocity[1] = -abs(ball.velocity[1]) + return ball + + ball.add_updater(update) + return ball + + +class GrowArrowScene(Scene): + def construct(self): + arrow = Arrow(UP, DOWN, color=WHITE) + self.play(GrowArrow(arrow)) + self.wait() + + +class Poem(Scene): + def construct(self): + picture = ImageMobject("Lewis_Richardson") + picture.set_height(4) + picture.center().to_edge(LEFT, buff=LARGE_BUFF) + + title = TextMobject("Poem by Lewis F. Richardson") + title.to_edge(UP) + + poem_text = """ + Big{\\,\\,}whirls have little{\\,\\,}whirls\\\\ + which feed on their velocity,\\\\ + And little{\\,\\,}whirls have lesser{\\,\\,}whirls\\\\ + And so on to viscosity.\\\\ + """ + poem_words = [s for s in poem_text.split(" ") if s] + poem = TextMobject(*poem_words, alignment="") + poem.next_to(picture, RIGHT, LARGE_BUFF) + + self.add(picture) + self.play(FadeInFrom(title, DOWN)) + self.wait() + for word in poem: + if "whirl" in word.get_tex_string(): + word.set_color(BLUE) + self.play(ShowWord(word)) + self.wait(0.005 * len(word)**1.5) + + +class SwirlDiameterD(Scene): + def construct(self): + kwargs = { + "path_arc": PI, + "buff": SMALL_BUFF, + "use_rectangular_stem": False, + "color": WHITE + } + swirl = VGroup( + Arrow(RIGHT, LEFT, **kwargs), + Arrow(LEFT, RIGHT, **kwargs), + ) + swirl.set_stroke(width=5) + f = 1.5 + swirl.scale(f) + + h_line = DashedLine( + f * LEFT, f * RIGHT, + color=YELLOW, + ) + D_label = TexMobject("D") + D_label.scale(2) + D_label.next_to(h_line, UP, SMALL_BUFF) + D_label.match_color(h_line) + # diam = VGroup(h_line, D_label) + + self.play(*map(ShowCreation, swirl)) + self.play( + GrowFromCenter(h_line), + FadeInFrom(D_label, UP), + ) + self.wait() + + +class KolmogorovGraph(Scene): + def construct(self): + axes = Axes( + x_min=-1, + y_min=-1, + x_max=7, + y_max=9, + y_axis_config={ + "unit_size": 0.7, + } + ) + axes.center().shift(1.5 * RIGHT) + x_label = TexMobject("\\log(D)") + x_label.next_to(axes.x_axis.get_right(), UP) + y_label = TexMobject("\\log(\\text{K.E. at length scale D})") + y_label.scale(0.8) + y_label.next_to(axes.y_axis.get_top(), LEFT) + y_label.shift_onto_screen() + axes.add(x_label, y_label) + + v_lines = VGroup(*[ + DashedLine( + axes.coords_to_point(x, 0), + axes.coords_to_point(x, 9), + color=YELLOW, + stroke_width=1 + ) + for x in [0.5, 5] + ]) + inertial_subrange = TextMobject("``Inertial subrange''") + inertial_subrange.scale(0.7) + inertial_subrange.next_to(v_lines.get_bottom(), UP) + + def func(x): + if 0.5 < x < 5: + return (5 / 3) * x + elif x < 0.5: + return 5 * (x - 0.5) + 0.5 * (5 / 3) + elif x > 5: + return np.log(x) + (5 / 3) * 5 - np.log(5) + + graph = axes.get_graph(func, x_min=0.3, x_max=7) + + prop_label = TexMobject("\\text{K.E.} \\propto D^{5/3}") + prop_label.next_to( + graph.point_from_proportion(0.5), UL, + buff=0 + ) + + self.add(axes) + self.play(ShowCreation(graph)) + self.play(FadeInFromDown(prop_label)) + self.wait() + self.add(v_lines) + self.play(Write(inertial_subrange)) + self.wait() + + +class TechnicalNote(Scene): + def construct(self): + title = TextMobject("Technical note:") + title.to_edge(UP) + title.set_color(RED) + self.add(title) + + words = TextMobject(""" + This idea of quantifying the energy held at different + length scales is typically defined + in terms of an ``energy spectrum'' involving the Fourier + transform of a function measuring the correlations + between the fluid's velocities at different points in space. + I know, yikes! + \\quad\\\\ + \\quad\\\\ + Building up the relevant background for that is a bit cumbersome, + so we'll be thinking about the energy at different scales in + terms of all eddy's with a given diameter. This is admittedly + a less well-defined notion, but it does capture the spirit + of Kolmogorov's result. + \\quad\\\\ + \\quad\\\\ + See the links in the description for more details, + if you're curious. + """, alignment="") + words.scale(0.75) + words.next_to(title, DOWN, LARGE_BUFF) + + self.add(title, words) + + +class FiveThirds(TeacherStudentsScene): + def construct(self): + words = TextMobject( + "5/3", "is a sort of fundamental\\\\ constant of turbulence" + ) + self.teacher_says(words) + self.change_student_modes("pondering", "maybe", "erm") + self.play( + FadeOut(self.teacher.bubble), + FadeOut(words[1]), + self.teacher.change, "raise_right_hand", + words[0].scale, 1.5, + words[0].move_to, self.hold_up_spot + ) + self.change_student_modes("thinking", "pondering", "hooray") + self.wait(3) + + +class TurbulenceGifLabel(Scene): + def construct(self): + title = TextMobject("Turbulence in 2d") + title.to_edge(UP) + + attribution = TextMobject( + "Animation by Gabe Weymouth (@gabrielweymouth)" + ) + attribution.scale(0.5) + attribution.to_edge(DOWN) + + self.play(Write(title)) + self.play(FadeInFrom(attribution, UP)) + self.wait() + + +class VortexStretchingLabel(Scene): + def construct(self): + title = TextMobject("Vortex stretching") + self.play(Write(title)) + self.wait() + + +class VortedStretching(ThreeDScene): + CONFIG = { + "n_circles": 200, + } + + def construct(self): + axes = ThreeDAxes() + axes.set_stroke(width=1) + self.add(axes) + self.move_camera( + phi=70 * DEGREES, + theta=-145 * DEGREES, + run_time=0, + ) + self.begin_ambient_camera_rotation() + + short_circles = self.get_cylinder_circles(2, 0.5, 0.5) + tall_circles = short_circles.copy().scale(0.125) + tall_circles.stretch(16 * 4, 2) + torus_circles = tall_circles.copy() + for circle in torus_circles: + circle.shift(RIGHT) + z = circle.get_center()[2] + circle.shift(z * IN) + angle = PI * z / 2 + circle.rotate(angle, axis=DOWN, about_point=ORIGIN) + + circles = short_circles.copy() + flow_lines = self.get_flow_lines(circles) + + self.add(circles, flow_lines) + self.play(LaggedStart(ShowCreation, circles)) + self.wait(5) + self.play(Transform(circles, tall_circles, run_time=3)) + self.wait(10) + self.play(Transform( + circles, torus_circles, + run_time=3 + )) + self.wait(10) + + def get_cylinder_circles(self, radius, radius_var, max_z): + return VGroup(*[ + ParametricFunction( + lambda t: np.array([ + np.cos(TAU * t) * r, + np.sin(TAU * t) * r, + z + ]), + **self.get_circle_kwargs() + ) + for z in sorted(max_z * np.random.random(self.n_circles)) + for r in [radius + radius_var * random.random()] + ]).center() + + def get_torus_circles(self, out_r, in_r, in_r_var): + result = VGroup() + for u in sorted(np.random.random(self.n_circles)): + r = in_r + in_r_var * random.random() + circle = ParametricFunction( + lambda t: r * np.array([ + np.cos(TAU * t), + np.sin(TAU * t), + 0, + ]), + **self.get_circle_kwargs() + ) + circle.shift(out_r * RIGHT) + circle.rotate( + TAU * u - PI, + about_point=ORIGIN, + axis=DOWN, + ) + result.add(circle) + return result + + def get_flow_lines(self, circle_group): + window = 0.3 + + def update_circle(circle, dt): + circle.total_time += dt + diameter = get_norm( + circle.template.point_from_proportion(0) - + circle.template.point_from_proportion(0.5) + ) + modulus = np.sqrt(diameter) + 0.1 + alpha = (circle.total_time % modulus) / modulus + circle.pointwise_become_partial( + circle.template, + max(interpolate(-window, 1, alpha), 0), + min(interpolate(0, 1 + window, alpha), 1), + ) + + result = VGroup() + for template in circle_group: + circle = template.deepcopy() + circle.set_stroke( + color=interpolate_color(BLUE_A, BLUE_E, random.random()), + # width=3 * random.random() + width=1, + ) + circle.template = template + circle.total_time = 4 * random.random() + circle.add_updater(update_circle) + result.add(circle) + return result + + def get_circle_kwargs(self): + return { + "stroke_color": BLACK, + "stroke_width": 0, + } + + +class TurbulenceEndScreen(PatreonEndScreen): + CONFIG = { + "specific_patrons": [ + "1stViewMaths", + "Adrian Robinson", + "Alexis Olson", + "Andrew Busey", + "Ankalagon", + "Art Ianuzzi", + "Awoo", + "Ayan Doss", + "Bernd Sing", + "Boris Veselinovich", + "Brian Staroselsky", + "Britt Selvitelle", + "Carla Kirby", + "Charles Southerland", + "Chris Connett", + "Christian Kaiser", + "Clark Gaebel", + "Cooper Jones", + "Danger Dai", + "Dave B", + "Dave Kester", + "David Clark", + "Delton Ding", + "Devarsh Desai", + "eaglle", + "Eric Younge", + "Eryq Ouithaqueue", + "Federico Lebron", + "Florian Chudigiewitsch", + "Giovanni Filippi", + "Hal Hildebrand", + "Igor Napolskikh", + "Jacob Magnuson", + "Jameel Syed", + "James Hughes", + "Jan Pijpers", + "Jason Hise", + "Jeff Linse", + "Jeff Straathof", + "Jerry Ling", + "John Griffith", + "John Haley", + "John V Wertheim", + "Jonathan Eppele", + "Jonathan Wilson", + "Jordan Scales", + "Joseph John Cox", + "Julian Pulgarin", + "Kai-Siang Ang", + "Kanan Gill", + "L0j1k", + "Linh Tran", + "Luc Ritchie", + "Ludwig Schubert", + "Lukas -krtek.net- Novy", + "Magister Mugit", + "Magnus Dahlström", + "Mark B Bahu", + "Markus Persson", + "Mathew Bramson", + "Mathias Jansson", + "Matt Langford", + "Matt Roveto", + "Matthew Cocke", + "Mehdi Razavi", + "Michael Faust", + "Michael Hardel", + "Mustafa Mahdi", + "Márton Vaitkus", + "Nero Li", + "Oliver Steele", + "Omar Zrien", + "Peter Ehrnstrom", + "Prasant Jagannath", + "Randy C. Will", + "Richard Burgmann", + "Ripta Pasay", + "Rish Kundalia", + "Robert Teed", + "Roobie", + "Ryan Atallah", + "Ryan Williams", + "Sindre Reino Trosterud", + "Solara570", + "Song Gao", + "Steven Soloway", + "Steven Tomlinson", + "Stevie Metke", + "Ted Suzman", + "Valeriy Skobelev", + "Xavier Bernard", + "Yaw Etse", + "YinYangBalance.Asia", + "Zach Cardwell", + ], + } + + +class LaserWord(Scene): + def construct(self): + self.add(TextMobject("Laser").scale(2)) + + +class TurbulenceWord(Scene): + def construct(self): + self.add(TextMobject("Turbulence").scale(2)) + + +class ArrowScene(Scene): + def construct(self): + arrow = Arrow(LEFT, RIGHT, color=WHITE) + arrow.add_to_back(arrow.copy().set_stroke(BLACK, 5)) + self.add(arrow) diff --git a/old_projects/wallis.py b/old_projects/wallis.py index 768aece7..37ad09cb 100644 --- a/old_projects/wallis.py +++ b/old_projects/wallis.py @@ -5219,19 +5219,21 @@ class Thumbnail(DistanceProductScene): product[i-1:i+2] for i in frac_indices ]) - parts[::2].set_color(GREEN) - parts[1::2].set_color(BLUE) + parts[::2].set_color(WHITE) + parts[1::2].set_color(WHITE) parts[-1].set_color(WHITE) - parts[-1].set_stroke(RED, 1) + parts[-1].set_background_stroke(color=YELLOW, width=3) parts[-1].scale(1.5, about_edge=LEFT) + product[-4:].next_to(product[:-4], DOWN, MED_LARGE_BUFF) + product.scale(1.2).center().to_edge(UP) - new_proof = TextMobject("New proof") - new_proof.scale(2.5) - new_proof.set_color(YELLOW) - new_proof.set_stroke(RED, 0.75) - new_proof.next_to(product, DOWN, MED_LARGE_BUFF) + # new_proof = TextMobject("New proof") + # new_proof.scale(2.5) + # new_proof.set_color(YELLOW) + # new_proof.set_stroke(RED, 0.75) + # new_proof.next_to(product, DOWN, MED_LARGE_BUFF) - circle = self.circle = Circle(radius=8, color=RED) + circle = self.circle = Circle(radius=8, color=YELLOW) circle.move_to(3 * DOWN, DOWN) bottom_dot = Dot(color=BLUE) bottom_dot.move_to(circle.get_bottom()) @@ -5260,7 +5262,7 @@ class Thumbnail(DistanceProductScene): self.add(circle) self.add(lights) self.add(product) - self.add(new_proof) + # self.add(new_proof) self.add(bottom_dot, observer) diff --git a/scene/scene.py b/scene/scene.py index a152334d..213475c9 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -173,7 +173,7 @@ class Scene(Container): ### def continual_update(self, dt): - for mobject in self.get_mobjects(): + for mobject in self.get_mobject_family_members(): mobject.update(dt) for continual_animation in self.continual_animations: continual_animation.update(dt) @@ -194,7 +194,7 @@ class Scene(Container): return True any_time_based_update = any([ len(m.get_time_based_updaters()) > 0 - for m in self.get_mobjects() + for m in self.get_mobject_family_members() ]) if any_time_based_update: return True diff --git a/utils/output_directory_getters.py b/utils/output_directory_getters.py index 14d317fc..6a52f1c8 100644 --- a/utils/output_directory_getters.py +++ b/utils/output_directory_getters.py @@ -1,7 +1,7 @@ import inspect import os -from constants import ANIMATIONS_DIR +from constants import VIDEO_DIR def add_extension_if_not_present(file_name, extension): @@ -28,7 +28,7 @@ def get_scene_output_directory(scene_class): file_path = os.path.join(*sub_parts) file_path = file_path.replace(".pyc", "") file_path = file_path.replace(".py", "") - return guarantee_existance(os.path.join(ANIMATIONS_DIR, file_path)) + return guarantee_existance(os.path.join(VIDEO_DIR, file_path)) def get_movie_output_directory(scene_class, camera_config, frame_duration): diff --git a/utils/space_ops.py b/utils/space_ops.py index 1ddf6f7f..2a1b604a 100644 --- a/utils/space_ops.py +++ b/utils/space_ops.py @@ -170,7 +170,7 @@ def get_unit_normal(v1, v2): def compass_directions(n=4, start_vect=RIGHT): - angle = 2 * np.pi / n + angle = TAU / n return np.array([ rotate_vector(start_vect, k * angle) for k in range(n)