diff --git a/active_projects/clacks/all_questions_scenes.py b/active_projects/clacks/all_questions_scenes.py deleted file mode 100644 index c29d03ce..00000000 --- a/active_projects/clacks/all_questions_scenes.py +++ /dev/null @@ -1,8 +0,0 @@ -from active_projects import clacks - -output_directory = "clacks_question" -all_scenes = [ - clacks.NameIntro, - clacks.MathAndPhysicsConspiring, - clacks.LightBouncing, -] diff --git a/active_projects/clacks/all_s2_scenes.py b/active_projects/clacks/all_s2_scenes.py new file mode 100644 index 00000000..64d949d8 --- /dev/null +++ b/active_projects/clacks/all_s2_scenes.py @@ -0,0 +1,28 @@ +# from active_projects.clacks import question +# from active_projects.clacks import solution1 +from active_projects.clacks.solution2 import block_collision_scenes +from active_projects.clacks.solution2 import simple_scenes +from active_projects.clacks.solution2 import wordy_scenes +from active_projects.clacks.solution2 import pi_creature_scenes +from active_projects.clacks.solution2 import position_phase_space + +OUTPUT_DIRECTORY = "clacks_solution2" +ALL_SCENE_CLASSES = [ + block_collision_scenes.IntroducePreviousTwoVideos, + block_collision_scenes.PreviousTwoVideos, + wordy_scenes.ConnectionToOptics, + pi_creature_scenes.OnAnsweringTwice, + simple_scenes.LastVideoWrapper, + position_phase_space.IntroducePositionPhaseSpace, + position_phase_space.UnscaledPositionPhaseSpaceMass100, + position_phase_space.EqualMassCase, + pi_creature_scenes.AskAboutEqualMassMomentumTransfer, + position_phase_space.FailedAngleRelation, + position_phase_space.UnscaledPositionPhaseSpaceMass10, + pi_creature_scenes.ComplainAboutRelevanceOfAnalogy, + simple_scenes.LastVideoWrapper, + position_phase_space.RescaleCoordinates, + wordy_scenes.ConnectionToOpticsTransparent, + position_phase_space.RescaleCoordinatesMass16, + position_phase_space.RescaleCoordinatesMass64, +] diff --git a/active_projects/clacks/name_bump.py b/active_projects/clacks/name_bump.py index e7972b53..cd79392d 100644 --- a/active_projects/clacks/name_bump.py +++ b/active_projects/clacks/name_bump.py @@ -6,7 +6,7 @@ from active_projects.clacks.question import BlocksAndWallExample class NameBump(BlocksAndWallExample): CONFIG = { - "name": "Magnus Lysfjord", + "name": "Grant Sanderson", "sliding_blocks_config": { "block1_config": { "mass": 1e6, diff --git a/active_projects/clacks/question.py b/active_projects/clacks/question.py index 17dab588..ca8642a2 100644 --- a/active_projects/clacks/question.py +++ b/active_projects/clacks/question.py @@ -1,6 +1,4 @@ from big_ol_pile_of_manim_imports import * -import subprocess -from pydub import AudioSegment class Block(Square): @@ -217,6 +215,10 @@ class ClackFlashes(ContinualAnimation): continue last_time = time flash = Flash(location, **self.flash_config) + for sm in flash.mobject.family_members_with_points(): + if isinstance(sm, VMobject): + sm.set_stroke(YELLOW, 3) + sm.set_stroke(WHITE, 6, 0.5, background=True) flash.start_time = time flash.end_time = time + flash.run_time self.flashes.append(flash) @@ -224,16 +226,17 @@ class ClackFlashes(ContinualAnimation): def update_mobject(self, dt): total_time = self.external_time + group = self.mobject for flash in self.flashes: if flash.start_time < total_time < flash.end_time: - if flash.mobject not in self.mobject: - self.mobject.add(flash.mobject) + if flash.mobject not in group: + group.add(flash.mobject) flash.update( (total_time - flash.start_time) / flash.run_time ) else: - if flash.mobject in self.mobject: - self.mobject.remove(flash.mobject) + if flash.mobject in group: + group.remove(flash.mobject) class Wall(Line): @@ -266,6 +269,7 @@ class Wall(Line): class BlocksAndWallScene(Scene): CONFIG = { "include_sound": True, + "collision_sound": "clack.wav", "count_clacks": True, "counter_group_shift_vect": LEFT, "sliding_blocks_config": {}, @@ -273,7 +277,6 @@ class BlocksAndWallScene(Scene): "wall_x": -6, "n_wall_ticks": 15, "counter_label": "\\# Collisions: ", - "collision_sound": "clack.wav", "show_flash_animations": True, "min_time_between_sounds": 0.004, } @@ -345,56 +348,30 @@ class BlocksAndWallScene(Scene): return self.counter_mob.set_value(n_clacks) - def create_sound_file(self, clack_data): - clack_file = os.path.join(SOUND_DIR, self.collision_sound) - output_file = self.get_movie_file_path(extension='.wav') + def add_clack_sounds(self, clack_data): + clack_file = self.collision_sound + total_time = self.get_time() times = [ time for location, time in clack_data - if time < 300 # In case of any extremes + if time < total_time ] - - clack = AudioSegment.from_wav(clack_file) - total_time = max(times) + 1 - clacks = AudioSegment.silent(int(1000 * total_time)) - last_position = 0 - min_diff = int(1000 * self.min_time_between_sounds) + last_time = 0 for time in times: - position = int(1000 * time) - d_position = position - last_position - if d_position < min_diff: + d_time = time - last_time + if d_time < self.min_time_between_sounds: continue - if time > self.get_time(): - break - last_position = position - clacks = clacks.fade(-50, start=position, end=position + 10) - clacks = clacks.overlay( - clack, - position=position + last_time = time + self.add_sound( + clack_file, + time_offset=(time - total_time), + gain=-20, ) - clacks.export(output_file, format="wav") - return output_file + return self - # TODO, this no longer works - # should use Scene.add_sound instead - def combine_movie_files(self): - Scene.combine_movie_files(self) + def tear_down(self): if self.include_sound: - sound_file_path = self.create_sound_file(self.clack_data) - movie_path = self.get_movie_file_path() - temp_path = self.get_movie_file_path(str(self) + "TempSound") - commands = [ - "ffmpeg", - "-i", movie_path, - "-i", sound_file_path, - "-c:v", "copy", "-c:a", "aac", - '-loglevel', 'error', - "-strict", "experimental", - temp_path, - ] - subprocess.call(commands) - subprocess.call(["rm", sound_file_path]) - subprocess.call(["mv", temp_path, movie_path]) + self.add_clack_sounds(self.clack_data) # Animated scenes @@ -1573,6 +1550,7 @@ class Thumbnail(BlocksAndWallExample, MovingCameraScene): "count_clacks": False, "show_flash_animations": False, "floor_y": -3.0, + "include_sound": False, } def setup(self): @@ -1580,7 +1558,16 @@ class Thumbnail(BlocksAndWallExample, MovingCameraScene): BlocksAndWallExample.setup(self) def construct(self): - self.camera_frame.shift(0.9 * UP) + # self.camera_frame.shift(0.9 * UP) + self.mobjects.insert( + 0, + FullScreenFadeRectangle( + color=DARK_GREY, + opacity=0.5, + sheen_direction=UL, + sheen=0.5, + ), + ) self.thicken_lines() self.grow_labels() self.add_vector() diff --git a/active_projects/clacks/solution2/block_collision_scenes.py b/active_projects/clacks/solution2/block_collision_scenes.py new file mode 100644 index 00000000..b300087f --- /dev/null +++ b/active_projects/clacks/solution2/block_collision_scenes.py @@ -0,0 +1,76 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks.question import BlocksAndWallExample + + +class PreviousTwoVideos(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e2, + "velocity": -1, + "width": 4, + "distance": 8, + }, + "block2_config": { + "width": 4, + "distance": 3, + }, + }, + "floor_y": -3, + "wait_time": 15, + } + + def setup(self): + super().setup() + blocks = self.blocks + videos = Group( + ImageMobject("ClacksSolution1Thumbnail"), + ImageMobject("ClacksQuestionThumbnail"), + ) + for n, video, block in zip([2, 1], videos, blocks): + block.fade(1) + video.add(SurroundingRectangle( + video, buff=0, + color=BLUE, + stroke_width=3, + )) + video.replace(block) + + title = TextMobject("Part {}".format(n)) + title.scale(1.5) + title.next_to(video, UP, MED_SMALL_BUFF) + video.add(title) + + def update_videos(videos): + for video, block in zip(videos, blocks): + video.move_to(block, DOWN) + video.shift(0.04 * UP) + + videos.add_updater(update_videos) + self.add(videos) + if self.show_flash_animations: + self.add(self.clack_flashes.mobject) + self.videos = videos + + +class IntroducePreviousTwoVideos(PreviousTwoVideos): + CONFIG = { + "show_flash_animations": False, + "include_sound": False, + } + + def construct(self): + blocks = self.blocks + videos = self.videos + + self.remove(blocks) + videos.clear_updaters() + self.remove(videos) + + self.play(FadeInFromLarge(videos[1])) + self.play(TransformFromCopy( + videos[0].copy().fade(1).shift(2 * RIGHT), + videos[0], + rate_func=lambda t: rush_into(t, 3), + )) + # self.wait() diff --git a/active_projects/clacks/solution2/pi_creature_scenes.py b/active_projects/clacks/solution2/pi_creature_scenes.py new file mode 100644 index 00000000..0189ea5f --- /dev/null +++ b/active_projects/clacks/solution2/pi_creature_scenes.py @@ -0,0 +1,78 @@ +from big_ol_pile_of_manim_imports import * + + +class OnAnsweringTwice(TeacherStudentsScene): + def construct(self): + question = TextMobject("Why $\\pi$?") + question.move_to(self.screen) + question.to_edge(UP) + other_questions = VGroup( + TextMobject("Frequency of collisions?"), + TextMobject("Efficient simulation?"), + TextMobject("Time until last collision?"), + ) + for mob in other_questions: + mob.move_to(self.hold_up_spot, DOWN) + + self.add(question) + + self.student_says( + "But we already \\\\ solved it", + bubble_kwargs={"direction": LEFT}, + target_mode="raise_left_hand", + added_anims=[self.teacher.change, "thinking"] + ) + self.change_student_modes("sassy", "angry") + self.wait() + self.play( + RemovePiCreatureBubble(self.students[2]), + self.get_student_changes("erm", "erm"), + ApplyMethod( + question.move_to, self.hold_up_spot, DOWN, + path_arc=-90 * DEGREES, + ), + self.teacher.change, "raise_right_hand", + ) + shown_questions = VGroup(question) + for oq in other_questions: + self.play( + shown_questions.shift, 0.85 * UP, + FadeInFromDown(oq), + self.get_student_changes( + *["pondering"] * 3, + look_at_arg=oq + ) + ) + shown_questions.add(oq) + self.wait(3) + + +class AskAboutEqualMassMomentumTransfer(TeacherStudentsScene): + def construct(self): + pass + + +class ComplainAboutRelevanceOfAnalogy(TeacherStudentsScene): + def construct(self): + self.student_says( + "Why would \\\\ you care", + target_mode="maybe" + ) + self.change_student_modes( + "angry", "sassy", "maybe", + added_anims=[self.teacher.change, "guilty"] + ) + self.wait(2) + self.play( + self.teacher.change, "raise_right_hand", + self.get_student_changes( + "pondering", "erm", "pondering", + look_at_arg=self.hold_up_spot, + ), + RemovePiCreatureBubble(self.students[2]) + ) + self.play( + self.students[2].change, "thinking", + self.hold_up_spot + UP, + ) + self.wait(3) diff --git a/active_projects/clacks/solution2/position_phase_space.py b/active_projects/clacks/solution2/position_phase_space.py new file mode 100644 index 00000000..2df54269 --- /dev/null +++ b/active_projects/clacks/solution2/position_phase_space.py @@ -0,0 +1,1337 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks.question import Block +from active_projects.clacks.question import Wall +from active_projects.clacks.question import ClackFlashes + + +class PositionPhaseSpaceScene(Scene): + CONFIG = { + "rescale_coordinates": True, + "wall_x": -6, + "wall_config": { + "height": 1.6, + "tick_spacing": 0.35, + "tick_length": 0.2, + }, + "wall_height": 1.5, + "floor_y": -3.5, + "block1_config": { + "mass": 10, + "distance": 9, + "velocity": 1, + "width": 1.6, + }, + "block2_config": { + "mass": 1, + "distance": 4, + }, + "axes_config": { + "x_min": -0.5, + "x_max": 31, + "y_min": -0.5, + "y_max": 10.5, + "x_axis_config": { + "unit_size": 0.4, + "tick_frequency": 2, + }, + "y_axis_config": { + "unit_size": 0.4, + "tick_frequency": 2, + }, + }, + "axes_center": 5 * LEFT + 0.65 * DOWN, + "ps_dot_config": { + "fill_color": RED, + "background_stroke_width": 1, + "background_stroke_color": BLACK, + "radius": 0.05, + }, + "ps_d2_label_vect": RIGHT, + "clack_sound": "clack", + "mirror_line_class": Line, + "mirror_line_style": { + "stroke_color": WHITE, + "stroke_width": 1, + }, + "d1_eq_e2_line_color": GREEN_SCREEN, + "trajectory_style": { + "stroke_color": YELLOW, + "stroke_width": 2, + } + } + + def setup(self): + self.total_sliding_time = 0 + self.all_items = [ + self.get_floor(), + self.get_wall(), + self.get_blocks(), + self.get_axes(), + self.get_phase_space_point(), + self.get_phase_space_x_line(), + self.get_phase_space_y_line(), + self.get_phase_space_dot(), + self.get_phase_space_d1_label(), + self.get_phase_space_d2_label(), + self.get_d1_brace(), + self.get_d2_brace(), + self.get_d1_label(), + self.get_d2_label(), + self.get_d1_eq_d2_line(), + self.get_d1_eq_d2_label(), + self.get_d2_eq_w2_line(), + self.get_d2_eq_w2_label(), + ] + + def get_floor_wall_corner(self): + return self.wall_x * RIGHT + self.floor_y * UP + + def get_mass_ratio(self): + return op.truediv( + self.block1.mass, + self.block2.mass, + ) + + def d1_to_x(self, d1): + if self.rescale_coordinates: + d1 *= np.sqrt(self.block1.mass) + return d1 + + def d2_to_y(self, d2): + if self.rescale_coordinates: + d2 *= np.sqrt(self.block2.mass) + return d2 + + def ds_to_point(self, d1, d2): + return self.axes.coords_to_point( + self.d1_to_x(d1), self.d2_to_y(d2), + ) + + def point_to_ds(self, point): + x, y = self.axes.point_to_coords(point) + if self.rescale_coordinates: + x /= np.sqrt(self.block1.mass) + y /= np.sqrt(self.block2.mass) + return (x, y) + + def get_d1(self): + return self.get_ds()[0] + + def get_d2(self): + return self.get_ds()[1] + + def get_ds(self): + return self.point_to_ds(self.ps_point.get_location()) + + # Relevant for sliding + def tie_blocks_to_ps_point(self): + def update_blocks(blocks): + d1, d2 = self.point_to_ds(self.ps_point.get_location()) + b1, b2 = blocks + corner = self.get_floor_wall_corner() + b1.move_to(corner + d1 * RIGHT, DL) + b2.move_to(corner + d2 * RIGHT, DR) + self.blocks.add_updater(update_blocks) + + def time_to_ds(self, time): + # Deals in its own phase space, different + # from the one displayed + m1 = self.block1.mass + m2 = self.block2.mass + v1 = self.block1.velocity + start_d1 = self.block1_config["distance"] + start_d2 = self.block2_config["distance"] + w2 = self.block2.width + start_d2 += w2 + ps_speed = np.sqrt(m1) * abs(v1) + theta = np.arctan(np.sqrt(m2 / m1)) + + def ds_to_ps_point(d1, d2): + return np.array([ + d1 * np.sqrt(m1), + d2 * np.sqrt(m2), + 0 + ]) + + def ps_point_to_ds(point): + return ( + point[0] / np.sqrt(m1), + point[1] / np.sqrt(m2), + ) + + ps_point = ds_to_ps_point(start_d1, start_d2) + wedge_corner = ds_to_ps_point(w2, w2) + ps_point -= wedge_corner + # Pass into the mirror worlds + ps_point += time * ps_speed * LEFT + # Reflect back to the real world + angle = angle_of_vector(ps_point) + n = int(angle / theta) + if n % 2 == 0: + ps_point = rotate_vector(ps_point, -n * theta) + else: + ps_point = rotate_vector( + ps_point, + -(n + 1) * theta, + ) + ps_point[1] = abs(ps_point[1]) + ps_point += wedge_corner + return ps_point_to_ds(ps_point) + + def get_clack_data(self): + # Copying from time_to_ds. Not great, but + # maybe I'll factor things out properly later. + m1 = self.block1.mass + m2 = self.block2.mass + v1 = self.block1.velocity + w2 = self.block2.get_width() + h2 = self.block2.get_height() + ps_speed = np.sqrt(m1) * abs(v1) + theta = np.arctan(np.sqrt(m2 / m1)) + + def ds_to_ps_point(d1, d2): + return np.array([ + d1 * np.sqrt(m1), + d2 * np.sqrt(m2), + 0 + ]) + + def ps_point_to_ds(point): + return ( + point[0] / np.sqrt(m1), + point[1] / np.sqrt(m2), + ) + + ps_point = ds_to_ps_point(*self.get_ds()) + wedge_corner = ds_to_ps_point(w2, w2) + ps_point -= wedge_corner + y = ps_point[1] + + clack_data = [] + for k in range(1, int(PI / theta) + 1): + clack_ps_point = np.array([ + y / np.tan(k * theta), y, 0 + ]) + time = get_norm(ps_point - clack_ps_point) / ps_speed + reflected_point = rotate_vector( + clack_ps_point, + -2 * np.ceil((k - 1) / 2) * theta + ) + d1, d2 = ps_point_to_ds(reflected_point + wedge_corner) + loc1 = self.get_floor_wall_corner() + h2 * UP / 2 + d2 * RIGHT + if k % 2 == 0: + loc1 += w2 * LEFT + loc2 = self.ds_to_point(d1, d2) + clack_data.append((time, loc1, loc2)) + return clack_data + + def get_clack_flashes(self): + pass # TODO + + def tie_ps_point_to_time_tracker(self): + time_tracker = self.get_time_tracker( + time=self.total_sliding_time + ) + + def update_ps_point(p): + time = time_tracker.get_value() + ds = self.time_to_ds(time) + p.move_to(self.ds_to_point(*ds)) + self.ps_point.add_updater(update_ps_point) + self.add(time_tracker, self.ps_point) + + def add_clack_flashes(self): + clack_data = self.get_clack_data() + self.clack_times = [ + time for (time, loc1, loc2) in clack_data + ] + self.block_flashes = ClackFlashes([ + (loc1, time) + for (time, loc1, loc2) in clack_data + ]) + self.ps_flashes = ClackFlashes([ + (loc2, time) + for (time, loc1, loc2) in clack_data + ]) + self.add( + self.block_flashes, + self.ps_flashes, + ) + + def get_continually_building_trajectory(self): + trajectory = VMobject() + self.trajectory = trajectory + trajectory.set_style(**self.trajectory_style) + + def get_point(): + return np.array(self.ps_point.get_location()) + + points = [get_point(), get_point()] + trajectory.set_points_as_corners(points) + epsilon = 0.001 + + def update_trajectory(trajectory): + new_point = get_point() + p1, p2 = trajectory.get_anchors()[-2:] + angle = angle_between_vectors( + p2 - p1, + new_point - p2, + ) + if angle > epsilon: + points.append(new_point) + else: + points[-1] = new_point + trajectory.set_points_as_corners(points) + + trajectory.add_updater(update_trajectory) + return trajectory + + def begin_sliding(self, show_trajectory=True): + self.tie_ps_point_to_time_tracker() + self.add_clack_flashes() + if show_trajectory: + if hasattr(self, "trajectory"): + self.trajectory.resume_updating() + else: + self.add(self.get_continually_building_trajectory()) + + def end_sliding(self): + self.ps_point.clear_updaters() + self.remove(self.time_tracker) + to_remove = ["block_flashes", "ps_flashes"] + for attr in to_remove: + if hasattr(self, attr): + self.remove(getattr(self, attr)) + if hasattr(self, "trajectory"): + self.trajectory.suspend_updating() + total_time = self.time_tracker.get_value() + self.total_sliding_time += total_time + for time in self.clack_times: + if time < total_time: + offset = total_time - time + self.add_sound( + "clack", + time_offset=-offset, + ) + + def slide(self, time, stop_condition=None): + self.begin_sliding() + self.wait(time, stop_condition) + self.end_sliding() + + def slide_until(self, stop_condition, max_time=60): + self.slide(max_time, stop_condition=stop_condition) + + def get_ps_point_change_anim(self, d1, d2, **added_kwargs): + b1 = self.block1 + ps_speed = np.sqrt(b1.mass) * abs(b1.velocity) + curr_d1, curr_d2 = self.get_ds() + distance = get_norm([curr_d1 - d1, curr_d2 - d2]) + + # Default + kwargs = { + "run_time": (distance / ps_speed), + "rate_func": None, + } + kwargs.update(added_kwargs) + return ApplyMethod( + self.ps_point.move_to, + self.ds_to_point(d1, d2), + **kwargs + ) + + # Mobject getters + def get_floor(self): + floor = self.floor = Line( + self.wall_x * RIGHT, + FRAME_WIDTH * RIGHT / 2, + stroke_color=WHITE, + stroke_width=3, + ) + floor.move_to(self.get_floor_wall_corner(), LEFT) + return floor + + def get_wall(self): + wall = self.wall = Wall(**self.wall_config) + wall.move_to(self.get_floor_wall_corner(), DR) + return wall + + def get_blocks(self): + blocks = self.blocks = VGroup() + for n in [1, 2]: + config = getattr(self, "block{}_config".format(n)) + block = Block(**config) + block.move_to(self.get_floor_wall_corner(), DL) + block.shift(config["distance"] * RIGHT) + block.label.move_to(block) + block.label.set_fill(BLACK) + block.label.set_stroke(WHITE, 1, background=True) + self.blocks.add(block) + self.block1, self.block2 = blocks + return blocks + + def get_axes(self): + axes = self.axes = Axes(**self.axes_config) + axes.set_stroke(LIGHT_GREY, 2) + axes.shift( + self.axes_center - axes.coords_to_point(0, 0) + ) + axes.labels = self.get_axes_labels(axes) + axes.add(axes.labels) + axes.added_lines = self.get_added_axes_lines(axes) + axes.add(axes.added_lines) + return axes + + def get_added_axes_lines(self, axes): + c2p = axes.coords_to_point + x_mult = y_mult = 1 + if self.rescale_coordinates: + x_mult = np.sqrt(self.block1.mass) + y_mult = np.sqrt(self.block2.mass) + y_lines = VGroup(*[ + Line( + c2p(0, 0), c2p(0, axes.y_max * y_mult + 1), + ).move_to(c2p(x, 0), DOWN) + for x in np.arange(0, axes.x_max) * x_mult + ]) + x_lines = VGroup(*[ + Line( + c2p(0, 0), c2p(axes.x_max * x_mult, 0), + ).move_to(c2p(0, y), LEFT) + for y in np.arange(0, axes.y_max) * y_mult + ]) + line_groups = VGroup(x_lines, y_lines) + for lines in line_groups: + lines.set_stroke(BLUE, 1, 0.5) + lines[1::2].set_stroke(width=0.5, opacity=0.25) + return line_groups + + def get_axes_labels(self, axes, with_sqrts=None): + if with_sqrts is None: + with_sqrts = self.rescale_coordinates + x_label = TexMobject("x = ", "d_1") + y_label = TexMobject("y = ", "d_2") + labels = VGroup(x_label, y_label) + if with_sqrts: + additions = map(TexMobject, [ + "\\sqrt{m_1}", "\\sqrt{m_2}" + ]) + for label, addition in zip(labels, additions): + addition.move_to(label[1], DL) + label[1].next_to( + addition, RIGHT, SMALL_BUFF, + aligned_edge=DOWN + ) + addition[2:].set_color(BLUE) + label.add(addition) + x_label.next_to(axes.x_axis.get_right(), DL, MED_SMALL_BUFF) + y_label.next_to(axes.y_axis.get_top(), DR, MED_SMALL_BUFF) + for label in labels: + label.shift_onto_screen() + return labels + + def get_phase_space_point(self): + ps_point = self.ps_point = VectorizedPoint() + ps_point.move_to(self.ds_to_point( + self.block1.distance, + self.block2.distance + self.block2.width + )) + self.tie_blocks_to_ps_point() + return ps_point + + def get_phase_space_x_line(self): + def get_x_line(): + origin = self.axes.coords_to_point(0, 0) + point = self.ps_point.get_location() + y_axis_point = np.array(origin) + y_axis_point[1] = point[1] + return DashedLine( + y_axis_point, point, + color=GREEN, + stroke_width=2, + ) + self.x_line = updating_mobject_from_func(get_x_line) + return self.x_line + + def get_phase_space_y_line(self): + def get_y_line(): + origin = self.axes.coords_to_point(0, 0) + point = self.ps_point.get_location() + x_axis_point = np.array(origin) + x_axis_point[0] = point[0] + return DashedLine( + x_axis_point, point, + color=RED, + stroke_width=2, + ) + self.y_line = updating_mobject_from_func(get_y_line) + return self.y_line + + def get_phase_space_dot(self): + self.ps_dot = ps_dot = Dot(**self.ps_dot_config) + ps_dot.add_updater(lambda m: m.move_to(self.ps_point)) + return ps_dot + + def get_d_label(self, n, get_d): + label = VGroup( + TexMobject("d_{}".format(n), "="), + DecimalNumber(), + ) + color = GREEN if n == 1 else RED + label[0].set_color_by_tex("d_", color) + label.scale(0.7) + label.set_stroke(BLACK, 3, background=True) + + def update_value(label): + lhs, rhs = label + rhs.set_value(get_d()) + rhs.next_to( + lhs, RIGHT, SMALL_BUFF, + aligned_edge=DOWN, + ) + label.add_updater(update_value) + return label + + def get_phase_space_d_label(self, n, get_d, line, vect): + label = self.get_d_label(n, get_d) + label.add_updater( + lambda m: m.next_to(line, vect, SMALL_BUFF) + ) + return label + + def get_phase_space_d1_label(self): + self.ps_d1_label = self.get_phase_space_d_label( + 1, self.get_d1, self.x_line, UP, + ) + return self.ps_d1_label + + def get_phase_space_d2_label(self): + self.ps_d2_label = self.get_phase_space_d_label( + 2, self.get_d2, self.y_line, + self.ps_d2_label_vect, + ) + return self.ps_d2_label + + def get_d_brace(self, get_right_point): + line = Line(LEFT, RIGHT).set_width(6) + + def get_brace(): + right_point = get_right_point() + left_point = np.array(right_point) + left_point[0] = self.wall_x + line.put_start_and_end_on(left_point, right_point) + return Brace(line, UP, buff=SMALL_BUFF) + + brace = updating_mobject_from_func(get_brace) + return brace + + def get_d1_brace(self): + self.d1_brace = self.get_d_brace( + lambda: self.block1.get_corner(UL) + ) + return self.d1_brace + + def get_d2_brace(self): + self.d2_brace = self.get_d_brace( + lambda: self.block2.get_corner(UR) + ) + # self.flip_brace_nip() + return self.d2_brace + + def flip_brace_nip(self, brace): + nip_index = (len(brace) // 2) - 1 + nip = brace[nip_index:nip_index + 2] + rect = brace[nip_index - 1] + center = rect.get_center() + center[0] = nip.get_center()[0] + nip.rotate(PI, about_point=center) + + def get_brace_d_label(self, n, get_d, brace, vect, buff): + label = self.get_d_label(n, get_d) + label.add_updater( + lambda m: m.next_to(brace, vect, buff) + ) + return label + + def get_d1_label(self): + self.d1_label = self.get_brace_d_label( + 1, self.get_d1, self.d1_brace, UP, SMALL_BUFF, + ) + return self.d1_label + + def get_d2_label(self): + self.d2_label = self.get_brace_d_label( + 2, self.get_d2, self.d2_brace, UP, 0 + ) + return self.d2_label + + def get_d1_eq_d2_line(self): + start = self.ds_to_point(0, 0) + end = self.ds_to_point(15, 15) + line = self.d1_eq_d2_line = self.mirror_line_class(start, end) + line.set_style(**self.mirror_line_style) + line.set_color(self.d1_eq_e2_line_color) + return self.d1_eq_d2_line + + def get_d1_eq_d2_label(self): + label = TexMobject("d1 = d2") + label.scale(0.75) + line = self.d1_eq_d2_line + point = interpolate( + line.get_start(), line.get_end(), + 0.7, + ) + label.next_to(point, DR, SMALL_BUFF) + label.match_color(line) + label.set_stroke(BLACK, 5, background=True) + self.d1_eq_d2_label = label + return label + + def get_d2_eq_w2_line(self): + w2 = self.block2.width + start = self.ds_to_point(0, w2) + end = np.array(start) + end[0] = FRAME_WIDTH / 2 + self.d2_eq_w2_line = self.mirror_line_class(start, end) + self.d2_eq_w2_line.set_style(**self.mirror_line_style) + return self.d2_eq_w2_line + + def get_d2_eq_w2_label(self): + label = TexMobject("d2 = \\text{block width}") + label.scale(0.75) + label.next_to(self.d2_eq_w2_line, UP, SMALL_BUFF) + label.to_edge(RIGHT, buff=MED_SMALL_BUFF) + self.d2_eq_w2_label = label + return label + + def get_time_tracker(self, time=0): + time_tracker = self.time_tracker = ValueTracker(time) + time_tracker.add_updater( + lambda m, dt: m.increment_value(dt) + ) + return time_tracker + + +class IntroducePositionPhaseSpace(PositionPhaseSpaceScene): + CONFIG = { + "rescale_coordinates": False, + "block1_config": { + "velocity": 1.5, + }, + "slide_wait_time": 30, + } + + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + ) + + def construct(self): + self.show_coordinates() + self.show_xy_line() + self.let_process_play_out() + self.show_w2_line() + + def show_coordinates(self): + ps_point = self.ps_point + axes = self.axes + + self.play(Write(axes.added_lines)) + self.play(FadeInFromLarge(self.ps_dot, scale_factor=10)) + self.play( + ShowCreation(self.x_line), + GrowFromPoint( + self.d1_brace, + self.d1_brace.get_left(), + ), + Indicate(axes.labels[0]), + ) + self.play( + FadeInFromDown(self.ps_d1_label), + FadeInFromDown(self.d1_label), + ) + self.play(ps_point.shift, 0.5 * LEFT) + self.play(ps_point.shift, 0.5 * RIGHT) + self.wait() + self.play( + ShowCreation(self.y_line), + GrowFromPoint( + self.d2_brace, + self.d2_brace.get_left(), + ), + Indicate(axes.labels[1]), + ) + self.play( + FadeInFromDown(self.ps_d2_label), + FadeInFromDown(self.d2_label), + ) + self.play(ps_point.shift, 0.5 * UP) + self.play(ps_point.shift, 0.5 * DOWN) + self.wait() + self.play(Rotating( + ps_point, + about_point=ps_point.get_location() + 0.5 * RIGHT, + run_time=3, + rate_func=smooth, + )) + self.wait() + + def show_xy_line(self): + ps_point = self.ps_point + ps_point.save_state() + d1, d2 = self.point_to_ds(ps_point.get_location()) + + xy_line = self.d1_eq_d2_line + xy_label = self.d1_eq_d2_label + + self.play( + ShowCreation(xy_line), + Write(xy_label), + ) + self.play( + ps_point.move_to, self.ds_to_point(d2, d2), + run_time=3 + ) + self.wait() + for d in [3, 7]: + self.play( + ps_point.move_to, self.ds_to_point(d, d), + run_time=2 + ) + self.wait() + self.play(ps_point.restore) + self.wait() + + def let_process_play_out(self): + self.begin_sliding() + sliding_trajectory = self.get_continually_building_trajectory() + self.add(sliding_trajectory, self.ps_dot) + self.wait(self.slide_wait_time) + self.end_sliding() + + def show_w2_line(self): + line = self.d2_eq_w2_line + label = self.d2_eq_w2_label + + self.play(ShowCreation(line)) + self.play(FadeInFromDown(label)) + self.wait() + + +class SpecialShowPassingFlash(ShowPassingFlash): + CONFIG = { + "max_time_width": 0.1, + } + + def get_bounds(self, alpha): + tw = self.time_width + max_tw = self.max_time_width + upper = interpolate(0, 1 + max_tw, alpha) + lower = upper - tw + upper = min(upper, 1) + lower = max(lower, 0) + return (lower, upper) + + +class EqualMassCase(PositionPhaseSpaceScene): + CONFIG = { + "block1_config": { + "mass": 1, + "width": 1, + "velocity": 1.5, + }, + "rescale_coordinates": False, + } + + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.d2_eq_w2_label, + self.ps_dot, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + ) + + def construct(self): + self.show_same_mass() + self.show_first_point() + self.up_to_first_collision() + self.up_to_second_collision() + self.up_to_third_collision() + + self.fade_distance_indicators() + self.show_beam_bouncing() + + def show_same_mass(self): + blocks = self.blocks + self.play(LaggedStart( + Indicate, blocks, + lag_ratio=0.8, + run_time=1, + )) + + def show_first_point(self): + ps_dot = self.ps_dot + ps_point = self.ps_point + d1, d2 = self.get_ds() + + self.play(FocusOn(ps_dot)) + self.play(ShowCreationThenFadeOut( + Circle(color=RED).replace(ps_dot).scale(2), + run_time=1 + )) + self.wait() + self.play( + ps_point.move_to, self.ds_to_point(d1 - 1, d2), + rate_func=wiggle, + run_time=3, + ) + # self.play(ps_point.move_to, self.ds_to_point(d1, d2)) + self.wait() + + def up_to_first_collision(self): + ps_point = self.ps_point + d1, d2 = self.get_ds() + block1 = self.block1 + block2 = self.block2 + xy_line = self.d1_eq_d2_line + xy_line_label = self.d1_eq_d2_label + + block_arrow = Vector(LEFT, color=RED) + block_arrow.block = block1 + block_arrow.add_updater( + lambda m: m.shift( + m.block.get_center() - m.get_start() + ) + ) + ps_arrow = Vector(LEFT, color=RED) + ps_arrow.next_to(ps_point, DL, buff=SMALL_BUFF) + + block_labels = VGroup(block1.label, block2.label) + block_label_copies = block_labels.copy() + + def update_bl_copies(bl_copies): + for bc, b in zip(bl_copies, block_labels): + bc.move_to(b) + block_label_copies.add_updater(update_bl_copies) + + trajectory = self.get_continually_building_trajectory() + + self.add(block_arrow, ps_arrow, block_label_copies) + self.play( + GrowArrow(block_arrow), + GrowArrow(ps_arrow), + ) + self.add(trajectory) + self.play(self.get_ps_point_change_anim(d2, d2)) + block_arrow.block = block2 + ps_arrow.rotate(90 * DEGREES) + ps_arrow.next_to(ps_point, DR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(ps_point), + Flash(block1.get_left()), + self.get_ps_point_change_anim(d2, d2 - 1) + ) + self.play( + ShowPassingFlash( + xy_line.copy().set_stroke(YELLOW, 3) + ), + Indicate(xy_line_label), + ) + + trajectory.suspend_updating() + self.wait() + + self.ps_arrow = ps_arrow + self.block_arrow = block_arrow + + def up_to_second_collision(self): + trajectory = self.trajectory + ps_point = self.ps_point + ps_arrow = self.ps_arrow + block_arrow = self.block_arrow + + d1, d2 = self.get_ds() + w2 = self.block2.get_width() + + trajectory.resume_updating() + self.play(self.get_ps_point_change_anim(d1, w2)) + block_arrow.rotate(PI) + ps_arrow.rotate(PI) + ps_arrow.next_to(ps_point, UR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(self.block2.get_left()), + Flash(ps_point), + self.get_ps_point_change_anim(d1, w2 + 1) + ) + + trajectory.suspend_updating() + self.wait() + + def up_to_third_collision(self): + trajectory = self.trajectory + ps_point = self.ps_point + ps_arrow = self.ps_arrow + block_arrow = self.block_arrow + d1, d2 = self.get_ds() + + trajectory.resume_updating() + self.play(self.get_ps_point_change_anim(d1, d1)) + block_arrow.block = self.block1 + ps_arrow.rotate(-90 * DEGREES) + ps_arrow.next_to(ps_point, DR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(self.block2.get_left()), + Flash(ps_point.get_location()), + self.get_ps_point_change_anim(d1 + 10, d1) + ) + trajectory.suspend_updating() + + def fade_distance_indicators(self): + trajectory = self.trajectory + self.play( + trajectory.set_stroke, {"width": 1}, + *map(FadeOut, [ + self.ps_arrow, + self.block_arrow, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + ]) + ) + trajectory.clear_updaters() + + def show_beam_bouncing(self): + d1, d2 = self.get_ds() + d1 = int(d1) + d2 = int(d2) + w2 = self.block2.get_width() + ps_point = self.ps_point + + points = [] + while d1 > d2: + points.append(self.ds_to_point(d1, d2)) + d1 -= 1 + while d2 >= int(w2): + points.append(self.ds_to_point(d1, d2)) + d2 -= 1 + points += list(reversed(points))[1:] + trajectory = VMobject() + trajectory.set_points_as_corners(points) + flashes = [ + SpecialShowPassingFlash( + trajectory.copy().set_stroke(YELLOW, width=6 - n), + time_width=(0.01 * n), + max_time_width=0.05, + remover=True + ) + for n in np.arange(0, 6, 0.25) + ] + flash_mob = flashes[0].mobject # Lol + + def update_ps_point_from_flas_mob(ps_point): + if len(flash_mob.points) > 0: + ps_point.move_to(flash_mob.points[-1]) + else: + ps_point.move_to(trajectory.points[0]) + + # Mirror words + xy_line = self.d1_eq_d2_line + w2_line = self.d2_eq_w2_line + lines = VGroup(xy_line, w2_line) + for line in lines: + word = TextMobject("Mirror") + word.next_to(ORIGIN, UP, SMALL_BUFF) + word.rotate(line.get_angle(), about_point=ORIGIN) + word.shift(line.get_center()) + line.word = word + + for line in lines: + line.set_stroke(LIGHT_GREY) + line.set_sheen(1, LEFT) + self.play( + Write(line.word), + line.set_sheen, 1, RIGHT, + line.set_stroke, {"width": 2}, + run_time=1, + ) + + # TODO, clacks? + for x in range(3): + self.play( + UpdateFromFunc( + ps_point, + update_ps_point_from_flas_mob, + ), + *flashes, + run_time=3, + rate_func=None, + ) + self.wait() + + +class FailedAngleRelation(PositionPhaseSpaceScene): + CONFIG = { + "block1_config": { + "distance": 10, + "velocity": -1.5, + }, + "block2_config": { + "distance": 5, + }, + "rescale_coordinates": False, + "trajectory_style": { + "stroke_width": 2, + } + } + + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.ps_dot, + self.x_line, + self.y_line, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.d2_eq_w2_label, + ) + + def construct(self): + self.show_first_collision() + self.show_angles() + + def show_first_collision(self): + self.slide_until(lambda: self.get_ds()[1] < 2) + + def show_angles(self): + trajectory = self.trajectory + arcs = self.get_arcs(trajectory) + equation = self.get_word_equation() + equation.next_to( + trajectory.points[0], UR, MED_SMALL_BUFF, + index_of_submobject_to_align=0, + ) + + for arc in arcs: + line = Line(ORIGIN, RIGHT) + line.set_stroke(WHITE, 2) + line.rotate(arc.start_angle) + line.shift(arc.arc_center - line.get_start()) + arc.line = line + + arc1, arc2 = arcs + arc1.arrow = Arrow( + equation[0].get_left(), arc1.get_right(), + buff=SMALL_BUFF, + color=WHITE, + path_arc=0, + ) + arc2.arrow = Arrow( + equation[2].get_corner(DL), + arc2.get_left(), + use_rectangular_stem=False, + path_arc=-120 * DEGREES, + buff=SMALL_BUFF, + ) + arc2.arrow.pointwise_become_partial(arc.arrow, 0, 0.95) + + arc1.word = equation[0] + arc2.word = equation[1:] + + for arc in arcs: + self.play( + FadeInFrom(arc.word, LEFT), + GrowArrow(arc.arrow, path_arc=arc.arrow.path_arc), + ) + self.play( + ShowCreation(arc), + arc.line.rotate, arc.angle, + {"about_point": arc.line.get_start()}, + UpdateFromAlphaFunc( + arc.line, + lambda m, a: m.set_stroke( + opacity=(there_and_back(a)**0.5) + ) + ), + ) + + # + def get_arcs(self, trajectory): + p0, p1, p2 = trajectory.get_anchors()[1:4] + arc_config = { + "stroke_color": WHITE, + "stroke_width": 2, + "radius": 0.5, + "arc_center": p1, + } + arc1 = Arc( + start_angle=0, + angle=45 * DEGREES, + **arc_config + ) + a2_start = angle_of_vector(DL) + a2_angle = angle_between_vectors((p2 - p1), DL) + arc2 = Arc( + start_angle=a2_start, + angle=a2_angle, + **arc_config + ) + return VGroup(arc1, arc2) + + def get_word_equation(self): + result = VGroup( + TextMobject("Angle of incidence"), + TexMobject("\\ne").rotate(90 * DEGREES), + TextMobject("Angle of refraction") + ) + result.arrange_submobjects(DOWN) + result.set_stroke(BLACK, 5, background=True) + return result + + +class UnscaledPositionPhaseSpaceMass10(FailedAngleRelation): + CONFIG = { + "block1_config": { + "mass": 10 + }, + "wait_time": 25, + } + + def construct(self): + self.slide(self.wait_time) + + +class UnscaledPositionPhaseSpaceMass100(UnscaledPositionPhaseSpaceMass10): + CONFIG = { + "block1_config": { + "mass": 100 + } + } + + +class RescaleCoordinates(PositionPhaseSpaceScene, MovingCameraScene): + CONFIG = { + "rescale_coordinates": False, + "ps_d2_label_vect": LEFT, + "axes_center": 6 * LEFT + 0.65 * DOWN, + "block1_config": {"distance": 7}, + "wait_time": 30, + } + + def setup(self): + PositionPhaseSpaceScene.setup(self) + MovingCameraScene.setup(self) + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.ps_dot, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + self.d1_brace, + self.d2_brace, + self.d1_label, + self.d2_label, + ) + + def construct(self): + self.show_rescaling() + self.comment_on_ugliness() + self.put_into_frame() + + def show_rescaling(self): + axes = self.axes + blocks = self.blocks + to_stretch = VGroup( + axes.added_lines, + self.d1_eq_d2_line, + self.ps_point, + ) + m1 = self.block1.mass + new_axes_labels = self.get_axes_labels(axes, with_sqrts=True) + + # Show label + def show_label(index, block, vect): + self.play( + ShowCreationThenFadeAround(axes.labels[index]) + ) + self.play( + Transform( + axes.labels[index], + new_axes_labels[index][:2], + ), + GrowFromCenter(new_axes_labels[index][2]) + ) + group = VGroup( + new_axes_labels[index][2][-2:].copy(), + TexMobject("="), + block.label.copy(), + ) + group.generate_target() + group.target.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + group.target.next_to(block, vect) + group[1].scale(0) + group[1].move_to(group.target[1]) + group.target[2].set_fill(WHITE) + group.target[2].set_stroke(width=0, background=True) + self.play(MoveToTarget( + group, + rate_func=there_and_back_with_pause, + run_time=3 + )) + self.remove(group) + self.wait() + + show_label(0, self.block1, RIGHT) + + # The stretch + blocks.suspend_updating() + self.play( + ApplyMethod( + to_stretch.stretch, np.sqrt(m1), 0, + {"about_point": axes.coords_to_point(0, 0)}, + ), + self.d1_eq_d2_label.shift, 6 * RIGHT, + run_time=2, + ) + self.rescale_coordinates = True + blocks.resume_updating() + self.wait() + + # Show wiggle + d1, d2 = self.get_ds() + for new_d1 in [d1 - 2, d1]: + self.play(self.get_ps_point_change_anim( + new_d1, d2, + rate_func=smooth, + run_time=2, + )) + self.wait() + + # Change y-coord + show_label(1, self.block2, LEFT) + + axes.remove(axes.labels) + self.remove(axes.labels) + axes.labels = new_axes_labels + axes.add(axes.labels) + self.add(axes) + + def comment_on_ugliness(self): + axes = self.axes + + randy = Randolph(height=1.7) + randy.flip() + randy.next_to(self.d2_eq_w2_line, UP, buff=0) + randy.to_edge(RIGHT) + randy.change("sassy") + randy.save_state() + randy.fade(1) + randy.change("plain") + + self.play(Restore(randy)) + self.play( + PiCreatureSays( + randy, "Hideous!", + bubble_kwargs={"height": 1.5, "width": 2}, + target_mode="angry", + look_at_arg=axes.labels[0] + ) + ) + self.play(randy.look_at, axes.labels[1]) + self.play(Blink(randy)) + self.play( + RemovePiCreatureBubble( + randy, target_mode="confused" + ) + ) + self.play(Blink(randy)) + self.play(randy.look_at, axes.labels[0]) + self.wait() + self.play(FadeOut(randy)) + + def put_into_frame(self): + rect = ScreenRectangle(height=FRAME_HEIGHT + 10) + inner_rect = ScreenRectangle(height=FRAME_HEIGHT) + rect.add_subpath(inner_rect.points[::-1]) + rect.set_fill(DARK_GREY, opacity=1) + frame = self.camera_frame + + self.begin_sliding() + self.add(rect) + self.play( + frame.scale, 1.5, + {"about_point": frame.get_bottom() + UP}, + run_time=2, + ) + self.wait(self.wait_time) + self.end_sliding() + + # + def get_ds(self): + if self.rescale_coordinates: + return super().get_ds() + return ( + self.block1_config["distance"], + self.block2_config["distance"], + ) + + +class RescaleCoordinatesMass16(RescaleCoordinates): + CONFIG = { + "block1_config": { + "mass": 16, + "distance": 10, + }, + "rescale_coordinates": True, + "wait_time": 20, + } + + def construct(self): + self.put_into_frame() + + +class RescaleCoordinatesMass64(RescaleCoordinatesMass16): + CONFIG = { + "block1_config": { + "mass": 64, + "distance": 6, + }, + "block2_config": {"distance": 3}, + } + + def construct(self): + self.put_into_frame() + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/clacks/solution2/simple_scenes.py b/active_projects/clacks/solution2/simple_scenes.py new file mode 100644 index 00000000..42bfdb7b --- /dev/null +++ b/active_projects/clacks/solution2/simple_scenes.py @@ -0,0 +1,15 @@ +from big_ol_pile_of_manim_imports import * + + +class LastVideoWrapper(Scene): + def construct(self): + title = TextMobject("Last time...") + title.scale(1.5) + title.to_edge(UP) + rect = ScreenRectangle(height=6) + rect.next_to(title, DOWN) + self.play( + FadeInFromDown(title), + ShowCreation(rect) + ) + self.wait() diff --git a/active_projects/clacks/solution2/wordy_scenes.py b/active_projects/clacks/solution2/wordy_scenes.py new file mode 100644 index 00000000..c2f3a61a --- /dev/null +++ b/active_projects/clacks/solution2/wordy_scenes.py @@ -0,0 +1,205 @@ +from big_ol_pile_of_manim_imports import * + + +class ConnectionToOptics(Scene): + def construct(self): + e_group, m_group = k_groups = self.get_kinematics_groups() + c_group, a_group = o_groups = self.get_optics_groups() + arrows = VGroup() + for g1, g2 in zip(k_groups, o_groups): + g2.align_to(g1, UP) + g2.to_edge(RIGHT) + arrow = TexMobject("\\Rightarrow") + arrow.scale(1.5) + arrow.move_to(interpolate( + g1[0].get_right(), g2[0].get_left(), 0.5 + )) + arrows.add(arrow) + everything = VGroup(k_groups, arrows, o_groups) + everything.to_edge(UP) + + everything.generate_target() + everything.target.scale(0.9) + everything.target.to_edge(DOWN) + width = max([m.get_width() for m in everything.target]) + width += 2 * MED_SMALL_BUFF + rects = VGroup() + for k in [0, 2]: + rect = DashedMobject(Rectangle( + height=FRAME_HEIGHT - 1.5, + width=width + ), dashes_num=100) + rect.move_to(everything.target[k]) + rect.to_edge(DOWN, buff=SMALL_BUFF) + rects.add(rect) + titles = VGroup( + TextMobject("Kinematics"), + TextMobject("Optics"), + ) + titles.scale(1.5) + for title, rect in zip(titles, rects): + title.next_to(rect, UP) + titles[0].align_to(titles[1], UP) + + self.play(FadeInFromDown(e_group)) + self.play( + Write(arrows[0]), + FadeInFrom(c_group, LEFT) + ) + self.wait() + self.play(FadeInFromDown(m_group)) + self.play( + Write(arrows[1]), + FadeInFrom(a_group, LEFT) + ) + self.wait(4) + for k in range(2): + anims = [ + ShowCreation(rects[k]), + FadeInFromDown(titles[k]), + ] + if k == 0: + anims.append(MoveToTarget(everything)) + self.play(*anims) + self.wait() + self.wait() + self.wait(4) + + def get_kinematics_groups(self): + tex_to_color_map = { + "m_1": BLUE, + "m_2": BLUE, + "v_1": RED, + "v_2": RED, + } + energy_eq = TexMobject( + "\\frac{1}{2} m_1 (v_1)^2 + " + "\\frac{1}{2} m_2 (v_2)^2 = " + "\\text{const.}", + tex_to_color_map=tex_to_color_map + ) + energy_eq.scale(0.8) + momentum_eq = TexMobject( + "m_1 v_1 + m_2 v_2 = \\text{const.}", + tex_to_color_map=tex_to_color_map + ) + energy_label = TextMobject( + "Conservation of energy" + ) + momentum_label = TextMobject( + "Conservation of momentum" + ) + energy_group = VGroup(energy_label, energy_eq) + momentum_group = VGroup(momentum_label, momentum_eq) + groups = VGroup(energy_group, momentum_group) + for group in groups: + group.arrange_submobjects(DOWN, buff=MED_LARGE_BUFF) + group[0].set_color(GREEN) + groups.arrange_submobjects(DOWN, buff=2) + groups.to_edge(LEFT) + return groups + + def get_optics_groups(self): + self.time_tracker = ValueTracker(0) + self.time_tracker.add_updater( + lambda m, dt: m.increment_value(dt) + ) + self.add(self.time_tracker) + return VGroup( + self.get_speed_group(), + self.get_angle_group() + ) + + def get_speed_group(self): + speed_label = TextMobject("Constant speed of light") + speed_label.set_color(YELLOW) + speed_light_template = Line(LEFT, RIGHT) + speed_light_template.fade(1) + speed_light_template.match_width(speed_label) + speed_light_template.next_to(speed_label, DOWN, MED_LARGE_BUFF) + speed_light = speed_light_template.deepcopy() + + def update_speed_light(light, period=2, time_width=0.05): + time = self.time_tracker.get_value() + alpha = (time / period) % 1 + # alpha = 1 - 2 * abs(alpha - 0.5) + alpha *= 1.5 + a = alpha - time_width / 2 + b = alpha + time_width / 2 + light.pointwise_become_partial( + speed_light_template, max(a, 0), min(b, 1) + ) + opacity = speed_label.family_members_with_points()[0].get_fill_opacity() + light.set_stroke(YELLOW, width=3, opacity=opacity) + # light.stretch(0.5, 0) + # point = speed_light_template.point_from_proportion(0.25) + # light.stretch(2, 0, about_point=point) + + speed_light.add_updater(update_speed_light) + result = VGroup( + speed_label, speed_light_template, speed_light + ) + return result + + def get_angle_group(self): + title = VGroup(*map(TextMobject, [ + "Angle of\\\\Incidence", + "=", + "Angle of\\\\Refraction", + ])).arrange_submobjects(RIGHT) + title.set_color(YELLOW) + h_line = Line(LEFT, RIGHT) + h_line.match_width(title) + h_line.set_stroke(LIGHT_GREY) + h_line.set_sheen(1, UL) + points = [ + h_line.get_left() + UP, + h_line.get_center(), + h_line.get_right() + UP, + ] + dashed_lines = VGroup( + DashedLine(*points[0:2]), DashedLine(*points[1:3]) + ) + dashed_lines.set_stroke(WHITE, 2) + v_shape = VMobject() + v_shape.set_points_as_corners(points) + v_shape.fade(1) + + theta = dashed_lines[1].get_angle() + arcs = VGroup( + Arc(start_angle=0, angle=theta), + Arc(start_angle=PI, angle=-theta), + ) + arcs.set_stroke(WHITE, 2) + thetas = VGroup() + for v in LEFT, RIGHT: + theta = TexMobject("\\theta") + theta.next_to(arcs, v, aligned_edge=DOWN) + theta.shift(SMALL_BUFF * UP) + thetas.add(theta) + + beam = VMobject() + + def update_beam(beam, period=2, time_width=0.05): + time = self.time_tracker.get_value() + alpha = (time / period) % 1 + alpha *= 1.5 + a = alpha - time_width / 2 + b = alpha + time_width / 2 + beam.pointwise_become_partial( + v_shape, max(a, 0), min(b, 1) + ) + opacity = title.family_members_with_points()[0].get_fill_opacity() + beam.set_stroke(YELLOW, width=3, opacity=opacity) + + beam.add_updater(update_beam) + title.next_to(v_shape, UP, MED_LARGE_BUFF) + + return VGroup( + title, h_line, arcs, thetas, + dashed_lines, v_shape, beam + ) + + +class ConnectionToOpticsTransparent(ConnectionToOptics): + pass diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index b5818d21..8763ff89 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -220,10 +220,14 @@ class LaggedStart(Animation): } def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs): + for key in ["rate_func", "run_time"]: + if key in AnimationClass.CONFIG: + setattr(self, key, AnimationClass.CONFIG[key]) digest_config(self, kwargs) for key in "rate_func", "run_time", "lag_ratio": if key in kwargs: kwargs.pop(key) + if arg_creator is None: def arg_creator(mobject): return (mobject,) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index f4f52bb6..441dd874 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -24,7 +24,6 @@ from manimlib.utils.rate_functions import smooth from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.rate_functions import there_and_back from manimlib.utils.rate_functions import wiggle -from manimlib.utils.rate_functions import double_smooth class FocusOn(Transform): @@ -125,10 +124,11 @@ class ShowPassingFlash(ShowPartial): } def get_bounds(self, alpha): - alpha *= (1 + self.time_width) - alpha -= self.time_width / 2.0 - lower = max(0, alpha - self.time_width / 2.0) - upper = min(1, alpha + self.time_width / 2.0) + tw = self.time_width + upper = interpolate(0, 1 + tw, alpha) + lower = upper - tw + upper = min(upper, 1) + lower = max(lower, 0) return (lower, upper) def clean_up(self, *args, **kwargs): diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index e439f88d..86507111 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -298,6 +298,8 @@ class TeacherStudentsScene(PiCreatureScene): "raise_left_hand", ]) kwargs["target_mode"] = target_mode + if "bubble_kwargs" not in kwargs: + kwargs["bubble_kwargs"] = {"direction": LEFT} student = self.get_students()[kwargs.get("student_index", 2)] return self.pi_creature_says( student, *content, **kwargs diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 5b98d718..8224c2b2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -17,7 +17,7 @@ from manimlib.utils.color import interpolate_color from manimlib.utils.iterables import list_update from manimlib.utils.iterables import remove_list_redundancies from manimlib.utils.paths import straight_path -from manimlib.utils.simple_functions import get_num_args +from manimlib.utils.simple_functions import get_parameters from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import rotation_matrix @@ -45,6 +45,7 @@ class Mobject(Container): if self.name is None: self.name = self.__class__.__name__ self.updaters = [] + self.updating_suspended = False self.reset_points() self.generate_points() self.init_colors() @@ -145,24 +146,23 @@ class Mobject(Container): # Updating - def update(self, dt=0): - for updater in self.updaters: - num_args = get_num_args(updater) - if num_args == 1: - updater(self) - elif num_args == 2: - updater(self, dt) - else: - raise Exception( - "Mobject updater expected 1 or 2 " - "arguments, %d given" % num_args - ) + def update(self, dt=0, recursive=True): + if not self.updating_suspended: + for updater in self.updaters: + parameters = get_parameters(updater) + if "dt" in parameters: + updater(self, dt) + else: + updater(self) + if recursive: + for submob in self.submobjects: + submob.update(dt, recursive) + return self def get_time_based_updaters(self): return [ - updater - for updater in self.updaters - if get_num_args(updater) == 2 + updater for updater in self.updaters + if "dt" in get_parameters(updater) ] def get_updaters(self): @@ -186,6 +186,21 @@ class Mobject(Container): self.updaters = [] return self + def suspend_updating(self, recursive=True): + self.updating_suspended = True + if recursive: + for submob in self.submobjects: + submob.suspend_updating(recursive) + return self + + def resume_updating(self, recursive=True): + self.updating_suspended = False + if recursive: + for submob in self.submobjects: + submob.resume_updating(recursive) + self.update(dt=0, recursive=recursive) + return self + # Transforming operations def apply_to_family(self, func): diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 5f48ce2e..8ec82d9e 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -15,7 +15,7 @@ class DecimalNumber(VMobject): "edge_to_fix": LEFT, } - def __init__(self, number, **kwargs): + def __init__(self, number=0, **kwargs): VMobject.__init__(self, **kwargs) self.number = number self.initial_config = kwargs diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 66f4dc5e..1ec7e674 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -69,10 +69,6 @@ class Scene(Container): def tear_down(self): pass - def setup_bases(self): - for base in self.__class__.__bases__: - base.setup(self) - def construct(self): pass # To be implemented in subclasses @@ -151,7 +147,7 @@ class Scene(Container): ### def continual_update(self, dt): - for mobject in self.get_mobject_family_members(): + for mobject in self.mobjects: mobject.update(dt) for continual_animation in self.continual_animations: continual_animation.update(dt) @@ -470,6 +466,9 @@ class Scene(Container): # scene gets added to the scene if animation.mobject not in self.get_mobject_family_members(): self.add(animation.mobject) + # Don't call the update functions of a mobject + # being animated + animation.mobject.suspend_updating() moving_mobjects = self.get_moving_mobjects(*animations) # Paint all non-moving objects onto the screen, so they don't @@ -500,6 +499,7 @@ class Scene(Container): def clean_up_animations(self, *animations): for animation in animations: animation.clean_up(self) + animation.mobject.resume_updating() return self def get_mobjects_from_last_animation(self): @@ -568,9 +568,9 @@ class Scene(Container): for frame in frames: self.file_writer.write_frame(frame) - def add_sound(self, sound_file, time_offset=0): + def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): time = self.get_time() + time_offset - self.file_writer.add_sound(sound_file, time) + self.file_writer.add_sound(sound_file, time, gain, **kwargs) def show_frame(self): self.update_frame(ignore_skipping=True) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 7337748e..cd098afc 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -119,7 +119,9 @@ class SceneFileWriter(object): def create_audio_segment(self): self.audio_segment = AudioSegment.silent() - def add_audio_segment(self, new_segment, time=None): + def add_audio_segment(self, new_segment, + time=None, + gain_to_background=None): if not self.includes_sound: self.includes_sound = True self.create_audio_segment() @@ -138,13 +140,17 @@ class SceneFileWriter(object): crossfade=0, ) self.audio_segment = segment.overlay( - new_segment, position=int(1000 * time) + new_segment, + position=int(1000 * time), + gain_during_overlay=gain_to_background, ) - def add_sound(self, sound_file, time): + def add_sound(self, sound_file, time=None, gain=None, **kwargs): file_path = get_full_sound_file_path(sound_file) new_segment = AudioSegment.from_file(file_path) - self.add_audio_segment(new_segment, time) + if gain: + new_segment = new_segment.apply_gain(gain) + self.add_audio_segment(new_segment, time, **kwargs) # Writers def begin_animation(self, allow_write=False): @@ -305,17 +311,25 @@ class SceneFileWriter(object): ) # Makes sure sound file length will match video file self.add_audio_segment(AudioSegment.silent(0)) - self.audio_segment.export(sound_file_path) + self.audio_segment.export( + sound_file_path, + bitrate='312k', + ) temp_file_path = movie_file_path.replace(".", "_temp.") - commands = commands = [ + commands = [ "ffmpeg", "-i", movie_file_path, "-i", sound_file_path, '-y', # overwrite output file if it exists - "-c:v", "copy", "-c:a", "aac", + "-c:v", "copy", + "-c:a", "aac", + "-b:a", "320k", + # select video stream from first file + "-map", "0:v:0", + # select audio stream from second file + "-map", "1:a:0", '-loglevel', 'error', - "-shortest", - "-strict", "experimental", + # "-shortest", temp_file_path, ] subprocess.call(commands) diff --git a/manimlib/utils/rate_functions.py b/manimlib/utils/rate_functions.py index fbbe7f5e..39f9cd15 100644 --- a/manimlib/utils/rate_functions.py +++ b/manimlib/utils/rate_functions.py @@ -16,12 +16,12 @@ def smooth(t, inflection=10.0): ) -def rush_into(t): - return 2 * smooth(t / 2.0) +def rush_into(t, inflection=10.0): + return 2 * smooth(t / 2.0, inflection) -def rush_from(t): - return 2 * smooth(t / 2.0 + 0.5) - 1 +def rush_from(t, inflection=10.0): + return 2 * smooth(t / 2.0 + 0.5, inflection) - 1 def slow_into(t): diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index 2cd048f9..f567cf9a 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -30,7 +30,11 @@ def choose(n, r): def get_num_args(function): - return len(inspect.signature(function).parameters) + return len(get_parameters(function)) + + +def get_parameters(function): + return inspect.signature(function).parameters # Just to have a less heavyweight name for this extremely common operation # diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 50efb097..959fd198 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -142,9 +142,8 @@ def angle_between_vectors(v1, v2): Returns the angle between two 3D vectors. This angle will always be btw 0 and TAU/2. """ - l1 = get_norm(v1) - l2 = get_norm(v2) - return np.arccos(np.dot(v1, v2) / (l1 * l2)) + diff = (angle_of_vector(v1) - angle_of_vector(v2)) % TAU + return min(diff, TAU - diff) def project_along_vector(point, vector):