From 43a583fc4fe559df11fa60e226de71c59b2130bf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 25 Jan 2019 11:47:55 -0800 Subject: [PATCH 01/18] Beginning clacks_solution2 animations --- .../solution2/block_collision_scenes.py | 63 +++++++++++++++++++ .../clacks/solution2/pi_creature_scenes.py | 6 ++ .../clacks/solution2/simple_scenes.py | 17 +++++ active_projects/clacks_solution2.py | 6 ++ 4 files changed, 92 insertions(+) create mode 100644 active_projects/clacks/solution2/block_collision_scenes.py create mode 100644 active_projects/clacks/solution2/pi_creature_scenes.py create mode 100644 active_projects/clacks/solution2/simple_scenes.py create mode 100644 active_projects/clacks_solution2.py diff --git a/active_projects/clacks/solution2/block_collision_scenes.py b/active_projects/clacks/solution2/block_collision_scenes.py new file mode 100644 index 00000000..3a54fed5 --- /dev/null +++ b/active_projects/clacks/solution2/block_collision_scenes.py @@ -0,0 +1,63 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks.question import BlocksAndWallExample + + +class PreviousTwoVideos(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e4, + "velocity": -2, + "width": 3, + }, + "block2_config": { + "width": 3, + }, + }, + "wait_time": 15, + } + + def setup(self): + videos = Group( + ImageMobject + ) + + name_mobs = VGroup(*map(TextMobject, names)) + name_mobs.set_stroke(BLACK, 3, background=True) + name_mobs.set_fill(LIGHT_GREY, 1) + name_mobs.set_sheen(3, UL) + name_mobs.scale(2) + configs = [ + self.sliding_blocks_config["block1_config"], + self.sliding_blocks_config["block2_config"], + ] + for name_mob, config in zip(name_mobs, configs): + config["width"] = name_mob.get_width() + self.name_mobs = name_mobs + + super().setup() + + def add_blocks(self): + super().add_blocks() + blocks = self.blocks + name_mobs = self.name_mobs + + blocks.fade(1) + + def update_name_mobs(name_mobs): + for name_mob, block in zip(name_mobs, self.blocks): + name_mob.move_to(block) + target_y = block.get_bottom()[1] + SMALL_BUFF + curr_y = name_mob[0].get_bottom()[1] + name_mob.shift((target_y - curr_y) * UP) + + name_mobs.add_updater(update_name_mobs) + self.add(name_mobs) + + clack_y = self.name_mobs[1].get_center()[1] + for location, time in self.clack_data: + location[1] = clack_y + + for block, name_mob in zip(blocks, name_mobs): + block.label.next_to(name_mob, UP) + block.label.set_fill(YELLOW, opacity=1) diff --git a/active_projects/clacks/solution2/pi_creature_scenes.py b/active_projects/clacks/solution2/pi_creature_scenes.py new file mode 100644 index 00000000..b6c3492e --- /dev/null +++ b/active_projects/clacks/solution2/pi_creature_scenes.py @@ -0,0 +1,6 @@ +from big_ol_pile_of_manim_imports import * + + +class NewSceneName(TeacherStudentsScene): + def construct(self): + pass diff --git a/active_projects/clacks/solution2/simple_scenes.py b/active_projects/clacks/solution2/simple_scenes.py new file mode 100644 index 00000000..955f5503 --- /dev/null +++ b/active_projects/clacks/solution2/simple_scenes.py @@ -0,0 +1,17 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks.question import BlocksAndWallExample + + +class PreviousTwoVideos(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e0, + "velocity": -2, + } + }, + "wait_time": 15, + } + + def construct(self): + pass diff --git a/active_projects/clacks_solution2.py b/active_projects/clacks_solution2.py new file mode 100644 index 00000000..6840c08d --- /dev/null +++ b/active_projects/clacks_solution2.py @@ -0,0 +1,6 @@ +from active_projects.clacks.solution2 import block_collision_scenes + +OUTPUT_DIRECTORY = "clacks_solution2" +ALL_SCENE_CLASSES = [ + block_collision_scenes.PreviousTwoVideos +] From eb355fc7a196c45fb0654e4e2d9f8453e244a17d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 28 Jan 2019 10:22:08 -0800 Subject: [PATCH 02/18] Added gain to Scene.add_sound --- manimlib/scene/scene.py | 4 ++-- manimlib/scene/scene_file_writer.py | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 66f4dc5e..8be55952 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -568,9 +568,9 @@ class Scene(Container): for frame in frames: self.file_writer.write_frame(frame) - def add_sound(self, sound_file, time_offset=0): + def add_sound(self, sound_file, time_offset=0, gain=0): time = self.get_time() + time_offset - self.file_writer.add_sound(sound_file, time) + self.file_writer.add_sound(sound_file, time, gain) def show_frame(self): self.update_frame(ignore_skipping=True) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 7337748e..9ca5664f 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -119,7 +119,7 @@ class SceneFileWriter(object): def create_audio_segment(self): self.audio_segment = AudioSegment.silent() - def add_audio_segment(self, new_segment, time=None): + def add_audio_segment(self, new_segment, time=None, gain=0): if not self.includes_sound: self.includes_sound = True self.create_audio_segment() @@ -138,13 +138,15 @@ class SceneFileWriter(object): crossfade=0, ) self.audio_segment = segment.overlay( - new_segment, position=int(1000 * time) + new_segment, + position=int(1000 * time), + gain_during_overlay=gain, ) - def add_sound(self, sound_file, time): + def add_sound(self, sound_file, time, gain=0): file_path = get_full_sound_file_path(sound_file) new_segment = AudioSegment.from_file(file_path) - self.add_audio_segment(new_segment, time) + self.add_audio_segment(new_segment, time, gain) # Writers def begin_animation(self, allow_write=False): From 3aadc1fb78101e0a9ee92d8f6e9dd47c2d080a8d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 28 Jan 2019 10:24:55 -0800 Subject: [PATCH 03/18] Add inflection parameter to rush_into and rush_from --- manimlib/utils/rate_functions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manimlib/utils/rate_functions.py b/manimlib/utils/rate_functions.py index fbbe7f5e..39f9cd15 100644 --- a/manimlib/utils/rate_functions.py +++ b/manimlib/utils/rate_functions.py @@ -16,12 +16,12 @@ def smooth(t, inflection=10.0): ) -def rush_into(t): - return 2 * smooth(t / 2.0) +def rush_into(t, inflection=10.0): + return 2 * smooth(t / 2.0, inflection) -def rush_from(t): - return 2 * smooth(t / 2.0 + 0.5) - 1 +def rush_from(t, inflection=10.0): + return 2 * smooth(t / 2.0 + 0.5, inflection) - 1 def slow_into(t): From acb5b2cfea1e975918f84ef7d744bd859bba9aa8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 28 Jan 2019 10:25:15 -0800 Subject: [PATCH 04/18] Setup for clacks solution2 --- .../clacks/all_questions_scenes.py | 8 -- active_projects/clacks/name_bump.py | 2 +- active_projects/clacks/question.py | 53 ++++++----- active_projects/clacks/solution2.py | 15 +++ .../solution2/block_collision_scenes.py | 93 +++++++++++-------- .../clacks/solution2/simple_scenes.py | 49 ++++++++-- active_projects/clacks_solution2.py | 6 -- 7 files changed, 138 insertions(+), 88 deletions(-) delete mode 100644 active_projects/clacks/all_questions_scenes.py create mode 100644 active_projects/clacks/solution2.py delete mode 100644 active_projects/clacks_solution2.py diff --git a/active_projects/clacks/all_questions_scenes.py b/active_projects/clacks/all_questions_scenes.py deleted file mode 100644 index c29d03ce..00000000 --- a/active_projects/clacks/all_questions_scenes.py +++ /dev/null @@ -1,8 +0,0 @@ -from active_projects import clacks - -output_directory = "clacks_question" -all_scenes = [ - clacks.NameIntro, - clacks.MathAndPhysicsConspiring, - clacks.LightBouncing, -] diff --git a/active_projects/clacks/name_bump.py b/active_projects/clacks/name_bump.py index e7972b53..cd79392d 100644 --- a/active_projects/clacks/name_bump.py +++ b/active_projects/clacks/name_bump.py @@ -6,7 +6,7 @@ from active_projects.clacks.question import BlocksAndWallExample class NameBump(BlocksAndWallExample): CONFIG = { - "name": "Magnus Lysfjord", + "name": "Grant Sanderson", "sliding_blocks_config": { "block1_config": { "mass": 1e6, diff --git a/active_projects/clacks/question.py b/active_projects/clacks/question.py index 17dab588..acefc690 100644 --- a/active_projects/clacks/question.py +++ b/active_projects/clacks/question.py @@ -266,6 +266,7 @@ class Wall(Line): class BlocksAndWallScene(Scene): CONFIG = { "include_sound": True, + "collision_sound": "clack.wav", "count_clacks": True, "counter_group_shift_vect": LEFT, "sliding_blocks_config": {}, @@ -273,7 +274,6 @@ class BlocksAndWallScene(Scene): "wall_x": -6, "n_wall_ticks": 15, "counter_label": "\\# Collisions: ", - "collision_sound": "clack.wav", "show_flash_animations": True, "min_time_between_sounds": 0.004, } @@ -345,35 +345,30 @@ class BlocksAndWallScene(Scene): return self.counter_mob.set_value(n_clacks) - def create_sound_file(self, clack_data): - clack_file = os.path.join(SOUND_DIR, self.collision_sound) - output_file = self.get_movie_file_path(extension='.wav') + def add_clack_sounds(self, clack_data): + clack_file = self.collision_sound + total_time = self.get_time() times = [ time for location, time in clack_data - if time < 300 # In case of any extremes + if time < total_time ] - - clack = AudioSegment.from_wav(clack_file) - total_time = max(times) + 1 - clacks = AudioSegment.silent(int(1000 * total_time)) - last_position = 0 - min_diff = int(1000 * self.min_time_between_sounds) + last_time = 0 for time in times: - position = int(1000 * time) - d_position = position - last_position - if d_position < min_diff: + d_time = time - last_time + if d_time < self.min_time_between_sounds: continue - if time > self.get_time(): - break - last_position = position - clacks = clacks.fade(-50, start=position, end=position + 10) - clacks = clacks.overlay( - clack, - position=position + last_time = time + self.add_sound( + clack_file, + time_offset=(time - total_time), + gain=-20, ) - clacks.export(output_file, format="wav") - return output_file + return self + + def tear_down(self): + if self.include_sound: + self.add_clack_sounds(self.clack_data) # TODO, this no longer works # should use Scene.add_sound instead @@ -1573,6 +1568,7 @@ class Thumbnail(BlocksAndWallExample, MovingCameraScene): "count_clacks": False, "show_flash_animations": False, "floor_y": -3.0, + "include_sound": False, } def setup(self): @@ -1580,7 +1576,16 @@ class Thumbnail(BlocksAndWallExample, MovingCameraScene): BlocksAndWallExample.setup(self) def construct(self): - self.camera_frame.shift(0.9 * UP) + # self.camera_frame.shift(0.9 * UP) + self.mobjects.insert( + 0, + FullScreenFadeRectangle( + color=DARK_GREY, + opacity=0.5, + sheen_direction=UL, + sheen=0.5, + ), + ) self.thicken_lines() self.grow_labels() self.add_vector() diff --git a/active_projects/clacks/solution2.py b/active_projects/clacks/solution2.py new file mode 100644 index 00000000..0e586de6 --- /dev/null +++ b/active_projects/clacks/solution2.py @@ -0,0 +1,15 @@ +# from active_projects.clacks import question +# from active_projects.clacks import solution1 +from active_projects.clacks.solution2 import block_collision_scenes +from active_projects.clacks.solution2 import simple_scenes + +OUTPUT_DIRECTORY = "clacks_solution2" +ALL_SCENE_CLASSES = [ + # question.Thumbnail, + # solution1.SolutionThumbnail, + # question.BlocksAndWallExampleMass1e2, + # question.BlocksAndWallExampleMass1e4, + block_collision_scenes.IntroducePreviousTwoVideos, + block_collision_scenes.PreviousTwoVideos, + simple_scenes.TwoSolutionsWrapper, +] diff --git a/active_projects/clacks/solution2/block_collision_scenes.py b/active_projects/clacks/solution2/block_collision_scenes.py index 3a54fed5..b300087f 100644 --- a/active_projects/clacks/solution2/block_collision_scenes.py +++ b/active_projects/clacks/solution2/block_collision_scenes.py @@ -6,58 +6,71 @@ class PreviousTwoVideos(BlocksAndWallExample): CONFIG = { "sliding_blocks_config": { "block1_config": { - "mass": 1e4, - "velocity": -2, - "width": 3, + "mass": 1e2, + "velocity": -1, + "width": 4, + "distance": 8, }, "block2_config": { - "width": 3, + "width": 4, + "distance": 3, }, }, + "floor_y": -3, "wait_time": 15, } def setup(self): - videos = Group( - ImageMobject - ) - - name_mobs = VGroup(*map(TextMobject, names)) - name_mobs.set_stroke(BLACK, 3, background=True) - name_mobs.set_fill(LIGHT_GREY, 1) - name_mobs.set_sheen(3, UL) - name_mobs.scale(2) - configs = [ - self.sliding_blocks_config["block1_config"], - self.sliding_blocks_config["block2_config"], - ] - for name_mob, config in zip(name_mobs, configs): - config["width"] = name_mob.get_width() - self.name_mobs = name_mobs - super().setup() - - def add_blocks(self): - super().add_blocks() blocks = self.blocks - name_mobs = self.name_mobs + videos = Group( + ImageMobject("ClacksSolution1Thumbnail"), + ImageMobject("ClacksQuestionThumbnail"), + ) + for n, video, block in zip([2, 1], videos, blocks): + block.fade(1) + video.add(SurroundingRectangle( + video, buff=0, + color=BLUE, + stroke_width=3, + )) + video.replace(block) - blocks.fade(1) + title = TextMobject("Part {}".format(n)) + title.scale(1.5) + title.next_to(video, UP, MED_SMALL_BUFF) + video.add(title) - def update_name_mobs(name_mobs): - for name_mob, block in zip(name_mobs, self.blocks): - name_mob.move_to(block) - target_y = block.get_bottom()[1] + SMALL_BUFF - curr_y = name_mob[0].get_bottom()[1] - name_mob.shift((target_y - curr_y) * UP) + def update_videos(videos): + for video, block in zip(videos, blocks): + video.move_to(block, DOWN) + video.shift(0.04 * UP) - name_mobs.add_updater(update_name_mobs) - self.add(name_mobs) + videos.add_updater(update_videos) + self.add(videos) + if self.show_flash_animations: + self.add(self.clack_flashes.mobject) + self.videos = videos - clack_y = self.name_mobs[1].get_center()[1] - for location, time in self.clack_data: - location[1] = clack_y - for block, name_mob in zip(blocks, name_mobs): - block.label.next_to(name_mob, UP) - block.label.set_fill(YELLOW, opacity=1) +class IntroducePreviousTwoVideos(PreviousTwoVideos): + CONFIG = { + "show_flash_animations": False, + "include_sound": False, + } + + def construct(self): + blocks = self.blocks + videos = self.videos + + self.remove(blocks) + videos.clear_updaters() + self.remove(videos) + + self.play(FadeInFromLarge(videos[1])) + self.play(TransformFromCopy( + videos[0].copy().fade(1).shift(2 * RIGHT), + videos[0], + rate_func=lambda t: rush_into(t, 3), + )) + # self.wait() diff --git a/active_projects/clacks/solution2/simple_scenes.py b/active_projects/clacks/solution2/simple_scenes.py index 955f5503..a4b6ef3b 100644 --- a/active_projects/clacks/solution2/simple_scenes.py +++ b/active_projects/clacks/solution2/simple_scenes.py @@ -1,17 +1,48 @@ from big_ol_pile_of_manim_imports import * -from active_projects.clacks.question import BlocksAndWallExample -class PreviousTwoVideos(BlocksAndWallExample): +class TwoSolutionsWrapper(Scene): CONFIG = { - "sliding_blocks_config": { - "block1_config": { - "mass": 1e0, - "velocity": -2, - } - }, - "wait_time": 15, } 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.py b/active_projects/clacks_solution2.py deleted file mode 100644 index 6840c08d..00000000 --- a/active_projects/clacks_solution2.py +++ /dev/null @@ -1,6 +0,0 @@ -from active_projects.clacks.solution2 import block_collision_scenes - -OUTPUT_DIRECTORY = "clacks_solution2" -ALL_SCENE_CLASSES = [ - block_collision_scenes.PreviousTwoVideos -] From 9bb3b8f4bf91fb88d2fcb473978b75dedb16c011 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:21:56 -0800 Subject: [PATCH 05/18] Changed the way Mobject updaters are checked to be dependent on time --- manimlib/mobject/mobject.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 5b98d718..cdf70ae2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -17,7 +17,7 @@ from manimlib.utils.color import interpolate_color from manimlib.utils.iterables import list_update from manimlib.utils.iterables import remove_list_redundancies from manimlib.utils.paths import straight_path -from manimlib.utils.simple_functions import get_num_args +from manimlib.utils.simple_functions import get_parameters from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import rotation_matrix @@ -147,22 +147,16 @@ class Mobject(Container): def update(self, dt=0): for updater in self.updaters: - num_args = get_num_args(updater) - if num_args == 1: - updater(self) - elif num_args == 2: + parameters = get_parameters(updater) + if "dt" in parameters: updater(self, dt) else: - raise Exception( - "Mobject updater expected 1 or 2 " - "arguments, %d given" % num_args - ) + updater(self) def get_time_based_updaters(self): return [ - updater - for updater in self.updaters - if get_num_args(updater) == 2 + updater for updater in self.updaters + if "dt" in get_parameters(updater) ] def get_updaters(self): From a72b769c0fec1fa8f771d25301b87e4f30f99ac7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:22:23 -0800 Subject: [PATCH 06/18] Made 0 a default value for DecimalNumber --- manimlib/mobject/numbers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 5f48ce2e..8ec82d9e 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -15,7 +15,7 @@ class DecimalNumber(VMobject): "edge_to_fix": LEFT, } - def __init__(self, number, **kwargs): + def __init__(self, number=0, **kwargs): VMobject.__init__(self, **kwargs) self.number = number self.initial_config = kwargs From c8c003ba4b9164393c3e0649d3b8ee7873ca71cc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:22:46 -0800 Subject: [PATCH 07/18] Added simple get_parameters function --- manimlib/utils/simple_functions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index 2cd048f9..f567cf9a 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -30,7 +30,11 @@ def choose(n, r): def get_num_args(function): - return len(inspect.signature(function).parameters) + return len(get_parameters(function)) + + +def get_parameters(function): + return inspect.signature(function).parameters # Just to have a less heavyweight name for this extremely common operation # From dc24c033492ed490ceb9e0ae6c625bfd182ea16a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:23:13 -0800 Subject: [PATCH 08/18] 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 + ) From a409fd6fe5b0368191c2be793997786c480b1805 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 14:40:44 -0800 Subject: [PATCH 09/18] Suspend mobject updating during animations --- manimlib/mobject/mobject.py | 34 +++++++++++++++++++++++++++------- manimlib/scene/scene.py | 6 +++++- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index cdf70ae2..1f14cbcd 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -45,6 +45,7 @@ class Mobject(Container): if self.name is None: self.name = self.__class__.__name__ self.updaters = [] + self.updating_suspended = False self.reset_points() self.generate_points() self.init_colors() @@ -145,13 +146,18 @@ class Mobject(Container): # Updating - def update(self, dt=0): - for updater in self.updaters: - parameters = get_parameters(updater) - if "dt" in parameters: - updater(self, dt) - else: - updater(self) + def update(self, dt=0, recursive=True): + if not self.updating_suspended: + for updater in self.updaters: + parameters = get_parameters(updater) + if "dt" in parameters: + updater(self, dt) + else: + updater(self) + if recursive: + for submob in self.submobjects: + submob.update(dt, recursive) + return self def get_time_based_updaters(self): return [ @@ -180,6 +186,20 @@ class Mobject(Container): self.updaters = [] return self + def suspend_updating(self, recursive=True): + self.updating_suspended = True + if recursive: + for submob in self.submobjects: + submob.suspend_updating(recursive) + return self + + def resume_updating(self, recursive=True): + self.updating_suspended = False + if recursive: + for submob in self.submobjects: + submob.resume_updating(recursive) + return self + # Transforming operations def apply_to_family(self, func): diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 8be55952..d7977a5a 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -151,7 +151,7 @@ class Scene(Container): ### def continual_update(self, dt): - for mobject in self.get_mobject_family_members(): + for mobject in self.mobjects: mobject.update(dt) for continual_animation in self.continual_animations: continual_animation.update(dt) @@ -470,6 +470,9 @@ class Scene(Container): # scene gets added to the scene if animation.mobject not in self.get_mobject_family_members(): self.add(animation.mobject) + # Don't call the update functions of a mobject + # being animated + animation.mobject.suspend_updating() moving_mobjects = self.get_moving_mobjects(*animations) # Paint all non-moving objects onto the screen, so they don't @@ -500,6 +503,7 @@ class Scene(Container): def clean_up_animations(self, *animations): for animation in animations: animation.clean_up(self) + animation.mobject.resume_updating() return self def get_mobjects_from_last_animation(self): From 358b1e63137b80b41ed63d0bcbb14d665beb036b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 23:51:24 -0800 Subject: [PATCH 10/18] Make LaggedStart adopt default rate_func and run_time of the animation it takes in --- manimlib/animation/composition.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index b5818d21..8763ff89 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -220,10 +220,14 @@ class LaggedStart(Animation): } def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs): + for key in ["rate_func", "run_time"]: + if key in AnimationClass.CONFIG: + setattr(self, key, AnimationClass.CONFIG[key]) digest_config(self, kwargs) for key in "rate_func", "run_time", "lag_ratio": if key in kwargs: kwargs.pop(key) + if arg_creator is None: def arg_creator(mobject): return (mobject,) From 40273bab1dce52342cee05ce0c87750966fb1cbb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 23:52:01 -0800 Subject: [PATCH 11/18] Changed implementation of ShowPassingFlash to something that felt less confusing --- manimlib/animation/indication.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index f4f52bb6..441dd874 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -24,7 +24,6 @@ from manimlib.utils.rate_functions import smooth from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.rate_functions import there_and_back from manimlib.utils.rate_functions import wiggle -from manimlib.utils.rate_functions import double_smooth class FocusOn(Transform): @@ -125,10 +124,11 @@ class ShowPassingFlash(ShowPartial): } def get_bounds(self, alpha): - alpha *= (1 + self.time_width) - alpha -= self.time_width / 2.0 - lower = max(0, alpha - self.time_width / 2.0) - upper = min(1, alpha + self.time_width / 2.0) + tw = self.time_width + upper = interpolate(0, 1 + tw, alpha) + lower = upper - tw + upper = min(upper, 1) + lower = max(lower, 0) return (lower, upper) def clean_up(self, *args, **kwargs): From 7a5c4192975dfcc5e0faf5764debd272c6173757 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 23:52:56 -0800 Subject: [PATCH 12/18] Changed what the gain argument of Scene.play means to instead actually apply gain to the passed in sound, not the background --- manimlib/scene/scene.py | 4 ++-- manimlib/scene/scene_file_writer.py | 30 ++++++++++++++++++++--------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index d7977a5a..1f7f9bc1 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -572,9 +572,9 @@ class Scene(Container): for frame in frames: self.file_writer.write_frame(frame) - def add_sound(self, sound_file, time_offset=0, gain=0): + def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): time = self.get_time() + time_offset - self.file_writer.add_sound(sound_file, time, gain) + self.file_writer.add_sound(sound_file, time, gain, **kwargs) def show_frame(self): self.update_frame(ignore_skipping=True) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 9ca5664f..cd098afc 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -119,7 +119,9 @@ class SceneFileWriter(object): def create_audio_segment(self): self.audio_segment = AudioSegment.silent() - def add_audio_segment(self, new_segment, time=None, gain=0): + def add_audio_segment(self, new_segment, + time=None, + gain_to_background=None): if not self.includes_sound: self.includes_sound = True self.create_audio_segment() @@ -140,13 +142,15 @@ class SceneFileWriter(object): self.audio_segment = segment.overlay( new_segment, position=int(1000 * time), - gain_during_overlay=gain, + gain_during_overlay=gain_to_background, ) - def add_sound(self, sound_file, time, gain=0): + def add_sound(self, sound_file, time=None, gain=None, **kwargs): file_path = get_full_sound_file_path(sound_file) new_segment = AudioSegment.from_file(file_path) - self.add_audio_segment(new_segment, time, gain) + if gain: + new_segment = new_segment.apply_gain(gain) + self.add_audio_segment(new_segment, time, **kwargs) # Writers def begin_animation(self, allow_write=False): @@ -307,17 +311,25 @@ class SceneFileWriter(object): ) # Makes sure sound file length will match video file self.add_audio_segment(AudioSegment.silent(0)) - self.audio_segment.export(sound_file_path) + self.audio_segment.export( + sound_file_path, + bitrate='312k', + ) temp_file_path = movie_file_path.replace(".", "_temp.") - commands = commands = [ + commands = [ "ffmpeg", "-i", movie_file_path, "-i", sound_file_path, '-y', # overwrite output file if it exists - "-c:v", "copy", "-c:a", "aac", + "-c:v", "copy", + "-c:a", "aac", + "-b:a", "320k", + # select video stream from first file + "-map", "0:v:0", + # select audio stream from second file + "-map", "1:a:0", '-loglevel', 'error', - "-shortest", - "-strict", "experimental", + # "-shortest", temp_file_path, ] subprocess.call(commands) From a65dbf27d9519585056632b45e8d57f2fe814667 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 23:53:20 -0800 Subject: [PATCH 13/18] Changed implementation of angle_between --- manimlib/utils/space_ops.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 50efb097..959fd198 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -142,9 +142,8 @@ def angle_between_vectors(v1, v2): Returns the angle between two 3D vectors. This angle will always be btw 0 and TAU/2. """ - l1 = get_norm(v1) - l2 = get_norm(v2) - return np.arccos(np.dot(v1, v2) / (l1 * l2)) + diff = (angle_of_vector(v1) - angle_of_vector(v2)) % TAU + return min(diff, TAU - diff) def project_along_vector(point, vector): From 5d8c08c114d0d18a3b012b0e88fe401d10535bbb Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 29 Jan 2019 23:53:37 -0800 Subject: [PATCH 14/18] Further animations for clacks solution 2 project --- active_projects/clacks/all_s2_scenes.py | 7 +- active_projects/clacks/question.py | 13 +- .../clacks/solution2/pi_creature_scenes.py | 5 + .../clacks/solution2/position_phase_space.py | 713 ++++++++++++++++-- 4 files changed, 673 insertions(+), 65 deletions(-) diff --git a/active_projects/clacks/all_s2_scenes.py b/active_projects/clacks/all_s2_scenes.py index 9ddff962..579cdeb3 100644 --- a/active_projects/clacks/all_s2_scenes.py +++ b/active_projects/clacks/all_s2_scenes.py @@ -8,14 +8,13 @@ from active_projects.clacks.solution2 import position_phase_space OUTPUT_DIRECTORY = "clacks_solution2" ALL_SCENE_CLASSES = [ - # question.Thumbnail, - # solution1.SolutionThumbnail, - # question.BlocksAndWallExampleMass1e2, - # question.BlocksAndWallExampleMass1e4, block_collision_scenes.IntroducePreviousTwoVideos, block_collision_scenes.PreviousTwoVideos, simple_scenes.TwoSolutionsWrapper, wordy_scenes.ConnectionToOptics, pi_creature_scenes.OnAnsweringTwice, position_phase_space.IntroducePositionPhaseSpace, + position_phase_space.EqualMassCase, + pi_creature_scenes.AskAboutEqualMassMomentumTransfer, + position_phase_space.FailedAngleRelation, ] diff --git a/active_projects/clacks/question.py b/active_projects/clacks/question.py index edf5241b..ca8642a2 100644 --- a/active_projects/clacks/question.py +++ b/active_projects/clacks/question.py @@ -215,6 +215,10 @@ class ClackFlashes(ContinualAnimation): continue last_time = time flash = Flash(location, **self.flash_config) + for sm in flash.mobject.family_members_with_points(): + if isinstance(sm, VMobject): + sm.set_stroke(YELLOW, 3) + sm.set_stroke(WHITE, 6, 0.5, background=True) flash.start_time = time flash.end_time = time + flash.run_time self.flashes.append(flash) @@ -222,16 +226,17 @@ class ClackFlashes(ContinualAnimation): def update_mobject(self, dt): total_time = self.external_time + group = self.mobject for flash in self.flashes: if flash.start_time < total_time < flash.end_time: - if flash.mobject not in self.mobject: - self.mobject.add(flash.mobject) + if flash.mobject not in group: + group.add(flash.mobject) flash.update( (total_time - flash.start_time) / flash.run_time ) else: - if flash.mobject in self.mobject: - self.mobject.remove(flash.mobject) + if flash.mobject in group: + group.remove(flash.mobject) class Wall(Line): diff --git a/active_projects/clacks/solution2/pi_creature_scenes.py b/active_projects/clacks/solution2/pi_creature_scenes.py index daf0c493..a272d439 100644 --- a/active_projects/clacks/solution2/pi_creature_scenes.py +++ b/active_projects/clacks/solution2/pi_creature_scenes.py @@ -45,3 +45,8 @@ class OnAnsweringTwice(TeacherStudentsScene): ) shown_questions.add(oq) self.wait(3) + + +class AskAboutEqualMassMomentumTransfer(TeacherStudentsScene): + def construct(self): + pass diff --git a/active_projects/clacks/solution2/position_phase_space.py b/active_projects/clacks/solution2/position_phase_space.py index 6931d4e0..60398ad1 100644 --- a/active_projects/clacks/solution2/position_phase_space.py +++ b/active_projects/clacks/solution2/position_phase_space.py @@ -1,6 +1,7 @@ from big_ol_pile_of_manim_imports import * from active_projects.clacks.question import Block from active_projects.clacks.question import Wall +from active_projects.clacks.question import ClackFlashes class PositionPhaseSpaceScene(Scene): @@ -16,7 +17,7 @@ class PositionPhaseSpaceScene(Scene): "floor_y": -3.5, "block1_config": { "mass": 10, - "distance": 7, + "distance": 9, "velocity": 1, "width": 1.6, }, @@ -44,7 +45,13 @@ class PositionPhaseSpaceScene(Scene): "background_stroke_width": 1, "background_stroke_color": BLACK, "radius": 0.05, - } + }, + "clack_sound": "clack", + "mirror_line_class": Line, + "mirror_line_style": { + "stroke_color": WHITE, + "stroke_width": 1, + }, } def setup(self): @@ -67,10 +74,9 @@ class PositionPhaseSpaceScene(Scene): 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): + def get_floor_wall_corner(self): return self.wall_x * RIGHT + self.floor_y * UP def get_mass_ratio(self): @@ -110,11 +116,12 @@ class PositionPhaseSpaceScene(Scene): def get_ds(self): return self.point_to_ds(self.ps_point.get_location()) + # Relevant for sliding def tie_blocks_to_ps_point(self): def update_blocks(blocks): d1, d2 = self.point_to_ds(self.ps_point.get_location()) b1, b2 = blocks - corner = self.get_corner() + corner = self.get_floor_wall_corner() b1.move_to(corner + d1 * RIGHT, DL) b2.move_to(corner + d2 * RIGHT, DR) self.blocks.add_updater(update_blocks) @@ -125,10 +132,10 @@ class PositionPhaseSpaceScene(Scene): 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"] + start_d1 = self.block1_config["distance"] + start_d2 = self.block2_config["distance"] w2 = self.block2.width + start_d2 += w2 ps_speed = np.sqrt(m1) * abs(v1) theta = np.arctan(np.sqrt(m2 / m1)) @@ -145,7 +152,7 @@ class PositionPhaseSpaceScene(Scene): point[1] / np.sqrt(m2), ) - ps_point = ds_to_ps_point(d1, d2 + w2) + ps_point = ds_to_ps_point(start_d1, start_d2) wedge_corner = ds_to_ps_point(w2, w2) ps_point -= wedge_corner # Pass into the mirror worlds @@ -164,9 +171,143 @@ class PositionPhaseSpaceScene(Scene): ps_point += wedge_corner return ps_point_to_ds(ps_point) + def get_clack_data(self): + # Copying from time_to_ds. Not great, but + # maybe I'll factor things out properly later. + m1 = self.block1.mass + m2 = self.block2.mass + v1 = self.block1.velocity + w2 = self.block2.get_width() + h2 = self.block2.get_height() + ps_speed = np.sqrt(m1) * abs(v1) + theta = np.arctan(np.sqrt(m2 / m1)) + + def ds_to_ps_point(d1, d2): + return np.array([ + d1 * np.sqrt(m1), + d2 * np.sqrt(m2), + 0 + ]) + + def ps_point_to_ds(point): + return ( + point[0] / np.sqrt(m1), + point[1] / np.sqrt(m2), + ) + + ps_point = ds_to_ps_point(*self.get_ds()) + wedge_corner = ds_to_ps_point(w2, w2) + ps_point -= wedge_corner + y = ps_point[1] + + clack_data = [] + for k in range(1, int(PI / theta) + 1): + clack_ps_point = np.array([ + y / np.tan(k * theta), y, 0 + ]) + time = get_norm(ps_point - clack_ps_point) / ps_speed + reflected_point = rotate_vector( + clack_ps_point, + -2 * np.ceil((k - 1) / 2) * theta + ) + d1, d2 = ps_point_to_ds(reflected_point + wedge_corner) + loc1 = self.get_floor_wall_corner() + h2 * UP / 2 + d2 * RIGHT + if k % 2 == 0: + loc1 += w2 * LEFT + loc2 = self.ds_to_point(d1, d2) + clack_data.append((time, loc1, loc2)) + return clack_data + def get_clack_flashes(self): pass # TODO + def tie_ps_point_to_time_tracker(self): + time_tracker = self.get_time_tracker() + + def update_ps_point(p): + time = time_tracker.get_value() + ds = self.time_to_ds(time) + p.move_to(self.ds_to_point(*ds)) + self.ps_point.add_updater(update_ps_point) + self.add(time_tracker) + + def add_clack_flashes(self): + clack_data = self.get_clack_data() + self.clack_times = [ + time for (time, loc1, loc2) in clack_data + ] + self.block_flashes = ClackFlashes([ + (loc1, time) + for (time, loc1, loc2) in clack_data + ]) + self.ps_flashes = ClackFlashes([ + (loc2, time) + for (time, loc1, loc2) in clack_data + ]) + self.add( + self.block_flashes, + self.ps_flashes, + ) + + def begin_sliding(self): + self.tie_ps_point_to_time_tracker() + self.add_clack_flashes() + + def end_sliding(self): + self.ps_point.clear_updaters() + self.remove(self.time_tracker) + for attr in ["block_flashes", "ps_flashes"]: + if hasattr(self, attr): + self.remove(getattr(self, attr)) + total_time = self.time_tracker.get_value() + for time in self.clack_times: + if time < total_time: + offset = total_time - time + self.add_sound( + "clack", + time_offset=-offset, + ) + + def get_continually_building_trajectory(self): + trajectory = VMobject() + self.continually_building_trajectory = trajectory + trajectory.set_stroke(YELLOW, 1) + + def get_point(): + return np.array(self.ps_point.get_location()) + + points = [get_point(), get_point()] + trajectory.set_points_as_corners(points) + epsilon = 0.001 + + def update_trajectory(trajectory): + new_point = get_point() + p1, p2 = trajectory.get_anchors()[-2:] + angle = angle_between_vectors( + p2 - p1, + new_point - p2, + ) + if angle > epsilon: + points.append(new_point) + else: + points[-1] = new_point + trajectory.set_points_as_corners(points) + + trajectory.add_updater(update_trajectory) + return trajectory + + def get_ps_point_change_anim(self, d1, d2): + b1 = self.block1 + ps_speed = np.sqrt(b1.mass) * abs(b1.velocity) + curr_d1, curr_d2 = self.get_ds() + distance = get_norm([curr_d1 - d1, curr_d2 - d2]) + return ApplyMethod( + self.ps_point.move_to, + self.ds_to_point(d1, d2), + run_time=(distance / ps_speed), + rate_func=None, + ) + # Mobject getters def get_floor(self): floor = self.floor = Line( @@ -175,12 +316,12 @@ class PositionPhaseSpaceScene(Scene): stroke_color=WHITE, stroke_width=3, ) - floor.move_to(self.get_corner(), LEFT) + floor.move_to(self.get_floor_wall_corner(), LEFT) return floor def get_wall(self): wall = self.wall = Wall(**self.wall_config) - wall.move_to(self.get_corner(), DR) + wall.move_to(self.get_floor_wall_corner(), DR) return wall def get_blocks(self): @@ -188,7 +329,7 @@ class PositionPhaseSpaceScene(Scene): for n in [1, 2]: config = getattr(self, "block{}_config".format(n)) block = Block(**config) - block.move_to(self.get_corner(), DL) + block.move_to(self.get_floor_wall_corner(), DL) block.shift(config["distance"] * RIGHT) block.label.move_to(block) block.label.set_fill(BLACK) @@ -203,9 +344,32 @@ class PositionPhaseSpaceScene(Scene): axes.shift( self.axes_center - axes.coords_to_point(0, 0) ) - axes.add(self.get_axes_labels(axes)) + axes.labels = self.get_axes_labels(axes) + axes.add(axes.labels) + axes.added_lines = self.get_added_axes_lines(axes) + axes.add(axes.added_lines) return axes + def get_added_axes_lines(self, axes): + c2p = axes.coords_to_point + y_lines = VGroup(*[ + Line( + c2p(0, 0), c2p(0, axes.y_max + 1), + ).move_to(c2p(x, 0), DOWN) + for x in np.arange(0, axes.x_max) + ]) + x_lines = VGroup(*[ + Line( + c2p(0, 0), c2p(axes.x_max, 0), + ).move_to(c2p(0, y), LEFT) + for y in np.arange(0, axes.y_max) + ]) + line_groups = VGroup(x_lines, y_lines) + for lines in line_groups: + lines.set_stroke(BLUE, 1, 0.5) + lines[1::2].set_stroke(width=0.5, opacity=0.25) + return line_groups + def get_axes_labels(self, axes): x_label = TexMobject("x = ", "d_1") y_label = TexMobject("y = ", "d_2") @@ -278,6 +442,7 @@ class PositionPhaseSpaceScene(Scene): color = GREEN if n == 1 else RED label[0].set_color_by_tex("d_", color) label.scale(0.7) + label.set_stroke(BLACK, 3, background=True) def update_value(label): lhs, rhs = label @@ -310,17 +475,15 @@ class PositionPhaseSpaceScene(Scene): def get_d_brace(self, get_right_point): line = Line(LEFT, RIGHT).set_width(6) - brace = Brace(line, UP) - def update_brace(brace): + def get_brace(): right_point = get_right_point() left_point = np.array(right_point) left_point[0] = self.wall_x line.put_start_and_end_on(left_point, right_point) - brace.match_width(line, stretch=True) - brace.next_to(line, UP, buff=0) + return Brace(line, UP, buff=SMALL_BUFF) - brace.add_updater(update_brace) + brace = updating_mobject_from_func(get_brace) return brace def get_d1_brace(self): @@ -344,40 +507,44 @@ class PositionPhaseSpaceScene(Scene): center[0] = nip.get_center()[0] nip.rotate(PI, about_point=center) - def get_brace_d_label(self, n, get_d, brace, vect): + def get_brace_d_label(self, n, get_d, brace, vect, buff): label = self.get_d_label(n, get_d) label.add_updater( - lambda m: m.next_to(brace, vect, 0) + lambda m: m.next_to(brace, vect, buff) ) return label def get_d1_label(self): self.d1_label = self.get_brace_d_label( - 1, self.get_d1, self.d1_brace, UP + 1, self.get_d1, self.d1_brace, UP, SMALL_BUFF, ) return self.d1_label def get_d2_label(self): self.d2_label = self.get_brace_d_label( - 2, self.get_d2, self.d2_brace, UP + 2, self.get_d2, self.d2_brace, UP, 0 ) return self.d2_label def get_d1_eq_d2_line(self): start = self.ds_to_point(0, 0) end = self.ds_to_point(15, 15) - self.d1_eq_d2_line = DashedLine(start, end) - self.d1_eq_d2_line.set_stroke(WHITE, 2) + line = self.d1_eq_d2_line = self.mirror_line_class(start, end) + line.set_style(**self.mirror_line_style) + line.set_color(PINK) 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) + point = interpolate( + line.get_start(), line.get_end(), + 0.7, + ) + label.next_to(point, DR, SMALL_BUFF) + label.set_stroke(BLACK, 3, background=True) + label.match_color(line) self.d1_eq_d2_label = label return label @@ -385,8 +552,8 @@ class PositionPhaseSpaceScene(Scene): 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) + self.d2_eq_w2_line = self.mirror_line_class(start, end) + self.d2_eq_w2_line.set_style(**self.mirror_line_style) return self.d2_eq_w2_line def get_d2_eq_w2_label(self): @@ -394,7 +561,7 @@ class PositionPhaseSpaceScene(Scene): 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 + self.d2_eq_w2_label = label return label def get_time_tracker(self): @@ -402,63 +569,495 @@ class PositionPhaseSpaceScene(Scene): 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, + "block1_config": { + "velocity": 1.5, + }, + "slide_wait_time": 30, } + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + ) + def construct(self): self.show_coordinates() self.show_xy_line() self.let_process_play_out() - - self.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 - # )) + self.show_w2_line() def show_coordinates(self): - pass + ps_point = self.ps_point + axes = self.axes + + self.play(Write(axes.added_lines)) + self.play(FadeInFromLarge(self.ps_dot, scale_factor=10)) + self.play( + ShowCreation(self.x_line), + GrowFromPoint( + self.d1_brace, + self.d1_brace.get_left(), + ), + Indicate(axes.labels[0]), + ) + self.play( + FadeInFromDown(self.ps_d1_label), + FadeInFromDown(self.d1_label), + ) + self.play(ps_point.shift, 0.5 * LEFT) + self.play(ps_point.shift, 0.5 * RIGHT) + self.wait() + self.play( + ShowCreation(self.y_line), + GrowFromPoint( + self.d2_brace, + self.d2_brace.get_left(), + ), + Indicate(axes.labels[1]), + ) + self.play( + FadeInFromDown(self.ps_d2_label), + FadeInFromDown(self.d2_label), + ) + self.play(ps_point.shift, 0.5 * UP) + self.play(ps_point.shift, 0.5 * DOWN) + self.wait() + self.play(Rotating( + ps_point, + about_point=ps_point.get_location() + 0.5 * RIGHT, + run_time=3, + rate_func=smooth, + )) + self.wait() def show_xy_line(self): - pass + ps_point = self.ps_point + ps_point.save_state() + d1, d2 = self.point_to_ds(ps_point.get_location()) + + xy_line = self.d1_eq_d2_line + xy_label = self.d1_eq_d2_label + xy_line.set_stroke(YELLOW) + xy_label.set_color(YELLOW) + + self.play( + ShowCreation(xy_line), + Write(xy_label), + ) + self.play( + ps_point.move_to, self.ds_to_point(d2, d2), + run_time=3 + ) + self.wait() + for d in [3, 7]: + self.play( + ps_point.move_to, self.ds_to_point(d, d), + run_time=2 + ) + self.wait() + self.play(ps_point.restore) + self.wait() def let_process_play_out(self): - pass + self.begin_sliding() + sliding_trajectory = self.get_continually_building_trajectory() + self.add(sliding_trajectory, self.ps_dot) + self.wait(self.slide_wait_time) + self.end_sliding() + + def show_w2_line(self): + line = self.d2_eq_w2_line + label = self.d2_eq_w2_label + + self.play(ShowCreation(line)) + self.play(FadeInFromDown(label)) + self.wait() -class EqualMassCase(IntroducePositionPhaseSpace): +class SpecialShowPassingFlash(ShowPassingFlash): + CONFIG = { + "max_time_width": 0.1, + } + + def get_bounds(self, alpha): + tw = self.time_width + max_tw = self.max_time_width + upper = interpolate(0, 1 + max_tw, alpha) + lower = upper - tw + upper = min(upper, 1) + lower = max(lower, 0) + return (lower, upper) + + +class EqualMassCase(PositionPhaseSpaceScene): + CONFIG = { + "block1_config": { + "mass": 1, + "width": 1, + "velocity": 1.5, + }, + "rescale_coordinates": False, + } + + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.ps_dot, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.d2_eq_w2_label, + ) + def construct(self): + self.show_same_mass() self.show_first_point() self.up_to_first_collision() - self.ask_about_momentum_transfer() self.up_to_second_collision() self.up_to_third_collision() + self.fade_distance_indicators() + self.show_beam_bouncing() + + def show_same_mass(self): + blocks = self.blocks + self.play(LaggedStart( + Indicate, blocks, + lag_ratio=0.8, + run_time=1, + )) + def show_first_point(self): - pass + ps_dot = self.ps_dot + ps_point = self.ps_point + d1, d2 = self.get_ds() + + self.play(FocusOn(ps_dot)) + self.play(ShowCreationThenFadeOut( + Circle(color=RED).replace(ps_dot).scale(2), + run_time=1 + )) + self.wait() + self.play( + ps_point.move_to, self.ds_to_point(d1 - 1, d2), + rate_func=wiggle, + run_time=3, + ) + # self.play(ps_point.move_to, self.ds_to_point(d1, d2)) + self.wait() def up_to_first_collision(self): - pass + ps_point = self.ps_point + d1, d2 = self.get_ds() + block1 = self.block1 + block2 = self.block2 + xy_line = self.d1_eq_d2_line + xy_line_label = self.d1_eq_d2_label - def ask_about_momentum_transfer(self): - pass + block_arrow = Vector(LEFT, color=RED) + block_arrow.block = block1 + block_arrow.add_updater( + lambda m: m.shift( + m.block.get_center() - m.get_start() + ) + ) + ps_arrow = Vector(LEFT, color=RED) + ps_arrow.next_to(ps_point, DL, buff=SMALL_BUFF) + + block_labels = VGroup(block1.label, block2.label) + block_label_copies = block_labels.copy() + + def update_bl_copies(bl_copies): + for bc, b in zip(bl_copies, block_labels): + bc.move_to(b) + block_label_copies.add_updater(update_bl_copies) + + trajectory = self.get_continually_building_trajectory() + + self.add(block_arrow, ps_arrow, block_label_copies) + self.play( + GrowArrow(block_arrow), + GrowArrow(ps_arrow), + ) + self.add(trajectory) + self.play(self.get_ps_point_change_anim(d2, d2)) + block_arrow.block = block2 + ps_arrow.rotate(90 * DEGREES) + ps_arrow.next_to(ps_point, DR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(ps_point), + Flash(block1.get_left()), + self.get_ps_point_change_anim(d2, d2 - 1) + ) + self.play( + ShowPassingFlash( + xy_line.copy().set_stroke(YELLOW, 3) + ), + Indicate(xy_line_label), + ) + + trajectory.suspend_updating() + self.wait() + + self.ps_arrow = ps_arrow + self.block_arrow = block_arrow def up_to_second_collision(self): - pass + trajectory = self.continually_building_trajectory + ps_point = self.ps_point + ps_arrow = self.ps_arrow + block_arrow = self.block_arrow + + d1, d2 = self.get_ds() + w2 = self.block2.get_width() + + trajectory.resume_updating() + self.play(self.get_ps_point_change_anim(d1, w2)) + block_arrow.rotate(PI) + ps_arrow.rotate(PI) + ps_arrow.next_to(ps_point, UR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(self.block2.get_left()), + Flash(ps_point), + self.get_ps_point_change_anim(d1, w2 + 1) + ) + + trajectory.suspend_updating() + self.wait() def up_to_third_collision(self): - pass + trajectory = self.continually_building_trajectory + ps_point = self.ps_point + ps_arrow = self.ps_arrow + block_arrow = self.block_arrow + d1, d2 = self.get_ds() + + trajectory.resume_updating() + self.play(self.get_ps_point_change_anim(d1, d1)) + block_arrow.block = self.block1 + ps_arrow.rotate(-90 * DEGREES) + ps_arrow.next_to(ps_point, DR, SMALL_BUFF) + self.add_sound(self.clack_sound) + self.play( + Flash(self.block2.get_left()), + Flash(ps_point.get_location()), + self.get_ps_point_change_anim(d1 + 10, d1) + ) + trajectory.suspend_updating() + + def fade_distance_indicators(self): + trajectory = self.continually_building_trajectory + self.play( + trajectory.set_stroke, {"width": 1}, + *map(FadeOut, [ + self.ps_arrow, + self.block_arrow, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + ]) + ) + trajectory.clear_updaters() + + def show_beam_bouncing(self): + d1, d2 = self.get_ds() + d1 = int(d1) + d2 = int(d2) + w2 = self.block2.get_width() + ps_point = self.ps_point + + points = [] + while d1 > d2: + points.append(self.ds_to_point(d1, d2)) + d1 -= 1 + while d2 >= int(w2): + points.append(self.ds_to_point(d1, d2)) + d2 -= 1 + points += list(reversed(points))[1:] + trajectory = VMobject() + trajectory.set_points_as_corners(points) + flashes = [ + SpecialShowPassingFlash( + trajectory.copy().set_stroke(YELLOW, width=6 - n), + time_width=(0.01 * n), + max_time_width=0.05, + remover=True + ) + for n in np.arange(0, 6, 0.25) + ] + flash_mob = flashes[0].mobject # Lol + + def update_ps_point_from_flas_mob(ps_point): + if len(flash_mob.points) > 0: + ps_point.move_to(flash_mob.points[-1]) + else: + ps_point.move_to(trajectory.points[0]) + + # Mirror words + xy_line = self.d1_eq_d2_line + w2_line = self.d2_eq_w2_line + lines = VGroup(xy_line, w2_line) + for line in lines: + word = TextMobject("Mirror") + word.next_to(ORIGIN, UP, SMALL_BUFF) + word.rotate(line.get_angle(), about_point=ORIGIN) + word.shift(line.get_center()) + line.word = word + + for line in lines: + line.set_stroke(LIGHT_GREY) + line.set_sheen(1, LEFT) + self.play( + Write(line.word), + line.set_sheen, 1, RIGHT, + line.set_stroke, {"width": 2}, + run_time=1, + ) + + # TODO, clacks? + for x in range(3): + self.play( + UpdateFromFunc( + ps_point, + update_ps_point_from_flas_mob, + ), + *flashes, + run_time=3, + rate_func=None, + ) + self.wait() + + +class FailedAngleRelation(PositionPhaseSpaceScene): + CONFIG = { + "block1_config": { + "distance": 10, + "velocity": -1.5, + }, + "block2_config": { + "distance": 5, + }, + "rescale_coordinates": False, + } + + def setup(self): + super().setup() + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.ps_dot, + self.x_line, + self.y_line, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.d2_eq_w2_label, + ) + + def construct(self): + self.show_first_collision() + self.show_angles() + + def show_first_collision(self): + ps_point = self.ps_point + trajectory = self.get_continually_building_trajectory() + trajectory.set_stroke(YELLOW, 2) + + self.add(ps_point, trajectory) + self.begin_sliding() + self.wait_until(lambda: self.get_ds()[1] < 2) + self.end_sliding() + trajectory.suspend_updating() + + self.trajectory = trajectory + + def show_angles(self): + trajectory = self.trajectory + arcs = self.get_arcs(trajectory) + equation = self.get_word_equation() + equation.next_to( + trajectory.points[0], UR, MED_SMALL_BUFF, + index_of_submobject_to_align=0, + ) + + for arc in arcs: + line = Line(ORIGIN, RIGHT) + line.set_stroke(WHITE, 2) + line.rotate(arc.start_angle) + line.shift(arc.arc_center - line.get_start()) + arc.line = line + + self.play(LaggedStart( + FadeInFromDown, + VGroup(*reversed(equation)), + lag_ratio=0.75, + )) + for arc in arcs: + # TODO, add arrows + self.play( + ShowCreation(arc), + arc.line.rotate, arc.angle, + {"about_point": arc.line.get_start()}, + UpdateFromAlphaFunc( + arc.line, + lambda m, a: m.set_stroke( + opacity=(there_and_back(a)**0.5) + ) + ), + run_time=2, + ) + + # + def get_arcs(self, trajectory): + p0, p1, p2 = trajectory.get_anchors()[1:4] + arc_config = { + "stroke_color": WHITE, + "stroke_width": 2, + "radius": 0.5, + "arc_center": p1, + } + arc1 = Arc( + start_angle=0, + angle=45 * DEGREES, + **arc_config + ) + a2_start = angle_of_vector(DL) + a2_angle = angle_between_vectors((p2 - p1), DL) + arc2 = Arc( + start_angle=a2_start, + angle=a2_angle, + **arc_config + ) + return VGroup(arc1, arc2) + + def get_word_equation(self): + result = VGroup( + TextMobject("Angle of incidence"), + TexMobject("\\ne").rotate(90 * DEGREES), + TextMobject("Angle of refraction") + ) + result.arrange_submobjects(DOWN) + return result From b7f733823feb4c80344e4c9f13e89b84f038c2d3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 30 Jan 2019 11:19:24 -0800 Subject: [PATCH 15/18] Changed default for TeacherStudentScene.student_says --- manimlib/for_3b1b_videos/pi_creature_scene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/for_3b1b_videos/pi_creature_scene.py b/manimlib/for_3b1b_videos/pi_creature_scene.py index e439f88d..86507111 100644 --- a/manimlib/for_3b1b_videos/pi_creature_scene.py +++ b/manimlib/for_3b1b_videos/pi_creature_scene.py @@ -298,6 +298,8 @@ class TeacherStudentsScene(PiCreatureScene): "raise_left_hand", ]) kwargs["target_mode"] = target_mode + if "bubble_kwargs" not in kwargs: + kwargs["bubble_kwargs"] = {"direction": LEFT} student = self.get_students()[kwargs.get("student_index", 2)] return self.pi_creature_says( student, *content, **kwargs From 4b15079318df5776e8364d2a75e7d1238ecd36dd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 30 Jan 2019 11:19:58 -0800 Subject: [PATCH 16/18] Mobject should update when Mobject.resume_updating is called --- manimlib/mobject/mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 1f14cbcd..8224c2b2 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -198,6 +198,7 @@ class Mobject(Container): if recursive: for submob in self.submobjects: submob.resume_updating(recursive) + self.update(dt=0, recursive=recursive) return self # Transforming operations From 4a5b611afffad34a8825f9ff37e1cd7f73dc415b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 30 Jan 2019 11:20:17 -0800 Subject: [PATCH 17/18] Nix setup_bases --- manimlib/scene/scene.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 1f7f9bc1..1ec7e674 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -69,10 +69,6 @@ class Scene(Container): def tear_down(self): pass - def setup_bases(self): - for base in self.__class__.__bases__: - base.setup(self) - def construct(self): pass # To be implemented in subclasses From d4702327496c8a737dc36113b39f5edfed4bb93e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 30 Jan 2019 11:20:24 -0800 Subject: [PATCH 18/18] Several more PositionPhaseSpaceScene scenes --- active_projects/clacks/all_s2_scenes.py | 10 +- .../clacks/solution2/pi_creature_scenes.py | 26 ++ .../clacks/solution2/position_phase_space.py | 406 +++++++++++++++--- .../clacks/solution2/simple_scenes.py | 16 +- .../clacks/solution2/wordy_scenes.py | 4 + 5 files changed, 390 insertions(+), 72 deletions(-) diff --git a/active_projects/clacks/all_s2_scenes.py b/active_projects/clacks/all_s2_scenes.py index 579cdeb3..64d949d8 100644 --- a/active_projects/clacks/all_s2_scenes.py +++ b/active_projects/clacks/all_s2_scenes.py @@ -10,11 +10,19 @@ OUTPUT_DIRECTORY = "clacks_solution2" ALL_SCENE_CLASSES = [ block_collision_scenes.IntroducePreviousTwoVideos, block_collision_scenes.PreviousTwoVideos, - simple_scenes.TwoSolutionsWrapper, wordy_scenes.ConnectionToOptics, pi_creature_scenes.OnAnsweringTwice, + simple_scenes.LastVideoWrapper, position_phase_space.IntroducePositionPhaseSpace, + position_phase_space.UnscaledPositionPhaseSpaceMass100, position_phase_space.EqualMassCase, pi_creature_scenes.AskAboutEqualMassMomentumTransfer, position_phase_space.FailedAngleRelation, + position_phase_space.UnscaledPositionPhaseSpaceMass10, + pi_creature_scenes.ComplainAboutRelevanceOfAnalogy, + simple_scenes.LastVideoWrapper, + position_phase_space.RescaleCoordinates, + wordy_scenes.ConnectionToOpticsTransparent, + position_phase_space.RescaleCoordinatesMass16, + position_phase_space.RescaleCoordinatesMass64, ] diff --git a/active_projects/clacks/solution2/pi_creature_scenes.py b/active_projects/clacks/solution2/pi_creature_scenes.py index a272d439..0189ea5f 100644 --- a/active_projects/clacks/solution2/pi_creature_scenes.py +++ b/active_projects/clacks/solution2/pi_creature_scenes.py @@ -50,3 +50,29 @@ class OnAnsweringTwice(TeacherStudentsScene): class AskAboutEqualMassMomentumTransfer(TeacherStudentsScene): def construct(self): pass + + +class ComplainAboutRelevanceOfAnalogy(TeacherStudentsScene): + def construct(self): + self.student_says( + "Why would \\\\ you care", + target_mode="maybe" + ) + self.change_student_modes( + "angry", "sassy", "maybe", + added_anims=[self.teacher.change, "guilty"] + ) + self.wait(2) + self.play( + self.teacher.change, "raise_right_hand", + self.get_student_changes( + "pondering", "erm", "pondering", + look_at_arg=self.hold_up_spot, + ), + RemovePiCreatureBubble(self.students[2]) + ) + self.play( + self.students[2].change, "thinking", + self.hold_up_spot + UP, + ) + self.wait(3) diff --git a/active_projects/clacks/solution2/position_phase_space.py b/active_projects/clacks/solution2/position_phase_space.py index 60398ad1..2df54269 100644 --- a/active_projects/clacks/solution2/position_phase_space.py +++ b/active_projects/clacks/solution2/position_phase_space.py @@ -46,15 +46,22 @@ class PositionPhaseSpaceScene(Scene): "background_stroke_color": BLACK, "radius": 0.05, }, + "ps_d2_label_vect": RIGHT, "clack_sound": "clack", "mirror_line_class": Line, "mirror_line_style": { "stroke_color": WHITE, "stroke_width": 1, }, + "d1_eq_e2_line_color": GREEN_SCREEN, + "trajectory_style": { + "stroke_color": YELLOW, + "stroke_width": 2, + } } def setup(self): + self.total_sliding_time = 0 self.all_items = [ self.get_floor(), self.get_wall(), @@ -222,14 +229,16 @@ class PositionPhaseSpaceScene(Scene): pass # TODO def tie_ps_point_to_time_tracker(self): - time_tracker = self.get_time_tracker() + time_tracker = self.get_time_tracker( + time=self.total_sliding_time + ) def update_ps_point(p): time = time_tracker.get_value() ds = self.time_to_ds(time) p.move_to(self.ds_to_point(*ds)) self.ps_point.add_updater(update_ps_point) - self.add(time_tracker) + self.add(time_tracker, self.ps_point) def add_clack_flashes(self): clack_data = self.get_clack_data() @@ -249,29 +258,10 @@ class PositionPhaseSpaceScene(Scene): self.ps_flashes, ) - def begin_sliding(self): - self.tie_ps_point_to_time_tracker() - self.add_clack_flashes() - - def end_sliding(self): - self.ps_point.clear_updaters() - self.remove(self.time_tracker) - for attr in ["block_flashes", "ps_flashes"]: - if hasattr(self, attr): - self.remove(getattr(self, attr)) - total_time = self.time_tracker.get_value() - for time in self.clack_times: - if time < total_time: - offset = total_time - time - self.add_sound( - "clack", - time_offset=-offset, - ) - def get_continually_building_trajectory(self): trajectory = VMobject() - self.continually_building_trajectory = trajectory - trajectory.set_stroke(YELLOW, 1) + self.trajectory = trajectory + trajectory.set_style(**self.trajectory_style) def get_point(): return np.array(self.ps_point.get_location()) @@ -296,16 +286,58 @@ class PositionPhaseSpaceScene(Scene): trajectory.add_updater(update_trajectory) return trajectory - def get_ps_point_change_anim(self, d1, d2): + def begin_sliding(self, show_trajectory=True): + self.tie_ps_point_to_time_tracker() + self.add_clack_flashes() + if show_trajectory: + if hasattr(self, "trajectory"): + self.trajectory.resume_updating() + else: + self.add(self.get_continually_building_trajectory()) + + def end_sliding(self): + self.ps_point.clear_updaters() + self.remove(self.time_tracker) + to_remove = ["block_flashes", "ps_flashes"] + for attr in to_remove: + if hasattr(self, attr): + self.remove(getattr(self, attr)) + if hasattr(self, "trajectory"): + self.trajectory.suspend_updating() + total_time = self.time_tracker.get_value() + self.total_sliding_time += total_time + for time in self.clack_times: + if time < total_time: + offset = total_time - time + self.add_sound( + "clack", + time_offset=-offset, + ) + + def slide(self, time, stop_condition=None): + self.begin_sliding() + self.wait(time, stop_condition) + self.end_sliding() + + def slide_until(self, stop_condition, max_time=60): + self.slide(max_time, stop_condition=stop_condition) + + def get_ps_point_change_anim(self, d1, d2, **added_kwargs): b1 = self.block1 ps_speed = np.sqrt(b1.mass) * abs(b1.velocity) curr_d1, curr_d2 = self.get_ds() distance = get_norm([curr_d1 - d1, curr_d2 - d2]) + + # Default + kwargs = { + "run_time": (distance / ps_speed), + "rate_func": None, + } + kwargs.update(added_kwargs) return ApplyMethod( self.ps_point.move_to, self.ds_to_point(d1, d2), - run_time=(distance / ps_speed), - rate_func=None, + **kwargs ) # Mobject getters @@ -333,7 +365,7 @@ class PositionPhaseSpaceScene(Scene): block.shift(config["distance"] * RIGHT) block.label.move_to(block) block.label.set_fill(BLACK) - block.label.set_stroke(WHITE, 3, background=True) + block.label.set_stroke(WHITE, 1, background=True) self.blocks.add(block) self.block1, self.block2 = blocks return blocks @@ -352,17 +384,21 @@ class PositionPhaseSpaceScene(Scene): def get_added_axes_lines(self, axes): c2p = axes.coords_to_point + x_mult = y_mult = 1 + if self.rescale_coordinates: + x_mult = np.sqrt(self.block1.mass) + y_mult = np.sqrt(self.block2.mass) y_lines = VGroup(*[ Line( - c2p(0, 0), c2p(0, axes.y_max + 1), + c2p(0, 0), c2p(0, axes.y_max * y_mult + 1), ).move_to(c2p(x, 0), DOWN) - for x in np.arange(0, axes.x_max) + for x in np.arange(0, axes.x_max) * x_mult ]) x_lines = VGroup(*[ Line( - c2p(0, 0), c2p(axes.x_max, 0), + c2p(0, 0), c2p(axes.x_max * x_mult, 0), ).move_to(c2p(0, y), LEFT) - for y in np.arange(0, axes.y_max) + for y in np.arange(0, axes.y_max) * y_mult ]) line_groups = VGroup(x_lines, y_lines) for lines in line_groups: @@ -370,11 +406,13 @@ class PositionPhaseSpaceScene(Scene): lines[1::2].set_stroke(width=0.5, opacity=0.25) return line_groups - def get_axes_labels(self, axes): + def get_axes_labels(self, axes, with_sqrts=None): + if with_sqrts is None: + with_sqrts = self.rescale_coordinates x_label = TexMobject("x = ", "d_1") y_label = TexMobject("y = ", "d_2") labels = VGroup(x_label, y_label) - if self.rescale_coordinates: + if with_sqrts: additions = map(TexMobject, [ "\\sqrt{m_1}", "\\sqrt{m_2}" ]) @@ -469,7 +507,8 @@ class PositionPhaseSpaceScene(Scene): 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, + 2, self.get_d2, self.y_line, + self.ps_d2_label_vect, ) return self.ps_d2_label @@ -531,7 +570,7 @@ class PositionPhaseSpaceScene(Scene): end = self.ds_to_point(15, 15) line = self.d1_eq_d2_line = self.mirror_line_class(start, end) line.set_style(**self.mirror_line_style) - line.set_color(PINK) + line.set_color(self.d1_eq_e2_line_color) return self.d1_eq_d2_line def get_d1_eq_d2_label(self): @@ -543,15 +582,16 @@ class PositionPhaseSpaceScene(Scene): 0.7, ) label.next_to(point, DR, SMALL_BUFF) - label.set_stroke(BLACK, 3, background=True) label.match_color(line) + label.set_stroke(BLACK, 5, background=True) self.d1_eq_d2_label = label return label def get_d2_eq_w2_line(self): w2 = self.block2.width start = self.ds_to_point(0, w2) - end = self.ds_to_point(30, w2) + end = np.array(start) + end[0] = FRAME_WIDTH / 2 self.d2_eq_w2_line = self.mirror_line_class(start, end) self.d2_eq_w2_line.set_style(**self.mirror_line_style) return self.d2_eq_w2_line @@ -564,8 +604,8 @@ class PositionPhaseSpaceScene(Scene): self.d2_eq_w2_label = label return label - def get_time_tracker(self): - time_tracker = self.time_tracker = ValueTracker(0) + def get_time_tracker(self, time=0): + time_tracker = self.time_tracker = ValueTracker(time) time_tracker.add_updater( lambda m, dt: m.increment_value(dt) ) @@ -647,8 +687,6 @@ class IntroducePositionPhaseSpace(PositionPhaseSpaceScene): xy_line = self.d1_eq_d2_line xy_label = self.d1_eq_d2_label - xy_line.set_stroke(YELLOW) - xy_label.set_color(YELLOW) self.play( ShowCreation(xy_line), @@ -716,15 +754,15 @@ class EqualMassCase(PositionPhaseSpaceScene): self.wall, self.blocks, self.axes, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.d2_eq_w2_label, self.ps_dot, self.x_line, self.y_line, self.ps_d1_label, self.ps_d2_label, - self.d1_eq_d2_line, - self.d1_eq_d2_label, - self.d2_eq_w2_line, - self.d2_eq_w2_label, ) def construct(self): @@ -822,7 +860,7 @@ class EqualMassCase(PositionPhaseSpaceScene): self.block_arrow = block_arrow def up_to_second_collision(self): - trajectory = self.continually_building_trajectory + trajectory = self.trajectory ps_point = self.ps_point ps_arrow = self.ps_arrow block_arrow = self.block_arrow @@ -846,7 +884,7 @@ class EqualMassCase(PositionPhaseSpaceScene): self.wait() def up_to_third_collision(self): - trajectory = self.continually_building_trajectory + trajectory = self.trajectory ps_point = self.ps_point ps_arrow = self.ps_arrow block_arrow = self.block_arrow @@ -866,7 +904,7 @@ class EqualMassCase(PositionPhaseSpaceScene): trajectory.suspend_updating() def fade_distance_indicators(self): - trajectory = self.continually_building_trajectory + trajectory = self.trajectory self.play( trajectory.set_stroke, {"width": 1}, *map(FadeOut, [ @@ -959,6 +997,9 @@ class FailedAngleRelation(PositionPhaseSpaceScene): "distance": 5, }, "rescale_coordinates": False, + "trajectory_style": { + "stroke_width": 2, + } } def setup(self): @@ -982,17 +1023,7 @@ class FailedAngleRelation(PositionPhaseSpaceScene): self.show_angles() def show_first_collision(self): - ps_point = self.ps_point - trajectory = self.get_continually_building_trajectory() - trajectory.set_stroke(YELLOW, 2) - - self.add(ps_point, trajectory) - self.begin_sliding() - self.wait_until(lambda: self.get_ds()[1] < 2) - self.end_sliding() - trajectory.suspend_updating() - - self.trajectory = trajectory + self.slide_until(lambda: self.get_ds()[1] < 2) def show_angles(self): trajectory = self.trajectory @@ -1010,13 +1041,30 @@ class FailedAngleRelation(PositionPhaseSpaceScene): line.shift(arc.arc_center - line.get_start()) arc.line = line - self.play(LaggedStart( - FadeInFromDown, - VGroup(*reversed(equation)), - lag_ratio=0.75, - )) + arc1, arc2 = arcs + arc1.arrow = Arrow( + equation[0].get_left(), arc1.get_right(), + buff=SMALL_BUFF, + color=WHITE, + path_arc=0, + ) + arc2.arrow = Arrow( + equation[2].get_corner(DL), + arc2.get_left(), + use_rectangular_stem=False, + path_arc=-120 * DEGREES, + buff=SMALL_BUFF, + ) + arc2.arrow.pointwise_become_partial(arc.arrow, 0, 0.95) + + arc1.word = equation[0] + arc2.word = equation[1:] + for arc in arcs: - # TODO, add arrows + self.play( + FadeInFrom(arc.word, LEFT), + GrowArrow(arc.arrow, path_arc=arc.arrow.path_arc), + ) self.play( ShowCreation(arc), arc.line.rotate, arc.angle, @@ -1027,7 +1075,6 @@ class FailedAngleRelation(PositionPhaseSpaceScene): opacity=(there_and_back(a)**0.5) ) ), - run_time=2, ) # @@ -1060,4 +1107,231 @@ class FailedAngleRelation(PositionPhaseSpaceScene): TextMobject("Angle of refraction") ) result.arrange_submobjects(DOWN) + result.set_stroke(BLACK, 5, background=True) return result + + +class UnscaledPositionPhaseSpaceMass10(FailedAngleRelation): + CONFIG = { + "block1_config": { + "mass": 10 + }, + "wait_time": 25, + } + + def construct(self): + self.slide(self.wait_time) + + +class UnscaledPositionPhaseSpaceMass100(UnscaledPositionPhaseSpaceMass10): + CONFIG = { + "block1_config": { + "mass": 100 + } + } + + +class RescaleCoordinates(PositionPhaseSpaceScene, MovingCameraScene): + CONFIG = { + "rescale_coordinates": False, + "ps_d2_label_vect": LEFT, + "axes_center": 6 * LEFT + 0.65 * DOWN, + "block1_config": {"distance": 7}, + "wait_time": 30, + } + + def setup(self): + PositionPhaseSpaceScene.setup(self) + MovingCameraScene.setup(self) + self.add( + self.floor, + self.wall, + self.blocks, + self.axes, + self.d1_eq_d2_line, + self.d1_eq_d2_label, + self.d2_eq_w2_line, + self.ps_dot, + self.x_line, + self.y_line, + self.ps_d1_label, + self.ps_d2_label, + self.d1_brace, + self.d2_brace, + self.d1_label, + self.d2_label, + ) + + def construct(self): + self.show_rescaling() + self.comment_on_ugliness() + self.put_into_frame() + + def show_rescaling(self): + axes = self.axes + blocks = self.blocks + to_stretch = VGroup( + axes.added_lines, + self.d1_eq_d2_line, + self.ps_point, + ) + m1 = self.block1.mass + new_axes_labels = self.get_axes_labels(axes, with_sqrts=True) + + # Show label + def show_label(index, block, vect): + self.play( + ShowCreationThenFadeAround(axes.labels[index]) + ) + self.play( + Transform( + axes.labels[index], + new_axes_labels[index][:2], + ), + GrowFromCenter(new_axes_labels[index][2]) + ) + group = VGroup( + new_axes_labels[index][2][-2:].copy(), + TexMobject("="), + block.label.copy(), + ) + group.generate_target() + group.target.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + group.target.next_to(block, vect) + group[1].scale(0) + group[1].move_to(group.target[1]) + group.target[2].set_fill(WHITE) + group.target[2].set_stroke(width=0, background=True) + self.play(MoveToTarget( + group, + rate_func=there_and_back_with_pause, + run_time=3 + )) + self.remove(group) + self.wait() + + show_label(0, self.block1, RIGHT) + + # The stretch + blocks.suspend_updating() + self.play( + ApplyMethod( + to_stretch.stretch, np.sqrt(m1), 0, + {"about_point": axes.coords_to_point(0, 0)}, + ), + self.d1_eq_d2_label.shift, 6 * RIGHT, + run_time=2, + ) + self.rescale_coordinates = True + blocks.resume_updating() + self.wait() + + # Show wiggle + d1, d2 = self.get_ds() + for new_d1 in [d1 - 2, d1]: + self.play(self.get_ps_point_change_anim( + new_d1, d2, + rate_func=smooth, + run_time=2, + )) + self.wait() + + # Change y-coord + show_label(1, self.block2, LEFT) + + axes.remove(axes.labels) + self.remove(axes.labels) + axes.labels = new_axes_labels + axes.add(axes.labels) + self.add(axes) + + def comment_on_ugliness(self): + axes = self.axes + + randy = Randolph(height=1.7) + randy.flip() + randy.next_to(self.d2_eq_w2_line, UP, buff=0) + randy.to_edge(RIGHT) + randy.change("sassy") + randy.save_state() + randy.fade(1) + randy.change("plain") + + self.play(Restore(randy)) + self.play( + PiCreatureSays( + randy, "Hideous!", + bubble_kwargs={"height": 1.5, "width": 2}, + target_mode="angry", + look_at_arg=axes.labels[0] + ) + ) + self.play(randy.look_at, axes.labels[1]) + self.play(Blink(randy)) + self.play( + RemovePiCreatureBubble( + randy, target_mode="confused" + ) + ) + self.play(Blink(randy)) + self.play(randy.look_at, axes.labels[0]) + self.wait() + self.play(FadeOut(randy)) + + def put_into_frame(self): + rect = ScreenRectangle(height=FRAME_HEIGHT + 10) + inner_rect = ScreenRectangle(height=FRAME_HEIGHT) + rect.add_subpath(inner_rect.points[::-1]) + rect.set_fill(DARK_GREY, opacity=1) + frame = self.camera_frame + + self.begin_sliding() + self.add(rect) + self.play( + frame.scale, 1.5, + {"about_point": frame.get_bottom() + UP}, + run_time=2, + ) + self.wait(self.wait_time) + self.end_sliding() + + # + def get_ds(self): + if self.rescale_coordinates: + return super().get_ds() + return ( + self.block1_config["distance"], + self.block2_config["distance"], + ) + + +class RescaleCoordinatesMass16(RescaleCoordinates): + CONFIG = { + "block1_config": { + "mass": 16, + "distance": 10, + }, + "rescale_coordinates": True, + "wait_time": 20, + } + + def construct(self): + self.put_into_frame() + + +class RescaleCoordinatesMass64(RescaleCoordinatesMass16): + CONFIG = { + "block1_config": { + "mass": 64, + "distance": 6, + }, + "block2_config": {"distance": 3}, + } + + def construct(self): + self.put_into_frame() + + +class NewSceneName(Scene): + def construct(self): + pass diff --git a/active_projects/clacks/solution2/simple_scenes.py b/active_projects/clacks/solution2/simple_scenes.py index bc3d5f02..42bfdb7b 100644 --- a/active_projects/clacks/solution2/simple_scenes.py +++ b/active_projects/clacks/solution2/simple_scenes.py @@ -1,9 +1,15 @@ from big_ol_pile_of_manim_imports import * -class TwoSolutionsWrapper(Scene): - CONFIG = { - } - +class LastVideoWrapper(Scene): def construct(self): - pass + title = TextMobject("Last time...") + title.scale(1.5) + title.to_edge(UP) + rect = ScreenRectangle(height=6) + rect.next_to(title, DOWN) + self.play( + FadeInFromDown(title), + ShowCreation(rect) + ) + self.wait() diff --git a/active_projects/clacks/solution2/wordy_scenes.py b/active_projects/clacks/solution2/wordy_scenes.py index c5c39002..c2f3a61a 100644 --- a/active_projects/clacks/solution2/wordy_scenes.py +++ b/active_projects/clacks/solution2/wordy_scenes.py @@ -199,3 +199,7 @@ class ConnectionToOptics(Scene): title, h_line, arcs, thetas, dashed_lines, v_shape, beam ) + + +class ConnectionToOpticsTransparent(ConnectionToOptics): + pass