diff --git a/active_projects/clacks.py b/active_projects/clacks.py index 0b1792d6..387cb647 100644 --- a/active_projects/clacks.py +++ b/active_projects/clacks.py @@ -344,7 +344,7 @@ class BlocksAndWallScene(Scene): self.counter_mob.set_value(n_clacks) def create_sound_file(self, clack_data): - directory = get_scene_output_directory(self.__class__) + directory = get_scene_output_directory(BlocksAndWallScene) clack_file = os.path.join( directory, 'sounds', self.collision_sound, ) @@ -376,8 +376,8 @@ class BlocksAndWallScene(Scene): clacks.export(output_file, format="wav") return output_file - def close_movie_pipe(self): - Scene.close_movie_pipe(self) + 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() @@ -398,6 +398,42 @@ class BlocksAndWallScene(Scene): # Animated scenes +class NameIntro(Scene): + def construct(self): + name = TextMobject("3Blue", "1Brown", arg_separator="") + blue, brown = name + name.scale(2.5) + for part in name: + part.save_state() + brown.to_edge(RIGHT, buff=0) + flash_time = 0.75 + + self.add(blue, brown) + self.play( + VFadeIn(blue), + VFadeIn(brown), + Restore(brown, rate_func=None), + ) + self.play( + Flash(blue.get_right(), run_time=flash_time), + ApplyMethod( + blue.to_edge, LEFT, {"buff": 0}, + rate_func=None, + ), + ) + self.play( + Flash(blue.get_left(), run_time=flash_time), + Restore(blue, rate_func=None), + ) + self.play( + Flash(blue.get_right(), run_time=flash_time), + ApplyMethod( + brown.to_edge, RIGHT, {"buff": 0}, + rate_func=None, + ) + ) + + class MathAndPhysicsConspiring(Scene): def construct(self): v_line = Line(DOWN, UP).scale(FRAME_HEIGHT) @@ -547,11 +583,12 @@ class BlocksAndWallExample(BlocksAndWallScene): CONFIG = { "sliding_blocks_config": { "block1_config": { - "mass": 1e0, + # "mass": 1e0, + "mass": 64, "velocity": -2, } }, - "wait_time": 10, + "wait_time": 15, } def construct(self): @@ -570,6 +607,37 @@ class BlocksAndWallExampleMass1e1(BlocksAndWallExample): } +class TwoBlocksLabel(Scene): + def construct(self): + label = TextMobject("Two sliding \\\\ blocks") + label.to_edge(UP) + arrows = VGroup(*[ + Arrow(label.get_bottom(), point) + for point in [RIGHT, LEFT] + ]) + arrows.set_color(RED) + self.play( + Write(label), + LaggedStart(GrowArrow, arrows, lag_ratio=0.7), + run_time=1 + ) + self.wait() + + +class WallLabel(Scene): + def construct(self): + wall = Line(TOP, 2 * DOWN) + wall.set_stroke(YELLOW, 10) + word = TextMobject("Wall") + word.rotate(-90 * DEGREES) + word.next_to(wall, RIGHT, MED_SMALL_BUFF) + self.play( + Write(word), + ShowPassingFlash(wall) + ) + self.wait() + + class CowToSphere(ExternallyAnimatedScene): pass @@ -606,9 +674,11 @@ class Mass1e1WithElasticLabel(BlocksAndWallExampleMass1e1): def get_arrow(self, label, clack_flashes, flash): arrow = Arrow( label.get_bottom(), - flash.mobject.get_center() + 0.5 * UP, + flash.mobject.get_center() + 0.0 * UP, ) arrow.set_fill(YELLOW) + arrow.set_stroke(BLACK, 1, background=True) + arrow.original_length = arrow.get_length() def set_opacity(arrow): time = self.get_time() @@ -616,8 +686,14 @@ class Mass1e1WithElasticLabel(BlocksAndWallExampleMass1e1): if from_start < 0: opacity = 0 else: - opacity = smooth(1 - from_start) + opacity = smooth(1 - 2 * from_start) arrow.set_fill(opacity=opacity) + arrow.set_stroke(opacity=opacity, background=True) + # if opacity > 0: + # arrow.scale( + # opacity * arrow.original_length / arrow.get_length(), + # about_point=arrow.get_end() + # ) arrow.add_updater(set_opacity) return arrow @@ -626,14 +702,13 @@ class Mass1e1WithElasticLabel(BlocksAndWallExampleMass1e1): class AskAboutSoundlessness(TeacherStudentsScene): def construct(self): self.student_says( - "Wait, elastic collisions should\\\\" - "make no sound, right?", + "No sound,\\\\right?" ) self.play(self.teacher.change, "guilty") self.wait(2) - self.play( - RemovePiCreatureBubble(self.students[1], target_mode="confused"), - self.teacher.change, "raise_right_hand", + self.teacher_says( + "Focus on \\\\ collisions", + target_mode="speaking", added_anims=[ self.get_student_changes("pondering", "confused", "thinking") ] @@ -681,10 +756,10 @@ class BlocksAndWallExampleMass1e2(BlocksAndWallExample): "sliding_blocks_config": { "block1_config": { "mass": 1e2, - "velocity": -1, + "velocity": -0.6, } }, - "wait_time": 20, + "wait_time": 25, } @@ -772,6 +847,24 @@ class BlocksAndWallExampleMass1e10(BlocksAndWallExample): } +class DigitsOfPi(Scene): + def construct(self): + equation = TexMobject( + "\\pi = 3.14159265..." + ) + equation.set_color(YELLOW) + pi_creature = Randolph(color=YELLOW) + pi_creature.match_width(equation[0]) + pi_creature.scale(1.4) + pi_creature.move_to(equation[0], DOWN) + self.add(pi_creature, equation[1]) + for digit in equation[2:]: + self.add(digit) + self.wait(0.1) + self.play(Blink(pi_creature)) + self.wait() + + class GalperinPaperScroll(ExternallyAnimatedScene): pass @@ -902,19 +995,7 @@ class PiComputingAlgorithmsAxes(Scene): class StepsOfTheAlgorithm(TeacherStudentsScene): def construct(self): - steps = VGroup( - TextMobject("Step 1:", "Implement a physics engine"), - TextMobject( - "Step 2:", - "Choose the number of digits, $d$,\\\\" - "of $\\pi$ that you want to compute" - ), - TextMobject( - "Step 3:", - "Set one mass to $100^{d - 1}$, the other to $1$" - ), - TextMobject("Step 4:", "Count collisions"), - ) + steps = self.get_steps() steps.arrange_submobjects( DOWN, buff=MED_LARGE_BUFF, @@ -943,6 +1024,59 @@ class StepsOfTheAlgorithm(TeacherStudentsScene): ) self.wait(3) + def get_steps(self): + return VGroup( + TextMobject("Step 1:", "Implement a physics engine"), + TextMobject( + "Step 2:", + "Choose the number of digits, $d$,\\\\" + "of $\\pi$ that you want to compute" + ), + TextMobject( + "Step 3:", + "Set one mass to $100^{d - 1}$,\\\\" + "the other to $1$" + ), + TextMobject("Step 4:", "Count collisions"), + ) + + +class StepsOfTheAlgorithmJustTitles(StepsOfTheAlgorithm): + def construct(self): + self.remove(*self.pi_creatures) + titles = self.get_steps() + for title in titles: + title.scale(1.5) + title.to_edge(UP) + + last_title = VectorizedPoint() + for title in titles: + self.play( + FadeInFromDown(title), + FadeOutAndShift(last_title, UP), + ) + self.wait() + last_title = title + + +class BlocksAndWallExampleToShowWithSteps(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e22, + "velocity": -1, + "label_text": "$100^{(12 - 1)}$\\,kg", + "width": 2, + }, + "collect_clack_data": False, + }, + "wait_time": 25, + "counter_group_shift_vect": 5 * LEFT, + "count_clacks": True, + "include_sound": False, + "show_flash_animations": False, + } + class CompareToGalacticMass(Scene): def construct(self): @@ -1174,6 +1308,22 @@ class BlocksAndWallExampleGalacticMass(BlocksAndWallExample): self.add(words) +class RealPhysicsVsThis(Scene): + def construct(self): + physics = TextMobject("Real physics") + this = TextMobject("This process") + this.set_color() + physics.to_edge(LEFT) + this.next_to(physics) + self.add(physics, this) + self.play( + this.shift, FRAME_WIDTH * RIGHT, + rate_func=rush_into, + run_time=3, + ) + self.wait() + + class CompareAlgorithmToPhysics(PiCreatureScene): def construct(self): morty = self.pi_creature @@ -1284,4 +1434,165 @@ class LightBouncingFanning(LightBouncingNoFanning): class NextVideo(Scene): def construct(self): - pass + videos = VGroup(*[VideoIcon() for x in range(2)]) + videos.set_height(2) + for video in videos: + video.set_color(BLUE) + video.set_sheen(0.5, UL) + videos.arrange_submobjects(RIGHT, buff=2) + + titles = VGroup( + TextMobject("Here and now"), + TextMobject("Solution"), + ) + for title, video in zip(titles, videos): + # title.scale(1.5) + title.next_to(video, UP) + video.add(title) + + dots = TextMobject(".....") + dots.scale(2) + dots.move_to(videos) + + mid_words = TextMobject( + "Patient\\\\", "problem\\\\", "solving" + ) + mid_words.next_to(dots, DOWN) + randy = Randolph(height=1) + randy.next_to(dots, UP, SMALL_BUFF) + thought_bubble = ThoughtBubble(height=2, width=2, direction=LEFT) + thought_bubble.set_stroke(width=2) + thought_bubble.move_to(randy.get_corner(UR), DL) + speech_bubble = SpeechBubble(height=2, width=2) + speech_bubble.pin_to(randy) + speech_bubble.write("What do \\\\ you think?") + friends = VGroup( + PiCreature(color=BLUE_E), + PiCreature(color=BLUE_C), + Mortimer() + ) + friends.set_height(1) + friends.arrange_submobjects(RIGHT, buff=MED_SMALL_BUFF) + friends[:2].next_to(randy, LEFT) + friends[2].next_to(randy, RIGHT) + + self.add(videos[0]) + self.wait() + self.play( + TransformFromCopy(*videos), + ) + self.play(Write(dots)) + self.wait() + self.play( + LaggedStart( + FadeInFrom, mid_words, + lambda m: (m, UP), + lag_ratio=0.8, + ), + randy.change, "pondering", + VFadeIn(randy), + videos.space_out_submobjects, 1.3, + ) + self.play(ShowCreation(thought_bubble)) + self.play(Blink(randy)) + self.play( + Uncreate(thought_bubble), + ShowCreation(speech_bubble), + Write(speech_bubble.content), + randy.change, "maybe", friends[0].eyes, + LaggedStart(FadeInFromDown, friends), + videos.space_out_submobjects, 1.6, + ) + self.play( + LaggedStart( + ApplyMethod, friends, + lambda m: (m.change, "pondering"), + run_time=1, + lag_ratio=0.7, + ) + ) + self.play(Blink(friends[2])) + self.play(friends[0].change, "confused") + self.wait() + + +class EndScreen(Scene): + def construct(self): + width = (475 / 1280) * FRAME_WIDTH + height = width * (323 / 575) + video_rect = Rectangle( + width=width, + height=height, + ) + video_rect.shift(UP) + video_rects = VGroup(*[ + video_rect.copy().set_color(color) + for color in [BLUE_E, BLUE_C, BLUE_D, GREY_BROWN] + ]) + for rect in video_rects[1::2]: + rect.reverse_points() + video_rect.set_fill(DARK_GREY, 0.5) + video_rect.set_stroke(GREY_BROWN, 0.5) + date = TextMobject( + "Solution will be\\\\" + "posted", "1/20/19", + ) + date[1].set_color(YELLOW) + date.set_width(video_rect.get_width() - 2 * MED_SMALL_BUFF) + date.move_to(video_rect) + + handle = TextMobject("@3blue1brown") + handle.next_to(video_rect, DOWN, MED_LARGE_BUFF) + + self.add(video_rect, date, handle) + for n in range(10): + self.play( + FadeOut(video_rects[(n - 1) % 4]), + ShowCreation(video_rects[n % 4]), + run_time=2, + ) + + +class Thumbnail(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 1e4, + "velocity": -1.5, + }, + "collect_clack_data": False, + }, + "wait_time": 0, + "count_clacks": False, + "show_flash_animations": False, + "floor_y": -3, + } + + def construct(self): + self.floor.set_stroke(WHITE, 10) + self.wall.set_stroke(WHITE, 10) + self.wall[1:].set_stroke(WHITE, 4) + blocks = self.blocks + for block in blocks.block1, blocks.block2: + block.remove(block.label) + block.label.scale(2.5, about_point=block.get_top()) + self.add(block.label) + + arrow = Vector( + 2.5 * LEFT, + color=RED, + rectangular_stem_width=1.5, + tip_length=0.5 + ) + arrow.move_to(blocks.block1.get_center(), RIGHT) + arrow.add_to_back( + arrow.copy().set_stroke(GREY, 5) + ) + self.add(arrow) + + question = TextMobject("How many\\\\collisions?") + question.scale(2.5) + question.to_edge(UP) + question.set_color(YELLOW) + question.set_stroke(RED, 2, background=True) + self.add(question) \ No newline at end of file diff --git a/active_projects/clacks_solution1.py b/active_projects/clacks_solution1.py new file mode 100644 index 00000000..4484583b --- /dev/null +++ b/active_projects/clacks_solution1.py @@ -0,0 +1,43 @@ +from big_ol_pile_of_manim_imports import * +from active_projects.clacks import * + + + +class LastVideo(Scene): + def construct(self): + pass + + +class BlocksAndWallExampleMass16(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 16, + "velocity": -1.5, + }, + }, + "wait_time": 25, + } + + + +class Mass16WithElasticLabel(Mass1e1WithElasticLabel): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 16, + } + }, + } + + +class BlocksAndWallExampleMass64(BlocksAndWallExample): + CONFIG = { + "sliding_blocks_config": { + "block1_config": { + "mass": 64, + "velocity": -1.5, + }, + }, + "wait_time": 25, + } \ No newline at end of file diff --git a/manimlib/config.py b/manimlib/config.py index ac150bc1..ce540a72 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -18,8 +18,8 @@ def parse_cli(): help="path to file holding the python code for the scene", ) parser.add_argument( - "scene_name", - nargs="?", + "scene_names", + nargs="+", help="Name of the Scene class you want to see", ) optional_args = [ @@ -36,7 +36,7 @@ def parse_cli(): ] for short_arg, long_arg in optional_args: parser.add_argument(short_arg, long_arg, action="store_true") - parser.add_argument("-o", "--output_name") + parser.add_argument("-o", "--output_file_name") parser.add_argument("-n", "--start_at_animation_number") parser.add_argument("-r", "--resolution") parser.add_argument("-c", "--color") @@ -96,23 +96,23 @@ def get_module(file_name): def get_configuration(args): - if args.output_name is not None: - output_name_root, output_name_ext = os.path.splitext( - args.output_name) + if args.output_file_name is not None: + output_file_name_root, output_file_name_ext = os.path.splitext( + args.output_file_name) expected_ext = '.png' if args.show_last_frame else '.mp4' - if output_name_ext not in ['', expected_ext]: + if output_file_name_ext not in ['', expected_ext]: print("WARNING: The output will be to (doubly-dotted) %s%s" % - output_name_root % expected_ext) - output_name = args.output_name + output_file_name_root % expected_ext) + output_file_name = args.output_file_name else: # If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad. - output_name = output_name_root + output_file_name = output_file_name_root else: - output_name = args.output_name + output_file_name = args.output_file_name config = { "module": get_module(args.file), - "scene_name": args.scene_name, + "scene_names": args.scene_names, "open_video_upon_completion": args.preview, "show_file_in_finder": args.show_file_in_finder, # By default, write to file @@ -125,7 +125,7 @@ def get_configuration(args): "quiet": args.quiet or args.write_all, "ignore_waits": args.preview, "write_all": args.write_all, - "output_name": output_name, + "output_file_name": output_file_name, "start_at_animation_number": args.start_at_animation_number, "end_at_animation_number": None, "sound": args.sound, diff --git a/manimlib/constants.py b/manimlib/constants.py index 969d5dfa..8dcc14d1 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -25,8 +25,6 @@ with open("media_dir.txt", 'w') as media_file: VIDEO_DIR = os.path.join(MEDIA_DIR, "videos") RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images") SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images") -# TODO, staged scenes should really go into a subdirectory of a given scenes directory -STAGED_SCENES_DIR = os.path.join(VIDEO_DIR, "staged_scenes") ### THIS_DIR = os.path.dirname(os.path.realpath(__file__)) FILE_DIR = os.path.join(THIS_DIR, "files") @@ -35,8 +33,8 @@ TEX_DIR = os.path.join(FILE_DIR, "Tex") MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") -for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, TEX_DIR, - MOBJECT_DIR, IMAGE_MOBJECT_DIR, STAGED_SCENES_DIR]: +for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, VIDEO_DIR, + TEX_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR]: if not os.path.exists(folder): os.makedirs(folder) @@ -71,7 +69,7 @@ HELP_MESSAGE = """ -c specify a background color """ SCENE_NOT_FOUND_MESSAGE = """ - That scene is not in the script + {} is not in the script """ CHOOSE_NUMBER_MESSAGE = """ Choose number corresponding to desired scene/arguments. diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 6885d869..2e2348b3 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -98,20 +98,29 @@ def get_scene_classes(scene_names_to_classes, config): if len(scene_names_to_classes) == 0: print(manimlib.constants.NO_SCENE_MESSAGE) return [] - if config["scene_name"] in scene_names_to_classes: - return [scene_names_to_classes[config["scene_name"]]] - if config["scene_name"] != "": - print(manimlib.constants.SCENE_NOT_FOUND_MESSAGE, file=sys.stderr) - sys.exit(2) if config["write_all"]: return list(scene_names_to_classes.values()) + scene_classes = [] + for scene_name in config["scene_names"]: + if scene_name in scene_names_to_classes: + scene_classes.append(scene_names_to_classes[scene_name]) + elif scene_name != "": + print( + manimlib.constants.SCENE_NOT_FOUND_MESSAGE.format( + scene_name + ), + file=sys.stderr + ) + if scene_classes: + return scene_classes return prompt_user_for_choice(scene_names_to_classes) def main(config): module = config["module"] scene_names_to_classes = dict( - inspect.getmembers(module, lambda x: is_child_scene(x, module))) + inspect.getmembers(module, lambda x: is_child_scene(x, module)) + ) scene_kwargs = dict([ (key, config[key]) @@ -124,10 +133,9 @@ def main(config): "movie_file_extension", "start_at_animation_number", "end_at_animation_number", + "output_file_name" ] ]) - - scene_kwargs["name"] = config["output_name"] if config["save_pngs"]: print("We are going to save a PNG sequence as well...") scene_kwargs["save_pngs"] = True @@ -138,14 +146,12 @@ def main(config): handle_scene(SceneClass(**scene_kwargs), **config) if config["sound"]: play_finish_sound() - sys.exit(0) except Exception: print("\n\n") traceback.print_exc() print("\n\n") if config["sound"]: play_error_sound() - sys.exit(2) if __name__ == "__main__": diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index 1e22f107..20234d61 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -769,10 +769,55 @@ class Logo(VMobject): fill_color=BLACK, fill_opacity=1, stroke_width=0, - sheen=0.0 + sheen=0.0, + start_angle=90 * DEGREES, ) self.add(self.pupil) + def cut_pupil(self): + pupil = self.pupil + center = pupil.get_center() + new_pupil = VGroup(*[ + pupil.copy().pointwise_become_partial(pupil, a, b) + for (a, b) in [(0.25, 1), (0, 0.25)] + ]) + for sector in new_pupil: + sector.add_control_points([ + sector.points[-1], + *[center] * 3, + *[sector.points[0]] * 2 + ]) + self.remove(pupil) + self.add(new_pupil) + self.pupil = new_pupil + + def get_blue_part_and_brown_part(self): + if len(self.pupil) == 1: + self.cut_pupil() + # circle = Circle() + # circle.set_stroke(width=0) + # circle.set_fill(BLACK, opacity=1) + # circle.match_width(self) + # circle.move_to(self) + blue_part = VGroup( + self.iris_background[0], + *[ + layer[:layer.brown_index] + for layer in self.spike_layers + ], + self.pupil[0], + ) + brown_part = VGroup( + self.iris_background[1], + *[ + layer[layer.brown_index:] + for layer in self.spike_layers + ], + self.pupil[1], + ) + return blue_part, brown_part + + # Cards diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index ab60ce77..b4331f79 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -47,6 +47,7 @@ class Scene(Container): "livestreaming": False, "to_twitch": False, "twitch_key": None, + "output_file_name": None, } def __init__(self, **kwargs): @@ -111,6 +112,11 @@ class Scene(Container): def __str__(self): return self.__class__.__name__ + def get_output_file_name(self): + if self.output_file_name is not None: + return self.output_file_name + return str(self) + def set_variables_as_attrs(self, *objects, **newly_named_objects): """ This method is slightly hacky, making it a little easier @@ -597,10 +603,13 @@ class Scene(Container): def get_image_file_path(self, name=None, dont_update=False): sub_dir = "images" + output_file_name = self.get_output_file_name() if dont_update: - sub_dir = str(self) + sub_dir = output_file_name path = get_image_output_directory(self.__class__, sub_dir) - file_name = add_extension_if_not_present(name or str(self), ".png") + file_name = add_extension_if_not_present( + name or output_file_name, ".png" + ) return os.path.join(path, file_name) def save_image(self, name=None, mode="RGB", dont_update=False): @@ -618,7 +627,7 @@ class Scene(Container): if extension is None: extension = self.movie_file_extension if name is None: - name = str(self) + name = self.get_output_file_name() file_path = os.path.join(directory, name) if not file_path.endswith(extension): file_path += extension diff --git a/manimlib/utils/output_directory_getters.py b/manimlib/utils/output_directory_getters.py index decd1ea2..3e364271 100644 --- a/manimlib/utils/output_directory_getters.py +++ b/manimlib/utils/output_directory_getters.py @@ -37,7 +37,11 @@ def get_movie_output_directory(scene_class, camera_config, frame_duration): def get_partial_movie_output_directory(scene_class, camera_config, frame_duration): directory = get_movie_output_directory(scene_class, camera_config, frame_duration) return guarantee_existance( - os.path.join(directory, scene_class.__name__) + os.path.join( + directory, + "partial_movie_files", + scene_class.__name__ + ) ) @@ -74,4 +78,4 @@ def get_sorted_integer_files(directory, elif remove_non_integer_files: os.remove(full_path) indexed_files.sort(key=lambda p: p[0]) - return map(lambda p: p[1], indexed_files) + return list(map(lambda p: p[1], indexed_files))