from manimlib.imports import * def q_mult(q1, q2): w1, x1, y1, z1 = q1 w2, x2, y2, z2 = q2 w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2 x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2 y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2 z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2 return np.array([w, x, y, z]) def stereo_project_point(point, axis=0, r=1, max_norm=10000): point = fdiv(point * r, point[axis] + r) point[axis] = 0 norm = get_norm(point) if norm > max_norm: point *= max_norm / norm return point def stereo_project(mobject, axis=0, r=1, outer_r=10, **kwargs): epsilon = 1 for submob in mobject.family_members_with_points(): points = submob.points n = len(points) for i in range(n): if points[i, axis] == -r: js = it.chain( range(i + 1, n), range(i - 1, -1, -1) ) for j in js: if points[j, axis] == -r: continue else: vect = points[j] - points[i] points[i] += epsilon * vect break submob.apply_function( lambda p: stereo_project_point(p, axis, r, **kwargs) ) # If all points are outside a certain range, this # shouldn't be displayed norms = np.apply_along_axis(get_norm, 1, submob.points) if np.all(norms > outer_r): # TODO, instead set opacity? # submob.points[:, :] = 0 submob.set_fill(opacity=0) submob.set_stroke(opacity=0) return mobject class Linus(VGroup): CONFIG = { "body_config": { "stroke_width": 15, "stroke_color": LIGHT_GREY, "sheen": 0.4, }, "height": 2, } def __init__(self, **kwargs): VGroup.__init__(self, **kwargs) self.body = self.get_body_line() self.eyes = Eyes(self.body) self.add(self.body, self.eyes) self.set_height(self.height) self.center() def change_mode(self, mode, thing_to_look_at=None): self.eyes.change_mode(mode, thing_to_look_at) if mode == "sad": self.become_squiggle() elif mode == "confused": self.become_squiggle(factor=-0.1) elif mode == "pleading": self.become_squiggle(factor=0.3) else: self.become_line() return self def change(self, *args, **kwargs): self.change_mode(*args, **kwargs) return self def look_at(self, thing_to_look_at=None): self.eyes.look_at(thing_to_look_at) return self def blink(self): self.eyes.blink() return self def get_squiggle(self, factor=0.2): sine_curve = FunctionGraph( lambda x: factor * np.sin(x), x_min=0, x_max=TAU, ) sine_curve.rotate(TAU / 4) sine_curve.match_style(self.body) sine_curve.match_height(self.body) sine_curve.move_to(self.body, UP) return sine_curve def get_body_line(self, **kwargs): config = dict(self.body_config) config.update(kwargs) line = Line(ORIGIN, 1.5 * UP, **config) if hasattr(self, "body"): line.match_style(self.body) line.match_height(self.body) line.move_to(self.body, UP) return line def become_squiggle(self, **kwargs): self.body.become(self.get_squiggle(**kwargs)) return self def become_line(self, **kwargs): self.body.become(self.get_body_line(**kwargs)) return self def copy(self): return self.deepcopy() class Felix(PiCreature): CONFIG = { "color": GREEN_D } class PushPin(SVGMobject): CONFIG = { "file_name": "push_pin", "height": 0.5, "sheen": 0.7, "fill_color": GREY, } def __init__(self, **kwargs): SVGMobject.__init__(self, **kwargs) self.rotate(20 * DEGREES) def pin_to(self, point): self.move_to(point, DR) class Hand(SVGMobject): CONFIG = { "file_name": "pinch_hand", "height": 0.5, "sheen": 0.2, "fill_color": ORANGE, } def __init__(self, **kwargs): SVGMobject.__init__(self, **kwargs) # self.rotate(30 * DEGREES) self.add(VectorizedPoint().next_to(self, UP, buff=0.17)) class CheckeredCircle(Circle): CONFIG = { "n_pieces": 16, "colors": [BLUE_E, BLUE_C], "stroke_width": 5, } def __init__(self, **kwargs): Circle.__init__(self, **kwargs) pieces = self.get_pieces(self.n_pieces) self.points = np.zeros((0, 3)) self.add(*pieces) n_colors = len(self.colors) for i, color in enumerate(self.colors): self[i::n_colors].set_color(color) class StereoProjectedSphere(Sphere): CONFIG = { "stereo_project_config": { "axis": 2, }, "max_r": 32, "max_width": FRAME_WIDTH, "max_height": FRAME_WIDTH, "max_depth": FRAME_WIDTH, "radius": 1, } def __init__(self, rotation_matrix=None, **kwargs): digest_config(self, kwargs) if rotation_matrix is None: rotation_matrix = np.identity(3) self.rotation_matrix = rotation_matrix self.stereo_project_config["r"] = self.radius ParametricSurface.__init__( self, self.post_projection_func, **kwargs ) self.submobjects.sort( key=lambda m: -m.get_width() ) self.fade_far_out_submobjects() def post_projection_func(self, u, v): point = self.radius * Sphere.func(self, u, v) rot_point = np.dot(point, self.rotation_matrix.T) result = stereo_project_point( rot_point, **self.stereo_project_config ) epsilon = 1e-4 if np.any(np.abs(result) == np.inf) or np.any(np.isnan(result)): return self.func(u + epsilon, v) return result def fade_far_out_submobjects(self, **kwargs): max_r = kwargs.get("max_r", self.max_r) max_width = kwargs.get("max_width", self.max_width) max_height = kwargs.get("max_height", self.max_height) max_depth = kwargs.get("max_depth", self.max_depth) for submob in self.submobjects: violations = [ np.any(np.apply_along_axis(get_norm, 1, submob.get_anchors()) > max_r), submob.get_width() > max_width, submob.get_height() > max_height, submob.get_depth() > max_depth ] if any(violations): # self.remove(submob) submob.fade(1) return self class StereoProjectedSphereFromHypersphere(StereoProjectedSphere): CONFIG = { "stereo_project_config": { "axis": 0, }, "radius": 2, "multiply_from_right": False, } def __init__(self, quaternion=None, null_axis=0, **kwargs): if quaternion is None: quaternion = np.array([1, 0, 0, 0]) self.quaternion = quaternion self.null_axis = null_axis ParametricSurface.__init__(self, self.q_mult_projection_func, **kwargs) self.fade_far_out_submobjects() def q_mult_projection_func(self, u, v): point = list(Sphere.func(self, u, v)) point.insert(self.null_axis, 0) if self.multiply_from_right: post_q_mult = q_mult(point, self.quaternion) else: post_q_mult = q_mult(self.quaternion, point) projected = list(self.radius * stereo_project_point( post_q_mult, **self.stereo_project_config )) if np.any(np.abs(projected) == np.inf): return self.func(u + 0.001, v) ignored_axis = self.stereo_project_config["axis"] projected.pop(ignored_axis) return np.array(projected) class StereoProjectedCircleFromHypersphere(CheckeredCircle): CONFIG = { "n_pieces": 48, "radius": 2, "max_length": 3 * FRAME_WIDTH, "max_r": 50, "basis_vectors": [ [1, 0, 0, 0], [0, 1, 0, 0], ], "multiply_from_right": False, } def __init__(self, quaternion=None, **kwargs): CheckeredCircle.__init__(self, **kwargs) if quaternion is None: quaternion = [1, 0, 0, 0] self.quaternion = quaternion self.pre_positioning_matrix = self.get_pre_positioning_matrix() self.apply_function(self.projection) self.remove_large_pieces() self.set_shade_in_3d(True) def get_pre_positioning_matrix(self): v1, v2 = [np.array(v) for v in self.basis_vectors] v1 = normalize(v1) v2 = v2 - np.dot(v1, v2) * v1 v2 = normalize(v2) return np.array([v1, v2]).T def projection(self, point): q1 = self.quaternion q2 = np.dot(self.pre_positioning_matrix, point[:2]).flatten() if self.multiply_from_right: new_q = q_mult(q2, q1) else: new_q = q_mult(q1, q2) projected = stereo_project_point( new_q, axis=0, r=self.radius, ) if np.any(projected == np.inf) or np.any(np.isnan(projected)): epsilon = 1e-6 return self.projection(rotate_vector(point, epsilon)) return projected[1:] def remove_large_pieces(self): for piece in self: length = get_norm(piece.points[0] - piece.points[-1]) violations = [ length > self.max_length, get_norm(piece.get_center()) > self.max_r, ] if any(violations): piece.fade(1) class QuaternionTracker(ValueTracker): CONFIG = { "force_unit": True, "dim": 4, } def __init__(self, four_vector=None, **kwargs): Mobject.__init__(self, **kwargs) if four_vector is None: four_vector = np.array([1, 0, 0, 0]) self.set_value(four_vector) if self.force_unit: self.add_updater(lambda q: q.normalize()) def set_value(self, vector): self.points = np.array(vector).reshape((1, 4)) return self def get_value(self): return self.points[0] def normalize(self): self.set_value(normalize( self.get_value(), fall_back=np.array([1, 0, 0, 0]) )) return self class RubiksCube(VGroup): CONFIG = { "colors": [ "#FFD500", # Yellow "#C41E3A", # Orange "#009E60", # Green "#FF5800", # Red "#0051BA", # Blue "#FFFFFF" # White ], } def __init__(self, **kwargs): digest_config(self, kwargs) vectors = [OUT, RIGHT, UP, LEFT, DOWN, IN] faces = [ self.create_face(color, vector) for color, vector in zip(self.colors, vectors) ] VGroup.__init__(self, *it.chain(*faces), **kwargs) self.set_shade_in_3d(True) def create_face(self, color, vector): squares = VGroup(*[ self.create_square(color) for x in range(9) ]) squares.arrange_in_grid( 3, 3, buff=0 ) squares.set_width(2) squares.move_to(OUT, OUT) squares.apply_matrix(z_to_vector(vector)) return squares def create_square(self, color): square = Square( stroke_width=3, stroke_color=BLACK, fill_color=color, fill_opacity=1, side_length=1, ) square.flip() return square # back = square.copy() # back.set_fill(BLACK, 0.85) # back.set_stroke(width=0) # back.shift(0.5 * IN) # return VGroup(square, back) def get_face(self, vect): self.sort(lambda p: np.dot(p, vect)) return self[-(12 + 9):] # Abstract scenes class ManyNumberSystems(Scene): def construct(self): # Too much dumb manually positioning in here... title = Title("Number systems") name_location_color_example_tuples = [ ("Reals", [-4, 2, 0], YELLOW, "1.414"), ("Complex numbers", [4, 0, 0], BLUE, "2 + i"), ("Quaternions", [0, 2, 0], PINK, "2 + 7i + 1j + 8k"), ("Rationals", [3, -2, 0], RED, "1 \\over 3"), ("p-adic numbers", [-2, -2, 0], GREEN, "\\overline{142857}2"), ("Octonions", [-3, 0, 0], LIGHT_GREY, "3e_1 - 2.3e_2 + \\dots + 1.6e_8"), ] systems = VGroup() for name, location, color, ex in name_location_color_example_tuples: system = TextMobject(name) system.set_color(color) system.move_to(location) example = TexMobject(ex) example.next_to(system, DOWN) system.add(example) systems.add(system) R_label, C_label, H_label = systems[:3] number_line = NumberLine(x_min=-3, x_max=3) number_line.add_numbers() number_line.shift(0.25 * FRAME_WIDTH * LEFT) number_line.shift(0.5 * DOWN) R_example_dot = Dot(number_line.number_to_point(1.414)) plane = ComplexPlane(x_radius=3.5, y_radius=2.5) plane.add_coordinates() plane.shift(0.25 * FRAME_WIDTH * RIGHT) plane.shift(0.5 * DOWN) C_example_dot = Dot(plane.coords_to_point(2, 1)) self.add(title) self.play(FadeInFromLarge(H_label)) self.play(LaggedStartMap( FadeInFromLarge, VGroup(*it.chain(systems[:2], systems[3:])), lambda m: (m, 4) )) self.wait() self.add(number_line, plane, systems) self.play( R_label.move_to, 0.25 * FRAME_WIDTH * LEFT + 2 * UP, C_label.move_to, 0.25 * FRAME_WIDTH * RIGHT + 2 * UP, H_label.move_to, 0.75 * FRAME_WIDTH * RIGHT + 2 * UP, FadeOutAndShift(systems[3:], 2 * DOWN), Write(number_line), Write(plane), GrowFromCenter(R_example_dot), R_label[-1].next_to, R_example_dot, UP, GrowFromCenter(C_example_dot), C_label[-1].next_to, C_example_dot, UR, SMALL_BUFF, C_label[-1].shift, 0.4 * LEFT, ) number_line.add(R_example_dot) plane.add(C_example_dot) self.wait(2) self.play(LaggedStartMap( ApplyMethod, VGroup( H_label, VGroup(plane, C_label), VGroup(number_line, R_label), ), lambda m: (m.shift, 0.5 * FRAME_WIDTH * LEFT), lag_ratio=0.8, )) randy = Randolph(height=1.5) randy.next_to(plane, RIGHT) randy.to_edge(DOWN) self.play( randy.change, "maybe", H_label, VFadeIn(randy), ) self.play(Blink(randy)) self.play(randy.change, "confused", H_label.get_top()) self.wait() class RotationsIn3d(SpecialThreeDScene): def construct(self): self.set_camera_orientation(**self.get_default_camera_position()) self.begin_ambient_camera_rotation(rate=0.02) sphere = self.get_sphere() vectors = VGroup(*[ Vector(u * v, color=color).next_to(sphere, u * v, buff=0) for v, color in zip( [RIGHT, UP, OUT], [GREEN, RED, BLUE], ) for u in [-1, 1] ]) vectors.set_shade_in_3d(True) sphere.add(vectors) self.add(self.get_axes()) self.add(sphere) angle_axis_pairs = [ (90 * DEGREES, RIGHT), (120 * DEGREES, UR), (-45 * DEGREES, OUT), (60 * DEGREES, IN + DOWN), (90 * DEGREES, UP), (30 * DEGREES, UP + OUT + RIGHT), ] for angle, axis in angle_axis_pairs: self.play(Rotate( sphere, angle, axis=axis, run_time=2, )) self.wait() class IntroduceHamilton(Scene): def construct(self): hamilton = ImageMobject("Hamilton", height=6) hamilton.to_corner(UL) shamrock = SVGMobject(file_name="shamrock") shamrock.set_height(1) shamrock.set_color("#009a49") shamrock.set_fill(opacity=0.25) shamrock.next_to(hamilton.get_corner(UL), DR) shamrock.align_to(hamilton, UP) hamilton_name = TextMobject( "William Rowan Hamilton" ) hamilton_name.match_width(hamilton) hamilton_name.next_to(hamilton, DOWN) quote = TextMobject( """\\huge ...Every morning in the early part of the above-cited month, on my coming down to breakfast, your (then) little brother William Edwin, and yourself, used to ask me,""", "``Well, Papa, can you multiply triplets''?", """Whereto I was always obliged to reply, with a sad shake of the head: ``No, I can only add and subtract them.''...""", alignment="" ) quote.set_color_by_tex("Papa", YELLOW) quote = VGroup(*it.chain(*quote)) quote.set_width(FRAME_WIDTH - hamilton.get_width() - 2) quote.to_edge(RIGHT) quote_rect = SurroundingRectangle(quote, buff=MED_SMALL_BUFF) quote_rect.set_stroke(WHITE, 2) quote_rect.stretch(1.1, 1) quote_label = TextMobject( "August 5, 1865 Letter\\\\from Hamilton to his son" ) quote_label.next_to(quote_rect, UP) quote_label.set_color(BLUE) VGroup(quote, quote_rect, quote_label).to_edge(UP) plaque = ImageMobject("BroomBridgePlaque") plaque.set_width(FRAME_WIDTH / 2) plaque.to_edge(LEFT) plaque.shift(UP) equation = TexMobject( "i^2 = j^2 = k^2 = ijk = -1", tex_to_color_map={"i": GREEN, "j": RED, "k": BLUE} ) equation_rect = Rectangle(width=3.25, height=0.7) equation_rect.move_to(3.15 * LEFT + 0.25 * DOWN) equation_rect.set_color(WHITE) equation_arrow = Vector(DOWN) equation_arrow.match_color(equation_rect) equation_arrow.next_to(equation_rect, DOWN) equation.next_to(equation_arrow, DOWN) self.play( FadeInFromDown(hamilton), Write(hamilton_name), ) self.play(DrawBorderThenFill(shamrock)) self.wait() self.play( LaggedStartMap( FadeIn, quote, lag_ratio=0.2, run_time=4 ), FadeInFromDown(quote_label), ShowCreation(quote_rect) ) self.wait(3) self.play( ApplyMethod( VGroup(hamilton, shamrock, hamilton_name).to_edge, RIGHT, run_time=2, rate_func=squish_rate_func(smooth, 0.5, 1), ), LaggedStartMap( FadeOutAndShiftDown, VGroup(*it.chain( quote, quote_rect, quote_label )) ) ) self.wait() self.play(FadeIn(plaque)) self.play( ShowCreation(equation_rect), GrowArrow(equation_arrow) ) self.play(ReplacementTransform( equation.copy().replace(equation_rect).fade(1), equation )) self.wait() class QuaternionHistory(Scene): CONFIG = { "names_and_quotes": [ ( "Oliver Heaviside", """\\huge ``As far as the vector analysis I required was concerned, the quaternion was not only not required, but was a positive evil of no inconsiderable magnitude.''""" ), ( "Lord Kelvin", """\\huge ``Quaternions... though beautifully \\\\ ingenious, have been an unmixed evil to those who have touched them in any way, including Clerk Maxwell.''""" ), ] } def construct(self): self.show_dot_product_and_cross_product() self.teaching_students_quaternions() self.show_anti_quaternion_quote() self.mad_hatter() def show_dot_product_and_cross_product(self): date = TexMobject("1843") date.scale(2) date.to_edge(UP) t2c = self.t2c = { "x_1": GREEN, "x_2": GREEN, "y_1": RED, "y_2": RED, "z_1": BLUE, "z_2": BLUE, } def get_colored_tex_mobject(tex): return TexMobject(tex, tex_to_color_map=t2c) v1, v2 = [ Matrix([ ["{}_{}".format(c, i)] for c in "xyz" ], element_to_mobject=get_colored_tex_mobject) for i in (1, 2) ] dot_rhs = get_colored_tex_mobject( "x_1 x_2 + y_1 y_2 + z_1 z_2", ) cross_rhs = Matrix([ ["y_1 z_2 - z_1 y_2"], ["z_1 x_2 - x_1 z_2"], ["x_1 y_2 - y_1 x_2"], ], element_to_mobject=get_colored_tex_mobject) dot_product = VGroup( v1.copy(), TexMobject("\\cdot").scale(2), v2.copy(), TexMobject("="), dot_rhs ) cross_product = VGroup( v1.copy(), TexMobject("\\times"), v2.copy(), TexMobject("="), cross_rhs ) for product in dot_product, cross_product: product.arrange(RIGHT, buff=2 * SMALL_BUFF) product.set_height(1.5) dot_product.next_to(date, DOWN, buff=MED_LARGE_BUFF) dot_product.to_edge(LEFT, buff=LARGE_BUFF) cross_product.next_to( dot_product, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT, ) self.play(FadeInFrom(dot_product, 2 * RIGHT)) self.play(FadeInFrom(cross_product, 2 * LEFT)) self.wait() self.play(FadeInFromDown(date)) self.play(ApplyMethod(dot_product.fade, 0.7)) self.play(ApplyMethod(cross_product.fade, 0.7)) self.wait() self.play( FadeOutAndShift(dot_product, 2 * LEFT), FadeOutAndShift(cross_product, 2 * RIGHT), ) self.date = date def teaching_students_quaternions(self): hamilton = ImageMobject("Hamilton") hamilton.set_height(4) hamilton.pixel_array = hamilton.pixel_array[:, ::-1, :] hamilton.to_corner(UR) hamilton.shift(MED_SMALL_BUFF * DOWN) colors = color_gradient([BLUE_E, GREY_BROWN, BLUE_B], 7) random.shuffle(colors) students = VGroup(*[ PiCreature(color=color) for color in colors ]) students.set_height(2) students.arrange(RIGHT) students.set_width(FRAME_WIDTH - hamilton.get_width() - 1) students.to_corner(DL) equation = TexMobject(""" (x_1 i + y_1 j + z_1 k) (x_2 i + y_2 j + z_2 k) = (-x_1 x_2 - y_1 y_2 - z_1 z_2) + (y_1 z_2 - z_1 y_2)i + (z_1 x_2 - x_1 z_2)j + (x_1 y_2 - y_1 x_2)k """, tex_to_color_map=self.t2c) equation.set_width(FRAME_WIDTH - 1) equation.to_edge(UP, buff=MED_SMALL_BUFF) images = Group() image_labels = VGroup() images_with_labels = Group() names = ["Peter Tait", "Robert Ball", "Macfarlane Alexander"] for name in names: image = ImageMobject(name) image.set_height(3) label = TextMobject(name) label.scale(0.5) label.next_to(image, DOWN) image.label = label image_labels.add(label) images.add(image) images_with_labels.add(Group(image, label)) images_with_labels.arrange(RIGHT) images_with_labels.next_to(hamilton, LEFT, LARGE_BUFF) images_with_labels.shift(MED_LARGE_BUFF * DOWN) society_title = TextMobject("Quaternion society") society_title.next_to(images, UP, MED_LARGE_BUFF, UP) def blink_wait(n_loops): for x in range(n_loops): self.play(Blink(random.choice(students))) self.wait(random.random()) self.play( FadeInFromDown(hamilton), Write( self.date, rate_func=lambda t: smooth(1 - t), remover=True ) ) self.play(LaggedStartMap( FadeInFrom, students, lambda m: (m, LEFT), )) self.play( LaggedStartMap( ApplyMethod, students, lambda pi: ( pi.change, random.choice(["confused", "maybe", "erm"]), 3 * LEFT + 2 * UP, ), ), Write(equation), ) blink_wait(3) self.play( LaggedStartMap(FadeInFromDown, images), LaggedStartMap(FadeInFromLarge, image_labels), Write(society_title) ) blink_wait(3) self.play( FadeOutAndShift(hamilton, RIGHT), LaggedStartMap( FadeOutAndShift, images_with_labels, lambda m: (m, UP) ), FadeOutAndShift(students, DOWN), FadeOut(society_title), run_time=1 ) self.equation = equation def show_anti_quaternion_quote(self): group = self.get_dissenter_images_quotes_and_names() images, quotes, names = group self.play( LaggedStartMap(FadeInFromDown, images), LaggedStartMap(FadeInFromLarge, names), lag_ratio=0.75, run_time=2, ) for quote in quotes: self.play(LaggedStartMap( FadeIn, VGroup(*quote.family_members_with_points()), lag_ratio=0.3 )) self.wait() self.play(FadeOut(images_with_quotes)) def mad_hatter(self): title = TextMobject( "Lewis Carroll's", "``Alice in wonderland''" ) title.to_edge(UP, buff=LARGE_BUFF) author_brace = Brace(title[0], DOWN) aka = TextMobject("a.k.a. Mathematician Charles Dodgson") aka.scale(0.8) aka.set_color(BLUE) aka.next_to(author_brace, DOWN) quote = TextMobject( """ ``Why, you might just as well say that\\\\ ‘I see what I eat’ is the same thing as\\\\ ‘I eat what I see’!'' """, tex_to_color_map={ "I see what I eat": BLUE, "I eat what I see": YELLOW, }, alignment="" ) quote.to_edge(UP, buff=LARGE_BUFF) hatter = PiCreature(color=RED, mode="surprised") hat = SVGMobject(file_name="hat") hat_back = hat.copy() hat_back[0].remove(*[ sm for sm in hat_back[0] if sm.is_subpath ]) hat_back.set_fill(DARK_GREY) hat.add_to_back(hat_back) hat.set_height(1.25) hat.next_to(hatter.body, UP, buff=-MED_SMALL_BUFF) hatter.add(hat) hatter.look(DL) hatter.pupils[1].save_state() hatter.look(UL) hatter.pupils[1].restore() hatter.set_height(2) hare = SVGMobject(file_name="bunny") mouse = SVGMobject(file_name="mouse") for mob in hare, mouse: mob.set_color(LIGHT_GREY) mob.set_sheen(0.2, UL) mob.set_height(1.5) characters = VGroup(hatter, hare, mouse) for mob, p in zip(characters, [UP, DL, DR]): mob.move_to(p) hare.shift(MED_SMALL_BUFF * LEFT) characters.space_out_submobjects(1.5) characters.to_edge(DOWN) def change_single_place(char, **kwargs): i = characters.submobjects.index(char) target = characters[(i + 1) % 3] return ApplyMethod( char.move_to, target, path_arc=-90 * DEGREES, **kwargs ) def get_change_places(): return LaggedStartMap( change_single_place, characters, lag_ratio=0.6 ) self.play( Write(title), LaggedStartMap(FadeInFromDown, characters) ) self.play( get_change_places(), GrowFromCenter(author_brace), FadeIn(aka) ) for x in range(4): self.play(get_change_places()) self.play( FadeOutAndShift(VGroup(title, author_brace, aka)), FadeInFromDown(quote), ) self.play(get_change_places()) self.play( get_change_places(), VFadeOut(characters, run_time=2) ) self.remove(characters) self.wait() # def get_dissenter_images_quotes_and_names(self): names_and_quotes = self.names_and_quotes images = Group() quotes = VGroup() names = VGroup() images_with_quotes = Group() for name, quote_text in names_and_quotes: image = Group(ImageMobject(name)) image.set_height(4) label = TextMobject(name) label.next_to(image, DOWN) names.add(label) quote = TextMobject( quote_text, tex_to_color_map={ "positive evil": RED, "unmixed evil": RED, }, alignment="" ) quote.scale(0.3) quote.next_to(image, UP) images.add(image) quotes.add(quote) images_with_quotes.add(Group(image, label, quote)) images_with_quotes.arrange(RIGHT, buff=LARGE_BUFF) images_with_quotes.to_edge(DOWN, MED_LARGE_BUFF) return Group(images, quotes, names) class QuaternionRotationOverlay(Scene): def construct(self): equations = VGroup( TexMobject( "p", "\\rightarrow", "{}", "{}", "\\left(q_1", "p", "q_1^{-1}\\right)", "{}", "{}", ), TexMobject( "p", "\\rightarrow", "{}", "\\left(q_2", "\\left(q_1", "p", "q_1^{-1}\\right)", "q_2^{-1}\\right)", "{}", ), TexMobject( "p", "\\rightarrow", "\\left(q_3", "\\left(q_2", "\\left(q_1", "p", "q_1^{-1}\\right)", "q_2^{-1}\\right)", "q_3^{-1}\\right)", ), ) for equation in equations: equation.set_color_by_tex_to_color_map({ "1": GREEN, "2": RED, "3": BLUE, }) equation.set_color_by_tex("rightarrow", WHITE) equation.to_corner(UL) equation = equations[0].copy() self.play(Write(equation)) self.wait() for new_equation in equations[1:]: self.play( Transform(equation, new_equation) ) self.wait(2) class RotateCubeThreeTimes(SpecialThreeDScene): def construct(self): cube = RubiksCube() cube.set_fill(opacity=0.8) cube.set_stroke(width=1) randy = Randolph(mode="pondering") randy.set_height(cube.get_height() - 2 * SMALL_BUFF) randy.move_to(cube.get_edge_center(OUT)) randy.set_fill(opacity=0.8) # randy.set_shade_in_3d(True) cube.add(randy) axes = self.get_axes() self.add(axes, cube) self.move_camera( phi=70 * DEGREES, theta=-140 * DEGREES, ) self.begin_ambient_camera_rotation(rate=0.02) self.wait(2) self.play(Rotate(cube, TAU / 4, RIGHT, run_time=3)) self.wait(2) self.play(Rotate(cube, TAU / 4, UP, run_time=3)) self.wait(2) self.play(Rotate(cube, -TAU / 3, np.ones(3), run_time=3)) self.wait(7) class QuantumSpin(Scene): def construct(self): title = Title("Two-state system") electron = Dot(color=BLUE) electron.set_height(1) electron.set_sheen(0.3, UL) electron.set_fill(opacity=0.8) kwargs = { "path_arc": PI, } curved_arrows = VGroup( Arrow(RIGHT, LEFT, **kwargs), Arrow(LEFT, RIGHT, **kwargs), ) curved_arrows.set_color(LIGHT_GREY) curved_arrows.set_stroke(width=2) y_arrow = Vector(UP) y_arrow.set_color(RED) y_ca = curved_arrows.copy() y_ca.rotate(70 * DEGREES, LEFT) y_group = VGroup(y_ca[0], y_arrow, electron.copy(), y_ca[1]) x_arrow = y_arrow.copy().rotate(90 * DEGREES, IN, about_point=ORIGIN) x_arrow.set_color(GREEN) x_ca = curved_arrows.copy() x_ca.rotate(70 * DEGREES, UP) x_group = VGroup(x_ca[0], x_arrow, electron.copy(), x_ca[1]) z_ca = curved_arrows.copy() z_group = VGroup(electron.copy(), z_ca, Dot(color=BLUE)) groups = VGroup(x_group, y_group, z_group) groups.arrange(RIGHT, buff=1.5) groups.move_to(UP) y_ca.rotation = Rotating( y_ca, axis=rotate_vector(OUT, 70 * DEGREES, LEFT) ) x_ca.rotation = Rotating( x_ca, axis=rotate_vector(OUT, 70 * DEGREES, UP) ) z_ca.rotation = Rotating(z_ca, axis=OUT) rotations = [ca.rotation for ca in [x_ca, y_ca, z_ca]] matrices = VGroup( Matrix([["0", "1"], ["1", "0"]]), Matrix([["0", "-i"], ["i", "0"]]), Matrix([["1", "0"], ["0", "-1"]]), ) for matrix, group in zip(matrices, groups): matrix.next_to(group, DOWN) for matrix in matrices[1:]: matrix.align_to(matrices[0], DOWN) self.add(title, groups) self.play() self.play( LaggedStartMap(FadeInFromDown, matrices, run_time=3), *rotations ) for x in range(2): self.play(*rotations) class HereWeTackle4d(TeacherStudentsScene): def construct(self): titles = VGroup( TextMobject( "This video:\\\\", "Quaternions in 4d" ), TextMobject( "Next video:\\\\", "Quaternions acting on 3d" ) ) for title in titles: title.move_to(self.hold_up_spot, DOWN) titles[0].set_color(YELLOW) self.play( self.teacher.change, "raise_right_hand", FadeInFromDown(titles[0]), self.get_student_changes("confused", "horrified", "sad") ) self.look_at(self.screen) self.wait() self.change_student_modes( "erm", "thinking", "pondering", look_at_arg=self.screen ) self.wait(3) self.change_student_modes( "pondering", "confused", "happy" ) self.look_at(self.screen) self.wait(3) self.play( self.teacher.change, "hooray", FadeInFrom(titles[1]), ApplyMethod( titles[0].shift, 2 * UP, rate_func=squish_rate_func(smooth, 0.2, 1) ) ) self.change_all_student_modes("hooray") self.play(self.teacher.change, "happy") self.look_at(self.screen) self.wait(3) self.change_student_modes("pondering", "happy", "thinking") self.wait(4) class TableOfContents(Scene): def construct(self): chapters = VGroup( TextMobject( "\\underline{Chapter 1}\\\\", "Linus the Linelander" ), TextMobject( "\\underline{Chapter 2}\\\\", "Felix the Flatlander" ), TextMobject( "\\underline{Chapter 3}\\\\", " You, the 3d-lander" ), ) for chapter in chapters: chapter.space_out_submobjects(1.5) chapters.arrange( DOWN, buff=1.5, aligned_edge=LEFT ) chapters.to_edge(LEFT) for chapter in chapters: self.play(FadeInFromDown(chapter)) self.wait(2) for chapter in chapters: chapters.save_state() other_chapters = VGroup(*[ c for c in chapters if c is not chapter ]) self.play( chapter.set_width, 0.5 * FRAME_WIDTH, chapter.center, other_chapters.fade, 1 ) self.wait(3) self.play(chapters.restore) class IntroduceLinusTheLinelander(Scene): def construct(self): self.introduce_linus() self.show_real_number_line() self.look_at_complex_plane() def introduce_linus(self): linus = Linus() linus.move_to(3 * LEFT) name = TextMobject("Linus the Linelander") name.next_to(linus, DR, buff=MED_LARGE_BUFF) arrow = Arrow(name.get_top(), linus.get_right()) self.play(FadeInFromDown(linus)) self.play( Write(name), GrowArrow(arrow), linus.change, "gracious", name, ) self.play( linus.become_squiggle, {"factor": -0.1}, ) self.play(Blink(linus)) self.wait() self.name_text = name self.name_arrow = arrow self.linus = linus def show_real_number_line(self): linus = self.linus number_line = NumberLine() number_line.add_numbers() number_line.to_edge(UP) algebra = VGroup( TexMobject("3 \\cdot 4 = 12"), TexMobject("3 + 4 = 7"), TexMobject("(-2) \\cdot 3 = -6"), ) algebra.arrange(DOWN) algebra.next_to(number_line, DOWN, LARGE_BUFF) algebra.shift(3 * RIGHT) self.play( ShowCreation(number_line), linus.look_at, number_line ) self.play( LaggedStartMap(FadeInFromDown, number_line.numbers), LaggedStartMap(ShowCreation, number_line.tick_marks), linus.change, "happy" ) self.play( LaggedStartMap(FadeInFromDown, algebra), linus.look_at, algebra ) self.play(Blink(linus)) self.wait() self.algebra = algebra def look_at_complex_plane(self): linus = self.linus to_fade = VGroup( self.name_text, self.name_arrow, self.algebra, ) frame = ScreenRectangle() frame.set_width(8) frame.to_corner(DR) q_marks = VGroup(*[ TexMobject("?").shift( random.random() * RIGHT, random.random() * UP, ) for x in range(50) ]) q_marks.next_to(linus.body, UP, buff=0) q_marks.set_color_by_gradient(BLUE, GREEN, YELLOW) random.shuffle(q_marks.submobjects) q_marks_anim = LaggedStartMap( FadeIn, q_marks, run_time=15, rate_func=there_and_back, lag_ratio=0.1 ) q_marks_continual = turn_animation_into_updater(q_marks_anim) self.play( FadeOut(to_fade), ShowCreation(frame), linus.look_at, frame ) self.add(q_marks_continual) self.play(linus.change_mode, "confused") self.wait() self.play(Blink(linus)) self.play(linus.change, "confused", frame.get_bottom()) self.wait() self.play(linus.change, "sad", frame.get_center()) self.wait(10) class ShowComplexMultiplicationExamples(Scene): CONFIG = { "plane_config": { "x_radius": 9, "y_radius": 9, "stroke_width": 3, }, "background_plane_config": { "color": LIGHT_GREY, "secondary_color": DARK_GREY, "stroke_width": 0.5, "stroke_opacity": 0.5, "secondary_line_ratio": 0, } } def construct(self): self.add_planes() z_tuples = [ (complex(2, 1), "2 + i", UP), (complex(5, 2), "5 + 2i", LEFT), ( complex(-np.sqrt(2) / 2, np.sqrt(2) / 2), "-\\frac{\\sqrt{2}}{2} + \\frac{\\sqrt{2}}{2} i", LEFT, ), (complex(-4, 1.5), "-4 + 1.5i", RIGHT), (complex(3, 0), "3 + 0i", UP), (complex(4, -3), "4 + -3i", UP), ] for z, z_tex, label_vect in z_tuples: self.show_multiplication(z, z_tex, label_vect) def add_planes(self, include_title=True): plane = ComplexPlane(**self.plane_config) self.plane = plane background_plane = ComplexPlane(**self.background_plane_config) background_plane.add_coordinates() self.background_plane = background_plane self.add(background_plane) self.add(plane) if include_title: title = TextMobject("Complex plane") title.scale(1.5) title.to_corner(UL, buff=MED_LARGE_BUFF) title.shift(SMALL_BUFF * UR) self.title = title self.add_foreground_mobjects(title) def show_multiplication(self, z, z_tex, label_vect): z_color = WHITE plane = self.plane new_plane = plane.deepcopy() real_tex, imag_tex = z_tex.split("+") label = TexMobject( "\\text{Multiply by}\\\\", real_tex, "+", imag_tex, alignment="", ) label[1].set_color(GREEN) label[3].set_color(RED) label.scale(1.2) h_line = Line( plane.number_to_point(0), plane.number_to_point(z.real), color=GREEN, stroke_width=5, ) v_line = Line( plane.number_to_point(z.real), plane.number_to_point(z), color=RED, stroke_width=5, ) lines = VGroup(h_line, v_line) z_point = plane.number_to_point(z) z_dot = Dot(z_point) z_dot.set_color(z_color) label[1:].next_to(z_dot, label_vect) label[0].next_to(label[1:], UP) for mob in label: label.add_to_back(BackgroundRectangle(mob)) one_dot = Dot(plane.number_to_point(1)) one_dot.set_color(YELLOW) for dot in z_dot, one_dot: dot.save_state() dot.scale(5) dot.set_fill(opacity=0) dot.set_stroke(width=1, opacity=0.5) to_fade_out = VGroup( plane, label, lines, z_dot, one_dot ) self.play( ShowCreation(lines), FadeInFromDown(label), Restore(z_dot), ) self.play(Restore(one_dot)) angle = np.log(z).imag self.play( one_dot.move_to, z_dot, plane.apply_complex_function, lambda w: z * w, path_arc=angle, run_time=3 ) self.wait() self.play( FadeOut(to_fade_out), FadeIn(new_plane), ) self.plane = new_plane class DefineComplexNumbersPurelyAlgebraically(Scene): def construct(self): self.add_linus() self.add_title() self.show_example_number() self.show_multiplication() self.emphsize_result_has_same_form() def add_linus(self): linus = self.linus = Linus() linus.move_to(3 * LEFT) def add_title(self): title = self.title = Title( "No spatial reasoning, just symbols" ) self.play( FadeInFromDown(title[:-1]), ShowCreation(title[-1]), self.linus.look_at, title ) def show_example_number(self): linus = self.linus number = TexMobject("2.35", "+", "3.14", "i") number.next_to(self.title, DOWN, buff=1.5) number.shift(3 * RIGHT) real, imag = number[0], number[2] real_brace = Brace(real, UP) imag_brace = Brace(imag, DOWN) real_label = real_brace.get_text("Some real number") imag_label = imag_brace.get_text("Some other real number") VGroup(real, real_label).set_color(GREEN) VGroup(imag, imag_label).set_color(RED) i = number[-1] i_def = TexMobject("i", "^2", "=", "-1") i_def.next_to( self.title, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT, ) i_def_rect = SurroundingRectangle(i_def, color=YELLOW, buff=MED_SMALL_BUFF) definition_label = TextMobject("Definition") definition_label.next_to(i_def_rect, DOWN) definition_label.match_color(i_def_rect) self.play(Write(number, run_time=1)) self.play( GrowFromCenter(real_brace), LaggedStartMap(FadeIn, real_label), linus.change, "confused", number, run_time=1 ) self.wait() self.play( GrowFromCenter(imag_brace), LaggedStartMap(FadeIn, imag_label), run_time=1 ) self.play(Blink(linus)) self.play( linus.change, "erm", i_def, ReplacementTransform( i.copy(), i_def[0], path_arc=-30 * DEGREES ), FadeIn(i_def_rect), FadeIn(definition_label), ) self.play(Write(i_def[1:])) self.wait() self.play(Blink(linus)) self.to_fade = VGroup( real_brace, real_label, imag_brace, imag_label, ) self.number = number def show_multiplication(self): linus = self.linus to_fade = self.to_fade z1 = self.number z2 = TexMobject("4", "+", "5", "i") z2.match_style(z1) for z in z1, z2: lp, rp = z.parens = TexMobject("()") lp.next_to(z, LEFT, SMALL_BUFF) rp.next_to(z, RIGHT, SMALL_BUFF) z.real = z[0] z.imag = z[2:] for part in z.real, z.imag: part.targets = [part.copy(), part.copy()] z1.generate_target() product = VGroup( VGroup(z1.target, z1.parens), VGroup(z2, z2.parens), ) product.arrange(RIGHT, SMALL_BUFF) product.move_to(2 * RIGHT + 2 * UP) foil = VGroup(*map(TextMobject, [ "First", "Outside", "Inside", "Last", ])) foil.arrange( DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT ) foil.scale(1.25) for word in foil: word[0].set_color(BLUE) foil.move_to(product).to_edge(DOWN, LARGE_BUFF) def get_cdot(): return TexMobject("\\cdot") def get_lp(): return TexMobject("(") def get_rp(): return TexMobject(")") def get_plus(): return TexMobject("+") expansion = VGroup( z1.real.targets[0], get_cdot(), z2.real.targets[0], get_plus(), z1.real.targets[1], get_cdot(), z2.imag.targets[0], get_plus(), z1.imag.targets[0], get_cdot(), z2.real.targets[1], get_plus(), z1.imag.targets[1], get_cdot(), z2.imag.targets[1], ) expansion.arrange(RIGHT, buff=0.15) expansion.next_to(product, DOWN, buff=LARGE_BUFF) expansion_parts = VGroup(*[ expansion[4 * i: 4 * i + 3] for i in range(4) ]) expansion_part_braces = VGroup(*[ Brace(part, DOWN) for part in expansion_parts ]) for word, brace in zip(foil, expansion_part_braces): word.next_to(brace, DOWN) final_prouct = VGroup( get_lp(), z1[0].copy(), get_cdot(), z2[0].copy(), TexMobject("-"), z1[2].copy(), get_cdot(), z2[2].copy(), get_rp(), get_plus(), get_lp(), z1[0].copy(), get_cdot(), z2[2].copy(), get_plus(), z1[2].copy(), get_cdot(), z2[0].copy(), get_rp(), TexMobject("i") ) final_prouct.arrange(RIGHT, buff=0.15) final_prouct.next_to(expansion, DOWN, buff=2) final_arrows = VGroup() for i, brace in zip([1, 11, 15, 5], expansion_part_braces): target = final_prouct[i:i + 3] if i == 5: arrow = Line( brace.get_bottom() + SMALL_BUFF * DOWN, target.get_top() + MED_SMALL_BUFF * UP, ) arrow.points[1] = arrow.points[0] + DOWN arrow.points[2] = arrow.points[3] + UP tip = RegularPolygon(3, start_angle=-100 * DEGREES) tip.set_height(0.2) tip.set_stroke(width=0) tip.set_fill(WHITE, opacity=1) tip.move_to(arrow.get_end()) arrow.add(tip) else: arrow = Arrow( brace.get_bottom(), target.get_top(), ) final_arrows.add(arrow) final_arrows.set_stroke(BLACK, width=6, background=True) # Move example number into product self.play( FadeOut(to_fade), MoveToTarget(z1), FadeIn(z1.parens), FadeInFromDown(z2), FadeIn(z2.parens), linus.change, "happy", product, ) self.wait() # Show distribution pairs = list(it.product([z1.real, z1.imag], [z2.real, z2.imag])) for i in range(4): left, right = pair = VGroup(*pairs[i]) word = foil[i] dot = expansion[4 * i + 1] plus = expansion[4 * i + 3] if i < 3 else VMobject() brace = expansion_part_braces[i] self.play(pair.shift, 0.5 * DOWN) self.play( FadeIn(dot), GrowFromCenter(brace), FadeInFromDown(word), linus.move_to, 4 * LEFT + DOWN, *[ ReplacementTransform( part.copy(), part.targets.pop(0) ) for part in pair ] ) self.play( pair.shift, 0.5 * UP, FadeIn(plus) ) self.play(Blink(linus)) self.play( FadeOut(foil), FadeInFromDown(final_prouct), linus.look_at, final_prouct, ) self.play( LaggedStartMap(ShowCreation, final_arrows), run_time=3, ) self.play(linus.change, "confused") self.wait() self.final_prouct = final_prouct def emphsize_result_has_same_form(self): final_product = self.final_prouct real = final_product[1:1 + 7] imag = final_product[11:11 + 7] real_brace = Brace(real, DOWN) real_label = real_brace.get_text("Some real number") real_label.set_color(GREEN) imag_brace = Brace(imag, DOWN) imag_label = imag_brace.get_text( "Some other \\\\ real number" ) imag_label.set_color(RED) braces = VGroup(real_brace, imag_brace) labels = VGroup(real_label, imag_label) self.play( LaggedStartMap(GrowFromCenter, braces), LaggedStartMap(Write, labels), ) self.wait() class TextbookQuaternionDefinition(TeacherStudentsScene): CONFIG = { "random_seed": 1, } def construct(self): equation = TexMobject( """ (w_1 + x_1 i + y_1 j + z_1 k) (w_2 + x_2 i + y_2 j + z_2 k) = &(w_1 w_2 - x_1 x_2 - y_1 y_2 - z_1 z_2) \\, +\\\\ &(w_1 x_2 + x_1 w_2 + y_1 z_2 - z_1 y_2)i \\, +\\\\ &(w_1 y_2 + y_1 w_2 + z_1 x_2 - x_1 z_2)j \\, +\\\\ &(w_1 z_2 + z_1 w_2 + x_1 y_2 - y_1 x_2)k \\\\ """, tex_to_color_map={ "w_1": YELLOW, "w_2": YELLOW, "x_1": GREEN, "x_2": GREEN, "y_1": RED, "y_2": RED, "z_1": BLUE, "z_2": BLUE, } ) equation.set_width(FRAME_WIDTH - 1) equation.to_edge(UP) defining_products = VGroup(*[ TexMobject( tex, tex_to_color_map={ "i": GREEN, "j": RED, "k": BLUE, } ) for tex in [ "i^2 = j^2 = k^2 = -1", "ij = -ji = k", "ki = -ik = j", "jk = -kj = i", ] ]) defining_products.arrange(DOWN) defining_products.next_to(self.students, UP, LARGE_BUFF) def_rect = SurroundingRectangle(defining_products) self.play( LaggedStartMap(FadeInFromDown, defining_products), self.get_student_changes(*3 * ["confused"]), self.teacher.change, "raise_right_hand", ) self.play(ShowCreation(def_rect)) self.play( Write(equation, run_time=4, lag_ratio=0.2), self.get_student_changes( "horrified", "pleading", "sick", equation ), self.teacher.change, "erm", equation, ) self.blink() self.look_at(equation.get_corner(UL)) self.blink() self.look_at(equation.get_corner(UR)) self.play(self.teacher.change, "sassy", equation) self.wait(2) self.change_all_student_modes("pondering") self.look_at(equation) self.wait() self.play(self.teacher.change, "thinking", equation) self.wait(8) class ProblemsWhereComplexNumbersArise(Scene): def construct(self): text = "Problems where complex numbers are surprisingly useful" title = TextMobject(*text.split(" ")) title.to_edge(UP) title.set_color(BLUE) underline = Line(LEFT, RIGHT) underline.set_width(title.get_width() + 0.5) underline.next_to(title, DOWN) problems = VGroup( TextMobject( "Integer solutions to\\\\ $a^2 + b^2 = c^2$", alignment="" ), TextMobject( "Understanding\\\\", "$1 - \\frac{1}{3} + \\frac{1}{5} - \\frac{1}{7} + \\cdots" + "=\\frac{\\pi}{4}$", alignment="", ), TextMobject("Frequency analysis") ) problems.arrange( DOWN, buff=LARGE_BUFF, aligned_edge=LEFT ) for problem in problems: problems.add(Dot().next_to(problem[0], LEFT)) problems.next_to(underline, DOWN, buff=MED_LARGE_BUFF) problems.to_edge(LEFT) v_dots = TexMobject("\\vdots") v_dots.scale(2) v_dots.next_to(problems, DOWN, aligned_edge=LEFT) self.add(problems, v_dots) self.play( ShowCreation(underline), LaggedStartMap(FadeInFromDown, title, lag_ratio=0.5), run_time=3 ) self.wait() class WalkThroughComplexMultiplication(ShowComplexMultiplicationExamples): CONFIG = { "z": complex(2, 3), "w": complex(1, -1), "z_color": YELLOW, "w_color": PINK, "product_color": RED, } def construct(self): self.add_planes(include_title=False) self.introduce_z_and_w() self.show_action_on_all_complex_numbers() def introduce_z_and_w(self): # Tolerating code repetition here in case I want # to specialize behavior for z or w... plane = self.plane origin = plane.number_to_point(0) z_point = plane.number_to_point(self.z) z_dot = Dot(z_point) z_line = Line(origin, z_point) z_label = VGroup( TexMobject("z="), DecimalNumber(self.z, num_decimal_places=0) ) z_label.arrange( RIGHT, buff=SMALL_BUFF, ) z_label.next_to(z_dot, UP, buff=SMALL_BUFF) z_label.set_color(self.z_color) z_label.add_background_rectangle() VGroup(z_line, z_dot).set_color(self.z_color) w_point = plane.number_to_point(self.w) w_dot = Dot(w_point) w_line = Line(origin, w_point) w_label = VGroup( TexMobject("w="), DecimalNumber(self.w, num_decimal_places=0) ) w_label.arrange(RIGHT, buff=SMALL_BUFF) w_label.next_to(w_dot, DOWN, buff=SMALL_BUFF) w_label.set_color(self.w_color) w_label.add_background_rectangle() VGroup(w_line, w_dot).set_color(self.w_color) VGroup( z_label[1], w_label[1] ).shift(0.25 * SMALL_BUFF * DOWN) product = TexMobject("z", "\\cdot", "w") z_sym, w_sym = product[0], product[2] z_sym.set_color(self.z_color) w_sym.set_color(self.w_color) product.scale(2) product.to_corner(UL) product.add_background_rectangle() self.add( z_line, z_label, w_line, w_label, ) self.play(LaggedStartMap( FadeInFromLarge, VGroup(z_dot, w_dot), lambda m: (m, 5), lag_ratio=0.8, run_time=1.5 )) self.play( ReplacementTransform(z_label[1][0].copy(), z_sym) ) self.add(product[:-1]) self.play( ReplacementTransform(w_label[1][0].copy(), w_sym), FadeInFrom(product[2], LEFT), FadeIn(product[0]), ) self.wait() self.set_variables_as_attrs( product, z_point, w_point, z_dot, w_dot, z_line, w_line, ) def show_action_on_all_complex_numbers(self): plane = self.plane plane.save_state() origin = plane.number_to_point(0) z = self.z angle = np.log(z).imag product_tex = self.product[1:] z_sym, cdot, w_sym = product_tex product = self.z * self.w product_point = plane.number_to_point(product) product_dot = Dot(product_point) product_line = Line(origin, product_point) for mob in product_line, product_dot: mob.set_color(self.product_color) rect = SurroundingRectangle(VGroup(z_sym, cdot)) rect.set_fill(BLACK, opacity=1) func_words = TextMobject("Function on the plane") func_words.next_to( rect, DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT, ) func_words.set_color(self.z_color) sparkly_plane = VGroup() for line in plane.family_members_with_points(): if self.camera.is_in_frame(line): for piece in line.get_pieces(10): p1, p2 = piece.get_pieces(2) p1.rotate(PI) pair = VGroup(p1, p2) pair.scale(0.3) sparkly_plane.add(pair) sparkly_plane.sort( lambda p: 0.1 * get_norm(p) + random.random() ) sparkly_plane.set_color_by_gradient(YELLOW, RED, PINK, BLUE) sparkly_plane.set_stroke(width=4) pin = PushPin() pin.move_to(origin, DR) one_dot = Dot(plane.number_to_point(1)) one_dot.set_fill(WHITE) one_dot.set_stroke(BLACK, 1) hand = Hand() hand.move_to(plane.number_to_point(1), LEFT) zero_eq = TexMobject("z \\cdot 0 = 0") one_eq = TexMobject("z \\cdot 1 = z") equations = VGroup(zero_eq, one_eq) equations.arrange(DOWN) equations.scale(1.5) for eq in equations: eq.add_background_rectangle() equations.next_to(func_words, DOWN) equations.to_edge(LEFT) product_label = VGroup( TexMobject("z \\cdot w ="), DecimalNumber(product, num_decimal_places=0) ) product_label.arrange(RIGHT) product_label[0].shift(0.025 * DOWN) product_label.next_to(product_dot, UP, SMALL_BUFF) product_label.add_background_rectangle() big_rect = Rectangle( height=4, width=6, fill_color=BLACK, fill_opacity=0.9, stroke_width=0, ) big_rect.to_corner(UL, buff=0) self.add(big_rect, product_tex, rect, z_sym, cdot) self.play( FadeIn(big_rect), ShowCreation(rect), Write(func_words), run_time=1 ) self.play( ReplacementTransform( self.w_line.copy(), product_line, ), ReplacementTransform( self.w_dot.copy(), product_dot, ), path_arc=angle, run_time=3 ) self.wait() self.play(FadeOut(VGroup(product_line, product_dot))) self.play(LaggedStartMap( ShowCreationThenDestruction, sparkly_plane, lag_ratio=0.5, run_time=2 )) self.play( plane.apply_complex_function, lambda w: z * w, Transform(self.w_line, product_line), Transform(self.w_dot, product_dot), path_arc=angle, run_time=9, rate_func=lambda t: there_and_back_with_pause(t, 2 / 9) ) self.wait() self.play(FadeInFrom(pin, UL)) self.play(Write(zero_eq)) self.play( FadeInFromLarge(one_dot), FadeInFrom(hand, UR) ) self.play(Write(one_eq)) self.wait() self.play( plane.apply_complex_function, lambda w: z * w, ReplacementTransform(self.w_line.copy(), product_line), ReplacementTransform(self.w_dot.copy(), product_dot), one_dot.move_to, self.z_point, hand.move_to, self.z_point, LEFT, path_arc=angle, run_time=4, ) self.play(Write(product_label)) class ShowUnitCircleActions(ShowComplexMultiplicationExamples): CONFIG = { "random_seed": 0, "plane_config": { "secondary_line_ratio": 0, } } def construct(self): self.add_planes(include_title=False) self.show_unit_circle_actions() def show_unit_circle_actions(self): plane = self.plane origin = plane.number_to_point(0) one_point = plane.number_to_point(1) one_dot = Dot(one_point) one_dot.set_fill(WHITE) one_dot.set_stroke(BLACK, 1) plane.add(one_dot) pin = PushPin() pin.move_to(origin, DR) hand = Hand() update_hand = UpdateFromFunc( hand, lambda m: m.move_to(one_dot.get_center(), LEFT) ) circle = Circle( color=YELLOW, radius=get_norm(one_point - origin) ) self.add(circle) self.add(pin, one_dot) self.add_foreground_mobjects(hand) title = TextMobject( "Numbers on the unit circle", "$\\rightarrow$", "pure rotation." ) title.set_width(FRAME_WIDTH - 1) title.to_edge(UP, buff=MED_SMALL_BUFF) title.add_background_rectangle(buff=SMALL_BUFF) self.add_foreground_mobjects(title) self.background_plane.coordinate_labels.submobjects.pop(-1) n_angles = 12 angles = list(np.linspace(-PI, PI, n_angles + 2)[1:-1]) random.shuffle(angles) for angle in angles: plane.save_state() temp_plane = plane.copy() z = np.exp(complex(0, angle)) if angle is angles[0]: z_label = DecimalNumber( z, num_decimal_places=2, ) z_label.set_stroke(BLACK, width=11, background=True) z_label_rect = BackgroundRectangle(z_label) z_label_rect.set_fill(opacity=0) z_point = plane.number_to_point(z) z_arrow = Arrow(2.5 * z_point, z_point, buff=SMALL_BUFF) z_dot = Dot(z_point) z_label_start_center = z_label.get_center() z_label.next_to( z_arrow.get_start(), np.sign(z_arrow.get_start()[1]) * UP, ) z_label_end_center = z_label.get_center() z_group = VGroup(z_arrow, z_dot, z_label) z_group.set_color(GREEN) z_group.add_to_back(z_label_rect) z_arrow.set_stroke(BLACK, 1) z_dot.set_stroke(BLACK, 1) if angle is angles[0]: self.play( FadeInFromDown(z_label_rect), FadeInFromDown(z_label), GrowArrow(z_arrow), FadeInFromLarge(z_dot), ) else: alpha_tracker = ValueTracker(0) self.play( ReplacementTransform(old_z_dot, z_dot), ReplacementTransform(old_z_arrow, z_arrow), UpdateFromFunc( z_label_rect, lambda m: m.replace(z_label) ), ChangeDecimalToValue( z_label, z, position_update_func=lambda m: m.move_to( interpolate( z_label_start_center, z_label_end_center, alpha_tracker.get_value() ) ) ), alpha_tracker.set_value, 1, # hand.move_to, one_point, LEFT ) old_z_dot = z_dot old_z_arrow = z_arrow VGroup(old_z_arrow, old_z_dot) self.play( Rotate(plane, angle, run_time=2), update_hand, Animation(z_group), ) self.wait() self.add(temp_plane, z_group) self.play( FadeOut(plane), FadeOut(hand), FadeIn(temp_plane), ) plane.restore() self.remove(temp_plane) self.add(plane, *z_group) class IfYouNeedAWarmUp(TeacherStudentsScene): def construct(self): screen = self.screen screen.set_height(4) screen.to_corner(UL) self.add(screen) self.teacher_says( "If you need \\\\ a warm up", bubble_kwargs={"width": 3.5, "height": 3}, ) self.change_all_student_modes( "pondering", look_at_arg=screen, ) self.wait(3) self.play(RemovePiCreatureBubble(self.teacher)) self.wait(3) class LinusThinksAboutStretching(Scene): def construct(self): linus = Linus() top_line = NumberLine(color=GREY) top_line.to_edge(UP) top_line.add_numbers() linus.move_to(3 * LEFT + DOWN) self.add(linus, top_line) scalars = [3, 0.5, 2] for scalar in scalars: lower_line = NumberLine( x_min=-14, x_max=14, color=BLUE ) lower_line.next_to(top_line, DOWN, MED_LARGE_BUFF) lower_line.numbers = lower_line.get_number_mobjects() for number in lower_line.numbers: number.add_updater(lambda m: m.next_to( lower_line.number_to_point(m.get_value()), DOWN, MED_SMALL_BUFF, )) lower_line.save_state() lower_line.numbers.save_state() self.add(lower_line, *lower_line.numbers) words = TextMobject("Multiply by {}".format(scalar)) words.next_to(lower_line.numbers, DOWN) self.play( ApplyMethod( lower_line.stretch, scalar, 0, run_time=2 ), # LaggedStartMap(FadeIn, words, run_time=1), FadeInFromLarge(words, 1.0 / scalar), linus.look_at, top_line.number_to_point(scalar) ) self.play(Blink(linus)) self.play( FadeOut(lower_line), FadeOut(lower_line.numbers), FadeOut(words), FadeIn(lower_line.saved_state, remover=True), FadeIn(lower_line.numbers.saved_state, remover=True), linus.look_at, top_line.number_to_point(0) ) self.play(linus.change, "confused", DOWN + RIGHT) self.wait(2) self.play(Blink(linus)) self.wait(2) class LinusReactions(Scene): def construct(self): linus = Linus() for mode in "confused", "sad", "erm", "angry", "pleading": self.play(linus.change, mode, 2 * RIGHT) self.wait() self.play(Blink(linus)) self.wait() class OneDegreeOfFreedomForRotation(Scene): def construct(self): circle = CheckeredCircle(radius=2, stroke_width=10) r_line = Line(circle.get_center(), circle.get_right()) moving_r_line = r_line.copy() right_dot = Dot(color=WHITE) right_dot.move_to(circle.get_right()) circle.add(moving_r_line, right_dot) center_dot = Dot(color=WHITE) def get_angle(): return moving_r_line.get_angle() % TAU angle_label = Integer(0, unit="^\\circ") angle_label.scale(2) angle_label.add_updater( lambda m: m.set_value(get_angle() / DEGREES) ) angle_label.add_updater( lambda m: m.next_to(circle, UP, MED_LARGE_BUFF) ) def get_arc(): return Arc( angle=get_angle(), radius=0.5, color=LIGHT_GREY, ) arc = get_arc() arc.add_updater(lambda m: m.become(get_arc())) self.add(circle, center_dot, r_line, angle_label, arc) angles = IntroduceStereographicProjection.CONFIG.get( "example_angles" ) for angle in angles: self.play(Rotate(circle, angle, run_time=4)) self.wait() class StereographicProjectionTitle(Scene): def construct(self): title = TextMobject("Stereographic projection") final_title = title.copy() final_title.set_width(10) final_title.to_edge(UP) title.rotate(-90 * DEGREES) title.next_to(RIGHT, RIGHT, SMALL_BUFF) title.apply_complex_function(np.exp) title.rotate(90 * DEGREES) title.set_height(6) title.to_edge(UP) self.play(Write(title)) self.play(Transform(title, final_title, run_time=2)) self.wait() class IntroduceStereographicProjection(MovingCameraScene): CONFIG = { "n_sample_points": 16, "circle_config": { "n_pieces": 16, "radius": 2, "stroke_width": 7, }, "example_angles": [ 30 * DEGREES, 120 * DEGREES, 240 * DEGREES, 80 * DEGREES, -60 * DEGREES, 135 * DEGREES, ] } def construct(self): self.setup_plane() self.draw_lines() self.describe_individual_points() self.remind_that_most_points_are_not_projected() def setup_plane(self): self.plane = self.get_plane() self.circle = self.get_circle() self.circle_shadow = self.get_circle_shadow() self.add(self.plane) self.add(self.circle_shadow) self.add(self.circle) def draw_lines(self): plane = self.plane circle = self.circle circle.save_state() circle.generate_target() self.project_mobject(circle.target) circle_points = self.get_sample_circle_points() dots = VGroup(*[Dot(p) for p in circle_points]) dots.set_sheen(-0.2, DR) dots.set_stroke(DARK_GREY, 2, background=True) arrows = VGroup() for dot in dots: dot.scale(0.75) dot.generate_target() dot.target.move_to( self.project(dot.get_center()) ) arrow = Arrow( dot.get_center(), dot.target.get_center(), ) arrows.add(arrow) neg_one_point = plane.number_to_point(-1) neg_one_dot = Dot(neg_one_point) neg_one_dot.set_fill(YELLOW) lines = self.get_lines() special_index = self.n_sample_points // 2 + 1 line = lines[special_index] dot = dots[special_index] arrow = arrows[special_index] dot_target_outline = dot.target.copy() dot_target_outline.set_stroke(RED, 2) dot_target_outline.set_fill(opacity=0) dot_target_outline.scale(1.5) v_line = Line(UP, DOWN) v_line.set_height(FRAME_HEIGHT) v_line.set_stroke(RED, 5) self.play(LaggedStartMap(FadeInFromLarge, dots)) self.play(FadeInFromLarge(neg_one_dot)) self.add(lines, neg_one_dot, dots) self.play(LaggedStartMap(ShowCreation, lines)) self.wait() self.play( lines.set_stroke, {"width": 0.5}, line.set_stroke, {"width": 4}, ) self.play(ShowCreation(dot_target_outline)) self.play(ShowCreationThenDestruction(v_line)) self.play(MoveToTarget(dot)) self.wait() self.play( lines.set_stroke, {"width": 1}, FadeOut(dot_target_outline), MoveToTarget(circle), *map(MoveToTarget, dots), run_time=2, ) self.wait() self.lines = lines self.dots = dots def describe_individual_points(self): plane = self.plane one_point, zero_point, i_point, neg_i_point, neg_one_point = [ plane.number_to_point(n) for n in [1, 0, complex(0, 1), complex(0, -1), -1] ] i_pin = PushPin() i_pin.pin_to(i_point) neg_i_pin = PushPin() neg_i_pin.pin_to(neg_i_point) dot = Dot() dot.set_stroke(RED, 3) dot.set_fill(opacity=0) dot.scale(1.5) dot.move_to(one_point) arc1 = Arc(angle=TAU / 4, radius=2) arc2 = Arc( angle=85 * DEGREES, radius=2, start_angle=TAU / 4, ) arc3 = Arc( angle=-85 * DEGREES, radius=2, start_angle=-TAU / 4, ) VGroup(arc1, arc2, arc3).set_stroke(RED) frame = self.camera_frame frame_height_tracker = ValueTracker(frame.get_height()) frame_height_growth = frame_height_tracker.add_updater( lambda m, dt: m.set_value(m.get_value + 0.5 * dt) ) neg_one_tangent = VGroup( Line(ORIGIN, UP), Line(ORIGIN, DOWN), ) neg_one_tangent.set_height(25) neg_one_tangent.set_stroke(YELLOW, 5) neg_one_tangent.move_to(neg_one_point) self.play(ShowCreation(dot)) self.wait() self.play(dot.move_to, zero_point) self.wait() dot.move_to(i_point) self.play(ShowCreation(dot)) self.play(FadeInFrom(i_pin, UL)) self.wait() self.play( dot.move_to, neg_i_point, path_arc=-60 * DEGREES ) self.play(FadeInFrom(neg_i_pin, UL)) self.wait() self.play( dot.move_to, one_point, path_arc=-60 * DEGREES ) frame.add_updater( lambda m: m.set_height(frame_height_tracker.get_value()) ) triplets = [ (arc1, i_point, TAU / 4), (arc2, neg_one_point, TAU / 4), (arc3, neg_one_point, -TAU / 4), ] for arc, point, path_arc in triplets: self.play( ShowCreation(arc), dot.move_to, point, path_arc=path_arc, run_time=2 ) self.wait() self.play( ApplyFunction(self.project_mobject, arc, run_time=2) ) self.wait() self.play(FadeOut(arc)) self.wait() if arc is arc1: self.add(frame, frame_height_growth) elif arc is arc2: self.play(dot.move_to, neg_i_point) self.wait(2) self.play(*map(ShowCreation, neg_one_tangent)) self.wait() self.play(FadeOut(neg_one_tangent)) self.wait(2) frame.clear_updaters() self.play( frame.set_height, FRAME_HEIGHT, self.lines.set_stroke, {"width": 0.5}, FadeOut(self.dots), FadeOut(dot), run_time=2, ) def remind_that_most_points_are_not_projected(self): plane = self.plane circle = self.circle sample_values = [0, complex(1, 1), complex(-2, -1)] sample_points = [ plane.number_to_point(value) for value in sample_values ] sample_dots = VGroup(*[Dot(p) for p in sample_points]) sample_dots.set_fill(GREEN) self.play( FadeOut(self.lines), Restore(circle), ) for value, dot in zip(sample_values, sample_dots): cross = Cross(dot) cross.scale(2) label = Integer(value) if value is sample_values[1]: label.next_to(dot, UL, SMALL_BUFF) else: label.next_to(dot, UR, SMALL_BUFF) self.play( FadeInFromLarge(dot, 3), FadeInFromDown(label) ) self.play(ShowCreation(cross)) self.play(*map(FadeOut, [dot, cross, label])) self.wait() self.play( FadeIn(self.lines), MoveToTarget(circle, run_time=2), ) self.wait() # Helpers def get_plane(self): plane = ComplexPlane( unit_size=2, color=GREY, secondary_color=DARK_GREY, x_radius=FRAME_WIDTH, y_radius=FRAME_HEIGHT, stroke_width=2, ) plane.add_coordinates() return plane def get_circle(self): circle = CheckeredCircle( **self.circle_config ) circle.set_stroke(width=7) return circle def get_circle_shadow(self): circle_shadow = CheckeredCircle( **self.circle_config ) circle_shadow.set_stroke(opacity=0.65) return circle_shadow def get_sample_circle_points(self): plane = self.plane n = self.n_sample_points rotater = 1 if hasattr(self, "circle_shadow"): point = self.circle_shadow[0].points[0] rotater = complex(*point[:2]) rotater /= abs(rotater) numbers = [ rotater * np.exp(complex(0, TAU * x / n)) for x in range(-(n // 2) + 1, n // 2) ] return [ plane.number_to_point(number) for number in numbers ] def get_lines(self): plane = self.plane neg_one_point = plane.number_to_point(-1) circle_points = self.get_sample_circle_points() lines = VGroup(*[ Line(neg_one_point, point) for point in circle_points ]) for line in lines: length = line.get_length() line.scale(20 / length, about_point=neg_one_point) line.set_stroke(YELLOW, np.clip(length, 0, 1)) return lines def project(self, point): return stereo_project_point(point, axis=0, r=2) def project_mobject(self, mobject): return stereo_project(mobject, axis=0, r=2, outer_r=6) class IntroduceStereographicProjectionLinusView(IntroduceStereographicProjection): def construct(self): self.describe_individual_points() self.point_at_infinity() self.show_90_degree_rotation() self.talk_through_90_degree_rotation() self.show_four_rotations() self.show_example_angles() def describe_individual_points(self): plane = self.plane = self.get_plane() circle = self.circle = self.get_circle() linus = self.linus = self.get_linus() angles = np.arange(-135, 180, 45) * DEGREES sample_numbers = [ np.exp(complex(0, angle)) for angle in angles ] sample_points = [ plane.number_to_point(number) for number in sample_numbers ] projected_sample_points = [ self.project(point) for point in sample_points ] dots = VGroup(*[Dot() for x in range(8)]) dots.set_fill(WHITE) dots.set_stroke(BLACK, 1) def generate_dot_updater(circle_piece): return lambda d: d.move_to(circle_piece.points[0]) for dot, piece in zip(dots, circle[::(len(circle) // 8)]): dot.add_updater(generate_dot_updater(piece)) stot = "(\\sqrt{2} / 2)" labels_tex = [ "-{}-{}i".format(stot, stot), "-i", "{}-{}i".format(stot, stot), "1", "{}+{}i".format(stot, stot), "i", "-{}+{}i".format(stot, stot), ] labels = VGroup(*[TexMobject(tex) for tex in labels_tex]) vects = it.cycle([RIGHT, RIGHT]) arrows = VGroup() for label, point, vect in zip(labels, projected_sample_points, vects): arrow = Arrow(vect, ORIGIN) arrow.next_to(point, vect, 2 * SMALL_BUFF) arrows.add(arrow) label.set_stroke(width=0, background=True) if stot in label.get_tex_string(): label.set_height(0.5) else: label.set_height(0.5) label.set_stroke(WHITE, 2, background=True) label.next_to(arrow, vect, SMALL_BUFF) frame = self.camera_frame frame.set_height(12) self.add(linus) self.add(circle, *dots) self.play( ApplyFunction(self.project_mobject, circle), run_time=2 ) self.play(linus.change, "confused") self.wait() for i in [1, 0]: self.play( LaggedStartMap(GrowArrow, arrows[i::2]), LaggedStartMap(Write, labels[i::2]) ) self.play(Blink(linus)) self.dots = dots def point_at_infinity(self): circle = self.circle linus = self.linus label = TextMobject( "$-1$ is \\\\ at $\\pm \\infty$" ) label.scale(1.5) label.next_to(circle, LEFT, buff=1.25) arrows = VGroup(*[ Vector(3 * v + 0.0 * RIGHT).next_to(label, v, buff=MED_LARGE_BUFF) for v in [UP, DOWN] ]) arrows.set_color(YELLOW) self.play( Write(label), linus.change, "awe", label, *map(GrowArrow, arrows) ) self.neg_one_label = VGroup(label, arrows) def show_90_degree_rotation(self): angle_tracker = ValueTracker(0) circle = self.circle linus = self.linus hand = Hand() hand.flip() one_dot = self.dots[0] hand.add_updater( lambda h: h.move_to(one_dot.get_center(), RIGHT) ) def update_circle(circle): angle = angle_tracker.get_value() new_circle = self.get_circle() new_circle.rotate(angle) self.project_mobject(new_circle) circle.become(new_circle) circle.add_updater(update_circle) self.play( FadeIn(hand), one_dot.set_fill, RED, ) for angle in 90 * DEGREES, 0: self.play( ApplyMethod( angle_tracker.set_value, angle, run_time=3, ), linus.change, "confused", hand ) self.wait() self.play(Blink(linus)) self.hand = hand self.angle_tracker = angle_tracker def talk_through_90_degree_rotation(self): linus = self.linus dots = self.dots one_dot = dots[0] i_dot = dots[2] neg_i_dot = dots[-2] kwargs1 = { "path_arc": -90 * DEGREES, "buff": SMALL_BUFF, } kwargs2 = dict(kwargs1) kwargs2["path_arc"] = -40 * DEGREES arrows = VGroup( Arrow(one_dot, i_dot, **kwargs1), Arrow(i_dot, 6 * UP + LEFT, **kwargs2), Arrow(6 * DOWN + LEFT, neg_i_dot, **kwargs2), Arrow(neg_i_dot, one_dot, **kwargs1) ) arrows.set_stroke(WHITE, 3) one_to_i, i_to_neg_1, neg_one_to_neg_i, neg_i_to_one = arrows for arrow in arrows: self.play( ShowCreation(arrow), linus.look_at, arrow ) self.wait(2) self.arrows = arrows def show_four_rotations(self): angle_tracker = self.angle_tracker linus = self.linus hand = self.hand linus.add_updater(lambda l: l.look_at(hand)) linus.add_updater(lambda l: l.eyes.next_to(l.body, UP, 0)) for angle in np.arange(TAU / 4, 5 * TAU / 4, TAU / 4): self.play( ApplyMethod( angle_tracker.set_value, angle, run_time=3, ), ) self.wait() self.play(FadeOut(self.arrows)) def show_example_angles(self): angle_tracker = self.angle_tracker angle_tracker.set_value(0) for angle in self.example_angles: self.play( ApplyMethod( angle_tracker.set_value, angle, run_time=4, ), ) self.wait() # def get_linus(self): linus = Linus() linus.move_to(3 * RIGHT) linus.to_edge(DOWN) linus.look_at(ORIGIN) return linus class ShowRotationUnderStereographicProjection(IntroduceStereographicProjection): def construct(self): self.setup_plane() self.apply_projection() self.show_90_degree_rotation() self.talk_through_90_degree_rotation() self.show_four_rotations() self.show_example_angles() def apply_projection(self): plane = self.plane circle = self.circle neg_one_point = plane.number_to_point(-1) neg_one_dot = Dot(neg_one_point) neg_one_dot.set_fill(YELLOW) lines = always_redraw(self.get_lines) def generate_dot_updater(circle_piece): return lambda d: d.move_to(circle_piece.points[0]) for circ, color in [(self.circle_shadow, RED), (self.circle, WHITE)]: for piece in circ[::(len(circ) // 8)]: dot = Dot(color=color) dot.set_fill(opacity=circ.get_stroke_opacity()) dot.add_updater(generate_dot_updater(piece)) self.add(dot) self.add(lines, neg_one_dot) self.play(*map(ShowCreation, lines)) self.play( ApplyFunction(self.project_mobject, circle), lines.set_stroke, {"width": 0.5}, run_time=2 ) self.play( self.camera_frame.set_height, 12, run_time=2 ) self.wait() def show_90_degree_rotation(self): circle = self.circle circle_shadow = self.circle_shadow def get_rotated_one_point(): return circle_shadow[0].points[0] def get_angle(): return angle_of_vector(get_rotated_one_point()) self.get_angle = get_angle one_dot = Dot(color=RED) one_dot.add_updater( lambda m: m.move_to(get_rotated_one_point()) ) hand = Hand() hand.move_to(one_dot.get_center(), LEFT) def update_circle(circle): new_circle = self.get_circle() new_circle.rotate(get_angle()) self.project_mobject(new_circle) circle.become(new_circle) circle.add_updater(update_circle) self.add(one_dot, hand) hand.add_updater( lambda h: h.move_to(one_dot.get_center(), LEFT) ) self.play( FadeInFrom(hand, RIGHT), FadeInFromLarge(one_dot, 3), ) for angle in 90 * DEGREES, -90 * DEGREES: self.play( Rotate(circle_shadow, angle, run_time=3), ) self.wait(2) def talk_through_90_degree_rotation(self): plane = self.plane points = [ plane.number_to_point(z) for z in [1, complex(0, 1), -1, complex(0, -1)] ] arrows = VGroup() for p1, p2 in adjacent_pairs(points): arrow = Arrow( p1, p2, path_arc=180 * DEGREES, ) arrow.set_stroke(LIGHT_GREY, width=3) arrow.tip.set_fill(LIGHT_GREY) arrows.add(arrow) for arrow in arrows: self.play(ShowCreation(arrow)) self.wait(2) self.arrows = arrows def show_four_rotations(self): circle_shadow = self.circle_shadow for x in range(4): self.play( Rotate(circle_shadow, TAU / 4, run_time=3) ) self.wait() self.play(FadeOut(self.arrows)) def show_example_angles(self): circle_shadow = self.circle_shadow angle_label = Integer(0, unit="^\\circ") angle_label.scale(1.5) angle_label.next_to( circle_shadow.get_top(), UR, ) self.play(FadeInFromDown(angle_label)) self.add(angle_label) for angle in self.example_angles: d_angle = angle - self.get_angle() self.play( Rotate(circle_shadow, d_angle), ChangingDecimal( angle_label, lambda a: (self.get_angle() % TAU) / DEGREES ), run_time=4 ) self.wait() class WriteITimesW(Scene): def construct(self): mob = TexMobject("i", "\\cdot", "w") mob[0].set_color(GREEN) mob.scale(3) self.play(Write(mob)) self.wait() class IntroduceFelix(PiCreatureScene, SpecialThreeDScene): def setup(self): PiCreatureScene.setup(self) SpecialThreeDScene.setup(self) def construct(self): self.introduce_felix() self.add_plane() self.show_in_three_d() def introduce_felix(self): felix = self.felix = self.pi_creature arrow = Vector(DL, color=WHITE) arrow.next_to(felix, UR) label = TextMobject("Felix the Flatlander") label.next_to(arrow.get_start(), UP) self.add(felix) self.play( felix.change, "wave_1", label, Write(label), GrowArrow(arrow), ) self.play(Blink(felix)) self.play(felix.change, "thinking", label) self.to_fade = VGroup(label, arrow) def add_plane(self): plane = NumberPlane(y_radius=10) axes = self.get_axes() to_fade = self.to_fade felix = self.felix self.add(axes, plane, felix) self.play( ShowCreation(axes), ShowCreation(plane), FadeOut(to_fade), ) self.wait() self.plane = plane self.axes = axes def show_in_three_d(self): felix = self.pi_creature plane = self.plane axes = self.axes # back_plane = Rectangle().replace(plane, stretch=True) # back_plane.shade_in_3d = True # back_plane.set_fill(LIGHT_GREY, opacity=0.5) # back_plane.set_sheen(1, UL) # back_plane.shift(SMALL_BUFF * IN) # back_plane.set_stroke(width=0) # back_plane = ParametricSurface( # lambda u, v: u * RIGHT + v * UP # ) # back_plane.replace(plane, stretch=True) # back_plane.set_stroke(width=0) # back_plane.set_fill(LIGHT_GREY, opacity=0.5) sphere = self.get_sphere() # sphere.set_fill(BLUE_E, 0.5) self.move_camera( phi=70 * DEGREES, theta=-110 * DEGREES, added_anims=[FadeOut(plane)], run_time=2 ) self.begin_ambient_camera_rotation() self.add(axes, sphere) self.play( Write(sphere), felix.change, "confused" ) self.wait() axis_angle_pairs = [ (RIGHT, 90 * DEGREES), (OUT, 45 * DEGREES), (UR + OUT, 120 * DEGREES), (RIGHT, 90 * DEGREES), ] for axis, angle in axis_angle_pairs: self.play(Rotate( sphere, angle=angle, axis=axis, run_time=2, )) self.wait(2) # def create_pi_creature(self): return Felix().move_to(4 * LEFT + 2 * DOWN) class IntroduceThreeDNumbers(SpecialThreeDScene): CONFIG = { "camera_config": { "exponential_projection": False, } } def construct(self): self.add_third_axis() self.reorient_axes() self.show_example_number() def add_third_axis(self): plane = ComplexPlane( y_radius=FRAME_WIDTH / 4, unit_size=2, secondary_line_ratio=1, ) plane.add_coordinates() title = TextMobject("Complex Plane") title.scale(1.8) title.add_background_rectangle() title.to_corner(UL, buff=MED_SMALL_BUFF) real_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) imag_line = Line(DOWN, UP).set_height(FRAME_HEIGHT) real_line.set_color(YELLOW) imag_line.set_color(RED) for label in plane.coordinate_labels: label.remove(label.background_rectangle) label.shift(SMALL_BUFF * IN) self.add_fixed_orientation_mobjects(label) reals = plane.coordinate_labels[:7] imags = plane.coordinate_labels[7:] self.add(plane, title) for line, group in (real_line, reals), (imag_line, imags): line.set_stroke(width=5) self.play( ShowCreationThenDestruction(line), LaggedStartMap( Indicate, group, rate_func=there_and_back, color=line.get_color(), ), run_time=2, ) self.plane = plane self.title = title def reorient_axes(self): z_axis = NumberLine(unit_size=2) z_axis.rotate(90 * DEGREES, axis=DOWN) z_axis.rotate(90 * DEGREES, axis=OUT) z_axis.set_color(WHITE) z_axis_top = Line( z_axis.number_to_point(0), z_axis.get_end(), ) z_axis_top.match_style(z_axis) z_unit_line = Line( z_axis.number_to_point(0), z_axis.number_to_point(1), color=RED, stroke_width=5 ) j_labels = VGroup( TexMobject("-2j"), TexMobject("-j"), TexMobject("j"), TexMobject("2j"), ) for label, num in zip(j_labels, [-2, -1, 1, 2]): label.next_to(z_axis.number_to_point(num), RIGHT, MED_SMALL_BUFF) self.add_fixed_orientation_mobjects(label) plane = self.plane colored_ = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) y_line = Line(DOWN, UP).set_height(FRAME_WIDTH) z_line = Line(IN, OUT).set_depth(FRAME_WIDTH) colored_.set_stroke(GREEN, 5) y_line.set_stroke(RED, 5) z_line.set_stroke(YELLOW, 5) colored_coord_lines = VGroup(colored_, y_line, z_line) coord_lines = VGroup( plane.axes[0], plane.axes[1], z_axis, ) for i1, i2 in [(0, 2), (1, 0), (2, 1)]: coord_lines[i1].target = coord_lines[i2].copy() new_title = TextMobject("Imaginary plane") new_title.replace(self.title) new_title.move_to(self.title) self.add(z_axis, plane, z_axis_top) self.move_camera( phi=70 * DEGREES, theta=-80 * DEGREES, added_anims=[ plane.set_stroke, {"opacity": 0.5}, ], run_time=2, ) self.begin_ambient_camera_rotation(rate=0.02) self.wait() self.play(FadeInFrom(j_labels, IN)) z_axis.add(j_labels) self.play( ShowCreationThenDestruction(z_unit_line), run_time=2 ) self.wait(4) group = VGroup(*it.chain(plane.coordinate_labels, j_labels)) for label in group: label.generate_target() axis = np.ones(3) label.target.rotate_about_origin(-120 * DEGREES, axis=axis) label.target.rotate(120 * DEGREES, axis=axis) for y, label in zip([-2, -1, 1, 2], j_labels): label.target.scale(0.65) label.target.next_to( 2 * y * UP, RIGHT, 2 * SMALL_BUFF ) self.remove(z_axis_top) self.play( LaggedStartMap(MoveToTarget, group, lag_ratio=0.8), LaggedStartMap(MoveToTarget, coord_lines, lag_ratio=0.8), FadeOut(self.title), FadeIn(new_title), run_time=3 ) self.add(z_axis_top) self.wait(3) for line, wait in zip(colored_coord_lines, [False, True, True]): self.play( ShowCreationThenDestruction(line), run_time=2 ) if wait: self.wait() def show_example_number(self): x, y, z = coords = 2 * np.array([1.5, -1, 1.25]) dot = Sphere(radius=0.05) dot.set_fill(LIGHT_GREY) dot.move_to(coords) point_line = Line(ORIGIN, coords) point_line.set_stroke(WHITE, 1) z_line = Line(ORIGIN, z * OUT) x_line = Line(z_line.get_end(), z_line.get_end() + x * RIGHT) y_line = Line(x_line.get_end(), x_line.get_end() + y * UP) x_line.set_stroke(GREEN, 5) y_line.set_stroke(RED, 5) z_line.set_stroke(YELLOW, 5) lines = VGroup(z_line, x_line, y_line) number_label = TexMobject( str(z / 2), "+", str(x / 2), "i", "+", str(y / 2), "j", tex_to_color_map={ str(z / 2): YELLOW, str(x / 2): GREEN, str(y / 2): RED, } ) number_label.next_to(ORIGIN, RIGHT, LARGE_BUFF) number_label.to_edge(UP) self.add_fixed_in_frame_mobjects(number_label) self.play( ShowCreation(point_line), FadeInFrom(dot, -coords), FadeInFromDown(number_label) ) self.wait() for num, line in zip([z, x, y], lines): tex = number_label.get_part_by_tex(str(num / 2)) rect = SurroundingRectangle(tex) rect.set_color(WHITE) self.add_fixed_in_frame_mobjects(rect) self.play( ShowCreation(line), ShowCreationThenDestruction(rect), run_time=2 ) self.remove_fixed_in_frame_mobjects(rect) self.wait() self.wait(15) class MentionImpossibilityOf3dNumbers(TeacherStudentsScene): def construct(self): equations = VGroup( TexMobject("ij = ?"), TexMobject("ji = ?"), ) equations.arrange(RIGHT, buff=LARGE_BUFF) equations.scale(1.5) equations.to_edge(UP) self.add(equations) why = TextMobject("Why not?") why.next_to(self.students[1], UP) self.teacher_says( "Such 3d numbers \\\\ have no good \\\\ multiplication rule", bubble_kwargs={"width": 4, "height": 3}, ) self.change_all_student_modes("confused") self.wait(2) self.play( self.students[1].change, "maybe", FadeInFromLarge(why), ) self.wait(4) class SphereExamplePointsDecimal(Scene): CONFIG = { "point_rotation_angle_axis_pairs": [ (45 * DEGREES, DOWN), (120 * DEGREES, OUT), (35 * DEGREES, rotate_vector(RIGHT, 30 * DEGREES)), (90 * DEGREES, IN), ] } def construct(self): decimals = VGroup(*[ DecimalNumber( 0, num_decimal_places=3, color=color, include_sign=True, edge_to_fix=RIGHT, ) for color in [YELLOW, GREEN, RED] ]) number_label = VGroup( decimals[0], TexMobject("+"), decimals[1], TexMobject("i"), TexMobject("+"), decimals[2], TexMobject("j"), ) number_label.arrange(RIGHT, buff=SMALL_BUFF) number_label.to_corner(UL) point = VectorizedPoint(OUT) def generate_decimal_updater(decimal, index): shifted_i = (index - 1) % 3 decimal.add_updater(lambda d: d.set_value( point.get_location()[shifted_i] )) return decimal for i, decimal in enumerate(decimals): self.add(generate_decimal_updater(decimal, i)) decimal_braces = VGroup() for decimal, char in zip(decimals, "wxy"): brace = Brace(decimal, DOWN, buff=SMALL_BUFF) label = brace.get_tex(char, buff=SMALL_BUFF) label.match_color(decimal) brace.add(label) decimal_braces.add(brace) equation = TexMobject( "w^2 + x^2 + y^2 = 1", tex_to_color_map={ "w": YELLOW, "x": GREEN, "y": RED, } ) equation.next_to(decimal_braces, DOWN, MED_LARGE_BUFF) self.add(number_label) self.add(decimal_braces) self.add(equation) pairs = self.point_rotation_angle_axis_pairs for angle, axis in pairs: self.play( Rotate(point, angle, axis=axis, about_point=ORIGIN), run_time=2 ) self.wait() class TwoDStereographicProjection(IntroduceFelix): CONFIG = { "camera_config": { "exponential_projection": False, }, "sphere_sample_point_u_range": np.arange( 0, PI, PI / 16, ), "sphere_sample_point_v_range": np.arange( 0, TAU, TAU / 16, ), "n_sample_rotation_cycles": 2, "lift_labels": True, } def construct(self): self.add_parts() self.talk_through_sphere() self.draw_projection_lines() self.show_point_at_infinity() self.show_a_few_rotations() def add_parts(self, run_time=1): felix = self.felix = self.pi_creature felix.shift(1.5 * DL) axes = self.axes = self.get_axes() sphere = self.sphere = self.get_sphere() c2p = axes.coords_to_point labels = self.labels = VGroup( TexMobject("i").next_to(c2p(1, 0, 0), DR, SMALL_BUFF), TexMobject("-i").next_to(c2p(-1, 0, 0), DL, SMALL_BUFF), TexMobject("j").next_to(c2p(0, 1, 0), UL, SMALL_BUFF), TexMobject("-j").next_to(c2p(0, -1, 0), DL, SMALL_BUFF), TexMobject("1").rotate( 90 * DEGREES, RIGHT, ).next_to(c2p(0, 0, 1), RIGHT + OUT, SMALL_BUFF), TexMobject("-1").rotate( 90 * DEGREES, RIGHT, ).next_to(c2p(0, 0, -1), RIGHT + IN, SMALL_BUFF), ) if self.lift_labels: for sm in labels[:4].family_members_with_points(): sm.add(VectorizedPoint( 0.25 * DOWN + 0.25 * OUT )) labels.set_stroke(width=0, background=True) for submob in labels.get_family(): submob.shade_in_3d = True self.add(felix, axes, sphere, labels) self.move_camera( **self.get_default_camera_position(), run_time=run_time ) self.begin_ambient_camera_rotation(rate=0.01) self.play( felix.change, "pondering", sphere, run_time=run_time, ) def talk_through_sphere(self): point = VectorizedPoint(OUT) arrow = Vector(IN, shade_in_3d=True) arrow.set_fill(PINK) arrow.set_stroke(BLACK, 1) def get_dot(): dot = Sphere(radius=0.05, u_max=PI / 2) dot.set_fill(PINK) dot.set_stroke(width=0) dot.move_to(2.05 * OUT) dot.apply_matrix( z_to_vector(normalize(point.get_location())), about_point=ORIGIN ) return dot dot = get_dot() dot.add_updater( lambda d: d.become(get_dot()) ) def update_arrow(arrow): target_point = 2.1 * point.get_location() rot_matrix = np.dot( z_to_vector(normalize(target_point)), np.linalg.inv( z_to_vector(normalize(-arrow.get_vector())) ) ) arrow.apply_matrix(rot_matrix) arrow.shift(target_point - arrow.get_end()) return arrow arrow.add_updater(update_arrow) self.add(self.sphere, dot, arrow) pairs = SphereExamplePointsDecimal.CONFIG.get( "point_rotation_angle_axis_pairs" ) for angle, axis in pairs: self.play( Rotate(point, angle, axis=axis, about_point=ORIGIN), run_time=2 ) self.wait() self.play(FadeOut(dot), FadeOut(arrow)) def draw_projection_lines(self): sphere = self.sphere axes = self.axes radius = sphere.get_width() / 2 neg_one_point = axes.coords_to_point(0, 0, -1) neg_one_dot = Dot( neg_one_point, color=YELLOW, shade_in_3d=True ) xy_plane = StereoProjectedSphere( u_max=15 * PI / 16, **self.sphere_config ) xy_plane.set_fill(WHITE, 0.25) xy_plane.set_stroke(width=0) point_mob = VectorizedPoint(2 * OUT) point_mob.add_updater( lambda m: m.move_to(radius * normalize(m.get_center())) ) point_mob.move_to([1, -1, 1]) point_mob.update(0) def get_projection_line(sphere_point): to_sphere = Line(neg_one_point, sphere_point) to_plane = Line( sphere_point, self.project_point(sphere_point) ) line = VGroup(to_sphere, to_plane) line.set_stroke(YELLOW, 3) for submob in line: submob.shade_in_3d = True return line def get_sphere_dot(sphere_point): dot = Dot() dot.set_shade_in_3d(True) dot.set_fill(PINK) dot.shift(OUT) dot.apply_matrix( z_to_vector(sphere_point), about_point=ORIGIN, ) dot.move_to(1.01 * sphere_point) dot.add(VectorizedPoint(5 * sphere_point)) return dot def get_projection_dot(sphere_point): projection = self.project_point(sphere_point) dot = Dot(projection, shade_in_3d=True) dot.add(VectorizedPoint(dot.get_center() + 0.1 * OUT)) dot.set_fill(WHITE) return dot point = point_mob.get_location() dot = get_sphere_dot(point) line = get_projection_line(point) projection_dot = get_projection_dot(point) sample_points = [ radius * sphere.func(u, v) for u in self.sphere_sample_point_u_range for v in self.sphere_sample_point_v_range ] lines = VGroup(*[get_projection_line(p) for p in sample_points]) lines.set_stroke(width=1) north_lines = lines[:len(lines) // 2] south_lines = lines[len(lines) // 2:] self.add(xy_plane, sphere) self.play(Write(xy_plane)) self.wait(2) self.play(sphere.set_fill, BLUE_E, 0.5) self.play(FadeInFromLarge(dot)) self.play( FadeIn(neg_one_dot), ShowCreation(line), ) self.wait(2) self.play(ReplacementTransform( dot.copy(), projection_dot )) def get_point(): return 2 * normalize(point_mob.get_location()) dot.add_updater( lambda d: d.become(get_sphere_dot(get_point())) ) line.add_updater( lambda l: l.become(get_projection_line(get_point())) ) projection_dot.add_updater( lambda d: d.become(get_projection_dot(get_point())) ) self.play( point_mob.move_to, radius * normalize(np.array([1, -1, -1])), run_time=3 ) self.move_camera( theta=-150 * DEGREES, run_time=3 ) self.add(axes, sphere, xy_plane, dot, line) for point in np.array([-2, 1, -0.5]), np.array([-0.01, -0.01, 1]): self.play( point_mob.move_to, radius * normalize(point), run_time=3 ) self.wait(2) # Project norther hemisphere north_hemisphere = self.get_sphere() n = len(north_hemisphere) north_hemisphere.remove(*north_hemisphere[n // 2:]) north_hemisphere.generate_target() self.project_mobject(north_hemisphere.target) north_hemisphere.set_fill(opacity=0.8) self.play( LaggedStartMap(ShowCreation, north_lines), FadeIn(north_hemisphere) ) self.play( MoveToTarget(north_hemisphere), run_time=3, rate_func=lambda t: smooth(0.99 * t) ) self.play(FadeOut(north_lines)) self.wait(2) # Unit circle circle = Sphere( radius=2.01, u_min=PI / 2 - 0.01, u_max=PI / 2 + 0.01, resolution=(1, 24), ) for submob in circle: submob.add(VectorizedPoint(1.5 * submob.get_center())) circle.set_fill(YELLOW) circle_path = Circle(radius=2) circle_path.rotate(-90 * DEGREES) self.play(FadeInFromLarge(circle)) self.play(point_mob.move_to, circle_path.points[0]) self.play(MoveAlongPath(point_mob, circle_path, run_time=6)) self.move_camera( phi=0, theta=-90 * DEGREES, rate_func=there_and_back_with_pause, run_time=6, ) self.play(point_mob.move_to, OUT) self.wait() # Southern hemisphere south_hemisphere = self.get_sphere() n = len(south_hemisphere) south_hemisphere.remove(*south_hemisphere[:n // 2]) south_hemisphere.remove( *south_hemisphere[-sphere.resolution[1]:] ) south_hemisphere.generate_target() self.project_mobject(south_hemisphere.target) south_hemisphere.set_fill(opacity=0.8) south_hemisphere.target[-sphere.resolution[1] // 2:].set_fill( opacity=0 ) self.play( LaggedStartMap(ShowCreation, south_lines), FadeIn(south_hemisphere) ) self.play( MoveToTarget(south_hemisphere), FadeOut(south_lines), FadeOut(xy_plane), run_time=3, rate_func=lambda t: smooth(0.99 * t) ) self.wait(3) self.projected_sphere = VGroup( north_hemisphere, south_hemisphere, ) self.equator = circle self.point_mob = point_mob def show_point_at_infinity(self): points = list(compass_directions( 12, start_vect=rotate_vector(RIGHT, 3.25 * DEGREES) )) points.pop(7) points.pop(2) arrows = VGroup(*[ Arrow(6 * p, 11 * p) for p in points ]) arrows.set_fill(RED) arrows.set_stroke(RED, 5) neg_ones = VGroup(*[ TexMobject("-1").next_to(arrow.get_start(), -p) for p, arrow in zip(points, arrows) ]) neg_ones.set_stroke(width=0, background=True) sphere_arcs = VGroup() for angle in np.arange(0, TAU, TAU / 12): arc = Arc(PI, radius=2) arc.set_stroke(RED) arc.rotate(PI / 2, axis=DOWN, about_point=ORIGIN) arc.rotate(angle, axis=OUT, about_point=ORIGIN) sphere_arcs.add(arc) sphere_arcs.set_stroke(RED) self.play( LaggedStartMap(GrowArrow, arrows), LaggedStartMap(Write, neg_ones) ) self.wait(3) self.play( FadeOut(self.projected_sphere), FadeOut(arrows), FadeOut(neg_ones), ) for x in range(2): self.play( ShowCreationThenDestruction( sphere_arcs, lag_ratio=0, run_time=3, ) ) def show_a_few_rotations(self): sphere = self.sphere felix = self.felix point_mob = self.point_mob point_mob.add_updater( lambda m: m.move_to(sphere.get_all_points()[0]) ) coord_point_mobs = VGroup( VectorizedPoint(RIGHT), VectorizedPoint(UP), VectorizedPoint(OUT), ) for pm in coord_point_mobs: pm.shade_in_3d = True def get_rot_matrix(): return np.array([ pm.get_location() for pm in coord_point_mobs ]).T def get_projected_sphere(): result = StereoProjectedSphere( get_rot_matrix(), max_r=10, **self.sphere_config, ) result.set_fill(opacity=0.2) result.fade_far_out_submobjects(max_r=32) for submob in result: if submob.get_center()[1] < -11: submob.fade(1) return result projected_sphere = get_projected_sphere() projected_sphere.add_updater( lambda m: m.become(get_projected_sphere()) ) def get_projected_equator(): equator = CheckeredCircle( n_pieces=24, radius=2, ) for submob in equator.get_family(): submob.shade_in_3d = True equator.set_stroke(YELLOW, 5) equator.apply_matrix(get_rot_matrix()) self.project_mobject(equator) return equator projected_equator = get_projected_equator() projected_equator.add_updater( lambda m: m.become(get_projected_equator()) ) self.add(sphere, projected_sphere) self.move_camera(phi=60 * DEGREES) self.play( sphere.set_fill_by_checkerboard, BLUE_E, BLUE_D, {"opacity": 0.8}, FadeIn(projected_sphere) ) sphere.add(coord_point_mobs) sphere.add(self.equator) self.add(projected_equator) pairs = self.get_sample_rotation_angle_axis_pairs() for x in range(self.n_sample_rotation_cycles): for angle, axis in pairs: self.play( Rotate( sphere, angle=angle, axis=axis, about_point=ORIGIN, run_time=3, ), felix.change, "confused", ) self.wait() self.projected_sphere = projected_sphere # def project_mobject(self, mobject): return stereo_project(mobject, axis=2, r=2, outer_r=20) def project_point(self, point): return stereo_project_point(point, axis=2, r=2) def get_sample_rotation_angle_axis_pairs(self): return SphereExamplePointsDecimal.CONFIG.get( "point_rotation_angle_axis_pairs" ) class FelixViewOfProjection(TwoDStereographicProjection): CONFIG = {} def construct(self): self.add_axes() self.show_a_few_rotations() def add_axes(self): axes = Axes( number_line_config={ "unit_size": 2, "color": WHITE, } ) labels = VGroup( TexMobject("i"), TexMobject("-i"), TexMobject("j"), TexMobject("-j"), TexMobject("1"), ) coords = [(1, 0), (-1, 0), (0, 1), (0, -1), (0, 0)] vects = [DOWN, DOWN, RIGHT, RIGHT, 0.25 * DR] for label, coords, vect in zip(labels, coords, vects): point = axes.coords_to_point(*coords) label.next_to(point, vect, buff=MED_SMALL_BUFF) self.add(axes, labels) self.pi_creature.change("confused") def show_a_few_rotations(self): felix = self.pi_creature coord_point_mobs = VGroup([ VectorizedPoint(point) for point in [RIGHT, UP, OUT] ]) def get_rot_matrix(): return np.array([ pm.get_location() for pm in coord_point_mobs ]).T def get_projected_sphere(): return StereoProjectedSphere( get_rot_matrix(), **self.sphere_config, ) def get_projected_equator(): equator = Circle(radius=2, num_anchors=24) equator.set_stroke(YELLOW, 5) equator.apply_matrix(get_rot_matrix()) self.project_mobject(equator) return equator projected_sphere = get_projected_sphere() projected_sphere.add_updater( lambda m: m.become(get_projected_sphere()) ) equator = get_projected_equator() equator.add_updater( lambda m: m.become(get_projected_equator()) ) dot = Dot(color=PINK) dot.add_updater( lambda d: d.move_to( self.project_point( np.dot(2 * OUT, get_rot_matrix().T) ) ) ) hand = Hand() hand.add_updater( lambda h: h.move_to(dot.get_center(), LEFT) ) felix.add_updater(lambda f: f.look_at(dot)) self.add(projected_sphere) self.add(equator) self.add(dot) self.add(hand) pairs = self.get_sample_rotation_angle_axis_pairs() for x in range(self.n_sample_rotation_cycles): for angle, axis in pairs: self.play( Rotate( coord_point_mobs, angle=angle, axis=axis, about_point=ORIGIN, run_time=3, ), ) self.wait() class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): CONFIG = { "flat_view": False, } def construct(self): self.add_parts(run_time=1) self.begin_ambient_camera_rotation(rate=0.03) self.edit_parts() self.show_1i_circle() self.show_1j_circle() self.show_random_circle() self.show_rotations() def edit_parts(self): sphere = self.sphere axes = self.axes axes.set_stroke(width=1) xy_plane = StereoProjectedSphere(u_max=15 * PI / 16) xy_plane.set_fill(WHITE, 0.2) xy_plane.set_stroke(width=0, opacity=0) self.add(xy_plane, sphere) self.play( FadeIn(xy_plane), sphere.set_fill, BLUE_E, {"opacity": 0.2}, sphere.set_stroke, {"width": 0.1, "opacity": 0.5} ) def show_1i_circle(self): axes = self.axes circle = self.get_circle(GREEN_E, GREEN) circle.rotate(TAU / 4, RIGHT) circle.rotate(TAU / 4, DOWN) projected = self.get_projected_circle(circle) labels = VGroup(*map(TexMobject, ["0", "2i", "3i"])) labels.set_shade_in_3d(True) if self.flat_view: labels.fade(1) for label, x in zip(labels, [0, 2, 3]): label.next_to( axes.coords_to_point(x, 0, 0), DR, SMALL_BUFF ) self.play(ShowCreation(circle, run_time=3)) self.wait() self.play(ReplacementTransform( circle.copy(), projected, run_time=3 )) # self.axes.x_axis.pieces.set_stroke(width=0) self.wait(7) self.move_camera( phi=60 * DEGREES, ) self.play( LaggedStartMap( FadeInFrom, labels, lambda m: (m, UP) ) ) self.wait(2) self.play(FadeOut(labels)) self.one_i_circle = circle self.projected_one_i_circle = projected def show_1j_circle(self): circle = self.get_circle(RED_E, RED) circle.rotate(TAU / 4, DOWN) projected = self.get_projected_circle(circle) self.move_camera(theta=-170 * DEGREES) self.play(ShowCreation(circle, run_time=3)) self.wait() self.play(ReplacementTransform( circle.copy(), projected, run_time=3 )) # self.axes.y_axis.pieces.set_stroke(width=0) self.wait(3) self.one_j_circle = circle self.projected_one_j_circle = projected def show_random_circle(self): sphere = self.sphere circle = self.get_circle(BLUE_E, BLUE) circle.set_width(2 * sphere.radius * np.sin(30 * DEGREES)) circle.shift(sphere.radius * np.cos(30 * DEGREES) * OUT) circle.rotate(150 * DEGREES, UP, about_point=ORIGIN) projected = self.get_projected_circle(circle) self.play(ShowCreation(circle, run_time=2)) self.wait() self.play(ReplacementTransform( circle.copy(), projected, run_time=2 )) self.wait(3) self.play( FadeOut(circle), FadeOut(projected), ) def show_rotations(self): sphere = self.sphere c1i = self.one_i_circle pc1i = self.projected_one_i_circle c1j = self.one_j_circle pc1j = self.projected_one_j_circle cij = self.get_circle(YELLOW_E, YELLOW) pcij = self.get_projected_circle(cij) circles = VGroup(c1i, c1j, cij) x_axis = self.axes.x_axis y_axis = self.axes.y_axis arrow = Arrow( 2 * RIGHT, 2 * UP, buff=SMALL_BUFF, path_arc=PI, ) arrow.set_stroke(LIGHT_GREY, 3) arrow.tip.set_fill(LIGHT_GREY) arrows = VGroup(arrow, *[ arrow.copy().rotate(angle, about_point=ORIGIN) for angle in np.arange(TAU / 4, TAU, TAU / 4) ]) arrows.rotate(TAU / 4, RIGHT, about_point=ORIGIN) arrows.rotate(TAU / 2, OUT, about_point=ORIGIN) arrows.rotate(TAU / 4, UP, about_point=ORIGIN) arrows.space_out_submobjects(1.2) self.play(FadeInFromLarge(cij)) sphere.add(circles) pc1i.add_updater( lambda c: c.become(self.get_projected_circle(c1i)) ) pc1j.add_updater( lambda c: c.become(self.get_projected_circle(c1j)) ) pcij.add_updater( lambda c: c.become(self.get_projected_circle(cij)) ) self.add(pcij) # About j-axis self.play(ShowCreation(arrows, run_time=3, rate_func=linear)) self.wait(3) for x in range(2): y_axis.pieces.set_stroke(width=1) self.play( Rotate(sphere, 90 * DEGREES, axis=UP), run_time=4, ) y_axis.pieces.set_stroke(width=0) self.wait(2) # About i axis self.move_camera(theta=-45 * DEGREES) self.play(Rotate(arrows, TAU / 4, axis=OUT)) self.wait(2) for x in range(2): x_axis.pieces.set_stroke(width=1) self.play( Rotate(sphere, -90 * DEGREES, axis=RIGHT), run_time=4, ) x_axis.pieces.set_stroke(width=0) self.wait(2) self.wait(2) # About real axis self.move_camera( theta=-135 * DEGREES, added_anims=[FadeOut(arrows)] ) self.ambient_camera_rotation.rate = 0.01 for x in range(2): x_axis.pieces.set_stroke(width=1) y_axis.pieces.set_stroke(width=1) self.play( Rotate(sphere, 90 * DEGREES, axis=OUT), run_time=4, ) # x_axis.pieces.set_stroke(width=0) # y_axis.pieces.set_stroke(width=0) self.wait(2) # def get_circle(self, *colors): sphere = self.sphere circle = CheckeredCircle(colors=colors, n_pieces=48) circle.set_shade_in_3d(True) circle.match_width(sphere) if self.flat_view: circle[::2].fade(1) return circle def get_projected_circle(self, circle): result = circle.deepcopy() self.project_mobject(result) result[::2].fade(1) for sm in result: if sm.get_width() > FRAME_WIDTH: sm.fade(1) if sm.get_height() > FRAME_HEIGHT: sm.fade(1) return result class ReferernceSpheresFelixView(ShowRotationsJustWithReferenceCircles): CONFIG = { "flat_view": True, "lift_labels": False, } def add_parts(self, **kwargs): ShowRotationsJustWithReferenceCircles.add_parts(self, **kwargs) one = TexMobject("1") one.next_to(ORIGIN, DR, SMALL_BUFF) self.add(one) def get_default_camera_position(self): return {} def move_camera(self, **kwargs): kwargs["phi"] = 0 kwargs["theta"] = -90 * DEGREES ShowRotationsJustWithReferenceCircles.move_camera(self, **kwargs) def begin_ambient_camera_rotation(self, rate): self.ambient_camera_rotation = VectorizedPoint() def capture_mobjects_in_camera(self, mobjects, **kwargs): mobs_on_xy = [ sm for sm in self.camera.extract_mobject_family_members( mobjects, only_those_with_points=True ) if abs(sm.get_center()[2]) < 0.001 ] return Scene.capture_mobjects_in_camera(self, mobs_on_xy, **kwargs) class IntroduceQuaternions(Scene): def construct(self): self.compare_three_number_systems() self.mention_four_perpendicular_axes() self.bring_back_complex() self.show_components_of_quaternion() def compare_three_number_systems(self): numbers = self.get_example_numbers() labels = VGroup( TextMobject("Complex number"), TextMobject("Not-actually-a-number-system 3d number"), TextMobject("Quaternion"), ) for number, label in zip(numbers, labels): label.next_to(number, UP, aligned_edge=LEFT) self.play( FadeInFromDown(number), Write(label), ) self.play(ShowCreationThenFadeAround( number[2:], surrounding_rectangle_config={"color": BLUE} )) self.wait() shift_size = FRAME_HEIGHT / 2 - labels[2].get_top()[1] - MED_LARGE_BUFF self.play( numbers.shift, shift_size * UP, labels.shift, shift_size * UP, ) self.numbers = numbers self.labels = labels def mention_four_perpendicular_axes(self): number = self.numbers[2] three_axes = VGroup(*[ self.get_simple_axes(label, color) for label, color in zip( ["i", "j", "k"], [GREEN, RED, BLUE], ) ]) three_axes.arrange(RIGHT, buff=LARGE_BUFF) three_axes.next_to(number, DOWN, LARGE_BUFF) self.play(LaggedStartMap(FadeInFromLarge, three_axes)) self.wait(2) self.three_axes = three_axes def bring_back_complex(self): numbers = self.numbers labels = self.labels numbers[0].move_to(numbers[1], LEFT) labels[0].move_to(labels[1], LEFT) numbers.remove(numbers[1]) labels.remove(labels[1]) group = VGroup(numbers, labels) self.play( group.to_edge, UP, FadeOutAndShift(self.three_axes, DOWN) ) self.wait() def show_components_of_quaternion(self): quat = self.numbers[-1] real_part = quat[0] imag_part = quat[2:] real_brace = Brace(real_part, DOWN) imag_brace = Brace(imag_part, DOWN) real_word = TextMobject("Real \\\\ part") imag_word = TextMobject("Imaginary \\\\ part") scalar_word = TextMobject("Scalar \\\\ part") vector_word = TextMobject("``Vector'' \\\\ part") for word in real_word, scalar_word: word.next_to(real_brace, DOWN, SMALL_BUFF) for word in imag_word, vector_word: word.next_to(imag_brace, DOWN, SMALL_BUFF) braces = VGroup(real_brace, imag_brace) VGroup(scalar_word, vector_word).set_color(YELLOW) self.play( LaggedStartMap(GrowFromCenter, braces), LaggedStartMap( FadeInFrom, VGroup(real_word, imag_word), lambda m: (m, UP) ) ) self.wait() self.play( FadeOutAndShift(real_word, DOWN), FadeInFrom(scalar_word, DOWN), ) self.wait(2) self.play(ChangeDecimalToValue(real_part, 0)) self.wait() self.play( FadeOutAndShift(imag_word, DOWN), FadeInFrom(vector_word, DOWN) ) self.wait(2) # def get_example_numbers(self): number_2d = VGroup( DecimalNumber(3.14), TexMobject("+"), DecimalNumber(1.59), TexMobject("i") ) number_3d = VGroup( DecimalNumber(2.65), TexMobject("+"), DecimalNumber(3.58), TexMobject("i"), TexMobject("+"), DecimalNumber(9.79), TexMobject("j"), ) number_4d = VGroup( DecimalNumber(3.23), TexMobject("+"), DecimalNumber(8.46), TexMobject("i"), TexMobject("+"), DecimalNumber(2.64), TexMobject("j"), TexMobject("+"), DecimalNumber(3.38), TexMobject("k"), ) numbers = VGroup(number_2d, number_3d, number_4d) for number in numbers: number.arrange(RIGHT, buff=SMALL_BUFF) for part in number: if isinstance(part, TexMobject): # part.set_color_by_tex_to_color_map({ # "i": GREEN, # "j": RED, # "k": BLUE, # }) if part.get_tex_string() == "j": part.shift(0.5 * SMALL_BUFF * DL) number[2].set_color(GREEN) if len(number) > 5: number[5].set_color(RED) if len(number) > 8: number[8].set_color(BLUE) numbers.arrange( DOWN, buff=2, aligned_edge=LEFT ) numbers.center() numbers.shift(LEFT) return numbers def get_simple_axes(self, label, color): axes = Axes( x_min=-2.5, x_max=2.5, y_min=-2.5, y_max=2.5, ) axes.set_height(2.5) label_mob = TexMobject(label) label_mob.set_color(color) label_mob.next_to(axes.coords_to_point(0, 1.5), RIGHT, SMALL_BUFF) reals_label_mob = TextMobject("Reals") reals_label_mob.next_to( axes.coords_to_point(1, 0), DR, SMALL_BUFF ) axes.add(label_mob, reals_label_mob) return axes class SimpleImaginaryQuaternionAxes(SpecialThreeDScene): def construct(self): self.three_d_axes_config.update({ "number_line_config": {"unit_size": 2}, "x_min": -2, "x_max": 2, "y_min": -2, "y_max": 2, "z_min": -1.25, "z_max": 1.25, }) axes = self.get_axes() labels = VGroup(*[ TexMobject(tex).set_color(color) for tex, color in zip( ["i", "j", "k"], [GREEN, RED, BLUE] ) ]) labels[0].next_to(axes.coords_to_point(1, 0, 0), DOWN + IN, SMALL_BUFF) labels[1].next_to(axes.coords_to_point(0, 1, 0), RIGHT, SMALL_BUFF) labels[2].next_to(axes.coords_to_point(0, 0, 1), RIGHT, SMALL_BUFF) self.add(axes) self.add(labels) for label in labels: self.add_fixed_orientation_mobjects(label) self.move_camera(**self.get_default_camera_position()) self.begin_ambient_camera_rotation(rate=0.05) self.wait(15) class ShowDotProductCrossProductFromOfQMult(Scene): def construct(self): v_tex = "\\vec{\\textbf{v}}" product = TexMobject( "(", "w_1", "+", "x_1", "i", "+", "y_1", "j", "+", "z_1", "k", ")" "(", "w_2", "+", "x_2", "i", "+", "y_2", "j", "+", "z_2", "k", ")", "=", "(w_1", ",", v_tex + "_1", ")", "(w_2", ",", v_tex + "_2", ")", "=" ) product.set_width(FRAME_WIDTH - 1) i1 = product.index_of_part_by_tex("x_1") i2 = product.index_of_part_by_tex(")") i3 = product.index_of_part_by_tex("x_2") i4 = product.index_of_part_by_tex("z_2") + 2 vector_parts = [product[i1:i2], product[i3:i4]] vector_defs = VGroup() braces = VGroup() for i, vp in zip(it.count(1), vector_parts): brace = Brace(vp, UP) vector = Matrix([ ["x_" + str(i)], ["y_" + str(i)], ["z_" + str(i)], ]) colors = [GREEN, RED, BLUE] for mob, color in zip(vector.get_entries(), colors): mob.set_color(color) group = VGroup( TexMobject("{}_{} = ".format(v_tex, i)), vector, ) group.arrange(RIGHT, SMALL_BUFF) group.next_to(brace, UP) braces.add(brace) vector_defs.add(group) result = TexMobject( "\\left(", "w_1", "w_2", "-", v_tex + "_1", "\\cdot", v_tex, "_2", ",\\,", "w_1", v_tex + "_2", "+", "w_2", v_tex + "_1", "+", "{}_1 \\times {}_2".format(v_tex, v_tex), "\\right)" ) result.match_width(product) result.next_to(product, DOWN, LARGE_BUFF) for mob in product, result: mob.set_color_by_tex_to_color_map({ "w": YELLOW, "x": GREEN, "y": RED, "z": BLUE, }) mob.set_color_by_tex(v_tex, WHITE) self.add(product) self.add(braces) self.add(vector_defs) self.play(LaggedStartMap(FadeInFromLarge, result)) self.wait() class ShowComplexMagnitude(ShowComplexMultiplicationExamples): def construct(self): self.add_planes() plane = self.plane tex_to_color_map = { "a": YELLOW, "b": GREEN, } z = complex(3, 2) z_point = plane.number_to_point(z) z_dot = Dot(z_point) z_dot.set_color(PINK) z_line = Line(plane.number_to_point(0), z_point) z_line.set_stroke(WHITE, 2) z_label = TexMobject( "z", "=", "a", "+", "b", "i", tex_to_color_map=tex_to_color_map ) z_label.add_background_rectangle() z_label.next_to(z_dot, UR, buff=SMALL_BUFF) z_norm_label = TexMobject("||z||") z_norm_label.add_background_rectangle() z_norm_label.next_to(ORIGIN, UP, SMALL_BUFF) z_norm_label.rotate(z_line.get_angle(), about_point=ORIGIN) z_norm_label.shift(z_line.get_center()) h_line = Line( plane.number_to_point(0), plane.number_to_point(z.real), stroke_color=YELLOW, stroke_width=5, ) v_line = Line( plane.number_to_point(z.real), plane.number_to_point(z), stroke_color=GREEN, stroke_width=5, ) z_norm_equation = TexMobject( "||z||", "=", "\\sqrt", "{a^2", "+", "b^2", "}", tex_to_color_map=tex_to_color_map ) z_norm_equation.set_background_stroke(width=0) z_norm_equation.add_background_rectangle() z_norm_equation.next_to(z_label, UP) self.add(z_line, h_line, v_line, z_dot, z_label) self.play(ShowCreation(z_line)) self.play(FadeInFromDown(z_norm_label)) self.wait() self.play( FadeIn(z_norm_equation[0]), FadeIn(z_norm_equation[2:]), TransformFromCopy( z_norm_label[1:], VGroup(z_norm_equation[1]), ), ) self.wait() class BreakUpQuaternionMultiplicationInParts(Scene): def construct(self): q1_color = MAROON_B q2_color = YELLOW product = TexMobject( "q_1", "\\cdot", "q_2", "=", "\\left(", "{q_1", "\\over", "||", "q_1", "||}", "\\right)", "||", "q_1", "||", "\\cdot", "q_2", ) product.set_color_by_tex("q_1", q1_color) product.set_color_by_tex("q_2", q2_color) lhs = product[:3] scale_part = product[-5:] rotate_part = product[4:-5] lhs_rect = SurroundingRectangle(lhs) lhs_rect.set_color(YELLOW) lhs_words = TextMobject("Quaternion \\\\ multiplication") lhs_words.next_to(lhs_rect, UP, LARGE_BUFF) scale_brace = Brace(scale_part, UP) rotate_brace = Brace(rotate_part, DOWN) scale_words = TextMobject("Scale", "$q_2$") scale_words.set_color_by_tex("q_2", q2_color) scale_words.next_to(scale_brace, UP) rotate_words = TextMobject("Apply special \\\\ 4d rotation") rotate_words.next_to(rotate_brace, DOWN) norm_equation = TexMobject( "||", "q_1", "||", "=", "||", "w_1", "+", "x_1", "i", "+", "y_1", "j", "+", "z_1", "k", "||", "=", "\\sqrt", "{w_1^2", "+", "x_1^2", "+", "y_1^2", "+", "z_1^2", "}", ) # norm_equation.set_color_by_tex_to_color_map({ # "w": YELLOW, # "x": GREEN, # "y": RED, # "z": BLUE, # }) norm_equation.set_color_by_tex("q_1", q1_color) norm_equation.to_edge(UP) norm_equation.set_background_stroke(width=0) line1 = Line(ORIGIN, 0.5 * LEFT + 3 * UP) line2 = Line(ORIGIN, UR) zero_dot = Dot() zero_label = TexMobject("0") zero_label.next_to(zero_dot, DOWN, SMALL_BUFF) q1_dot = Dot(line1.get_end()) q2_dot = Dot(line2.get_end()) q1_label = TexMobject("q_1").next_to(q1_dot, UP, SMALL_BUFF) q2_label = TexMobject("q_2").next_to(q2_dot, UR, SMALL_BUFF) VGroup(q1_dot, q1_label).set_color(q1_color) VGroup(q2_dot, q2_label).set_color(q2_color) dot_group = VGroup( line1, line2, q1_dot, q2_dot, q1_label, q2_label, zero_dot, zero_label, ) dot_group.set_height(3) dot_group.center() dot_group.to_edge(LEFT) q1_dot.add_updater(lambda d: d.move_to(line1.get_end())) q1_label.add_updater(lambda l: l.next_to(q1_dot, UP, SMALL_BUFF)) q2_dot.add_updater(lambda d: d.move_to(line2.get_end())) q2_label.add_updater(lambda l: l.next_to(q2_dot, UR, SMALL_BUFF)) self.add(norm_equation) self.wait() self.play( FadeInFromDown(lhs), Write(dot_group), ) self.add(*dot_group) self.add( VGroup(line2, q2_dot, q2_label).copy().fade(0.5) ) self.play( ShowCreation(lhs_rect), FadeIn(lhs_words) ) self.play(FadeOut(lhs_rect)) self.wait() self.play( TransformFromCopy(lhs, product[3:]), # FadeOut(lhs_words) ) self.play( GrowFromCenter(scale_brace), Write(scale_words), ) self.play( line2.scale, 2, {"about_point": line2.get_start()} ) self.wait() self.play( GrowFromCenter(rotate_brace), FadeInFrom(rotate_words, UP), ) self.play( Rotate( line2, -line1.get_angle(), about_point=line2.get_start(), run_time=3 ) ) self.wait() # Ask randy = Randolph(height=2) randy.flip() randy.next_to(rotate_words, RIGHT) randy.to_edge(DOWN) q_marks = TexMobject("???") random.shuffle(q_marks.submobjects) q_marks.next_to(randy, UP) self.play( FadeIn(randy) ) self.play( randy.change, "confused", rotate_words, ShowCreationThenFadeAround(rotate_words), ) self.play(LaggedStartMap( FadeInFrom, q_marks, lambda m: (m, LEFT), lag_ratio=0.8, )) self.play(Blink(randy)) self.wait(2) class SphereProjectionsWrapper(Scene): def construct(self): rect_rows = VGroup(*[ VGroup(*[ ScreenRectangle(height=3) for x in range(3) ]).arrange(RIGHT, buff=LARGE_BUFF) for y in range(2) ]).arrange(DOWN, buff=2 * LARGE_BUFF) rect_rows.set_width(FRAME_WIDTH - 1) sphere_labels = VGroup( TextMobject("Circle in 2d"), TextMobject("Sphere in 3d"), TextMobject("Hypersphere in 4d"), ) for label, rect in zip(sphere_labels, rect_rows[0]): label.next_to(rect, UP) projected_labels = VGroup( TextMobject("Sterographically projected \\\\ circle in 1d"), TextMobject("Sterographically projected \\\\ sphere in 2d"), TextMobject("Sterographically projected \\\\ hypersphere in 3d"), ) for label, rect in zip(projected_labels, rect_rows[1]): label.match_width(rect) label.next_to(rect, UP) q_marks = TexMobject("???") q_marks.scale(2) q_marks.move_to(rect_rows[0][2]) self.add(rect_rows) for l1, l2 in zip(sphere_labels, projected_labels): added_anims = [] if l1 is sphere_labels[2]: added_anims.append(FadeIn(q_marks)) self.play(FadeIn(l1), *added_anims) self.play(FadeIn(l2)) self.wait() class HypersphereStereographicProjection(SpecialThreeDScene): CONFIG = { # "fancy_dot": False, "fancy_dot": True, "initial_quaternion_sample_values": [ [0, 1, 0, 0], [-1, 1, 0, 0], [0, 0, 1, 1], [0, 1, -1, 1], ], "unit_labels_scale_factor": 1, } def construct(self): self.setup_axes() self.introduce_quaternion_label() self.show_one() self.show_unit_sphere() self.show_quaternions_with_nonzero_real_part() self.emphasize_only_units() self.show_reference_spheres() def setup_axes(self): axes = self.axes = self.get_axes() axes.set_stroke(width=1) self.add(axes) self.move_camera( **self.get_default_camera_position(), run_time=0 ) self.begin_ambient_camera_rotation(rate=0.01) def introduce_quaternion_label(self): q_tracker = QuaternionTracker() coords = [ DecimalNumber(0, color=color, include_sign=sign, edge_to_fix=RIGHT) for color, sign in zip( [YELLOW, GREEN, RED, BLUE], [False, True, True, True], ) ] label = VGroup( coords[0], VectorizedPoint(), coords[1], TexMobject("i"), coords[2], TexMobject("j"), coords[3], TexMobject("k"), ) label.arrange(RIGHT, buff=SMALL_BUFF) label.to_corner(UR) def update_label(label): self.remove_fixed_in_frame_mobjects(label) quat = q_tracker.get_value() for value, coord in zip(quat, label[::2]): coord.set_value(value) self.add_fixed_in_frame_mobjects(label) return label label.add_updater(update_label) self.pink_dot_label = label def get_pq_point(): point = self.project_quaternion(q_tracker.get_value()) if get_norm(point) > 100: return point * 100 / get_norm(point) return point pq_dot = self.get_dot() pq_dot.add_updater(lambda d: d.move_to(get_pq_point())) dot_radius = pq_dot.get_width() / 2 def get_pq_line(): point = get_pq_point() norm = get_norm(point) origin = self.axes.coords_to_point(0, 0, 0) if norm > dot_radius: point -= origin point *= (norm - dot_radius) / norm point += origin result = Line(origin, point) result.set_stroke(width=1) return result pq_line = get_pq_line() pq_line.add_updater(lambda cl: cl.become(get_pq_line())) self.add(q_tracker, label, pq_line, pq_dot) self.q_tracker = q_tracker self.q_label = label self.pq_line = pq_line self.pq_dot = pq_dot rect = SurroundingRectangle(label, color=WHITE) self.add_fixed_in_frame_mobjects(rect) self.play(ShowCreation(rect)) self.play(FadeOut(rect)) self.remove_fixed_orientation_mobjects(rect) for value in self.initial_quaternion_sample_values: self.set_quat(value) self.wait() def show_one(self): q_tracker = self.q_tracker one_label = TexMobject("1") one_label.rotate(TAU / 4, RIGHT) one_label.next_to(ORIGIN, IN + RIGHT, SMALL_BUFF) one_label.set_shade_in_3d(True) one_label.set_background_stroke(width=0) self.play( ApplyMethod( q_tracker.set_value, [1, 0, 0, 0], run_time=2 ), FadeInFromDown(one_label) ) self.wait(4) def show_unit_sphere(self): sphere = self.sphere = self.get_projected_sphere( quaternion=[1, 0, 0, 0], null_axis=0, solid=False, stroke_width=0.5 ) self.specially_color_sphere(sphere) labels = self.get_unit_labels() labels.remove(labels[3]) real_part = self.q_label[0] brace = Brace(real_part, DOWN) words = TextMobject("Real part zero") words.next_to(brace, DOWN, SMALL_BUFF, LEFT) self.play(Write(sphere)) self.play(LaggedStartMap( FadeInFrom, labels, lambda m: (m, IN) )) self.add_fixed_in_frame_mobjects(brace, words) self.set_quat( [0, 1, 0, 0], added_anims=[ GrowFromCenter(brace), Write(words), ] ) self.wait() self.set_quat([0, 1, -1, 1]) self.wait(2) self.set_quat([0, -1, -1, 1]) self.wait(2) self.set_quat([0, 0, 0, 1]) self.wait(2) self.set_quat([0, 0, -1, 0]) self.wait(2) self.set_quat([0, 1, 0, 0]) self.wait(2) self.play(FadeOut(words)) self.remove_fixed_in_frame_mobjects(words) self.real_part_brace = brace def show_quaternions_with_nonzero_real_part(self): # Positive real part self.set_quat( [1, 1, 2, 0], added_anims=[ ApplyMethod( self.sphere.copy().scale, 0, remover=True ) ] ) self.wait(2) self.set_quat([4, 0, -1, -1]) self.wait(2) # Negative real part self.set_quat( [-1, 1, 2, 0], added_anims=[ ApplyFunction( lambda s: s.scale(10).fade(1), self.sphere.copy(), remover=True ) ] ) self.wait(2) self.set_quat([-2, 0, -1, 1]) self.wait(2) self.set_quat([-1, 1, 0, 0]) self.move_camera(theta=-160 * DEGREES, run_time=3) self.set_quat([-1, 0.001, 0, 0]) self.wait(2) def emphasize_only_units(self): q_label = self.q_label brace = self.real_part_brace brace.target = Brace(q_label, DOWN, buff=SMALL_BUFF) words = TextMobject( "Only those where \\\\", "$w^2 + x^2 + y^2 + z^2 = 1$" ) words.next_to(brace.target, DOWN, SMALL_BUFF) self.add_fixed_in_frame_mobjects(words) self.play( MoveToTarget(brace), Write(words) ) self.set_quat([1, 1, 1, 1]) self.wait(2) self.set_quat([1, 1, -1, 1]) self.wait(2) self.set_quat([-1, 1, -1, 1]) self.wait(8) self.play(FadeOut(brace), FadeOut(words)) self.remove_fixed_in_frame_mobjects(brace, words) # TODO def show_reference_spheres(self): sphere = self.sphere self.move_camera( phi=60 * DEGREES, theta=-150 * DEGREES, added_anims=[ self.q_tracker.set_value, [1, 0, 0, 0] ] ) sphere_ijk = self.get_projected_sphere(null_axis=0) sphere_1jk = self.get_projected_sphere(null_axis=1) sphere_1ik = self.get_projected_sphere(null_axis=2) sphere_1ij = self.get_projected_sphere(null_axis=3) circle = StereoProjectedCircleFromHypersphere(axes=[0, 1]) circle_words = TextMobject( "Circle through\\\\", "$1, i, -1, -i$" ) sphere_1ij_words = TextMobject( "Sphere through\\\\", "$1, i, j, -1, -i, -j$" ) sphere_1jk_words = TextMobject( "Sphere through\\\\", "$1, j, k, -1, -j, -k$" ) sphere_1ik_words = TextMobject( "Sphere through\\\\", "$1, i, k, -1, -i, -k$" ) for words in [circle_words, sphere_1ij_words, sphere_1jk_words, sphere_1ik_words]: words.to_corner(UL) self.add_fixed_in_frame_mobjects(words) self.play( ShowCreation(circle), Write(circle_words), ) self.set_quat([0, 1, 0, 0]) self.set_quat([1, 0, 0, 0]) self.remove(sphere) sphere_ijk.match_style(sphere) self.add(sphere_ijk) # Show xy plane self.play( FadeOutAndShift(circle_words, DOWN), FadeInFromDown(sphere_1ij_words), FadeOut(circle), sphere_ijk.set_stroke, {"width": 0.0} ) self.play(Write(sphere_1ij)) self.wait(10) return # Show yz plane self.play( FadeOutAndShift(sphere_1ij_words, DOWN), FadeInFromDown(sphere_1jk_words), sphere_1ij.set_fill, BLUE_E, 0.25, sphere_1ij.set_stroke, {"width": 0.0}, Write(sphere_1jk) ) self.wait(5) # Show xz plane self.play( FadeOutAndShift(sphere_1jk_words, DOWN), FadeInFromDown(sphere_1ik_words), sphere_1jk.set_fill, GREEN_E, 0.25, sphere_1jk.set_stroke, {"width": 0.0}, Write(sphere_1ik) ) self.wait(5) self.play( sphere_1ik.set_fill, RED_E, 0.25, sphere_1ik.set_stroke, {"width": 0.0}, FadeOut(sphere_1ik_words) ) # Start applying quaternion multiplication kwargs = {"solid": False, "stroke_width": 0} sphere_ijk.add_updater( lambda s: s.become(self.get_projected_sphere(0, **kwargs)) ) sphere_1jk.add_updater( lambda s: s.become(self.get_projected_sphere(1, **kwargs)) ) sphere_1ik.add_updater( lambda s: s.become(self.get_projected_sphere(2, **kwargs)) ) sphere_1ij.add_updater( lambda s: s.become(self.get_projected_sphere(3, **kwargs)) ) self.set_quat([0, 1, 1, 1]) # def project_quaternion(self, quat): return self.axes.coords_to_point( *stereo_project_point(quat, axis=0, r=1)[1:] ) def get_dot(self): if self.fancy_dot: sphere = self.get_sphere() sphere.set_width(0.2) sphere.set_stroke(width=0) sphere.set_fill(PINK) return sphere else: return VGroup( Dot(color=PINK), Dot(color=PINK).rotate(TAU / 4, RIGHT), ) def get_unit_labels(self): c2p = self.axes.coords_to_point tex_coords_vects = [ ("i", [1, 0, 0], IN + RIGHT), ("-i", [-1, 0, 0], IN + LEFT), ("j", [0, 1, 0], UP + OUT + RIGHT), ("-j", [0, -1, 0], RIGHT + DOWN), ("k", [0, 0, 1], OUT + RIGHT), ("-k", [0, 0, -1], IN + RIGHT), ] labels = VGroup() for tex, coords, vect in tex_coords_vects: label = TexMobject(tex) label.scale(self.unit_labels_scale_factor) label.rotate(90 * DEGREES, RIGHT) label.next_to(c2p(*coords), vect, SMALL_BUFF) labels.add(label) labels.set_shade_in_3d(True) labels.set_background_stroke(width=0) return labels def set_quat(self, value, run_time=3, added_anims=None): if added_anims is None: added_anims = [] self.play( self.q_tracker.set_value, value, *added_anims, run_time=run_time ) def get_projected_sphere(self, null_axis, quaternion=None, solid=True, **kwargs): if quaternion is None: quaternion = self.get_multiplier() axes_to_color = { 0: interpolate_color(YELLOW, BLACK, 0.5), 1: GREEN_E, 2: RED_D, 3: BLUE_E, } color = axes_to_color[null_axis] config = dict(self.sphere_config) config.update({ "stroke_color": WHITE, "stroke_width": 0.5, "stroke_opacity": 0.5, "max_r": 24, }) if solid: config.update({ "checkerboard_colors": [ color, interpolate_color(color, BLACK, 0.5) ], "fill_opacity": 1, }) else: config.update({ "checkerboard_colors": [], "fill_color": color, "fill_opacity": 0.25, }) config.update(kwargs) sphere = StereoProjectedSphereFromHypersphere( quaternion=quaternion, null_axis=null_axis, **config ) sphere.set_shade_in_3d(True) return sphere def get_projected_circle(self, quaternion=None, **kwargs): if quaternion is None: quaternion = self.get_multiplier() return StereoProjectedCircleFromHypersphere(quaternion, **kwargs) def get_multiplier(self): return self.q_tracker.get_value() def specially_color_sphere(self, sphere): sphere.set_color_by_gradient(BLUE, GREEN, PINK) return sphere # for submob in sphere: # u, v = submob.u1, submob.v1 # x = np.cos(v) * np.sin(u) # y = np.sin(v) * np.sin(u) # z = np.cos(u) # # rgb = sum([ # # (x**2) * hex_to_rgb(GREEN), # # (y**2) * hex_to_rgb(RED), # # (z**2) * hex_to_rgb(BLUE), # # ]) # # clip_in_place(rgb, 0, 1) # # color = rgb_to_hex(rgb) # color = interpolate_color(BLUE, RED, ((z**3) + 1) / 2) # submob.set_fill(color) # return sphere class MissingLabels(Scene): def construct(self): labels = VGroup( TexMobject("i").move_to(UR), TexMobject("1").move_to(0.3 * DOWN), TexMobject("-i").move_to(DOWN + 1.2 * LEFT), TexMobject("-j").move_to(1.7 * RIGHT + 0.8 * DOWN), ) labels.set_background_stroke(width=0) self.add(labels) class RuleOfQuaternionMultiplicationOverlay(Scene): def construct(self): q_mob, times_mob, p_mob = q_times_p = TexMobject( "q", "\\cdot", "p" ) q_times_p.scale(2) q_mob.set_color(MAROON_B) p_mob.set_color(YELLOW) q_arrow = Vector(DOWN, color=WHITE) q_arrow.next_to(q_mob, UP) p_arrow = Vector(UP, color=WHITE) p_arrow.next_to(p_mob, DOWN) q_words = TextMobject("Think of as\\\\ an action") q_words.next_to(q_arrow, UP) p_words = TextMobject("Think of as\\\\ a point") p_words.next_to(p_arrow, DOWN) i_mob = TexMobject("i")[0] i_mob.scale(2) i_mob.move_to(q_mob, RIGHT) i_mob.set_color(GREEN) self.add(q_times_p) self.play( FadeInFrom(q_words, UP), GrowArrow(q_arrow), ) self.play( FadeInFrom(p_words, DOWN), GrowArrow(p_arrow), ) self.wait() self.play(*map(FadeOut, [ q_words, q_arrow, p_words, p_arrow, ])) self.play( FadeInFromDown(i_mob), FadeOutAndShift(q_mob, UP) ) product = VGroup(i_mob, times_mob, p_mob) self.play(product.to_edge, UP) # Show i products underline = Line(LEFT, RIGHT) underline.set_width(product.get_width() + MED_SMALL_BUFF) underline.next_to(product, DOWN) kwargs = { "tex_to_color_map": { "i": GREEN, "j": RED, "k": BLUE } } i_products = VGroup( TexMobject("i", "\\cdot", "1", "=", "i", **kwargs), TexMobject("i", "\\cdot", "i", "=", "-1", **kwargs), TexMobject("i", "\\cdot", "j", "=", "k", **kwargs), TexMobject("i", "\\cdot", "k", "=", "-j", **kwargs), ) i_products.scale(2) i_products.arrange( DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT, ) i_products.next_to(underline, DOWN, LARGE_BUFF) i_products.align_to(i_mob, LEFT) self.play(ShowCreation(underline)) self.wait() for i_product in i_products: self.play(TransformFromCopy( product, i_product[:3] )) self.wait() self.play(TransformFromCopy( i_product[:3], i_product[3:], )) self.wait() rect = SurroundingRectangle( VGroup(product, i_products), buff=0.4 ) rect.set_stroke(WHITE, width=5) self.play(ShowCreation(rect)) self.play(FadeOut(rect)) class RuleOfQuaternionMultiplication(HypersphereStereographicProjection): CONFIG = { "fancy_dot": True, "initial_quaternion_sample_values": [], } def construct(self): self.setup_all_trackers() self.show_multiplication_by_i_on_circle_1i() self.show_multiplication_by_i_on_circle_jk() self.show_multiplication_by_i_on_ijk_sphere() def setup_all_trackers(self): self.setup_multiplier_tracker() self.force_skipping() self.setup_axes() self.introduce_quaternion_label() self.add_unit_labels() self.revert_to_original_skipping_status() def setup_multiplier_tracker(self): self.multiplier_tracker = QuaternionTracker([1, 0, 0, 0]) self.multiplier_tracker.add_updater( lambda m: m.set_value(normalize( m.get_value(), fall_back=[1, 0, 0, 0] )) ) self.add(self.multiplier_tracker) def add_unit_labels(self): labels = self.unit_labels = self.get_unit_labels() one_label = TexMobject("1") one_label.scale(self.unit_labels_scale_factor) one_label.set_shade_in_3d(True) one_label.rotate(90 * DEGREES, RIGHT) one_label.next_to(ORIGIN, IN + RIGHT, SMALL_BUFF) labels.add(one_label) self.add(labels) def show_multiplication_by_i_on_circle_1i(self): m_tracker = self.multiplier_tracker def get_circle_1i(): return self.get_projected_circle( basis_vectors=[ [1, 0, 0, 0], [1, 1, 0, 0], ], colors=[GREEN, YELLOW], quaternion=m_tracker.get_value(), ) circle = get_circle_1i() arrows = self.get_i_circle_arrows() def set_to_q_value(mt): mt.set_value(self.q_tracker.get_value()) self.play(ShowCreation(circle, run_time=2)) self.play(LaggedStartMap(ShowCreation, arrows, lag_ratio=0.25)) self.wait() circle.add_updater(lambda c: c.become(get_circle_1i())) m_tracker.add_updater(set_to_q_value) self.add(m_tracker) self.set_quat([0, 1, 0, 0]) self.wait() self.set_quat([-1, 0.001, 0, 0]) self.wait() self.q_tracker.set_value([-1, -0.001, 0, 0]) self.set_quat([0, -1, 0, 0]) self.wait() self.set_quat([1, 0, 0, 0]) self.wait(3) self.play(FadeOut(arrows)) m_tracker.remove_updater(set_to_q_value) self.circle_1i = circle def show_multiplication_by_i_on_circle_jk(self): def get_circle_jk(): return self.get_projected_circle( basis_vectors=[ [0, 0, 1, 0], [0, 0, 0, 1], ], colors=[RED, BLUE_E] ) circle = get_circle_jk() arrows = self.get_jk_circle_arrows() m_tracker = self.multiplier_tracker q_tracker = self.q_tracker def set_q_to_mj(qt): qt.set_value(q_mult( m_tracker.get_value(), [0, 0, 1, 0] )) self.move_camera(theta=-50 * DEGREES) self.play(ShowCreation(circle, run_time=2)) circle.add_updater(lambda c: c.become(get_circle_jk())) self.wait(10) self.stop_ambient_camera_rotation() self.begin_ambient_camera_rotation(rate=-0.01) self.play(*map(ShowCreation, arrows)) self.wait() self.set_quat([0, 0, 1, 0], run_time=1) q_tracker.add_updater(set_q_to_mj, index=0) self.add(self.circle_1i) self.play( m_tracker.set_value, [0, 1, 0, 0], run_time=3 ) self.wait() self.play( m_tracker.set_value, [-1, 0.001, 0, 0], run_time=3 ) self.wait() m_tracker.set_value([-1, 0.001, 0, 0]) self.play( m_tracker.set_value, [0, -1, 0, 0], run_time=3 ) self.wait() self.play( m_tracker.set_value, [1, 0, 0, 0], run_time=3 ) self.wait() q_tracker.remove_updater(set_q_to_mj) self.play( FadeOut(arrows), q_tracker.set_value, [1, 0, 0, 0], ) self.wait(10) self.circle_jk = circle def show_multiplication_by_i_on_ijk_sphere(self): m_tracker = self.multiplier_tracker q_tracker = self.q_tracker m_tracker.add_updater(lambda m: m.set_value(q_tracker.get_value())) def get_sphere(): result = self.get_projected_sphere(null_axis=0, solid=False) self.specially_color_sphere(result) return result sphere = get_sphere() self.play(Write(sphere)) sphere.add_updater(lambda s: s.become(get_sphere())) self.set_quat([0, 1, 0, 0]) self.wait() self.set_quat([-1, 0.001, 0, 0]) self.wait() self.q_tracker.set_value([-1, -0.001, 0, 0]) self.set_quat([0, -1, 0, 0]) self.wait() self.set_quat([1, 0, 0, 0]) self.wait(3) # def get_multiplier(self): return self.multiplier_tracker.get_value() def get_i_circle_arrows(self): c2p = self.axes.coords_to_point i_arrow = Arrow( ORIGIN, 2 * RIGHT, path_arc=-120 * DEGREES, buff=SMALL_BUFF, ) neg_one_arrow = Arrow( ORIGIN, 5.5 * RIGHT + UP, path_arc=-30 * DEGREES, buff=SMALL_BUFF, ) neg_i_arrow = Arrow( 4.5 * LEFT + 1.5 * UP, ORIGIN, path_arc=-30 * DEGREES, buff=SMALL_BUFF, ) one_arrow = i_arrow.copy() result = VGroup(i_arrow, neg_one_arrow, neg_i_arrow, one_arrow) for arrow in result: arrow.set_color(LIGHT_GREY) arrow.set_stroke(width=3) arrow.rotate(90 * DEGREES, RIGHT) i_arrow.next_to(c2p(0, 0, 0), OUT + RIGHT, SMALL_BUFF) neg_one_arrow.next_to(c2p(1, 0, 0), OUT + RIGHT, SMALL_BUFF) neg_i_arrow.next_to(c2p(-1, 0, 0), OUT + LEFT, SMALL_BUFF) one_arrow.next_to(c2p(0, 0, 0), OUT + LEFT, SMALL_BUFF) return result def get_jk_circle_arrows(self): arrow = Arrow( 1.5 * RIGHT, 1.5 * UP, path_arc=90 * DEGREES, buff=SMALL_BUFF, use_rectangular_stem=False ) arrow.set_color(LIGHT_GREY) arrow.set_stroke(width=3) arrows = VGroup(*[ arrow.copy().rotate(angle, about_point=ORIGIN) for angle in np.arange(0, TAU, TAU / 4) ]) arrows.rotate(90 * DEGREES, RIGHT) arrows.rotate(90 * DEGREES, OUT) return arrows class ShowDistributionOfI(TeacherStudentsScene): def construct(self): tex_to_color_map = { "q": PINK, "w": YELLOW, "x": GREEN, "y": RED, "z": BLUE, } top_product = TexMobject( "q", "\\cdot", "\\left(", "w", "1", "+", "x", "i", "+", "y", "j", "+", "z", "k", "\\right)" ) top_product.to_edge(UP) self.add(top_product) bottom_product = TexMobject( "w", "q", "\\cdot", "1", "+", "x", "q", "\\cdot", "i", "+", "y", "q", "\\cdot", "j", "+", "z", "q", "\\cdot", "k", ) bottom_product.next_to(top_product, DOWN, MED_LARGE_BUFF) for product in [top_product, bottom_product]: for tex, color in tex_to_color_map.items(): product.set_color_by_tex(tex, color, substring=False) self.student_says( "What does it do \\\\ to other quaternions?", target_mode="raise_left_hand" ) self.change_student_modes( "pondering", "raise_left_hand", "erm", look_at_arg=top_product, ) self.wait(2) self.play( self.teacher.change, "raise_right_hand", RemovePiCreatureBubble(self.students[1], target_mode="pondering"), *[ TransformFromCopy( top_product.get_parts_by_tex(tex, substring=False), bottom_product.get_parts_by_tex(tex, substring=False), run_time=2 ) for tex in ["1", "w", "x", "i", "y", "j", "z", "k", "+"] ] ) self.play(*[ TransformFromCopy( top_product.get_parts_by_tex(tex, substring=False), bottom_product.get_parts_by_tex(tex, substring=False), run_time=2 ) for tex in ["q", "\\cdot"] ]) self.change_all_student_modes("thinking") self.wait(3) class ComplexPlane135(Scene): def construct(self): plane = ComplexPlane(unit_size=2) plane.add_coordinates() for mob in plane.coordinate_labels: mob.scale(2, about_edge=UR) angle = 3 * TAU / 8 circle = Circle(radius=2, color=YELLOW) arc = Arc(angle, radius=0.5) angle_label = Integer(0, unit="^\\circ") angle_label.next_to(arc.point_from_proportion(0.5), UR, SMALL_BUFF) line = Line(ORIGIN, 2 * RIGHT) point = circle.point_from_proportion(angle / TAU) dot = Dot(point, color=PINK) arrow = Vector(DR) arrow.next_to(dot, UL, SMALL_BUFF) arrow.match_color(dot) label = TexMobject("-\\frac{\\sqrt{2}}{2} + \\frac{\\sqrt{2}}{2} i") label.next_to(arrow.get_start(), UP, SMALL_BUFF) label.set_background_stroke(width=0) self.add(plane, circle, line, dot, label, arrow) self.play( Rotate(line, angle, about_point=ORIGIN), ShowCreation(arc), ChangeDecimalToValue(angle_label, 135), run_time=3 ) self.wait() class ShowMultiplicationBy135Example(RuleOfQuaternionMultiplication): CONFIG = { "fancy_dot": True, } def construct(self): self.setup_all_trackers() self.add_circles() self.add_ijk_sphere() self.show_multiplication() def add_circles(self): self.circle_1i = self.add_auto_updating_circle( basis_vectors=[ [1, 0, 0, 0], [0, 1, 0, 0], ], colors=[YELLOW, GREEN_E] ) self.circle_jk = self.add_auto_updating_circle( basis_vectors=[ [0, 0, 1, 0], [0, 0, 0, 1], ], colors=[RED, BLUE_E] ) def add_auto_updating_circle(self, **circle_config): circle = self.get_projected_circle(**circle_config) circle.add_updater( lambda c: c.become(self.get_projected_circle(**circle_config)) ) self.add(circle) return circle def add_ijk_sphere(self): def get_sphere(): result = self.get_projected_sphere( null_axis=0, solid=False, stroke_width=0.5, stroke_opacity=0.2, fill_opacity=0.2, ) self.specially_color_sphere(result) return result sphere = get_sphere() sphere.add_updater(lambda s: s.become(get_sphere())) self.add(sphere) self.sphere = sphere def show_multiplication(self): m_tracker = self.multiplier_tracker quat = normalize(np.array([-1, 1, 0, 0])) point = self.project_quaternion(quat) arrow = Vector(DR) arrow.next_to(point, UL, MED_SMALL_BUFF) arrow.set_color(PINK) label = TexMobject( "-{\\sqrt{2} \\over 2}", "+", "{\\sqrt{2} \\over 2}", "i", ) label.next_to(arrow.get_start(), UP) label.set_background_stroke(width=0) def get_one_point(): return self.circle_1i[0].points[0] def get_j_point(): return self.circle_jk[0].points[0] one_point = VectorizedPoint() one_point.add_updater(lambda v: v.set_location(get_one_point())) self.add(one_point) hand = Hand() hand.rotate(45 * DEGREES, RIGHT) hand.add_updater( lambda h: h.move_to(get_one_point(), LEFT) ) j_line = Line(ORIGIN, get_j_point()) moving_j_line = j_line.deepcopy() moving_j_line.add_updater( lambda m: m.put_start_and_end_on(ORIGIN, get_j_point()) ) self.add(j_line, moving_j_line) self.set_camera_orientation( phi=60 * DEGREES, theta=-70 * DEGREES ) self.play( FadeInFromLarge(label, 3), GrowArrow(arrow) ) self.set_quat(quat) self.wait(5) self.play(FadeInFromLarge(hand)) self.add(m_tracker) for q in [quat, [1, 0, 0, 0], quat]: self.play( m_tracker.set_value, q, UpdateFromFunc( m_tracker, lambda m: m.set_value(normalize(m.get_value())) ), run_time=5 ) self.wait() class JMultiplicationChart(Scene): def construct(self): # Largely copy-pasted....what are you gonna do about it? product = TexMobject("j", "\\cdot", "p") product[0].set_color(RED) product.scale(2) product.to_edge(UP) underline = Line(LEFT, RIGHT) underline.set_width(product.get_width() + MED_SMALL_BUFF) underline.next_to(product, DOWN) kwargs = { "tex_to_color_map": { "i": GREEN, "j": RED, "k": BLUE } } j_products = VGroup( TexMobject("j", "\\cdot", "1", "=", "j", **kwargs), TexMobject("j", "\\cdot", "j", "=", "-1", **kwargs), TexMobject("j", "\\cdot", "i", "=", "-k", **kwargs), TexMobject("j", "\\cdot", "k", "=", "i", **kwargs), ) j_products.scale(2) j_products.arrange( DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT, ) j_products.next_to(underline, DOWN, LARGE_BUFF) j_products.align_to(product, LEFT) self.play(FadeInFromDown(product)) self.play(ShowCreation(underline)) self.wait() for j_product in j_products: self.play(TransformFromCopy( product, j_product[:3] )) self.wait() self.play(TransformFromCopy( j_product[:3], j_product[3:], )) self.wait() rect = SurroundingRectangle( VGroup(product, j_products), buff=MED_SMALL_BUFF ) rect.set_stroke(WHITE, width=5) self.play(ShowCreation(rect)) self.play(FadeOut(rect)) class ShowJMultiplication(ShowMultiplicationBy135Example): CONFIG = { "fancy_dot": True, "run_time_per_rotation": 4, } def construct(self): self.setup_all_trackers() self.add_circles() self.add_ijk_sphere() self.show_multiplication() def add_circles(self): self.circle_1j = self.add_auto_updating_circle( basis_vectors=[ [1, 0, 0, 0], [0, 0, 1, 0], ], colors=[YELLOW, RED] ) self.circle_ik = self.add_auto_updating_circle( basis_vectors=[ [0, 1, 0, 0], [0, 0, 0, 1], ], colors=[GREEN, BLUE_E] ) def show_multiplication(self): self.set_camera_orientation(theta=-80 * DEGREES) q_tracker = self.q_tracker m_tracker = self.multiplier_tracker def normalize_tracker(t): t.set_value(normalize(t.get_value())) updates = [ UpdateFromFunc(tracker, normalize_tracker) for tracker in (q_tracker, m_tracker) ] run_time = self.run_time_per_rotation self.play( m_tracker.set_value, [0, 0, 1, 0], q_tracker.set_value, [0, 0, 1, 0], *updates, run_time=run_time, ) self.wait(2) self.play( m_tracker.set_value, [-1, 0, 1e-3, 0], q_tracker.set_value, [-1, 0, 1e-3, 0], *updates, run_time=run_time, ) self.wait(2) # Show ik circle circle = self.circle_ik.deepcopy() circle.clear_updaters() self.play(FadeInFromLarge(circle, remover=True)) m_tracker.set_value([-1, 0, 0, 0]) q_tracker.set_value([0, 1, 0, 0]) self.wait() self.play( m_tracker.set_value, [0, 0, -1, 0], q_tracker.set_value, [0, 0, 0, -1], *updates, run_time=run_time, ) self.wait(2) self.play( m_tracker.set_value, [1, 0, -1e-3, 0], q_tracker.set_value, [0, -1, 0, 0], *updates, run_time=run_time, ) self.wait(2) class ShowArbitraryMultiplication(ShowMultiplicationBy135Example): CONFIG = { "fancy_dot": True, "run_time_per_rotation": 4, "special_quaternion": [-0.5, 0.5, 0.5, 0.5], } def construct(self): self.setup_all_trackers() self.add_circles() self.add_ijk_sphere() self.show_multiplication() def add_circles(self): self.circle1 = self.add_auto_updating_circle( basis_vectors=[ [1, 0, 0, 0], [0, 1, 1, 1], ], colors=[YELLOW_E, YELLOW] ) bv1 = normalize([0, -1, -1, 2]) bv2 = [0] + list(normalize(np.cross([1, 1, 1], bv1[1:]))) self.circle2 = self.add_auto_updating_circle( basis_vectors=[bv1, bv2], colors=[WHITE, GREY] ) def show_multiplication(self): q_tracker = self.q_tracker m_tracker = self.multiplier_tracker run_time = self.run_time_per_rotation def normalize_tracker(t): t.set_value(normalize(t.get_value())) # for tracker in q_tracker, m_tracker: # self.add(Mobject.add_updater(tracker, normalize_tracker)) updates = [ UpdateFromFunc(tracker, normalize_tracker) for tracker in (q_tracker, m_tracker) ] special_q = self.special_quaternion pq_point = self.project_quaternion(special_q) label = TextMobject("Some unit quaternion") label.set_color(PINK) label.rotate(90 * DEGREES, RIGHT) label.next_to(pq_point, IN + RIGHT, SMALL_BUFF) circle1, circle2 = self.circle1, self.circle2 for circle in [circle1, circle2]: circle.tucked_away_updaters = circle.updaters circle.clear_updaters() self.remove(circle) hand = Hand() hand.rotate(90 * DEGREES, RIGHT) hand.move_to(ORIGIN, LEFT) hand.set_shade_in_3d(True) one_dot = self.get_dot() one_dot.set_color(YELLOW_E) one_dot.move_to(ORIGIN) one_dot.add_updater( lambda m: m.move_to(circle1[0].points[0]) ) self.add(one_dot) self.stop_ambient_camera_rotation() self.begin_ambient_camera_rotation(rate=0.02) self.set_quat(special_q) self.play(FadeInFrom(label, IN)) self.wait(3) for circle in [circle1, circle2]: self.play(ShowCreation(circle, run_time=3)) circle.updaters = circle.tucked_away_updaters self.wait(2) self.play( FadeInFrom(hand, 2 * IN + 2 * RIGHT), run_time=2 ) hand.add_updater( lambda h: h.move_to(circle1[0].points[0], LEFT) ) for quat in [special_q, [1, 0, 0, 0], special_q]: self.play( m_tracker.set_value, special_q, *updates, run_time=run_time, ) self.wait() class MentionCommutativity(TeacherStudentsScene): def construct(self): kwargs = { "tex_to_color_map": { "q": MAROON_B, "p": YELLOW, "i": GREEN, "j": RED, "k": BLUE, } } general_eq = TexMobject("q \\cdot p \\ne p \\cdot q", **kwargs) general_eq.get_part_by_tex("\\ne").submobjects.reverse() ij_eq = TexMobject("i \\cdot j = k", **kwargs) ji_eq = TexMobject("j \\cdot i = -k", **kwargs) for mob in [general_eq, ij_eq, ji_eq]: mob.move_to(self.hold_up_spot, DOWN) words = TextMobject("Multiplication doesn't \\\\ commute") words.next_to(general_eq, UP, MED_LARGE_BUFF) words.shift_onto_screen() joke = TextMobject("Quaternions work from home") joke.scale(0.75) joke.to_corner(UL, MED_SMALL_BUFF) self.play( FadeInFromDown(general_eq), self.teacher.change, "raise_right_hand", self.get_student_changes("erm", "confused", "sassy") ) self.play(FadeInFrom(words, RIGHT)) self.wait(2) self.play( ReplacementTransform(words, joke), general_eq.shift, UP, FadeInFromDown(ij_eq), self.get_student_changes(*["pondering"] * 3) ) self.look_at(self.screen) self.wait(3) self.play( FadeInFrom(ji_eq), LaggedStartMap( ApplyMethod, VGroup(ij_eq, general_eq), lambda m: (m.shift, UP), lag_ratio=0.8, ) ) self.look_at(self.screen) self.wait(5) class RubuiksCubeOperations(SpecialThreeDScene): def construct(self): self.set_camera_orientation(**self.get_default_camera_position()) self.begin_ambient_camera_rotation() cube = RubiksCube() cube.shift(2.5 * RIGHT) cube2 = cube.copy() self.add(cube) self.play( Rotate(cube.get_face(RIGHT), 90 * DEGREES, RIGHT), run_time=2 ) self.play( Rotate(cube.get_face(DOWN), 90 * DEGREES, UP), run_time=2 ) self.wait() self.play( cube.shift, 5 * LEFT, FadeIn(cube2) ) self.play( Rotate(cube2.get_face(DOWN), 90 * DEGREES, UP), run_time=2 ) self.play( Rotate(cube2.get_face(RIGHT), 90 * DEGREES, RIGHT), run_time=2 ) self.wait(6) class RotationsOfCube(SpecialThreeDScene): def construct(self): self.set_camera_orientation(**self.get_default_camera_position()) self.begin_ambient_camera_rotation(0.0001) cube = RubiksCube() cube2 = cube.copy() axes = self.get_axes() axes.scale(0.75) label1 = TextMobject( "z-axis\\\\", "then x-axis" ) label2 = TextMobject( "x-axis\\\\", "then z-axis" ) for label in [label1, label2]: for part in label: part.add_background_rectangle() label.rotate(90 * DEGREES, RIGHT) label.move_to(3 * OUT + 0.5 * IN) self.add(axes, cube) self.play( Rotate(cube, 90 * DEGREES, OUT, run_time=2), FadeInFrom(label1[0], IN), ) self.play( Rotate(cube, 90 * DEGREES, RIGHT, run_time=2), FadeInFrom(label1[1], IN), ) self.wait() self.play( cube.shift, 5 * RIGHT, label1.shift, 5 * RIGHT, Write(cube2, run_time=1) ) self.play( Rotate(cube2, 90 * DEGREES, RIGHT, run_time=2), FadeInFrom(label2[0], IN), ) self.play( Rotate(cube2, 90 * DEGREES, OUT, run_time=2), FadeInFrom(label2[1], IN), ) self.wait(5) class OneFinalPoint(TeacherStudentsScene): def construct(self): self.teacher_says("One final point!") self.change_student_modes("happy", "tease", "thinking") self.wait(3) class MultiplicationFromTheRight(Scene): def construct(self): i, dot, j = product = TexMobject("i", "\\cdot", "j") product.set_height(1.5) product.to_edge(UP, buff=LARGE_BUFF) i.set_color(GREEN) j.set_color(RED) i_rect = SurroundingRectangle(i) j_rect = SurroundingRectangle(j) i_rect.match_height(j_rect, about_edge=UP, stretch=True) VGroup(i_rect, j_rect).set_color(WHITE) action_words = TextMobject("Think of as \\\\ an action") point_words = TextMobject("Think of as \\\\ a point") action_words.next_to(i_rect, LEFT) point_words.next_to(j_rect, RIGHT) arrow = Arrow( i.get_top(), j.get_top(), path_arc=-PI, ) arrow.set_stroke(width=2) self.add(product) self.play(ShowCreation(arrow)) self.play( FadeIn(i_rect), Write(action_words) ) self.wait() self.play( FadeIn(j_rect), Write(point_words) ) self.wait(2) self.play(arrow.flip) self.play(Swap(point_words, action_words)) class MultiplicationByJFromRight(ShowJMultiplication): def show_multiplication(self): self.set_camera_orientation(theta=-80 * DEGREES) q_tracker = self.q_tracker m_tracker = self.multiplier_tracker def normalize_tracker(t): t.set_value(normalize(t.get_value())) updates = [ UpdateFromFunc(tracker, normalize_tracker) for tracker in (q_tracker, m_tracker) ] run_time = self.run_time_per_rotation m_values = [[1, 0, 0, 0]] q_values = [[0, 1, 0, 0]] for values in m_values, q_values: for x in range(4): values.append( q_mult(values[-1], [0, 0, 1, 0]) ) for m_val, q_val in zip(m_values, q_values): self.play( m_tracker.set_value, m_val, q_tracker.set_value, q_val, *updates, run_time=run_time, ) self.wait(2) def get_projected_sphere(self, *args, **kwargs): kwargs["multiply_from_right"] = True return ShowJMultiplication.get_projected_sphere( self, *args, **kwargs ) def get_projected_circle(self, *args, **kwargs): kwargs["multiply_from_right"] = True return ShowJMultiplication.get_projected_circle( self, *args, **kwargs ) class HowQuaternionsRotate3dPoints(Scene): def construct(self): title = TextMobject( "Coming up:\\\\", "How quaternions act on 3d points" ) title.to_edge(UP) expression = TexMobject( "q", "\\cdot", "p", "\\cdot", "q^{-1}" ) expression.scale(2) expression.set_color_by_tex("q", PINK) expression.set_color_by_tex("p", YELLOW) right_arrow = Arrow( expression[0].get_top(), expression[2].get_top(), path_arc=-PI, color=WHITE, ) right_arrow.set_stroke(width=4) left_arrow = right_arrow.copy() left_arrow.flip(about_point=expression[2].get_top()) left_arrow.shift(SMALL_BUFF * RIGHT) self.add(title) self.play(Write(expression)) self.play(ShowCreation(right_arrow)) self.play(ShowCreation(left_arrow)) self.wait() class HoldUpQuanta(TeacherStudentsScene): def construct(self): logo = ImageMobject("Quanta logo") logo.set_width(6) logo.next_to(self.teacher, UR) logo.to_edge(RIGHT, buff=2) words = TextMobject("Associated quaternion post") words.to_edge(UP, buff=LARGE_BUFF) arrow = Arrow(logo.get_top(), words.get_bottom()) self.play( FadeInFromDown(logo), self.teacher.change, "raise_right_hand" ) self.change_all_student_modes("hooray") self.play( GrowArrow(arrow), GrowFromPoint(words, arrow.get_start()) ) self.wait(3) class ShareWithFriends(PiCreatureScene): def construct(self): pi1, pi2 = self.pi_creatures self.pi_creature_says( pi1, "Come learn about \\\\ quaternions!", bubble_kwargs={ "direction": LEFT, "width": 4, "height": 3, }, target_mode="hooray", look_at_arg=pi2.eyes, added_anims=[ ApplyMethod( pi2.change, "confused", run_time=1.5, rate_func=squish_rate_func(smooth, 0.3, 1) ) ] ) bubble = ThoughtBubble( direction=RIGHT, width=4, height=4, ) bubble.pin_to(pi2) bubble.write("What's that, some\\\\kind of particle?") bubble.resize_to_content() VGroup(bubble, bubble.content).shift_onto_screen(buff=SMALL_BUFF) self.play( pi2.look_at, pi1.eyes, pi1.look_at, pi2.eyes, ShowCreation(bubble), Write(bubble.content), ) self.wait(1) self.play( RemovePiCreatureBubble(pi1, target_mode="raise_right_hand") ) self.wait() time_words = TextMobject("Oh man...\\\\30 minutes") time_words.move_to(bubble.content) self.play( Animation(VectorizedPoint().next_to(pi1, UL, LARGE_BUFF)), pi2.change, "sad", FadeOutAndShift(bubble.content, DOWN), FadeInFromDown(time_words, DOWN), ) self.wait(7) def create_pi_creatures(self): pi1 = PiCreature(color=BLUE) pi1.to_edge(DOWN) pi1.flip() pi1.shift(2.5 * RIGHT) pi2 = PiCreature(color=RED) pi2.to_edge(DOWN) pi2.shift(2 * LEFT) return [pi1, pi2] class QuaternionEndscreen(PatreonEndScreen): CONFIG = { "specific_patrons": [ "Juan Benet", "Matt Russell", "soekul", "Desmos", "Burt Humburg", "Dinesh Dharme", "Scott Walter", "Brice Gower", "Peter Mcinerney", "brian tiger chow", "Joseph Kelly", "Roy Larson", "Andrew Sachs", "Hoàng Tùng Lâm", "Devin Scott", "Akash Kumar", "Arthur Zey", "David Kedmey", "Ali Yahya", "Mayank M. Mehrotra", "Lukas Biewald", "Yana Chernobilsky", "Kaustuv DeBiswas", "Yu Jun", "dave nicponski", "Jordan Scales", "Markus Persson", "Lukáš Nový", "Fela", "Randy C. Will", "Britt Selvitelle", "Jonathan Wilson", "Ryan Atallah", "Joseph John Cox", "Luc Ritchie", "Ryan Williams", "Michael Hardel", "Federico Lebron", "L0j1k", "Ayan Doss", "Dylan Houlihan", "Steven Soloway", "Art Ianuzzi", "Nate Heckmann", "Michael Faust", "Richard Comish", "Nero Li", "Valeriy Skobelev", "Adrian Robinson", "Solara570", "Peter Ehrnstrom", "Kai Siang Ang", "Alexis Olson", "Ludwig Schubert", "Omar Zrien", "Sindre Reino Trosterud", "Jeff Straathof", "Matt Langford", "Matt Roveto", "Magister Mugit", "Stevie Metke", "Cooper Jones", "James Hughes", "John V Wertheim", "Song Gao", "Richard Burgmann", "John Griffith", "Chris Connett", "Steven Tomlinson", "Jameel Syed", "Bong Choung", "Zhilong Yang", "Giovanni Filippi", "Eric Younge", "Prasant Jagannath", "Cody Brocious", "James H. Park", "Norton Wang", "Kevin Le", "Oliver Steele", "Yaw Etse", "Dave B", "Delton Ding", "Thomas Tarler", "1stViewMaths", "Jacob Magnuson", "Clark Gaebel", "Mathias Jansson", "David Clark", "Michael Gardner", "Mads Elvheim", "Awoo", "Dr David G. Stork", "Ted Suzman", "Linh Tran", "Andrew Busey", "John Haley", "Ankalagon", "Eric Lavault", "Boris Veselinovich", "Julian Pulgarin", "Jeff Linse", "Robert Teed", "Jason Hise", "Bernd Sing", "James Thornton", "Mustafa Mahdi", "Mathew Bramson", "Jerry Ling", "Rish Kundalia", "Achille Brighton", "Ripta Pasay", ], } class ThumbnailP1(RuleOfQuaternionMultiplication): CONFIG = { "three_d_axes_config": { "num_axis_pieces": 20, }, "unit_labels_scale_factor": 1.5, "quaternion": [1, 0, 0, 0], } def construct(self): self.setup_all_trackers() self.remove(self.pink_dot_label) q_tracker = self.q_tracker m_tracker = self.multiplier_tracker # quat = normalize([-0.5, 0.5, -0.5, 0.5]) quat = normalize(self.quaternion) m_tracker.set_value(quat) q_tracker.set_value(quat) proj_sphere = self.get_projected_sphere(0, solid=False) # self.specially_color_sphere(proj_sphere) proj_sphere.set_color_by_gradient( BLUE, YELLOW ) proj_sphere.set_stroke(WHITE) proj_sphere.set_fill(opacity=0.4) for i, face in enumerate(proj_sphere): alpha = i / len(proj_sphere) opacity = 0.7 * (1 - there_and_back(alpha)) face.set_fill(opacity=opacity) # unit_sphere = self.get_projected_sphere(0, quaternion=[1, 0, 0, 0], solid=False) # self.specially_color_sphere(unit_sphere) # unit_sphere.set_stroke(width=0) # proj_sphere.set_fill_by_checkerboard(BLUE_E, BLUE, opacity=0.8) for face in proj_sphere: face.points = face.points[::-1] max_r = np.max(np.apply_along_axis(get_norm, 1, face.points)) if max_r > 30: face.fade(1) for label in self.unit_labels: label.set_shade_in_3d(False) label.set_background_stroke(color=BLACK, width=2) self.add(proj_sphere) # self.add(unit_sphere) for mobject in self.mobjects: try: mobject.shift(IN) except ValueError: pass self.set_camera_orientation( phi=70 * DEGREES, theta=-110 * DEGREES, ) class ThumbnailP2(ThumbnailP1): CONFIG = { "quaternion": [0, 1, 0, 0], } class ThumbnailOverlay(Scene): def construct(self): title = TextMobject("Quaternions \\\\", "visualized") title.set_width(7) # title[1].scale(0.7, about_edge=UP) title.to_edge(UP, buff=MED_SMALL_BUFF) v_line = Line(DOWN, UP) v_line.set_height(FRAME_HEIGHT) title.set_background_stroke(color=BLACK, width=1) for part in (title[0][4:6], title[1][4:5]): rect = BackgroundRectangle(part) rect.set_fill(opacity=1) rect.stretch(0.9, 0) rect.stretch(1.1, 1) title.add_to_back(rect) # title.add_to_back(BackgroundRectangle(title[0])) arrow = Arrow(LEFT, RIGHT) arrow.scale(1.5) arrow.tip.scale(2) arrow.set_stroke(width=10) arrow.set_color(YELLOW) self.add(v_line) self.add(arrow) self.add(title)