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