From 9cf4dfac3d958be7a67a279ac4e4bac6bf650d2b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 4 Oct 2018 18:23:18 -0700 Subject: [PATCH] Preliminary finish to quat3d preamble --- active_projects/quat3d.py | 613 ++++++++++++++++++++++++++++++++++++-- animation/movement.py | 2 - camera/three_d_camera.py | 1 + 3 files changed, 596 insertions(+), 20 deletions(-) diff --git a/active_projects/quat3d.py b/active_projects/quat3d.py index 8e216b29..18d228fe 100644 --- a/active_projects/quat3d.py +++ b/active_projects/quat3d.py @@ -193,22 +193,16 @@ class Introduction(QuaternionHistory): Write(title_equation) ) self.wait() - self.play( - LaggedStart( - FadeInFrom, images, - lambda m: (m, 3 * DOWN), - lag_ratio=0.75 - ), - LaggedStart(FadeInFromLarge, names, lag_ratio=0.75), - *[ + for image, name, quote in zip(images, names, quotes): + self.play( + FadeInFrom(image, 3 * DOWN), + FadeInFromLarge(name), LaggedStart( FadeIn, VGroup(*it.chain(*quote)), lag_ratio=0.3, - run_time=3 + run_time=2 ) - for quote in quotes - ], - ) + ) self.wait(2) self.play( title.shift, 2 * UP, @@ -251,6 +245,7 @@ class WhoCares(TeacherStudentsScene): target_mode="sassy", added_anims=[self.teacher.change, "guilty"] ) + self.change_student_modes("angry", "sassy", "sad") self.wait(2) self.play( RemovePiCreatureBubble(self.students[1]), @@ -362,6 +357,7 @@ class ShowSeveralQuaternionRotations(SpecialThreeDScene): ], "start_phi": 70 * DEGREES, "start_theta": -140 * DEGREES, + "ambient_rotation_rate": 0.01, } def construct(self): @@ -428,7 +424,7 @@ class ShowSeveralQuaternionRotations(SpecialThreeDScene): phi=self.start_phi, theta=self.start_theta, ) - self.begin_ambient_camera_rotation(0.01) + self.begin_ambient_camera_rotation(self.ambient_rotation_rate) def add_prism(self): prism = self.prism = self.get_prism() @@ -623,9 +619,6 @@ class RotationMatrix(ShowSeveralQuaternionRotations): class EulerAnglesAndGimbal(ShowSeveralQuaternionRotations): - CONFIG = { - } - def construct(self): self.setup_position() self.setup_angle_trackers() @@ -774,6 +767,590 @@ class EulerAnglesAndGimbal(ShowSeveralQuaternionRotations): return line -class NewSceneName(Scene): +class QuaternionInterpolation(ShowSeveralQuaternionRotations): def construct(self): - pass \ No newline at end of file + self.add_q_tracker() + self.setup_camera_position() + self.add_prism() + self.add_axes() + + self.change_q([1, 1, 1, 0], run_time=0) + self.wait(2) + self.change_q([1, 0.2, 0.6, -0.5], run_time=4) + self.wait(2) + self.change_q([1, -0.6, 0.2, -1], run_time=4) + self.wait(2) + + +class QuaternionInterpolationScematic(Scene): + def construct(self): + title = TextMobject("Slice of a hypersphere") + title.to_edge(UP, buff=MED_SMALL_BUFF) + self.add(title) + + radius = 3 + circle = Circle(radius=radius) + circle.set_stroke(LIGHT_GREY, 1) + qs = [circle.point_from_proportion(p) for p in (0.55, 0.35, 0.15)] + colors = [YELLOW, PINK, RED] + q_dots = [Dot(q, color=c) for q, c in zip(qs, colors)] + q_labels = [ + TexMobject("q_{}".format(i + 1)).next_to( + dot, normalize(dot.get_center()), SMALL_BUFF + ).match_color(dot) + for i, dot in enumerate(q_dots) + ] + + q1, q2, q3 = qs + lines = [ + DashedLine(q1, q2) + for q1, q2 in zip(qs, qs[1:]) + ] + for color, line in zip([GREEN, BLUE], lines): + line.set_stroke(color, 3) + line.proj = line.copy().apply_function( + lambda p: radius * normalize(p) + ) + dot = Dot(qs[0], color=WHITE) + + self.add(circle) + self.add(dot) + self.add(*q_dots + q_labels) + + self.wait(2) + for line, q in zip(lines, qs[1:]): + self.play( + ShowCreation(line), + ShowCreation(line.proj), + dot.move_to, q, + run_time=4 + ) + self.wait(2) + + +class RememberComplexNumbers(TeacherStudentsScene): + def construct(self): + complex_number = TexMobject( + "\\cos(\\theta) + \\sin(\\theta)i", + tex_to_color_map={ + "\\cos(\\theta)": GREEN, + "\\sin(\\theta)": RED + } + ) + complex_number.scale(1.2) + complex_number.next_to(self.students, UP, MED_LARGE_BUFF) + + self.teacher_says( + "Remember how \\\\ complex numbers \\\\ compute rotations" + ) + self.change_all_student_modes("pondering") + self.wait() + self.play( + FadeInFromDown(complex_number), + self.get_student_changes( + "thinking", "confused", "happy", + look_at_arg=complex_number.get_center() + UP + ), + run_time=2 + ) + self.change_student_modes( + ) + self.wait(5) + + +class ComplexNumberRotation(Scene): + CONFIG = { + "angle": 30 * DEGREES, + } + + def construct(self): + self.add_plane() + self.add_number() + self.show_complex_unit() + self.show_product() + + def add_plane(self): + plane = self.plane = ComplexPlane() + self.play(Write(plane)) + + def add_number(self): + plane = self.plane + origin = plane.coords_to_point(0, 0) + angle = self.angle + + point = plane.coords_to_point(4, 1) + dot = Dot(point, color=YELLOW) + label = TexMobject("(4, 1)") + label.next_to(dot, UR, buff=0) + line = DashedLine(origin, point) + rotated_line = line.copy().rotate(angle, about_point=origin) + rotated_line.set_color(GREY) + rotated_dot = dot.copy().rotate(angle, about_point=origin) + rotated_dot.set_color(YELLOW_E) + mystery_label = TexMobject("(?, ?)") + mystery_label.next_to(rotated_dot, UR, buff=0) + + arc = Arc( + start_angle=line.get_angle(), + angle=angle, + radius=0.75 + ) + angle_tex = str(int(np.round(angle / DEGREES))) + "^\\circ" + angle_label = TexMobject(angle_tex) + angle_label.next_to( + arc.point_from_proportion(0.3), + UR, buff=SMALL_BUFF + ) + + self.play( + FadeInFromDown(label), + GrowFromCenter(dot), + ShowCreation(line) + ) + self.wait() + self.play( + ShowCreation(arc), + FadeInFromDown(angle_label), + TransformFromCopy(line, rotated_line), + TransformFromCopy(dot, rotated_dot), + path_arc=angle, + ) + self.play(Write(mystery_label)) + self.wait() + + self.rotation_mobs = VGroup( + arc, angle_label, + rotated_line, rotated_dot, + mystery_label + ) + self.angle_tex = angle_tex + self.number_label = label + + def show_complex_unit(self): + plane = self.plane + angle = self.angle + angle_tex = self.angle_tex + complex_coordinate_labels = plane.get_coordinate_labels() + unit_circle = Circle(radius=1, color=YELLOW) + + origin = plane.number_to_point(0) + z_point = plane.coords_to_point(np.cos(angle), np.sin(angle)) + one_point = plane.number_to_point(1) + z_dot = Dot(z_point, color=WHITE) + one_dot = Dot(one_point, color=WHITE) + one_dot.fade(1) + z_line = Line(origin, z_point) + one_line = Line(origin, one_point) + VGroup(z_dot, one_dot, z_line, one_line).set_color(BLUE) + + cos_tex = "\\cos(" + angle_tex + ")" + sin_tex = "\\sin(" + angle_tex + ")" + label = TexMobject( + cos_tex, "+", sin_tex, "i", + tex_to_color_map={cos_tex: GREEN, sin_tex: RED} + ) + label.add_background_rectangle() + label.scale(0.8) + label.next_to(plane.coords_to_point(0, 1), UR, SMALL_BUFF) + arrow = Arrow(label.get_bottom(), z_point) + + number_label = self.number_label + new_number_label = TexMobject( + "4 + 1i", tex_to_color_map={"4": GREEN, "1": RED} + ) + new_number_label.move_to(number_label, LEFT) + new_number_label.add_background_rectangle() + + self.play( + Write(complex_coordinate_labels, run_time=2), + FadeOut(self.rotation_mobs) + ) + self.play(ShowCreation(unit_circle)) + self.play( + TransformFromCopy(one_dot, z_dot), + TransformFromCopy(one_line, z_line), + FadeInFromDown(label), + GrowArrow(arrow), + ) + self.wait() + self.play(FadeOutAndShiftDown(number_label)) + self.play(FadeInFromDown(new_number_label)) + self.wait() + + self.left_z_label = label + self.right_z_label = new_number_label + self.cos_tex = cos_tex + self.sin_tex = sin_tex + self.unit_z_group = VGroup( + unit_circle, z_line, z_dot, label, arrow + ) + + def show_product(self): + plane = self.plane + cos_tex = self.cos_tex + sin_tex = self.sin_tex + angle = self.angle + + line = Line( + FRAME_WIDTH * LEFT / 2 + FRAME_HEIGHT * UP / 2, + plane.coords_to_point(-0.5, 1.5) + ) + rect = BackgroundRectangle(line, buff=0) + + left_z = self.left_z_label + right_z = self.right_z_label + new_left_z = left_z.copy() + new_right_z = right_z.copy() + + lp1, rp1, lp2, rp2 = parens = TexMobject("()()") + top_line = VGroup( + lp1, new_left_z, rp1, + lp2, new_right_z, rp2, + ) + top_line.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + top_line.set_width(rect.get_width() - 1) + top_line.next_to(rect.get_top(), DOWN, MED_SMALL_BUFF) + + mid_line = TexMobject( + "\\big(", "4", cos_tex, "-", "1", sin_tex, "\\big)", "+", + "\\big(", "1", cos_tex, "+", "4", sin_tex, "\\big)", "i", + tex_to_color_map={ + cos_tex: GREEN, + sin_tex: RED, + "4": GREEN, + "1": RED, + } + ) + mid_line.set_width(rect.get_width() - 0.5) + mid_line.next_to(top_line, DOWN, MED_LARGE_BUFF) + + new_z = np.exp(angle * complex(0, 1)) * complex(4, 1) + low_line = TexMobject( + "\\approx", + str(np.round(new_z.real, 2)), "+", + str(np.round(new_z.imag, 2)), "i", + ) + low_line.next_to(mid_line, DOWN, MED_LARGE_BUFF) + + self.play(FadeIn(rect)) + self.play( + TransformFromCopy(left_z, new_left_z), + TransformFromCopy(right_z, new_right_z), + Write(parens) + ) + self.wait() + self.play(FadeInFrom(mid_line, UP)) + self.wait() + self.play(FadeInFrom(low_line, UP)) + self.wait(2) + self.play(FadeOut(self.unit_z_group)) + self.rotation_mobs.save_state() + self.rotation_mobs.rotate(-angle, about_point=ORIGIN) + self.rotation_mobs.fade(1) + self.play(self.rotation_mobs.restore) + self.wait() + + mystery_label = self.rotation_mobs[-1] + result = low_line[1:].copy() + result.add_background_rectangle() + self.play( + result.move_to, mystery_label, LEFT, + FadeOutAndShiftDown(mystery_label), + ) + self.wait() + + +class ISquaredRule(Scene): + def construct(self): + tex = TextMobject("Use", "$i^2 = -1$") + tex[1].set_color(RED) + tex.scale(2) + self.add(tex) + self.play(Write(tex)) + self.wait() + + +class RuleForQuaternionRotations(EulerAnglesAndGimbal): + CONFIG = { + "start_phi": 70 * DEGREES, + "start_theta": -120 * DEGREES, + "ambient_rotation_rate": 0.015, + } + + def construct(self): + self.add_q_tracker() + self.setup_camera_position() + self.add_prism() + self.add_axes() + + self.show_axis() + self.construct_quaternion() + self.add_point_with_coordinates() + self.add_inverse() + + def get_axes(self): + axes = EulerAnglesAndGimbal.get_axes(self) + for axis in axes: + vect = normalize(axis.get_vector()) + perp = rotate_vector(vect, TAU / 3, axis=[1, 1, 1]) + for i in range(1, 4): + tick = Line(-perp, perp).scale(0.1) + tick.match_style(axis) + tick.move_to(2 * i * vect) + axis.add(tick) + axes.set_shade_in_3d(True) + return axes + + def show_axis(self): + vect = normalize([1, -1, -0.5]) + line = self.get_dotted_line(vect, 0, 4) + quat = np.append(0, vect) + + axis_label = TextMobject("Axis of rotation") + axis_label.next_to(line.get_corner(DR), DOWN, MED_LARGE_BUFF) + axis_label.match_color(line) + + self.add_fixed_orientation_mobjects(axis_label) + self.play( + ShowCreation(line), + Write(axis_label) + ) + self.change_q(quat, run_time=2) + self.change_q([1, 0, 0, 0], run_time=2) + + # Unit vector + vect_mob = Vector(2 * vect, use_rectangular_stem=False) + vect_mob.pointwise_become_partial(vect_mob, 0, 0.95) + pieces = VGroup(*vect_mob.get_pieces(25)) + pieces.set_stroke(vect_mob.get_color(), 2) + vect_mob.set_stroke(width=0) + vect_mob.add_to_back(pieces) + vect_mob.set_shade_in_3d(True) + + vect_label = TexMobject( + "{:.2f}".format(vect[0]), "i", + "{:+.2f}".format(vect[1]), "j", + "{:+.2f}".format(vect[2]), "k", + ) + magnitude_label = TexMobject( + "x", "^2 + ", + "y", "^2 + ", + "z", "^2 = 1", + ) + for label in vect_label, magnitude_label: + decimals = label[::2] + colors = [I_COLOR, J_COLOR, K_COLOR] + for d1, color in zip(decimals, colors): + d1.set_color(color) + label.rotate(TAU / 4, RIGHT).scale(0.7) + label.next_to(vect_mob.get_end(), RIGHT, SMALL_BUFF) + + magnitude_label.next_to(vect_label, IN) + + self.play( + FadeOut(line), + FadeOutAndShiftDown(axis_label), + ShowCreation(vect_mob) + ) + # self.add_fixed_orientation_mobjects(vect_label) + self.play(FadeInFromDown(vect_label)) + self.wait(3) + self.play(TransformFromCopy(vect_label, magnitude_label)) + self.wait(3) + + self.vect = vect + self.vect_mob = vect_mob + self.vect_label = vect_label + self.magnitude_label = magnitude_label + + def construct_quaternion(self): + full_angle_q = self.get_quaternion_label("40^\\circ") + half_angle_q = self.get_quaternion_label("40^\\circ / 2") + for label in full_angle_q, half_angle_q: + label.to_corner(UL) + brace = Brace(half_angle_q, DOWN) + q_label = brace.get_tex("q") + full_angle_q.align_data(half_angle_q) + rect = SurroundingRectangle(full_angle_q[5]) + + for mob in full_angle_q, half_angle_q, brace, q_label, rect: + self.add_fixed_in_frame_mobjects(mob) + self.remove(mob) + self.play(FadeInFromDown(full_angle_q[1])) + self.wait() + self.play( + FadeInFromDown(full_angle_q[0]), + LaggedStart(FadeInFromDown, full_angle_q[2:]), + ) + self.play( + GrowFromCenter(brace), + Write(q_label) + ) + self.wait(2) + self.play(ShowCreation(rect)) + self.play(FadeOut(rect)) + self.wait(2) + self.play(ReplacementTransform(full_angle_q, half_angle_q)) + self.wait(6) + # TODO + + def add_point_with_coordinates(self): + prism = self.prism + point = prism.get_corner(UR + OUT) + template_sphere = Sphere(radius=0.1) + template_sphere.set_stroke(width=0) + template_sphere.set_color(PINK) + ghost_sphere = template_sphere.copy() + ghost_sphere.fade(0.8) + for face in template_sphere: + c = face.get_center() + if c[0] < 0 and c[2] < 0: + template_sphere.remove(face) + template_sphere.move_to(point) + ghost_sphere.move_to(point) + + def get_sphere(): + result = template_sphere.copy() + quat = self.q_tracker.get_value() + angle, axis = angle_axis_from_quaternion(quat) + result.rotate(angle=angle, axis=axis, about_point=ORIGIN) + return result + + sphere = updating_mobject_from_func(get_sphere) + + point_label = TexMobject( + "p", "=", + "{:.2f}".format(point[0]), "i", "+" + "{:.2f}".format(point[1]), "j", "+" + "{:.2f}".format(point[2]), "k", + ) + colors = [PINK, I_COLOR, J_COLOR, K_COLOR] + for part, color in zip(point_label[::2], colors): + part.set_color(color) + point_label.scale(0.7) + point_label.rotate(TAU / 4, RIGHT) + point_label.next_to(point, RIGHT) + + self.stop_ambient_camera_rotation() + self.begin_ambient_camera_rotation(-0.01) + self.play(FadeInFromLarge(sphere)) + self.play(Write(point_label)) + self.wait(3) + + # Rotate + quat = quaternion_from_angle_axis(40 * DEGREES, self.vect) + r = get_norm(point) + curved_arrow = Arrow( + r * RIGHT, rotate_vector(r * RIGHT, 30 * DEGREES, OUT), + buff=0, + use_rectangular_stem=False, + path_arc=60 * DEGREES, + color=LIGHT_GREY, + ) + curved_arrow.pointwise_become_partial(curved_arrow, 0, 0.9) + curved_arrow.rotate(150 * DEGREES, about_point=ORIGIN) + curved_arrow.apply_matrix(z_to_vector(self.vect)) + self.add(ghost_sphere, sphere) + self.change_q( + quat, + added_anims=[ShowCreation(curved_arrow)], + run_time=3 + ) + + mystery_label = TexMobject("(?, ?, ?)") + mystery_label.add_background_rectangle() + arrow = Vector(0.5 * DR, color=WHITE) + arrow.next_to(mystery_label, DR, buff=0) + # mystery_label.add(arrow) + mystery_label.rotate(TAU / 4, RIGHT) + mystery_label.next_to(sphere, OUT + LEFT, buff=0) + self.play(FadeInFromDown(mystery_label)) + self.wait(5) + + def add_inverse(self): + label = TexMobject( + "p", "\\rightarrow", + "q", "\\cdot", "p", "\\cdot", "q^{-1}", + tex_to_color_map={"p": PINK} + ) + label.to_corner(UR) + label.shift(2 * LEFT) + self.add_fixed_in_frame_mobjects(label) + + self.play(FadeInFromDown(label)) + self.wait(3) + self.change_q( + [1, 0, 0, 0], + rate_func=there_and_back, + run_time=5 + ) + self.wait(3) + + # + def get_quaternion_label(self, angle_tex): + vect_label = self.vect_label.copy() + vect_label.rotate(TAU / 4, LEFT) + vect_label.replace(TexMobject(vect_label.get_tex_string())) + vect_label.add_background_rectangle() + result = VGroup( + TexMobject("\\big("), + TexMobject("\\text{cos}(", angle_tex, ")"), + TexMobject("+"), + TexMobject("\\text{sin}(", angle_tex, ")"), + TexMobject("("), + vect_label, + TexMobject(")"), + TexMobject("\\big)"), + ) + for i in 1, 3: + result[i][1].set_color(YELLOW) + result.arrange_submobjects(RIGHT, buff=SMALL_BUFF) + result.scale(0.7) + return result + + +class ExpandOutFullProduct(TeacherStudentsScene): + def construct(self): + product = TexMobject( + """ + (w_0 + x_0 i + y_0 j + z_0 k) + (x_1 i + y_1 j + z_1 k) + (w_0 - x_0 i - y_0 j - z_0 k) + """, + tex_to_color_map={ + "w_0": W_COLOR, "(": WHITE, ")": WHITE, + "x_0": I_COLOR, "y_0": J_COLOR, "z_0": K_COLOR, + "x_1": I_COLOR, "y_1": J_COLOR, "z_1": K_COLOR, + } + ) + product.set_width(FRAME_WIDTH - 1) + product.to_edge(UP) + + n = 10 + q_brace = Brace(product[:n], DOWN) + p_brace = Brace(product[n:-n], DOWN) + q_inv_brace = Brace(product[-n:], DOWN) + braces = VGroup(q_brace, p_brace, q_inv_brace) + for brace, tex in zip(braces, ["q", "p", "q^{-1}"]): + brace.add(brace.get_tex(tex)) + + words = TextMobject("= Rotation of $p$") + words.next_to(braces, DOWN) + + self.play( + self.teacher.change, "raise_right_hand", + FadeInFromDown(product) + ) + self.play( + LaggedStart(GrowFromCenter, braces), + self.get_student_changes( + "confused", "horrified", "confused" + ) + ) + self.wait(2) + self.play(Write(words)) + self.change_student_modes( + "pondering", "confused", "erm", + look_at_arg=words + ) + self.wait(5) diff --git a/animation/movement.py b/animation/movement.py index 9950aa9e..097af71a 100644 --- a/animation/movement.py +++ b/animation/movement.py @@ -1,5 +1,3 @@ - - from constants import * from animation.animation import Animation diff --git a/camera/three_d_camera.py b/camera/three_d_camera.py index 77008609..2ef4874e 100644 --- a/camera/three_d_camera.py +++ b/camera/three_d_camera.py @@ -34,6 +34,7 @@ class ThreeDCamera(Camera): "frame_center": ORIGIN, "should_apply_shading": True, "exponential_projection": False, + "max_allowable_norm": 3 * FRAME_WIDTH, } def __init__(self, *args, **kwargs):