From dc24c033492ed490ceb9e0ae6c625bfd182ea16a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:23:13 -0800 Subject: [PATCH] Updates to light-bouncing clack solution --- .../clacks/{solution2.py => all_s2_scenes.py} | 6 + active_projects/clacks/question.py | 23 - .../clacks/solution2/pi_creature_scenes.py | 45 +- .../clacks/solution2/position_phase_space.py | 464 ++++++++++++++++++ .../clacks/solution2/simple_scenes.py | 39 -- .../clacks/solution2/wordy_scenes.py | 201 ++++++++ 6 files changed, 714 insertions(+), 64 deletions(-) rename active_projects/clacks/{solution2.py => all_s2_scenes.py} (64%) create mode 100644 active_projects/clacks/solution2/position_phase_space.py create mode 100644 active_projects/clacks/solution2/wordy_scenes.py diff --git a/active_projects/clacks/solution2.py b/active_projects/clacks/all_s2_scenes.py similarity index 64% rename from active_projects/clacks/solution2.py rename to active_projects/clacks/all_s2_scenes.py index 0e586de6..9ddff962 100644 --- a/active_projects/clacks/solution2.py +++ b/active_projects/clacks/all_s2_scenes.py @@ -2,6 +2,9 @@ # 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 = [ @@ -12,4 +15,7 @@ ALL_SCENE_CLASSES = [ block_collision_scenes.IntroducePreviousTwoVideos, block_collision_scenes.PreviousTwoVideos, simple_scenes.TwoSolutionsWrapper, + wordy_scenes.ConnectionToOptics, + pi_creature_scenes.OnAnsweringTwice, + position_phase_space.IntroducePositionPhaseSpace, ] diff --git a/active_projects/clacks/question.py b/active_projects/clacks/question.py index acefc690..edf5241b 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): @@ -370,27 +368,6 @@ class BlocksAndWallScene(Scene): if self.include_sound: self.add_clack_sounds(self.clack_data) - # TODO, this no longer works - # should use Scene.add_sound instead - def combine_movie_files(self): - Scene.combine_movie_files(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]) - # Animated scenes diff --git a/active_projects/clacks/solution2/pi_creature_scenes.py b/active_projects/clacks/solution2/pi_creature_scenes.py index b6c3492e..daf0c493 100644 --- a/active_projects/clacks/solution2/pi_creature_scenes.py +++ b/active_projects/clacks/solution2/pi_creature_scenes.py @@ -1,6 +1,47 @@ from big_ol_pile_of_manim_imports import * -class NewSceneName(TeacherStudentsScene): +class OnAnsweringTwice(TeacherStudentsScene): def construct(self): - pass + 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) 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..6931d4e0 --- /dev/null +++ b/active_projects/clacks/solution2/position_phase_space.py @@ -0,0 +1,464 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks.question import Block +from active_projects.clacks.question import Wall + + +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": 7, + "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, + } + } + + def setup(self): + 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(), + self.get_time_tracker(), + ] + + def get_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()) + + 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_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 + # v2 = self.block2.velocity + d1 = self.block1_config["distance"] + d2 = self.block2_config["distance"] + w2 = self.block2.width + 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(d1, d2 + w2) + 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_flashes(self): + pass # TODO + + # 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_corner(), LEFT) + return floor + + def get_wall(self): + wall = self.wall = Wall(**self.wall_config) + wall.move_to(self.get_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_corner(), DL) + block.shift(config["distance"] * RIGHT) + block.label.move_to(block) + block.label.set_fill(BLACK) + block.label.set_stroke(WHITE, 3, 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.add(self.get_axes_labels(axes)) + return axes + + def get_axes_labels(self, axes): + x_label = TexMobject("x = ", "d_1") + y_label = TexMobject("y = ", "d_2") + labels = VGroup(x_label, y_label) + if self.rescale_coordinates: + 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) + + 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, RIGHT, + ) + return self.ps_d2_label + + def get_d_brace(self, get_right_point): + line = Line(LEFT, RIGHT).set_width(6) + brace = Brace(line, UP) + + def update_brace(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) + brace.match_width(line, stretch=True) + brace.next_to(line, UP, buff=0) + + brace.add_updater(update_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): + label = self.get_d_label(n, get_d) + label.add_updater( + lambda m: m.next_to(brace, vect, 0) + ) + return label + + def get_d1_label(self): + self.d1_label = self.get_brace_d_label( + 1, self.get_d1, self.d1_brace, UP + ) + 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 + ) + 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) + self.d1_eq_d2_line = DashedLine(start, end) + self.d1_eq_d2_line.set_stroke(WHITE, 2) + 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 + for i in range(len(line)): + if line[i].get_top()[1] > FRAME_HEIGHT / 2: + break + label.next_to(line[i - 3], DR, SMALL_BUFF) + 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 = self.ds_to_point(30, w2) + self.d2_eq_w2_line = DashedLine(start, end) + self.d2_eq_w2_line.set_stroke(WHITE, 2) + 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_w_label = label + return label + + def get_time_tracker(self): + time_tracker = self.time_tracker = ValueTracker(0) + time_tracker.add_updater( + lambda m, dt: m.increment_value(dt) + ) + self.get_time = time_tracker.get_value + self.add(time_tracker) + return time_tracker + + +class IntroducePositionPhaseSpace(PositionPhaseSpaceScene): + CONFIG = { + "rescale_coordinates": False, + } + + def construct(self): + self.show_coordinates() + self.show_xy_line() + self.let_process_play_out() + + self.add(*self.all_items) + self.ps_point.add_updater( + lambda m: m.move_to(self.ds_to_point( + *self.time_to_ds(self.get_time()) + )) + ) + self.wait(10) + # self.play(Rotating( + # self.ps_point, + # about_point=self.ps_point.get_location() + RIGHT, + # run_time=3 + # )) + + def show_coordinates(self): + pass + + def show_xy_line(self): + pass + + def let_process_play_out(self): + pass + + +class EqualMassCase(IntroducePositionPhaseSpace): + def construct(self): + self.show_first_point() + self.up_to_first_collision() + self.ask_about_momentum_transfer() + self.up_to_second_collision() + self.up_to_third_collision() + + def show_first_point(self): + pass + + def up_to_first_collision(self): + pass + + def ask_about_momentum_transfer(self): + pass + + def up_to_second_collision(self): + pass + + def up_to_third_collision(self): + pass diff --git a/active_projects/clacks/solution2/simple_scenes.py b/active_projects/clacks/solution2/simple_scenes.py index a4b6ef3b..bc3d5f02 100644 --- a/active_projects/clacks/solution2/simple_scenes.py +++ b/active_projects/clacks/solution2/simple_scenes.py @@ -7,42 +7,3 @@ class TwoSolutionsWrapper(Scene): def construct(self): pass - - -class ConnectionToOptics(Scene): - def construct(self): - e_group, m_group = k_groups = self.get_kinematics_groups() - - self.add(k_groups) - - 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 - ) - 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_eq, energy_label) - momentum_group = VGroup(momentum_eq, momentum_label) - groups = VGroup(energy_group, momentum_group) - for group in groups: - group.arrange_submobjects(DOWN) - groups.arrange_submobjects(DOWN, LARGE_BUFF) - groups.to_edge(LEFT) - return groups diff --git a/active_projects/clacks/solution2/wordy_scenes.py b/active_projects/clacks/solution2/wordy_scenes.py new file mode 100644 index 00000000..c5c39002 --- /dev/null +++ b/active_projects/clacks/solution2/wordy_scenes.py @@ -0,0 +1,201 @@ +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 + )