From 98b4427930f7581d0befe73ac608f236e0ba44db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 15 Nov 2016 20:08:28 -0800 Subject: [PATCH] Initial hanoi animations --- animation/simple_animations.py | 37 +- eoc/chapter1.py | 12 - extract_scene.py | 2 +- hanoi.py | 667 +++++++++++++++++++++++++++++++++ mobject/mobject.py | 5 +- scene/tk_scene.py | 12 +- 6 files changed, 708 insertions(+), 27 deletions(-) create mode 100644 hanoi.py diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 410f812c..2d102e57 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -253,18 +253,43 @@ class Succession(Animation): else: run_time = sum([anim.run_time for anim in animations]) self.num_anims = len(animations) - self.anims = animations - mobject = animations[0].mobject + self.anims = (animations) + mobject = Mobject(*[anim.mobject for anim in self.anims]) + self.last_index = 0 Animation.__init__(self, mobject, run_time = run_time, **kwargs) def __str__(self): return self.__class__.__name__ + \ "".join(map(str, self.anims)) - def update(self, alpha): + def update_mobject(self, alpha): scaled_alpha = alpha*self.num_anims - self.mobject = self.anims - for index in range(len(self.anims)): - self.anims[index].update(scaled_alpha - index) + index = min(int(scaled_alpha), len(self.anims)-1) + curr_anim = self.anims[index] + if index != self.last_index: + last_anim = self.anims[self.last_index] + last_anim.clean_up() + if last_anim.mobject is curr_anim.mobject: + self.anims[index].starting_mobject = curr_anim.mobject.copy() + self.anims[index].update_mobject(scaled_alpha - index) + self.last_index = index + + + + + + + + + + + + + + + + + + diff --git a/eoc/chapter1.py b/eoc/chapter1.py index 958898e9..554f7d1c 100644 --- a/eoc/chapter1.py +++ b/eoc/chapter1.py @@ -2661,18 +2661,6 @@ class Thumbnail(CircleScene): self.add(title, arrow, deriv_eq, int_eq) - # dR = 0.25 - # rings = VGroup(*[ - # self.get_ring(rad, 0.9*dR) - # for rad in np.arange(0, self.radius, dR) - # ]) - # for ring in rings: - # ring.add(ring.copy().rotate(np.pi)) - # rings.gradient_highlight(BLUE, GREEN_E) - # rings.next_to(title, DOWN) - - # self.add(title, rings) - diff --git a/extract_scene.py b/extract_scene.py index d2943cc3..e03a4712 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -22,7 +22,7 @@ HELP_MESSAGE = """ -l use low quality -m use medium quality -a run and save every scene in the script, or all args for the given scene - -q don't pring progress + -q don't print progress """ SCENE_NOT_FOUND_MESSAGE = """ That scene is not in the script diff --git a/hanoi.py b/hanoi.py new file mode 100644 index 00000000..a573a088 --- /dev/null +++ b/hanoi.py @@ -0,0 +1,667 @@ +from helpers import * + +from mobject.tex_mobject import TexMobject +from mobject import Mobject +from mobject.image_mobject import ImageMobject +from mobject.vectorized_mobject import * + +from animation.animation import Animation +from animation.transform import * +from animation.simple_animations import * +from animation.playground import * +from topics.geometry import * +from topics.characters import * +from topics.functions import * +from topics.fractals import * +from topics.number_line import * +from topics.combinatorics import * +from topics.numerals import * +from topics.three_dimensions import * +from topics.objects import * +from scene import Scene +from camera import Camera +from mobject.svg_mobject import * +from mobject.tex_mobject import * + +class CountingScene(Scene): + CONFIG = { + "base" : 10, + "power_colors" : [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D], + "counting_dot_starting_position" : (SPACE_WIDTH-1)*RIGHT + (SPACE_HEIGHT-1)*UP, + "count_dot_starting_radius" : 0.5, + "dot_configuration_height" : 2, + "ones_configuration_location" : UP+2*RIGHT, + "num_scale_factor" : 2, + "num_start_location" : 2*DOWN, + } + def setup(self): + self.dots = VGroup() + self.number = 0 + self.number_mob = VGroup(TexMobject(str(self.number))) + self.number_mob.scale(self.num_scale_factor) + self.number_mob.shift(self.num_start_location) + + self.initialize_configurations() + + self.arrows = VGroup() + + self.add(self.number_mob) + + def get_template_configuration(self): + #This should probably be replaced for non-base-10 counting scenes + down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN + result = [] + for down_right_steps in range(5): + for left_steps in range(down_right_steps): + result.append( + down_right_steps*down_right + left_steps*LEFT + ) + return reversed(result[:self.base]) + + def get_dot_template(self): + #This should be replaced for non-base-10 counting scenes + down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN + dots = VGroup(*[ + Dot( + point, + radius = 0.25, + fill_opacity = 0, + stroke_width = 2, + stroke_color = WHITE, + ) + for point in self.get_template_configuration() + ]) + dots.scale_to_fit_height(self.dot_configuration_height) + return dots + + def initialize_configurations(self): + self.dot_templates = [] + self.dot_template_iterators = [] + self.curr_configurations = [] + + def add_configuration(self): + new_template = self.get_dot_template() + new_template.move_to(self.ones_configuration_location) + left_vect = (new_template.get_width()+LARGE_BUFF)*LEFT + new_template.shift( + left_vect*len(self.dot_templates) + ) + self.dot_templates.append(new_template) + self.dot_template_iterators.append( + it.cycle(new_template) + ) + self.curr_configurations.append(VGroup()) + + def count(self, max_val, run_time_per_anim = 1): + for x in range(max_val): + self.increment(run_time_per_anim) + + def increment(self, run_time_per_anim = 1): + moving_dot = Dot( + self.counting_dot_starting_position, + radius = self.count_dot_starting_radius, + color = self.power_colors[0], + ) + moving_dot.generate_target() + moving_dot.set_fill(opacity = 0) + kwargs = { + "run_time" : run_time_per_anim + } + + continue_rolling_over = True + first_move = True + place = 0 + while continue_rolling_over: + added_anims = [] + if first_move: + added_anims += self.get_digit_increment_animations() + first_move = False + moving_dot.target.replace( + self.dot_template_iterators[place].next() + ) + self.play(MoveToTarget(moving_dot), *added_anims, **kwargs) + self.curr_configurations[place].add(moving_dot) + + + if len(self.curr_configurations[place].split()) == self.base: + full_configuration = self.curr_configurations[place] + self.curr_configurations[place] = VGroup() + place += 1 + center = full_configuration.get_center_of_mass() + radius = 0.6*max( + full_configuration.get_width(), + full_configuration.get_height(), + ) + circle = Circle( + radius = radius, + stroke_width = 0, + fill_color = self.power_colors[place], + fill_opacity = 0.5, + ) + circle.move_to(center) + moving_dot = VGroup(circle, full_configuration) + moving_dot.generate_target() + moving_dot[0].set_fill(opacity = 0) + else: + continue_rolling_over = False + + def get_digit_increment_animations(self): + result = [] + self.number += 1 + new_number_mob = self.get_number_mob(self.number) + new_number_mob.move_to(self.number_mob, RIGHT) + if self.is_perfect_power(): + self.add_configuration() + place = len(new_number_mob.split())-1 + result.append(FadeIn(self.dot_templates[place])) + arrow = Arrow( + new_number_mob[place].get_top(), + self.dot_templates[place].get_bottom(), + color = self.power_colors[place] + ) + self.arrows.add(arrow) + result.append(ShowCreation(arrow)) + result.append(Transform( + self.number_mob, new_number_mob, + submobject_mode = "lagged_start" + )) + return result + + def get_number_mob(self, num): + result = VGroup() + place = 0 + while num > 0: + digit = TexMobject(str(num % self.base)) + if place >= len(self.power_colors): + self.power_colors += self.power_colors + digit.highlight(self.power_colors[place]) + digit.scale(self.num_scale_factor) + digit.next_to(result, LEFT, buff = SMALL_BUFF, aligned_edge = DOWN) + result.add(digit) + num /= self.base + place += 1 + return result + + def is_perfect_power(self): + number = self.number + while number > 1: + if number%self.base != 0: + return False + number /= self.base + return True + + +class CountInDecimal(CountingScene): + def construct(self): + for x in range(11): + self.increment() + for x in range(85): + self.increment(0.25) + for x in range(20): + self.increment() + +class CountInTernary(CountingScene): + CONFIG = { + "base" : 3, + "dot_configuration_height" : 1, + "ones_configuration_location" : UP+4*RIGHT + } + def construct(self): + self.count(27) + + # def get_template_configuration(self): + # return [ORIGIN, UP] + +class CountInBinaryTo256(CountingScene): + CONFIG = { + "base" : 2, + "dot_configuration_height" : 1, + "ones_configuration_location" : UP+5*RIGHT + } + def construct(self): + self.count(128, 0.3) + + def get_template_configuration(self): + return [ORIGIN, UP] + + +class TowersOfHanoiScene(Scene): + CONFIG = { + "disk_start_and_end_colors" : [BLUE_E, BLUE_A], + "num_disks" : 5, + "peg_width" : 0.25, + "peg_height" : 2.5, + "disk_height" : 0.4, + "disk_min_width" : 1, + "disk_max_width" : 3, + } + def setup(self): + self.add_pegs() + self.add_disks() + + def add_pegs(self): + peg = Rectangle( + height = self.peg_height, + width = self.peg_width, + stroke_width = 0, + fill_color = GREY_BROWN, + fill_opacity = 1, + ) + peg.shift(UP) + self.pegs = VGroup(*[ + peg.copy().shift(vect) + for vect in 4*LEFT, ORIGIN, 4*RIGHT + ]) + self.peg_labels = VGroup(*[ + TexMobject(char).next_to(peg, DOWN) + for char, peg in zip("ABC", self.pegs) + ]) + self.add(self.pegs, self.peg_labels) + + def add_disks(self): + self.disks = VGroup(*[ + Rectangle( + height = self.disk_height, + width = width, + fill_color = color, + fill_opacity = 1, + stroke_width = 0, + ) + for width, color in zip( + np.linspace( + self.disk_min_width, + self.disk_max_width, + self.num_disks + ), + color_gradient( + self.disk_start_and_end_colors, + self.num_disks + ) + ) + ]) + for number, disk in enumerate(self.disks): + label = TexMobject(str(number)) + label.highlight(BLACK) + label.scale_to_fit_height(self.disk_height/2) + label.move_to(disk) + disk.add(label) + disk.label = label + self.reset_disks(run_time = 0) + + self.add(self.disks) + + def reset_disks(self, **kwargs): + self.disks.generate_target() + self.disks.target.arrange_submobjects(DOWN, buff = 0) + self.disks.target.move_to(self.pegs[0], DOWN) + self.play( + MoveToTarget(self.disks), + **kwargs + ) + self.disk_tracker = [ + set(range(self.num_disks)), + set([]), + set([]) + ] + + def disk_index_to_peg_index(self, disk_index): + for index, disk_set in enumerate(self.disk_tracker): + if disk_index in disk_set: + return index + raise Exception("Somehow this disk wasn't accounted for...") + + def min_disk_index_on_peg(self, peg_index): + disk_index_set = self.disk_tracker[peg_index] + if disk_index_set: + return min(self.disk_tracker[peg_index]) + else: + return self.num_disks + + def bottom_point_for_next_disk(self, peg_index): + min_disk_index = self.min_disk_index_on_peg(peg_index) + if min_disk_index >= self.num_disks: + return self.pegs[peg_index].get_bottom() + else: + return self.disks[min_disk_index].get_top() + + + def get_next_disk_0_peg(self): + curr_peg_index = self.disk_index_to_peg_index(0) + return (curr_peg_index+1)%3 + + def get_available_peg(self, disk_index): + if disk_index == 0: + return self.get_next_disk_0_peg() + for index in range(len(list(self.pegs))): + if self.min_disk_index_on_peg(index) > disk_index: + return index + raise Exception("Tower's of Honoi rule broken: No available disks") + + def move_disk(self, disk_index, **kwargs): + next_peg_index = self.get_available_peg(disk_index) + self.move_disks_to_peg([disk_index], next_peg_index, **kwargs) + + def move_subtower_to_peg(self, num_disks, next_peg_index, **kwargs): + disk_indices = range(num_disks) + peg_indices = map(self.disk_index_to_peg_index, disk_indices) + if len(set(peg_indices)) != 1: + warnings.warn("These disks don't make up a tower right now") + self.move_disks_to_peg(disk_indices, next_peg_index, **kwargs) + + def move_disks_to_peg(self, disk_indices, next_peg_index, run_time = 1, stay_on_peg = True, added_anims = []): + disks = VGroup(*[self.disks[index] for index in disk_indices]) + max_disk_index = max(disk_indices) + next_peg = self.pegs[next_peg_index] + curr_peg_index = self.disk_index_to_peg_index(max_disk_index) + curr_peg = self.pegs[curr_peg_index] + if self.min_disk_index_on_peg(curr_peg_index) != max_disk_index: + warnings.warn("Tower's of Hanoi rule broken: disk has crap on top of it") + target_bottom_point = self.bottom_point_for_next_disk(next_peg_index) + path_arc = np.sign(curr_peg_index-next_peg_index)*np.pi/3 + if stay_on_peg: + self.play( + Succession( + ApplyMethod(disks.next_to, curr_peg, UP, 0), + ApplyMethod(disks.next_to, next_peg, UP, 0, path_arc = path_arc), + ApplyMethod(disks.move_to, target_bottom_point, DOWN), + ), + *added_anims, + run_time = run_time, + rate_func = lambda t : smooth(t, 2) + ) + else: + self.play( + ApplyMethod(disks.move_to, target_bottom_point, DOWN), + *added_anims, + path_arc = path_arc*2, + run_time = run_time, + rate_func = lambda t : smooth(t, 2) + ) + for disk_index in disk_indices: + self.disk_tracker[curr_peg_index].remove(disk_index) + self.disk_tracker[next_peg_index].add(disk_index) + +class ConstrainedTowersOfHanoiScene(TowersOfHanoiScene): + def get_next_disk_0_peg(self): + if not hasattr(self, "total_disk_0_movements"): + self.total_disk_0_movements = 0 + curr_peg_index = self.disk_index_to_peg_index(0) + if (self.total_disk_0_movements/2)%2 == 0: + result = curr_peg_index + 1 + else: + result = curr_peg_index - 1 + self.total_disk_0_movements += 1 + return result + +def get_ruler_sequence(order = 4): + if order == -1: + return [] + else: + smaller = get_ruler_sequence(order - 1) + return smaller + [order] + smaller + +def get_ternary_ruler_sequence(order = 4): + if order == -1: + return [] + else: + smaller = get_ternary_ruler_sequence(order-1) + return smaller+[order]+smaller+[order]+smaller + +class SolveHanoi(TowersOfHanoiScene): + def construct(self): + self.dither() + for x in get_ruler_sequence(self.num_disks-1): + self.move_disk(x, stay_on_peg = False) + self.dither() + +class SolveConstrainedHanoi(ConstrainedTowersOfHanoiScene): + def construct(self): + self.dither() + for x in get_ternary_ruler_sequence(self.num_disks-1): + self.move_disk(x, run_time = 0.5, stay_on_peg = False) + self.dither() + +class Keith(PiCreature): + CONFIG = { + "color" : GREEN_D + } + + +#################### + +class IntroduceKeith(Scene): + def construct(self): + morty = Mortimer(mode = "happy") + keith = Keith(mode = "dance_kick") + keith_image = ImageMobject("keith_schwarz", invert = False) + # keith_image = Rectangle() + keith_image.scale_to_fit_height(2*SPACE_HEIGHT - 2) + keith_image.next_to(ORIGIN, LEFT) + keith.move_to(keith_image, DOWN+RIGHT) + morty.next_to(keith, buff = LARGE_BUFF, aligned_edge = DOWN) + morty.make_eye_contact(keith) + + bubble = keith.get_bubble("speech") + bubble.write("Check this out...") + bubble.resize_to_content() + bubble.pin_to(keith) + VGroup(bubble, bubble.content).shift(DOWN) + + title = TextMobject("Keith Schwarz (Computer scientist)") + title.to_edge(UP) + + self.add(keith_image, morty) + self.play(Write(title)) + self.play(FadeIn(keith, run_time = 2)) + self.play(FadeOut(keith_image), Animation(keith)) + self.play(Blink(morty)) + self.play( + keith.change_mode, "speaking", + keith.scale_to_fit_height, morty.get_height(), + keith.next_to, morty, LEFT, LARGE_BUFF, + run_time = 1.5 + ) + self.play( + ShowCreation(bubble), + Write(bubble.content) + ) + self.play( + morty.change_mode, "pondering", + morty.look_at, bubble + ) + self.play(Blink(keith)) + self.dither() + +class IntroduceTowersOfHanoi(TowersOfHanoiScene): + def construct(self): + self.clear() + self.add_title() + self.show_setup() + self.note_disk_labels() + self.show_more_disk_possibility() + self.move_full_tower() + self.move_single_disk() + self.cannot_move_disk_with_crap_on_top() + self.cannot_move_disk_onto_smaller_disk() + + def add_title(self): + title = TextMobject("Towers of Hanoi") + title.to_edge(UP) + self.add(title) + self.title = title + + def show_setup(self): + self.pegs.save_state() + bottom = self.pegs.get_bottom() + self.pegs.stretch_to_fit_height(0) + self.pegs.move_to(bottom) + self.play( + ApplyMethod( + self.pegs.restore, + submobject_mode = "lagged_start", + run_time = 2 + ), + ) + self.play(Write(self.peg_labels)) + self.dither() + self.bring_in_disks() + self.dither() + + def bring_in_disks(self): + peg = self.pegs[0] + disk_groups = VGroup() + for disk in self.disks: + top = Circle(radius = disk.get_width()/2) + inner = Circle(radius = self.peg_width/2) + inner.flip() + top.add_subpath(inner.points) + top.set_stroke(width = 0) + top.set_fill(disk.get_color()) + top.rotate(np.pi/2, RIGHT) + top.move_to(disk, UP) + group = VGroup(disk, top) + group.original_location = group.get_center() + group.next_to(peg, UP, 0) + group.save_state() + group.rotate_in_place(-np.pi/2, RIGHT) + disk.set_fill(opacity = 0) + disk_groups.add(group) + disk_groups.arrange_submobjects() + disk_groups.next_to(self.peg_labels, DOWN) + + self.play(FadeIn( + disk_groups, + run_time = 2, + submobject_mode = "lagged_start" + )) + for group in reversed(list(disk_groups)): + self.play(group.restore) + self.play(group.move_to, group.original_location) + self.remove(disk_groups) + self.add(self.disks) + + def note_disk_labels(self): + labels = [disk.label for disk in self.disks] + last = VGroup().save_state() + for label in labels: + label.save_state() + self.play( + label.scale_in_place, 2, + label.highlight, YELLOW, + last.restore, + run_time = 0.5 + ) + last = label + self.play(last.restore) + self.dither() + + def show_more_disk_possibility(self): + original_num_disks = self.num_disks + original_disk_height = self.disk_height + original_disks = self.disks + original_disks_copy = original_disks.copy() + + #Hacky + self.num_disks = 10 + self.disk_height = 0.3 + self.add_disks() + new_disks = self.disks + self.disks = original_disks + self.remove(new_disks) + + self.play(Transform(self.disks, new_disks)) + self.dither() + self.play(Transform(self.disks, original_disks_copy)) + + self.remove(self.disks) + self.disks = original_disks_copy + self.add(self.disks) + + self.num_disks = original_num_disks + self.disk_height = original_disk_height + + def move_full_tower(self): + self.move_subtower_to_peg(self.num_disks, 1) + self.dither() + self.reset_disks(run_time = 1, submobject_mode = "lagged_start") + self.dither() + + def move_single_disk(self): + for x in 0, 1, 0: + self.move_disk(x) + self.dither() + + def cannot_move_disk_with_crap_on_top(self): + not_allowed = TextMobject("Not allowed") + not_allowed.to_edge(UP) + not_allowed.highlight(RED) + cross = TexMobject("\\times") + cross.set_fill(RED, opacity = 0.5) + + disk = self.disks[3] + disk.save_state() + self.move_disks_to_peg([3], 1, added_anims = [ + Transform(self.title, not_allowed) + ]) + cross.replace(disk, stretch = False) + self.play(FadeIn(cross)) + self.dither() + self.play( + FadeOut(cross), + disk.restore + ) + + def cannot_move_disk_onto_smaller_disk(self): + also_not_allowed = TextMobject("Also not allowed") + also_not_allowed.to_edge(UP) + also_not_allowed.highlight(RED) + cross = TexMobject("\\times") + cross.set_fill(RED, opacity = 0.5) + + disk = self.disks[2] + disk.save_state() + self.move_disks_to_peg([2], 2, added_anims = [ + Transform(self.title, also_not_allowed) + ]) + cross.replace(disk) + self.play(FadeIn(cross)) + self.dither() + self.play( + FadeOut(cross), + FadeOut(self.title), + disk.restore + ) + self.dither() + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mobject/mobject.py b/mobject/mobject.py index 43262fbb..e4dd73c2 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -13,9 +13,7 @@ from helpers import * class Mobject(object): """ Mathematical Object - """ - #Number of numbers used to describe a point (3 for pos, 3 for normal vector) CONFIG = { "color" : WHITE, "stroke_width" : DEFAULT_POINT_THICKNESS, @@ -371,6 +369,9 @@ class Mobject(object): ## def save_state(self): + if hasattr(self, "saved_state"): + #Prevent exponential growth of data + self.saved_state = None self.saved_state = self.copy() return self diff --git a/scene/tk_scene.py b/scene/tk_scene.py index dac217b7..cd7e8467 100644 --- a/scene/tk_scene.py +++ b/scene/tk_scene.py @@ -18,14 +18,15 @@ class TkSceneRoot(Tkinter.Tk): self.frame.pack() self.canvas = Tkinter.Canvas(self.frame, **kwargs) self.canvas.configure(background='black') - self.canvas.place(x=0,y=0) + self.canvas.place(x=0, y=0) last_time = time.time() for frame in it.cycle(scene.frames): - try: - self.show_new_image(frame) - except: - break + # try: + # self.show_new_image(frame) + # except: + # break + self.show_new_image(frame) sleep_time = scene.frame_duration sleep_time -= time.time() - last_time time.sleep(max(0, sleep_time)) @@ -34,7 +35,6 @@ class TkSceneRoot(Tkinter.Tk): def show_new_image(self, frame): image = Image.fromarray(frame).convert('RGB') - image.resize(self.frame.size()) photo = ImageTk.PhotoImage(image) self.canvas.delete(Tkinter.ALL) self.canvas.create_image(