diff --git a/active_projects/clacks.py b/active_projects/clacks.py new file mode 100644 index 00000000..d957e5c2 --- /dev/null +++ b/active_projects/clacks.py @@ -0,0 +1,576 @@ +from big_ol_pile_of_manim_imports import * +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": None, + "color": None, + }, + "block2_config": { + "mass": 1, + "velocity": 0, + "distance": 3, + "width": None, + "color": None, + }, + "block_style": { + "fill_opacity": 1, + "stroke_width": 3, + "stroke_color": WHITE, + "sheen_direction": UL, + "sheen_factor": 0.5, + "sheen_direction": UL, + } + } + + 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 + + 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, + self.phase_space_point_tracker, + ) + self.add_updater(self.__class__.update_positions) + + self.clack_data = self.get_clack_data() + + 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 + + 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, + DL, + ) + label = block.label = TextMobject( + "{:,}\\,kg".format(int(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) + if blocks[0].get_left()[0] < blocks[1].get_right()[0]: + # Two blocks collide + m1 = blocks[0].mass + m2 = blocks[1].mass + v1 = blocks[0].velocity + v2 = blocks[1].velocity + v_phase_space_point = np.array([ + np.sqrt(m1) * v1, -np.sqrt(m2) * v2 + ]) + angle = 2 * np.arctan(np.sqrt(m2 / m1)) + new_vps_point = rotate_vector(v_phase_space_point, angle) + for block, value in zip(blocks, new_vps_point): + block.velocity = value / np.sqrt(block.mass) + blocks[1].move_to(blocks[0].get_corner(DL), DR) + self.surrounding_scene.clack(blocks[0].get_left()) + if blocks[1].get_left()[0] < self.wall.get_right()[0]: + # Second block hits wall + blocks[1].velocity *= -1 + blocks[1].move_to(self.wall.get_corner(DR), DL) + if blocks[0].get_left()[0] < blocks[1].get_right()[0]: + blocks[0].move_to(blocks[1].get_corner(DR), DL) + self.surrounding_scene.clack(blocks[1].get_left()) + return self + + def update_blocks_from_phase_space_point_tracker(self): + block1, block2 = self.block1, self.block2 + + ps_point = self.phase_space_point_tracker.get_location() + theta = np.arctan(np.sqrt(self.mass_ratio)) + ps_point_angle = angle_of_vector(ps_point) + n_clacks = int(ps_point_angle / theta) + 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, + ) + + 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 + + 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 = { + "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() + 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 + 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 = { + "include_sound_file": True, + "count_clacks": True, + "sliding_blocks_config": {}, + "floor_y": -2, + "wall_x": -6, + "counter_label": "\\# Collisions: ", + "collision_sound": "clack.wav", + } + + def setup(self): + self.floor = self.get_floor() + self.wall = self.get_wall() + self.blocks = SlidingBlocks(self, **self.sliding_blocks_config) + 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_counter() + self.track_time() + + 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_counter(self): + self.n_clacks = 0 + 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( + counter_label, + counter_mob, + ) + clack_group.to_corner(UR) + clack_group.shift(LEFT) + self.add(clack_group) + + self.counter_mob = counter_mob + + 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 update_num_clacks(self, n_clacks): + if hasattr(self, "n_clacks"): + if n_clacks == self.n_clacks: + return + 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', self.collision_sound, + ) + output_file = self.get_movie_file_path(extension='.wav') + times = [ + time + for location, time in clack_data + 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 = int(1000 * MIN_TIME_BETWEEN_FLASHES) + for time in times: + position = int(1000 * time) + d_position = position - last_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, + 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 + + +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 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, + "velocity": -1.5, + }, + }, + "wait_time": 25, + } + + +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, + } 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/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 4eb519b3..6885d869 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"]: @@ -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) 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): 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) 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, 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) 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) 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): 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,