diff --git a/eola/chapter0.py b/eola/chapter0.py index a3afc9d4..b16c7cae 100644 --- a/eola/chapter0.py +++ b/eola/chapter0.py @@ -38,13 +38,47 @@ class OpeningQuote(Scene): words.to_edge(UP) for mob in words.submobjects[48:49+13]: mob.highlight(GREEN) - author = TextMobject("-Hermann Weyl") + author = TextMobject("-Jean Dieudonn\\'e") author.highlight(YELLOW) author.next_to(words, DOWN) self.play(FadeIn(words)) self.dither(3) - self.play(Write(author)) + self.play(Write(author, run_time = 5)) + self.dither() + +class VideoIcon(SVGMobject): + def __init__(self, **kwargs): + SVGMobject.__init__(self, "video_icon", **kwargs) + self.center() + self.scale_to_fit_width(2*SPACE_WIDTH/12.) + self.set_stroke(color = WHITE, width = 0) + self.set_fill(color = WHITE, opacity = 1) + + + +class UpcomingSeriesOfVidoes(Scene): + def construct(self): + icons = [VideoIcon() for x in range(10)] + colors = Color(BLUE_A).range_to(BLUE_D, len(icons)) + for icon, color in zip(icons, colors): + icon.set_fill(color, opacity = 1) + icons = VMobject(*icons) + icons.arrange_submobjects(RIGHT) + icons.to_edge(LEFT) + icons.shift(UP) + icons = icons.split() + + def rate_func_creator(offset): + return lambda a : min(max(2*(a-offset), 0), 1) + self.play(*[ + FadeIn( + icon, + run_time = 5, + rate_func = rate_func_creator(offset) + ) + for icon, offset in zip(icons, np.linspace(0, 0.5, len(icons))) + ]) self.dither() @@ -612,12 +646,300 @@ class PhysicsExample(Scene): class LinearAlgebraIntuitions(Scene): def construct(self): - pass - - - - - + title = TextMobject("Preview of core visual intuitions") + title.to_edge(UP) + h_line = Line(SPACE_WIDTH*LEFT, SPACE_WIDTH*RIGHT) + h_line.next_to(title, DOWN) + h_line.highlight(BLUE_E) + intuitions = [ + "Matrices transform space", + "Matrix multiplication corresponds to applying " + + "one transformation after another", + "The determinant gives the factor by which areas change", + ] + + self.play( + Write(title), + ShowCreation(h_line), + run_time = 2 + ) + + for count, intuition in enumerate(intuitions, 3): + intuition += " (details coming in chapter %d)"%count + mob = TextMobject(intuition) + mob.scale(0.7) + mob.next_to(h_line, DOWN) + self.play(FadeIn(mob)) + self.dither(4) + self.play(FadeOut(mob)) + self.remove(mob) + self.dither() + +class MatricesAre(Scene): + def construct(self): + matrix = matrix_to_mobject([[1, -1], [1, 2]]) + matrix.scale_to_fit_height(6) + arrow = Arrow(LEFT, RIGHT, stroke_width = 8, preserve_tip_size_when_scaling = False) + arrow.scale(2) + arrow.to_edge(RIGHT) + matrix.next_to(arrow, LEFT) + + self.play(Write(matrix, run_time = 1)) + self.play(ShowCreation(arrow, submobject_mode = "one_at_a_time")) + self.dither() + +class ExampleTransformationForIntuitionList(LinearTransformationScene): + def construct(self): + self.setup() + self.apply_matrix([[1, -1], [1, 2]]) + self.dither() + +class MatrixMultiplicationIs(Scene): + def construct(self): + matrix1 = matrix_to_mobject([[1, -1], [1, 2]]) + matrix1.highlight(BLUE) + matrix2 = matrix_to_mobject([[2, 1], [1, 2]]) + matrix2.highlight(GREEN) + for m in matrix1, matrix2: + m.scale_to_fit_height(3) + arrow = Arrow(LEFT, RIGHT, stroke_width = 6, preserve_tip_size_when_scaling = False) + arrow.scale(2) + arrow.to_edge(RIGHT) + matrix1.next_to(arrow, LEFT) + matrix2.next_to(matrix1, LEFT) + brace1 = Brace(matrix1, UP) + apply_first = TextMobject("Apply first").next_to(brace1, UP) + brace2 = Brace(matrix2, DOWN) + apply_second = TextMobject("Apply second").next_to(brace2, DOWN) + + self.play( + Write(matrix1), + ShowCreation(arrow), + GrowFromCenter(brace1), + Write(apply_first), + run_time = 1 + ) + self.dither() + self.play( + Write(matrix2), + GrowFromCenter(brace2), + Write(apply_second), + run_time = 1 + ) + self.dither() + +class ComposedTransformsForIntuitionList(LinearTransformationScene): + def construct(self): + self.setup() + self.apply_matrix([[1, -1], [1, 2]]) + self.dither() + self.apply_matrix([[2, 1], [1, 2]]) + self.dither() + +class DeterminantsAre(Scene): + def construct(self): + tex_mob = TexMobject(""" + \\text{Det}\\left(\\left[ + \\begin{array}{cc} + 1 & -1 \\\\ + 1 & 2 + \\end{array} + \\right]\\right) + """) + tex_mob.scale_to_fit_height(4) + arrow = Arrow(LEFT, RIGHT, stroke_width = 8, preserve_tip_size_when_scaling = False) + arrow.scale(2) + arrow.to_edge(RIGHT) + tex_mob.next_to(arrow, LEFT) + + self.play( + Write(tex_mob), + ShowCreation(arrow, submobject_mode = "one_at_a_time"), + run_time = 1 + ) + +class TransformationForDeterminant(LinearTransformationScene): + def construct(self): + self.setup() + square = Square(side_length = 1) + square.shift(-square.get_corner(DOWN+LEFT)) + square.set_fill(YELLOW_A, 0.5) + self.add_transformable_mobject(square) + self.apply_matrix([[1, -1], [1, 2]]) + +class ProfessorsTry(Scene): + def construct(self): + morty = Mortimer() + morty.to_corner(DOWN+RIGHT) + morty.shift(3*LEFT) + speech_bubble = morty.get_bubble("speech", height = 4, width = 8) + speech_bubble.shift(RIGHT) + words = TextMobject( + "It really is beautiful! I want you to \\\\" + \ + "see it the way I do...", + ) + speech_bubble.position_mobject_inside(words) + thought_bubble = ThoughtBubble(width = 4, height = 3.5) + thought_bubble.next_to(morty, UP) + thought_bubble.to_edge(RIGHT) + randy = Randolph() + randy.scale(0.8) + randy.to_corner() + + self.add(randy, morty) + self.play( + ApplyMethod(morty.change_mode, "speaking"), + FadeIn(speech_bubble), + FadeIn(words) + ) + self.play(Blink(randy)) + self.play(FadeIn(thought_bubble )) + self.play(Blink(morty)) + + +class ExampleMatrixMultiplication(NumericalMatrixMultiplication): + CONFIG = { + "left_matrix" : [[-3, 1], [2, 5]], + "right_matrix" : [[5, 3], [7, -3]] + } + +class TableOfContents(Scene): + def construct(self): + title = TextMobject("Essence of Linear Algebra") + title.highlight(BLUE) + title.to_corner(UP+LEFT) + h_line = Line(SPACE_WIDTH*LEFT, SPACE_WIDTH*RIGHT) + h_line.next_to(title, DOWN) + h_line.to_edge(LEFT, buff = 0) + chapters = VMobject(*map(TextMobject, [ + "Chapter 1: Vectors, what even are they?", + "Chapter 2: Linear combinations, span and bases", + "Chapter 3: Matrices as linear transformations", + "Chapter 4: Matrix multiplication as composition", + "Chapter 5: The determinant", + "Chapter 6: Inverse matrices, column space and null space", + "Chapter 7: Dot products and cross products", + "Chapter 8: Change of basis", + "Chapter 9: Eigenvectors and eigenvalues", + "Chapter 10: Abstract vector spaces", + ])) + chapters.arrange_submobjects(DOWN) + chapters.scale(0.7) + chapters.next_to(h_line, DOWN) + + self.play( + Write(title), + ShowCreation(h_line) + ) + for chapter in chapters.split(): + chapter.to_edge(LEFT, buff = 1) + self.play(FadeIn(chapter)) + self.dither(2) + + entry3 = chapters.split()[2] + added_words = TextMobject("(Personally, I'm most excited \\\\ to do this one)") + added_words.scale(0.5) + added_words.highlight(YELLOW) + added_words.next_to(h_line, DOWN) + added_words.to_edge(RIGHT) + arrow = Arrow(added_words.get_bottom(), entry3) + + self.play( + ApplyMethod(entry3.highlight, YELLOW), + ShowCreation(arrow, submobject_mode = "one_at_a_time"), + Write(added_words), + run_time = 1 + ) + self.dither() + removeable = VMobject(added_words, arrow, h_line, title) + self.play(FadeOut(removeable)) + self.remove(removeable) + + self.series_of_videos(chapters) + + def series_of_videos(self, chapters): + icon = SVGMobject("video_icon") + icon.center() + icon.scale_to_fit_width(2*SPACE_WIDTH/12.) + icon.set_stroke(color = WHITE, width = 0) + icons = [icon.copy() for chapter in chapters.split()] + colors = Color(BLUE_A).range_to(BLUE_D, len(icons)) + for icon, color in zip(icons, colors): + icon.set_fill(color, opacity = 1) + icons = VMobject(*icons) + icons.arrange_submobjects(RIGHT) + icons.to_edge(LEFT) + icons.shift(UP) + + randy = Randolph() + randy.to_corner() + bubble = randy.get_bubble() + new_icons = icons.copy().scale(0.2) + bubble.position_mobject_inside(new_icons) + + self.play(Transform( + chapters, icons, + path_arc = np.pi/2, + )) + self.clear() + self.add(icons) + self.play(FadeIn(randy)) + self.play(Blink(randy)) + self.dither() + self.play( + ShowCreation(bubble), + Transform(icons, new_icons) + ) + self.dither() + + +class ResourceForTeachers(Scene): + def construct(self): + morty = Mortimer(mode = "speaking") + morty.to_corner(DOWN + RIGHT) + bubble = morty.get_bubble("speech") + bubble.write("I'm assuming you \\\\ know linear algebra\\dots") + words = bubble.content + bubble.clear() + randys = VMobject(*[ + Randolph(color = c) + for c in BLUE_D, BLUE_C, BLUE_E + ]) + randys.arrange_submobjects(RIGHT) + randys.scale(0.8) + randys.to_corner(DOWN+LEFT) + + self.add(randys, morty) + self.play(FadeIn(bubble), Write(words), run_time = 3) + for randy in np.array(randys.split())[[2,0,1]]: + self.play(Blink(randy)) + self.dither() + +class PauseAndPonder(Scene): + def construct(self): + pause = TexMobject("=").rotate(np.pi/2) + pause.stretch(0.5, 1) + pause.scale_to_fit_height(1.5) + bubble = ThoughtBubble().scale_to_fit_height(2) + pause.shift(LEFT) + bubble.next_to(pause, RIGHT, buff = 1) + + self.play(FadeIn(pause)) + self.play(ShowCreation(bubble)) + self.dither() + + +class NextVideo(Scene): + def construct(self): + title = TextMobject("Next video: Vectors, what even are they?") + title.to_edge(UP) + rect = Rectangle(width = 16, height = 9, color = BLUE) + rect.scale_to_fit_height(6) + rect.next_to(title, DOWN) + + self.add(title) + self.play(ShowCreation(rect)) + self.dither() diff --git a/eola/utils.py b/eola/utils.py index ee344dd5..3a6bfb4b 100644 --- a/eola/utils.py +++ b/eola/utils.py @@ -1,11 +1,14 @@ import numpy as np from scene import Scene +from mobject import Mobject from mobject.vectorized_mobject import VMobject from mobject.tex_mobject import TexMobject, TextMobject -from animation.transform import ApplyPointwiseFunction, Transform +from animation.transform import ApplyPointwiseFunction, Transform, \ + ApplyMethod, FadeOut +from animation.simple_animations import ShowCreation from topics.number_line import NumberPlane -from topics.geometry import Vector +from topics.geometry import Vector, Line, Circle from helpers import * @@ -32,8 +35,13 @@ class LinearTransformationScene(Scene): "foreground_plane_kwargs" : { "x_radius" : 2*SPACE_WIDTH, "y_radius" : 2*SPACE_HEIGHT, + "secondary_line_ratio" : 0 + }, + "background_plane_kwargs" : { + "color" : GREY, + "secondary_color" : DARK_GREY, + "axes_color" : GREY, }, - "background_plane_kwargs" : {}, "show_coordinates" : False, "show_basis_vectors" : True, "i_hat_color" : GREEN_B, @@ -45,29 +53,27 @@ class LinearTransformationScene(Scene): self.moving_vectors = [] self.background_plane = NumberPlane( - color = GREY, - secondary_color = DARK_GREY, **self.background_plane_kwargs ) if self.show_coordinates: self.background_plane.add_coordinates() if self.include_background_plane: - self.add_to_background(self.background_plane) + self.add_background_mobject(self.background_plane) if self.include_foreground_plane: self.plane = NumberPlane(**self.foreground_plane_kwargs) - self.add_to_transformable(self.plane) + self.add_transformable_mobject(self.plane) if self.show_basis_vectors: self.add_vector((1, 0), self.i_hat_color) self.add_vector((0, 1), self.j_hat_color) - def add_to_background(self, *mobjects): + def add_background_mobject(self, *mobjects): for mobject in mobjects: if mobject not in self.background_mobjects: self.background_mobjects.append(mobject) self.add(mobject) - def add_to_transformable(self, *mobjects): + def add_transformable_mobject(self, *mobjects): for mobject in mobjects: if mobject not in self.transformable_mobject: self.transformable_mobject.append(mobject) @@ -109,6 +115,190 @@ class LinearTransformationScene(Scene): ) ) +class Matrix(VMobject): + CONFIG = { + "v_buff" : 0.5, + "h_buff" : 1, + } + def __init__(self, matrix, **kwargs): + """ + Matrix can either either include numbres, tex_strings, + or mobjects + """ + VMobject.__init__(self, **kwargs) + matrix = np.array(matrix) + assert(matrix.ndim == 2) + if not isinstance(matrix[0][0], Mobject): + matrix = matrix.astype("string") + matrix = self.string_matrix_to_mob_matrix(matrix) + self.organize_mob_matrix(matrix) + self.add(*matrix.flatten()) + self.add_brackets() + self.center() + self.mob_matrix = matrix + + def string_matrix_to_mob_matrix(self, matrix): + return np.array([ + map(TexMobject, row) + for row in matrix + ]) + + def organize_mob_matrix(self, matrix): + for i, row in enumerate(matrix): + for j, elem in enumerate(row): + mob = matrix[i][j] + if i == 0 and j == 0: + continue + elif i == 0: + mob.next_to(matrix[i][j-1], RIGHT, self.h_buff) + else: + mob.next_to(matrix[i-1][j], DOWN, self.v_buff) + return self + + def get_mob_matrix(self): + return self.mob_matrix + + def add_brackets(self): + bracket_pair = TexMobject("\\big[ \\big]") + bracket_pair.scale(2) + bracket_pair.stretch_to_fit_height(self.get_height() + 0.5) + l_bracket, r_bracket = bracket_pair.split() + l_bracket.next_to(self, LEFT) + r_bracket.next_to(self, RIGHT) + self.add(l_bracket, r_bracket) + return self + + +class NumericalMatrixMultiplication(Scene): + CONFIG = { + "left_matrix" : [[1, 2], [3, 4]], + "right_matrix" : [[5, 6], [7, 8]] + } + def construct(self): + left_string_matrix, right_string_matrix = [ + np.array(matrix).astype("string") + for matrix in self.left_matrix, self.right_matrix + ] + if right_string_matrix.shape[0] != left_string_matrix.shape[1]: + raise Exception("Incompatible shapes for matrix multiplication") + + left = Matrix(left_string_matrix) + right = Matrix(right_string_matrix) + result = self.get_result_matrix( + left_string_matrix, right_string_matrix + ) + + self.organize_matrices(left, right, result) + # self.add_lines(left, right) + self.animate_product(left, right, result) + + + def get_result_matrix(self, left, right): + (m, k), n = left.shape, right.shape[1] + mob_matrix = np.array([VMobject()]).repeat(m*n).reshape((m, n)) + for a in range(m): + for b in range(n): + parts = [ + prefix + "(%s)(%s)"%(left[a][c], right[c][b]) + for c in range(k) + for prefix in ["" if c == 0 else "+"] + ] + mob_matrix[a][b] = TexMobject(parts, next_to_buff = 0.1) + return Matrix(mob_matrix) + + def add_lines(self, left, right): + line_kwargs = { + "color" : BLUE, + "stroke_width" : 2, + } + left_rows = [ + VMobject(*row) for row in left.get_mob_matrix() + ] + h_lines = VMobject() + for row in left_rows[:-1]: + h_line = Line(row.get_left(), row.get_right(), **line_kwargs) + h_line.next_to(row, DOWN, buff = left.v_buff/2.) + h_lines.add(h_line) + + right_cols = [ + VMobject(*col) for col in np.transpose(right.get_mob_matrix()) + ] + v_lines = VMobject() + for col in right_cols[:-1]: + v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs) + v_line.next_to(col, RIGHT, buff = right.h_buff/2.) + v_lines.add(v_line) + + self.play(ShowCreation(h_lines)) + self.play(ShowCreation(v_lines)) + self.dither() + self.show_frame() + + def organize_matrices(self, left, right, result): + equals = TexMobject("=") + everything = VMobject(left, right, equals, result) + everything.arrange_submobjects() + everything.scale_to_fit_width(2*SPACE_WIDTH-1) + self.add(everything) + + + def animate_product(self, left, right, result): + l_matrix = left.get_mob_matrix() + r_matrix = right.get_mob_matrix() + result_matrix = result.get_mob_matrix() + circle = Circle( + radius = l_matrix[0][0].get_height(), + color = GREEN + ) + circles = VMobject(*[ + entry.get_point_mobject() + for entry in l_matrix[0][0], r_matrix[0][0] + ]) + (m, k), n = l_matrix.shape, r_matrix.shape[1] + for mob in result_matrix.flatten(): + mob.highlight(BLACK) + lagging_anims = [] + for a in range(m): + for b in range(n): + for c in range(k): + l_matrix[a][c].highlight(YELLOW) + r_matrix[c][b].highlight(YELLOW) + for c in range(k): + start_parts = VMobject( + l_matrix[a][c].copy(), + r_matrix[c][b].copy() + ) + result_entry = result_matrix[a][b].split()[c] + + new_circles = VMobject(*[ + circle.copy().shift(part.get_center()) + for part in start_parts.split() + ]) + self.play(Transform(circles, new_circles)) + self.play( + Transform( + start_parts, + result_entry.copy().highlight(YELLOW), + path_arc = -np.pi/2 + ), + *lagging_anims + ) + result_entry.highlight(YELLOW) + self.remove(start_parts) + lagging_anims = [ + ApplyMethod(result_entry.highlight, WHITE) + ] + + for c in range(k): + l_matrix[a][c].highlight(WHITE) + r_matrix[c][b].highlight(WHITE) + self.play(FadeOut(circles), *lagging_anims) + self.dither() + + + + + diff --git a/helpers.py b/helpers.py index b32a8dff..345c973c 100644 --- a/helpers.py +++ b/helpers.py @@ -11,6 +11,8 @@ from scipy import linalg from constants import * +CLOSED_THRESHOLD = 0.01 + def get_smooth_handle_points(points): num_handles = len(points) - 1 dim = points.shape[1] @@ -76,7 +78,7 @@ def diag_to_matrix(l_and_u, diag): return matrix def is_closed(points): - return np.all(points[0] == points[-1]) + return np.linalg.norm(points[0] - points[-1]) < CLOSED_THRESHOLD def color_to_rgb(color): return np.array(Color(color).get_rgb()) diff --git a/topics/characters.py b/topics/characters.py index 55d17208..5da9658a 100644 --- a/topics/characters.py +++ b/topics/characters.py @@ -26,6 +26,7 @@ class PiCreature(SVGMobject): "fill_opacity" : 1.0, "initial_scale_val" : 0.01, "corner_scale_factor" : 0.75, + "flip_at_start" : False, } def __init__(self, mode = "plain", **kwargs): self.parts_named = False @@ -36,6 +37,8 @@ class PiCreature(SVGMobject): digest_config(self, kwargs, locals()) SVGMobject.__init__(self, svg_file, **kwargs) self.init_colors() + if self.flip_at_start: + self.flip() def name_parts(self): self.mouth = self.submobjects[MOUTH_INDEX] @@ -122,14 +125,11 @@ class Randolph(PiCreature): class Mortimer(PiCreature): CONFIG = { - "color" : "#736357" + "color" : "#736357", + "flip_at_start" : True, } - def __init__(self, *args, **kwargs): - PiCreature.__init__(self, *args, **kwargs) - self.flip() - class Mathematician(PiCreature): CONFIG = { "color" : GREY, @@ -219,6 +219,7 @@ class Bubble(SVGMobject): class SpeechBubble(Bubble): CONFIG = { "file_name" : "Bubbles_speech.svg", + "height" : 4 } class ThoughtBubble(Bubble): @@ -226,3 +227,10 @@ class ThoughtBubble(Bubble): "file_name" : "Bubbles_thought.svg", } + def __init__(self, **kwargs): + Bubble.__init__(self, **kwargs) + self.submobjects.sort( + lambda m1, m2 : int((m1.get_bottom()-m2.get_bottom())[1]) + ) + + diff --git a/topics/geometry.py b/topics/geometry.py index 6678e259..c07ec01b 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -8,7 +8,7 @@ class Arc(VMobject): "radius" : 1.0, "start_angle" : 0, "num_anchors" : 8, - "anchors_span_full_range" : True + "anchors_span_full_range" : True, } def __init__(self, angle, **kwargs): digest_locals(self) @@ -164,6 +164,7 @@ class Arrow(Line): if self.preserve_tip_size_when_scaling: self.remove(self.tip) self.add_tip() + return self class Vector(Arrow): CONFIG = {