3b1b-manim/active_projects/quat3d.py

779 lines
23 KiB
Python
Raw Normal View History

2018-09-20 10:39:51 -07:00
from big_ol_pile_of_manim_imports import *
from active_projects.quaternions import *
W_COLOR = YELLOW
I_COLOR = GREEN
J_COLOR = RED
K_COLOR = BLUE
class QuaternionLabel(VGroup):
CONFIG = {
"decimal_config": {}
}
def __init__(self, quat, **kwargs):
VGroup.__init__(self, **kwargs)
dkwargs = dict(self.decimal_config)
2018-09-27 17:37:40 -07:00
decimals = VGroup()
decimals.add(DecimalNumber(quat[0], color=W_COLOR, **dkwargs))
2018-09-20 10:39:51 -07:00
dkwargs["include_sign"] = True
2018-09-27 17:37:40 -07:00
decimals.add(
2018-09-20 10:39:51 -07:00
DecimalNumber(quat[1], color=I_COLOR, **dkwargs),
DecimalNumber(quat[2], color=J_COLOR, **dkwargs),
DecimalNumber(quat[3], color=K_COLOR, **dkwargs),
2018-09-27 17:37:40 -07:00
)
self.add(
decimals[0],
decimals[1], TexMobject("i"),
decimals[2], TexMobject("j"),
decimals[3], TexMobject("k"),
2018-09-20 10:39:51 -07:00
)
self.arrange_submobjects(RIGHT, buff=SMALL_BUFF)
2018-09-27 17:37:40 -07:00
self.decimals = decimals
def set_value(self, quat):
for decimal, coord in zip(self.decimals, quat):
decimal.set_value(coord)
return self
class RandyPrism(Cube):
CONFIG = {
"height": 0.25,
"width": 1,
"depth": 1.2,
"fill_color": BLUE_D,
"fill_opacity": 0.9,
"stroke_color": WHITE,
"stroke_width": 1,
}
def __init__(self, **kwargs):
Cube.__init__(self, **kwargs)
self.set_height(1)
randy = Randolph(mode="pondering")
randy.set_height(0.8)
randy.rotate(TAU / 4, RIGHT)
randy.shift(0.7 * DOWN)
randy.set_shade_in_3d(True, z_index_as_group=True)
self.randy = randy
self.add(randy)
self.set_height(self.height, stretch=True)
self.set_width(self.width, stretch=True)
self.set_depth(self.depth, stretch=True)
self.center()
2018-09-20 10:39:51 -07:00
2018-09-27 18:58:27 -07:00
class Gimbal(VGroup):
CONFIG = {
"inner_r": 1.2,
"outer_r": 2.6,
}
def __init__(self, alpha=0, beta=0, gamma=0, inner_mob=None, **kwargs):
VGroup.__init__(self, **kwargs)
r1, r2, r3, r4, r5, r6, r7 = np.linspace(
self.inner_r, self.outer_r, 7
)
rings = VGroup(
self.get_ring(r5, r6),
self.get_ring(r3, r4),
self.get_ring(r1, r2),
)
for i, p1, p2 in [(0, r6, r7), (1, r4, r5), (2, r2, r3)]:
annulus = rings[i]
lines = VGroup(
Line(p1 * UP, p2 * UP),
Line(p1 * DOWN, p2 * DOWN),
)
lines.set_stroke(RED)
annulus.lines = lines
annulus.add(lines)
rings[1].lines.rotate(90 * DEGREES, about_point=ORIGIN)
rings.rotate(90 * DEGREES, RIGHT, about_point=ORIGIN)
rings.set_shade_in_3d(True)
self.rings = rings
self.add(rings)
if inner_mob is not None:
corners = [
inner_mob.get_corner(v1 + v2)
for v1 in [LEFT, RIGHT]
for v2 in [IN, OUT]
]
lines = VGroup()
for corner in corners:
corner[1] = 0
line = Line(
corner, self.inner_r * normalize(corner),
color=WHITE,
stroke_width=1
)
lines.add(line)
lines.set_shade_in_3d(True)
rings[2].add(lines, inner_mob)
# Rotations
angles = [alpha, beta, gamma]
for i, angle in zip(it.count(), angles):
vect = rings[i].lines[0].get_vector()
rings[i:].rotate(angle=angle, axis=vect)
def get_ring(self, in_r, out_r, angle=TAU / 4):
result = VGroup()
for start_angle in np.arange(0, TAU, angle):
start_angle += angle / 2
sector = AnnularSector(
inner_radius=in_r,
outer_radius=out_r,
angle=angle,
start_angle=start_angle
)
sector.set_fill(LIGHT_GREY, 0.8)
arcs = VGroup(*[
Arc(
angle=angle,
start_angle=start_angle,
radius=r
)
for r in [in_r, out_r]
])
arcs.set_stroke(BLACK, 1, opacity=0.5)
sector.add(arcs)
result.add(sector)
return result
2018-09-20 10:39:51 -07:00
# Scenes
class Introduction(QuaternionHistory):
CONFIG = {
"names_and_quotes": [
(
"Oliver Heaviside",
"""\\Huge ``the quaternion was not only not
required, but was a positive evil''"""
),
(
"Lord Kelvin",
"""\\Huge ``Quaternions... though beautifully \\\\ ingenious,
have been an unmixed evil'' """
),
]
}
def construct(self):
title_word = TextMobject("Quaternions:")
title_equation = TexMobject(
"i^2 = j^2 = k^2 = ijk = -1",
tex_to_color_map={
"i": I_COLOR,
"j": J_COLOR,
"k": K_COLOR,
}
)
# label = QuaternionLabel([
# float(str((TAU * 10**(3 * k)) % 10)[:4])
# for k in range(4)
# ])
title = VGroup(title_word, title_equation)
title.arrange_submobjects(RIGHT)
title.to_edge(UP)
images_group = self.get_dissenter_images_quotes_and_names()
images_group.to_edge(DOWN)
images, quotes, names = images_group
for pair in images_group:
pair[1].align_to(pair[0], UP)
self.play(
FadeInFromDown(title_word),
Write(title_equation)
)
self.wait()
self.play(
LaggedStart(
FadeInFrom, images,
lambda m: (m, 3 * DOWN),
lag_ratio=0.75
),
LaggedStart(FadeInFromLarge, names, lag_ratio=0.75),
*[
LaggedStart(
FadeIn, VGroup(*it.chain(*quote)),
lag_ratio=0.3,
run_time=3
)
for quote in quotes
],
)
self.wait(2)
self.play(
title.shift, 2 * UP,
*[
ApplyMethod(mob.shift, FRAME_WIDTH * vect / 2)
for pair in images_group
for mob, vect in zip(pair, [LEFT, RIGHT])
],
)
class WhoCares(TeacherStudentsScene):
def construct(self):
quotes = Group(*[
ImageMobject(
"CoderQuaternionResponse_{}".format(d),
height=2
)
for d in range(4)
])
logos = Group(*[
ImageMobject(name, height=0.5)
for name in [
"TwitterLogo",
"HackerNewsLogo",
"RedditLogo",
"YouTubeLogo",
]
])
for quote, logo in zip(quotes, logos):
logo.move_to(quote.get_corner(UR))
quote.add(logo)
quotes.arrange_submobjects_in_grid()
quotes.set_height(4)
quotes.to_corner(UL)
self.student_says(
"Um...who cares?",
target_mode="sassy",
added_anims=[self.teacher.change, "guilty"]
)
self.wait(2)
self.play(
RemovePiCreatureBubble(self.students[1]),
self.teacher.change, "raise_right_hand"
)
2018-09-27 17:37:40 -07:00
# self.play(
# LaggedStart(
# FadeInFromDown, quotes,
# run_time=3
# ),
# self.get_student_changes(*3 * ["pondering"], look_at_arg=quotes)
# )
# self.wait(2)
# # Show HN
# hn_quote = quotes[1]
# hn_context = TextMobject("news.ycombinator.com/item?id=17933908")
# hn_context.scale(0.7)
# hn_context.to_corner(UL)
# vr_headsets = VGroup()
# for pi in self.students:
# vr_headset = SVGMobject("VR_headset")
# vr_headset.set_fill(LIGHT_GREY, opacity=0.9)
# vr_headset.set_width(pi.eyes.get_width() + 0.3)
# vr_headset.move_to(pi.eyes)
# vr_headsets.add(vr_headset)
# self.play(
# hn_quote.scale, 2, {"about_edge": DL},
# FadeOutAndShift(quotes[0], 5 * UP),
# FadeOutAndShift(quotes[2], UR),
# FadeOutAndShift(quotes[3], RIGHT),
# FadeInFromDown(hn_context),
# )
# hn_rect = Rectangle(
# height=0.1 * hn_quote.get_height(),
# width=0.6 * hn_quote.get_width(),
# color=RED
# )
# hn_rect.move_to(hn_quote, UL)
# hn_rect.shift(0.225 * RIGHT + 0.75 * DOWN)
# self.play(
# ShowCreation(hn_rect),
# self.get_student_changes(
# "erm", "thinking", "confused",
# look_at_arg=hn_quote,
# )
# )
# self.add_foreground_mobjects(vr_headsets)
# self.play(
# LaggedStart(
# FadeInFrom, vr_headsets,
# lambda m: (m, UP),
# ),
# self.get_student_changes(
# *3 * ["sick"],
# look_at_arg=hn_quote,
# run_time=3
# )
# )
# self.wait(3)
# Show Twitter
t_quote = quotes[0]
# t_quote.next_to(FRAME_WIDTH * LEFT / 2 + FRAME_WIDTH * UP / 2, UR)
# t_quote.set_opacity(0)
# self.play(
# FadeOutAndShift(hn_quote, 4 * LEFT),
# FadeOutAndShift(hn_rect, 4 * LEFT),
# FadeOutAndShift(hn_context, UP),
# FadeOut(vr_headsets),
# t_quote.set_opacity, 1,
# t_quote.scale, 2,
# t_quote.to_corner, UL,
# )
# self.remove_foreground_mobjects(vr_headsets)
t_quote.fade(1)
t_quote.to_corner(UL)
2018-09-20 10:39:51 -07:00
self.play(
2018-09-27 17:37:40 -07:00
self.get_student_changes(*3 * ["pondering"], look_at_arg=quotes),
t_quote.set_opacity, 1,
t_quote.scale, 2,
t_quote.to_corner, UL,
)
self.wait(2)
self.change_student_modes(
"pondering", "happy", "tease",
look_at_arg=t_quote
)
self.wait(2)
self.play(FadeOut(t_quote))
self.wait(5)
class ShowSeveralQuaternionRotations(SpecialThreeDScene):
CONFIG = {
"quaternions": [
[0, 1, 0, 0],
[1, 0, 0, 0],
[1, 0, 1, 0],
[1, 1, 1, -1],
[0, -1, 2, 1],
[1, 0, 0, -1],
[1, -1, 0, 0],
[1, -1, 1, 0],
[1, -1, 1, -1],
[1, 0, 0, 0],
],
"start_phi": 70 * DEGREES,
"start_theta": -140 * DEGREES,
}
def construct(self):
self.add_q_tracker()
self.setup_labels()
self.setup_camera_position()
self.add_prism()
self.add_axes()
self.apply_quaternions()
def add_q_tracker(self):
self.q_tracker = QuaternionTracker()
self.q_tracker.add_updater(lambda m: m.normalize())
self.add(self.q_tracker)
def setup_labels(self):
left_q_label = QuaternionLabel([1, 0, 0, 0])
right_q_label = QuaternionLabel([1, 0, 0, 0])
for label in left_q_label, right_q_label:
lp, rp = TexMobject("()")
lp.next_to(label, LEFT, SMALL_BUFF)
rp.next_to(label, RIGHT, SMALL_BUFF)
label.add(lp, rp)
point_label = TexMobject(
*"(xi+yj+zk)",
tex_to_color_map={
"i": I_COLOR,
"j": J_COLOR,
"k": K_COLOR,
}
)
left_q_label.next_to(point_label, LEFT)
right_q_label.next_to(point_label, RIGHT)
group = VGroup(left_q_label, point_label, right_q_label)
group.arrange_submobjects(RIGHT)
group.set_width(FRAME_WIDTH - 1)
group.to_edge(UP)
self.add_fixed_in_frame_mobjects(BackgroundRectangle(group))
for label, text in zip(group, ["$q$", "Some 3d point", "$q^{-1}$"]):
brace = Brace(label, DOWN)
text_mob = TextMobject(text)
if text_mob.get_width() > brace.get_width():
text_mob.match_width(brace)
text_mob.next_to(brace, DOWN, buff=SMALL_BUFF)
text_mob.add_background_rectangle()
label.add(brace, text_mob)
self.add_fixed_in_frame_mobjects(*group)
left_q_label.add_updater(
lambda m: m.set_value(self.q_tracker.get_value())
)
left_q_label.add_updater(lambda m: self.add_fixed_in_frame_mobjects(m))
right_q_label.add_updater(
lambda m: m.set_value(quaternion_conjugate(
self.q_tracker.get_value()
))
)
right_q_label.add_updater(lambda m: self.add_fixed_in_frame_mobjects(m))
def setup_camera_position(self):
self.set_camera_orientation(
phi=self.start_phi,
theta=self.start_theta,
)
self.begin_ambient_camera_rotation(0.01)
def add_prism(self):
prism = self.prism = self.get_prism()
prism.add_updater(
lambda p: p.become(self.get_prism(
self.q_tracker.get_value()
))
)
self.add(prism)
def add_axes(self):
axes = self.axes = updating_mobject_from_func(self.get_axes)
self.add(axes)
def apply_quaternions(self):
for quat in self.quaternions:
self.change_q(quat)
self.wait(2)
#
def get_unrotated_prism(self):
return RandyPrism().scale(2)
def get_prism(self, quaternion=[1, 0, 0, 0]):
prism = self.get_unrotated_prism()
angle, axis = angle_axis_from_quaternion(quaternion)
prism.rotate(angle=angle, axis=axis, about_point=ORIGIN)
return prism
def get_axes(self):
prism = self.prism
centers = [sm.get_center() for sm in prism[:6]]
axes = VGroup()
for i in range(3):
for u in [-1, 1]:
vect = np.zeros(3)
vect[i] = u
dots = [np.dot(normalize(c), vect) for c in centers]
max_i = np.argmax(dots)
ec = centers[max_i]
prism.get_edge_center(vect)
p1 = np.zeros(3)
p1[i] = ec[i]
p1 *= dots[max_i]
p2 = 10 * vect
axes.add(Line(p1, p2))
axes.set_stroke(LIGHT_GREY, 1)
axes.set_shade_in_3d(True)
return axes
def change_q(self, value, run_time=3, added_anims=None, **kwargs):
if added_anims is None:
added_anims = []
self.play(
self.q_tracker.set_value, value,
*added_anims,
run_time=run_time,
**kwargs
)
class PauseAndPlayOverlay(Scene):
def construct(self):
pause = TexMobject("=").rotate(TAU / 4)
pause.stretch(2, 0)
pause.scale(1.5)
arrow = Vector(RIGHT, color=WHITE)
interact = TextMobject("Interact...")
group = VGroup(pause, arrow, interact)
group.arrange_submobjects(RIGHT)
group.scale(2)
not_yet = TextMobject("...well, not yet")
not_yet.scale(2)
not_yet.next_to(group, DOWN, MED_LARGE_BUFF)
self.play(Write(pause))
self.play(
GrowFromPoint(
interact, arrow.get_left(),
rate_func=squish_rate_func(smooth, 0.3, 1)
2018-09-20 10:39:51 -07:00
),
2018-09-27 17:37:40 -07:00
VFadeIn(interact),
GrowArrow(arrow),
2018-09-20 10:39:51 -07:00
)
self.wait(2)
2018-09-27 17:37:40 -07:00
self.play(Write(not_yet))
self.wait()
2018-09-20 10:39:51 -07:00
2018-09-27 17:37:40 -07:00
class RotationMatrix(ShowSeveralQuaternionRotations):
CONFIG = {
"start_phi": 60 * DEGREES,
"start_theta": -60 * DEGREES,
}
def construct(self):
self.add_q_tracker()
self.setup_camera_position()
self.add_prism()
self.add_basis_vector_labels()
self.add_axes()
title = TextMobject("Rotation matrix")
title.scale(1.5)
title.to_corner(UL)
self.add_fixed_in_frame_mobjects(title)
angle = 75 * DEGREES
axis = [0.3, 1, 0.3]
matrix = rotation_matrix(angle=angle, axis=axis)
matrix_mob = DecimalMatrix(matrix, h_buff=1.6)
matrix_mob.next_to(title, DOWN)
matrix_mob.to_edge(LEFT)
title.next_to(matrix_mob, UP)
self.add_fixed_in_frame_mobjects(matrix_mob)
colors = [I_COLOR, J_COLOR, K_COLOR]
matrix_mob.set_column_colors(*colors)
columns = matrix_mob.get_columns()
column_rects = VGroup(*[
SurroundingRectangle(c).match_color(c[0])
for c in columns
])
labels = VGroup(*[
TextMobject(
"Where", tex, "goes",
tex_to_color_map={tex: rect.get_color()}
).next_to(rect, DOWN)
for letter, rect in zip(["\\i", "\\j", "k"], column_rects)
for tex in ["$\\hat{\\textbf{%s}}$" % (letter)]
])
labels.space_out_submobjects(0.8)
quaternion = quaternion_from_angle_axis(angle, axis)
self.play(Write(matrix_mob))
self.change_q(quaternion)
self.wait()
last_label = VectorizedPoint(matrix_mob.get_bottom())
last_rect = VMobject()
for label, rect in zip(labels, column_rects):
self.add_fixed_in_frame_mobjects(rect, label)
self.play(
FadeIn(label),
FadeOut(last_label),
ShowCreation(rect),
FadeOut(last_rect)
)
self.wait()
last_label = label
last_rect = rect
self.play(FadeOut(last_label), FadeOut(last_rect))
self.wait(5)
def get_unrotated_prism(self):
prism = RandyPrism()
prism.scale(1.5)
arrows = VGroup()
for i, color in enumerate([I_COLOR, J_COLOR, K_COLOR]):
vect = np.zeros(3)
vect[i] = 1
arrow = Arrow(
prism.get_edge_center(vect), 2 * vect,
preserve_tip_size_when_scaling=False,
color=color,
buff=0,
)
arrows.add(arrow)
arrows.set_shade_in_3d(True)
prism.arrows = arrows
prism.add(arrows)
return prism
def add_basis_vector_labels(self):
labels = VGroup(
TexMobject("\\hat{\\textbf{\\i}}"),
TexMobject("\\hat{\\textbf{\\j}}"),
TexMobject("\\hat{\\textbf{k}}"),
)
def generate_updater(arrow):
return lambda m: m.move_to(
arrow.get_end() + 0.2 * normalize(arrow.get_vector()),
)
for arrow, label in zip(self.prism.arrows, labels):
label.match_color(arrow)
label.add_updater(generate_updater(arrow))
self.add_fixed_orientation_mobjects(label)
2018-09-27 18:58:27 -07:00
class EulerAnglesAndGimbal(ShowSeveralQuaternionRotations):
2018-09-27 17:37:40 -07:00
CONFIG = {
}
def construct(self):
self.setup_position()
2018-09-27 18:58:27 -07:00
self.setup_angle_trackers()
2018-09-27 17:37:40 -07:00
self.setup_gimbal()
2018-09-27 18:58:27 -07:00
self.add_axes()
2018-09-27 17:37:40 -07:00
self.add_title()
self.show_rotations()
def setup_position(self):
self.set_camera_orientation(
theta=-140 * DEGREES,
phi=70 * DEGREES,
)
self.begin_ambient_camera_rotation(rate=0.015)
2018-09-27 18:58:27 -07:00
def setup_angle_trackers(self):
self.alpha_tracker = ValueTracker(0)
self.beta_tracker = ValueTracker(0)
self.gamma_tracker = ValueTracker(0)
2018-09-27 17:37:40 -07:00
def setup_gimbal(self):
2018-09-27 18:58:27 -07:00
gimbal = updating_mobject_from_func(self.get_gimbal)
2018-09-27 17:37:40 -07:00
self.gimbal = gimbal
2018-09-27 18:58:27 -07:00
self.add(gimbal)
2018-09-27 17:37:40 -07:00
def add_title(self):
title = TextMobject("Euler angles")
title.scale(1.5)
title.to_corner(UL)
angle_labels = VGroup(
TexMobject("\\alpha").set_color(YELLOW),
TexMobject("\\beta").set_color(GREEN),
TexMobject("\\gamma").set_color(PINK),
)
angle_labels.scale(2)
angle_labels.arrange_submobjects(RIGHT, buff=MED_LARGE_BUFF)
angle_labels.next_to(title, DOWN, aligned_edge=LEFT)
self.angle_labels = angle_labels
gl_label = VGroup(
Arrow(LEFT, RIGHT, color=WHITE),
TextMobject("Gimbal lock").scale(1.5),
)
gl_label.arrange_submobjects(RIGHT)
gl_label.next_to(title, RIGHT)
self.gimbal_lock_label = gl_label
VGroup(title, angle_labels, gl_label).center().to_edge(UP)
self.add_fixed_in_frame_mobjects(title, angle_labels, gl_label)
self.remove(angle_labels)
self.remove(gl_label)
def show_rotations(self):
gimbal = self.gimbal
2018-09-27 18:58:27 -07:00
alpha_tracker = self.alpha_tracker
beta_tracker = self.beta_tracker
gamma_tracker = self.gamma_tracker
2018-09-27 17:37:40 -07:00
angles = [-60 * DEGREES, 50 * DEGREES, 45 * DEGREES]
2018-09-27 18:58:27 -07:00
trackers = [alpha_tracker, beta_tracker, gamma_tracker]
in_rs = [0.6, 0.5, 0.6]
for i in range(3):
tracker = trackers[i]
angle = angles[i]
in_r = in_rs[i]
ring = gimbal.rings[i]
vect = ring.lines[0].get_vector()
line = self.get_dotted_line(vect, in_r=in_r)
2018-09-27 17:37:40 -07:00
angle_label = self.angle_labels[i]
line.match_color(angle_label)
self.play(
ShowCreation(line),
FadeInFromDown(angle_label)
)
self.play(
2018-09-27 18:58:27 -07:00
tracker.set_value, angle,
run_time=3
2018-09-27 17:37:40 -07:00
)
self.play(FadeOut(line))
self.wait()
self.wait(3)
self.play(Write(self.gimbal_lock_label))
self.play(
2018-09-27 18:58:27 -07:00
alpha_tracker.set_value, 0,
beta_tracker.set_value, 0,
run_time=2
2018-09-27 17:37:40 -07:00
)
self.play(
2018-09-27 18:58:27 -07:00
alpha_tracker.set_value, 90 * DEGREES,
gamma_tracker.set_value, -90 * DEGREES,
run_time=3
)
self.play(
FadeOut(self.gimbal_lock_label),
*[ApplyMethod(t.set_value, 0) for t in trackers],
run_time=3
)
self.play(
alpha_tracker.set_value, 30 * DEGREES,
beta_tracker.set_value, 120 * DEGREES,
gamma_tracker.set_value, -50 * DEGREES,
run_time=3
)
self.play(
alpha_tracker.set_value, 120 * DEGREES,
beta_tracker.set_value, -30 * DEGREES,
gamma_tracker.set_value, 90 * DEGREES,
run_time=4
)
self.play(
beta_tracker.set_value, 150 * DEGREES,
run_time=2
)
self.play(
alpha_tracker.set_value, 0,
beta_tracker.set_value, 0,
gamma_tracker.set_value, 0,
run_time=4
2018-09-27 17:37:40 -07:00
)
self.wait()
#
2018-09-27 18:58:27 -07:00
def get_gimbal(self):
self.prism = RandyPrism()
return Gimbal(
alpha=self.alpha_tracker.get_value(),
beta=self.beta_tracker.get_value(),
gamma=self.gamma_tracker.get_value(),
inner_mob=self.prism
)
def get_dotted_line(self, vect, in_r=0, out_r=10):
line = VGroup(*it.chain(*[
DashedLine(
in_r * normalize(u * vect),
out_r * normalize(u * vect),
)
for u in [-1, 1]
]))
line.sort_submobjects(get_norm)
2018-09-27 17:37:40 -07:00
line.set_shade_in_3d(True)
line.set_stroke(YELLOW, 5)
line.center()
return line
2018-09-27 18:58:27 -07:00
class NewSceneName(Scene):
2018-09-27 17:37:40 -07:00
def construct(self):
2018-09-27 18:58:27 -07:00
pass