From 62f0a20843d9269fe73a042712792bffef9456d6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:43:45 -0700 Subject: [PATCH 01/25] Dumb perspective hack --- camera/three_d_camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index fbb603b2..87eacac2 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -180,7 +180,8 @@ class ThreeDCamera(Camera): factor[lt0] = (distance / (distance - zs[lt0])) else: factor = (distance / (distance - zs)) - clip_in_place(factor, 0, 10**6) + factor[(distance - zs) < 0] = 10**6 + # clip_in_place(factor, 0, 10**6) points[:, i] *= factor points += frame_center return points From dd2ee72900c3020a09e5692b607afbd870f7f7e6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:44:10 -0700 Subject: [PATCH 02/25] Safer default for Mobject.apply_matrix --- mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobject/mobject.py b/mobject/mobject.py index 6b0c3f03..35dcf573 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -254,7 +254,7 @@ class Mobject(Container): def apply_matrix(self, matrix, **kwargs): # Default to applying matrix about the origin, not mobjects center - if len(kwargs) == 0: + if ("about_point" not in kwargs) and ("about_edge" not in kwargs): kwargs["about_point"] = ORIGIN full_matrix = np.identity(self.dim) matrix = np.array(matrix) From df9137b1ef0c250e770e91706ccaa3e798fe59db Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:44:32 -0700 Subject: [PATCH 03/25] ParametricSurface parts should remember their coordinates --- mobject/three_dimensions.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobject/three_dimensions.py b/mobject/three_dimensions.py index fac84959..c49405a0 100644 --- a/mobject/three_dimensions.py +++ b/mobject/three_dimensions.py @@ -32,6 +32,7 @@ class ParametricSurface(VGroup): "stroke_color": LIGHT_GREY, "stroke_width": 0.5, "should_make_jagged": False, + "pre_function_handle_to_anchor_scale_factor": 0.00001, } def __init__(self, func, **kwargs): @@ -70,6 +71,10 @@ class ParametricSurface(VGroup): faces.add(face) face.u_index = i face.v_index = j + face.u1 = u1 + face.u2 = u2 + face.v1 = v1 + face.v2 = v2 faces.set_fill( color=self.fill_color, opacity=self.fill_opacity From 6a225211a54029d40a0ca96d44a6177d48d003a7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:44:46 -0700 Subject: [PATCH 04/25] VMobject.set_shade_in_3d --- mobject/types/vectorized_mobject.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mobject/types/vectorized_mobject.py b/mobject/types/vectorized_mobject.py index 4a93ff65..61ec6dd8 100644 --- a/mobject/types/vectorized_mobject.py +++ b/mobject/types/vectorized_mobject.py @@ -308,6 +308,10 @@ class VMobject(Mobject): self.color_using_background_image(vmobject.get_background_image_file()) return self + def set_shade_in_3d(self, value=True): + for submob in self.get_family(): + submob.shade_in_3d = value + # Drawing def start_at(self, point): if len(self.points) == 0: From ce1eb4eb15a1c839735a8d8522a4c7258a38623d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:45:12 -0700 Subject: [PATCH 05/25] get_winding_number --- utils/space_ops.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/utils/space_ops.py b/utils/space_ops.py index 40026a1a..3b65b4cd 100644 --- a/utils/space_ops.py +++ b/utils/space_ops.py @@ -2,7 +2,10 @@ import numpy as np from constants import OUT from constants import RIGHT +from constants import PI +from constants import TAU from functools import reduce +from utils.iterables import adjacent_pairs # Matrix operations @@ -164,3 +167,12 @@ def line_intersection(line1, line2): x = det(d, x_diff) / div y = det(d, y_diff) / div return np.array([x, y, 0]) + + +def get_winding_number(points): + total_angle = 0 + for p1, p2 in adjacent_pairs(points): + d_angle = angle_of_vector(p2) - angle_of_vector(p1) + d_angle = ((d_angle + PI) % TAU) - PI + total_angle += d_angle + return total_angle / TAU From 8e163a7bcb4b76ea46d29d1b8ea0bad6ebeeb146 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 28 Aug 2018 09:45:57 -0700 Subject: [PATCH 06/25] Finished(?) 3d stereographic projection animations --- active_projects/quaternions.py | 581 +++++++++++++++++++++++++++++++-- 1 file changed, 553 insertions(+), 28 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 712d3efa..92fdef22 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -16,10 +16,12 @@ def get_three_d_scene_config(high_quality=True): # "tick_frequency": 0.5, "tick_frequency": 1, "numbers_with_elongated_ticks": [0, 1, 2], + "stroke_width": 2, } }, "sphere_config": { "radius": 2, + "resolution": (24, 48), } } lq_added_config = { @@ -30,7 +32,7 @@ def get_three_d_scene_config(high_quality=True): "num_axis_pieces": 1, }, "sphere_config": { - "resolution": (4, 12), + # "resolution": (4, 12), } } if high_quality: @@ -52,7 +54,7 @@ def q_mult(q1, q2): return np.array([w, x, y, z]) -def stereo_project_point(point, axis=0, r=1, max_norm=100): +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) @@ -223,6 +225,69 @@ class CheckeredCircle(Circle): self[i::n_colors].set_color(color) +class StereoProjectedSphere(Sphere): + CONFIG = { + "stereo_project_config": { + "axis": 2, + }, + "max_r": 20, + "max_center": 32, + "max_width": 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.handle_outer_patch() + 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 + ) + if np.any(np.abs(result) == np.inf): + return self.func(u + 0.001, v) + return result + + def fade_far_out_submobjects(self, max_center=None, max_width=None): + if max_center is None: + max_center = self.max_center + if max_width is None: + max_width = self.max_width + for submob in self.submobjects: + if get_norm(submob.get_center()) > max_center: + submob.fade(1) + if submob.get_width() > max_width: + submob.fade(1) + return self + + # def handle_outer_patch(self): + # submobs_around_origin = [] + # for submob in self.submobjects: + # anchors = submob.get_anchors() + # if np.all(np.apply_along_axis(get_norm, 1, anchors) > self.max_r): + # submob.points[:, :] = 0 + # winding_number = get_winding_number(anchors) + # if abs(winding_number) > 0.1: + # submobs_around_origin.append(submob) + # if submobs_around_origin: + # widths = [sm.get_width() for sm in submobs_around_origin] + # outer_patch = submobs_around_origin[np.argmax(widths)] + + # Abstract scenes class SpecialThreeDScene(ThreeDScene): CONFIG = { @@ -2433,7 +2498,7 @@ class SphereExamplePointsDecimal(Scene): class TwoDStereographicProjection(IntroduceFelix): CONFIG = { "camera_config": { - "exponential_projection": True, + "exponential_projection": False, }, "sphere_sample_point_u_range": np.arange( 0, PI, PI / 16, @@ -2441,29 +2506,53 @@ class TwoDStereographicProjection(IntroduceFelix): "sphere_sample_point_v_range": np.arange( 0, TAU, TAU / 16, ), + "n_sample_rotation_cycles": 2, } def construct(self): self.add_parts() - # self.talk_through_sphere() + self.talk_through_sphere() self.draw_projection_lines() self.show_point_at_infinity() self.show_a_few_rotations() - self.show_reference_circles() - def add_parts(self): + 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() - felix.shift(1.5 * DL) + c2p = axes.coords_to_point + 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), + ) + 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) + 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) + self.play( + felix.change, "pondering", sphere, + run_time=run_time, + ) def talk_through_sphere(self): point = VectorizedPoint(OUT) @@ -2524,10 +2613,12 @@ class TwoDStereographicProjection(IntroduceFelix): shade_in_3d=True ) - xy_plane = sphere.copy() + xy_plane = StereoProjectedSphere( + u_max=15 * PI / 16, + **self.sphere_config + ) xy_plane.set_fill(WHITE, 0.25) xy_plane.set_stroke(width=0) - self.project_mobject(xy_plane) point_mob = VectorizedPoint(2 * OUT) point_mob.add_updater( @@ -2562,6 +2653,7 @@ class TwoDStereographicProjection(IntroduceFelix): 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 @@ -2595,20 +2687,17 @@ class TwoDStereographicProjection(IntroduceFelix): dot.copy(), projection_dot )) + def get_point(): + return 2 * normalize(point_mob.get_location()) + dot.add_updater( - lambda d: d.become(get_sphere_dot( - point_mob.get_location() - )) + lambda d: d.become(get_sphere_dot(get_point())) ) line.add_updater( - lambda l: l.become(get_projection_line( - point_mob.get_location() - )) + lambda l: l.become(get_projection_line(get_point())) ) projection_dot.add_updater( - lambda d: d.become(get_projection_dot( - point_mob.get_location() - )) + lambda d: d.become(get_projection_dot(get_point())) ) self.play( @@ -2656,8 +2745,8 @@ class TwoDStereographicProjection(IntroduceFelix): u_max=PI / 2 + 0.01, resolution=(1, 24), ) - # for submob in circle.get_family(): - # submob.shade_in_3d = True + 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) @@ -2678,6 +2767,9 @@ class TwoDStereographicProjection(IntroduceFelix): south_hemisphere = self.get_sphere() n = len(south_hemisphere) south_hemisphere.remove(*south_hemisphere[:n // 2]) + south_hemisphere.remove( + *south_hemisphere[-3 * sphere.resolution[1] // 2:] + ) south_hemisphere.generate_target() self.project_mobject(south_hemisphere.target) south_hemisphere.set_fill(opacity=0.8) @@ -2697,20 +2789,135 @@ class TwoDStereographicProjection(IntroduceFelix): self.projected_sphere = VGroup( north_hemisphere, - circle, south_hemisphere, ) - + self.equator = circle self.point_mob = point_mob def show_point_at_infinity(self): - pass + points = list(compass_directions( + 12, start_vect=rotate_vector(RIGHT, 7.5 * DEGREES) + )) + points.pop(7) + points.pop(2) + arrows = VGroup(*[ + Arrow(6 * p, 9 * p) + for p in points + ]) + arrows.set_fill(YELLOW) + 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( + LaggedStart(GrowArrow, arrows), + LaggedStart(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, + submobject_mode="all_at_once", + run_time=3, + ) + ) def show_a_few_rotations(self): - pass + 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 show_reference_circles(self): - pass + 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(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.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): @@ -2718,3 +2925,321 @@ class TwoDStereographicProjection(IntroduceFelix): 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"), + ) + coords = [(1, 0), (-1, 0), (0, 1), (0, -1)] + vects = [DOWN, DOWN, RIGHT, RIGHT] + 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): + 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 + 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) + 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( + LaggedStart( + 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, DOWN, about_point=ORIGIN) + + projected = self.get_projected_circle(circle) + + self.move_camera(phi=130 * DEGREES) + self.play(ShowCreation(circle, run_time=2)) + self.move_camera(phi=60 * DEGREES) + 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, + use_rectangular_stem=False, + ) + 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=None)) + self.wait(3) + for x in range(2): + y_axis.pieces.set_stroke(width=2) + 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=2) + 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=2) + y_axis.pieces.set_stroke(width=2) + self.play( + Rotate(sphere, 90 * DEGREES, axis=OUT), + run_time=4, + ) + self.wait(2) + x_axis.pieces.set_stroke(width=0) + y_axis.pieces.set_stroke(width=0) + + # + 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) + + return circle + + def get_projected_circle(self, circle): + result = circle.deepcopy() + self.project_mobject(result) + 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 IntroduceQuaternions(Scene): + def construct(self): + self.compare_three_number_systems() + self.mention_four_perpendicular_axes() From 5e06869caffd1557ba10b4046d4ba8d0029df2b4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:08:25 -0700 Subject: [PATCH 07/25] Small fix to AnimationOnSurroundingRectangle --- animation/indication.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/animation/indication.py b/animation/indication.py index 3e77365d..a97e5f13 100644 --- a/animation/indication.py +++ b/animation/indication.py @@ -115,6 +115,8 @@ class AnimationOnSurroundingRectangle(AnimationGroup): rect = SurroundingRectangle( mobject, **self.surrounding_rectangle_config ) + if "surrounding_rectangle_config" in kwargs: + kwargs.pop("surrounding_rectangle_config") AnimationGroup.__init__(self, self.rect_to_animation(rect, **kwargs)) From bfa37e251f869816a1c81a84db498bfcbaad1faa Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:08:46 -0700 Subject: [PATCH 08/25] Got rid of ValueTracker special cases in camera --- camera/camera.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index 06c03443..9c090672 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -14,7 +14,6 @@ from mobject.types.image_mobject import AbstractImageMobject from mobject.mobject import Mobject from mobject.types.point_cloud_mobject import PMobject from mobject.types.vectorized_mobject import VMobject -from mobject.value_tracker import ValueTracker from utils.color import color_to_int_rgba from utils.color import rgb_to_hex from utils.config_ops import digest_config @@ -202,8 +201,7 @@ class Camera(object): def extract_mobject_family_members( self, mobjects, - only_those_with_points=False, - ignore_value_trackers=False): + only_those_with_points=False): if only_those_with_points: method = Mobject.family_members_with_points else: @@ -213,20 +211,16 @@ class Camera(object): method(m) for m in mobjects if not (isinstance(m, VMobject) and m.is_subpath) - if not (ignore_value_trackers and isinstance(m, ValueTracker)) ]) )) def get_mobjects_to_display( self, mobjects, include_submobjects=True, - ignore_value_trackers=True, excluded_mobjects=None): if include_submobjects: mobjects = self.extract_mobject_family_members( - mobjects, - only_those_with_points=True, - ignore_value_trackers=ignore_value_trackers, + mobjects, only_those_with_points=True, ) if excluded_mobjects: all_excluded = self.extract_mobject_family_members( From 0d95fe72347e3894fd0a8b82b65f3e391dedebfa Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:09:09 -0700 Subject: [PATCH 09/25] Change where z-sorting happens in 3d camera --- camera/three_d_camera.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index 87eacac2..a4f4fe3d 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -6,6 +6,7 @@ from constants import * from camera.camera import Camera from mobject.types.point_cloud_mobject import Point +from mobject.types.vectorized_mobject import VMobject from mobject.three_d_utils import get_3d_vmob_start_corner from mobject.three_d_utils import get_3d_vmob_start_corner_unit_normal from mobject.three_d_utils import get_3d_vmob_end_corner @@ -94,22 +95,27 @@ class ThreeDCamera(Camera): vmobject, vmobject.get_fill_rgbas() ) - def display_multiple_vectorized_mobjects(self, vmobjects, pixel_array): + def get_mobjects_to_display(self, *args, **kwargs): + mobjects = Camera.get_mobjects_to_display( + self, *args, **kwargs + ) rot_matrix = self.get_rotation_matrix() - def z_key(vmob): + def z_key(mob): + if not isinstance(mob, VMobject): + return np.inf + if not mob.shade_in_3d: + return np.inf # Assign a number to a three dimensional mobjects # based on how close it is to the camera - if vmob.shade_in_3d: - return np.dot( - vmob.get_center(), - rot_matrix.T - )[2] - else: - return np.inf - Camera.display_multiple_vectorized_mobjects( - self, sorted(vmobjects, key=z_key), pixel_array - ) + points = mob.points + if len(points) == 0: + return 0 + return np.dot( + center_of_mass(points), + rot_matrix.T + )[2] + return sorted(mobjects, key=z_key) def get_phi(self): return self.phi_tracker.get_value() From 7736e321a0653d59d321cc068c12e04bd6dbfae2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:09:34 -0700 Subject: [PATCH 10/25] + for 0 in Decimal number --- mobject/numbers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobject/numbers.py b/mobject/numbers.py index 56bc4fae..1b90d352 100644 --- a/mobject/numbers.py +++ b/mobject/numbers.py @@ -29,7 +29,10 @@ class DecimalNumber(VMobject): shows_zero = np.round(number, self.num_decimal_places) == 0 if num_string.startswith("-") and shows_zero: - num_string = num_string[1:] + if self.include_sign: + num_string = "+" + num_string[1:] + else: + num_string = num_string[1:] self.add(*[ SingleStringTexMobject(char, **kwargs) From 6b7ca78ad5e68836dce8a59d5a46c1bc92bc9337 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:09:57 -0700 Subject: [PATCH 11/25] Make fall back value in normalize an optional thing --- utils/space_ops.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/utils/space_ops.py b/utils/space_ops.py index 3b65b4cd..0bc6b744 100644 --- a/utils/space_ops.py +++ b/utils/space_ops.py @@ -100,12 +100,15 @@ def project_along_vector(point, vector): return np.dot(point, matrix.T) -def normalize(vect): +def normalize(vect, fall_back=None): norm = get_norm(vect) if norm > 0: return vect / norm else: - return np.zeros(len(vect)) + if fall_back is not None: + return fall_back + else: + return np.zeros(len(vect)) def cross(v1, v2): From e0d6bd5449ae6cc99ba5793851123d7048fe9264 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 29 Aug 2018 00:10:14 -0700 Subject: [PATCH 12/25] Up to partial progress on HypersphereStereographicProjection --- active_projects/quaternions.py | 1026 ++++++++++++++++++++++++++++++-- 1 file changed, 985 insertions(+), 41 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 92fdef22..3f3e8561 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -230,9 +230,10 @@ class StereoProjectedSphere(Sphere): "stereo_project_config": { "axis": 2, }, - "max_r": 20, - "max_center": 32, + "max_r": 32, "max_width": FRAME_WIDTH, + "max_height": FRAME_WIDTH, + "max_depth": FRAME_WIDTH, "radius": 1, } @@ -246,7 +247,6 @@ class StereoProjectedSphere(Sphere): ParametricSurface.__init__( self, self.post_projection_func, **kwargs ) - # self.handle_outer_patch() self.submobjects.sort( key=lambda m: -m.get_width() ) @@ -262,30 +262,128 @@ class StereoProjectedSphere(Sphere): return self.func(u + 0.001, v) return result - def fade_far_out_submobjects(self, max_center=None, max_width=None): - if max_center is None: - max_center = self.max_center - if max_width is None: - max_width = self.max_width + 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: - if get_norm(submob.get_center()) > max_center: - submob.fade(1) - if submob.get_width() > max_width: + 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 - # def handle_outer_patch(self): - # submobs_around_origin = [] - # for submob in self.submobjects: - # anchors = submob.get_anchors() - # if np.all(np.apply_along_axis(get_norm, 1, anchors) > self.max_r): - # submob.points[:, :] = 0 - # winding_number = get_winding_number(anchors) - # if abs(winding_number) > 0.1: - # submobs_around_origin.append(submob) - # if submobs_around_origin: - # widths = [sm.get_width() for sm in submobs_around_origin] - # outer_patch = submobs_around_origin[np.argmax(widths)] + +class StereoProjectedSphereFromHypersphere(StereoProjectedSphere): + CONFIG = { + "stereo_project_config": { + "axis": 0, + }, + "radius": 2, + } + + 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) + 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": FRAME_WIDTH, + "basis_vectors": [ + [1, 0, 0, 0], + [0, 1, 0, 0], + ] + } + + 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() + new_q = q_mult(q1, q2) + projected = stereo_project_point( + new_q, axis=0, r=self.radius, + ) + if np.any(projected == np.inf): + 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]) + if length > self.max_length: + self.remove(piece) + + +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 # Abstract scenes @@ -2642,6 +2740,7 @@ class TwoDStereographicProjection(IntroduceFelix): def get_sphere_dot(sphere_point): dot = Dot(shade_in_3d=True) dot.set_fill(PINK) + dot.insert_n_anchor_points(12) # Helps with flashing? dot.apply_matrix( z_to_vector(sphere_point), about_point=ORIGIN, @@ -2768,24 +2867,27 @@ class TwoDStereographicProjection(IntroduceFelix): n = len(south_hemisphere) south_hemisphere.remove(*south_hemisphere[:n // 2]) south_hemisphere.remove( - *south_hemisphere[-3 * sphere.resolution[1] // 2:] + *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( - FadeOut(xy_plane), LaggedStart(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.play(FadeOut(south_lines)) - self.wait(2) + self.wait(3) self.projected_sphere = VGroup( north_hemisphere, @@ -2796,15 +2898,16 @@ class TwoDStereographicProjection(IntroduceFelix): def show_point_at_infinity(self): points = list(compass_directions( - 12, start_vect=rotate_vector(RIGHT, 7.5 * DEGREES) + 12, start_vect=rotate_vector(RIGHT, 3.25 * DEGREES) )) points.pop(7) points.pop(2) arrows = VGroup(*[ - Arrow(6 * p, 9 * p) + Arrow(6 * p, 11 * p) for p in points ]) - arrows.set_fill(YELLOW) + arrows.set_fill(RED) + arrows.set_stroke(RED, 10) neg_ones = VGroup(*[ TexMobject("-1").next_to(arrow.get_start(), -p) for p, arrow in zip(points, arrows) @@ -2896,6 +2999,7 @@ class TwoDStereographicProjection(IntroduceFelix): ) 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}, @@ -3041,6 +3145,8 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): 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) @@ -3074,7 +3180,7 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): circle.copy(), projected, run_time=3 )) - self.axes.x_axis.pieces.set_stroke(width=0) + # self.axes.x_axis.pieces.set_stroke(width=0) self.wait(7) self.move_camera( phi=60 * DEGREES, @@ -3103,7 +3209,7 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): self.play(ReplacementTransform( circle.copy(), projected, run_time=3 )) - self.axes.y_axis.pieces.set_stroke(width=0) + # self.axes.y_axis.pieces.set_stroke(width=0) self.wait(3) self.one_j_circle = circle @@ -3115,13 +3221,12 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): 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, DOWN, about_point=ORIGIN) + circle.rotate(150 * DEGREES, UP, about_point=ORIGIN) projected = self.get_projected_circle(circle) - self.move_camera(phi=130 * DEGREES) self.play(ShowCreation(circle, run_time=2)) - self.move_camera(phi=60 * DEGREES) + self.wait() self.play(ReplacementTransform( circle.copy(), projected, run_time=2 @@ -3180,7 +3285,7 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): self.play(ShowCreation(arrows, run_time=3, rate_func=None)) self.wait(3) for x in range(2): - y_axis.pieces.set_stroke(width=2) + y_axis.pieces.set_stroke(width=1) self.play( Rotate(sphere, 90 * DEGREES, axis=UP), run_time=4, @@ -3193,7 +3298,7 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): self.play(Rotate(arrows, TAU / 4, axis=OUT)) self.wait(2) for x in range(2): - x_axis.pieces.set_stroke(width=2) + x_axis.pieces.set_stroke(width=1) self.play( Rotate(sphere, -90 * DEGREES, axis=RIGHT), run_time=4, @@ -3209,15 +3314,15 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): ) self.ambient_camera_rotation.rate = 0.01 for x in range(2): - x_axis.pieces.set_stroke(width=2) - y_axis.pieces.set_stroke(width=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) - x_axis.pieces.set_stroke(width=0) - y_axis.pieces.set_stroke(width=0) # def get_circle(self, *colors): @@ -3231,6 +3336,7 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): 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) @@ -3243,3 +3349,841 @@ 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(CircleThenFadeAround( + 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_submobjects(RIGHT, buff=LARGE_BUFF) + three_axes.next_to(number, DOWN, LARGE_BUFF) + + self.play(LaggedStart(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( + LaggedStart(GrowFromCenter, braces), + LaggedStart( + 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_submobjects(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_submobjects( + 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_submobjects(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(LaggedStart(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 = YELLOW + q2_color = PINK + + 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, + CircleThenFadeAround(rotate_words), + ) + self.play(LaggedStart( + 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_submobjects(RIGHT, buff=LARGE_BUFF) + for y in range(2) + ]).arrange_submobjects(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, + } + + 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_submobjects(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) + + def get_pq_point(): + return self.project_quaternion(q_tracker.get_value()) + + 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) + if norm > dot_radius: + point *= (norm - dot_radius) / norm + 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) + + sample_values = [ + [0, 1, 0, 0], + [-1, 1, 0, 0], + [0, 0, 1, 1], + [0, 1, -1, 1], + ] + for value in 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, + ) + + 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.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) + + 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(LaggedStart( + 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]) + self.wait(2) + self.set_quat([4, 0, -1, -1]) + self.wait(2) + # Negative real part + self.set_quat([-1, 1, 2, 0]) + 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([-200, 1, 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(10) + self.play(FadeOut(brace), FadeOut(words)) + self.remove_fixed_in_frame_mobjects(brace, words) + + 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$" + ) + circle_words.to_corner(UL) + sphere_1ij_words = TextMobject( + "Sphere through\\\\", "$1, i, j, -1, -i, -j$" + ) + sphere_1ij_words.to_corner(UL) + self.add_fixed_in_frame_mobjects(circle_words, sphere_1ij_words) + + self.play( + ShowCreation(circle), + Write(circle_words), + ) + self.set_quat([0, 1, 0, 0]) + self.set_quat([1, 0, 0, 0]) + self.play( + FadeOutAndShift(circle_words, DOWN), + FadeInFromDown(sphere_1ij_words) + ) + self.add(sphere_ijk) + self.play(Write(sphere_1ij)) + # sphere.set_fill_by_checkerboard( + # YELLOW_E, interpolate_color(YELLOW_E, BLACK, 0.5), + # opacity=1 + # ) + self.wait(3) + + + # + 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 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.q_tracker.get_value() + 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 + ) + return sphere From 8a1f121207f4dc4c60e9b91caaa525729b5e5e2c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:23:26 -0700 Subject: [PATCH 13/25] Don't even try to display points with nan or infinity --- camera/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/camera/camera.py b/camera/camera.py index 9c090672..a06791a3 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -341,6 +341,8 @@ class Camera(object): points = self.transform_points_pre_display( vmob, vmob.points ) + if np.any(np.isnan(points)) or np.any(points == np.inf): + points = np.zeros((1, 3)) ctx.new_sub_path() ctx.move_to(*points[0][:2]) for triplet in zip(points[1::3], points[2::3], points[3::3]): From 5782c13813fcb9ff0829fd511408a388eb780433 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:23:48 -0700 Subject: [PATCH 14/25] Back to old way of z-indexing --- camera/three_d_camera.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index a4f4fe3d..4957c6c2 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -102,17 +102,12 @@ class ThreeDCamera(Camera): rot_matrix = self.get_rotation_matrix() def z_key(mob): - if not isinstance(mob, VMobject): - return np.inf - if not mob.shade_in_3d: + if not (hasattr(mob, "shade_in_3d") and mob.shade_in_3d): return np.inf # Assign a number to a three dimensional mobjects # based on how close it is to the camera - points = mob.points - if len(points) == 0: - return 0 return np.dot( - center_of_mass(points), + mob.get_center(), rot_matrix.T )[2] return sorted(mobjects, key=z_key) From cab3b1b3c6a556063448902e7380a4645c2b2361 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:24:05 -0700 Subject: [PATCH 15/25] Reverse arrow rectangle orientation --- mobject/geometry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobject/geometry.py b/mobject/geometry.py index 8dbb61a2..78d06ffc 100644 --- a/mobject/geometry.py +++ b/mobject/geometry.py @@ -554,10 +554,10 @@ class Arrow(Line): self.second_tip.get_anchors()[1:] ) self.rect.set_points_as_corners([ - tip_base + perp_vect * width / 2, - start + perp_vect * width / 2, - start - perp_vect * width / 2, tip_base - perp_vect * width / 2, + start - perp_vect * width / 2, + start + perp_vect * width / 2, + tip_base + perp_vect * width / 2, ]) self.stem = self.rect # Alternate name return self From 9a8575401456910e387d650a2796b73848234524 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:24:25 -0700 Subject: [PATCH 16/25] Better Mobject.become --- mobject/mobject.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mobject/mobject.py b/mobject/mobject.py index 35dcf573..93fae106 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -170,8 +170,11 @@ class Mobject(Container): def get_updaters(self): return self.updaters - def add_updater(self, update_function, call_updater=True): - self.updaters.append(update_function) + def add_updater(self, update_function, index=None, call_updater=True): + if index is None: + self.updaters.append(update_function) + else: + self.updaters.insert(index, update_function) if call_updater: self.update(0) return self @@ -982,7 +985,8 @@ class Mobject(Container): """ self.align_data(mobject) for sm1, sm2 in zip(self.get_family(), mobject.get_family()): - sm1.interpolate(sm1, sm2, 1) + sm1.points = np.array(sm2.points) + sm1.interpolate_color(sm1, sm2, 1) return self From 289d822a9250f1b23524c2e8789bddeb65d6e3e7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:24:40 -0700 Subject: [PATCH 17/25] Tiny bug fix --- mobject/numbers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mobject/numbers.py b/mobject/numbers.py index 1b90d352..e7724f45 100644 --- a/mobject/numbers.py +++ b/mobject/numbers.py @@ -117,6 +117,7 @@ class DecimalNumber(VMobject): # of animated mobjects mob.points[:] = 0 self.number = number + return self def get_value(self): return self.number From 1b0056f05d431877b7ea56770d3b3dd2f7ca5114 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:24:57 -0700 Subject: [PATCH 18/25] Make sure normalize can take in a list --- utils/space_ops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils/space_ops.py b/utils/space_ops.py index 0bc6b744..07d8ea49 100644 --- a/utils/space_ops.py +++ b/utils/space_ops.py @@ -103,7 +103,7 @@ def project_along_vector(point, vector): def normalize(vect, fall_back=None): norm = get_norm(vect) if norm > 0: - return vect / norm + return np.array(vect) / norm else: if fall_back is not None: return fall_back From acf23a5e9d34ab24385c7083d475e0104b2dc2fe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:25:21 -0700 Subject: [PATCH 19/25] Cube rotations, intro scenes and more --- active_projects/quaternions.py | 1495 ++++++++++++++++++++++++++++++-- 1 file changed, 1414 insertions(+), 81 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 3f3e8561..53469d01 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -258,8 +258,9 @@ class StereoProjectedSphere(Sphere): result = stereo_project_point( rot_point, **self.stereo_project_config ) - if np.any(np.abs(result) == np.inf): - return self.func(u + 0.001, v) + 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): @@ -314,7 +315,8 @@ class StereoProjectedCircleFromHypersphere(CheckeredCircle): CONFIG = { "n_pieces": 48, "radius": 2, - "max_length": FRAME_WIDTH, + "max_length": 2 * FRAME_WIDTH, + "max_r": 20, "basis_vectors": [ [1, 0, 0, 0], [0, 1, 0, 0], @@ -345,7 +347,7 @@ class StereoProjectedCircleFromHypersphere(CheckeredCircle): projected = stereo_project_point( new_q, axis=0, r=self.radius, ) - if np.any(projected == np.inf): + 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:] @@ -353,8 +355,12 @@ class StereoProjectedCircleFromHypersphere(CheckeredCircle): def remove_large_pieces(self): for piece in self: length = get_norm(piece.points[0] - piece.points[-1]) - if length > self.max_length: - self.remove(piece) + violations = [ + length > self.max_length, + get_norm(piece.get_center()) > self.max_r, + ] + if any(violations): + piece.fade(1) class QuaternionTracker(ValueTracker): @@ -386,6 +392,59 @@ class QuaternionTracker(ValueTracker): return self +class RubiksCube(VGroup): + CONFIG = { + "colors": [ + "#C41E3A", "#009E60", "#0051BA", + "#FF5800", "#FFD500", "#FFFFFF" + ], + } + + 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_submobjects_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_submobjects(lambda p: np.dot(p, vect)) + return self[-(12 + 9):] + + # Abstract scenes class SpecialThreeDScene(ThreeDScene): CONFIG = { @@ -433,41 +492,384 @@ class SpecialThreeDScene(ThreeDScene): # Animated scenes -class Test(SpecialThreeDScene): - CONFIG = { - "sphere_config": {} - } +class ManyNumberSystems(Scene): def construct(self): - sphere = self.get_sphere() - # sphere.set_fill(opacity=0.5) - axes = self.get_axes() - cube = Cube() - cube.set_depth(4) - cube.set_fill(BLUE_E, opacity=1) + # 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", [5, 0, 0], BLUE, "2 + i"), + ("Quaternions", [3, 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] - sphere_shadow = sphere.deepcopy() - sphere_shadow.add_updater( - lambda ss: ss.become( - stereo_project(sphere.deepcopy()) - ).set_fill(BLUE_E, 0.5) - ) + 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(axes) - self.add(sphere) - self.add(sphere_shadow) - # self.add(cube) - self.move_camera( - phi=70 * DEGREES, - theta=-45 * DEGREES, - run_time=0 - ) - # self.begin_ambient_camera_rotation() + self.add(title) + self.play(LaggedStart( + FadeInFromLarge, systems, + lambda m: (m, 4) + )) + self.wait() + self.add(number_line, plane, systems) self.play( - Rotate(sphere, 90 * DEGREES, axis=UP), - run_time=3, + 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(LaggedStart( + 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() + randy.next_to(plane, RIGHT) + 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 TODOInsertIntroduceThreeDNumbers(TODOStub): + CONFIG = {"message": "Insert IntroduceThreeDNumbers"} + + +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( + LaggedStart( + 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), + ), + LaggedStart( + 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 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 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) + + +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_submobjects( + 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): @@ -3690,8 +4092,8 @@ class ShowComplexMagnitude(ShowComplexMultiplicationExamples): class BreakUpQuaternionMultiplicationInParts(Scene): def construct(self): - q1_color = YELLOW - q2_color = PINK + q1_color = MAROON_B + q2_color = YELLOW product = TexMobject( "q_1", "\\cdot", "q_2", "=", @@ -3870,8 +4272,14 @@ class SphereProjectionsWrapper(Scene): class HypersphereStereographicProjection(SpecialThreeDScene): CONFIG = { - "fancy_dot": False, - # "fancy_dot": True, + # "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], + ] } def construct(self): @@ -3879,8 +4287,8 @@ class HypersphereStereographicProjection(SpecialThreeDScene): self.introduce_quaternion_label() self.show_one() self.show_unit_sphere() - # self.show_quaternions_with_nonzero_real_part() - # self.emphasize_only_units() + self.show_quaternions_with_nonzero_real_part() + self.emphasize_only_units() self.show_reference_spheres() def setup_axes(self): @@ -3922,7 +4330,10 @@ class HypersphereStereographicProjection(SpecialThreeDScene): label.add_updater(update_label) def get_pq_point(): - return self.project_quaternion(q_tracker.get_value()) + 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())) @@ -3953,13 +4364,7 @@ class HypersphereStereographicProjection(SpecialThreeDScene): self.play(FadeOut(rect)) self.remove_fixed_orientation_mobjects(rect) - sample_values = [ - [0, 1, 0, 0], - [-1, 1, 0, 0], - [0, 0, 1, 1], - [0, 1, -1, 1], - ] - for value in sample_values: + for value in self.initial_quaternion_sample_values: self.set_quat(value) self.wait() @@ -3985,25 +4390,11 @@ class HypersphereStereographicProjection(SpecialThreeDScene): sphere = self.sphere = self.get_projected_sphere( quaternion=[1, 0, 0, 0], null_axis=0, solid=False, + stroke_width=0.5 ) - - 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.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) + 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) @@ -4052,7 +4443,7 @@ class HypersphereStereographicProjection(SpecialThreeDScene): self.wait(2) self.set_quat([-1, 1, 0, 0]) self.move_camera(theta=-160 * DEGREES, run_time=3) - self.set_quat([-200, 1, 0, 0]) + self.set_quat([-1, 0.001, 0, 0]) self.wait(2) def emphasize_only_units(self): @@ -4074,10 +4465,13 @@ class HypersphereStereographicProjection(SpecialThreeDScene): self.set_quat([1, 1, 1, 1]) self.wait(2) self.set_quat([1, 1, -1, 1]) - self.wait(10) + 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( @@ -4088,20 +4482,26 @@ class HypersphereStereographicProjection(SpecialThreeDScene): ] ) 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_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$" ) - circle_words.to_corner(UL) sphere_1ij_words = TextMobject( "Sphere through\\\\", "$1, i, j, -1, -i, -j$" ) - sphere_1ij_words.to_corner(UL) - self.add_fixed_in_frame_mobjects(circle_words, sphere_1ij_words) + 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), @@ -4109,18 +4509,62 @@ class HypersphereStereographicProjection(SpecialThreeDScene): ) 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) + FadeInFromDown(sphere_1ij_words), + FadeOut(circle), + sphere_ijk.set_stroke, {"width": 0.0} ) - self.add(sphere_ijk) self.play(Write(sphere_1ij)) - # sphere.set_fill_by_checkerboard( - # YELLOW_E, interpolate_color(YELLOW_E, BLACK, 0.5), - # opacity=1 - # ) - self.wait(3) + 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): @@ -4141,6 +4585,26 @@ class HypersphereStereographicProjection(SpecialThreeDScene): 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.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 = [] @@ -4152,7 +4616,7 @@ class HypersphereStereographicProjection(SpecialThreeDScene): def get_projected_sphere(self, null_axis, quaternion=None, solid=True, **kwargs): if quaternion is None: - quaternion = self.q_tracker.get_value() + quaternion = self.get_multiplier() axes_to_color = { 0: interpolate_color(YELLOW, BLACK, 0.5), 1: GREEN_E, @@ -4186,4 +4650,873 @@ class HypersphereStereographicProjection(SpecialThreeDScene): 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): + 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) + submob.set_fill(rgb_to_hex(rgb)) + return sphere + + +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", "=", "1", **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_submobjects( + 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.set_shade_in_3d(True) + one_label.rotate(90 * DEGREES, RIGHT) + one_label.next_to(ORIGIN, IN + RIGHT, SMALL_BUFF) + self.add(labels, one_label) + + 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(LaggedStart(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, + use_rectangular_stem=False, + buff=SMALL_BUFF, + ) + neg_one_arrow = Arrow( + ORIGIN, 5.5 * RIGHT + UP, + path_arc=-30 * DEGREES, + use_rectangular_stem=False, + buff=SMALL_BUFF, + ) + neg_i_arrow = Arrow( + 4.5 * LEFT + 1.5 * UP, ORIGIN, + path_arc=-30 * DEGREES, + use_rectangular_stem=False, + 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", "+", "x", "i", "+", "y", "j", "+", "z", "k", + "\\right)" + ) + top_product.to_edge(UP) + self.add(top_product) + bottom_product = TexMobject( + "q", "w", + "+", "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 ["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 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.1, + ) + 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", "=", "1", **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_submobjects( + 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=-30 * 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 + self.move_camera(theta=-110 * DEGREES) + 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(ContinualUpdate(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 = circle1.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), + LaggedStart( + 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) + # for square in cube: + # square.set_sheen(0.2, DOWN + LEFT + IN) + 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) From a102cc2cea0cd630f8e4c6687308c954a8ac24ad Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 30 Aug 2018 14:25:30 -0700 Subject: [PATCH 20/25] Cube rotations, intro scenes and more --- active_projects/quaternions.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 53469d01..303b8a98 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -5444,8 +5444,6 @@ class RubuiksCubeOperations(SpecialThreeDScene): self.begin_ambient_camera_rotation() cube = RubiksCube() cube.shift(2.5 * RIGHT) - # for square in cube: - # square.set_sheen(0.2, DOWN + LEFT + IN) cube2 = cube.copy() self.add(cube) From 6c590b6de4fb19efab8facaf595d00e7eb27a91d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 2 Sep 2018 21:55:53 -0700 Subject: [PATCH 21/25] Fixed Endscreen --- for_3b1b_videos/common_scenes.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/for_3b1b_videos/common_scenes.py b/for_3b1b_videos/common_scenes.py index ab3f1d66..bdd84192 100644 --- a/for_3b1b_videos/common_scenes.py +++ b/for_3b1b_videos/common_scenes.py @@ -193,14 +193,14 @@ class PatreonEndScreen(PatreonThanks): black_rect = Rectangle( fill_color=BLACK, fill_opacity=1, - stroke_width=0, + stroke_width=3, + stroke_color=BLACK, width=FRAME_WIDTH, height=0.6 * FRAME_HEIGHT, ) black_rect.to_edge(UP, buff=0) line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT) line.move_to(ORIGIN) - self.add(line) thanks = TextMobject("Funded by the community, with special thanks to:") thanks.scale(0.9) @@ -210,7 +210,6 @@ class PatreonEndScreen(PatreonThanks): underline.set_width(thanks.get_width() + MED_SMALL_BUFF) underline.next_to(thanks, DOWN, SMALL_BUFF) thanks.add(underline) - self.add(thanks) patrons = VGroup(*list(map(TextMobject, self.specific_patrons))) patrons.scale(self.patron_scale_val) @@ -234,12 +233,10 @@ class PatreonEndScreen(PatreonThanks): thanks.align_to(columns, alignment_vect=RIGHT) + self.add(columns, black_rect, line, thanks) self.play( columns.move_to, 2 * DOWN, DOWN, columns.to_edge, RIGHT, - Animation(black_rect), - Animation(line), - Animation(thanks), rate_func=None, run_time=self.run_time, ) From e967e472f35e2ed1bf5c147e57f433c6608e2e23 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 2 Sep 2018 21:56:10 -0700 Subject: [PATCH 22/25] Quaternion animations up to Patron draft --- active_projects/quaternions.py | 292 ++++++++++++++++++++++++++++++--- 1 file changed, 266 insertions(+), 26 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 303b8a98..186e6697 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -198,14 +198,15 @@ class PushPin(SVGMobject): class Hand(SVGMobject): CONFIG = { "file_name": "pinch_hand", - "height": 0.4, + "height": 0.5, "sheen": 0.2, - "fill_color": GREY, + "fill_color": ORANGE, } def __init__(self, **kwargs): SVGMobject.__init__(self, **kwargs) - self.add(VectorizedPoint().next_to(self, UP, buff=0.15)) + # self.rotate(30 * DEGREES) + self.add(VectorizedPoint().next_to(self, UP, buff=0.17)) class CheckeredCircle(Circle): @@ -493,14 +494,15 @@ class SpecialThreeDScene(ThreeDScene): # Animated 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", [5, 0, 0], BLUE, "2 + i"), - ("Quaternions", [3, 2, 0], PINK, "2 + 7i + 1j + 8k"), + ("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"), @@ -528,8 +530,9 @@ class ManyNumberSystems(Scene): C_example_dot = Dot(plane.coords_to_point(2, 1)) self.add(title) + self.play(FadeInFromLarge(H_label)) self.play(LaggedStart( - FadeInFromLarge, systems, + FadeInFromLarge, VGroup(*it.chain(systems[:2], systems[3:])), lambda m: (m, 4) )) self.wait() @@ -560,8 +563,9 @@ class ManyNumberSystems(Scene): lambda m: (m.shift, 0.5 * FRAME_WIDTH * LEFT), lag_ratio=0.8, )) - randy = Randolph() + randy = Randolph(height=1.5) randy.next_to(plane, RIGHT) + randy.to_edge(DOWN) self.play( randy.change, "maybe", H_label, VFadeIn(randy), @@ -606,10 +610,6 @@ class RotationsIn3d(SpecialThreeDScene): self.wait() -class TODOInsertIntroduceThreeDNumbers(TODOStub): - CONFIG = {"message": "Insert IntroduceThreeDNumbers"} - - class IntroduceHamilton(Scene): def construct(self): hamilton = ImageMobject("Hamilton", height=6) @@ -709,6 +709,41 @@ class IntroduceHamilton(Scene): self.wait() +class QuaternionHistory(Scene): + def construct(self): + self.show_dot_product_and_cross_product() # With date + self.teaching_students_quaternions() + self.show_anti_quaternion_quote() + self.mad_hatter() + self.vestiges_in_modern_notation() + + + def show_dot_product_and_cross_product(self): + date = TexMobject("1843") + date.to_edge(UP) + + v1, v2 = [ + Matrix([ + ["{}_{}".format(c, i)] + for c in "xyz" + ]) + for i in (1, 2) + ] + + def teaching_students_quaternions(self): + pass + + def show_anti_quaternion_quote(self): + pass + + def mad_hatter(self): + pass + + def vestiges_in_modern_notation(self): + pass + + + class QuaternionRotationOverlay(Scene): def construct(self): equations = VGroup( @@ -833,6 +868,8 @@ class HereWeTackle4d(TeacherStudentsScene): 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): @@ -1428,7 +1465,13 @@ class TextbookQuaternionDefinition(TeacherStudentsScene): 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): @@ -1938,9 +1981,12 @@ class OneDegreeOfFreedomForRotation(Scene): arc.add_updater(lambda m: m.become(get_arc())) self.add(circle, r_line, angle_label, arc) - angles = np.random.uniform(-TAU, TAU, 10) + angles = IntroduceStereographicProjection.CONFIG.get( + "example_angles" + ) for angle in angles: - self.play(Rotate(circle, angle, run_time=2)) + self.play(Rotate(circle, angle, run_time=4)) + self.wait() class StereographicProjectionTitle(Scene): @@ -2184,7 +2230,10 @@ class IntroduceStereographicProjection(MovingCameraScene): cross = Cross(dot) cross.scale(2) label = Integer(value) - label.next_to(dot, UR, SMALL_BUFF) + 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) @@ -2611,6 +2660,15 @@ class ShowRotationUnderStereographicProjection(IntroduceStereographicProjection) 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) @@ -3140,9 +3198,11 @@ class TwoDStereographicProjection(IntroduceFelix): return line def get_sphere_dot(sphere_point): - dot = Dot(shade_in_3d=True) + dot = Dot() + dot.rotate(90 * DEGREES) + dot.set_shade_in_3d(True) dot.set_fill(PINK) - dot.insert_n_anchor_points(12) # Helps with flashing? + dot.shift(OUT) dot.apply_matrix( z_to_vector(sphere_point), about_point=ORIGIN, @@ -3309,7 +3369,7 @@ class TwoDStereographicProjection(IntroduceFelix): for p in points ]) arrows.set_fill(RED) - arrows.set_stroke(RED, 10) + arrows.set_stroke(RED, 5) neg_ones = VGroup(*[ TexMobject("-1").next_to(arrow.get_start(), -p) for p, arrow in zip(points, arrows) @@ -3372,7 +3432,7 @@ class TwoDStereographicProjection(IntroduceFelix): **self.sphere_config, ) result.set_fill(opacity=0.2) - result.fade_far_out_submobjects(32) + result.fade_far_out_submobjects(max_r=32) for submob in result: if submob.get_center()[1] < -11: submob.fade(1) @@ -4734,7 +4794,7 @@ class RuleOfQuaternionMultiplicationOverlay(Scene): } } i_products = VGroup( - TexMobject("i", "\\cdot", "1", "=", "1", **kwargs), + TexMobject("i", "\\cdot", "1", "=", "i", **kwargs), TexMobject("i", "\\cdot", "i", "=", "-1", **kwargs), TexMobject("i", "\\cdot", "j", "=", "k", **kwargs), TexMobject("i", "\\cdot", "k", "=", "-j", **kwargs), @@ -4993,13 +5053,13 @@ class ShowDistributionOfI(TeacherStudentsScene): } top_product = TexMobject( "q", "\\cdot", "\\left(", - "w", "+", "x", "i", "+", "y", "j", "+", "z", "k", + "w", "1", "+", "x", "i", "+", "y", "j", "+", "z", "k", "\\right)" ) top_product.to_edge(UP) self.add(top_product) bottom_product = TexMobject( - "q", "w", + "w", "q", "\\cdot", "1", "+", "x", "q", "\\cdot", "i", "+", "y", "q", "\\cdot", "j", "+", "z", "q", "\\cdot", "k", @@ -5028,7 +5088,7 @@ class ShowDistributionOfI(TeacherStudentsScene): bottom_product.get_parts_by_tex(tex, substring=False), run_time=2 ) - for tex in ["w", "x", "i", "y", "j", "z", "k", "+"] + for tex in ["1", "w", "x", "i", "y", "j", "z", "k", "+"] ] ) self.play(*[ @@ -5043,6 +5103,39 @@ class ShowDistributionOfI(TeacherStudentsScene): 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, @@ -5176,7 +5269,7 @@ class JMultiplicationChart(Scene): } } j_products = VGroup( - TexMobject("j", "\\cdot", "1", "=", "1", **kwargs), + TexMobject("j", "\\cdot", "1", "=", "j", **kwargs), TexMobject("j", "\\cdot", "j", "=", "-1", **kwargs), TexMobject("j", "\\cdot", "i", "=", "-k", **kwargs), TexMobject("j", "\\cdot", "k", "=", "i", **kwargs), @@ -5240,7 +5333,7 @@ class ShowJMultiplication(ShowMultiplicationBy135Example): ) def show_multiplication(self): - self.set_camera_orientation(theta=-30 * DEGREES) + self.set_camera_orientation(theta=-80 * DEGREES) q_tracker = self.q_tracker m_tracker = self.multiplier_tracker @@ -5270,7 +5363,9 @@ class ShowJMultiplication(ShowMultiplicationBy135Example): self.wait(2) # Show ik circle - self.move_camera(theta=-110 * DEGREES) + 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() @@ -5342,7 +5437,7 @@ class ShowArbitraryMultiplication(ShowMultiplicationBy135Example): circle1, circle2 = self.circle1, self.circle2 for circle in [circle1, circle2]: - circle.tucked_away_updaters = circle1.updaters + circle.tucked_away_updaters = circle.updaters circle.clear_updaters() self.remove(circle) @@ -5518,3 +5613,148 @@ class RotationsOfCube(SpecialThreeDScene): FadeInFrom(label2[1], IN), ) self.wait(5) + + +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", + "1st", + "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 Thumbnail(RuleOfQuaternionMultiplication): + CONFIG = { + "three_d_axes_config": { + "num_axis_pieces": 20, + } + } + + def construct(self): + self.setup_all_trackers() + quat = normalize([-0.5, 0.5, -0.5, 0.5]) + self.multiplier_tracker.set_value(quat) + self.q_tracker.set_value(quat) + sphere = self.get_projected_sphere(0, solid=False) + # self.specially_color_sphere(sphere) + # sphere.set_fill(opacity=0.5) + sphere.set_fill_by_checkerboard(BLUE_E, BLUE, opacity=0.8) + for face in sphere: + face.points = face.points[::-1] + + self.set_camera_orientation( + phi=70 * DEGREES, + theta=-110 * DEGREES, + ) + self.add(sphere) From 405d566aa93eae48ccc26787576e63a6153e9d46 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 4 Sep 2018 16:14:11 -0700 Subject: [PATCH 23/25] Bug fix to svg reading --- mobject/svg/svg_mobject.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/mobject/svg/svg_mobject.py b/mobject/svg/svg_mobject.py index 8ca662d5..ba2d9b19 100644 --- a/mobject/svg/svg_mobject.py +++ b/mobject/svg/svg_mobject.py @@ -128,6 +128,13 @@ class SVGMobject(VMobject): self.ref_to_element[ref] ) + def attribute_to_float(self, attr): + stripped_attr = "".join([ + char for char in attr + if char in string.digits + "." + "-" + ]) + return float(stripped_attr) + def polygon_to_mobject(self, polygon_element): # TODO, This seems hacky... path_string = polygon_element.getAttribute("points") @@ -140,7 +147,9 @@ class SVGMobject(VMobject): def circle_to_mobject(self, circle_element): x, y, r = [ - float(circle_element.getAttribute(key)) + self.attribute_to_float( + circle_element.getAttribute(key) + ) if circle_element.hasAttribute(key) else 0.0 for key in ("cx", "cy", "r") @@ -149,7 +158,9 @@ class SVGMobject(VMobject): def ellipse_to_mobject(self, circle_element): x, y, rx, ry = [ - float(circle_element.getAttribute(key)) + self.attribute_to_float( + circle_element.getAttribute(key) + ) if circle_element.hasAttribute(key) else 0.0 for key in ("cx", "cy", "rx", "ry") @@ -183,8 +194,12 @@ class SVGMobject(VMobject): if corner_radius == 0: mob = Rectangle( - width=float(rect_element.getAttribute("width")), - height=float(rect_element.getAttribute("height")), + width=self.attribute_to_float( + rect_element.getAttribute("width") + ), + height=self.attribute_to_float( + rect_element.getAttribute("height") + ), stroke_width=stroke_width, stroke_color=stroke_color, fill_color=fill_color, @@ -192,8 +207,12 @@ class SVGMobject(VMobject): ) else: mob = RoundedRectangle( - width=float(rect_element.getAttribute("width")), - height=float(rect_element.getAttribute("height")), + width=self.attribute_to_float( + rect_element.getAttribute("width") + ), + height=self.attribute_to_float( + rect_element.getAttribute("height") + ), stroke_width=stroke_width, stroke_color=stroke_color, fill_color=fill_color, @@ -207,9 +226,9 @@ class SVGMobject(VMobject): def handle_transforms(self, element, mobject): x, y = 0, 0 try: - x = float(element.getAttribute('x')) + x = self.attribute_to_float(element.getAttribute('x')) # Flip y - y = -float(element.getAttribute('y')) + y = -self.attribute_to_float(element.getAttribute('y')) mobject.shift(x * RIGHT + y * UP) except: pass From 5b9ae8ddb3081374d58d38bc23f64c25bc46ec8a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 4 Sep 2018 16:14:39 -0700 Subject: [PATCH 24/25] Fixed flashing 3d dot problem --- mobject/three_d_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mobject/three_d_utils.py b/mobject/three_d_utils.py index b0649f6b..187aad27 100644 --- a/mobject/three_d_utils.py +++ b/mobject/three_d_utils.py @@ -38,11 +38,11 @@ def get_3d_vmob_unit_normal(vmob, point_index): if len(vmob.get_anchors()) <= 2: return np.array(UP) i = point_index - im1 = i - 1 if i > 0 else (n_points - 2) - ip1 = i + 1 if i < (n_points - 1) else 1 + im3 = i - 3 if i > 2 else (n_points - 4) + ip3 = i + 3 if i < (n_points - 3) else 3 unit_normal = get_unit_normal( - vmob.points[ip1] - vmob.points[i], - vmob.points[im1] - vmob.points[i], + vmob.points[ip3] - vmob.points[i], + vmob.points[im3] - vmob.points[i], ) if get_norm(unit_normal) == 0: return np.array(UP) From a1e21a4606c5026043f522732844af1ae451879e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 4 Sep 2018 16:14:54 -0700 Subject: [PATCH 25/25] Added QuaternionHistory Scene --- active_projects/quaternions.py | 350 ++++++++++++++++++++++++++++++--- 1 file changed, 324 insertions(+), 26 deletions(-) diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index 186e6697..c130bec9 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -32,7 +32,7 @@ def get_three_d_scene_config(high_quality=True): "num_axis_pieces": 1, }, "sphere_config": { - # "resolution": (4, 12), + "resolution": (12, 24), } } if high_quality: @@ -396,8 +396,12 @@ class QuaternionTracker(ValueTracker): class RubiksCube(VGroup): CONFIG = { "colors": [ - "#C41E3A", "#009E60", "#0051BA", - "#FF5800", "#FFD500", "#FFFFFF" + "#FFD500", # Yellow + "#C41E3A", # Orange + "#009E60", # Green + "#FF5800", # Red + "#0051BA", # Blue + "#FFFFFF" # White ], } @@ -711,37 +715,328 @@ class IntroduceHamilton(Scene): class QuaternionHistory(Scene): def construct(self): - self.show_dot_product_and_cross_product() # With date + self.show_dot_product_and_cross_product() self.teaching_students_quaternions() self.show_anti_quaternion_quote() self.mad_hatter() - self.vestiges_in_modern_notation() - 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_submobjects(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): - pass + 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_submobjects(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_submobjects(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(LaggedStart( + FadeInFrom, students, + lambda m: (m, LEFT), + )) + self.play( + LaggedStart( + ApplyMethod, students, + lambda pi: ( + pi.change, + random.choice(["confused", "maybe", "erm"]), + 3 * LEFT + 2 * UP, + ), + ), + Write(equation), + ) + blink_wait(3) + self.play( + LaggedStart(FadeInFromDown, images), + LaggedStart(FadeInFromLarge, image_labels), + Write(society_title) + ) + blink_wait(3) + self.play( + FadeOutAndShift(hamilton, RIGHT), + LaggedStart( + 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): - pass + names_and_quotes = [ + ( + "Oliver Heaviside", + """``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", + """``Quaternions... though beautifully \\\\ ingenious, + have been an unmixed evil to those who have + touched them in any way, including Clerk Maxwell.''""" + ), + ] + 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( + "\\huge " + 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_submobjects(RIGHT, buff=LARGE_BUFF) + images_with_quotes.to_edge(DOWN, MED_LARGE_BUFF) + + self.play( + LaggedStart(FadeInFromDown, images), + LaggedStart(FadeInFromLarge, names), + lag_ratio=0.75, + run_time=2, + ) + for quote in quotes: + self.play(LaggedStart( + FadeIn, VGroup(*quote.family_members_with_points()), + lag_ratio=0.3 + )) + self.wait() + self.play(FadeOut(images_with_quotes)) def mad_hatter(self): - pass + 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) - def vestiges_in_modern_notation(self): - pass + 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 LaggedStart( + change_single_place, characters, + lag_ratio=0.6 + ) + + self.play( + Write(title), + LaggedStart(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() class QuaternionRotationOverlay(Scene): @@ -3199,7 +3494,6 @@ class TwoDStereographicProjection(IntroduceFelix): def get_sphere_dot(sphere_point): dot = Dot() - dot.rotate(90 * DEGREES) dot.set_shade_in_3d(True) dot.set_fill(PINK) dot.shift(OUT) @@ -4722,19 +5016,23 @@ class HypersphereStereographicProjection(SpecialThreeDScene): return self.q_tracker.get_value() def specially_color_sphere(self, 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) - submob.set_fill(rgb_to_hex(rgb)) + 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 RuleOfQuaternionMultiplicationOverlay(Scene): @@ -5179,7 +5477,7 @@ class ShowMultiplicationBy135Example(RuleOfQuaternionMultiplication): solid=False, stroke_width=0.5, stroke_opacity=0.2, - fill_opacity=0.1, + fill_opacity=0.2, ) self.specially_color_sphere(result) return result