diff --git a/active_projects/windmill.py b/active_projects/windmill.py new file mode 100644 index 00000000..d6afa51b --- /dev/null +++ b/active_projects/windmill.py @@ -0,0 +1,738 @@ +from manimlib.imports import * +import json + + +class IntroduceIMO(Scene): + CONFIG = { + "num_countries": 130, + "use_real_images": True, + # "use_real_images": False, + "include_labels": False, + "camera_config": {"background_color": DARKER_GREY}, + "random_seed": 6, + } + + def construct(self): + self.add_title() + self.show_flags() + self.show_students() + self.move_title() + self.isolate_usa() + + def add_title(self): + title = TextMobject( + "International ", "Mathematical ", "Olympiad", + ) + title.scale(1.25) + logo = ImageMobject("imo_logo") + logo.set_height(1) + + group = Group(logo, title) + group.arrange(RIGHT) + group.to_edge(UP, buff=MED_SMALL_BUFF) + + self.add(title, logo) + self.title = title + self.logo = logo + + def show_flags(self): + flags = self.get_flags() + flags.set_height(6) + flags.to_edge(DOWN) + random_flags = Group(*flags) + random_flags.shuffle() + + self.play( + LaggedStartMap( + FadeInFromDown, random_flags, + run_time=2, + lag_ratio=0.03, + ) + ) + self.remove(random_flags) + self.add(flags) + self.wait() + + self.flags = flags + + def show_students(self): + flags = self.flags + + student_groups = VGroup() + all_students = VGroup() + for flag in flags: + group = self.get_students(flag) + student_groups.add(group) + for student in group: + student.preimage = VectorizedPoint() + student.preimage.move_to(flag) + all_students.add(student) + all_students.shuffle() + + student_groups.generate_target() + student_groups.target.arrange_in_grid( + n_rows=10, + buff=SMALL_BUFF, + ) + student_groups.target[-9:].align_to(student_groups.target[0], LEFT) + student_groups.target.match_height(flags) + student_groups.target.match_y(flags) + student_groups.target.to_edge(RIGHT, buff=1) + + self.play(LaggedStart( + *[ + ReplacementTransform( + student.preimage, student + ) + for student in all_students + ], + run_time=2, + lag_ratio=0.2, + )) + self.wait() + self.play( + MoveToTarget(student_groups), + flags.space_out_submobjects, 0.8, + flags.to_edge, LEFT, MED_SMALL_BUFF, + ) + self.wait() + + self.student_groups = student_groups + + def move_title(self): + title = self.title + logo = self.logo + + new_title = TextMobject("IMO") + new_title.match_height(title) + + logo.generate_target() + group = Group(logo.target, new_title) + group.arrange(RIGHT, buff=SMALL_BUFF) + group.match_y(title) + group.match_x(self.student_groups, UP) + + title.generate_target() + for word, letter in zip(title.target, new_title[0]): + for nl in word: + nl.move_to(letter) + word.set_opacity(0) + word[0].set_opacity(1) + word[0].become(letter) + + self.play( + MoveToTarget(title), + MoveToTarget(logo), + ) + self.wait() + + def isolate_usa(self): + flags = self.flags + student_groups = self.student_groups + + us_flag = flags[0] + random_flags = Group(*flags[1:]) + random_flags.shuffle() + + old_height = us_flag.get_height() + us_flag.label.set_width(0.8 * us_flag.get_width()) + us_flag.label.next_to( + us_flag, DOWN, + buff=0.2 * us_flag.get_height(), + ) + us_flag.label.set_opacity(0) + us_flag.add(us_flag.label) + us_flag.generate_target() + us_flag.target.scale(1 / old_height) + us_flag.target.to_corner(UL) + us_flag.target[1].set_opacity(1) + + self.remove(us_flag) + self.play( + LaggedStart( + *[ + FadeOutAndShift(flag, DOWN) + for flag in random_flags + ], + lag_ratio=0.05, + run_time=1.5 + ), + MoveToTarget(us_flag), + student_groups[1:].set_opacity, 0.1, + ) + self.wait() + + # + def get_students(self, flag): + dots = VGroup(*[Dot() for x in range(6)]) + dots.arrange_in_grid(n_cols=2, buff=SMALL_BUFF) + dots.match_height(flag) + dots.next_to(flag, RIGHT, SMALL_BUFF) + if isinstance(flag, ImageMobject): + rgba = random.choice(random.choice(flag.pixel_array)) + if np.all(rgba < 100): + rgba = interpolate(rgba, 256 * np.ones(len(rgba)), 0.5) + color = rgba_to_color(rgba / 256) + else: + color = random_bright_color() + dots.set_color(color) + return dots + + def get_flags(self): + with open(os.path.join("assets", "imo_countries.json")) as fp: + countries = json.load(fp) + with open(os.path.join("assets", "country_codes.json")) as fp: + code_map = json.load(fp) + + images = Group() + for country in countries: + country = country.upper() + if country not in code_map: + continue + short_code = code_map[country].lower() + try: + image = ImageMobject(os.path.join("flags", short_code)) + image.set_width(1) + label = VGroup(*[TextMobject(l) for l in country]) + label.arrange(RIGHT, buff=0.05, aligned_edge=DOWN) + label.set_height(0.25) + if not self.use_real_images: + rect = SurroundingRectangle(image, buff=0) + rect.set_stroke(WHITE, 1) + image = rect + image.label = label + images.add(image) + except OSError: + pass + # images.remove(*images[self.num_countries:]) + n_rows = 10 + images.arrange_in_grid( + n_rows=n_rows, + buff=1.25, + ) + images[-(len(images) % n_rows):].align_to(images[0], LEFT) + sf = 1.7 + images.stretch(sf, 0) + for i, image in enumerate(images): + image.set_height(1) + image.stretch(1 / sf, 0) + image.label.next_to(image, DOWN, SMALL_BUFF) + if self.include_labels: + image.add(image.label) + + images.set_width(FRAME_WIDTH - 1) + if images.get_height() > FRAME_HEIGHT - 1: + images.set_height(FRAME_HEIGHT - 1) + images.center() + return images + + +class ShowTest(Scene): + def construct(self): + test = self.get_test() + test.generate_target() + test.target.to_edge(UP) + + # Time label + time_labels = VGroup( + TextMobject("Day 1", ": 4.5 hours"), + TextMobject("Day 2", ": 4.5 hours"), + ) + time_labels.scale(1.5) + day_labels = VGroup() + hour_labels = VGroup() + for label, page in zip(time_labels, test.target): + label.next_to(page, DOWN) + label[0].save_state() + label[0].next_to(page, DOWN) + label[1][1:].set_color(YELLOW) + day_labels.add(label[0]) + hour_labels.add(label[1]) + + # Problem desciptions + problem_rects = self.get_problem_rects(test.target[0]) + proof_words = VGroup() + for rect in problem_rects: + word = TextMobject("Proof") + word.scale(2) + word.next_to(rect, RIGHT, buff=3) + word.set_color(BLUE) + proof_words.add(word) + + proof_words.space_out_submobjects(2) + + proof_arrows = VGroup() + for rect, word in zip(problem_rects, proof_words): + arrow = Arrow(word.get_left(), rect.get_right()) + arrow.match_color(word) + proof_arrows.add(arrow) + + scores = VGroup() + for word in proof_words: + score = VGroup(TexMobject("/"), Integer(0)) + score.arrange(RIGHT, buff=SMALL_BUFF) + score.scale(2) + score.next_to(word, RIGHT, buff=1.5) + scores.add(score) + + # Introduce test + self.play( + LaggedStart( + FadeInFrom(test[0], 2 * RIGHT), + FadeInFrom(test[1], 2 * LEFT), + lag_ratio=0.3, + ) + ) + self.wait() + self.play( + MoveToTarget(test, lag_ratio=0.2), + FadeInFrom(day_labels, UP, lag_ratio=0.2), + ) + self.wait() + self.play( + *map(Restore, day_labels), + FadeInFrom(hour_labels, LEFT), + ) + self.wait() + + # Discuss problems + self.play( + FadeOut(test[1]), + FadeOut(time_labels[1]), + LaggedStartMap(ShowCreation, problem_rects), + run_time=1, + ) + self.play( + LaggedStart(*[ + FadeInFrom(word, LEFT) + for word in proof_words + ]), + LaggedStart(*[ + GrowArrow(arrow) + for arrow in proof_arrows + ]), + ) + self.wait() + self.play(FadeIn(scores)) + self.play( + LaggedStart(*[ + ChangeDecimalToValue(score[1], 7) + for score in scores + ], lag_ratio=0, rate_func=rush_into) + ) + self.wait() + + def get_test(self): + group = Group( + ImageMobject("imo_2011_p1"), + ImageMobject("imo_2011_p2"), + ) + group.set_height(6) + group.arrange(RIGHT, buff=LARGE_BUFF) + for page in group: + rect = SurroundingRectangle(page, buff=0.01) + rect.set_stroke(WHITE, 1) + page.add(rect) + # page.pixel_array[:, :, :3] = 255 - page.pixel_array[:, :, :3] + return group + + def get_problem_rects(self, page): + pw = page.get_width() + rects = VGroup(*[Rectangle() for x in range(3)]) + rects.set_stroke(width=2) + rects.set_color_by_gradient([BLUE_E, BLUE_C, BLUE_D]) + + rects.set_width(pw * 0.75) + for factor, rect in zip([0.095, 0.16, 0.1], rects): + rect.set_height(factor * pw, stretch=True) + rects.arrange(DOWN, buff=0.08) + rects.move_to(page) + rects.shift(0.09 * pw * DOWN) + return rects + + +class USProcess(IntroduceIMO): + CONFIG = { + } + + def construct(self): + self.add_flag_and_label() + self.show_tests() + self.show_imo() + + def add_flag_and_label(self): + flag = ImageMobject("flags/us") + flag.set_height(1) + flag.to_corner(UL) + label = VGroup(*map(TextMobject, "USA")) + label.arrange(RIGHT, buff=0.05, aligned_edge=DOWN) + label.set_width(0.8 * flag.get_width()) + label.next_to(flag, DOWN, buff=0.2 * flag.get_height()) + + self.add(flag, label) + + self.flag = flag + + def show_tests(self): + tests = VGroup( + self.get_test( + ["American ", "Mathematics ", "Content"], + n_questions=25, + time_string="75 minutes", + hours=1.25, + n_students=100000, + ), + self.get_test( + ["American ", "Invitational ", "Math ", "Exam"], + n_questions=15, + time_string="3 hours", + hours=3, + n_students=3500, + ), + self.get_test( + ["U", "S", "A ", "Math ", "Olympiad"], + n_questions=6, + time_string="$2 \\times 4.5$ hours", + hours=4.5, + n_students=500, + ), + self.get_test( + ["Mathematical ", "Olympiad ", "Program"], + n_questions=None, + time_string="3 weeks", + hours=None, + n_students=60 + ) + ) + amc, aime, usamo, mop = tests + + amc.to_corner(UR) + top_point = amc.get_top() + last_arrow = VectorizedPoint() + last_arrow.to_corner(DL) + next_anims = [] + + for test in tests: + test.move_to(top_point, UP) + test.shift_onto_screen() + self.play( + Write(test.name), + *next_anims, + run_time=1, + ) + self.wait() + self.animate_name_abbreviation(test) + self.wait() + + if isinstance(test.nq_label[0], Integer): + int_mob = test.nq_label[0] + n = int_mob.get_value() + int_mob.set_value(0) + self.play( + ChangeDecimalToValue(int_mob, n), + FadeIn(test.nq_label[1:]) + ) + else: + self.play(FadeIn(test.nq_label)) + + self.play( + FadeIn(test.t_label) + ) + self.wait() + + test.generate_target() + test.target.scale(0.575) + test.target.next_to(last_arrow, RIGHT, buff=SMALL_BUFF) + test.target.shift_onto_screen() + + next_anims = [ + MoveToTarget(test), + GrowArrow(last_arrow), + ] + last_arrow = Vector(0.5 * RIGHT) + last_arrow.set_color(WHITE) + last_arrow.next_to(test.target, RIGHT, SMALL_BUFF) + self.play(*next_anims) + + self.tests = tests + + def show_imo(self): + tests = self.tests + logo = ImageMobject("imo_logo") + logo.set_height(1) + name = TextMobject("IMO") + name.scale(2) + group = Group(logo, name) + group.arrange(RIGHT) + group.to_corner(UR) + group.shift(2 * LEFT) + + students = VGroup(*[ + PiCreature() + for x in range(6) + ]) + students.arrange_in_grid(n_cols=3, buff=LARGE_BUFF) + students.set_height(2) + students.next_to(group, DOWN) + colors = it.cycle([RED, LIGHT_GREY, BLUE]) + for student, color in zip(students, colors): + student.set_color(color) + student.save_state() + student.move_to(tests[-1]) + student.fade(1) + + self.play(FadeInFromDown(group)) + self.play( + LaggedStartMap( + Restore, students, + run_time=3, + lag_ratio=0.3, + ) + ) + self.play( + LaggedStart(*[ + ApplyMethod(student.change, "hooray") + for student in students + ]) + ) + for x in range(3): + self.play(Blink(random.choice(students))) + self.wait() + + # + def animate_name_abbreviation(self, test): + name = test.name + short_name = test.short_name + short_name.move_to(name, LEFT) + name.generate_target() + for p1, p2 in zip(name.target, short_name): + for letter in p1: + letter.move_to(p2[0]) + letter.set_opacity(0) + p1[0].set_opacity(1) + self.add(test.rect, test.name, test.ns_label) + self.play( + FadeIn(test.rect), + MoveToTarget(name), + FadeIn(test.ns_label), + ) + + test.remove(name) + test.add(short_name) + self.remove(name) + self.add(short_name) + + def get_test(self, name_parts, n_questions, time_string, hours, n_students): + T_COLOR = GREEN_B + Q_COLOR = YELLOW + + name = TextMobject(*name_parts) + short_name = TextMobject(*[np[0] for np in name_parts]) + if n_questions: + nq_label = VGroup( + Integer(n_questions), + TextMobject("questions") + ) + nq_label.arrange(RIGHT) + else: + nq_label = TextMobject("Lots of training") + nq_label.set_color(Q_COLOR) + + if time_string: + t_label = TextMobject(time_string) + t_label.set_color(T_COLOR) + else: + t_label = Integer(0).set_opacity(0) + + clock = Clock() + clock.hour_hand.set_opacity(0) + clock.minute_hand.set_opacity(0) + clock.set_stroke(WHITE, 2) + if hours: + sector = Sector( + start_angle=TAU / 4, + angle=-TAU * (hours / 12), + outer_radius=clock.get_width() / 2, + arc_center=clock.get_center() + ) + sector.set_fill(T_COLOR, 0.5) + sector.set_stroke(T_COLOR, 2) + clock.add(sector) + if hours == 4.5: + plus = TexMobject("+").scale(2) + plus.next_to(clock, RIGHT) + clock_copy = clock.copy() + clock_copy.next_to(plus, RIGHT) + clock.add(plus, clock_copy) + else: + clock.set_opacity(0) + clock.set_height(1) + clock.next_to(t_label, RIGHT, buff=MED_LARGE_BUFF) + t_label.add(clock) + + ns_label = TextMobject("$\\sim${:,} students".format(n_students)) + + result = VGroup( + name, + nq_label, + t_label, + ns_label, + ) + result.arrange( + DOWN, + buff=MED_LARGE_BUFF, + aligned_edge=LEFT, + ) + rect = SurroundingRectangle(result, buff=MED_SMALL_BUFF) + rect.set_width( + result[1:].get_width() + MED_LARGE_BUFF, + about_edge=LEFT, + stretch=True, + ) + rect.set_stroke(WHITE, 2) + rect.set_fill(BLACK, 1) + result.add_to_back(rect) + + result.name = name + result.short_name = short_name + result.nq_label = nq_label + result.t_label = t_label + result.ns_label = ns_label + result.rect = rect + result.clock = clock + + return result + + +class WindmillScene(Scene): + CONFIG = { + "dot_config": { + "fill_color": LIGHT_GREY, + "radius": 0.05, + "background_stroke_width": 1, + "background_stroke_color": BLACK, + }, + "windmill_style": { + "stroke_color": RED, + "stroke_width": 2, + "background_stroke_width": 3, + "background_stroke_color": BLACK, + }, + "windmill_length": FRAME_WIDTH + FRAME_HEIGHT, + # "windmill_rotation_speed": 0.25, + "windmill_rotation_speed": 0.5, + } + + def get_random_point_set(self, n_points=11, width=6, height=6): + return np.array([ + [ + -width / 2 + np.random.random() * width, + -height / 2 + np.random.random() * height, + 0 + ] + for n in range(n_points) + ]) + + def get_dots(self, points): + return VGroup(*[ + Dot(point, **self.dot_config) + for point in points + ]) + + def get_windmill(self, points, pivot=None, theta=TAU / 4): + line = Line(LEFT, RIGHT) + line.set_length(self.windmill_length) + line.set_angle(theta) + line.set_style(**self.windmill_style) + + line.point_set = points + + if pivot is not None: + line.pivot = pivot + else: + line.pivot = points[0] + + line.rot_speed = self.windmill_rotation_speed + + line.add_updater(lambda l: l.move_to(l.pivot)) + return line + + def get_pivot_dot(self, windmill, color=YELLOW): + pivot_dot = Dot(color=YELLOW) + pivot_dot.add_updater(lambda d: d.move_to(windmill.pivot)) + return pivot_dot + + def next_pivot_and_angle(self, windmill): + curr_angle = windmill.get_angle() + pivot = windmill.pivot + non_pivots = list(filter( + lambda p: not np.all(p == pivot), + windmill.point_set + )) + + angles = np.array([ + -(angle_of_vector(point - pivot) - curr_angle) % PI + for point in non_pivots + ]) + angles[angles < 1e-6] = np.inf + index = np.argmin(angles) + return non_pivots[index], angles[index] + + def rotate_to_next_pivot(self, windmill, max_time=None, added_anims=None): + """ + Returns animations to play following the contact + """ + new_pivot, angle = self.next_pivot_and_angle(windmill) + change_pivot_at_end = True + + if added_anims is None: + added_anims = [] + + run_time = angle / windmill.rot_speed + if max_time is not None and run_time > max_time: + ratio = max_time / run_time + rate_func = (lambda t: ratio * t) + run_time = max_time + change_pivot_at_end = False + else: + rate_func = linear + + self.play( + Rotate( + windmill, + -angle, + run_time=run_time, + rate_func=rate_func, + ), + *added_anims, + ) + + if change_pivot_at_end: + windmill.pivot = new_pivot + + def let_windmill_run(self, windmill, time): + start_time = self.get_time() + end_time = start_time + time + curr_time = start_time + while curr_time < end_time: + self.rotate_to_next_pivot( + windmill, + max_time=(end_time - curr_time) + ) + curr_time = self.get_time() + + +class WindmillTest(WindmillScene): + def construct(self): + points = self.get_random_point_set() + # points = np.array(list(sorted(points, key=lambda p: p[0]))) + + dots = self.get_dots(points) + windmill = self.get_windmill(points) + pivot_dot = self.get_pivot_dot(windmill) + + self.add(windmill) + self.add(dots) + self.add(pivot_dot) + + self.let_windmill_run(windmill, 10)