From ac137eb68393752637f63019c43976c3a5312a00 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 8 Feb 2017 13:29:32 -0800 Subject: [PATCH] A few bug fixes to the very buggy SVGMobject --- borsuk.py | 470 +++++++++++++++++++++++++++++++++++++---- eoc/chapter1.py | 9 +- mobject/mobject.py | 9 + mobject/svg_mobject.py | 14 +- topics/objects.py | 20 ++ 5 files changed, 480 insertions(+), 42 deletions(-) diff --git a/borsuk.py b/borsuk.py index e9fef0a8..7c5019d5 100644 --- a/borsuk.py +++ b/borsuk.py @@ -22,6 +22,7 @@ from camera import Camera, ShadingCamera from mobject.svg_mobject import * from mobject.tex_mobject import * +from eoc.chapter1 import PatreonThanks from eoc.graph_scene import GraphScene class Jewel(VMobject): @@ -89,6 +90,10 @@ class Necklace(VMobject): ################ +class ThisIsGoingToBeGood(TeacherStudentsScene): + def construct(self): + pass + class CheckOutMathologer(PiCreatureScene): CONFIG = { "logo_height" : 1.5, @@ -98,12 +103,7 @@ class CheckOutMathologer(PiCreatureScene): "logo_color" : None, } def construct(self): - logo = ImageMobject(self.logo_file) - logo.scale_to_fit_height(self.logo_height) - logo.to_corner(UP+LEFT) - if self.logo_color is not None: - logo.highlight(self.logo_color) - logo.stroke_width = 1 + logo = self.get_logo() name = TextMobject(self.channel_name) name.next_to(logo, RIGHT) @@ -112,11 +112,8 @@ class CheckOutMathologer(PiCreatureScene): rect.next_to(logo, DOWN) rect.to_edge(LEFT) - logo.save_state() - logo.shift(DOWN) - logo.highlight(BLACK) self.play( - logo.restore, + self.get_logo_intro_animation(logo), self.pi_creature.change_mode, "hooray", ) self.play( @@ -127,6 +124,21 @@ class CheckOutMathologer(PiCreatureScene): self.change_mode("happy") self.dither(2) + def get_logo(self): + logo = ImageMobject(self.logo_file) + logo.scale_to_fit_height(self.logo_height) + logo.to_corner(UP+LEFT) + if self.logo_color is not None: + logo.highlight(self.logo_color) + logo.stroke_width = 1 + return logo + + def get_logo_intro_animation(self, logo): + logo.save_state() + logo.shift(DOWN) + logo.highlight(BLACK) + return ApplyMethod(logo.restore) + class IntroduceStolenNecklaceProblem(Scene): CONFIG = { "camera_class" : ShadingCamera, @@ -688,6 +700,20 @@ class PointOutVSauce(CheckOutMathologer): "logo_height" : 1, "logo_color" : GREY, } + def get_logo(self): + logo = SVGMobject(file_name = self.logo_file) + logo.scale_to_fit_height(self.logo_height) + logo.to_corner(UP+LEFT) + logo.set_stroke(width = 0) + logo.set_fill(GREEN) + logo.sort_submobjects() + return logo + + def get_logo_intro_animation(self, logo): + return DrawBorderThenFill( + logo, + run_time = 2, + ) class WalkEquatorPostTransform(GraphScene): CONFIG = { @@ -1097,6 +1123,7 @@ class GeneralizeBorsukUlam(Scene): arrow.next_to(sphere_set, RIGHT) f.next_to(arrow, UP) output_space.next_to(arrow, RIGHT) + equation.next_to(sphere_set, DOWN, buff = LARGE_BUFF) equation.to_edge(RIGHT) lhs = VGroup(*equation[:2]) eq = equation[2] @@ -1196,11 +1223,6 @@ class GeneralizeBorsukUlam(Scene): return group -# class FourDBorsukUlam(GeneralizeBorsukUlam): -# CONFIG = { -# "n_dims" : 4, -# } - # class FiveDBorsukUlam(GeneralizeBorsukUlam): # CONFIG = { # "n_dims" : 5, @@ -1714,10 +1736,11 @@ class ChoicesInNecklaceCutting(Scene): ###### - def get_groups(self): + def get_groups(self, indices = None): segments, tick_marks = self.necklace - n_segments = len(segments) - indices = [0, n_segments/6, n_segments/2, n_segments] + if indices is None: + n_segments = len(segments) + indices = [0, n_segments/6, n_segments/2, n_segments] groups = [ VGroup( @@ -1727,8 +1750,8 @@ class ChoicesInNecklaceCutting(Scene): for i1, i2 in zip(indices, indices[1:]) ] for group, index in zip(groups, indices[1:]): - group.add(tick_marks[index].copy()) - groups[-1][-1].add(tick_marks[-1]) + group[1].add(tick_marks[index].copy()) + groups[-1][1].add(tick_marks[-1]) for group in groups: group.target_points = [ @@ -1800,6 +1823,9 @@ class CompareThisToSphereChoice(TeacherStudentsScene): ) self.dither(3) +class SimpleRotatingSphereWithPoint(ExternallyAnimatedScene): + pass + class ChoicesForSpherePoint(GeneralizeBorsukUlam): def construct(self): self.add_sphere_set() @@ -1828,6 +1854,7 @@ class ChoicesForSpherePoint(GeneralizeBorsukUlam): sphere_set.shift(UP) self.add(sphere_set) + self.sphere_set = sphere_set def initialize_words(self): choice_one_words = TextMobject( @@ -2047,35 +2074,400 @@ class NecklaceDivisionSphereAssociation(ChoicesInNecklaceCutting): self.play(*self.swapping_anims) self.dither() +class SimpleRotatingSphereWithAntipodes(ExternallyAnimatedScene): + pass + class TotalLengthOfEachJewelEquals(NecklaceDivisionSphereAssociation): CONFIG = { - "random_seed" : 2, - "hard_coded_fair_division_indices" : [], + "camera_class" : ShadingCamera, + "random_seed" : 1, + "thief_box_offset" : 1.2, } def construct(self): random.seed(self.random_seed) self.add_necklace() self.add_boxes_and_labels() self.find_fair_division() + self.demonstrate_fair_division() + self.perform_antipodal_swap() def find_fair_division(self): - pass - - - - - - - - - - - - - - - - + segments, tick_marks = self.necklace + segments.sort_submobjects() + segment_colors = [ + segment.get_color() + for segment in segments + ] + indices = self.get_fair_division_indices(segment_colors) + groups = self.get_groups( + [0] + list(np.array(indices)+1) + [len(segments)] + ) + self.add(*groups) + binary_choice = [0, 1, 0] + + v_lines = VGroup(*[DashedLine(UP, DOWN) for x in range(2)]) + v_lines.move_to(self.necklace) + self.play(ShowCreation(v_lines)) + self.play(*[ + ApplyMethod(line.move_to, segments[index].get_right()) + for line, index in zip(v_lines, indices) + ]) + self.dither() + self.play(*[ + ApplyMethod(group.move_to, group.target_points[choice]) + for group, choice in zip(groups, binary_choice) + ]) + self.dither() + + self.groups = groups + self.v_lines = v_lines + + def get_fair_division_indices(self, colors): + colors = np.array(list(colors)) + color_types = map(Color, set([c.get_hex_l() for c in colors])) + type_to_count = dict([ + (color, sum(colors == color)) + for color in color_types + ]) + for i1, i2 in it.combinations(range(1, len(colors)-1), 2): + bools = [ + sum(colors[i1:i2] == color) == type_to_count[color]/2 + for color in color_types + ] + if np.all(bools): + return i1, i2 + raise Exception("No fair division found") + + def demonstrate_fair_division(self): + segments, tick_marks = self.necklace + color_types = map(Color, set([ + segment.get_color().get_hex_l() + for segment in segments + ])) + top_segments = VGroup(*it.chain( + self.groups[0][0], + self.groups[2][0], + )) + bottom_segments = self.groups[1][0] + for color in color_types: + monochrome_groups = [ + VGroup(*filter( + lambda segment: segment.get_color() == color, + segment_group + )) + for segment_group in top_segments, bottom_segments + ] + labels = VGroup() + for i, group in enumerate(monochrome_groups): + group.save_state() + group.generate_target() + group.target.arrange_submobjects(buff = SMALL_BUFF) + brace = Brace(group.target, UP) + label = VGroup( + TextMobject("Thief %d"%(i+1)), + Jewel(color = group[0].get_color()) + ) + label.arrange_submobjects() + label.next_to(brace, UP) + full_group = VGroup(group.target, brace, label) + vect = LEFT if i == 0 else RIGHT + full_group.next_to(ORIGIN, vect, buff = MED_LARGE_BUFF) + full_group.to_edge(UP) + labels.add(brace, label) + equals = TexMobject("=") + equals.next_to(monochrome_groups[0].target, RIGHT) + + for group, label in zip(monochrome_groups, labels): + self.play( + MoveToTarget(group), + FadeIn(labels) + ) + self.dither() + self.play(FadeIn(equals)) + self.dither() + self.play(*it.chain( + [group.restore for group in monochrome_groups], + map(FadeOut, list(labels)+[equals]), + )) + self.dither() + + def perform_antipodal_swap(self): + binary_choices_list = [(1, 0, 1), (0, 1, 0)] + for binary_choices in binary_choices_list: + self.play(*[ + ApplyMethod( + group.move_to, + group.target_points[choice] + ) + for group, choice in zip(self.groups, binary_choices) + ]) + self.dither() + +class ExclaimBorsukUlam(TeacherStudentsScene): + def construct(self): + self.student_says( + "Borsuk-Ulam!", + target_mode = "hooray" + ) + self.play(*[ + ApplyMethod(pi.change_mode, "hooray") + for pi in self.get_everyone() + ]) + self.dither(3) + +class ShowFunctionDiagram(TotalLengthOfEachJewelEquals): + CONFIG = { + "necklace_center" : ORIGIN, + "camera_class" : ShadingCamera, + "thief_box_offset" : 0.3, + } + def construct(self): + self.add_necklace() + self.add_number_pair() + self.add_sphere_arrow() + self.add_xy_plane() + + def add_necklace(self): + random.seed(self.random_seed) + ChoicesInNecklaceCutting.add_necklace(self) + self.necklace.scale_to_fit_width(SPACE_WIDTH-1) + self.necklace.to_edge(UP, buff = LARGE_BUFF) + self.necklace.to_edge(LEFT, buff = SMALL_BUFF) + self.add(self.necklace) + + self.find_fair_division() + + def add_number_pair(self): + colors = [BLUE, GREEN] + pair, alt_pair = [ + TextMobject( + "(Thief %d"%d, "X", ", Thief %d "%d, "X", ")" + ) + for d in 1, 2 + ] + for tup in pair, alt_pair: + jewels = [Jewel(color = color) for color in colors] + for i, jewel in zip([1, 3], jewels): + jewel.replace(tup[i]) + tup.submobjects[i] = jewel + + tup.scale_to_fit_width(SPACE_WIDTH-2) + tup.next_to(self.necklace, buff = 2*LARGE_BUFF) + + # arrow = Arrow(self.necklace, pair, color = WHITE) + arrow = TexMobject("\\rightarrow") + arrow.scale(1.5) + arrow.move_to( + Line(self.necklace.get_right(), pair.get_left()) + ) + arrow.highlight(YELLOW) + + self.play(Write(arrow)) + self.play(Write(pair)) + self.dither() + pair.save_state() + self.play(Transform(pair, alt_pair, path_arc = np.pi)) + self.dither(2) + self.play(ApplyMethod(pair.restore, path_arc = np.pi)) + self.dither() + for choices in [(1, 0, 0), (0, 1, 0)]: + self.play(*[ + ApplyMethod(group.move_to, group.target_points[i]) + for group, i in zip(self.groups, choices) + ]) + self.dither() + + self.num_pair = pair + + def add_sphere_arrow(self): + arrow = TexMobject("\\updownarrow") + arrow.scale(1.5) + arrow.highlight(YELLOW) + arrow.next_to(self.necklace, DOWN, buff = LARGE_BUFF) + + self.play(Write(arrow)) + self.dither() + + def add_xy_plane(self): + arrow = TexMobject("\\updownarrow") + arrow.scale(1.5) + arrow.highlight(YELLOW) + arrow.next_to(self.num_pair, DOWN, buff = 1.2*LARGE_BUFF) + + xy_plane = NumberPlane() + xy_plane.scale_to_fit_width(SPACE_WIDTH-1) + xy_plane.next_to(arrow, DOWN, buff = LARGE_BUFF) + + curved_arrow = Arc( + start_angle = 3*np.pi/4, + angle = -np.pi/2, + radius = 3, + color = YELLOW, + ) + curved_arrow.add_tip() + curved_arrow.shift(2*DOWN) + + self.play(Write(arrow)) + self.play(ShowCreation(xy_plane)) + self.dither() + self.play(ShowCreation(curved_arrow)) + self.dither() + +class WhatAboutGeneralCase(TeacherStudentsScene): + def construct(self): + self.student_says(""" + What about when + there's more than 2 jewels? + """) + self.change_student_modes("confused", None, "sassy") + self.dither() + self.play(self.get_teacher().change_mode, "thinking") + self.dither() + self.teacher_says( + """Use Borsuk-Ulam for + higher-dimensional spheres """, + target_mode = "hooray" + ) + self.change_student_modes(*["confused"]*3) + self.dither(2) + +class Simple3DSpace(ExternallyAnimatedScene): + pass + +class FourDBorsukUlam(GeneralizeBorsukUlam): + CONFIG = { + "n_dims" : 4, + } + def get_sphere_set(self): + sphere_set = GeneralizeBorsukUlam.get_sphere_set(self) + brace = Brace(sphere_set) + text = brace.get_text("Hypersphere in 4D") + sphere_set.add(brace, text) + return sphere_set + +class CircleToSphereToQMarks(Scene): + def construct(self): + pis = VGroup() + modes = ["happy", "pondering", "pleading"] + shapes = [ + Circle(color = BLUE, radius = 0.5), + VectorizedPoint(), + TexMobject("???") + ] + for d, mode, shape in zip(it.count(2), modes, shapes): + randy = Randolph(mode = mode) + randy.scale(0.7) + bubble = randy.get_bubble(direction = LEFT) + bubble.resize_to_content() + bubble.pin_to(randy) + bubble.position_mobject_inside(shape) + title = TextMobject("%dD"%d) + randy.add(bubble, shape) + title.next_to(randy, UP) + randy.add(title) + pis.add(randy) + + + progression = VGroup( + pis[0], + Arrow(LEFT, RIGHT), + pis[1], + Arrow(LEFT, RIGHT), + pis[2], + ) + progression.arrange_submobjects() + for mob in progression: + self.play(FadeIn(mob)) + self.dither() + +class BorsukPatreonThanks(PatreonThanks): + CONFIG = { + "specific_patrons" : [ + "Ali Yahya", + "Meshal Alshammari", + "CrypticSwarm ", + "Ankit Agarwal", + "Yu Jun", + "Shelby Doolittle", + "Dave Nicponski", + "Damion Kistler", + "Juan Benet", + "Othman Alikhan", + "Markus Persson", + "Dan Buchoff", + "Derek Dai", + "Joseph John Cox", + "Luc Ritchie", + "Guido Gambardella", + "Jerry Ling", + "Mark Govea", + "Vecht", + "Jonathan Eppele", + "Shimin Kuang", + "Rish Kundalia", + "Achille Brighton", + "Kirk Werklund", + "Ripta Pasay", + "Felipe Diniz", + ] + } + +class MortyLookingAtRectangle(Scene): + def construct(self): + morty = Mortimer() + morty.to_corner(DOWN+RIGHT) + url = TextMobject("www.thegreatcoursesplus.com/3blue1brown") + url.scale(0.75) + url.to_corner(UP+LEFT) + rect = Rectangle(height = 9, width = 16) + rect.scale_to_fit_height(5) + rect.next_to(url, DOWN) + rect.shift_onto_screen() + url.save_state() + url.next_to(morty.get_corner(UP+LEFT), UP) + url.shift_onto_screen() + + self.add(morty) + self.play( + morty.change_mode, "raise_right_hand", + morty.look_at, url, + ) + self.play(Write(url)) + self.play(Blink(morty)) + self.dither() + self.play( + url.restore, + morty.change_mode, "happy" + ) + self.play(ShowCreation(rect)) + self.dither() + self.play(Blink(morty)) + for mode in ["pondering", "hooray", "happy", "pondering", "happy"]: + self.play(morty.change_mode, mode) + self.dither(2) + self.play(Blink(morty)) + self.dither(2) + +class RotatingThreeDSphereProjection(Scene): + CONFIG = { + "camera_class" : ShadingCamera, + } + def construct(self): + sphere = VGroup(*[ + Circle(radius = np.sin(t)).shift(np.cos(t)*OUT) + for t in np.linspace(0, np.pi, 20) + ]) + sphere.set_stroke(BLUE, width = 2) + # sphere.set_fill(BLUE, opacity = 0.1) + + self.play(Rotating( + sphere, axis = RIGHT+OUT, + run_time = 10 + )) + self.repeat_frames(4) + +class FourDSphereProjectTo4D(ExternallyAnimatedScene): + pass diff --git a/eoc/chapter1.py b/eoc/chapter1.py index 4829116f..e4ee4a64 100644 --- a/eoc/chapter1.py +++ b/eoc/chapter1.py @@ -2596,6 +2596,10 @@ class PatreonThanks(Scene): special_thanks.highlight(YELLOW) special_thanks.to_edge(UP) + patreon_logo = PatreonLogo() + # patreon_logo.scale_to_fit_width(morty.get_width()) + patreon_logo.next_to(morty, UP, buff = MED_LARGE_BUFF) + left_patrons = VGroup(*map(TextMobject, self.specific_patrons[:n_patrons/2] )) @@ -2623,7 +2627,10 @@ class PatreonThanks(Scene): # rate_func = None # ) - self.play(morty.change_mode, "gracious") + self.play( + morty.change_mode, "gracious", + DrawBorderThenFill(patreon_logo) + ) self.play(Write(special_thanks, run_time = 1)) self.play( Write(left_patrons), diff --git a/mobject/mobject.py b/mobject/mobject.py index 6d3d4d03..7c3fa438 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -560,6 +560,15 @@ class Mobject(object): self.center() return self + def sort_submobjects(self, point_to_num_func = lambda p : p[0]): + self.submobjects.sort( + lambda *mobs : cmp(*[ + point_to_num_func(mob.get_center()) + for mob in mobs + ]) + ) + return self + ## Alignment def align_data(self, mobject): self.align_submobjects(mobject) diff --git a/mobject/svg_mobject.py b/mobject/svg_mobject.py index 01d1295c..834bd378 100644 --- a/mobject/svg_mobject.py +++ b/mobject/svg_mobject.py @@ -189,7 +189,7 @@ class VMobjectFromSVGPathstring(VMobject): #list. This variable may get modified in the conditionals below. points = self.growing_path.points new_points = self.string_to_points(coord_string) - if isLower: + if isLower and len(points) > 0: new_points += points[-1] if command == "M": #moveto if len(points) > 0: @@ -201,9 +201,12 @@ class VMobjectFromSVGPathstring(VMobject): if command == "H": new_points[0,1] = points[-1,1] elif command == "V": + if isLower: + new_points[0,0] -= points[-1,0] + new_points[0,0] += points[-1,1] new_points[0,1] = new_points[0,0] new_points[0,0] = points[-1,0] - new_points = new_points[[0, 0, 0]] + new_points = new_points.repeat(3, axis = 0) elif command == "C": #curveto pass #Yay! No action required elif command in ["S", "T"]: #smooth curveto @@ -219,6 +222,13 @@ class VMobjectFromSVGPathstring(VMobject): #Both handles and new anchor are the start new_points = points[[0, 0, 0]] # self.mark_paths_closed = True + + #Handle situations where there's multiple relative control points + if isLower and len(points) > 3: + for i in range(3, len(new_points), 3): + new_points[i:i+3] -= points[-1] + new_points[i:i+3] += new_points[i-1] + self.growing_path.add_control_points(new_points) def string_to_points(self, coord_string): diff --git a/topics/objects.py b/topics/objects.py index b55bfbfd..8252749d 100644 --- a/topics/objects.py +++ b/topics/objects.py @@ -11,6 +11,26 @@ from animation.simple_animations import Rotating from topics.geometry import Circle, Line +class PatreonLogo(SVGMobject): + CONFIG = { + "file_name" : "patreon_logo", + "fill_color" : "#ff5900", + "fill_opacity" : 1, + "stroke_width" : 0, + "height" : 2, + "propogate_style_to_family" : True + } + def __init__(self, **kwargs): + SVGMobject.__init__(self, **kwargs) + outer, inner = self.split() + # outer.add_subpath(inner.points) + # self.remove(inner) + inner.set_fill(BLACK, opacity = 1) + inner.set_stroke(self.fill_color, width = 0.5) + self.scale_to_fit_height(self.height) + self.center() + + class VideoIcon(SVGMobject): CONFIG = { "file_name" : "video_icon",