2021-01-01 20:10:38 -08:00
|
|
|
from manim_imports_ext import *
|
2021-11-11 15:22:35 -08:00
|
|
|
import scipy.spatial
|
2020-12-31 13:05:22 -08:00
|
|
|
|
|
|
|
|
|
|
|
# Helpers
|
2021-11-08 21:49:06 -08:00
|
|
|
def project_to_xy_plane(p1, p2):
|
|
|
|
"""
|
|
|
|
Draw a line from source to p1 to p2. Where does it
|
|
|
|
intersect the xy plane?
|
|
|
|
"""
|
|
|
|
x1, y1, z1 = p1
|
|
|
|
x2, y2, z2 = p2
|
|
|
|
if z2 < z1:
|
|
|
|
z2 = z1 + 1e-2 # TODO, bad hack
|
|
|
|
vect = p2 - p1
|
|
|
|
return p1 - (z2 / vect[2]) * vect
|
|
|
|
|
|
|
|
|
|
|
|
def flat_project(point):
|
|
|
|
return [*point[:2], 0]
|
|
|
|
|
|
|
|
|
|
|
|
def get_pre_shadow(mobject, opacity):
|
2020-12-31 13:05:22 -08:00
|
|
|
result = mobject.deepcopy()
|
2021-11-11 15:22:35 -08:00
|
|
|
if isinstance(result, Group) and isinstance(result[0], VMobject):
|
|
|
|
result = VGroup(*result)
|
2021-11-08 21:49:06 -08:00
|
|
|
result.clear_updaters()
|
|
|
|
|
|
|
|
for sm in result.family_members_with_points():
|
2021-11-11 15:22:35 -08:00
|
|
|
# color = interpolate_color(sm.get_color(), BLACK, sm.get_opacity())
|
|
|
|
color = interpolate_color(sm.get_color(), BLACK, opacity)
|
|
|
|
sm.set_color(color)
|
|
|
|
sm.set_opacity(opacity)
|
|
|
|
if isinstance(sm, VMobject):
|
|
|
|
sm.set_stroke(BLACK, 0.5, opacity=opacity)
|
2021-11-08 21:49:06 -08:00
|
|
|
sm.set_gloss(sm.get_gloss() * 0.5)
|
|
|
|
sm.set_shadow(0)
|
|
|
|
sm.set_reflectiveness(0)
|
2020-12-31 13:05:22 -08:00
|
|
|
return result
|
|
|
|
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
def update_shadow(shadow, mobject, light_source):
|
|
|
|
lp = light_source.get_center() if light_source is not None else None
|
|
|
|
|
|
|
|
def project(point):
|
|
|
|
if lp is None:
|
|
|
|
return flat_project(point)
|
|
|
|
else:
|
|
|
|
return project_to_xy_plane(lp, point)
|
|
|
|
|
|
|
|
for sm, mm in zip(shadow.family_members_with_points(), mobject.family_members_with_points()):
|
|
|
|
sm.set_points(np.apply_along_axis(project, 1, mm.get_points()))
|
2021-11-11 15:22:35 -08:00
|
|
|
if isinstance(sm, VMobject) and sm.get_unit_normal()[2] < 0:
|
|
|
|
sm.reverse_points()
|
|
|
|
sm.set_fill(opacity=mm.get_fill_opacity())
|
2021-11-08 21:49:06 -08:00
|
|
|
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
def get_shadow(mobject, light_source=None, opacity=0.7):
|
2021-11-08 21:49:06 -08:00
|
|
|
shadow = get_pre_shadow(mobject, opacity)
|
|
|
|
shadow.add_updater(lambda s: update_shadow(s, mobject, light_source))
|
|
|
|
return shadow
|
|
|
|
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
def get_area(shadow):
|
|
|
|
return 0.5 * sum(
|
|
|
|
get_norm(sm.get_area_vector())
|
|
|
|
for sm in shadow.get_family()
|
|
|
|
)
|
2020-12-31 13:05:22 -08:00
|
|
|
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
def get_convex_hull(mobject):
|
|
|
|
points = mobject.get_all_points()
|
|
|
|
hull = scipy.spatial.ConvexHull(points[:, :2])
|
|
|
|
return points[hull.vertices]
|
2020-12-31 13:05:22 -08:00
|
|
|
|
|
|
|
|
|
|
|
# Scenes
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
class ShadowScene(ThreeDScene):
|
|
|
|
object_center = [0, 0, 3]
|
|
|
|
frame_center = [0, 0, 2]
|
|
|
|
area_label_center = [0, -1.5, 0]
|
|
|
|
surface_area = 6.0
|
|
|
|
num_reorientations = 10
|
|
|
|
plane_dims = (20, 20)
|
|
|
|
plane_style = {
|
|
|
|
"stroke_width": 0,
|
|
|
|
"fill_color": GREY_A,
|
|
|
|
"fill_opacity": 0.5,
|
|
|
|
"gloss": 0.5,
|
|
|
|
"shadow": 0.2,
|
2020-12-31 13:05:22 -08:00
|
|
|
}
|
2021-11-11 15:22:35 -08:00
|
|
|
object_style = {
|
|
|
|
"stroke_color": WHITE,
|
|
|
|
"stroke_width": 0.5,
|
|
|
|
"fill_color": BLUE_E,
|
|
|
|
"fill_opacity": 0.7,
|
|
|
|
"reflectiveness": 0.3,
|
|
|
|
"gloss": 0.1,
|
|
|
|
"shadow": 0.5,
|
|
|
|
}
|
2021-11-08 21:49:06 -08:00
|
|
|
inf_light = False
|
|
|
|
glow_radius = 10
|
|
|
|
glow_factor = 10
|
2021-11-11 15:22:35 -08:00
|
|
|
area_label_center = [-2, -1, 0]
|
|
|
|
unit_size = 2
|
2020-12-31 13:05:22 -08:00
|
|
|
|
|
|
|
def setup(self):
|
2021-11-08 21:49:06 -08:00
|
|
|
self.camera.frame.reorient(-30, 75)
|
|
|
|
self.camera.frame.move_to(self.frame_center)
|
2020-12-31 13:05:22 -08:00
|
|
|
self.add_plane()
|
2021-11-08 21:49:06 -08:00
|
|
|
self.add_solid()
|
|
|
|
self.add_shadow()
|
|
|
|
self.setup_light_source()
|
2020-12-31 13:05:22 -08:00
|
|
|
|
|
|
|
def add_plane(self):
|
2021-11-08 21:49:06 -08:00
|
|
|
width, height = self.plane_dims
|
|
|
|
plane = self.plane = Rectangle(width, height)
|
|
|
|
plane.set_style(**self.plane_style)
|
|
|
|
|
2020-12-31 13:05:22 -08:00
|
|
|
grid = NumberPlane(
|
2021-11-08 21:49:06 -08:00
|
|
|
x_range=(-width // 2, width // 2, 2),
|
|
|
|
y_range=(-height // 2, height // 2, 2),
|
|
|
|
background_line_style={
|
|
|
|
"stroke_color": GREY_B,
|
|
|
|
"stroke_width": 1,
|
|
|
|
},
|
|
|
|
faded_line_ratio=4,
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-08 21:49:06 -08:00
|
|
|
grid.axes.match_style(grid.background_lines)
|
|
|
|
grid.set_flat_stroke(True)
|
2020-12-31 13:05:22 -08:00
|
|
|
plane.add(grid)
|
|
|
|
self.add(plane)
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
def add_solid(self):
|
2021-11-11 15:22:35 -08:00
|
|
|
self.solid = self.get_solid()
|
2021-11-08 21:49:06 -08:00
|
|
|
self.solid.move_to(self.object_center)
|
|
|
|
self.solid.add_updater(lambda m: self.sort_to_camera(m))
|
|
|
|
self.add(self.solid)
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
def get_solid(self):
|
2021-11-08 21:49:06 -08:00
|
|
|
cube = VCube()
|
|
|
|
cube.deactivate_depth_test()
|
|
|
|
cube.set_height(2)
|
2021-11-11 15:22:35 -08:00
|
|
|
cube.set_style(**self.object_style)
|
2021-11-08 21:49:06 -08:00
|
|
|
# Wrap in group so that strokes and fills
|
|
|
|
# are rendered in separate passes
|
|
|
|
cube = self.cube = Group(*cube)
|
|
|
|
return cube
|
|
|
|
|
|
|
|
def add_shadow(self):
|
|
|
|
light_source = None if self.inf_light else self.camera.light_source
|
|
|
|
shadow = get_shadow(self.solid, light_source)
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
self.add(shadow, self.solid)
|
|
|
|
self.shadow = shadow
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
def setup_light_source(self):
|
|
|
|
self.light = self.camera.light_source
|
2021-11-11 15:22:35 -08:00
|
|
|
if self.inf_light:
|
|
|
|
self.light.move_to(100 * OUT)
|
|
|
|
else:
|
|
|
|
glow = self.glow = TrueDot(
|
|
|
|
radius=self.glow_radius,
|
|
|
|
glow_factor=self.glow_factor,
|
|
|
|
)
|
|
|
|
glow.set_color(interpolate_color(YELLOW, WHITE, 0.5))
|
|
|
|
glow.add_updater(lambda m: m.move_to(self.light))
|
|
|
|
self.add(glow)
|
2021-11-08 21:49:06 -08:00
|
|
|
|
|
|
|
def sort_to_camera(self, mobject):
|
|
|
|
cl = self.camera.get_location()
|
|
|
|
mobject.sort(lambda p: -get_norm(p - cl))
|
|
|
|
for sm in mobject:
|
|
|
|
sm.refresh_unit_normal()
|
|
|
|
return mobject
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
def get_shadow_area_label(self):
|
2021-01-18 08:20:27 -10:00
|
|
|
text = TexText("Shadow area: ")
|
2020-12-31 13:05:22 -08:00
|
|
|
decimal = DecimalNumber(0)
|
2021-11-11 15:22:35 -08:00
|
|
|
decimal.add_updater(lambda d: d.set_value(
|
|
|
|
get_area(self.shadow) / (self.unit_size**2)
|
|
|
|
))
|
|
|
|
|
2020-12-31 13:05:22 -08:00
|
|
|
label = VGroup(text, decimal)
|
|
|
|
label.arrange(RIGHT)
|
|
|
|
label.move_to(self.area_label_center - decimal.get_center())
|
2021-11-11 15:22:35 -08:00
|
|
|
label.fix_in_frame()
|
|
|
|
label.set_stroke(BLACK, 3, background=True)
|
|
|
|
return label
|
|
|
|
|
|
|
|
def begin_ambient_rotation(self, mobject, speed=0.2):
|
|
|
|
mobject.rot_axis = np.array([1, 1, 1])
|
|
|
|
|
|
|
|
def update_mob(mob, dt):
|
|
|
|
mob.rotate(speed * dt, mob.rot_axis)
|
|
|
|
mob.rot_axis = rotate_vector(mob.rot_axis, speed * dt, OUT)
|
|
|
|
return mob
|
|
|
|
mobject.add_updater(update_mob)
|
|
|
|
return mobject
|
|
|
|
|
|
|
|
def get_shadow_outline(self, stroke_width=1):
|
|
|
|
outline = VMobject()
|
|
|
|
outline.set_stroke(WHITE, stroke_width)
|
|
|
|
outline.add_updater(lambda m: m.set_points_as_corners(get_convex_hull(self.shadow)).close_path())
|
|
|
|
return outline
|
|
|
|
|
|
|
|
def get_light_lines(self, outline=None, n_lines=100, only_vertices=False):
|
|
|
|
if outline is None:
|
|
|
|
outline = self.get_shadow_outline()
|
|
|
|
lp = self.light.get_center()
|
|
|
|
|
|
|
|
def update_lines(lines):
|
|
|
|
if only_vertices:
|
|
|
|
points = outline.get_vertices()
|
|
|
|
else:
|
|
|
|
points = [outline.pfp(a) for a in np.linspace(0, 1, n_lines)]
|
|
|
|
for line, point in zip(lines, points):
|
|
|
|
if self.inf_light:
|
|
|
|
line.put_start_and_end_on(lp, point)
|
|
|
|
else:
|
|
|
|
line.put_start_and_end_on(point + 10 * OUT, point)
|
|
|
|
|
|
|
|
line = Line()
|
|
|
|
line.insert_n_curves(5)
|
|
|
|
light_lines = line.replicate(n_lines)
|
|
|
|
light_lines.set_stroke(YELLOW, 0.5, 0.1)
|
|
|
|
light_lines.add_updater(update_lines)
|
|
|
|
return light_lines
|
|
|
|
|
|
|
|
def randomly_reorient(self, run_time=1):
|
|
|
|
# axis = normalize(np.random.random(3))
|
|
|
|
# angle = PI + np.random.random() * PI
|
|
|
|
self.solid.rot_axis = normalize(np.random.random(3))
|
|
|
|
self.solid.rot_time = 0
|
|
|
|
|
|
|
|
def update(mob, time):
|
|
|
|
dt = time - mob.rot_time
|
|
|
|
mob.rot_time = time
|
|
|
|
mob.rot_axis = rotate_vector(mob.rot_axis, 5 * dt, normalize(np.random.random(3)))
|
|
|
|
mob.rotate(TAU * dt, mob.rot_axis)
|
|
|
|
|
|
|
|
# self.play(Rotate(self.solid, angle, axis), run_time=run_time)
|
|
|
|
self.play(UpdateFromAlphaFunc(self.solid, update), run_time=run_time)
|
|
|
|
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
class IntroduceShadow(ShadowScene):
|
|
|
|
area_label_center = [-2.5, -2, 0]
|
|
|
|
plane_dims = (30, 20)
|
|
|
|
|
|
|
|
def construct(self):
|
|
|
|
# Setup
|
|
|
|
light = self.light
|
|
|
|
cube = self.solid
|
|
|
|
shadow = self.shadow
|
|
|
|
outline = self.get_shadow_outline()
|
|
|
|
frame = self.camera.frame
|
|
|
|
cube.scale(0.945) # Hack to make the appropriate area 1
|
|
|
|
|
|
|
|
# Ambient rotation
|
|
|
|
frame.add_updater(lambda f, dt: f.increment_theta(0.01 * dt))
|
|
|
|
light.move_to([-2, 2, 10])
|
|
|
|
|
|
|
|
area_label = self.get_shadow_area_label()
|
|
|
|
question = TexText(
|
|
|
|
"Puzzle: Find the average\\\\area of a cube's shadow",
|
|
|
|
font_size=48,
|
|
|
|
)
|
|
|
|
question.to_corner(UL)
|
|
|
|
question.fix_in_frame()
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
# Introductory animations
|
|
|
|
self.shadow.update()
|
|
|
|
self.play(
|
|
|
|
FadeIn(question, 0.5 * UP),
|
|
|
|
*(
|
|
|
|
LaggedStartMap(DrawBorderThenFill, mob, lag_ratio=0.1, run_time=3)
|
|
|
|
for mob in (cube, shadow)
|
|
|
|
)
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
area_label.update()
|
|
|
|
outline.update()
|
|
|
|
self.play(
|
|
|
|
FadeIn(area_label, lag_ratio=0.1),
|
|
|
|
ShowCreation(outline),
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
self.begin_ambient_rotation(cube)
|
|
|
|
self.wait(8)
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
# Ask questions
|
|
|
|
questions = VGroup(
|
|
|
|
Text("Where is the light?"),
|
|
|
|
TexText("``Average'' in what sense?"),
|
|
|
|
)
|
|
|
|
questions.set_color(TEAL)
|
|
|
|
questions.arrange(DOWN, MED_LARGE_BUFF)
|
|
|
|
questions.to_corner(UR)
|
|
|
|
questions.fix_in_frame()
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
light_lines = always_redraw(lambda: self.get_light_lines(outline))
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
LaggedStartMap(
|
|
|
|
FadeIn, questions,
|
|
|
|
shift=0.5 * DOWN,
|
|
|
|
lag_ratio=0.5
|
|
|
|
),
|
|
|
|
ShowCreation(
|
|
|
|
light_lines,
|
|
|
|
lag_ratio=0.01,
|
|
|
|
run_time=2
|
|
|
|
)
|
|
|
|
)
|
|
|
|
self.play(
|
|
|
|
light.animate.next_to(cube, OUT, 2),
|
|
|
|
frame.animate.set_height(12).set_z(3),
|
|
|
|
run_time=3,
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
self.play(light.animate.shift(1.0 * IN), run_time=2)
|
|
|
|
self.wait()
|
|
|
|
self.play(light.animate.shift(2 * OUT), run_time=2)
|
|
|
|
|
|
|
|
# Long ambient rotation
|
|
|
|
self.wait(18)
|
|
|
|
|
|
|
|
# Light infinitely far away
|
|
|
|
underlines = VGroup(*(
|
|
|
|
Underline(q, buff=-0.05) for q in questions
|
|
|
|
))
|
|
|
|
underlines.set_stroke(YELLOW, 1)
|
|
|
|
underlines.fix_in_frame()
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
questions[0].animate.set_color(YELLOW),
|
|
|
|
questions[1].animate.set_opacity(0.2),
|
|
|
|
ShowCreation(underlines[0]),
|
|
|
|
)
|
|
|
|
light_points = (
|
|
|
|
[-2, 2, 6],
|
|
|
|
[4, 2, 5.5],
|
|
|
|
[4, -2, 6.5],
|
|
|
|
[0, 0, 8],
|
|
|
|
[0, 0, 75],
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
for point in light_points:
|
|
|
|
self.play(light.animate.move_to(point), run_time=2)
|
|
|
|
self.wait()
|
|
|
|
self.wait(3)
|
|
|
|
|
|
|
|
# Flat projection
|
|
|
|
cube.clear_updaters()
|
|
|
|
cube.add_updater(lambda m: self.sort_to_camera(m))
|
|
|
|
cube_copy = cube.deepcopy()
|
|
|
|
shadow_copy = get_pre_shadow(cube_copy, 0.75)
|
|
|
|
shadow_copy.apply_function(lambda p: [*p[:2], 0])
|
|
|
|
self.play(LaggedStart(*(
|
|
|
|
ReplacementTransform(c.copy().fade(1), s)
|
|
|
|
for c, s in zip(cube_copy, shadow_copy)
|
|
|
|
)), lag_ratio=0.9, run_time=2)
|
|
|
|
self.play(FadeOut(shadow_copy))
|
|
|
|
self.wait(2)
|
|
|
|
|
|
|
|
# Square projection
|
|
|
|
top_face = cube[np.argmax([f.get_z() for f in cube])]
|
|
|
|
normal_vect = top_face.get_unit_normal()
|
|
|
|
theta = np.arccos(normal_vect[2])
|
|
|
|
axis = normalize(rotate_vector([*normal_vect[:2], 0], PI / 2, OUT))
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
Rotate(cube, -theta, axis),
|
|
|
|
run_time=2,
|
2020-12-31 13:05:22 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
top_face = cube[np.argmax([f.get_z() for f in cube])]
|
|
|
|
verts = top_face.get_vertices()
|
|
|
|
vect = verts[3] - verts[2]
|
|
|
|
angle = angle_of_vector(vect)
|
|
|
|
self.play(Rotate(cube, -angle, OUT))
|
|
|
|
self.wait()
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
corner = cube.get_corner(DL + OUT)
|
|
|
|
edge_lines = VGroup(
|
|
|
|
Line(corner, cube.get_corner(DR + OUT)),
|
|
|
|
Line(corner, cube.get_corner(UL + OUT)),
|
|
|
|
Line(corner, cube.get_corner(DL + IN)),
|
|
|
|
)
|
|
|
|
edge_lines.set_stroke(RED, 2)
|
|
|
|
s_labels = Tex("s").replicate(3)
|
|
|
|
s_labels.set_color(RED)
|
|
|
|
s_labels.rotate(PI / 2, RIGHT)
|
|
|
|
s_labels.set_stroke(BLACK, 3, background=True)
|
|
|
|
for label, line, vect in zip(s_labels, edge_lines, [OUT, LEFT, LEFT]):
|
|
|
|
label.next_to(line, vect, buff=SMALL_BUFF)
|
|
|
|
s_labels[1].next_to(edge_lines[1], OUT)
|
|
|
|
s_labels[2].next_to(edge_lines[2], LEFT)
|
|
|
|
|
|
|
|
s_squared = Tex("s^2")
|
|
|
|
s_squared.match_style(s_labels[0])
|
|
|
|
s_squared.move_to(self.shadow)
|
|
|
|
|
|
|
|
frame.generate_target()
|
|
|
|
frame.target.reorient(10, 60)
|
|
|
|
frame.target.scale(0.8)
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
LaggedStartMap(ShowCreation, edge_lines),
|
|
|
|
LaggedStartMap(FadeIn, s_labels, scale=2),
|
|
|
|
MoveToTarget(frame, run_time=3)
|
|
|
|
)
|
|
|
|
self.wait()
|
|
|
|
self.play(
|
|
|
|
TransformFromCopy(s_labels[:2], s_squared),
|
|
|
|
)
|
|
|
|
self.wait(2)
|
|
|
|
|
|
|
|
rect = SurroundingRectangle(area_label)
|
|
|
|
rect.fix_in_frame()
|
|
|
|
rect.set_stroke(YELLOW, 3)
|
|
|
|
s_eq = Tex("s = 1")
|
|
|
|
s_eq.next_to(area_label, DOWN)
|
|
|
|
s_eq.set_color(RED)
|
|
|
|
s_eq.set_stroke(BLACK, 3, background=True)
|
|
|
|
s_eq.fix_in_frame()
|
|
|
|
|
|
|
|
self.play(ShowCreation(rect))
|
|
|
|
self.play(FadeIn(s_eq, 0.5 * DOWN))
|
|
|
|
self.wait()
|
|
|
|
self.play(LaggedStart(*map(FadeOut, (
|
|
|
|
rect, s_eq, *edge_lines, *s_labels, s_squared,
|
|
|
|
))))
|
|
|
|
self.wait()
|
|
|
|
|
|
|
|
# Hexagonal orientation
|
|
|
|
axis = UL
|
|
|
|
angle = np.arccos(1 / math.sqrt(3))
|
|
|
|
area_label.suspend_updating()
|
|
|
|
self.play(
|
|
|
|
Rotate(cube, -angle, axis),
|
|
|
|
frame.animate.reorient(-10, 70),
|
|
|
|
ChangeDecimalToValue(area_label[1], math.sqrt(3)),
|
|
|
|
UpdateFromFunc(area_label[1], lambda m: m.fix_in_frame()),
|
|
|
|
run_time=2
|
|
|
|
)
|
|
|
|
self.add(area_label)
|
|
|
|
|
|
|
|
hex_area_label = Tex("\\sqrt{3} s^2")
|
|
|
|
hex_area_label.set_color(RED)
|
|
|
|
hex_area_label.move_to(self.shadow)
|
|
|
|
self.play(Write(hex_area_label))
|
|
|
|
self.wait(2)
|
|
|
|
area_label.resume_updating()
|
|
|
|
self.play(
|
|
|
|
FadeOut(hex_area_label),
|
|
|
|
Rotate(cube, 4, RIGHT)
|
|
|
|
)
|
|
|
|
self.wait(3)
|
|
|
|
|
|
|
|
# Talk about averages
|
|
|
|
light_lines.clear_updaters()
|
|
|
|
self.play(
|
|
|
|
FadeOut(underlines[0]),
|
|
|
|
FadeOut(light_lines),
|
|
|
|
ShowCreation(underlines[1]),
|
|
|
|
questions[0].animate.set_opacity(0.1),
|
|
|
|
questions[1].animate.set_fill(YELLOW, 1),
|
|
|
|
)
|
|
|
|
self.wait()
|
|
|
|
|
|
|
|
samples = VGroup(VectorizedPoint())
|
|
|
|
samples.next_to(questions, DOWN, buff=0.5)
|
|
|
|
samples.shift(RIGHT)
|
|
|
|
self.add(samples)
|
|
|
|
for x in range(7):
|
|
|
|
self.randomly_reorient()
|
|
|
|
sample = area_label[1].copy()
|
|
|
|
sample.clear_updaters()
|
|
|
|
sample.fix_in_frame()
|
|
|
|
self.play(sample.animate.next_to(samples, DOWN))
|
|
|
|
samples.add(sample)
|
|
|
|
|
|
|
|
v_dots = Tex("\\vdots")
|
|
|
|
v_dots.next_to(samples, DOWN)
|
|
|
|
v_dots.fix_in_frame()
|
|
|
|
samples.add(v_dots)
|
|
|
|
brace = Brace(samples, LEFT)
|
|
|
|
brace.fix_in_frame()
|
|
|
|
brace.next_to(samples, LEFT, SMALL_BUFF)
|
|
|
|
text = TexText(
|
|
|
|
"Take the mean.", "\\\\What does that\\\\approach?",
|
|
|
|
font_size=30
|
|
|
|
)
|
|
|
|
text[0].shift(MED_SMALL_BUFF * UP)
|
|
|
|
text.next_to(brace, LEFT)
|
|
|
|
text.fix_in_frame()
|
|
|
|
VGroup(text, brace).set_stroke(BLACK, 3, background=True)
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
GrowFromCenter(brace),
|
|
|
|
FadeIn(text),
|
|
|
|
Write(v_dots),
|
|
|
|
)
|
|
|
|
self.wait()
|
|
|
|
|
|
|
|
for x in range(7):
|
|
|
|
self.randomly_reorient()
|
|
|
|
self.wait()
|
|
|
|
|
|
|
|
|
|
|
|
class FocusOnOneFace(ShadowScene):
|
|
|
|
inf_light = True
|
2020-12-31 13:05:22 -08:00
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
def construct(self):
|
2021-11-11 15:22:35 -08:00
|
|
|
# Some random tumbling
|
|
|
|
cube = self.solid
|
|
|
|
shadow = self.shadow
|
|
|
|
frame = self.camera.frame
|
|
|
|
|
|
|
|
words = VGroup(
|
|
|
|
Text("Just one orientation"),
|
|
|
|
Text("Just one face"),
|
|
|
|
)
|
|
|
|
words.fix_in_frame()
|
|
|
|
words.arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
|
|
|
|
words.to_corner(UL)
|
|
|
|
|
|
|
|
for x in range(3):
|
|
|
|
self.wait()
|
|
|
|
self.randomly_reorient()
|
|
|
|
self.play(FadeIn(words[0], scale=0.75, run_time=0.5))
|
|
|
|
self.wait()
|
|
|
|
|
|
|
|
# Just one face
|
|
|
|
index = np.argmax([f.get_z() for f in cube])
|
|
|
|
face = cube[index]
|
|
|
|
prev_opacity = face.get_fill_opacity()
|
|
|
|
cube.generate_target()
|
|
|
|
cube.target.space_out_submobjects(2, about_point=face.get_center())
|
|
|
|
cube.target.set_opacity(0)
|
|
|
|
cube.target[index].set_opacity(prev_opacity)
|
2021-11-08 21:49:06 -08:00
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
self.shadow.set_stroke(width=0)
|
2021-11-08 21:49:06 -08:00
|
|
|
self.play(
|
2021-11-11 15:22:35 -08:00
|
|
|
MoveToTarget(cube),
|
|
|
|
FadeIn(words[1]),
|
2021-11-08 21:49:06 -08:00
|
|
|
)
|
2021-11-11 15:22:35 -08:00
|
|
|
self.play(
|
|
|
|
frame.animate.reorient(-10, 65),
|
|
|
|
run_time=3,
|
|
|
|
)
|
|
|
|
frame.add_updater(lambda f, dt: f.increment_theta(0.01 * dt))
|
|
|
|
|
|
|
|
self.solid = face
|
|
|
|
self.remove(shadow)
|
|
|
|
self.add_shadow()
|
|
|
|
shadow = self.shadow
|
|
|
|
|
|
|
|
# Ask about area
|
|
|
|
area_q = Text("Area?")
|
|
|
|
area_q.add_updater(lambda m: m.move_to(shadow))
|
|
|
|
self.play(Write(area_q))
|
2021-11-08 21:49:06 -08:00
|
|
|
self.wait()
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
# Orient straight up
|
|
|
|
unit_normal = face.get_unit_normal()
|
|
|
|
axis = rotate_vector(normalize([*unit_normal[:2], 0]), PI / 2, OUT)
|
|
|
|
angle = np.arccos(unit_normal[2])
|
|
|
|
face.generate_target()
|
|
|
|
face.target.rotate(-angle, axis)
|
|
|
|
face.target.move_to(3 * OUT)
|
|
|
|
face.target.rotate(-PI / 4, OUT)
|
|
|
|
self.play(MoveToTarget(face))
|
|
|
|
|
|
|
|
light_lines = self.get_light_lines(n_lines=4, outline=shadow, only_vertices=True)
|
|
|
|
light_lines.set_stroke(YELLOW, 1, 0.5)
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
frame.animate.set_phi(70 * DEGREES),
|
|
|
|
FadeIn(light_lines, lag_ratio=0.5),
|
|
|
|
TransformFromCopy(face, face.deepcopy().set_opacity(0).set_z(0), remover=True),
|
|
|
|
run_time=3,
|
|
|
|
)
|
|
|
|
self.wait(3)
|
|
|
|
self.play(
|
|
|
|
Rotate(face, PI / 2, UP),
|
|
|
|
FadeOut(area_q, scale=0),
|
|
|
|
run_time=3,
|
|
|
|
)
|
|
|
|
self.wait(3)
|
|
|
|
self.play(
|
|
|
|
Rotate(face, -PI / 3, UP),
|
|
|
|
UpdateFromAlphaFunc(light_lines, lambda m, a: m.set_opacity(0.5 * (1 - a)), remover=True),
|
|
|
|
run_time=2,
|
|
|
|
)
|
|
|
|
|
|
|
|
# Show normal vector
|
|
|
|
z_axis = VGroup(
|
|
|
|
Line(ORIGIN, face.get_center()),
|
|
|
|
Line(face.get_center(), 10 * OUT),
|
|
|
|
)
|
|
|
|
z_axis.set_stroke(WHITE, 1)
|
|
|
|
|
|
|
|
normal_vect = Vector()
|
|
|
|
get_fc = face.get_center
|
|
|
|
get_un = face.get_unit_normal
|
|
|
|
|
|
|
|
def get_theta():
|
|
|
|
return np.arccos(get_un()[2])
|
|
|
|
|
|
|
|
normal_vect.add_updater(lambda v: v.put_start_and_end_on(
|
|
|
|
get_fc(), get_fc() + get_un(),
|
|
|
|
))
|
|
|
|
arc = always_redraw(lambda: Arc(
|
|
|
|
start_angle=PI / 2,
|
|
|
|
angle=-get_theta(),
|
|
|
|
radius=0.5,
|
|
|
|
stroke_width=2,
|
|
|
|
).rotate(PI / 2, RIGHT, about_point=ORIGIN).shift(get_fc()))
|
|
|
|
theta = Tex("\\theta", font_size=30)
|
|
|
|
theta.rotate(PI / 2, RIGHT)
|
|
|
|
theta.add_updater(lambda m: m.move_to(
|
|
|
|
get_fc() + 1.3 * (arc.pfp(0.5) - get_fc())
|
|
|
|
))
|
|
|
|
theta.add_updater(lambda m: m.set_width(min(0.123, max(0.01, arc.get_width()))))
|
|
|
|
|
|
|
|
self.play(ShowCreation(normal_vect))
|
|
|
|
self.wait()
|
|
|
|
self.add(z_axis[0], face, z_axis[1], normal_vect)
|
|
|
|
self.play(*map(FadeIn, z_axis))
|
|
|
|
self.play(
|
|
|
|
FadeIn(theta, 0.5 * OUT), ShowCreation(arc),
|
|
|
|
frame.animate.reorient(-10),
|
|
|
|
)
|
|
|
|
self.wait(5)
|
|
|
|
|
|
|
|
# Show shadow area in the corner
|
|
|
|
axes = Axes(
|
|
|
|
(0, 180, 22.5), (0, 1, 0.25),
|
|
|
|
width=5,
|
|
|
|
height=2,
|
|
|
|
axis_config={
|
|
|
|
"include_tip": False,
|
|
|
|
"tick_size": 0.05,
|
|
|
|
"numbers_to_exclude": [],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
axes.to_corner(UR, buff=MED_SMALL_BUFF)
|
|
|
|
axes.x_axis.add_numbers([0, 45, 90, 135, 180], unit="^\\circ")
|
|
|
|
y_label = TexText("Shadow's area", font_size=24)
|
|
|
|
y_label.next_to(axes.y_axis.get_top(), RIGHT, MED_SMALL_BUFF)
|
|
|
|
ly_label = Tex("s^2", font_size=24)
|
|
|
|
ly_label.next_to(axes.y_axis.get_top(), LEFT, SMALL_BUFF)
|
|
|
|
ly_label.shift(0.05 * UP)
|
|
|
|
axes.add(y_label, ly_label)
|
|
|
|
axes.fix_in_frame()
|
|
|
|
|
|
|
|
graph = axes.get_graph(
|
|
|
|
lambda x: math.cos(x * DEGREES),
|
|
|
|
x_range=(0, 90),
|
|
|
|
)
|
|
|
|
graph.set_stroke(RED, 3)
|
|
|
|
graph.fix_in_frame()
|
|
|
|
|
|
|
|
question = Text("Can you guess?", font_size=36)
|
|
|
|
question.to_corner(UR)
|
|
|
|
question.set_color(RED)
|
|
|
|
|
|
|
|
dot = Dot(color=RED)
|
|
|
|
dot.scale(0.5)
|
|
|
|
dot.move_to(axes.c2p(0, 1))
|
|
|
|
dot.fix_in_frame()
|
|
|
|
|
|
|
|
self.play(
|
|
|
|
FadeIn(axes),
|
|
|
|
Rotate(face, -get_theta(), UP, run_time=2),
|
|
|
|
)
|
|
|
|
self.play(FadeIn(dot, shift=2 * UP + RIGHT))
|
|
|
|
self.wait(2)
|
|
|
|
self.add(graph, axes)
|
|
|
|
self.play(
|
|
|
|
MoveAlongPath(dot, graph.deepcopy()),
|
|
|
|
ShowCreation(graph),
|
|
|
|
Rotate(face, PI / 2, UP),
|
|
|
|
run_time=5
|
|
|
|
)
|
|
|
|
self.play(frame.animate.reorient(15), run_time=2)
|
|
|
|
self.play(frame.animate.reorient(-15), run_time=4)
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
self.embed()
|
|
|
|
|
|
|
|
|
2021-11-11 15:22:35 -08:00
|
|
|
# Older scenes
|
2021-11-08 21:49:06 -08:00
|
|
|
class ShowInfinitelyFarLightSource(ShadowScene):
|
2020-12-31 13:05:22 -08:00
|
|
|
CONFIG = {
|
|
|
|
"num_reorientations": 1,
|
|
|
|
"camera_center": [0, 0, 1],
|
|
|
|
}
|
|
|
|
|
|
|
|
def construct(self):
|
|
|
|
self.force_skipping()
|
|
|
|
ShowShadows.construct(self)
|
|
|
|
self.revert_to_original_skipping_status()
|
|
|
|
|
|
|
|
self.add_light_source_based_shadow_updater()
|
|
|
|
self.add_light()
|
|
|
|
self.move_light_around()
|
|
|
|
self.show_vertical_lines()
|
|
|
|
|
|
|
|
def add_light(self):
|
|
|
|
light = self.light = self.get_light()
|
|
|
|
light_source = self.camera.light_source
|
|
|
|
light.move_to(light_source)
|
|
|
|
light_source.add_updater(lambda m: m.move_to(light))
|
|
|
|
self.add(light_source)
|
|
|
|
self.add_fixed_orientation_mobjects(light)
|
|
|
|
|
|
|
|
def move_light_around(self):
|
|
|
|
light = self.light
|
|
|
|
self.add_foreground_mobjects(self.shadow_area_label)
|
|
|
|
self.play(
|
|
|
|
light.move_to, 5 * OUT + DOWN,
|
|
|
|
run_time=3
|
|
|
|
)
|
|
|
|
self.play(Rotating(
|
|
|
|
light, angle=TAU, about_point=5 * OUT,
|
|
|
|
rate_func=smooth, run_time=3
|
|
|
|
))
|
|
|
|
self.play(
|
|
|
|
light.move_to, 30 * OUT,
|
|
|
|
run_time=3,
|
|
|
|
)
|
|
|
|
self.remove(light)
|
|
|
|
|
|
|
|
def show_vertical_lines(self):
|
|
|
|
lines = self.get_vertical_lines()
|
|
|
|
obj3d = self.obj3d
|
|
|
|
shadow = self.shadow
|
|
|
|
target_obj3d = obj3d.copy()
|
|
|
|
target_obj3d.become(shadow)
|
|
|
|
target_obj3d.match_style(obj3d)
|
|
|
|
target_obj3d.set_shade_in_3d(False)
|
|
|
|
source_obj3d = obj3d.copy()
|
|
|
|
source_obj3d.set_shade_in_3d(False)
|
|
|
|
source_obj3d.fade(1)
|
|
|
|
|
|
|
|
self.play(LaggedStartMap(ShowCreation, lines))
|
|
|
|
self.wait()
|
|
|
|
self.add(source_obj3d, lines)
|
|
|
|
self.play(
|
|
|
|
ReplacementTransform(source_obj3d, target_obj3d),
|
|
|
|
run_time=2
|
|
|
|
)
|
|
|
|
self.add(target_obj3d, lines)
|
|
|
|
self.play(FadeOut(target_obj3d),)
|
|
|
|
self.wait()
|
|
|
|
lines.add_updater(lambda m: m.become(self.get_vertical_lines()))
|
|
|
|
for x in range(5):
|
|
|
|
self.randomly_reorient()
|
|
|
|
|
|
|
|
def add_light_source_based_shadow_updater(self):
|
|
|
|
shadow = self.shadow
|
|
|
|
light_source = self.camera.light_source
|
|
|
|
obj3d = self.obj3d
|
|
|
|
center = obj3d.get_center()
|
|
|
|
|
|
|
|
def update(shadow):
|
|
|
|
lsp = light_source.get_center()
|
|
|
|
proj_center = get_xy_plane_projection_point(lsp, center)
|
|
|
|
c_to_lsp = lsp - center
|
|
|
|
unit_c_to_lsp = normalize(c_to_lsp)
|
|
|
|
rotation = rotation_matrix(
|
|
|
|
angle=np.arccos(np.dot(unit_c_to_lsp, OUT)),
|
|
|
|
axis=normalize(np.cross(unit_c_to_lsp, OUT))
|
|
|
|
)
|
|
|
|
new_shadow = get_shadow(
|
|
|
|
self.obj3d.copy().apply_matrix(rotation)
|
|
|
|
)
|
|
|
|
shadow.become(new_shadow)
|
|
|
|
shadow.scale(get_norm(lsp) / get_norm(c_to_lsp))
|
|
|
|
shadow.move_to(proj_center)
|
|
|
|
return shadow
|
|
|
|
shadow.add_updater(update)
|
|
|
|
|
|
|
|
def get_light(self):
|
|
|
|
n_rings = 40
|
|
|
|
radii = np.linspace(0, 2, n_rings)
|
|
|
|
rings = VGroup(*[
|
|
|
|
Annulus(inner_radius=r1, outer_radius=r2)
|
|
|
|
for r1, r2 in zip(radii, radii[1:])
|
|
|
|
])
|
|
|
|
opacities = np.linspace(1, 0, n_rings)**1.5
|
|
|
|
for opacity, ring in zip(opacities, rings):
|
|
|
|
ring.set_fill(YELLOW, opacity)
|
|
|
|
ring.set_stroke(YELLOW, width=0.1, opacity=opacity)
|
|
|
|
return rings
|
|
|
|
|
|
|
|
def get_vertical_lines(self):
|
|
|
|
shadow = self.shadow
|
|
|
|
points = get_boundary_points(shadow, 10)
|
|
|
|
# half_points = [(p1 + p2) / 2 for p1, p2 in adjacent_pairs(points)]
|
|
|
|
# points = np.append(points, half_points, axis=0)
|
|
|
|
light_source = self.light.get_center()
|
|
|
|
lines = VGroup(*[
|
|
|
|
DashedLine(light_source, point)
|
|
|
|
for point in points
|
|
|
|
])
|
|
|
|
lines.set_shade_in_3d(True)
|
|
|
|
for line in lines:
|
|
|
|
line.remove(*line[:int(0.8 * len(line))])
|
|
|
|
line[-10:].set_shade_in_3d(False)
|
|
|
|
line.set_stroke(YELLOW, 1)
|
|
|
|
return lines
|
|
|
|
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
class CylinderShadows(ShadowScene):
|
2020-12-31 13:05:22 -08:00
|
|
|
CONFIG = {
|
|
|
|
"surface_area": 2 * PI + 2 * PI * 2,
|
|
|
|
"area_label_center": [0, -2, 0],
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_object(self):
|
|
|
|
height = 2
|
|
|
|
cylinder = ParametricSurface(
|
|
|
|
lambda u, v: np.array([
|
|
|
|
np.cos(TAU * v),
|
|
|
|
np.sin(TAU * v),
|
|
|
|
height * (1 - u)
|
|
|
|
]),
|
|
|
|
resolution=(6, 32)
|
|
|
|
)
|
|
|
|
# circle = Circle(radius=1)
|
|
|
|
circle = ParametricSurface(
|
|
|
|
lambda u, v: np.array([
|
|
|
|
(v + 0.01) * np.cos(TAU * u),
|
|
|
|
(v + 0.01) * np.sin(TAU * u),
|
|
|
|
0,
|
|
|
|
]),
|
|
|
|
resolution=(16, 8)
|
|
|
|
)
|
|
|
|
# circle.set_fill(GREEN, opacity=0.5)
|
|
|
|
for surface in cylinder, circle:
|
|
|
|
surface.set_fill_by_checkerboard(GREEN, GREEN_E, opacity=1.0)
|
|
|
|
# surface.set_fill(GREEN, opacity=0.5)
|
|
|
|
cylinder.add(circle)
|
|
|
|
cylinder.add(circle.copy().flip().move_to(height * OUT))
|
|
|
|
cylinder.set_shade_in_3d(True)
|
|
|
|
cylinder.set_stroke(width=0)
|
|
|
|
cylinder.scale(1.003)
|
|
|
|
return cylinder
|
|
|
|
|
|
|
|
|
2021-11-08 21:49:06 -08:00
|
|
|
class PrismShadows(ShadowScene):
|
2020-12-31 13:05:22 -08:00
|
|
|
CONFIG = {
|
|
|
|
"surface_area": 3 * np.sqrt(3) / 2 + 3 * (np.sqrt(3) * 2),
|
|
|
|
"object_center": [0, 0, 3],
|
|
|
|
"area_label_center": [0, -2.25, 0],
|
|
|
|
}
|
|
|
|
|
|
|
|
def get_object(self):
|
|
|
|
height = 2
|
|
|
|
prism = VGroup()
|
|
|
|
triangle = RegularPolygon(3)
|
|
|
|
verts = triangle.get_anchors()[:3]
|
|
|
|
rects = [
|
|
|
|
Polygon(v1, v2, v2 + height * OUT, v1 + height * OUT)
|
|
|
|
for v1, v2 in adjacent_pairs(verts)
|
|
|
|
]
|
|
|
|
prism.add(triangle, *rects)
|
|
|
|
prism.add(triangle.copy().shift(height * OUT))
|
|
|
|
triangle.reverse_points()
|
|
|
|
prism.set_shade_in_3d(True)
|
|
|
|
prism.set_fill(PINK, 0.8)
|
|
|
|
prism.set_stroke(WHITE, 1)
|
|
|
|
return prism
|
|
|
|
|
|
|
|
|
|
|
|
class TheseFourPiAreSquare(PiCreatureScene):
|
|
|
|
def construct(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def create_pi_creatures(self):
|
|
|
|
pass
|