From ba881041770fc6fea40b525de9e16abcba4ce49f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 11:50:06 -0800 Subject: [PATCH 01/13] Added set_style, get_style, and improved match_stlye --- manimlib/mobject/types/vectorized_mobject.py | 65 +++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index bb9c4511..43ba2319 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -101,8 +101,8 @@ class VMobject(Mobject): return rgbas def update_rgbas_array(self, array_name, color=None, opacity=None): - passed_color = color or BLACK - passed_opacity = opacity or 0 + passed_color = color if (color is not None) else BLACK + passed_opacity = opacity if (opacity is not None) else 0 rgbas = self.generate_rgbas_array(passed_color, passed_opacity) if not hasattr(self, array_name): setattr(self, array_name, rgbas) @@ -155,16 +155,56 @@ class VMobject(Mobject): self.set_stroke(**kwargs) return self - def set_color(self, color, family=True): - self.set_fill(color, family=family) - self.set_stroke(color, family=family) - return self + def set_style(self, + fill_color=None, + fill_opacity=None, + stroke_color=None, + stroke_width=None, + background_stroke_color=None, + background_stroke_width=None, + sheen_factor=None, + sheen_direction=None, + background_image_file=None, + family=True): + self.set_fill( + color=fill_color, + opacity=fill_opacity, + family=family + ) + self.set_stroke( + color=stroke_color, + width=stroke_width, + family=family, + ) + self.set_background_stroke( + color=background_stroke_color, + width=background_stroke_width, + family=family, + ) + if sheen_factor: + self.set_sheen( + factor=sheen_factor, + direction=sheen_direction, + family=family, + ) + if background_image_file: + self.color_using_background_image(background_image_file) + + def get_style(self): + return { + "fill_color": self.get_fill_colors(), + "fill_opacity": self.get_fill_opacities(), + "stroke_color": self.get_stroke_colors(), + "stroke_width": self.get_stroke_width(), + "background_stroke_color": self.get_stroke_colors(background=True), + "background_stroke_width": self.get_stroke_width(background=True), + "sheen_factor": self.get_sheen(), + "sheen_direction": self.get_sheen_direction(), + "background_image_file": self.get_background_image_file(), + } def match_style(self, vmobject, family=True): - for a_name in ["fill_rgbas", "stroke_rgbas", "background_stroke_rgbas"]: - setattr(self, a_name, np.array(getattr(vmobject, a_name))) - self.stroke_width = vmobject.stroke_width - self.background_stroke_width = vmobject.background_stroke_width + self.set_style(**vmobject.get_style(), family=False) if family: # Does its best to match up submobject lists, and @@ -178,6 +218,11 @@ class VMobject(Mobject): sm1.match_style(sm2) return self + def set_color(self, color, family=True): + self.set_fill(color, family=family) + self.set_stroke(color, family=family) + return self + def fade_no_recurse(self, darkness): opacity = 1.0 - darkness self.set_fill(opacity=opacity) From b7cb66fe5d8d9fc6ff36216737abd81194857db8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 12:46:52 -0800 Subject: [PATCH 02/13] Tiny PEP fix --- manimlib/extract_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 4eb519b3..d707be4d 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -40,7 +40,7 @@ def handle_scene(scene, **config): if (current_os == "Linux"): commands.append("xdg-open") - else: # Assume macOS + else: # Assume macOS commands.append("open") if config["show_file_in_finder"]: From 6c78110e0f90b56034b4869cf59b0ceaafb20f41 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 12:47:26 -0800 Subject: [PATCH 03/13] Changed default TexMobject.background_stroke_width to something less ugly in general --- manimlib/mobject/svg/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 80378ebf..10893b62 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -27,7 +27,7 @@ class SingleStringTexMobject(SVGMobject): "template_tex_file_body": TEMPLATE_TEX_FILE_BODY, "stroke_width": 0, "fill_opacity": 1.0, - "background_stroke_width": 5, + "background_stroke_width": 1, "background_stroke_color": BLACK, "should_center": True, "height": None, From 56f331e8c4213c4916af5a594c8abf0d30c75228 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 12:47:48 -0800 Subject: [PATCH 04/13] Added Scene.tear_down in symmetry with Scene.setup --- manimlib/scene/scene.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 6c72b545..de24a44b 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -84,6 +84,8 @@ class Scene(Container): self.skip_animations = False self.wait(self.frame_duration) + self.tear_down() + if self.write_to_movie: self.close_movie_pipe() print("Played a total of %d animations" % self.num_plays) @@ -96,6 +98,9 @@ class Scene(Container): """ pass + def tear_down(self): + pass + def setup_bases(self): for base in self.__class__.__bases__: base.setup(self) From 662598fcef1c3219e81deef530905dc43f5bc6a7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 12:48:05 -0800 Subject: [PATCH 05/13] Make sure rotate_vector works on 2d vectors --- manimlib/utils/space_ops.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/manimlib/utils/space_ops.py b/manimlib/utils/space_ops.py index 9b5af5bb..50efb097 100644 --- a/manimlib/utils/space_ops.py +++ b/manimlib/utils/space_ops.py @@ -53,13 +53,21 @@ def quaternion_conjugate(quaternion): def rotate_vector(vector, angle, axis=OUT): - quat = quaternion_from_angle_axis(angle, axis) - quat_inv = quaternion_conjugate(quat) - product = reduce( - quaternion_mult, - [quat, np.append(0, vector), quat_inv] - ) - return product[1:] + if len(vector) == 2: + # Use complex numbers...because why not + z = complex(*vector) * np.exp(complex(0, angle)) + return np.array([z.real, z.imag]) + elif len(vector) == 3: + # Use quaternions...because why not + quat = quaternion_from_angle_axis(angle, axis) + quat_inv = quaternion_conjugate(quat) + product = reduce( + quaternion_mult, + [quat, np.append(0, vector), quat_inv] + ) + return product[1:] + else: + raise Exception("vector must be of dimension 2 or 3") def thick_diagonal(dim, thickness=2): From 5454af3b65518b42c3860b07c0f69a03f6f1c226 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 12:48:18 -0800 Subject: [PATCH 06/13] Beginning clacks project --- active_projects/clacks.py | 139 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 active_projects/clacks.py diff --git a/active_projects/clacks.py b/active_projects/clacks.py new file mode 100644 index 00000000..0c3df6a4 --- /dev/null +++ b/active_projects/clacks.py @@ -0,0 +1,139 @@ +from big_ol_pile_of_manim_imports import * + + +class SlidingBlocks(VGroup): + CONFIG = { + "block_dynamic_configs": [ + { + "mass": 100, + "velocity": -2, + "distance": 7, + "width": 1, + }, + { + "mass": 1, + "velocity": 0, + "distance": 3, + "width": 1, + }, + ], + "block_style": { + "fill_opacity": 1, + "fill_color": (GREY, LIGHT_GREY), + "stroke_width": 3, + "stroke_color": WHITE, + } + } + + def __init__(self, surrounding_scene, **kwargs): + VGroup.__init__(self, **kwargs) + self.surrounding_scene = surrounding_scene + self.floor = surrounding_scene.floor + self.wall = surrounding_scene.wall + + for config in self.block_dynamic_configs: + self.add(self.get_block(**config)) + self.add_updater(self.__class__.update_positions) + + def get_block(self, mass, width, distance, velocity): + block = Square(side_length=width) + block.set_style(**self.block_style) + block.move_to( + self.floor.get_top()[1] * UP + + (self.wall.get_right()[0] + distance) * RIGHT, + DL, + ) + block.mass = mass + block.velocity = velocity + label = block.label = TextMobject("{}kg".format(mass)) + label.scale(0.8) + label.next_to(block, UP, SMALL_BUFF) + block.add(label) + return block + + def update_positions(self, dt): + blocks = self.submobjects + for block in blocks: + block.shift(block.velocity * dt * RIGHT) + if blocks[0].get_left()[0] < blocks[1].get_right()[0]: + # Two blocks collide + m1 = blocks[0].mass + m2 = blocks[1].mass + v1 = blocks[0].velocity + v2 = blocks[1].velocity + v_phase_space_point = np.array([ + np.sqrt(m1) * v1, -np.sqrt(m2) * v2 + ]) + angle = 2 * np.arctan(np.sqrt(m2 / m1)) + new_vps_point = rotate_vector(v_phase_space_point, angle) + for block, value in zip(blocks, new_vps_point): + block.velocity = value / np.sqrt(block.mass) + blocks[1].move_to(blocks[0].get_corner(DL), DR) + self.surrounding_scene.clack(blocks[0].get_left()) + if blocks[1].get_left()[0] < self.wall.get_right()[0]: + # Second block hits wall + blocks[1].velocity *= -1 + blocks[1].move_to(self.wall.get_corner(DR), DL) + self.surrounding_scene.clack(blocks[1].get_left()) + return self + + +class BlocksAndWallScene(Scene): + CONFIG = { + "print_clack_times": True, + "sliding_blocks_config": {}, + "floor_y": -2, + "wall_x": -5, + } + + def setup(self): + self.floor = self.get_floor() + self.wall = self.get_wall() + self.blocks = SlidingBlocks(self) + self.clack_times = [] + time_tracker = ValueTracker() + time_tracker.add_updater(lambda m, dt: m.increment_value(dt)) + self.add(time_tracker) + self.get_time = time_tracker.get_value + + self.add(self.floor, self.wall, self.blocks) + + def tear_down(self): + if self.print_clack_times: + print(self.clack_times) + + def get_wall(self): + wall = Line(self.floor_y * UP, FRAME_HEIGHT * UP / 2) + wall.shift(self.wall_x * RIGHT) + lines = VGroup(*[ + Line(ORIGIN, 0.25 * UR) + for x in range(15) + ]) + lines.set_stroke(width=1) + lines.arrange_submobjects(UP, buff=MED_SMALL_BUFF) + lines.move_to(wall, DR) + wall.add(lines) + return wall + + def get_floor(self): + floor = Line(self.wall_x * RIGHT, FRAME_WIDTH * RIGHT / 2) + floor.shift(self.floor_y * UP) + return floor + + def get_blocks(self): + pass + + def clack(self, location): + self.clack_times.append(self.get_time()) + + +# Animted scenes + +class MathAndPhysicsConspiring(Scene): + def construct(self): + pass + + +class ThreeSimpleClacks(BlocksAndWallScene): + def construct(self): + self.wait(10) From 36495478623e5692a967d578b21093608597ca9b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 14:13:52 -0800 Subject: [PATCH 07/13] Small fix to Mobject.fade_to --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 1407ca33..c679239b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -639,7 +639,7 @@ class Mobject(Container): def fade_to(self, color, alpha): for mob in self.get_family(): - mob.fade_to_no_recurse(self, color, alpha) + mob.fade_to_no_recurse(color, alpha) return self def fade_no_recurse(self, darkness): From d222c8aa14498c90f43e8da38d37c7eefdb7238f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 14:14:15 -0800 Subject: [PATCH 08/13] Add increment function to Integer --- manimlib/mobject/numbers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 89f7c393..df6ad50c 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -126,3 +126,6 @@ class Integer(DecimalNumber): CONFIG = { "num_decimal_places": 0, } + + def increment_value(self): + self.set_value(self.get_value() + 1) From 0c810b43a901160016601466e91040858759e578 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 4 Jan 2019 14:14:59 -0800 Subject: [PATCH 09/13] Most of BlocksAndWallScene is setup --- active_projects/clacks.py | 156 +++++++++++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 26 deletions(-) diff --git a/active_projects/clacks.py b/active_projects/clacks.py index 0c3df6a4..433fd4c0 100644 --- a/active_projects/clacks.py +++ b/active_projects/clacks.py @@ -3,25 +3,24 @@ from big_ol_pile_of_manim_imports import * class SlidingBlocks(VGroup): CONFIG = { - "block_dynamic_configs": [ - { - "mass": 100, - "velocity": -2, - "distance": 7, - "width": 1, - }, - { - "mass": 1, - "velocity": 0, - "distance": 3, - "width": 1, - }, - ], + "block1_config": { + "mass": 1, + "velocity": -2, + "distance": 7, + "width": 1, + }, + "block2_config": { + "mass": 1, + "velocity": 0, + "distance": 3, + "width": 1, + }, "block_style": { "fill_opacity": 1, "fill_color": (GREY, LIGHT_GREY), "stroke_width": 3, "stroke_color": WHITE, + "sheen_direction": UL, } } @@ -31,27 +30,69 @@ class SlidingBlocks(VGroup): self.floor = surrounding_scene.floor self.wall = surrounding_scene.wall - for config in self.block_dynamic_configs: - self.add(self.get_block(**config)) + self.block1 = self.get_block(**self.block1_config) + self.block2 = self.get_block(**self.block2_config) + self.phase_space_point_tracker = self.get_phase_space_point_tracker() + self.add( + self.block1, self.block2, + self.phase_space_point_tracker, + ) self.add_updater(self.__class__.update_positions) + # From here, there's enough information to create + # a list of all clack times and locations + def get_block(self, mass, width, distance, velocity): block = Square(side_length=width) + block.mass = mass + block.velocity = velocity + block.set_style(**self.block_style) + block.set_fill(color=interpolate_color( + block.get_fill_color(), + BLUE_E, + (1 - 1.0 / (max(np.log10(mass), 1))) + )) block.move_to( self.floor.get_top()[1] * UP + (self.wall.get_right()[0] + distance) * RIGHT, DL, ) - block.mass = mass - block.velocity = velocity - label = block.label = TextMobject("{}kg".format(mass)) + label = block.label = TextMobject( + "{:,}\\,kg".format(mass) + ) label.scale(0.8) label.next_to(block, UP, SMALL_BUFF) block.add(label) return block + def get_phase_space_point_tracker(self): + block1, block2 = self.block1, self.block2 + w2 = block2.get_width() + s1 = block1.get_left()[0] - self.wall.get_right()[0] - w2 + s2 = block2.get_right()[0] - self.wall.get_right()[0] - w2 + result = VectorizedPoint([ + s1 * np.sqrt(block1.mass), + s2 * np.sqrt(block2.mass), + 0 + ]) + + result.velocity = np.array([ + np.sqrt(block1.mass) * block1.velocity, + np.sqrt(block2.mass) * block2.velocity, + 0 + ]) + return result + def update_positions(self, dt): + self.phase_space_point_tracker.shift( + self.phase_space_point_tracker.velocity * dt + ) + self.update_blocks_from_phase_space_point_tracker() + + def old_update_positions(self, dt): + # Based on velocity diagram bouncing...didn't work for + # large masses, due to frame rate mismatch blocks = self.submobjects for block in blocks: block.shift(block.velocity * dt * RIGHT) @@ -74,13 +115,45 @@ class SlidingBlocks(VGroup): # Second block hits wall blocks[1].velocity *= -1 blocks[1].move_to(self.wall.get_corner(DR), DL) + if blocks[0].get_left()[0] < blocks[1].get_right()[0]: + blocks[0].move_to(blocks[1].get_corner(DR), DL) self.surrounding_scene.clack(blocks[1].get_left()) return self + def update_blocks_from_phase_space_point_tracker(self): + block1, block2 = self.block1, self.block2 + + ps_point = self.phase_space_point_tracker.get_location() + theta = np.arctan(np.sqrt(block2.mass / block1.mass)) + ps_point_angle = angle_of_vector(ps_point) + n_clacks = int(ps_point_angle / theta) + # TODO, pass on n_clack information to surrounding_scene + reflected_point = rotate_vector( + ps_point, + -2 * np.ceil(n_clacks / 2) * theta + ) + reflected_point = np.abs(reflected_point) + + shadow_wall_x = self.wall.get_right()[0] + block2.get_width() + floor_y = self.floor.get_top()[1] + s1 = reflected_point[0] / np.sqrt(block1.mass) + s2 = reflected_point[1] / np.sqrt(block2.mass) + block1.move_to( + (shadow_wall_x + s1) * RIGHT + + floor_y * UP, + DL, + ) + block2.move_to( + (shadow_wall_x + s2) * RIGHT + + floor_y * UP, + DR, + ) + class BlocksAndWallScene(Scene): CONFIG = { - "print_clack_times": True, + "print_clack_times": False, + "count_clacks": True, "sliding_blocks_config": {}, "floor_y": -2, "wall_x": -5, @@ -89,14 +162,36 @@ class BlocksAndWallScene(Scene): def setup(self): self.floor = self.get_floor() self.wall = self.get_wall() - self.blocks = SlidingBlocks(self) + self.blocks = SlidingBlocks(self, **self.sliding_blocks_config) + self.add(self.floor, self.wall, self.blocks) + if self.count_clacks: + self.add_clack_counter() + + self.track_times() + + def track_times(self): self.clack_times = [] time_tracker = ValueTracker() time_tracker.add_updater(lambda m, dt: m.increment_value(dt)) self.add(time_tracker) self.get_time = time_tracker.get_value - self.add(self.floor, self.wall, self.blocks) + def add_clack_counter(self): + clack_counter_label = TextMobject("\\# Clacks: ") + clack_counter = Integer(0) + clack_counter.next_to( + clack_counter_label[-1], RIGHT, + aligned_edge=DOWN, + ) + clack_group = VGroup( + clack_counter_label, + clack_counter, + ) + clack_group.to_corner(UR) + clack_group.shift(LEFT) + self.add(clack_group) + + self.clack_counter = clack_counter def tear_down(self): if self.print_clack_times: @@ -120,11 +215,10 @@ class BlocksAndWallScene(Scene): floor.shift(self.floor_y * UP) return floor - def get_blocks(self): - pass - def clack(self, location): self.clack_times.append(self.get_time()) + if self.count_clacks: + self.clack_counter.increment_value() # Animted scenes @@ -135,5 +229,15 @@ class MathAndPhysicsConspiring(Scene): class ThreeSimpleClacks(BlocksAndWallScene): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 100, + "width": 1.5, + } + } + } + def construct(self): - self.wait(10) + while self.blocks[0].get_left()[0] < FRAME_WIDTH / 2: + self.wait(1) From cf958684e8b6956155ee00dd5915b8dbd697ef34 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 5 Jan 2019 11:08:08 -0800 Subject: [PATCH 10/13] Moved uncertainty Flash animation into indication, and cleaned up for more general use --- manimlib/animation/indication.py | 47 +++++++++++++++++++++++++++++++- old_projects/uncertainty.py | 44 ------------------------------ 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/manimlib/animation/indication.py b/manimlib/animation/indication.py index adbe7f36..beb4f2cd 100644 --- a/manimlib/animation/indication.py +++ b/manimlib/animation/indication.py @@ -11,12 +11,16 @@ from manimlib.animation.creation import ShowCreation from manimlib.animation.creation import ShowPartial from manimlib.animation.creation import FadeOut from manimlib.animation.transform import Transform +from manimlib.animation.update import UpdateFromAlphaFunc from manimlib.mobject.mobject import Mobject from manimlib.mobject.geometry import Circle from manimlib.mobject.geometry import Dot from manimlib.mobject.shape_matchers import SurroundingRectangle +from manimlib.mobject.types.vectorized_mobject import VGroup +from manimlib.mobject.geometry import Line from manimlib.utils.bezier import interpolate from manimlib.utils.config_ops import digest_config +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 @@ -60,6 +64,46 @@ class Indicate(Transform): Transform.__init__(self, mobject, target, **kwargs) +class Flash(AnimationGroup): + CONFIG = { + "line_length": 0.2, + "num_lines": 12, + "flash_radius": 0.3, + "line_stroke_width": 3, + } + + def __init__(self, point, color=YELLOW, **kwargs): + digest_config(self, kwargs) + lines = VGroup() + for angle in np.arange(0, TAU, TAU / self.num_lines): + line = Line(ORIGIN, self.line_length * RIGHT) + line.shift((self.flash_radius - self.line_length) * RIGHT) + line.rotate(angle, about_point=ORIGIN) + lines.add(line) + lines.move_to(point) + lines.set_color(color) + lines.set_stroke(width=3) + line_anims = [ + ShowCreationThenDestruction( + line, rate_func=squish_rate_func(smooth, 0, 0.5) + ) + for line in lines + ] + fade_anims = [ + UpdateFromAlphaFunc( + line, lambda m, a: m.set_stroke( + width=self.line_stroke_width * (1 - a) + ), + rate_func=squish_rate_func(smooth, 0, 0.75) + ) + for line in lines + ] + + AnimationGroup.__init__( + self, *line_anims + fade_anims, **kwargs + ) + + class CircleIndicate(Indicate): CONFIG = { "rate_func": squish_rate_func(there_and_back, 0, 0.8), @@ -227,7 +271,8 @@ class Vibrate(Animation): )) for mob, start in zip(*families): mob.points = np.apply_along_axis( - lambda x_y_z: (x_y_z[0], x_y_z[1] + self.wave_function(x_y_z[0], time), x_y_z[2]), + lambda x_y_z: ( + x_y_z[0], x_y_z[1] + self.wave_function(x_y_z[0], time), x_y_z[2]), 1, start.points ) diff --git a/old_projects/uncertainty.py b/old_projects/uncertainty.py index b6382218..237fa083 100644 --- a/old_projects/uncertainty.py +++ b/old_projects/uncertainty.py @@ -234,50 +234,6 @@ class RadarPulse(ContinualAnimation): def is_finished(self): return all([ps.is_finished() for ps in self.pulse_singletons]) -class Flash(AnimationGroup): - CONFIG = { - "line_length" : 0.2, - "num_lines" : 12, - "flash_radius" : 0.3, - "line_stroke_width" : 3, - } - def __init__(self, mobject, color = YELLOW, **kwargs): - digest_config(self, kwargs) - original_color = mobject.get_color() - on_and_off = UpdateFromAlphaFunc( - mobject.copy(), lambda m, a : m.set_color( - color if a < 0.5 else original_color - ), - remover = True - ) - lines = VGroup() - for angle in np.arange(0, TAU, TAU/self.num_lines): - line = Line(ORIGIN, self.line_length*RIGHT) - line.shift((self.flash_radius - self.line_length)*RIGHT) - line.rotate(angle, about_point = ORIGIN) - lines.add(line) - lines.move_to(mobject) - lines.set_color(color) - line_anims = [ - ShowCreationThenDestruction( - line, rate_func = squish_rate_func(smooth, 0, 0.5) - ) - for line in lines - ] - fade_anims = [ - UpdateFromAlphaFunc( - line, lambda m, a : m.set_stroke( - width = self.line_stroke_width*(1-a) - ), - rate_func = squish_rate_func(smooth, 0, 0.75) - ) - for line in lines - ] - - AnimationGroup.__init__( - self, on_and_off, *line_anims+fade_anims, **kwargs - ) - class MultipleFlashes(Succession): CONFIG = { "run_time_per_flash" : 1.0, From e957517d7dd756e727c8102f8cdf3d2e6e0b804e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 5 Jan 2019 11:08:21 -0800 Subject: [PATCH 11/13] Added sound to collision animation --- active_projects/clacks.py | 175 ++++++++++++++++++++++++++++++++------ 1 file changed, 148 insertions(+), 27 deletions(-) diff --git a/active_projects/clacks.py b/active_projects/clacks.py index 433fd4c0..a7b344df 100644 --- a/active_projects/clacks.py +++ b/active_projects/clacks.py @@ -1,4 +1,6 @@ from big_ol_pile_of_manim_imports import * +import subprocess +from pydub import AudioSegment class SlidingBlocks(VGroup): @@ -17,7 +19,7 @@ class SlidingBlocks(VGroup): }, "block_style": { "fill_opacity": 1, - "fill_color": (GREY, LIGHT_GREY), + "fill_color": (GREY, WHITE), "stroke_width": 3, "stroke_color": WHITE, "sheen_direction": UL, @@ -32,6 +34,7 @@ class SlidingBlocks(VGroup): self.block1 = self.get_block(**self.block1_config) self.block2 = self.get_block(**self.block2_config) + self.mass_ratio = self.block2.mass / self.block1.mass self.phase_space_point_tracker = self.get_phase_space_point_tracker() self.add( self.block1, self.block2, @@ -39,8 +42,7 @@ class SlidingBlocks(VGroup): ) self.add_updater(self.__class__.update_positions) - # From here, there's enough information to create - # a list of all clack times and locations + self.clack_data = self.get_clack_data() def get_block(self, mass, width, distance, velocity): block = Square(side_length=width) @@ -59,7 +61,7 @@ class SlidingBlocks(VGroup): DL, ) label = block.label = TextMobject( - "{:,}\\,kg".format(mass) + "{:,}\\,kg".format(int(mass)) ) label.scale(0.8) label.next_to(block, UP, SMALL_BUFF) @@ -124,10 +126,9 @@ class SlidingBlocks(VGroup): block1, block2 = self.block1, self.block2 ps_point = self.phase_space_point_tracker.get_location() - theta = np.arctan(np.sqrt(block2.mass / block1.mass)) + theta = np.arctan(np.sqrt(self.mass_ratio)) ps_point_angle = angle_of_vector(ps_point) n_clacks = int(ps_point_angle / theta) - # TODO, pass on n_clack information to surrounding_scene reflected_point = rotate_vector( ps_point, -2 * np.ceil(n_clacks / 2) * theta @@ -149,36 +150,111 @@ class SlidingBlocks(VGroup): DR, ) + self.surrounding_scene.update_num_clacks(n_clacks) + + def get_clack_data(self): + ps_point = self.phase_space_point_tracker.get_location() + ps_velocity = self.phase_space_point_tracker.velocity + if ps_velocity[1] != 0: + raise Exception( + "Haven't implemented anything to gather clack " + "data from a start state with block2 moving" + ) + y = ps_point[1] + theta = np.arctan(np.sqrt(self.mass_ratio)) + + 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) / get_norm(ps_velocity) + reflected_point = rotate_vector( + clack_ps_point, + -2 * np.ceil((k - 1) / 2) * theta + ) + block2 = self.block2 + s2 = reflected_point[1] / np.sqrt(block2.mass) + location = np.array([ + self.wall.get_right()[0] + s2, + block2.get_center()[1], + 0 + ]) + if k % 2 == 1: + location += block2.get_width() * RIGHT + clack_data.append((location, time)) + return clack_data + + +class ClackFlashes(ContinualAnimation): + CONFIG = { + "flash_config": { + "run_time": 0.5, + "line_length": 0.1, + "flash_radius": 0.2, + }, + "start_up_time": 0, + } + + def __init__(self, clack_data, **kwargs): + digest_config(self, kwargs) + self.flashes = [] + group = Group() + for location, time in clack_data: + flash = Flash(location, **self.flash_config) + flash.start_time = time + flash.end_time = time + flash.run_time + self.flashes.append(flash) + ContinualAnimation.__init__(self, group, **kwargs) + + def update_mobject(self, dt): + total_time = self.external_time + 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) + flash.update( + (total_time - flash.start_time) / flash.run_time + ) + else: + if flash.mobject in self.mobject: + self.mobject.remove(flash.mobject) + class BlocksAndWallScene(Scene): CONFIG = { - "print_clack_times": False, + "include_sound_file": True, "count_clacks": True, "sliding_blocks_config": {}, "floor_y": -2, "wall_x": -5, + "clack_counter_label": "\\# Collisions: " } def setup(self): self.floor = self.get_floor() self.wall = self.get_wall() self.blocks = SlidingBlocks(self, **self.sliding_blocks_config) - self.add(self.floor, self.wall, self.blocks) + self.clack_data = self.blocks.clack_data + self.clack_flashes = ClackFlashes(self.clack_data) + self.add(self.floor, self.wall, self.blocks, self.clack_flashes) + if self.count_clacks: self.add_clack_counter() + self.track_time() - self.track_times() - - def track_times(self): - self.clack_times = [] + def track_time(self): time_tracker = ValueTracker() time_tracker.add_updater(lambda m, dt: m.increment_value(dt)) self.add(time_tracker) self.get_time = time_tracker.get_value def add_clack_counter(self): - clack_counter_label = TextMobject("\\# Clacks: ") - clack_counter = Integer(0) + self.n_clacks = 0 + clack_counter_label = TextMobject(self.clack_counter_label) + clack_counter = Integer(self.n_clacks) clack_counter.next_to( clack_counter_label[-1], RIGHT, aligned_edge=DOWN, @@ -193,10 +269,6 @@ class BlocksAndWallScene(Scene): self.clack_counter = clack_counter - def tear_down(self): - if self.print_clack_times: - print(self.clack_times) - def get_wall(self): wall = Line(self.floor_y * UP, FRAME_HEIGHT * UP / 2) wall.shift(self.wall_x * RIGHT) @@ -215,29 +287,78 @@ class BlocksAndWallScene(Scene): floor.shift(self.floor_y * UP) return floor - def clack(self, location): - self.clack_times.append(self.get_time()) - if self.count_clacks: - self.clack_counter.increment_value() + def update_num_clacks(self, n_clacks): + if hasattr(self, "n_clacks"): + if n_clacks == self.n_clacks: + return + self.clack_counter.set_value(n_clacks) + def create_sound_file(self, clack_data): + directory = get_scene_output_directory(self.__class__) + clack_file = os.path.join(directory, 'sounds', 'bell.wav') + output_file = self.get_movie_file_path(extension='.wav') + times = [ + time + for location, time in clack_data + if time < 30 + ] + + clack = AudioSegment.from_wav(clack_file) + total_time = max(times) + 1 + clacks = AudioSegment.silent(int(1000 * total_time)) + last_position = 0 + min_diff = 4 + for time in times: + position = int(1000 * time) + d_position = position - last_position + last_position = position + if d_position < min_diff: + continue + clacks = clacks.fade(-50, start=position, end=position + 10) + clacks = clacks.overlay( + clack, + position=position + ) + clacks.export(output_file, format="wav") + return output_file + + def close_movie_pipe(self): + Scene.close_movie_pipe(self) + if self.include_sound_file: + 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 -# Animted scenes class MathAndPhysicsConspiring(Scene): def construct(self): pass -class ThreeSimpleClacks(BlocksAndWallScene): +class BlocksAndWallExample(BlocksAndWallScene): CONFIG = { "sliding_blocks_config": { "block1_config": { - "mass": 100, + "mass": 1e4, "width": 1.5, + "velocity": -2, } } } def construct(self): - while self.blocks[0].get_left()[0] < FRAME_WIDTH / 2: - self.wait(1) + self.wait(20) From d98d1ec6104243986441a0290705faf5204e21e8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 7 Jan 2019 13:24:16 -0800 Subject: [PATCH 12/13] Changed default behavior to have no finishing sound, and instead of passing in --no-sound to prevent the sound, one would pass in --sound to activate it --- manimlib/config.py | 6 +++--- manimlib/extract_scene.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index cdae8d8e..a5d78f0d 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -41,9 +41,9 @@ def parse_cli(): parser.add_argument("-r", "--resolution") parser.add_argument("-c", "--color") parser.add_argument( - "--no_sound", + "--sound", action="store_true", - help="Don't play a success/failure sound", + help="Play a success/failure sound", ) module_location.add_argument( "--livestream", @@ -128,7 +128,7 @@ def get_configuration(args): "output_name": output_name, "start_at_animation_number": args.start_at_animation_number, "end_at_animation_number": None, - "no_sound": args.no_sound, + "sound": args.sound, } # Camera configuration diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index d707be4d..6885d869 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -136,14 +136,14 @@ def main(config): for SceneClass in get_scene_classes(scene_names_to_classes, config): try: handle_scene(SceneClass(**scene_kwargs), **config) - if not config["no_sound"]: + if config["sound"]: play_finish_sound() sys.exit(0) except Exception: print("\n\n") traceback.print_exc() print("\n\n") - if not config["no_sound"]: + if config["sound"]: play_error_sound() sys.exit(2) From a1c9e8dbb241652f6a686ded6eed5b24ad1c07b8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 7 Jan 2019 13:24:39 -0800 Subject: [PATCH 13/13] First few animation of clacks, with many variants on the block/wall setup --- active_projects/clacks.py | 278 +++++++++++++++++++++++++++++++++----- 1 file changed, 245 insertions(+), 33 deletions(-) diff --git a/active_projects/clacks.py b/active_projects/clacks.py index a7b344df..d957e5c2 100644 --- a/active_projects/clacks.py +++ b/active_projects/clacks.py @@ -3,26 +3,32 @@ import subprocess from pydub import AudioSegment +MIN_TIME_BETWEEN_FLASHES = 0.004 + + class SlidingBlocks(VGroup): CONFIG = { "block1_config": { "mass": 1, "velocity": -2, "distance": 7, - "width": 1, + "width": None, + "color": None, }, "block2_config": { "mass": 1, "velocity": 0, "distance": 3, - "width": 1, + "width": None, + "color": None, }, "block_style": { "fill_opacity": 1, - "fill_color": (GREY, WHITE), "stroke_width": 3, "stroke_color": WHITE, "sheen_direction": UL, + "sheen_factor": 0.5, + "sheen_direction": UL, } } @@ -44,17 +50,19 @@ class SlidingBlocks(VGroup): self.clack_data = self.get_clack_data() - def get_block(self, mass, width, distance, velocity): + def get_block(self, mass, distance, velocity, width, color): + if width is None: + width = self.mass_to_width(mass) + if color is None: + color = self.mass_to_color(mass) block = Square(side_length=width) block.mass = mass block.velocity = velocity - block.set_style(**self.block_style) - block.set_fill(color=interpolate_color( - block.get_fill_color(), - BLUE_E, - (1 - 1.0 / (max(np.log10(mass), 1))) - )) + style = dict(self.block_style) + style["fill_color"] = color + + block.set_style(**style) block.move_to( self.floor.get_top()[1] * UP + (self.wall.get_right()[0] + distance) * RIGHT, @@ -187,6 +195,23 @@ class SlidingBlocks(VGroup): clack_data.append((location, time)) return clack_data + def mass_to_color(self, mass): + colors = [ + LIGHT_GREY, + BLUE_B, + BLUE_D, + BLUE_E, + BLUE_E, + DARK_GREY, + DARK_GREY, + BLACK, + ] + index = min(int(np.log10(mass)), len(colors) - 1) + return colors[index] + + def mass_to_width(self, mass): + return 1 + 0.25 * np.log10(mass) + class ClackFlashes(ContinualAnimation): CONFIG = { @@ -202,7 +227,11 @@ class ClackFlashes(ContinualAnimation): digest_config(self, kwargs) self.flashes = [] group = Group() + last_time = 0 for location, time in clack_data: + if (time - last_time) < MIN_TIME_BETWEEN_FLASHES: + continue + last_time = time flash = Flash(location, **self.flash_config) flash.start_time = time flash.end_time = time + flash.run_time @@ -229,8 +258,9 @@ class BlocksAndWallScene(Scene): "count_clacks": True, "sliding_blocks_config": {}, "floor_y": -2, - "wall_x": -5, - "clack_counter_label": "\\# Collisions: " + "wall_x": -6, + "counter_label": "\\# Collisions: ", + "collision_sound": "clack.wav", } def setup(self): @@ -242,7 +272,7 @@ class BlocksAndWallScene(Scene): self.add(self.floor, self.wall, self.blocks, self.clack_flashes) if self.count_clacks: - self.add_clack_counter() + self.add_counter() self.track_time() def track_time(self): @@ -251,23 +281,23 @@ class BlocksAndWallScene(Scene): self.add(time_tracker) self.get_time = time_tracker.get_value - def add_clack_counter(self): + def add_counter(self): self.n_clacks = 0 - clack_counter_label = TextMobject(self.clack_counter_label) - clack_counter = Integer(self.n_clacks) - clack_counter.next_to( - clack_counter_label[-1], RIGHT, + counter_label = TextMobject(self.counter_label) + counter_mob = Integer(self.n_clacks) + counter_mob.next_to( + counter_label[-1], RIGHT, aligned_edge=DOWN, ) clack_group = VGroup( - clack_counter_label, - clack_counter, + counter_label, + counter_mob, ) clack_group.to_corner(UR) clack_group.shift(LEFT) self.add(clack_group) - self.clack_counter = clack_counter + self.counter_mob = counter_mob def get_wall(self): wall = Line(self.floor_y * UP, FRAME_HEIGHT * UP / 2) @@ -291,29 +321,33 @@ class BlocksAndWallScene(Scene): if hasattr(self, "n_clacks"): if n_clacks == self.n_clacks: return - self.clack_counter.set_value(n_clacks) + self.counter_mob.set_value(n_clacks) def create_sound_file(self, clack_data): directory = get_scene_output_directory(self.__class__) - clack_file = os.path.join(directory, 'sounds', 'bell.wav') + clack_file = os.path.join( + directory, 'sounds', self.collision_sound, + ) output_file = self.get_movie_file_path(extension='.wav') times = [ time for location, time in clack_data - if time < 30 + if time < 300 # In case of any extremes ] clack = AudioSegment.from_wav(clack_file) total_time = max(times) + 1 clacks = AudioSegment.silent(int(1000 * total_time)) last_position = 0 - min_diff = 4 + min_diff = int(1000 * MIN_TIME_BETWEEN_FLASHES) for time in times: position = int(1000 * time) d_position = position - last_position - last_position = position if d_position < min_diff: continue + if time > self.get_time(): + break + last_position = position clacks = clacks.fade(-50, start=position, end=position + 10) clacks = clacks.overlay( clack, @@ -345,20 +379,198 @@ class BlocksAndWallScene(Scene): class MathAndPhysicsConspiring(Scene): + def construct(self): + v_line = Line(DOWN, UP).scale(FRAME_HEIGHT) + v_line.save_state() + v_line.fade(1) + v_line.scale(0) + math_title = TextMobject("Math") + math_title.set_color(BLUE) + physics_title = TextMobject("Physics") + physics_title.set_color(YELLOW) + for title, vect in (math_title, LEFT), (physics_title, RIGHT): + title.scale(2) + title.shift(vect * FRAME_WIDTH / 4) + title.to_edge(UP) + + math_stuffs = VGroup( + TexMobject("\\pi = {:.16}\\dots".format(PI)), + self.get_tangent_image(), + ) + math_stuffs.arrange_submobjects(DOWN, buff=MED_LARGE_BUFF) + math_stuffs.next_to(math_title, DOWN, LARGE_BUFF) + to_fade = VGroup(math_title, *math_stuffs, physics_title) + + self.play( + LaggedStart( + FadeInFromDown, to_fade, + lag_ratio=0.7, + run_time=3, + ), + Restore(v_line, run_time=2, path_arc=PI / 2), + ) + self.wait() + + def get_tangent_image(self): + axes = Axes( + x_min=-1.5, + x_max=1.5, + y_min=-1.5, + y_max=1.5, + ) + circle = Circle() + circle.set_color(WHITE) + theta = 30 * DEGREES + arc = Arc(angle=theta, radius=0.4) + theta_label = TexMobject("\\theta") + theta_label.scale(0.5) + theta_label.next_to(arc.get_center(), RIGHT, buff=SMALL_BUFF) + theta_label.shift(0.025 * UL) + line = Line(ORIGIN, rotate_vector(RIGHT, theta)) + line.set_color(WHITE) + one = TexMobject("1").scale(0.5) + one.next_to(line.point_from_proportion(0.7), UL, 0.5 * SMALL_BUFF) + tan_line = Line( + line.get_end(), + (1.0 / np.cos(theta)) * RIGHT + ) + tan_line.set_color(RED) + tan_text = TexMobject("\\tan(\\theta)") + tan_text.rotate(tan_line.get_angle()) + tan_text.scale(0.5) + tan_text.move_to(tan_line) + tan_text.match_color(tan_line) + tan_text.shift(0.2 * normalize(line.get_vector())) + + result = VGroup( + axes, circle, + line, one, + arc, theta_label, + tan_line, tan_text, + ) + result.set_height(4) + return result + + +class LightBouncing(Scene): + CONFIG = { + "theta": np.arctan(0.1) + } + def construct(self): pass -class BlocksAndWallExample(BlocksAndWallScene): +class BlocksAndWallExampleSameMass(BlocksAndWallScene): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e0, + "velocity": -2, + } + }, + "wait_time": 10, + } + + def construct(self): + self.wait(self.wait_time) + + +class BlocksAndWallExampleMass1e1(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e1, + "velocity": -1.5, + } + }, + "wait_time": 20, + } + + +class BlocksAndWallExampleMass1e2(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e2, + "velocity": -1, + } + }, + "wait_time": 20, + } + + +class BlocksAndWallExampleMass1e4(BlocksAndWallExampleSameMass): CONFIG = { "sliding_blocks_config": { "block1_config": { "mass": 1e4, - "width": 1.5, - "velocity": -2, - } - } + "velocity": -1.5, + }, + }, + "wait_time": 25, } - def construct(self): - self.wait(20) + +class BlocksAndWallExampleMass1e4SlowMo(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e4, + "velocity": -0.1, + "distance": 4.1 + }, + }, + "wait_time": 50, + "collision_sound": "slow_clack.wav", + } + + +class BlocksAndWallExampleMass1e6(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e6, + "velocity": -1, + }, + }, + "wait_time": 20, + } + + +class BlocksAndWallExampleMass1e6SlowMo(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e6, + "velocity": -0.1, + "distance": 4.1 + }, + }, + "wait_time": 60, + "collision_sound": "slow_clack.wav", + } + + +class BlocksAndWallExampleMass1e8(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e8, + "velocity": -1, + }, + }, + "wait_time": 25, + } + + +class BlocksAndWallExampleMass1e10(BlocksAndWallExampleSameMass): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e10, + "velocity": -1, + }, + }, + "wait_time": 25, + }