diff --git a/borsuk.py b/borsuk.py new file mode 100644 index 00000000..288f9efc --- /dev/null +++ b/borsuk.py @@ -0,0 +1,552 @@ +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 scene import Scene +from camera import Camera, ShadingCamera +from mobject.svg_mobject import * +from mobject.tex_mobject import * + +class Jewel(VMobject): + CONFIG = { + "color" : WHITE, + "fill_opacity" : 0.75, + "stroke_width" : 0, + "propogate_style_to_family" : True, + "height" : 0.5, + "num_equator_points" : 5, + "sun_vect" : OUT+LEFT+UP, + } + def generate_points(self): + for vect in OUT, IN: + compass_vects = list(compass_directions(self.num_equator_points)) + if vect is IN: + compass_vects.reverse() + for vect_pair in adjascent_pairs(compass_vects): + self.add(Polygon(vect, *vect_pair)) + self.scale_to_fit_height(self.height) + self.rotate(-np.pi/2-np.pi/24, RIGHT) + self.rotate(-np.pi/12, UP) + self.submobjects.sort(lambda m1, m2 : cmp(-m1.get_center()[2], -m2.get_center()[2])) + return self + +################ + +class CheckOutMathologer(PiCreatureScene): + CONFIG = { + "logo_height" : 1.5, + "screen_height" : 5 + } + def construct(self): + logo = ImageMobject("Mathologer_logo") + logo.scale_to_fit_height(self.logo_height) + logo.to_corner(UP+LEFT) + name = TextMobject("Mathologer") + name.next_to(logo, RIGHT) + + rect = Rectangle(height = 9, width = 16) + rect.scale_to_fit_height(self.screen_height) + rect.next_to(logo, DOWN) + rect.to_edge(LEFT) + + logo.save_state() + logo.shift(DOWN) + logo.highlight(BLACK) + self.play( + logo.restore, + self.pi_creature.change_mode, "hooray", + ) + self.play( + ShowCreation(rect), + Write(name) + ) + self.dither(2) + self.change_mode("happy") + self.dither(2) + +class IntroduceStolenNecklaceProblem(Scene): + CONFIG = { + "camera_class" : ShadingCamera, + "jewel_colors" : [BLUE, GREEN, WHITE, RED], + "num_per_jewel" : [8, 10, 4, 6], + "num_shuffles" : 1, + "random_seed" : 2, + "forced_binary_choices" : (0, 1, 0, 1, 0), + "show_matching_after_divvying" : True, + } + def construct(self): + random.seed(self.random_seed) + self.add_thieves() + self.write_title() + self.introduce_necklace() + self.divvy_by_cutting_all() + self.divvy_with_n_cuts() + self.shuffle_jewels(self.necklace.jewels) + self.divvy_with_n_cuts() + + def add_thieves(self): + thieves = VGroup( + Randolph(), + Mortimer() + ) + thieves.arrange_submobjects(RIGHT, buff = 4*LARGE_BUFF) + thieves.to_edge(DOWN) + thieves[0].make_eye_contact(thieves[1]) + + self.add(thieves) + self.thieves = thieves + + def write_title(self): + title = TextMobject("Stolen necklace problem") + title.to_edge(UP) + self.play( + Write(title), + *[ + ApplyMethod(pi.look_at, title) + for pi in self.thieves + ] + ) + self.title = title + + def introduce_necklace(self): + necklace = self.get_necklace() + jewels = necklace.jewels + jewel_types = self.get_jewels_organized_by_type(jewels) + + enumeration_labels = VGroup() + for jewel_type in jewel_types: + num_mob = TexMobject(str(len(jewel_type))) + jewel_copy = jewel_type[0].copy().scale(2) + jewel_copy.next_to(num_mob) + label = VGroup(num_mob, jewel_copy) + enumeration_labels.add(label) + enumeration_labels.arrange_submobjects(RIGHT, buff = LARGE_BUFF) + enumeration_labels.to_edge(UP) + + self.play( + FadeIn( + necklace, + submobject_mode = "lagged_start", + run_time = 3 + ), + *it.chain(*[ + [pi.change_mode, "conniving", pi.look_at, necklace] + for pi in self.thieves + ]) + ) + self.play(*[ + ApplyMethod( + jewel.rotate_in_place, np.pi/6, UP, + rate_func = there_and_back + ) + for jewel in jewels + ]) + self.play(Blink(self.thieves[0])) + self.dither() + for x in range(self.num_shuffles): + self.shuffle_jewels(jewels) + self.play(FadeOut(self.title)) + for jewel_type, label in zip(jewel_types, enumeration_labels): + jewel_type.submobjects.sort(lambda m1, m2: cmp(m1.get_center()[0], m2.get_center()[0])) + jewel_type.save_state() + jewel_type.generate_target() + jewel_type.target.arrange_submobjects() + jewel_type.target.scale(2) + jewel_type.target.move_to(2*UP) + self.play( + MoveToTarget(jewel_type), + Write(label) + ) + self.play(jewel_type.restore) + self.play(Blink(self.thieves[1])) + + self.enumeration_labels = enumeration_labels + self.jewel_types = jewel_types + + def divvy_by_cutting_all(self): + enumeration_labels = self.enumeration_labels + necklace = self.necklace + jewel_types = self.jewel_types + thieves = self.thieves + + both_half_labels = VGroup() + for thief, vect in zip(self.thieves, [LEFT, RIGHT]): + half_labels = VGroup() + for label in enumeration_labels: + tex, jewel = label + num = int(tex.get_tex_string()) + half_label = VGroup( + TexMobject(str(num/2)), + jewel.copy() + ) + half_label.arrange_submobjects() + half_labels.add(half_label) + half_labels.arrange_submobjects(DOWN) + half_labels.scale_to_fit_height(thief.get_height()) + half_labels.next_to( + thief, vect, + buff = MED_LARGE_BUFF, + aligned_edge = DOWN + ) + both_half_labels.add(half_labels) + + for half_labels in both_half_labels: + self.play(ReplacementTransform( + enumeration_labels.copy(), + half_labels + )) + self.play(*[ApplyMethod(pi.change_mode, "pondering") for pi in thieves]) + self.dither() + + for type_index, jewel_type in enumerate(jewel_types): + jewel_type.save_state() + jewel_type_copy = jewel_type.copy() + n_jewels = len(jewel_type) + halves = [ + VGroup(*jewel_type_copy[:n_jewels/2]), + VGroup(*jewel_type_copy[n_jewels/2:]), + ] + for half, thief, vect in zip(halves, thieves, [RIGHT, LEFT]): + half.arrange_submobjects(DOWN) + half.next_to( + thief, vect, + buff = SMALL_BUFF + type_index*half.get_width(), + aligned_edge = DOWN + ) + self.play( + Transform(jewel_type, jewel_type_copy), + *[ + ApplyMethod(thief.look_at, jewel_type_copy) + for thief in thieves + ] + ) + self.play(*it.chain(*[ + [thief.change_mode, "happy", thief.look_at, necklace] + for thief in thieves + ])) + self.dither() + self.play(*[ + jewel_type.restore + for jewel_type in jewel_types + ]) + self.play(*it.chain(*[ + [thief.change_mode, "confused", thief.look_at, necklace] + for thief in thieves + ])) + + def divvy_with_n_cuts(self): + necklace = self.necklace + jewel_types = self.jewel_types + thieves = self.thieves + jewels = sorted( + necklace.jewels, + lambda m1, m2 : cmp(m1.get_center()[0], m2.get_center()[0]) + ) + slice_indices, binary_choices = self.find_slice_indices(jewels, jewel_types) + subgroups = [ + VGroup(*jewels[i1:i2]) + for i1, i2 in zip(slice_indices, slice_indices[1:]) + ] + buff = (jewels[1].get_left()[0]-jewels[0].get_right()[0])/2 + v_lines = VGroup(*[ + DashedLine(UP, DOWN).next_to(group, RIGHT, buff = buff) + for group in subgroups[:-1] + ]) + strand_groups = [VGroup(), VGroup()] + for group, choice in zip(subgroups, binary_choices): + strand = Line( + group[0].get_center(), group[-1].get_center(), + color = necklace.line.get_color() + ) + strand.add(*group) + strand_groups[choice].add(strand) + self.add(strand) + + self.play(ShowCreation(v_lines)) + self.play( + FadeOut(necklace.line), + *it.chain(*[ + map(Animation, group) + for group in strand_groups + ]) + ) + for group in strand_groups: + group.save_state() + self.play( + strand_groups[0].shift, UP/2., + strand_groups[1].shift, DOWN/2., + ) + self.play(*it.chain(*[ + [thief.change_mode, "happy", thief.look_at, self.necklace] + for thief in thieves + ])) + self.play(Blink(thieves[1])) + + for group in strand_groups: + box = Rectangle( + width = group.get_width()+2*SMALL_BUFF, + height = group.get_height()+2*SMALL_BUFF, + stroke_width = 0, + fill_color = YELLOW, + fill_opacity = 0.3, + ) + box.move_to(group) + self.play(FadeIn(box)) + self.dither() + self.play(FadeOut(box)) + + self.dither() + if self.show_matching_after_divvying: + for jewel_type in jewel_types: + self.play( + *it.chain(*[ + [ + jewel.scale_in_place, 2, + jewel.rotate_in_place, np.pi/12, UP, + ] + for jewel in jewel_type + ]), + rate_func = there_and_back, + run_time = 2 + ) + self.dither() + self.play( + FadeOut(v_lines), + FadeIn(necklace.line), + *[ + group.restore for group in strand_groups + ] + ) + self.remove(*strand_groups) + self.add(necklace) + + ######## + + def get_necklace(self): + colors = reduce(op.add, [ + num*[color] + for num, color in zip(self.num_per_jewel, self.jewel_colors) + ]) + jewels = VGroup(*[ + Jewel(color = color) + for color in colors + ]) + jewels.arrange_submobjects() + jewels.scale_to_fit_width(2*SPACE_WIDTH-1) + jewels.center().shift(UP) + + necklace = VGroup() + necklace.line = Line( + jewels[0].get_center(), + jewels[-1].get_center(), + color = GREY + ) + necklace.jewels = jewels + necklace.add(necklace.line, *jewels) + + self.necklace = necklace + return necklace + + def get_jewels_organized_by_type(self, jewels): + return [ + VGroup(*filter(lambda m : m.get_color() == color, jewels)) + for color in map(Color, self.jewel_colors) + ] + + def shuffle_jewels(self, jewels, run_time = 2, path_arc = np.pi/2, **kwargs): + shuffled_indices = range(len(jewels)) + random.shuffle(shuffled_indices) + target_group = VGroup(*[ + jewel.copy().move_to(jewels[shuffled_indices[i]]) + for i, jewel in enumerate(jewels) + ]) + self.play(Transform( + jewels, target_group, + run_time = run_time, + path_arc = path_arc, + **kwargs + )) + + def find_slice_indices(self, jewels, jewel_types): + + def jewel_to_type_number(jewel): + for i, jewel_type in enumerate(jewel_types): + if jewel in jewel_type: + return i + raise Exception("Not in any jewel_types") + type_numbers = map(jewel_to_type_number, jewels) + + n_types = len(jewel_types) + for slice_indices in it.combinations(range(1, len(jewels)), n_types): + slice_indices = [0] + list(slice_indices) + [len(jewels)] + if self.forced_binary_choices is not None: + all_binary_choices = [self.forced_binary_choices] + else: + all_binary_choices = it.product(*[range(2)]*(n_types+1)) + for binary_choices in all_binary_choices: + subsets = [ + type_numbers[i1:i2] + for i1, i2 in zip(slice_indices, slice_indices[1:]) + ] + left_sets, right_sets = [ + [ + subset + for subset, index in zip(subsets, binary_choices) + if index == target_index + ] + for target_index in range(2) + ] + flat_left_set = np.array(list(it.chain(*left_sets))) + flat_right_set = np.array(list(it.chain(*right_sets))) + + + match_array = [ + sum(flat_left_set == n) == sum(flat_right_set == n) + for n in range(n_types) + ] + if np.all(match_array): + return slice_indices, binary_choices + raise Exception("No fair division found") + +class FiveJewelCase(IntroduceStolenNecklaceProblem): + CONFIG = { + "jewel_colors" : [BLUE, GREEN, WHITE, RED, YELLOW], + "num_per_jewel" : [6, 4, 4, 2, 8], + "forced_binary_choices" : (0, 1, 0, 1, 0, 1), + } + def construct(self): + random.seed(self.random_seed) + self.add(self.get_necklace()) + jewels = self.necklace.jewels + self.shuffle_jewels(jewels, run_time = 0) + self.jewel_types = self.get_jewels_organized_by_type(jewels) + self.add_title() + self.add_thieves() + for thief in self.thieves: + ApplyMethod(thief.change_mode, "pondering").update(1) + thief.look_at(self.necklace) + self.divvy_with_n_cuts() + + def add_title(self): + n_cuts = len(self.jewel_colors) + title = TextMobject( + "%d jewel types, %d cuts"%(n_cuts, n_cuts) + ) + title.to_edge(UP) + self.add(title) + +class SixJewelCase(FiveJewelCase): + CONFIG = { + "jewel_colors" : [BLUE, GREEN, WHITE, RED, YELLOW, MAROON_B], + "num_per_jewel" : [6, 4, 4, 2, 2, 6], + "forced_binary_choices" : (0, 1, 0, 1, 0, 1, 0), + } + +class DiscussApplicability(TeacherStudentsScene): + def construct(self): + self.teacher_says(""" + Minize sharding, + allocate resources evenly + """) + self.change_student_modes(*["pondering"]*3) + self.dither(2) + +class ThreeJewelCase(FiveJewelCase): + CONFIG = { + "jewel_colors" : [BLUE, GREEN, WHITE], + "num_per_jewel" : [6, 4, 8], + "forced_binary_choices" : (0, 1, 0, 1), + } + +class RepeatedShuffling(IntroduceStolenNecklaceProblem): + CONFIG = { + "num_shuffles" : 5, + "random_seed" : 3, + "show_matching_after_divvying" : False, + } + def construct(self): + random.seed(self.random_seed) + self.add(self.get_necklace()) + jewels = self.necklace.jewels + self.jewel_types = self.get_jewels_organized_by_type(jewels) + self.add_thieves() + for thief in self.thieves: + ApplyMethod(thief.change_mode, "pondering").update(1) + thief.look_at(self.necklace) + + for x in range(self.num_shuffles): + self.shuffle_jewels(jewels) + self.divvy_with_n_cuts() + +class NowForTheTopology(TeacherStudentsScene): + def construct(self): + self.teacher_says("Now for the \\\\ topology") + self.change_student_modes(*["hooray"]*3) + self.dither(3) + +class ExternallyAnimatedScene(Scene): + def construct(self): + raise Exception("Don't actually run this class.") + +class SphereOntoPlaneIn3D(ExternallyAnimatedScene): + pass + +class DiscontinuousSphereOntoPlaneIn3D(ExternallyAnimatedScene): + pass + +class WriteNotAllowed(Scene): + def construct(self): + words = TextMobject("Not allowed") + words.highlight(RED) + words.scale(2) + self.play(Write(words)) + self.dither(2) + +class ManyPointsLandingOnEachOtherIn3D(ExternallyAnimatedScene): + pass + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/camera.py b/camera.py index 9736327b..e645cdda 100644 --- a/camera.py +++ b/camera.py @@ -128,15 +128,21 @@ class Camera(object): def get_pen_and_fill(self, vmobject): pen = aggdraw.Pen( - vmobject.get_stroke_color().get_hex_l(), + self.get_stroke_color(vmobject).get_hex_l(), max(vmobject.stroke_width, 0) ) fill = aggdraw.Brush( - vmobject.get_fill_color().get_hex_l(), + self.get_fill_color(vmobject).get_hex_l(), opacity = int(255*vmobject.get_fill_opacity()) ) return (pen, fill) + def get_stroke_color(self, vmobject): + return vmobject.get_stroke_color() + + def get_fill_color(self, vmobject): + return vmobject.get_fill_color() + def get_pathstring(self, vmobject): result = "" for mob in [vmobject]+vmobject.get_subpath_mobjects(): @@ -282,9 +288,46 @@ class MovingCamera(Camera): 0 if self.aligned_dimension == "height" else 1 ) - - - +class ShadingCamera(Camera): + CONFIG = { + # "sun_vect" : OUT+LEFT+UP, + "sun_vect" : UP+LEFT, + "shading_factor" : 0.5, + } + def __init__(self, *args, **kwargs): + Camera.__init__(self, *args, **kwargs) + self.unit_sun_vect = self.sun_vect/np.linalg.norm(self.sun_vect) + + def get_stroke_color(self, vmobject): + return Color(rgb = self.get_shaded_rgb( + color_to_rgb(vmobject.get_stroke_color()), + normal_vect = self.get_unit_normal_vect(vmobject) + )) + + def get_fill_color(self, vmobject): + return Color(rgb = self.get_shaded_rgb( + color_to_rgb(vmobject.get_fill_color()), + normal_vect = self.get_unit_normal_vect(vmobject) + )) + + def get_shaded_rgb(self, rgb, normal_vect): + brightness = np.dot(normal_vect, self.unit_sun_vect) + if brightness > 0: + alpha = self.shading_factor*brightness + return interpolate(rgb, np.ones(3), alpha) + else: + alpha = -self.shading_factor*brightness + return interpolate(rgb, np.zeros(3), alpha) + + def get_unit_normal_vect(self, vmobject): + anchors = vmobject.get_anchors() + if len(anchors) < 3: + return OUT + normal = np.cross(anchors[1]-anchors[0], anchors[2]-anchors[1]) + length = np.linalg.norm(normal) + if length == 0: + return OUT + return normal/length diff --git a/helpers.py b/helpers.py index 5b095332..229e68d0 100644 --- a/helpers.py +++ b/helpers.py @@ -530,7 +530,7 @@ def angle_of_vector(vector): z = complex(*vector[:2]) if z == 0: return 0 - return np.log(complex(*vector[:2])).imag + return np.angle(complex(*vector[:2])) diff --git a/scene/scene.py b/scene/scene.py index 2ac2ff12..87c17e31 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -20,6 +20,7 @@ from animation.transform import MoveToTarget, Transform class Scene(object): CONFIG = { + "camera_class" : Camera, "camera_config" : {}, "frame_duration" : LOW_QUALITY_FRAME_DURATION, "construct_args" : [], @@ -27,7 +28,7 @@ class Scene(object): } def __init__(self, **kwargs): digest_config(self, kwargs) - self.camera = Camera(**self.camera_config) + self.camera = self.camera_class(**self.camera_config) self.frames = [] self.mobjects = [] self.num_plays = 0