From 17505d4d9e8fbde732324e4daf01b9b9317f2824 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 22 Jul 2016 18:45:34 -0700 Subject: [PATCH] Up to formal definition of linearity in EoLA chapter 3 --- animation/transform.py | 8 +- eola/chapter3.py | 174 ++++++++++++++++++++++++++++++---- eola/two_d_space.py | 68 ++++++++----- helpers.py | 3 +- mobject/mobject.py | 2 +- mobject/vectorized_mobject.py | 32 ++++--- topics/characters.py | 24 ++--- topics/number_line.py | 10 +- 8 files changed, 244 insertions(+), 77 deletions(-) diff --git a/animation/transform.py b/animation/transform.py index 1db70193..9896f027 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -98,12 +98,8 @@ class ApplyMethod(Transform): "the method you want to animate" ) assert(isinstance(method.im_self, Mobject)) - Transform.__init__( - self, - method.im_self, - copy.deepcopy(method)(*args), - **kwargs - ) + target = copy.deepcopy(method)(*args) + Transform.__init__(self, method.im_self, target, **kwargs) class FadeOut(Transform): CONFIG = { diff --git a/eola/chapter3.py b/eola/chapter3.py index 50b4702e..e5700e18 100644 --- a/eola/chapter3.py +++ b/eola/chapter3.py @@ -20,6 +20,10 @@ from mobject.vectorized_mobject import * from eola.matrix import * from eola.two_d_space import * +def curvy_squish(point): + x, y, z = point + return (x+np.cos(y))*RIGHT + (y+np.sin(x))*UP + class OpeningQuote(Scene): def construct(self): words = TextMobject([ @@ -49,7 +53,6 @@ class OpeningQuote(Scene): self.play(Write(comment)) self.dither() - class Introduction(TeacherStudentsScene): def construct(self): title = TextMobject("Matrices as linear transformations") @@ -71,7 +74,6 @@ class Introduction(TeacherStudentsScene): return (SPACE_WIDTH+SPACE_HEIGHT)*p/np.linalg.norm(p) self.play(ApplyPointwiseFunction(spread_out, everything)) - class ShowGridCreation(Scene): def construct(self): plane = NumberPlane() @@ -114,7 +116,6 @@ class IntroduceLinearTransformations(LinearTransformationScene): ) self.dither() - class SimpleLinearTransformationScene(LinearTransformationScene): CONFIG = { "show_basis_vectors" : False, @@ -143,8 +144,7 @@ class SimpleNonlinearTransformationScene(LinearTransformationScene): self.dither() def func(self, point): - x, y, z = point - return (x+np.cos(y))*RIGHT + (y+np.sin(x))*UP + return curvy_squish(point) class MovingOrigin(SimpleNonlinearTransformationScene): CONFIG = { @@ -185,7 +185,6 @@ class SneakyNonlinearTransformationExplained(SneakyNonlinearTransformation): self.play(ShowCreation(diag)) self.add_transformable_mobject(diag) - class AnotherLinearTransformation(SimpleLinearTransformationScene): CONFIG = { "transposed_matrix" : [ @@ -209,7 +208,6 @@ class AnotherLinearTransformation(SimpleLinearTransformationScene): self.play(Write(text)) self.dither() - class Rotation(SimpleLinearTransformationScene): CONFIG = { "angle" : np.pi/3, @@ -221,8 +219,6 @@ class Rotation(SimpleLinearTransformationScene): ] SimpleLinearTransformationScene.construct(self) - - class YetAnotherLinearTransformation(SimpleLinearTransformationScene): CONFIG = { "transposed_matrix" : [ @@ -231,7 +227,6 @@ class YetAnotherLinearTransformation(SimpleLinearTransformationScene): ] } - class MoveAroundAllVectors(LinearTransformationScene): CONFIG = { "show_basis_vectors" : False, @@ -269,17 +264,15 @@ class MoveAroundAllVectors(LinearTransformationScene): self.add(vector.copy().highlight(DARK_GREY)) else: for vector in vectors.split(): - self.add_vector(vector) + self.add_vector(vector, animate = False) self.apply_transposed_matrix([[3, 0], [1, 2]]) self.dither() - class MoveAroundJustOneVector(MoveAroundAllVectors): CONFIG = { "focus_on_one_vector" : True, } - class RotateIHat(LinearTransformationScene): CONFIG = { "show_basis_vectors" : False @@ -297,8 +290,6 @@ class RotateIHat(LinearTransformationScene): self.play(Write(j_label, run_time = 1)) self.dither() - - class TransformationsAreFunctions(Scene): def construct(self): title = TextMobject([ @@ -345,7 +336,6 @@ class TransformationsAreFunctions(Scene): self.play(Write(v), ShowCreation(a), run_time = 1) self.dither() - class UsedToThinkinfOfFunctionsAsGraphs(VectorScene): def construct(self): self.show_graph() @@ -409,6 +399,158 @@ class UsedToThinkinfOfFunctionsAsGraphs(VectorScene): ) self.dither() +class TryingToVisualizeFourDimensions(Scene): + def construct(self): + randy = Randolph().to_corner() + bubble = randy.get_bubble() + formula = TexMobject(""" + L\\left(\\left[ + \\begin{array}{c} + x \\\\ + y + \\end{array} + \\right]\\right) = + \\left[ + \\begin{array}{c} + 2x + y \\\\ + x + 2y + \\end{array} + \\right] + """) + formula.next_to(randy, RIGHT) + formula.split()[3].highlight(X_COLOR) + formula.split()[4].highlight(Y_COLOR) + VMobject(*formula.split()[9:9+4]).highlight(MAROON_C) + VMobject(*formula.split()[13:13+4]).highlight(BLUE) + thought = TextMobject(""" + Do I imagine plotting + $(x, y, 2x+y, x+2y)$??? + """) + thought.split()[-17].highlight(X_COLOR) + thought.split()[-15].highlight(Y_COLOR) + VMobject(*thought.split()[-13:-13+4]).highlight(MAROON_C) + VMobject(*thought.split()[-8:-8+4]).highlight(BLUE) + + bubble.position_mobject_inside(thought) + thought.shift(0.2*UP) + + self.add(randy) + + self.play( + ApplyMethod(randy.look, DOWN+RIGHT), + Write(formula) + ) + self.play( + ApplyMethod(randy.change_mode, "pondering"), + ShowCreation(bubble), + Write(thought) + ) + self.play(Blink(randy)) + self.dither() + self.remove(thought) + bubble.make_green_screen() + self.dither() + self.play(Blink(randy)) + self.play(ApplyMethod(randy.change_mode, "confused")) + self.dither() + self.play(Blink(randy)) + self.dither() + +class ForgetAboutGraphs(Scene): + def construct(self): + self.play(Write("You must unlearn graphs")) + self.dither() + +class ThinkAboutFunctionAsMovingVector(LinearTransformationScene): + CONFIG = { + "show_basis_vectors" : False, + "leave_ghost_vectors" : True, + } + def construct(self): + self.setup() + vector = self.add_vector([2, 1]) + label = self.add_transformable_label(vector, "v") + self.dither() + self.apply_transposed_matrix([[1, 1], [-3, 1]]) + self.dither() + +class PrepareForFormalDefinition(TeacherStudentsScene): + def construct(self): + self.setup() + self.teacher_says("Get ready for a formal definition!") + self.dither(3) + bubble = self.student_thinks("") + bubble.make_green_screen() + self.dither(3) + +class AdditivityProperty(LinearTransformationScene): + CONFIG = { + "show_basis_vectors" : False, + "give_title" : True, + "transposed_matrix" : [[2, 0], [1, 1]], + "nonlinear_transformation" : None, + "vector_v" : [2, 1], + "vector_w" : [1, -2], + } + def construct(self): + self.setup() + added_anims = [] + if self.give_title: + title = TextMobject(""" + First fundamental property of + linear transformations + """) + title.to_edge(UP) + title.add_background_rectangle() + self.play(Write(title)) + added_anims.append(Animation(title)) + self.dither() + # self.play(ApplyMethod(self.plane.fade)) + + v, w = self.draw_all_vectors() + self.apply_transformation() + self.show_final_sum(v, w) + + def draw_all_vectors(self): + v = self.add_vector(self.vector_v, color = MAROON_C) + v_label = self.add_transformable_label(v, "v") + w = self.add_vector(self.vector_w, color = GREEN) + w_label = self.add_transformable_label(w, "w") + new_w = w.copy().fade(0.4) + self.play(ApplyMethod(new_w.shift, v.get_end())) + sum_vect = self.add_vector(new_w.get_end(), color = PINK) + sum_label = self.add_transformable_label( + sum_vect, + "%s + %s"%(v_label.expression, w_label.expression), + rotate = True + ) + self.play(FadeOut(new_w)) + return v, w + + def apply_transformation(selfe): + if self.nonlinear_transformation: + self.apply_nonlinear_transformation(self.nonlinear_transformation) + else: + self.apply_transposed_matrix( + self.transposed_matrix, + added_anims = added_anims + ) + self.dither() + + def show_final_sum(self, v, w): + new_w = w.copy() + self.play(ApplyMethod(new_w.shift, v.get_end())) + self.dither() + + +class NonlinearLacksAdditivity(AdditivityProperty): + CONFIG = { + "give_title" : False, + "nonlinear_transformation" : curvy_squish, + "vector_w" : [2, -3], + } + + diff --git a/eola/two_d_space.py b/eola/two_d_space.py index f30eb2b4..bc01756c 100644 --- a/eola/two_d_space.py +++ b/eola/two_d_space.py @@ -84,28 +84,22 @@ class VectorScene(Scene): direction = "left", rotate = False, color = WHITE, - buff_factor = 2, label_scale_val = VECTOR_LABEL_SCALE_VAL): if len(label) == 1: label = "\\vec{\\textbf{%s}}"%label label = TexMobject(label) - label.highlight(color) + label.highlight(color) label.scale(label_scale_val) - if rotate: - label.rotate(vector.get_angle()) + label.add_background_rectangle() - vector_vect = vector.get_end() - vector.get_start() - if direction is "left": - rot_angle = -np.pi/2 - else: - rot_angle = np.pi/2 - boundary_dir = -np.round(rotate_vector(vector_vect, rot_angle)) - boundary_point = label.get_critical_point(boundary_dir) - label.shift(buff_factor*boundary_point) - label.shift(vector_vect/2.) + angle = vector.get_angle() + label.shift(label.get_height()*UP) + label.rotate(angle) + label.shift((vector.get_end() - vector.get_start())/2) + if not rotate: + label.rotate_in_place(-angle) return label - def label_vector(self, vector, label, animate = True, **kwargs): label = self.get_vector_label(vector, label, **kwargs) if animate: @@ -252,11 +246,13 @@ class LinearTransformationScene(VectorScene): "show_basis_vectors" : True, "i_hat_color" : X_COLOR, "j_hat_color" : Y_COLOR, + "leave_ghost_vectors" : False, } def setup(self): self.background_mobjects = [] - self.transformable_mobject = [] + self.transformable_mobjects = [] self.moving_vectors = [] + self.transformable_labels = [] self.background_plane = NumberPlane( **self.background_plane_kwargs @@ -281,8 +277,8 @@ class LinearTransformationScene(VectorScene): def add_transformable_mobject(self, *mobjects): for mobject in mobjects: - if mobject not in self.transformable_mobject: - self.transformable_mobject.append(mobject) + if mobject not in self.transformable_mobjects: + self.transformable_mobjects.append(mobject) self.add(mobject) def add_vector(self, vector, color = YELLOW, **kwargs): @@ -292,6 +288,14 @@ class LinearTransformationScene(VectorScene): self.moving_vectors.append(vector) return vector + def add_transformable_label(self, vector, label, **kwargs): + label_mob = self.label_vector(vector, label, **kwargs) + label_mob.target_text = "L(%s)"%label_mob.expression + label_mob.vector = vector + label_mob.kwargs = kwargs + self.transformable_labels.append(label_mob) + return label_mob + def get_matrix_transformation(self, transposed_matrix): transposed_matrix = np.array(transposed_matrix) if transposed_matrix.shape == (2, 2): @@ -302,14 +306,24 @@ class LinearTransformationScene(VectorScene): raise "Matrix has bad dimensions" return lambda point: np.dot(point, transposed_matrix) + def get_piece_movement(self, pieces): + start = VMobject(*pieces) + target = VMobject(*[mob.target for mob in pieces]) + if self.leave_ghost_vectors: + self.add(start.copy().fade(0.7)) + return Transform(start, target, submobject_mode = "all_at_once") def get_vector_movement(self, func): - start = VMobject(*self.moving_vectors) - target = VMobject(*[ - Vector(func(v.get_end()), color = v.get_color()) - for v in self.moving_vectors - ]) - return Transform(start, target) + for v in self.moving_vectors: + v.target = Vector(func(v.get_end()), color = v.get_color()) + return self.get_piece_movement(self.moving_vectors) + + def get_transformable_label_movement(self): + for l in self.transformable_labels: + l.target = self.get_vector_label( + l.vector.target, l.target_text, **l.kwargs + ) + return self.get_piece_movement(self.transformable_labels) def apply_transposed_matrix(self, transposed_matrix, **kwargs): func = self.get_matrix_transformation(transposed_matrix) @@ -322,18 +336,20 @@ class LinearTransformationScene(VectorScene): self.apply_function(func, **kwargs) def apply_nonlinear_transformation(self, function, **kwargs): - self.plane.prepare_for_nonlinear_transform(100) + self.plane.prepare_for_nonlinear_transform() self.apply_function(function, **kwargs) - def apply_function(self, function, **kwargs): + def apply_function(self, function, added_anims = [], **kwargs): if "run_time" not in kwargs: kwargs["run_time"] = 3 self.play( ApplyPointwiseFunction( function, - VMobject(*self.transformable_mobject), + VMobject(*self.transformable_mobjects), ), self.get_vector_movement(function), + self.get_transformable_label_movement(), + *added_anims, **kwargs ) diff --git a/helpers.py b/helpers.py index c06c0749..f7390558 100644 --- a/helpers.py +++ b/helpers.py @@ -12,6 +12,7 @@ from scipy import linalg from constants import * CLOSED_THRESHOLD = 0.01 +STRAIGHT_PATH_THRESHOLD = 0.01 def get_smooth_handle_points(points): num_handles = len(points) - 1 @@ -262,7 +263,7 @@ def path_along_arc(arc_angle): If vect is vector from start to end, [vect[:,1], -vect[:,0]] is perpendicualr to vect in the left direction. """ - if arc_angle == 0: + if abs(arc_angle) < STRAIGHT_PATH_THRESHOLD: return straight_path def path(start_points, end_points, alpha): vects = end_points - start_points diff --git a/mobject/mobject.py b/mobject/mobject.py index 11fe2fe0..e050d09d 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -314,7 +314,7 @@ class Mobject(object): start = color_to_rgb(mob.get_color()) end = color_to_rgb(color) new_rgb = interpolate(start, end, alpha) - mob.highlight(Color(rgb = new_rgb)) + mob.highlight(Color(rgb = new_rgb), family = False) return self def fade(self, darkness = 0.5): diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 8a1dc9bb..46ceade5 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -45,22 +45,22 @@ class VMobject(Mobject): for mob in mobs: if stroke_color is not None: mob.stroke_rgb = color_to_rgb(stroke_color) - if stroke_width is not None: - mob.stroke_width = stroke_width if fill_color is not None: mob.fill_rgb = color_to_rgb(fill_color) + if stroke_width is not None: + mob.stroke_width = stroke_width if fill_opacity is not None: mob.fill_opacity = fill_opacity - probably_meant_to_change_opacity = reduce(op.and_, [ - fill_color is not None, - fill_opacity is None, - mob.fill_opacity == 0 - ]) - if probably_meant_to_change_opacity: - mob.fill_opacity = 1 return self def set_fill(self, color = None, opacity = None, family = True): + probably_meant_to_change_opacity = reduce(op.and_, [ + color is not None, + opacity is None, + self.fill_opacity == 0 + ]) + if probably_meant_to_change_opacity: + opacity = 1 return self.set_style_data( fill_color = color, fill_opacity = opacity, @@ -75,8 +75,11 @@ class VMobject(Mobject): ) def highlight(self, color, family = True): - self.set_fill(color = color, family = family) - self.set_stroke(color = color, family = family) + self.set_style_data( + stroke_color = color, + fill_color = color, + family = family + ) return self # def fade(self, darkness = 0.5): @@ -203,6 +206,13 @@ class VMobject(Mobject): self.submobjects ) + def apply_function(self, function, maintain_smoothness = True): + Mobject.apply_function(self, function) + if maintain_smoothness: + self.make_smooth() + return self + + ## Information about line def component_curves(self): diff --git a/topics/characters.py b/topics/characters.py index 967ed623..b88f8d14 100644 --- a/topics/characters.py +++ b/topics/characters.py @@ -226,12 +226,9 @@ class Bubble(SVGMobject): return mobject def add_content(self, mobject): - if self.content in self.submobjects: - self.submobjects.remove(self.content) self.position_mobject_inside(mobject) self.content = mobject - self.add(self.content) - return self + return self.content def write(self, text): self.add_content(TextMobject(text)) @@ -296,11 +293,12 @@ class TeacherStudentsScene(Scene): bubble.add_content(content) if pi_creature.bubble: content_intro_anims = [ - Transform(pi_creature.bubble, bubble) + Transform(pi_creature.bubble, bubble), + Transform(pi_creature.bubble.content, bubble.content) ] else: content_intro_anims = [ - FadeIn(bubble.split()[0]), + FadeIn(bubble), Write(content), ] pi_creature.bubble = bubble @@ -324,7 +322,10 @@ class TeacherStudentsScene(Scene): for p in self.get_everyone(): if p.bubble and p is not pi_creature: - added_anims.append(FadeOut(p.bubble)) + added_anims += [ + FadeOut(p.bubble), + FadeOut(p.bubble.content) + ] p.bubble = None added_anims.append(ApplyMethod(p.change_mode, "plain")) @@ -335,24 +336,25 @@ class TeacherStudentsScene(Scene): ), ] self.play(*anims) + return pi_creature.bubble def teacher_says(self, content, **kwargs): - self.introduce_bubble( + return self.introduce_bubble( content, "speech", self.get_teacher(), **kwargs ) def student_says(self, content, student_index = 1, **kwargs): student = self.get_students()[student_index] - self.introduce_bubble(content, "speech", student, **kwargs) + return self.introduce_bubble(content, "speech", student, **kwargs) def teacher_thinks(self, content, **kwargs): - self.introduce_bubble( + return self.introduce_bubble( content, "thought", self.get_teacher(), **kwargs ) def student_thinks(self, content, student_index = 1, **kwargs): student = self.get_students()[student_index] - self.introduce_bubble(content, "thought", student, **kwargs) + return self.introduce_bubble(content, "thought", student, **kwargs) def random_blink(self, num_times = 1): for x in range(num_times): diff --git a/topics/number_line.py b/topics/number_line.py index 5b0fcd3d..bcd9a20e 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -240,11 +240,11 @@ class NumberPlane(VMobject): arrow = Arrow(ORIGIN, coords, **kwargs) return arrow - def prepare_for_nonlinear_transform(self, num_inserted_anchor_points = 40): - for mob in self.submobject_family(): - if mob.get_num_points() > 0: - mob.insert_n_anchor_points(num_inserted_anchor_points) - mob.change_anchor_mode("smooth") + def prepare_for_nonlinear_transform(self, num_inserted_anchor_points = 50): + for mob in self.family_members_with_points(): + mob.insert_n_anchor_points(num_inserted_anchor_points) + mob.make_smooth() + return self