diff --git a/Dockerfile b/Dockerfile index ef72df0f..45ecc1f7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,37 +1,24 @@ FROM ubuntu:18.04 ENV DEBIAN_FRONTEND noninteractive - RUN apt-get update -qqy RUN apt-get install -qqy --no-install-recommends apt-utils - WORKDIR /root -RUN apt-get install -qqy build-essential libsqlite3-dev sqlite3 bzip2 \ - libbz2-dev zlib1g-dev libssl-dev openssl libgdbm-dev \ - libgdbm-compat-dev liblzma-dev libreadline-dev \ - libncursesw5-dev libffi-dev uuid-dev -RUN apt-get install -qqy wget +RUN apt-get install --no-install-recommends -qqy build-essential libsqlite3-dev sqlite3 bzip2 \ +libbz2-dev zlib1g-dev libssl-dev openssl libgdbm-dev \ +libgdbm-compat-dev liblzma-dev libreadline-dev \ +libncursesw5-dev libffi-dev uuid-dev wget ffmpeg apt-transport-https texlive-latex-base \ +texlive-full texlive-fonts-extra sox git libcairo2-dev libjpeg-dev libgif-dev && rm -rf /var/lib/apt/lists/* RUN wget -q https://www.python.org/ftp/python/3.7.0/Python-3.7.0.tgz RUN tar -xf Python-3.7.0.tgz WORKDIR Python-3.7.0 RUN ./configure > /dev/null && make -s && make -s install RUN python3 -m pip install --upgrade pip -RUN apt-get install -qqy libcairo2-dev libjpeg-dev libgif-dev COPY requirements.txt requirements.txt RUN python3 -m pip install -r requirements.txt RUN rm requirements.txt WORKDIR /root RUN rm -rf Python-3.7.0* - -RUN apt-get install -qqy ffmpeg - ENV TZ=America/Los_Angeles RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -RUN apt-get install -qqy apt-transport-https -RUN apt-get install -qqy texlive-latex-base -RUN apt-get install -qqy texlive-full -RUN apt-get install -qqy texlive-fonts-extra -RUN apt-get install -qqy sox -RUN apt-get install -qqy git - ENV DEBIAN_FRONTEND teletype ENTRYPOINT ["/bin/bash"] diff --git a/active_projects/clacks.py b/active_projects/clacks.py index 387cb647..ec80ed76 100644 --- a/active_projects/clacks.py +++ b/active_projects/clacks.py @@ -3,34 +3,75 @@ import subprocess from pydub import AudioSegment -MIN_TIME_BETWEEN_FLASHES = 0.004 +class Block(Square): + CONFIG = { + "mass": 1, + "velocity": 0, + "width": None, + "label_text": None, + "label_scale_value": 0.8, + "fill_opacity": 1, + "stroke_width": 3, + "stroke_color": WHITE, + "fill_color": None, + "sheen_direction": UL, + "sheen": 0.5, + "sheen_direction": UL, + } + + def __init__(self, **kwargs): + digest_config(self, kwargs) + if self.width is None: + self.width = self.mass_to_width(self.mass) + if self.fill_color is None: + self.fill_color = self.mass_to_color(self.mass) + if self.label_text is None: + self.label_text = self.mass_to_label_text(self.mass) + Square.__init__(self, side_length=self.width, **kwargs) + self.label = self.get_label() + self.add(self.label) + + def get_label(self): + label = TextMobject(self.label_text) + label.scale(self.label_scale_value) + label.next_to(self, UP, SMALL_BUFF) + return label + + def get_points_defining_boundary(self): + return self.points + + def mass_to_color(self, mass): + colors = [ + LIGHT_GREY, + BLUE_B, + BLUE_D, + BLUE_E, + BLUE_E, + DARK_GREY, + DARK_GREY, + BLACK, + ] + index = min(int(np.log10(mass)), len(colors) - 1) + return colors[index] + + def mass_to_width(self, mass): + return 1 + 0.25 * np.log10(mass) + + def mass_to_label_text(self, mass): + return "{:,}\\,kg".format(int(mass)) class SlidingBlocks(VGroup): CONFIG = { "block1_config": { - "mass": 1, - "velocity": -2, "distance": 7, - "width": None, - "color": None, - "label_text": None, + "mass": 1e6, + "velocity": -2, }, "block2_config": { + "distance": 3, "mass": 1, "velocity": 0, - "distance": 3, - "width": None, - "color": None, - "label_text": None, - }, - "block_style": { - "fill_opacity": 1, - "stroke_width": 3, - "stroke_color": WHITE, - "sheen_direction": UL, - "sheen_factor": 0.5, - "sheen_direction": UL, }, "collect_clack_data": True, } @@ -54,30 +95,13 @@ class SlidingBlocks(VGroup): if self.collect_clack_data: self.clack_data = self.get_clack_data() - def get_block(self, mass, distance, velocity, width, color, label_text): - if width is None: - width = self.mass_to_width(mass) - if color is None: - color = self.mass_to_color(mass) - if label_text is None: - label_text = "{:,}\\,kg".format(int(mass)) - block = Square(side_length=width) - block.mass = mass - block.velocity = velocity - - style = dict(self.block_style) - style["fill_color"] = color - - block.set_style(**style) + def get_block(self, distance, **kwargs): + block = Block(**kwargs) block.move_to( self.floor.get_top()[1] * UP + (self.wall.get_right()[0] + distance) * RIGHT, DL, ) - label = block.label = TextMobject(label_text) - label.scale(0.8) - label.next_to(block, UP, SMALL_BUFF) - block.add(label) return block def get_phase_space_point_tracker(self): @@ -104,36 +128,6 @@ class SlidingBlocks(VGroup): ) self.update_blocks_from_phase_space_point_tracker() - def old_update_positions(self, dt): - # Based on velocity diagram bouncing...didn't work for - # large masses, due to frame rate mismatch - blocks = self.submobjects - for block in blocks: - block.shift(block.velocity * dt * RIGHT) - if blocks[0].get_left()[0] < blocks[1].get_right()[0]: - # Two blocks collide - m1 = blocks[0].mass - m2 = blocks[1].mass - v1 = blocks[0].velocity - v2 = blocks[1].velocity - v_phase_space_point = np.array([ - np.sqrt(m1) * v1, -np.sqrt(m2) * v2 - ]) - angle = 2 * np.arctan(np.sqrt(m2 / m1)) - new_vps_point = rotate_vector(v_phase_space_point, angle) - for block, value in zip(blocks, new_vps_point): - block.velocity = value / np.sqrt(block.mass) - blocks[1].move_to(blocks[0].get_corner(DL), DR) - self.surrounding_scene.clack(blocks[0].get_left()) - if blocks[1].get_left()[0] < self.wall.get_right()[0]: - # Second block hits wall - blocks[1].velocity *= -1 - blocks[1].move_to(self.wall.get_corner(DR), DL) - if blocks[0].get_left()[0] < blocks[1].get_right()[0]: - blocks[0].move_to(blocks[1].get_corner(DR), DL) - self.surrounding_scene.clack(blocks[1].get_left()) - return self - def update_blocks_from_phase_space_point_tracker(self): block1, block2 = self.block1, self.block2 @@ -199,23 +193,6 @@ class SlidingBlocks(VGroup): clack_data.append((location, time)) return clack_data - def mass_to_color(self, mass): - colors = [ - LIGHT_GREY, - BLUE_B, - BLUE_D, - BLUE_E, - BLUE_E, - DARK_GREY, - DARK_GREY, - BLACK, - ] - index = min(int(np.log10(mass)), len(colors) - 1) - return colors[index] - - def mass_to_width(self, mass): - return 1 + 0.25 * np.log10(mass) - class ClackFlashes(ContinualAnimation): CONFIG = { @@ -225,6 +202,7 @@ class ClackFlashes(ContinualAnimation): "flash_radius": 0.2, }, "start_up_time": 0, + "min_time_between_flashes": 1 / 30, } def __init__(self, clack_data, **kwargs): @@ -233,7 +211,7 @@ class ClackFlashes(ContinualAnimation): group = Group() last_time = 0 for location, time in clack_data: - if (time - last_time) < MIN_TIME_BETWEEN_FLASHES: + if (time - last_time) < self.min_time_between_flashes: continue last_time = time flash = Flash(location, **self.flash_config) @@ -256,6 +234,33 @@ class ClackFlashes(ContinualAnimation): self.mobject.remove(flash.mobject) +class Wall(Line): + CONFIG = { + "tick_spacing": 0.5, + "tick_length": 0.25, + "tick_style": { + "stroke_width": 1, + "stroke_color": WHITE, + }, + } + + def __init__(self, height, **kwargs): + Line.__init__(self, ORIGIN, height * UP, **kwargs) + self.height = height + self.ticks = self.get_ticks() + self.add(self.ticks) + + def get_ticks(self): + n_lines = int(self.height / self.tick_spacing) + lines = VGroup(*[ + Line(ORIGIN, self.tick_length * UR).shift(n * self.tick_spacing * UP) + for n in range(n_lines) + ]) + lines.set_style(**self.tick_style) + lines.move_to(self, DR) + return lines + + class BlocksAndWallScene(Scene): CONFIG = { "include_sound": True, @@ -268,6 +273,8 @@ class BlocksAndWallScene(Scene): "counter_label": "\\# Collisions: ", "collision_sound": "clack.wav", "show_flash_animations": True, + "min_time_between_sounds": 0.004, + "allow_sound": True, } def setup(self): @@ -320,16 +327,10 @@ class BlocksAndWallScene(Scene): self.counter_mob = counter_mob def get_wall(self): - wall = Line(self.floor_y * UP, FRAME_HEIGHT * UP / 2) + height = (FRAME_HEIGHT / 2) - self.floor_y + wall = Wall(height=height) wall.shift(self.wall_x * RIGHT) - lines = VGroup(*[ - Line(ORIGIN, 0.25 * UR) - for x in range(self.n_wall_ticks) - ]) - lines.set_stroke(width=1) - lines.arrange_submobjects(UP, buff=MED_SMALL_BUFF) - lines.move_to(wall, DR) - wall.add(lines) + wall.to_edge(UP, buff=0) return wall def get_floor(self): @@ -359,7 +360,7 @@ class BlocksAndWallScene(Scene): total_time = max(times) + 1 clacks = AudioSegment.silent(int(1000 * total_time)) last_position = 0 - min_diff = int(1000 * MIN_TIME_BETWEEN_FLASHES) + min_diff = int(1000 * self.min_time_between_sounds) for time in times: position = int(1000 * time) d_position = position - last_position @@ -583,8 +584,7 @@ class BlocksAndWallExample(BlocksAndWallScene): CONFIG = { "sliding_blocks_config": { "block1_config": { - # "mass": 1e0, - "mass": 64, + "mass": 1e0, "velocity": -2, } }, @@ -927,7 +927,7 @@ class PiComputingAlgorithmsAxes(Scene): lag_ratio=0.4, )) self.wait() - self.play(CircleThenFadeAround(algorithms[-1][0])) + self.play(ShowCreationThenFadeAround(algorithms[-1][0])) def get_machin_like_formula(self): formula = TexMobject( @@ -1152,7 +1152,7 @@ class CompareToGalacticMass(Scene): "velocity": -0.01, "distance": 4.5, "label_text": "$100^{(20 - 1)}$ kg", - "color": BLACK, + "fill_color": BLACK, }, "block2_config": { "distance": 1, diff --git a/active_projects/clacks_solution1.py b/active_projects/clacks_solution1.py index 4484583b..5a99f54b 100644 --- a/active_projects/clacks_solution1.py +++ b/active_projects/clacks_solution1.py @@ -2,10 +2,46 @@ from big_ol_pile_of_manim_imports import * from active_projects.clacks import * - -class LastVideo(Scene): +# TODO, add solution image +class FromPuzzleToSolution(MovingCameraScene): def construct(self): - pass + big_rect = FullScreenFadeRectangle() + big_rect.set_fill(DARK_GREY, 0.5) + self.add(big_rect) + + rects = VGroup(ScreenRectangle(), ScreenRectangle()) + rects.set_height(3) + rects.arrange_submobjects(RIGHT, buff=2) + + titles = VGroup( + TextMobject("Puzzle"), + TextMobject("Solution"), + ) + + images = Group( + ImageMobject("BlocksAndWallExampleMass16"), + ImageMobject("SphereSurfaceProof2"), # TODO + ) + for title, rect, image in zip(titles, rects, images): + title.scale(1.5) + title.next_to(rect, UP) + image.replace(rect) + self.add(image, rect, title) + + frame = self.camera_frame + frame.save_state() + + self.play( + frame.replace, images[0], + run_time=3 + ) + self.wait() + self.play(Restore(frame, run_time=3)) + self.play( + frame.replace, images[1], + run_time=3, + ) + self.wait() class BlocksAndWallExampleMass16(BlocksAndWallExample): @@ -20,7 +56,6 @@ class BlocksAndWallExampleMass16(BlocksAndWallExample): } - class Mass16WithElasticLabel(Mass1e1WithElasticLabel): CONFIG = { "sliding_blocks_config": { @@ -40,4 +75,2207 @@ class BlocksAndWallExampleMass64(BlocksAndWallExample): }, }, "wait_time": 25, - } \ No newline at end of file + } + + +class BlocksAndWallExampleMass1e4(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e4, + "velocity": -1.5, + }, + }, + "wait_time": 25, + } + + +class BlocksAndWallExampleMassMillion(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e6, + "velocity": -0.9, + "label_text": "$100^{3}$ kg" + }, + }, + "wait_time": 30, + "million_fade_time": 4, + "min_time_between_sounds": 0.002, + } + + def setup(self): + super().setup() + self.add_million_label() + + def add_million_label(self): + first_label = self.blocks.block1.label + brace = Brace(first_label[:-2], UP, buff=SMALL_BUFF) + new_label = TexMobject("1{,}000{,}000") + new_label.next_to(brace, UP, buff=SMALL_BUFF) + new_label.add(brace) + new_label.set_color(YELLOW) + + def update_label(label): + d_time = self.get_time() - self.million_fade_time + opacity = smooth(d_time) + label.set_fill(opacity=d_time) + + new_label.add_updater(update_label) + first_label.add(new_label) + + +class BlocksAndWallExampleMassTrillion(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e12, + "velocity": -1, + }, + }, + "wait_time": 30, + "min_time_between_sounds": 0.001, + } + + +class AskAboutFindingNewVelocities(Scene): + CONFIG = { + "floor_y": -3, + "wall_x": -6.5, + "wall_height": 7, + "block1_config": { + "mass": 10, + "fill_color": BLUE_E, + "velocity": -1, + }, + "block2_config": {"mass": 1}, + "block1_start_x": 7, + "block2_start_x": 3, + "v_arrow_scale_value": 1.0, + "is_halted": False, + } + + def setup(self): + self.add_clack_sound_file() + + def construct(self): + self.add_clack_sound_file() + self.add_floor() + self.add_wall() + self.add_blocks() + self.add_velocity_labels() + + self.ask_about_transfer() + self.show_ms_and_vs() + self.show_value_on_equations() + + def add_clack_sound_file(self): + self.clack_file = os.path.join( + VIDEO_DIR, "active_projects", + "clacks", "sounds", "clack.wav" + ) + + def add_floor(self): + floor = self.floor = Line( + self.wall_x * RIGHT, + (FRAME_WIDTH / 2) * RIGHT, + ) + floor.shift(self.floor_y * UP) + self.add(floor) + + def add_wall(self): + wall = self.wall = Wall(height=self.wall_height) + wall.move_to( + self.wall_x * RIGHT + self.floor_y * UP, + DR, + ) + self.add(wall) + + def add_blocks(self): + block1 = self.block1 = Block(**self.block1_config) + block2 = self.block2 = Block(**self.block2_config) + blocks = self.blocks = VGroup(block1, block2) + block1.move_to(self.block1_start_x * RIGHT + self.floor_y * UP, DOWN) + block2.move_to(self.block2_start_x * RIGHT + self.floor_y * UP, DOWN) + + self.add_velocity_phase_space_point() + # Add arrows + for block in blocks: + arrow = Vector(self.block_to_v_vector(block)) + arrow.set_color(RED) + arrow.set_stroke(BLACK, 1, background=True) + arrow.move_to(block.get_center(), RIGHT) + block.arrow = arrow + block.add(arrow) + + block.v_label = DecimalNumber( + block.velocity, + num_decimal_places=2, + background_stroke_width=2, + ) + block.v_label.set_color(RED) + block.add(block.v_label) + + # Add updater + blocks.add_updater(self.update_blocks) + self.add( + blocks, + block2.arrow, block1.arrow, + block2.v_label, block1.v_label, + ) + + def add_velocity_phase_space_point(self): + self.vps_point = VectorizedPoint([ + np.sqrt(self.block1.mass) * self.block1.velocity, + np.sqrt(self.block2.mass) * self.block2.velocity, + 0 + ]) + + def add_velocity_labels(self): + v_labels = self.get_next_velocity_labels() + + self.add(v_labels) + + def ask_about_transfer(self): + energy_expression, momentum_expression = \ + self.get_energy_and_momentum_expressions() + energy_words = TextMobject("Conservation of energy:") + energy_words.move_to(UP) + energy_words.to_edge(LEFT, buff=1.5) + momentum_words = TextMobject("Conservation of momentum:") + momentum_words.next_to( + energy_words, DOWN, + buff=0.7, + ) + + energy_expression.next_to(energy_words, RIGHT, MED_LARGE_BUFF) + momentum_expression.next_to(energy_expression, DOWN) + momentum_expression.next_to(momentum_words, RIGHT) + + velocity_labels = self.all_velocity_labels + randy = Randolph(height=2) + randy.next_to(velocity_labels, DR) + randy.save_state() + randy.fade(1) + + # Up to collisions + self.go_through_next_collision(include_velocity_label_animation=True) + self.play( + randy.restore, + randy.change, "pondering", velocity_labels[0], + ) + self.halt() + self.play(randy.look_at, velocity_labels[-1]) + self.play(Blink(randy)) + self.play( + FadeInFrom(energy_words, RIGHT), + FadeInFromDown(energy_expression), + FadeOut(randy), + ) + self.wait() + self.play( + FadeInFrom(momentum_words, RIGHT), + FadeInFromDown(momentum_expression) + ) + self.wait() + + self.energy_expression = energy_expression + self.energy_words = energy_words + self.momentum_expression = momentum_expression + self.momentum_words = momentum_words + + def show_ms_and_vs(self): + block1 = self.block1 + block2 = self.block2 + energy_expression = self.energy_expression + momentum_expression = self.momentum_expression + + for block in self.blocks: + block.shift_onto_screen() + + m1_labels = VGroup( + block1.label, + energy_expression.get_part_by_tex("m_1"), + momentum_expression.get_part_by_tex("m_1"), + ) + m2_labels = VGroup( + block2.label, + energy_expression.get_part_by_tex("m_2"), + momentum_expression.get_part_by_tex("m_2"), + ) + v1_labels = VGroup( + block1.v_label, + energy_expression.get_part_by_tex("v_1"), + momentum_expression.get_part_by_tex("v_1"), + ) + v2_labels = VGroup( + block2.v_label, + energy_expression.get_part_by_tex("v_2"), + momentum_expression.get_part_by_tex("v_2"), + ) + label_groups = VGroup( + m1_labels, m2_labels, + v1_labels, v2_labels, + ) + for group in label_groups: + group.rects = VGroup(*map( + SurroundingRectangle, + group + )) + + for group in label_groups: + self.play(LaggedStart( + ShowCreation, group.rects, + lag_ratio=0.8, + run_time=1, + )) + self.play(FadeOut(group.rects)) + + def show_value_on_equations(self): + energy_expression = self.energy_expression + momentum_expression = self.momentum_expression + energy_text = VGroup(energy_expression, self.energy_words) + momentum_text = VGroup(momentum_expression, self.momentum_words) + block1 = self.block1 + block2 = self.block2 + block1.save_state() + block2.save_state() + + v_terms, momentum_v_terms = [ + VGroup(*[ + expr.get_part_by_tex("v_{}".format(d)) + for d in [1, 2] + ]) + for expr in [energy_expression, momentum_expression] + ] + v_braces = VGroup(*[ + Brace(term, UP, buff=SMALL_BUFF) + for term in v_terms + ]) + v_decimals = VGroup(*[DecimalNumber(0) for x in range(2)]) + + def update_v_decimals(v_decimals): + values = self.get_velocities() + for decimal, value, brace in zip(v_decimals, values, v_braces): + decimal.set_value(value) + decimal.next_to(brace, UP, SMALL_BUFF) + + update_v_decimals(v_decimals) + + energy_const_brace, momentum_const_brace = [ + Brace( + expr.get_part_by_tex("const"), UP, + buff=SMALL_BUFF, + ) + for expr in [energy_expression, momentum_expression] + ] + + sqrt_m_vect = np.array([ + np.sqrt(self.block1.mass), + np.sqrt(self.block2.mass), + 0 + ]) + + def get_energy(): + return 0.5 * get_norm(self.vps_point.get_location())**2 + + def get_momentum(): + return np.dot(self.vps_point.get_location(), sqrt_m_vect) + + energy_decimal = DecimalNumber(get_energy()) + energy_decimal.next_to(energy_const_brace, UP, SMALL_BUFF) + momentum_decimal = DecimalNumber(get_momentum()) + momentum_decimal.next_to(momentum_const_brace, UP, SMALL_BUFF) + + VGroup( + energy_const_brace, energy_decimal, + momentum_const_brace, momentum_decimal, + ).set_color(YELLOW) + + self.play( + ShowCreationThenFadeAround(energy_expression), + momentum_text.set_fill, {"opacity": 0.25}, + FadeOut(self.all_velocity_labels), + ) + self.play(*[ + *map(GrowFromCenter, v_braces), + *map(VFadeIn, v_decimals), + GrowFromCenter(energy_const_brace), + FadeIn(energy_decimal), + ]) + energy_decimal.add_updater( + lambda m: m.set_value(get_energy()) + ) + v_decimals.add_updater(update_v_decimals) + self.add(v_decimals) + self.unhalt() + for x in range(4): + self.go_through_next_collision() + energy_decimal.clear_updaters() + momentum_decimal.set_value(get_momentum()) + self.halt() + self.play(*[ + momentum_text.set_fill, {"opacity": 1}, + FadeOut(energy_text), + FadeOut(energy_const_brace), + FadeOut(energy_decimal), + GrowFromCenter(momentum_const_brace), + FadeIn(momentum_decimal), + *[ + ApplyMethod(b.next_to, vt, UP, SMALL_BUFF) + for b, vt in zip(v_braces, momentum_v_terms) + ], + ]) + self.unhalt() + momentum_decimal.add_updater( + lambda m: m.set_value(get_momentum()) + ) + momentum_decimal.add_updater( + lambda m: m.next_to(momentum_const_brace, UP, SMALL_BUFF) + ) + for x in range(4): + self.go_through_next_collision() + self.wait(10) + + # Helpers + + def get_energy_and_momentum_expressions(self): + tex_to_color_map = { + "v_1": RED_B, + "v_2": RED_B, + "m_1": BLUE_C, + "m_2": BLUE_C, + } + energy_expression = 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_expression = TexMobject( + "m_1 v_1 + m_2 v_2 =", "\\text{const.}", + tex_to_color_map=tex_to_color_map + ) + return VGroup( + energy_expression, + momentum_expression, + ) + + def go_through_next_collision(self, include_velocity_label_animation=False): + block2 = self.block2 + if block2.velocity >= 0: + self.wait_until(self.blocks_are_hitting) + self.add_sound(self.clack_file) + self.transfer_momentum() + edge = RIGHT + else: + self.wait_until(self.block2_is_hitting_wall) + self.add_sound(self.clack_file) + self.reflect_block2() + edge = LEFT + anims = [Flash(block2.get_edge_center(edge))] + if include_velocity_label_animation: + anims.append(self.get_next_velocity_labels_animation()) + self.play(*anims, run_time=0.5) + + def get_next_velocity_labels_animation(self): + return FadeInFrom( + self.get_next_velocity_labels(), + LEFT, + run_time=0.5 + ) + + def get_next_velocity_labels(self, v1=None, v2=None): + new_labels = self.get_velocity_labels(v1, v2) + if hasattr(self, "all_velocity_labels"): + arrow = Vector(RIGHT) + arrow.next_to(self.all_velocity_labels) + new_labels.next_to(arrow, RIGHT) + new_labels.add(arrow) + else: + self.all_velocity_labels = VGroup() + self.all_velocity_labels.add(new_labels) + return new_labels + + def get_velocity_labels(self, v1=None, v2=None): + default_vs = self.get_velocities() + v1 = v1 or default_vs[0] + v2 = v2 or default_vs[1] + labels = VGroup( + TexMobject("v_1 = {:.2f}".format(v1)), + TexMobject("v_2 = {:.2f}".format(v2)), + ) + labels.arrange_submobjects( + DOWN, + buff=MED_SMALL_BUFF, + aligned_edge=LEFT, + ) + labels.scale(0.9) + for label in labels: + label[:2].set_color(RED) + labels.next_to(self.wall, RIGHT) + labels.to_edge(UP, buff=MED_SMALL_BUFF) + return labels + + def update_blocks(self, blocks, dt): + for block, velocity in zip(blocks, self.get_velocities()): + block.velocity = velocity + if not self.is_halted: + block.shift(block.velocity * dt * RIGHT) + center = block.get_center() + block.arrow.put_start_and_end_on( + center, + center + self.block_to_v_vector(block), + ) + max_height = 0.25 + block.v_label.set_value(block.velocity) + if block.v_label.get_height() > max_height: + block.v_label.set_height(max_height) + block.v_label.next_to( + block.arrow.get_start(), UP, + buff=SMALL_BUFF, + ) + return blocks + + def block_to_v_vector(self, block): + return block.velocity * self.v_arrow_scale_value * RIGHT + + def blocks_are_hitting(self): + x1 = self.block1.get_left()[0] + x2 = self.block2.get_right()[0] + buff = 0.01 + return (x1 < x2 + buff) + + def block2_is_hitting_wall(self): + x2 = self.block2.get_left()[0] + buff = 0.01 + return (x2 < self.wall_x + buff) + + def get_velocities(self): + m1 = self.block1.mass + m2 = self.block2.mass + vps_coords = self.vps_point.get_location() + return [ + vps_coords[0] / np.sqrt(m1), + vps_coords[1] / np.sqrt(m2), + ] + + def transfer_momentum(self): + m1 = self.block1.mass + m2 = self.block2.mass + theta = np.arctan(np.sqrt(m2 / m1)) + self.reflect_block2() + self.vps_point.rotate(2 * theta, about_point=ORIGIN) + + def reflect_block2(self): + self.vps_point.points[:, 1] *= -1 + + def halt(self): + self.is_halted = True + + def unhalt(self): + self.is_halted = False + + +class IntroduceVelocityPhaseSpace(AskAboutFindingNewVelocities): + CONFIG = { + "wall_height": 1.5, + "floor_y": -3.5, + "block1_start_x": 5, + "block2_start_x": 0, + "axes_config": { + "x_axis_config": { + "x_min": -5.5, + "x_max": 6, + }, + "y_axis_config": { + "x_min": -3.5, + "x_max": 4, + }, + "number_line_config": { + "unit_size": 0.7, + }, + }, + } + + def construct(self): + self.add_wall_floor_and_blocks() + self.show_two_equations() + self.draw_axes() + self.draw_ellipse() + self.rescale_axes() + self.show_starting_point() + self.show_initial_collide() + self.ask_about_where_to_land() + self.show_conservation_of_momentum_equation() + self.show_momentum_line() + self.reiterate_meaning_of_line_and_circle() + self.reshow_first_jump() + self.show_bounce_off_wall() + self.show_reflection_about_x() + self.show_remaining_collisions() + + def add_wall_floor_and_blocks(self): + self.add_floor() + self.add_wall() + self.add_blocks() + self.halt() + + def show_two_equations(self): + equations = self.get_energy_and_momentum_expressions() + equations.arrange_submobjects(DOWN, buff=LARGE_BUFF) + equations.shift(UP) + v1_terms, v2_terms = v_terms = VGroup(*[ + VGroup(*[ + expr.get_parts_by_tex(tex) + for expr in equations + ]) + for tex in ("v_1", "v_2") + ]) + for eq in equations: + eq.highlighted_copy = eq.copy() + eq.highlighted_copy.set_fill(opacity=0) + eq.highlighted_copy.set_stroke(YELLOW, 3) + + self.add(equations) + self.play( + ShowCreation(equations[0].highlighted_copy), + run_time=0.75, + ) + self.play( + FadeOut(equations[0].highlighted_copy), + ShowCreation(equations[1].highlighted_copy), + run_time=0.75, + ) + self.play( + FadeOut(equations[1].highlighted_copy), + run_time=0.75, + ) + self.play(LaggedStart( + Indicate, v_terms, + lag_ratio=0.75, + rate_func=there_and_back, + )) + self.wait() + + self.equations = equations + + def draw_axes(self): + equations = self.equations + energy_expression, momentum_expression = equations + + axes = self.axes = Axes(**self.axes_config) + axes.to_edge(UP, buff=SMALL_BUFF) + axes.set_stroke(width=2) + + # Axes labels + x_axis_labels = VGroup( + TexMobject("x = ", "v_1"), + TexMobject("x = ", "\\sqrt{m_1}", "\\cdot", "v_1"), + ) + y_axis_labels = VGroup( + TexMobject("y = ", "v_2"), + TexMobject("y = ", "\\sqrt{m_2}", "\\cdot", "v_2"), + ) + axis_labels = self.axis_labels = VGroup(x_axis_labels, y_axis_labels) + for label_group in axis_labels: + for label in label_group: + label.set_color_by_tex("v_", RED) + label.set_color_by_tex("m_", BLUE) + for label in x_axis_labels: + label.next_to(axes.x_axis.get_right(), UP) + for label in y_axis_labels: + label.next_to(axes.y_axis.get_top(), DR) + + # Introduce axes and labels + self.play( + equations.scale, 0.8, + equations.to_corner, UL, {"buff": MED_SMALL_BUFF}, + Write(axes), + ) + self.wait() + self.play( + momentum_expression.set_fill, {"opacity": 0.2}, + Indicate(energy_expression, scale_factor=1.05), + ) + self.wait() + for n in range(2): + tex = "v_{}".format(n + 1) + self.play( + TransformFromCopy( + energy_expression.get_part_by_tex(tex), + axis_labels[n][0].get_part_by_tex(tex), + ), + FadeInFromDown(axis_labels[n][0][0]), + ) + + # Show vps_dot + vps_dot = self.vps_dot = Dot(color=RED) + vps_dot.set_stroke(BLACK, 2, background=True) + vps_dot.add_updater( + lambda m: m.move_to(axes.coords_to_point( + *self.get_velocities() + )) + ) + + vps_point = self.vps_point + vps_point.save_state() + kwargs = { + "path_arc": PI / 3, + "run_time": 2, + } + target_locations = [ + 6 * RIGHT + 2 * UP, + 6 * RIGHT + 2 * DOWN, + 6 * LEFT + 1 * UP, + ] + self.add(vps_dot) + for target_location in target_locations: + self.play( + vps_point.move_to, target_location, + **kwargs, + ) + self.play(Restore(vps_point, **kwargs)) + self.wait() + + def draw_ellipse(self): + vps_dot = self.vps_dot + vps_point = self.vps_point + axes = self.axes + energy_expression = self.equations[0] + + ellipse = self.ellipse = Circle(color=YELLOW) + ellipse.set_stroke(BLACK, 5, background=True) + ellipse.rotate(PI) + ellipse.replace( + Polygon(*[ + axes.coords_to_point(x, y * np.sqrt(10)) + for x, y in [(1, 0), (0, 1), (-1, 0), (0, -1)] + ]), + stretch=True + ) + + self.play(Indicate(energy_expression, scale_factor=1.05)) + self.add(ellipse, vps_dot) + self.play( + ShowCreation(ellipse), + Rotating(vps_point, about_point=ORIGIN), + run_time=6, + rate_func=lambda t: smooth(t, 3), + ) + self.wait() + + def rescale_axes(self): + ellipse = self.ellipse + axis_labels = self.axis_labels + equations = self.equations + vps_point = self.vps_point + vps_dot = self.vps_dot + vps_dot.clear_updaters() + vps_dot.add_updater( + lambda m: m.move_to(ellipse.get_left()) + ) + + brief_circle = ellipse.copy() + brief_circle.stretch(np.sqrt(10), 0) + brief_circle.set_stroke(WHITE, 2) + + xy_equation = self.xy_equation = TexMobject( + "\\frac{1}{2}", + "\\left(", "x^2", "+", "y^2", "\\right)", + "=", "\\text{const.}" + ) + xy_equation.scale(0.8) + xy_equation.next_to(equations[0], DOWN) + + self.play(ShowCreationThenFadeOut(brief_circle)) + for i, labels, block in zip(it.count(), axis_labels, self.blocks): + self.play(ShowCreationThenFadeAround(labels[0])) + self.play( + ReplacementTransform(labels[0][0], labels[1][0]), + ReplacementTransform(labels[0][-1], labels[1][-1]), + FadeInFromDown(labels[1][1:-1]), + ellipse.stretch, np.sqrt(block.mass), i, + ) + self.wait() + + vps_dot.clear_updaters() + vps_dot.add_updater( + lambda m: m.move_to(self.axes.coords_to_point( + *self.vps_point.get_location()[:2] + )) + ) + + self.play( + FadeInFrom(xy_equation, UP), + FadeOut(equations[1]) + ) + self.wait() + curr_x = vps_point.get_location()[0] + for x in [0.5 * curr_x, 2 * curr_x, curr_x]: + axes_center = self.axes.coords_to_point(0, 0) + self.play( + vps_point.move_to, x * RIGHT, + UpdateFromFunc( + ellipse, + lambda m: m.set_width( + 2 * get_norm( + vps_dot.get_center() - axes_center, + ), + ).move_to(axes_center) + ), + run_time=2, + ) + self.wait() + + def show_starting_point(self): + vps_dot = self.vps_dot + block1, block2 = self.blocks + + self.unhalt() + self.wait(3) + self.halt() + self.play(ShowCreationThenFadeAround(vps_dot)) + self.wait() + + def show_initial_collide(self): + self.unhalt() + self.go_through_next_collision() + self.wait() + self.halt() + self.wait() + + def ask_about_where_to_land(self): + self.play( + Rotating( + self.vps_point, + about_point=ORIGIN, + run_time=6, + rate_func=lambda t: smooth(t, 3), + ), + ) + self.wait(2) + + def show_conservation_of_momentum_equation(self): + equations = self.equations + energy_expression, momentum_expression = equations + momentum_expression.set_fill(opacity=1) + momentum_expression.shift(MED_SMALL_BUFF * UP) + momentum_expression.shift(MED_SMALL_BUFF * LEFT) + xy_equation = self.xy_equation + + momentum_xy_equation = self.momentum_xy_equation = TexMobject( + "\\sqrt{m_1}", "x", "+", + "\\sqrt{m_2}", "y", "=", + "\\text{const.}", + ) + momentum_xy_equation.set_color_by_tex("m_", BLUE) + momentum_xy_equation.scale(0.8) + momentum_xy_equation.next_to( + momentum_expression, DOWN, + buff=MED_LARGE_BUFF, + aligned_edge=RIGHT, + ) + + self.play( + FadeOut(xy_equation), + energy_expression.set_fill, {"opacity": 0.2}, + FadeInFromDown(momentum_expression) + ) + self.play(ShowCreationThenFadeAround(momentum_expression)) + self.wait() + self.play(FadeInFrom(momentum_xy_equation, UP)) + self.wait() + + def show_momentum_line(self): + vps_dot = self.vps_dot + m1 = self.block1.mass + m2 = self.block2.mass + line = Line(np.sqrt(m2) * LEFT, np.sqrt(m1) * DOWN) + line.scale(4) + line.set_stroke(GREEN, 3) + line.move_to(vps_dot) + + slope_label = TexMobject( + "\\text{Slope =}", "-\\sqrt{\\frac{m_1}{m_2}}" + ) + slope_label.scale(0.8) + slope_label.next_to(vps_dot, LEFT, LARGE_BUFF) + slope_arrow = Arrow( + slope_label.get_right(), + line.point_from_proportion(0.45), + buff=SMALL_BUFF, + ) + slope_group = VGroup(line, slope_label, slope_arrow) + foreground_mobs = VGroup( + self.equations[1], self.momentum_xy_equation, + self.blocks, self.vps_dot + ) + for mob in foreground_mobs: + if isinstance(mob, TexMobject): + mob.set_stroke(BLACK, 3, background=True) + + self.add(line, *foreground_mobs) + self.play(ShowCreation(line)) + self.play( + FadeInFrom(slope_label, RIGHT), + GrowArrow(slope_arrow), + ) + self.wait() + self.add(slope_group, *foreground_mobs) + self.play(slope_group.shift, 4 * RIGHT, run_time=3) + self.play(slope_group.shift, 5 * LEFT, run_time=3) + self.play( + slope_group.shift, RIGHT, + run_time=1, + rate_func=lambda t: t**4, + ) + self.wait() + + self.momentum_line = line + self.slope_group = slope_group + + def reiterate_meaning_of_line_and_circle(self): + line_vect = self.momentum_line.get_vector() + vps_point = self.vps_point + + for x in [0.25, -0.5, 0.25]: + self.play( + vps_point.shift, x * line_vect, + run_time=2 + ) + self.wait() + self.play(Rotating( + vps_point, + about_point=ORIGIN, + rate_func=lambda t: smooth(t, 3), + )) + self.wait() + + def reshow_first_jump(self): + vps_point = self.vps_point + curr_point = vps_point.get_location() + start_point = get_norm(curr_point) * LEFT + + for n in range(8): + vps_point.move_to( + [start_point, curr_point][n % 2] + ) + self.wait(0.5) + self.wait() + + def show_bounce_off_wall(self): + self.unhalt() + self.go_through_next_collision() + self.halt() + + def show_reflection_about_x(self): + vps_point = self.vps_point + + curr_location = vps_point.get_location() + old_location = np.array(curr_location) + old_location[1] *= -1 + + # self.play( + # ApplyMethod( + # self.block2.move_to, self.wall.get_corner(DR), DL, + # path_arc=30 * DEGREES, + # ) + # ) + for n in range(4): + self.play( + vps_point.move_to, + [old_location, curr_location][n % 2] + ) + self.wait() + + def show_remaining_collisions(self): + line = self.momentum_line + # slope_group = self.slope_group + vps_dot = self.vps_dot + axes = self.axes + slope = np.sqrt(self.block2.mass / self.block1.mass) + + end_region = Polygon( + axes.coords_to_point(0, 0), + axes.coords_to_point(10, 0), + axes.coords_to_point(10, slope * 10), + stroke_width=0, + fill_color=GREEN, + fill_opacity=0.3 + ) + + self.unhalt() + for x in range(7): + self.go_through_next_collision() + if x == 0: + self.halt() + self.play(line.move_to, vps_dot) + self.wait() + self.unhalt() + self.play(FadeIn(end_region)) + self.go_through_next_collision() + self.wait(5) + + # Helpers + def add_update_line(self, func): + if hasattr(self, "vps_dot"): + old_vps_point = self.vps_dot.get_center() + func() + self.vps_dot.update() + new_vps_point = self.vps_dot.get_center() + line = Line(old_vps_point, new_vps_point) + line.set_stroke(WHITE, 2) + self.add(line) + else: + func() + + def transfer_momentum(self): + self.add_update_line(super().transfer_momentum) + + def reflect_block2(self): + self.add_update_line(super().reflect_block2) + + +class CircleDiagramFromSlidingBlocks(Scene): + CONFIG = { + "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e1, + "circle_config": { + "radius": 2, + "stroke_color": YELLOW, + "stroke_width": 2, + }, + "lines_style": { + "stroke_color": WHITE, + "stroke_width": 1, + }, + "axes_config": { + "style": { + "stroke_color": LIGHT_GREY, + "stroke_width": 1, + }, + "width": 5, + "height": 4.5, + }, + "end_zone_style": { + "stroke_width": 0, + "fill_color": GREEN, + "fill_opacity": 0.3, + }, + } + + def construct(self): + sliding_blocks_scene = self.BlocksAndWallSceneClass( + show_flash_animations=False, + write_to_movie=False, + wait_time=0, + ) + blocks = sliding_blocks_scene.blocks + times = [pair[1] for pair in blocks.clack_data] + self.mass_ratio = 1 / blocks.mass_ratio + self.show_circle_lines( + times=times, + slope=(-1 / np.sqrt(blocks.mass_ratio)) + ) + + def show_circle_lines(self, times, slope): + circle = self.get_circle() + axes = self.get_axes() + lines = self.get_lines(circle.radius, slope) + end_zone = self.get_end_zone() + + dot = Dot(color=RED, radius=0.06) + dot.move_to(lines[0].get_start()) + + self.add(end_zone, axes, circle, dot) + + last_time = 0 + for time, line in zip(times, lines): + if time > 300: + time = last_time + 1 + self.wait(time - last_time) + last_time = time + dot.move_to(line.get_end()) + self.add(line, dot) + + def get_circle(self): + circle = Circle(**self.circle_config) + circle.rotate(PI) # Nice to have start point on left + return circle + + def get_axes(self): + config = self.axes_config + axes = VGroup( + Line(LEFT, RIGHT).set_width(config["width"]), + Line(DOWN, UP).set_height(config["height"]) + ) + axes.set_style(**config["style"]) + return axes + + def get_lines(self, radius, slope): + theta = np.arctan(-1 / slope) + n_clacks = int(PI / theta) + points = [] + for n in range(n_clacks + 1): + theta_mult = (n + 1) // 2 + angle = 2 * theta * theta_mult + if n % 2 == 0: + angle *= -1 + new_point = radius * np.array([ + -np.cos(angle), -np.sin(angle), 0 + ]) + points.append(new_point) + + lines = VGroup(*[ + Line(p1, p2) + for p1, p2 in zip(points, points[1:]) + ]) + lines.set_style(**self.lines_style) + return lines + + def get_end_zone(self): + slope = 1 / np.sqrt(self.mass_ratio) + x = self.axes_config["width"] / 2 + zone = Polygon( + ORIGIN, x * RIGHT, x * RIGHT + slope * x * UP, + ) + zone.set_style(**self.end_zone_style) + return zone + + +class CircleDiagramFromSlidingBlocksSameMass(CircleDiagramFromSlidingBlocks): + CONFIG = { + "BlocksAndWallSceneClass": BlocksAndWallExampleSameMass + } + + +class CircleDiagramFromSlidingBlocksSameMass1e1(CircleDiagramFromSlidingBlocks): + CONFIG = { + "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e1 + } + + +class CircleDiagramFromSlidingBlocks1e2(CircleDiagramFromSlidingBlocks): + CONFIG = { + "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e2 + } + + +class CircleDiagramFromSlidingBlocks1e4(CircleDiagramFromSlidingBlocks): + CONFIG = { + "BlocksAndWallSceneClass": BlocksAndWallExampleMass1e4 + } + + +class AnnouncePhaseDiagram(CircleDiagramFromSlidingBlocks): + def construct(self): + pd_words = TextMobject("Phase diagram") + pd_words.scale(1.5) + pd_words.move_to(self.hold_up_spot, DOWN) + pd_words_border = pd_words.copy() + pd_words_border.set_stroke(YELLOW, 2) + pd_words_border.set_fill(opacity=0) + + simple_words = TextMobject("Simple but powerful") + simple_words.next_to(pd_words, UP, LARGE_BUFF) + simple_words.shift(LEFT) + simple_words.set_color(BLUE) + simple_arrow = Arrow( + simple_words.get_bottom(), + pd_words.get_top(), + color=simple_words.get_color(), + ) + + self.play( + self.teacher.change, "raise_right_hand", + FadeInFromDown(pd_words) + ) + self.change_student_modes( + "pondering", "thinking", "pondering", + added_anims=[ShowCreationThenFadeOut(pd_words_border)] + ) + self.wait() + self.play( + FadeInFrom(simple_words, RIGHT), + GrowArrow(simple_arrow), + self.teacher.change, "hooray", + ) + self.change_student_modes( + "thinking", "happy", "thinking", + ) + self.wait(3) + + +class AnalyzeCircleGeometry(CircleDiagramFromSlidingBlocks, MovingCameraScene): + CONFIG = { + "mass_ratio": 16, + "circle_config": { + "radius": 3, + }, + "axes_config": { + "width": FRAME_WIDTH, + "height": FRAME_HEIGHT, + }, + "lines_style": { + "stroke_width": 2, + }, + } + + def construct(self): + self.add_mass_ratio_label() + self.add_circle_with_lines() + self.show_equal_arc_lengths() + self.use_arc_lengths_to_count() + self.focus_on_three_points() + self.show_likewise_for_all_jumps() + self.drop_arc_for_each_hop() + self.try_adding_one_more_arc() + self.zoom_out() + + def add_mass_ratio_label(self, mass_ratio=None): + mass_ratio = mass_ratio or self.mass_ratio + mass_ratio_label = TextMobject( + "Mass ratio =", "{:,} : 1".format(mass_ratio) + ) + mass_ratio_label.to_corner(UL, buff=MED_SMALL_BUFF) + self.add(mass_ratio_label) + self.mass_ratio_label = mass_ratio_label + + def add_circle_with_lines(self): + circle = self.get_circle() + axes = self.get_axes() + axes_labels = self.get_axes_labels(axes) + slope = -np.sqrt(self.mass_ratio) + lines = self.get_lines( + radius=circle.radius, + slope=slope, + ) + end_zone = self.get_end_zone() + + end_zone_words = TextMobject("End zone") + end_zone_words.set_height(0.25) + end_zone_words.next_to(ORIGIN, UP, SMALL_BUFF) + end_zone_words.to_edge(RIGHT, buff=MED_SMALL_BUFF) + end_zone_words.set_color(GREEN) + + self.add( + axes, axes_labels, + circle, end_zone, end_zone_words, + ) + self.play(ShowCreation(lines, run_time=3, rate_func=None)) + self.wait() + + self.set_variables_as_attrs( + circle, axes, lines, + end_zone, end_zone_words, + ) + + def show_equal_arc_lengths(self): + circle = self.circle + radius = circle.radius + theta = self.theta = np.arctan(1 / np.sqrt(self.mass_ratio)) + n_arcs = int(PI / (2 * theta)) + + lower_arcs = VGroup(*[ + Arc( + start_angle=(PI + n * 2 * theta), + angle=(2 * theta), + radius=radius + ) + for n in range(n_arcs + 1) + ]) + lower_arcs[0::2].set_color(RED) + lower_arcs[1::2].set_color(BLUE) + + upper_arcs = lower_arcs.copy() + upper_arcs.rotate(PI, axis=RIGHT, about_point=ORIGIN) + upper_arcs[0::2].set_color(BLUE) + upper_arcs[1::2].set_color(RED) + + all_arcs = VGroup(*it.chain(*zip(lower_arcs, upper_arcs))) + if int(PI / theta) % 2 == 1: + all_arcs.remove(all_arcs[-1]) + + arc_copies = lower_arcs.copy() + for arc_copy in arc_copies: + arc_copy.generate_target() + for arc in arc_copies: + arc.target.rotate(-(arc.start_angle - PI + theta)) + + equal_signs = VGroup(*[ + TexMobject("=") for x in range(len(lower_arcs)) + ]) + equal_signs.scale(0.8) + for sign in equal_signs: + sign.generate_target() + + movers = VGroup(*it.chain(*zip( + arc_copies, equal_signs + ))) + movers.remove(movers[-1]) + mover_targets = VGroup(*[mover.target for mover in movers]) + mover_targets.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + mover_targets.next_to(ORIGIN, DOWN) + mover_targets.to_edge(LEFT) + + equal_signs.scale(0) + equal_signs.fade(1) + equal_signs.move_to(mover_targets) + + all_arcs.save_state() + for arc in all_arcs: + arc.rotate(90 * DEGREES) + arc.fade(1) + arc.set_stroke(width=20) + self.play(Restore( + all_arcs, submobject_mode="lagged_start", + run_time=2, + )) + self.wait() + self.play(LaggedStart(MoveToTarget, movers)) + self.wait() + + self.arcs_equation = movers + self.lower_arcs = lower_arcs + self.upper_arcs = upper_arcs + self.all_arcs = all_arcs + + def use_arc_lengths_to_count(self): + all_arcs = self.all_arcs + lines = self.lines + + arc_counts = VGroup() + for n, arc in enumerate(all_arcs): + count_mob = Integer(n + 1) + count_mob.scale(0.75) + point = arc.point_from_proportion(0.5) + count_mob.next_to(point, normalize(point), SMALL_BUFF) + arc_counts.add(count_mob) + + self.play( + FadeOut(lines), + FadeOut(all_arcs), + FadeOut(self.arcs_equation), + ) + self.play( + ShowIncreasingSubsets(all_arcs), + ShowIncreasingSubsets(lines), + ShowIncreasingSubsets(arc_counts), + run_time=5, + rate_func=bezier([0, 0, 1, 1]) + ) + self.wait() + + for group in all_arcs, arc_counts: + targets = VGroup() + for elem in group: + elem.generate_target() + targets.add(elem.target) + targets.space_out_submobjects(1.2) + + kwargs = { + "rate_func": there_and_back, + "run_time": 3, + } + self.play( + LaggedStart(MoveToTarget, all_arcs, **kwargs), + LaggedStart(MoveToTarget, arc_counts, **kwargs), + ) + + self.arc_counts = arc_counts + + def focus_on_three_points(self): + lines = self.lines + arcs = self.all_arcs + arc_counts = self.arc_counts + theta = self.theta + + arc = arcs[4] + + lines.save_state() + line_pair = lines[3:5] + lines_to_fade = VGroup(*lines[:3], *lines[5:]) + + three_points = [ + line_pair[0].get_start(), + line_pair[1].get_start(), + line_pair[1].get_end(), + ] + three_dots = VGroup(*map(Dot, three_points)) + three_dots.set_color(RED) + + theta_arc = Arc( + radius=1, + start_angle=-90 * DEGREES, + angle=theta + ) + theta_arc.shift(three_points[1]) + theta_label = TexMobject("\\theta") + theta_label.next_to(theta_arc, DOWN, SMALL_BUFF) + + center_lines = VGroup( + Line(three_points[0], ORIGIN), + Line(ORIGIN, three_points[2]), + ) + center_lines.match_style(line_pair) + + two_theta_arc = Arc( + radius=1, + start_angle=(center_lines[0].get_angle() + PI), + angle=2 * theta + ) + two_theta_label = TexMobject("2\\theta") + arc_center = two_theta_arc.point_from_proportion(0.5) + two_theta_label.next_to( + arc_center, normalize(arc_center), SMALL_BUFF + ) + two_theta_label.shift(SMALL_BUFF * RIGHT) + + to_fade = VGroup(arc_counts, arcs, lines_to_fade) + + self.play( + LaggedStart( + FadeOut, VGroup(*to_fade.family_members_with_points()) + ) + ) + lines_to_fade.fade(1) + self.play(FadeInFromLarge(three_dots[0])) + self.play(TransformFromCopy(*three_dots[:2])) + self.play(TransformFromCopy(*three_dots[1:3])) + self.wait() + self.play( + ShowCreation(theta_arc), + FadeInFrom(theta_label, UP) + ) + self.wait() + self.play( + line_pair.set_stroke, WHITE, 1, + TransformFromCopy(line_pair, center_lines), + TransformFromCopy(theta_arc, two_theta_arc), + TransformFromCopy(theta_label, two_theta_label), + ) + self.wait() + self.play( + TransformFromCopy(two_theta_arc, arc), + two_theta_label.move_to, 2.7 * arc_center, + ) + self.wait() + + self.three_dots = three_dots + self.theta_group = VGroup(theta_arc, theta_label) + self.center_lines_group = VGroup( + center_lines, two_theta_arc, + ) + self.two_theta_label = two_theta_label + + def show_likewise_for_all_jumps(self): + lines = self.lines + arcs = self.all_arcs + + every_other_line = lines[::2] + + self.play( + Restore( + lines, + submobject_mode="lagged_start", + run_time=2 + ), + FadeOut(self.center_lines_group), + FadeOut(self.three_dots), + ) + self.play(LaggedStart( + ApplyFunction, every_other_line, + lambda line: ( + lambda l: l.scale(10 / l.get_length()).set_stroke(BLUE, 3), + line + ) + )) + self.play(Restore(lines)) + self.wait() + + # Shift theta label + last_point = lines[3].get_end() + last_arc = arcs[4] + two_theta_label = self.two_theta_label + theta_group_copy = self.theta_group.copy() + for line, arc in zip(lines[5:10:2], arcs[6:11:2]): + new_point = line.get_end() + arc_point = arc.point_from_proportion(0.5) + self.play( + theta_group_copy.shift, new_point - last_point, + two_theta_label.move_to, 1.1 * arc_point, + FadeIn(arc), + FadeOut(last_arc), + ) + self.wait() + last_point = new_point + last_arc = arc + self.play( + FadeOut(theta_group_copy), + FadeOut(two_theta_label), + FadeOut(last_arc), + ) + self.wait() + + def drop_arc_for_each_hop(self): + lines = self.lines + arcs = self.all_arcs + + two_theta_labels = VGroup() + wedges = VGroup() + for arc in arcs: + label = TexMobject("2\\theta") + label.scale(0.8) + label.move_to(1.1 * arc.point_from_proportion(0.5)) + two_theta_labels.add(label) + + wedge = arc.copy() + wedge.add_control_points([ + *3 * [ORIGIN], + *3 * [wedge.points[0]] + ]) + wedge.set_stroke(width=0) + wedge.set_fill(arc.get_color(), 0.2) + wedges.add(wedge) + + self.remove(lines) + for line, arc, label, wedge in zip(lines, arcs, two_theta_labels, wedges): + self.play( + ShowCreation(line), + FadeInFrom(arc, normalize(arc.get_center())), + FadeInFrom(label, normalize(arc.get_center())), + FadeIn(wedge), + ) + + self.wedges = wedges + self.two_theta_labels = two_theta_labels + + def try_adding_one_more_arc(self): + wedges = self.wedges + theta = self.theta + + last_wedge = wedges[-1] + new_wedge = last_wedge.copy() + new_wedge.set_color(PURPLE) + new_wedge.set_stroke(WHITE, 1) + + self.play(FadeIn(new_wedge)) + for angle in [-2 * theta, 4 * DEGREES, -2 * DEGREES]: + self.play(Rotate(new_wedge, angle, about_point=ORIGIN)) + self.wait() + self.play( + new_wedge.shift, 10 * RIGHT, + rate_func=running_start, + path_arc=-30 * DEGREES, + ) + self.remove(new_wedge) + + def zoom_out(self): + frame = self.camera_frame + self.play( + frame.scale, 2, {"about_point": (TOP + RIGHT_SIDE)}, + run_time=3 + ) + self.wait() + + # Helpers + def get_axes_labels(self, axes): + axes_labels = VGroup( + TexMobject("x = ", "\\sqrt{m_1}", "\\cdot", "v_1"), + TexMobject("y = ", "\\sqrt{m_2}", "\\cdot", "v_2"), + ) + for label in axes_labels: + label.set_height(0.4) + axes_labels[0].next_to(ORIGIN, DOWN, SMALL_BUFF) + axes_labels[0].to_edge(RIGHT, MED_SMALL_BUFF) + axes_labels[1].next_to(ORIGIN, RIGHT, SMALL_BUFF) + axes_labels[1].to_edge(UP, SMALL_BUFF) + return axes_labels + + +class InscribedAngleTheorem(Scene): + def construct(self): + self.add_title() + self.show_circle() + self.let_point_vary() + + def add_title(self): + title = TextMobject("Inscribed angle theorem") + title.scale(1.5) + title.to_edge(UP) + self.add(title) + self.title = title + + def show_circle(self): + # Boy is this over engineered... + circle = self.circle = Circle( + color=BLUE, + radius=2, + ) + center_dot = Dot(circle.get_center(), color=WHITE) + self.add(circle, center_dot) + + angle_trackers = self.angle_trackers = VGroup( + ValueTracker(TAU / 8), + ValueTracker(PI), + ValueTracker(-TAU / 8), + ) + + def get_point(angle): + return circle.point_from_proportion( + (angle % TAU) / TAU + ) + + def get_dot(angle): + dot = Dot(get_point(angle)) + dot.set_color(RED) + dot.set_stroke(BLACK, 3, background=True) + return dot + + def get_dots(): + return VGroup(*[ + get_dot(at.get_value()) + for at in angle_trackers + ]) + + def update_labels(labels): + center = circle.get_center() + for dot, label in zip(dots, labels): + label.move_to( + center + 1.2 * (dot.get_center() - center) + ) + + def get_lines(): + lines = VGroup(*[ + Line(d1.get_center(), d2.get_center()) + for d1, d2 in zip(dots, dots[1:]) + ]) + lines.set_stroke(WHITE, 3) + return lines + + def get_center_lines(): + points = [ + dots[0].get_center(), + circle.get_center(), + dots[2].get_center(), + ] + lines = VGroup(*[ + Line(p1, p2) + for p1, p2 in zip(points, points[1:]) + ]) + lines.set_stroke(LIGHT_GREY, 3) + return lines + + def get_angle_label(lines, tex, reduce_angle=True): + a1 = (lines[0].get_angle() + PI) % TAU + a2 = lines[1].get_angle() + diff = (a2 - a1) + if reduce_angle: + diff = ((diff + PI) % TAU) - PI + point = lines[0].get_end() + arc = Arc( + start_angle=a1, + angle=diff, + radius=0.5, + ) + arc.shift(point) + arc_center = arc.point_from_proportion(0.5) + label = TexMobject(tex) + vect = (arc_center - point) + vect = (0.3 + get_norm(vect)) * normalize(vect) + label.move_to(point + vect) + return VGroup(arc, label) + + def get_theta_label(): + return get_angle_label(lines, "\\theta") + + def get_2theta_label(): + return get_angle_label(center_lines, "2\\theta", False) + + dots = get_dots() + lines = get_lines() + center_lines = get_center_lines() + labels = VGroup(*[ + TexMobject("P_{}".format(n + 1)) + for n in range(3) + ]) + update_labels(labels) + theta_label = get_theta_label() + two_theta_label = get_2theta_label() + + self.play( + FadeInFromDown(labels[0]), + FadeInFromLarge(dots[0]), + ) + self.play( + TransformFromCopy(*labels[:2]), + TransformFromCopy(*dots[:2]), + ShowCreation(lines[0]), + ) + self.play( + ShowCreation(lines[1]), + TransformFromCopy(*labels[1:3]), + TransformFromCopy(*dots[1:3]), + Write(theta_label), + ) + self.wait() + self.play( + TransformFromCopy(lines, center_lines), + TransformFromCopy(theta_label, two_theta_label), + ) + self.wait() + + # Add updaters + labels.add_updater(update_labels) + dots.add_updater(lambda m: m.become(get_dots())) + lines.add_updater(lambda m: m.become(get_lines())) + center_lines.add_updater(lambda m: m.become(get_center_lines())) + theta_label.add_updater(lambda m: m.become(get_theta_label())) + two_theta_label.add_updater(lambda m: m.become(get_2theta_label())) + + self.add( + labels, lines, center_lines, dots, + theta_label, two_theta_label, + ) + + def let_point_vary(self): + p1_tracker, p2_tracker, p3_tracker = self.angle_trackers + + kwargs = {"run_time": 2} + for angle in [TAU / 4, 3 * TAU / 4]: + self.play( + p2_tracker.set_value, angle, + **kwargs + ) + self.wait() + self.play( + p1_tracker.set_value, PI, + **kwargs + ) + self.wait() + self.play( + p3_tracker.set_value, TAU / 3, + **kwargs + ) + self.wait() + self.play( + p2_tracker.set_value, 7 * TAU / 8, + **kwargs + ) + self.wait() + + +class AddTwoThetaManyTimes(Scene): + def construct(self): + expression = TexMobject( + "2\\theta", "+", + "2\\theta", "+", + "2\\theta", "+", + "\\cdots", "+", + "2\\theta", + "<", "2\\pi", + ) + expression.to_corner(UL) + + brace = Brace(expression[:-2], DOWN) + question = brace.get_text("Max number of times?") + question.set_color(YELLOW) + + central_question_group = self.get_central_question() + simplified, lil_brace, new_question = central_question_group + central_question_group.next_to(question, DOWN, LARGE_BUFF) + new_question.align_to(question, LEFT) + + for n in range(5): + self.add(expression[:2 * n + 1]) + self.wait(0.25) + self.play( + FadeInFrom(expression[-2:], LEFT), + GrowFromCenter(brace), + FadeInFrom(question, UP) + ) + self.wait(3) + self.play( + TransformFromCopy(expression[:-2], simplified[:3]), + TransformFromCopy(expression[-2:], simplified[3:]), + TransformFromCopy(brace, lil_brace), + ) + self.play(Write(new_question)) + self.wait() + + self.central_question_group = central_question_group + self.show_example() + + def get_central_question(self, brace_vect=DOWN): + expression = TexMobject( + "N", "\\cdot", "\\theta", "<", "\\pi" + ) + N = expression[0] + N.set_color(BLUE) + brace = Brace(N, brace_vect, buff=SMALL_BUFF) + question = brace.get_text( + "Maximal integer?", + ) + question.set_color(YELLOW) + result = VGroup(expression, brace, question) + result.to_corner(UL) + return result + + def show_example(self): + expression, brace, question = self.central_question_group + N_mob = Integer(1) + N_mob.match_height(expression[0]) + N_mob.set_color(BLUE) + dot_theta_eq = TexMobject("\\cdot", "(0.01)", "=") + rhs = DecimalNumber(0, num_decimal_places=2) + rhs.set_color(RED) + dot_theta_eq.move_to(expression[1:4]) + dot_theta_eq.shift(2 * DOWN) + + def align_value(mob): + mob.align_to(dot_theta_eq[1][1:-1], DOWN) + + N_mob.add_updater( + lambda m: m.next_to(dot_theta_eq, LEFT, SMALL_BUFF) + ) + N_mob.add_updater(align_value) + rhs.add_updater( + lambda m: m.set_value(0.01 * N_mob.get_value()) + ) + rhs.add_updater( + lambda m: m.next_to(dot_theta_eq, RIGHT, 2 * SMALL_BUFF) + ) + rhs.add_updater(align_value) + + self.play( + TransformFromCopy(expression[0], N_mob), + TransformFromCopy(expression[1:4], dot_theta_eq), + TransformFromCopy(expression[4], rhs), + ) + self.wait() + self.play( + ChangeDecimalToValue(N_mob, 314, run_time=5) + ) + self.wait() + self.play(ChangeDecimalToValue(N_mob, 315)) + self.wait() + self.play(ChangeDecimalToValue(N_mob, 314)) + self.wait() + self.play(ShowCreationThenFadeAround(N_mob)) + + +class AskAboutTheta(TeacherStudentsScene): + def construct(self): + self.student_says( + "But what is $\\theta$?", + target_mode="raise_left_hand", + ) + self.change_student_modes( + "confused", "sassy", "raise_left_hand", + added_anims=[self.teacher.change, "happy"] + ) + self.wait(3) + + +class ComputeThetaFor1e4(AnalyzeCircleGeometry): + CONFIG = { + "mass_ratio": 100, + } + + def construct(self): + self.add_mass_ratio_label() + self.add_circle_with_three_lines() + self.write_slope() + self.show_tangent() + + def add_circle_with_three_lines(self): + circle = self.get_circle() + axes = self.get_axes() + slope = -np.sqrt(self.mass_ratio) + lines = self.get_lines( + radius=circle.radius, + slope=slope, + ) + end_zone = self.get_end_zone() + axes_labels = self.get_axes_labels(axes) + axes.add(axes_labels) + + lines_to_fade = VGroup(*lines[:11], *lines[13:]) + two_lines = lines[11:13] + + theta = self.theta = np.arctan(-1 / slope) + arc = Arc( + start_angle=(-90 * DEGREES), + angle=theta, + radius=2, + arc_center=two_lines[0].get_end(), + ) + theta_label = TexMobject("\\theta") + theta_label.scale(0.8) + theta_label.next_to(arc, DOWN, SMALL_BUFF) + + self.add(end_zone, axes, circle) + self.play(ShowCreation(lines, rate_func=None)) + self.play( + lines_to_fade.set_stroke, WHITE, 1, 0.3, + ShowCreation(arc), + FadeInFrom(theta_label, UP) + ) + + self.two_lines = two_lines + self.lines = lines + self.circle = circle + self.axes = axes + self.theta_label_group = VGroup(theta_label, arc) + + def write_slope(self): + line = self.two_lines[1] + slope_label = TexMobject( + "\\text{Slope}", "=", + "\\frac{\\text{rise}}{\\text{run}}", "=", + "\\frac{-\\sqrt{m_1}}{\\sqrt{m_2}}", "=", "-10" + ) + for mob in slope_label: + mob.add_to_back(mob.copy().set_stroke(BLACK, 6)) + slope_label.next_to(line.get_center(), UR, buff=1) + slope_arrow = Arrow( + slope_label[0].get_bottom(), + line.point_from_proportion(0.45), + color=RED, + buff=SMALL_BUFF, + ) + new_line = line.copy().set_color(RED) + + self.play( + FadeInFromDown(slope_label[:3]), + ShowCreation(new_line), + GrowArrow(slope_arrow), + ) + self.remove(new_line) + line.match_style(new_line) + self.play( + FadeInFrom(slope_label[3:5], LEFT) + ) + self.wait() + self.play( + Write(slope_label[5]), + TransformFromCopy( + self.mass_ratio_label[1][:3], + slope_label[6] + ) + ) + self.wait() + + self.slope_label = slope_label + self.slope_arrow = slope_arrow + + def show_tangent(self): + l1, l2 = self.two_lines + theta = self.theta + theta_label_group = self.theta_label_group + + tan_equation = TexMobject( + "\\tan", "(", "\\theta", ")", "=", + "{\\text{run}", "\\over", "-\\text{rise}}", "=", + "\\frac{1}{10}", + ) + tan_equation.scale(0.9) + tan_equation.to_edge(LEFT, buff=MED_SMALL_BUFF) + tan_equation.shift(2 * UP) + run_word = tan_equation.get_part_by_tex("run") + rise_word = tan_equation.get_part_by_tex("rise") + + p1, p2 = l1.get_start(), l1.get_end() + p3 = p1 + get_norm(p2 - p1) * np.tan(theta) * RIGHT + triangle = Polygon(p1, p2, p3) + triangle.set_stroke(width=0) + triangle.set_fill(GREEN, 0.5) + + opposite = Line(p1, p3) + adjacent = Line(p1, p2) + opposite.set_stroke(BLUE, 3) + adjacent.set_stroke(PINK, 3) + + arctan_equation = TexMobject( + "\\theta", "=", "\\arctan", "(", "1 / 10", ")" + ) + arctan_equation.next_to(tan_equation, DOWN, MED_LARGE_BUFF) + + self.play( + FadeInFromDown(tan_equation[:8]), + ) + self.play( + TransformFromCopy(theta_label_group[1], opposite), + run_word.set_color, opposite.get_color() + ) + self.play(WiggleOutThenIn(run_word)) + self.play( + TransformFromCopy(opposite, adjacent), + rise_word.set_color, adjacent.get_color() + ) + self.play(WiggleOutThenIn(rise_word)) + self.wait() + self.play(TransformFromCopy( + self.slope_label[-1], + tan_equation[-2:], + )) + self.wait(2) + + indices = [2, 4, 0, 1, -1, 3] + movers = VGroup(*[tan_equation[i] for i in indices]).copy() + for mover, target in zip(movers, arctan_equation): + mover.target = target + # Swap last two + sm = movers.submobjects + sm[-1], sm[-2] = sm[-2], sm[-1] + self.play(LaggedStart( + Transform, movers, + lambda m: (m, m.target), + lag_ratio=0.3, + run_time=2, + )) + self.remove(movers) + self.add(arctan_equation) + self.play(ShowCreationThenFadeAround(arctan_equation)) + self.wait() + + +class ThetaChart(Scene): + def construct(self): + self.create_columns() + self.populate_columns() + self.show_values() + self.highlight_example(2) + self.highlight_example(3) + + def create_columns(self): + titles = VGroup(*[ + TextMobject("Mass ratio"), + TextMobject("$\\theta$ formula"), + TextMobject("$\\theta$ value"), + ]) + titles.scale(1.5) + titles.arrange_submobjects(RIGHT, buff=1.5) + titles[-1].shift(MED_SMALL_BUFF * RIGHT) + titles.to_corner(UL) + + lines = VGroup() + for t1, t2 in zip(titles, titles[1:]): + line = Line(TOP, BOTTOM) + x = np.mean([t1.get_center()[0], t2.get_center()[0]]) + line.shift(x * RIGHT) + lines.add(line) + + h_line = Line(LEFT_SIDE, RIGHT_SIDE) + h_line.next_to(titles, DOWN) + h_line.to_edge(LEFT, buff=0) + lines.add(h_line) + lines.set_stroke(WHITE, 1) + + self.play( + LaggedStart(FadeInFromDown, titles), + LaggedStart(ShowCreation, lines, lag_ratio=0.8), + ) + + self.h_line = h_line + self.titles = titles + + def populate_columns(self): + top_h_line = self.h_line + x_vals = [t.get_center()[0] for t in self.titles] + + entries = [ + ( + "$m_1$ : $m_2$", + "$\\arctan(\\sqrt{m2} / \\sqrt{m1})$", + "" + ) + ] + [ + ( + "{:,} : 1".format(10**(2 * exp)), + "$\\arctan(1 / {:,})$".format(10**exp), + "{:0.10f}".format(np.arctan(10**(-exp))) + ) + for exp in [1, 2, 3, 4, 5] + ] + + h_lines = VGroup(top_h_line) + entry_mobs = VGroup() + for entry in entries: + mobs = VGroup(*map(TextMobject, entry)) + for mob, x in zip(mobs, x_vals): + mob.shift(x * RIGHT) + delta_y = (mobs.get_height() / 2) + MED_SMALL_BUFF + y = h_lines[-1].get_center()[1] - delta_y + mobs.shift(y * UP) + mobs[0].set_color(BLUE) + mobs[2].set_color(YELLOW) + entry_mobs.add(mobs) + + h_line = DashedLine(LEFT_SIDE, RIGHT_SIDE) + h_line.shift((y - delta_y) * UP) + h_lines.add(h_line) + + self.play( + LaggedStart( + FadeInFromDown, + VGroup(*[em[:2] for em in entry_mobs]) + ), + LaggedStart(ShowCreation, h_lines[1:]) + ) + + self.entry_mobs = entry_mobs + self.h_lines = h_lines + + def show_values(self): + values = VGroup(*[em[2] for em in self.entry_mobs]) + for value in values: + self.play(LaggedStart( + FadeIn, value, + lag_ratio=0.1, + run_time=0.5 + )) + self.wait(0.5) + + def highlight_example(self, exp): + entry_mobs = self.entry_mobs + example = entry_mobs[exp] + other_entries = VGroup(*entry_mobs[:exp], *entry_mobs[exp + 1:]) + + value = example[-1] + rhs = TexMobject("\\approx {:}".format(10**(-exp))) + rhs.next_to(value, RIGHT) + rhs.to_edge(RIGHT, buff=MED_SMALL_BUFF) + value.generate_target() + value.target.set_fill(opacity=1) + value.target.next_to(rhs, LEFT, SMALL_BUFF) + + self.play( + other_entries.set_fill, {"opacity": 0.25}, + example.set_fill, {"opacity": 1}, + ShowCreationThenFadeAround(example) + ) + self.wait() + self.play( + MoveToTarget(value), + Write(rhs), + ) + self.wait() + value.add(rhs) + + +class CentralQuestionFor1e2(AddTwoThetaManyTimes): + CONFIG = { + "exp": 2, + } + + def construct(self): + exp = self.exp + question = self.get_central_question(UP) + pi_value = TexMobject(" = {:0.10f}\\dots".format(PI)) + pi_value.next_to(question[0][-1], RIGHT, SMALL_BUFF) + pi_value.shift(0.3 * SMALL_BUFF * UP) + question.add(pi_value) + + max_count = int(PI * 10**exp) + + question.center().to_edge(UP) + self.add(question) + + b10_equation = self.get_changable_equation( + 10**(-exp), n_decimal_places=exp + ) + b10_equation.next_to(question, DOWN, buff=1.5) + arctan_equation = self.get_changable_equation( + np.arctan(10**(-exp)), n_decimal_places=10, + ) + arctan_equation.next_to(b10_equation, DOWN, MED_LARGE_BUFF) + eq_centers = [ + eq[1][2].get_center() + for eq in [b10_equation, arctan_equation] + ] + arctan_equation.shift((eq_centers[1][0] - eq_centers[1][0]) * RIGHT) + + # b10_brace = Brace(b10_equation[1][1][1:-1], UP) + arctan_brace = Brace(arctan_equation[1][1][1:-1], DOWN) + # b10_tex = b10_brace.get_tex("1 / 10") + arctan_tex = arctan_brace.get_tex( + "\\theta = \\arctan(1 / {:,})".format(10**exp) + ) + + int_mobs = b10_equation[0], arctan_equation[0] + + self.add(*b10_equation, *arctan_equation) + # self.add(b10_brace, b10_tex) + self.add(arctan_brace, arctan_tex) + + self.wait() + self.play(*[ + ChangeDecimalToValue(int_mob, max_count, run_time=3) + for int_mob in int_mobs + ]) + self.wait() + self.play(*[ + ChangeDecimalToValue(int_mob, max_count + 1, run_time=1) + for int_mob in int_mobs + ]) + self.wait() + self.play(*[ + ChangeDecimalToValue(int_mob, max_count, run_time=1) + for int_mob in int_mobs + ]) + self.play(ShowCreationThenFadeAround(int_mobs[1])) + self.wait() + + # + def get_changable_equation(self, value, tex_string=None, n_decimal_places=10): + int_mob = Integer(1) + int_mob.set_color(BLUE) + formatter = "({:0." + str(n_decimal_places) + "f})" + tex_string = tex_string or formatter.format(value) + tex_mob = TexMobject("\\cdot", tex_string, "=") + rhs = DecimalNumber(value, num_decimal_places=n_decimal_places) + + def align_number(mob): + y0 = mob[0].get_center()[1] + y1 = tex_mob[1][1:-1].get_center()[1] + mob.shift((y1 - y0) * UP) + + int_mob.add_updater( + lambda m: m.next_to(tex_mob, LEFT, SMALL_BUFF) + ) + int_mob.add_updater(align_number) + rhs.add_updater( + lambda m: m.set_value(value * int_mob.get_value()) + ) + rhs.add_updater( + lambda m: m.next_to(tex_mob, RIGHT, SMALL_BUFF) + ) + rhs.add_updater(align_number) + + def get_comp_pi(): + if rhs.get_value() < np.pi: + result = TexMobject("< \\pi") + result.set_color(GREEN) + elif rhs.get_value() > np.pi: + result = TexMobject("> \\pi") + result.set_color(RED) + else: + result = TexMobject("= \\pi") + result.next_to(rhs, RIGHT, 2 * SMALL_BUFF) + result[1].scale(1.5, about_edge=LEFT) + return result + + comp_pi = updating_mobject_from_func(get_comp_pi) + + return VGroup(int_mob, tex_mob, rhs, comp_pi) + + +class AnalyzeCircleGeometry1e2(AnalyzeCircleGeometry): + CONFIG = { + "mass_ratio": 100, + } + + +class CentralQuestionFor1e3(CentralQuestionFor1e2): + CONFIG = {"exp": 3} diff --git a/manimlib/animation/creation.py b/manimlib/animation/creation.py index 54ed21aa..42f4f92f 100644 --- a/manimlib/animation/creation.py +++ b/manimlib/animation/creation.py @@ -113,6 +113,17 @@ class Write(DrawBorderThenFill): else: self.run_time = 2 + +class ShowIncreasingSubsets(Animation): + def __init__(self, group, **kwargs): + self.all_submobs = group.submobjects + Animation.__init__(self, group, **kwargs) + + def update_mobject(self, alpha): + n_submobs = len(self.all_submobs) + index = int(alpha * n_submobs) + self.mobject.submobjects = self.all_submobs[:index] + # Fading @@ -203,6 +214,7 @@ class VFadeIn(Animation): to mobjects while they are being animated in some other way (e.g. shifting then) in a way that does not work with FadeIn and FadeOut """ + def update_submobject(self, submobject, starting_submobject, alpha): submobject.set_stroke( opacity=interpolate(0, starting_submobject.get_stroke_opacity(), alpha) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index beb4f2cd..6b870378 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -24,6 +24,7 @@ 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): @@ -143,6 +144,20 @@ class ShowCreationThenDestruction(ShowPassingFlash): } +class ShowCreationThenFadeOut(Succession): + CONFIG = { + "remover": True, + } + + def __init__(self, mobject, **kwargs): + Succession.__init__( + self, + ShowCreation, mobject, + FadeOut, mobject, + **kwargs + ) + + class AnimationOnSurroundingRectangle(AnimationGroup): CONFIG = { "surrounding_rectangle_config": {}, @@ -174,7 +189,7 @@ class ShowCreationThenDestructionAround(AnimationOnSurroundingRectangle): } -class CircleThenFadeAround(AnimationOnSurroundingRectangle): +class ShowCreationThenFadeAround(AnimationOnSurroundingRectangle): CONFIG = { "rect_to_animation": lambda rect: Succession( ShowCreation, rect, diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 96e7c58a..6741288a 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -392,7 +392,10 @@ class Camera(object): vmobject ) ctx.set_line_width( - width * self.cairo_line_width_multiple + width * self.cairo_line_width_multiple * + # This ensures lines have constant width + # as you zoom in on them. + (self.get_frame_width() / FRAME_WIDTH) ) ctx.stroke_preserve() return self diff --git a/manimlib/config.py b/manimlib/config.py index ce540a72..19063713 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -19,32 +19,92 @@ def parse_cli(): ) parser.add_argument( "scene_names", - nargs="+", + nargs="*", help="Name of the Scene class you want to see", ) - optional_args = [ - ("-p", "--preview"), - ("-w", "--write_to_movie"), - ("-s", "--show_last_frame"), - ("-l", "--low_quality"), - ("-m", "--medium_quality"), - ("-g", "--save_pngs"), - ("-f", "--show_file_in_finder"), - ("-t", "--transparent"), - ("-q", "--quiet"), - ("-a", "--write_all") - ] - for short_arg, long_arg in optional_args: - parser.add_argument(short_arg, long_arg, action="store_true") - parser.add_argument("-o", "--output_file_name") - parser.add_argument("-n", "--start_at_animation_number") - parser.add_argument("-r", "--resolution") - parser.add_argument("-c", "--color") + parser.add_argument( + "-p", "--preview", + action="store_true", + help="Automatically open movie file once its done", + ), + parser.add_argument( + "-w", "--write_to_movie", + action="store_true", + help="Render the scene as a movie file", + ), + parser.add_argument( + "-s", "--show_last_frame", + action="store_true", + help="Save the last frame and open the image file", + ), + parser.add_argument( + "-l", "--low_quality", + action="store_true", + help="Render at a low quality (for faster rendering)", + ), + parser.add_argument( + "-m", "--medium_quality", + action="store_true", + help="Render at a medium quality", + ), + parser.add_argument( + "-g", "--save_pngs", + action="store_true", + help="Save each frame as a png", + ), + parser.add_argument( + "-f", "--show_file_in_finder", + action="store_true", + help="Show the output file in finder", + ), + parser.add_argument( + "-t", "--transparent", + action="store_true", + help="Render to a movie file with an alpha channel", + ), + parser.add_argument( + "-q", "--quiet", + action="store_true", + help="", + ), + parser.add_argument( + "-a", "--write_all", + action="store_true", + help="Write all the scenes from a file", + ), + parser.add_argument( + "-o", "--output_file_name", + nargs=1, + help="Specify the name of the output file, if" + "it should be different from the scene class name", + ) + parser.add_argument( + "-n", "--start_at_animation_number", + help="Start rendering not from the first animation, but" + "from another, specified by its index. If you pass" + "in two comma separated values, e.g. \"3,6\", it will end" + "the rendering at the second value", + ) + parser.add_argument( + "-r", "--resolution", + help="Resolution, passed as \"height,width\"", + ) + parser.add_argument( + "-c", "--color", + help="Background color", + ) parser.add_argument( "--sound", action="store_true", help="Play a success/failure sound", ) + parser.add_argument( + "--leave_progress_bars", + action="store_true", + help="Leave progress bars displayed in terminal", + ) + + # For live streaming module_location.add_argument( "--livestream", action="store_true", @@ -129,6 +189,7 @@ def get_configuration(args): "start_at_animation_number": args.start_at_animation_number, "end_at_animation_number": None, "sound": args.sound, + "leave_progress_bars": args.leave_progress_bars } # Camera configuration diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 2e2348b3..c2265db0 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -133,7 +133,8 @@ def main(config): "movie_file_extension", "start_at_animation_number", "end_at_animation_number", - "output_file_name" + "output_file_name", + "leave_progress_bars", ] ]) if config["save_pngs"]: diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index ee36564c..e439f88d 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -298,7 +298,7 @@ class TeacherStudentsScene(PiCreatureScene): "raise_left_hand", ]) kwargs["target_mode"] = target_mode - student = self.get_students()[kwargs.get("student_index", 1)] + student = self.get_students()[kwargs.get("student_index", 2)] return self.pi_creature_says( student, *content, **kwargs ) @@ -309,7 +309,7 @@ class TeacherStudentsScene(PiCreatureScene): ) def student_thinks(self, *content, **kwargs): - student = self.get_students()[kwargs.get("student_index", 1)] + student = self.get_students()[kwargs.get("student_index", 2)] return self.pi_creature_thinks(student, *content, **kwargs) def change_all_student_modes(self, mode, **kwargs): diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index fe6f1f53..03dfae06 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -9,6 +9,7 @@ from manimlib.mobject.svg.tex_mobject import TexMobject from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.config_ops import digest_config +from manimlib.utils.config_ops import merge_config from manimlib.utils.space_ops import angle_of_vector # TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene @@ -35,16 +36,21 @@ class Axes(VGroup): def __init__(self, **kwargs): VGroup.__init__(self, **kwargs) - self.x_axis = self.get_axis(self.x_min, self.x_max, self.x_axis_config) - self.y_axis = self.get_axis(self.y_min, self.y_max, self.y_axis_config) - self.y_axis.rotate(np.pi / 2, about_point=ORIGIN) + x_axis_config = merge_config([ + self.x_axis_config, + {"x_min": self.x_min, "x_max": self.x_max}, + self.number_line_config, + ]) + y_axis_config = merge_config([ + self.y_axis_config, + {"x_min": self.y_min, "x_max": self.y_max}, + self.number_line_config, + ]) + self.x_axis = NumberLine(**x_axis_config) + self.y_axis = NumberLine(**y_axis_config) + self.y_axis.rotate(90 * DEGREES, about_point=ORIGIN) self.add(self.x_axis, self.y_axis) - def get_axis(self, min_val, max_val, extra_config): - config = dict(self.number_line_config) - config.update(extra_config) - return NumberLine(x_min=min_val, x_max=max_val, **config) - def coords_to_point(self, *coords): origin = self.x_axis.number_to_point(0) result = np.array(origin) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index d838930d..093118d9 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -23,6 +23,7 @@ class Arc(VMobject): "start_angle": 0, "num_anchors": 9, "anchors_span_full_range": True, + "arc_center": ORIGIN, } def __init__(self, angle, **kwargs): @@ -50,6 +51,7 @@ class Arc(VMobject): anchors, handles1, handles2 ) self.scale(self.radius, about_point=ORIGIN) + self.shift(self.arc_center) def add_tip(self, tip_length=0.25, at_start=False, at_end=True): # clear out any old tips @@ -166,7 +168,7 @@ class Circle(Arc): } def __init__(self, **kwargs): - Arc.__init__(self, 2 * np.pi, **kwargs) + Arc.__init__(self, TAU, **kwargs) def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2): # Ignores dim_to_match and stretch; result will always be a circle @@ -180,6 +182,14 @@ class Circle(Arc): np.sqrt(mobject.get_width()**2 + mobject.get_height()**2)) self.scale(buffer_factor) + def get_point_from_angle(self, angle): + start_angle = angle_of_vector( + self.points[0] - self.get_center() + ) + return self.point_from_proportion( + (angle - start_angle) / TAU + ) + class Dot(Circle): CONFIG = { diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index dac35e32..5b98d718 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -145,7 +145,7 @@ class Mobject(Container): # Updating - def update(self, dt): + def update(self, dt=0): for updater in self.updaters: num_args = get_num_args(updater) if num_args == 1: diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 6bda2b95..6813c46b 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -105,7 +105,10 @@ class DecimalNumber(VMobject): full_config.update(self.initial_config) full_config.update(config) new_decimal = DecimalNumber(number, **full_config) - new_decimal.match_height(self) + # new_decimal.match_height(self) + new_decimal.scale( + self[0].get_height() / new_decimal[0].get_height() + ) new_decimal.move_to(self, self.edge_to_fix) new_decimal.match_style(self) @@ -129,3 +132,6 @@ class Integer(DecimalNumber): def increment_value(self): self.set_value(self.get_value() + 1) + + def get_value(self): + return int(np.round(super().get_value())) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b3135cf0..abd5baa7 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -10,6 +10,7 @@ import warnings from tqdm import tqdm as ProgressDisplay import numpy as np +from pydub import AudioSegment from manimlib.animation.animation import Animation from manimlib.animation.creation import Write @@ -35,7 +36,6 @@ class Scene(Container): "frame_duration": LOW_QUALITY_FRAME_DURATION, "construct_args": [], "skip_animations": False, - "ignore_waits": False, "write_to_movie": False, "save_pngs": False, "pngs_mode": "RGBA", @@ -48,6 +48,7 @@ class Scene(Container): "to_twitch": False, "twitch_key": None, "output_file_name": None, + "leave_progress_bars": False, } def __init__(self, **kwargs): @@ -66,17 +67,20 @@ class Scene(Container): random.seed(self.random_seed) np.random.seed(self.random_seed) + self.init_audio() self.setup() if self.livestreaming: return None try: self.construct(*self.construct_args) except EndSceneEarlyException: - pass + if hasattr(self, "writing_process"): + self.writing_process.terminate() self.tear_down() if self.write_to_movie: self.combine_movie_files() + self.print_end_message() def handle_play_like_call(func): def wrapper(self, *args, **kwargs): @@ -117,6 +121,9 @@ class Scene(Container): return self.output_file_name return str(self) + def print_end_message(self): + print("Played {} animations".format(self.num_plays)) + def set_variables_as_attrs(self, *objects, **newly_named_objects): """ This method is slightly hacky, making it a little easier @@ -135,6 +142,38 @@ class Scene(Container): def get_attrs(self, *keys): return [getattr(self, key) for key in keys] + # Sound + def init_audio(self): + self.includes_sound = False + + def create_audio_segment(self): + self.audio_segment = AudioSegment.silent() + + def add_audio_segment(self, new_segment, time_offset=0): + if not self.includes_sound: + self.includes_sound = True + self.create_audio_segment() + segment = self.audio_segment + overly_time = self.get_time() + time_offset + if overly_time < 0: + raise Exception("Adding sound at timestamp < 0") + + curr_end = segment.duration_seconds + new_end = overly_time + new_segment.duration_seconds + diff = new_end - curr_end + if diff > 0: + segment = segment.append( + AudioSegment.silent(int(np.ceil(diff * 1000))), + crossfade=0, + ) + self.audio_segment = segment.overlay( + new_segment, position=int(1000 * overly_time) + ) + + def add_sound(self, sound_file, time_offset=0): + new_segment = AudioSegment.from_file(sound_file) + self.add_audio_segment(new_segment, 0) + # Only these methods should touch the camera def set_camera(self, camera): @@ -386,20 +425,26 @@ class Scene(Container): return mobjects[i:] return [] - def get_time_progression(self, run_time): - if self.skip_animations: + def get_time_progression(self, run_time, n_iterations=None, override_skip_animations=False): + if self.skip_animations and not override_skip_animations: times = [run_time] else: step = self.frame_duration times = np.arange(0, run_time, step) - time_progression = ProgressDisplay(times) + time_progression = ProgressDisplay( + times, total=n_iterations, + leave=self.leave_progress_bars, + ) return time_progression + def get_run_time(self, animations): + return np.max([animation.run_time for animation in animations]) + def get_animation_time_progression(self, animations): - run_time = np.max([animation.run_time for animation in animations]) + run_time = self.get_run_time(animations) time_progression = self.get_time_progression(run_time) time_progression.set_description("".join([ - "Animation %d: " % self.num_plays, + "Animation {}: ".format(self.num_plays), str(animations[0]), (", etc." if len(animations) > 1 else ""), ])) @@ -498,20 +543,18 @@ class Scene(Container): # have to be rendered every frame self.update_frame(excluded_mobjects=moving_mobjects) static_image = self.get_frame() - total_run_time = 0 for t in self.get_animation_time_progression(animations): for animation in animations: animation.update(t / animation.run_time) - self.continual_update(dt=t - total_run_time) + self.continual_update(dt=self.frame_duration) self.update_frame(moving_mobjects, static_image) self.add_frames(self.get_frame()) - total_run_time = t self.mobjects_from_last_animation = [ anim.mobject for anim in animations ] self.clean_up_animations(*animations) if self.skip_animations: - self.continual_update(total_run_time) + self.continual_update(self.get_run_time(animations)) else: self.continual_update(0) @@ -542,15 +585,35 @@ class Scene(Container): return self.mobjects_from_last_animation return [] + def get_wait_time_progression(self, duration, stop_condition): + if stop_condition is not None: + time_progression = self.get_time_progression( + duration, + n_iterations=-1, # So it doesn't show % progress + override_skip_animations=True + + ) + time_progression.set_description( + "Waiting for {}".format(stop_condition.__name__) + ) + else: + time_progression = self.get_time_progression(duration) + time_progression.set_description( + "Waiting {}".format(self.num_plays) + ) + return time_progression + @handle_play_like_call - def wait(self, duration=DEFAULT_WAIT_TIME): + def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): if self.should_continually_update(): - total_time = 0 - for t in self.get_time_progression(duration): - self.continual_update(dt=t - total_time) + time_progression = self.get_wait_time_progression(duration, stop_condition) + for t in time_progression: + self.continual_update(dt=self.frame_duration) self.update_frame() self.add_frames(self.get_frame()) - total_time = t + if stop_condition and stop_condition(): + time_progression.close() + break elif self.skip_animations: # Do nothing return self @@ -561,16 +624,8 @@ class Scene(Container): self.add_frames(*[frame] * n_frames) return self - def wait_to(self, time, assert_positive=True): - if self.ignore_waits: - return - time -= self.get_time() - if assert_positive: - assert(time >= 0) - elif time < 0: - return - - self.wait(time) + def wait_until(self, stop_condition, max_time=60): + self.wait(max_time, stop_condition=stop_condition) def force_skipping(self): self.original_skipping_status = self.skip_animations @@ -647,7 +702,9 @@ class Scene(Container): ) ) temp_file_path = file_path.replace(".", "_temp.") - self.args_to_rename_file = (temp_file_path, file_path) + + self.movie_file_path = file_path + self.temp_movie_file_path = temp_file_path fps = int(1 / self.frame_duration) height = self.camera.get_pixel_height() @@ -693,9 +750,13 @@ class Scene(Container): self.writing_process.wait() if self.livestreaming: return True - shutil.move(*self.args_to_rename_file) + shutil.move( + self.temp_movie_file_path, + self.movie_file_path, + ) def combine_movie_files(self): + # TODO, this could probably use a refactor partial_movie_file_directory = self.get_partial_movie_directory() kwargs = { "remove_non_integer_files": True, @@ -732,19 +793,48 @@ class Scene(Container): '-safe', '0', '-i', file_list, '-c', 'copy', - '-an', # Tells FFMPEG not to expect any audio '-loglevel', 'error', movie_file_path ] + if not self.includes_sound: + commands.insert(-1, '-an') + combine_process = subprocess.Popen(commands) combine_process.wait() for pf_path in partial_movie_files: os.remove(pf_path) os.remove(file_list) - os.rmdir(partial_movie_file_directory) - os.rmdir(os.path.join(partial_movie_file_directory, os.path.pardir)) print("File ready at {}".format(movie_file_path)) + if self.includes_sound: + sound_file_path = movie_file_path.replace( + self.movie_file_extension, ".wav" + ) + # Makes sure sound file length will match video file + self.add_audio_segment(AudioSegment.silent(0)) + self.audio_segment.export(sound_file_path) + temp_file_path = movie_file_path.replace(".", "_temp.") + commands = commands = [ + "ffmpeg", + "-i", movie_file_path, + "-i", sound_file_path, + '-y', # overwrite output file if it exists + "-c:v", "copy", "-c:a", "aac", + '-loglevel', 'error', + "-shortest", + "-strict", "experimental", + temp_file_path, + ] + subprocess.call(commands) + shutil.move(temp_file_path, movie_file_path) + # subprocess.call(["rm", self.temp_movie_file_path]) + subprocess.call(["rm", sound_file_path]) + + print("\nAnimation ready at {}\n".format(movie_file_path)) + + # TODO, this doesn't belong in Scene, but should be + # part of some more specialized subclass optimized + # for livestreaming def tex(self, latex): eq = TextMobject(latex) anims = [] diff --git a/old_projects/borsuk_addition.py b/old_projects/borsuk_addition.py index 51509bd4..7c78944a 100644 --- a/old_projects/borsuk_addition.py +++ b/old_projects/borsuk_addition.py @@ -527,9 +527,9 @@ class FunctionGInSymbols(Scene): VGroup(seeking_text, g_equals_zero).shift, 1.5 * DOWN ) self.wait() - self.play(CircleThenFadeAround(g_of_neg_p[2])) + self.play(ShowCreationThenFadeAround(g_of_neg_p[2])) self.wait() - self.play(CircleThenFadeAround(neg_g_of_p)) + self.play(ShowCreationThenFadeAround(neg_g_of_p)) self.wait() self.play(neg_g_of_p.restore) rects = VGroup(*map(SurroundingRectangle, [f_of_p, f_of_neg_p])) diff --git a/old_projects/dandelin.py b/old_projects/dandelin.py index 9aaf8acf..a575219c 100644 --- a/old_projects/dandelin.py +++ b/old_projects/dandelin.py @@ -387,7 +387,7 @@ class ShowArrayOfEccentricities(Scene): e_copy.set_color(RED) self.play(ShowCreation(e_copy)) self.play( - CircleThenFadeAround( + ShowCreationThenFadeAround( eccentricity_labels[i], ), FadeOut(e_copy) diff --git a/old_projects/div_curl.py b/old_projects/div_curl.py index ffec9a96..e6e55675 100644 --- a/old_projects/div_curl.py +++ b/old_projects/div_curl.py @@ -857,7 +857,7 @@ class CylinderModel(Scene): self.wait() self.play( movers.apply_complex_function, joukowsky_map, - CircleThenFadeAround(self.func_label), + ShowCreationThenFadeAround(self.func_label), run_time=2 ) self.add(self.get_stream_lines_animation(stream_lines)) diff --git a/old_projects/for_flammy.py b/old_projects/for_flammy.py index f41e6391..912a2bc7 100644 --- a/old_projects/for_flammy.py +++ b/old_projects/for_flammy.py @@ -310,9 +310,9 @@ class IntegralSymbols(Scene): self.play(FadeInFrom(rhs, 4 * LEFT)) self.wait() - self.play(CircleThenFadeAround(rhs[1])) + self.play(ShowCreationThenFadeAround(rhs[1])) self.wait() - self.play(CircleThenFadeAround(rhs[2:])) + self.play(ShowCreationThenFadeAround(rhs[2:])) self.wait() self.play( GrowFromCenter(int_brace), diff --git a/old_projects/lost_lecture.py b/old_projects/lost_lecture.py index 599230c8..d9d81e0f 100644 --- a/old_projects/lost_lecture.py +++ b/old_projects/lost_lecture.py @@ -3360,7 +3360,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): delta_t_numerator.scale, 1.5, {"about_edge": DOWN}, delta_t_numerator.set_color, YELLOW ) - self.play(CircleThenFadeAround(prop_exp[:-2])) + self.play(ShowCreationThenFadeAround(prop_exp[:-2])) self.play( delta_t_numerator.fade, 1, MoveToTarget(moving_R_squared), @@ -3447,7 +3447,7 @@ class ShowEqualAngleSlices(IntroduceShapeOfVelocities): polygon.set_fill(BLUE_E, opacity=0.8) polygon.set_stroke(WHITE, 3) - self.play(CircleThenFadeAround(v1)) + self.play(ShowCreationThenFadeAround(v1)) self.play( MoveToTarget(v1), GrowFromCenter(root_dot), diff --git a/old_projects/quaternions.py b/old_projects/quaternions.py index f0e3ebe8..7e89ecfa 100644 --- a/old_projects/quaternions.py +++ b/old_projects/quaternions.py @@ -4182,7 +4182,7 @@ class IntroduceQuaternions(Scene): FadeInFromDown(number), Write(label), ) - self.play(CircleThenFadeAround( + self.play(ShowCreationThenFadeAround( number[2:], surrounding_rectangle_config={"color": BLUE} )) @@ -4630,7 +4630,7 @@ class BreakUpQuaternionMultiplicationInParts(Scene): ) self.play( randy.change, "confused", rotate_words, - CircleThenFadeAround(rotate_words), + ShowCreationThenFadeAround(rotate_words), ) self.play(LaggedStart( FadeInFrom, q_marks, diff --git a/old_projects/sphere_area.py b/old_projects/sphere_area.py index 249236e5..4fc4daa2 100644 --- a/old_projects/sphere_area.py +++ b/old_projects/sphere_area.py @@ -1861,7 +1861,7 @@ class JustifyHeightSquish(MovingCameraScene): )) self.wait() self.play(ReplacementTransform(q_mark, alpha_label1)) - self.play(CircleThenFadeAround( + self.play(ShowCreationThenFadeAround( equation, surrounding_rectangle_config={ "buff": 0.015, diff --git a/old_projects/turbulence.py b/old_projects/turbulence.py index c574b7de..46e508b6 100644 --- a/old_projects/turbulence.py +++ b/old_projects/turbulence.py @@ -1018,7 +1018,7 @@ class ShowNavierStokesEquations(Scene): FadeInFromDown(labels[0]), newtons_second.next_to, variables, RIGHT, LARGE_BUFF ) - self.play(CircleThenFadeAround(parts[0])) + self.play(ShowCreationThenFadeAround(parts[0])) self.wait() self.play(LaggedStart(FadeInFrom, labels[1:])) self.wait(3) diff --git a/requirements.txt b/requirements.txt index c8c05da1..33c71720 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ scipy==1.1.0 tqdm==4.24.0 opencv-python==3.4.2.17 pycairo==1.17.1 +pydub==0.23.0