Merge branch 'master' into lighthouse

This commit is contained in:
Ben Hambrecht 2018-01-29 19:49:40 +01:00
commit ad2b1c3933
11 changed files with 1824 additions and 417 deletions

View file

@ -128,7 +128,7 @@ class EquationSolver1d(GraphScene, ZoomedScene, ReconfigurableScene):
downBrace, upBrace = yBraces = TexMobject("||")
yBraces.stretch(2, 0)
yBraces.rotate(np.pi/2)
yBraces.rotate(TAU/4)
lowerX = self.initial_lower_x
lowerY = self.func(lowerX)
@ -256,28 +256,567 @@ class EquationSolver1d(GraphScene, ZoomedScene, ReconfigurableScene):
self.drawGraph()
self.solveEquation()
class FirstSqrtScene(EquationSolver1d):
# TODO: Perhaps have bullets (pulses) fade out and in at ends of line, instead of jarringly
# popping out and in?
#
# TODO: Perhaps have bullets change color corresponding to a function of their coordinates?
# This could involve some merging of functoinality with PiWalker
class LinePulser(ContinualAnimation):
def __init__(self, line, bullet_template, num_bullets, pulse_time, color_func = None, **kwargs):
self.line = line
self.num_bullets = num_bullets
self.pulse_time = pulse_time
self.bullets = [bullet_template.copy() for i in range(num_bullets)]
self.color_func = color_func
ContinualAnimation.__init__(self, VGroup(line, VGroup(*self.bullets)), **kwargs)
def update_mobject(self, dt):
alpha = self.external_time % self.pulse_time
start = self.line.get_start()
end = self.line.get_end()
for i in range(self.num_bullets):
position = interpolate(start, end,
np.true_divide((i + alpha),(self.num_bullets)))
self.bullets[i].move_to(position)
if self.color_func:
self.bullets.set_color(self.color_func(position))
def color_func(alpha):
alpha = alpha % 1
colors = ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"]
num_colors = len(colors)
beta = (alpha % (1.0/num_colors)) * num_colors
start_index = int(np.floor(num_colors * alpha)) % num_colors
end_index = (start_index + 1) % num_colors
return interpolate_color(colors[start_index], colors[end_index], beta)
class ArrowCircleTest(Scene):
def construct(self):
circle_radius = 3
circle = Circle(radius = circle_radius, color = WHITE)
self.add(circle)
base_arrow = Arrow(circle_radius * 0.7 * RIGHT, circle_radius * 1.3 * RIGHT)
def rev_rotate(x, revs):
x.rotate(revs * TAU)
x.set_color(color_func(revs))
return x
num_arrows = 8 * 3
arrows = [rev_rotate(base_arrow.copy(), (np.true_divide(i, num_arrows))) for i in range(num_arrows)]
arrows_vgroup = VGroup(*arrows)
self.play(ShowCreation(arrows_vgroup), run_time = 2.5, rate_func = None)
self.wait()
class FuncRotater(Animation):
CONFIG = {
"x_min" : 0,
"x_max" : 2.5,
"y_min" : 0,
"y_max" : 2.5**2,
"graph_origin" : 2*DOWN + 5 * LEFT,
"x_axis_width" : 12,
"zoom_factor" : 3,
"zoomed_canvas_center" : 2.25 * UP + 1.75 * LEFT,
"func" : lambda x : x**2,
"targetX" : np.sqrt(2),
"targetY" : 2,
"initial_lower_x" : 1,
"initial_upper_x" : 2,
"num_iterations" : 10,
"iteration_at_which_to_start_zoom" : 3,
"graph_label" : "y = x^2",
"show_target_line" : True,
"rotate_func" : lambda x : x # Func from alpha to revolutions
}
# Perhaps abstract this out into an "Animation updating from original object" class
def update_submobject(self, submobject, starting_submobject, alpha):
submobject.points = np.array(starting_submobject.points)
def update_mobject(self, alpha):
Animation.update_mobject(self, alpha)
angle_revs = self.rotate_func(alpha)
# We do a clockwise rotation
self.mobject.rotate(
-angle_revs * TAU,
about_point = ORIGIN
)
self.mobject.set_color(color_func(angle_revs))
class TestRotater(Scene):
def construct(self):
test_line = Line(ORIGIN, RIGHT)
self.play(FuncRotater(
test_line,
rotate_func = lambda x : x % 0.25,
run_time = 10))
# TODO: Be careful about clockwise vs. counterclockwise convention throughout!
# Make sure this is correct everywhere in resulting video.
class OdometerScene(Scene):
CONFIG = {
"rotate_func" : lambda x : np.sin(x * TAU),
"run_time" : 5,
"dashed_line_angle" : None,
"biased_display_start" : None
}
def construct(self):
radius = 1.3
circle = Circle(center = ORIGIN, radius = radius)
self.add(circle)
if self.dashed_line_angle:
dashed_line = DashedLine(ORIGIN, radius * RIGHT)
# Clockwise rotation
dashed_line.rotate(-self.dashed_line_angle * TAU, about_point = ORIGIN)
self.add(dashed_line)
num_display = DecimalNumber(0)
num_display.move_to(2 * DOWN)
display_val_bias = 0
if self.biased_display_start != None:
display_val_bias = self.biased_display_start - self.rotate_func(0)
display_func = lambda alpha : self.rotate_func(alpha) + display_val_bias
base_arrow = Arrow(ORIGIN, RIGHT, buff = 0)
self.play(
FuncRotater(base_arrow, rotate_func = self.rotate_func),
ChangingDecimal(num_display, display_func),
run_time = self.run_time,
rate_func = None)
def point_to_rev((x, y)):
# Warning: np.arctan2 would happily discontinuously returns the value 0 for (0, 0), due to
# design choices in the underlying atan2 library call, but for our purposes, this is
# illegitimate, and all winding number calculations must be set up to avoid this
if (x, y) == (0, 0):
print "Error! Angle of (0, 0) computed!"
return None
return np.true_divide(np.arctan2(y, x), TAU)
# Returns the value with the same fractional component as x, closest to m
def resit_near(x, m):
frac_diff = (x - m) % 1
if frac_diff > 0.5:
frac_diff -= 1
return m + frac_diff
# TODO?: Perhaps use modulus of (uniform) continuity instead of num_checkpoints, calculating
# latter as needed from former?
def make_alpha_winder(func, start, end, num_checkpoints):
check_points = [None for i in range(num_checkpoints)]
check_points[0] = func(start)
step_size = np.true_divide(end - start, num_checkpoints)
for i in range(num_checkpoints - 1):
check_points[i + 1] = \
resit_near(
func(start + (i + 1) * step_size),
check_points[i])
def return_func(alpha):
index = clamp(0, num_checkpoints - 1, int(alpha * num_checkpoints))
x = interpolate(start, end, alpha)
return resit_near(func(x), check_points[index])
return return_func
def split_interval((a, b)):
mid = (a + b)/2.0
return ((a, mid), (mid, b))
class RectangleData():
def __init__(self, x_interval, y_interval):
self.rect = (x_interval, y_interval)
def get_top_left(self):
return np.array((self.rect[0][0], self.rect[1][1]))
def get_top_right(self):
return np.array((self.rect[0][1], self.rect[1][1]))
def get_bottom_right(self):
return np.array((self.rect[0][1], self.rect[1][0]))
def get_bottom_left(self):
return np.array((self.rect[0][0], self.rect[1][0]))
def get_top(self):
return (self.get_top_left(), self.get_top_right())
def get_right(self):
return (self.get_top_right(), self.get_bottom_right())
def get_bottom(self):
return (self.get_bottom_right(), self.get_bottom_left())
def get_left(self):
return (self.get_bottom_left(), self.get_top_left())
def splits_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
# TODO: Can refactor the following; will do later
if dim == 0:
return_data = [RectangleData(new_interval, y_interval) for new_interval in split_interval(x_interval)]
elif dim == 1:
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)]
else:
print "RectangleData.splits_on_dim passed illegitimate dimension!"
return tuple(return_data)
def split_line_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
if dim == 0:
sides = (self.get_top(), self.get_bottom())
elif dim == 1:
sides = (self.get_left(), self.get_right())
else:
print "RectangleData.split_line_on_dim passed illegitimate dimension!"
return tuple([mid(x, y) for (x, y) in sides])
def complex_to_pair(c):
return (c.real, c.imag)
def plane_poly_with_roots(*points):
def f((x, y)):
return complex_to_pair(np.prod([complex(x, y) - complex(a,b) for (a,b) in points]))
return f
def plane_func_from_complex_func(f):
return lambda (x, y) : complex_to_pair(f(complex(x,y)))
def point_func_from_complex_func(f):
return lambda (x, y, z): complex_to_R3(f(complex(x, y)))
empty_animation = Animation(Mobject())
def EmptyAnimation():
return empty_animation
class WalkerAnimation(Animation):
CONFIG = {
"walk_func" : None, # Must be initialized to use
"remover" : True,
"rate_func" : None,
"coords_to_point" : None
}
def __init__(self, walk_func, rev_func, coords_to_point, **kwargs):
self.walk_func = walk_func
self.rev_func = rev_func
self.coords_to_point = coords_to_point
self.compound_walker = VGroup()
self.compound_walker.walker = PiCreature(color = RED)
self.compound_walker.walker.scale(0.35)
self.compound_walker.arrow = Arrow(ORIGIN, RIGHT) #, buff = 0)
self.compound_walker.digest_mobject_attrs()
Animation.__init__(self, self.compound_walker, **kwargs)
# Perhaps abstract this out into an "Animation updating from original object" class
def update_submobject(self, submobject, starting_submobject, alpha):
submobject.points = np.array(starting_submobject.points)
def update_mobject(self, alpha):
Animation.update_mobject(self, alpha)
cur_x, cur_y = cur_coords = self.walk_func(alpha)
self.mobject.walker.move_to(self.coords_to_point(cur_x, cur_y))
rev = self.rev_func(cur_coords)
self.mobject.walker.set_color(color_func(rev))
self.mobject.arrow.set_color(color_func(rev))
self.mobject.arrow.rotate(
rev * TAU,
about_point = ORIGIN #self.mobject.arrow.get_start()
)
def LinearWalker(start_coords, end_coords, coords_to_point, rev_func, **kwargs):
walk_func = lambda alpha : interpolate(start_coords, end_coords, alpha)
return WalkerAnimation(
walk_func = walk_func,
coords_to_point = coords_to_point,
rev_func = rev_func,
**kwargs)
class PiWalker(Scene):
CONFIG = {
"func" : plane_func_from_complex_func(lambda c : c**2),
"walk_coords" : [],
"step_run_time" : 1
}
def construct(self):
rev_func = lambda p : point_to_rev(self.func(p))
num_plane = NumberPlane()
num_plane.fade()
self.add(num_plane)
walk_coords = self.walk_coords
for i in range(len(walk_coords)):
start_x, start_y = start_coords = walk_coords[i]
start_point = num_plane.coords_to_point(start_x, start_y)
end_x, end_y = end_coords = walk_coords[(i + 1) % len(walk_coords)]
end_point = num_plane.coords_to_point(end_x, end_y)
self.play(
LinearWalker(
start_coords = start_coords,
end_coords = end_coords,
coords_to_point = num_plane.coords_to_point,
rev_func = rev_func,
remover = (i < len(walk_coords) - 1)
),
ShowCreation(Line(start_point, end_point)),
run_time = self.step_run_time)
self.wait()
class PiWalkerRect(PiWalker):
CONFIG = {
"start_x" : -1,
"start_y" : 1,
"walk_width" : 2,
"walk_height" : 2,
}
def setup(self):
TL = np.array((self.start_x, self.start_y))
TR = TL + (self.walk_width, 0)
BR = TR + (0, -self.walk_height)
BL = BR + (-self.walk_width, 0)
self.walk_coords = [TL, TR, BR, BL]
PiWalker.setup(self)
class PiWalkerCircle(PiWalker):
CONFIG = {
"radius" : 1,
"num_steps" : 100,
"step_run_time" : 0.01
}
def setup(self):
r = self.radius
N = self.num_steps
self.walk_coords = [r * np.array((np.cos(i * TAU/N), np.sin(i * TAU/N))) for i in range(N)]
PiWalker.setup(self)
# TODO: Perhaps restructure this to avoid using AnimationGroup, and instead
# use lists of animations or lists or other such data, to be merged and processed into parallel
# animations later
class EquationSolver2d(Scene):
CONFIG = {
"func" : plane_poly_with_roots((1, 2), (-1, 3)),
"initial_lower_x" : -5.1,
"initial_upper_x" : 5.1,
"initial_lower_y" : -3.1,
"initial_upper_y" : 3.1,
"num_iterations" : 5,
"num_checkpoints" : 10,
# TODO: Consider adding a "find_all_roots" flag, which could be turned off
# to only explore one of the two candidate subrectangles when both are viable
}
def construct(self):
num_plane = NumberPlane()
num_plane.fade()
self.add(num_plane)
rev_func = lambda p : point_to_rev(self.func(p))
def Animate2dSolver(cur_depth, rect, dim_to_split):
if cur_depth >= self.num_iterations:
return EmptyAnimation()
def draw_line_return_wind(start, end, start_wind):
alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints)
a0 = alpha_winder(0)
rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind
line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end),
stroke_width = 5,
color = RED)
thin_line = line.copy()
thin_line.set_stroke(width = 1)
walker_anim = LinearWalker(
start_coords = start,
end_coords = end,
coords_to_point = num_plane.coords_to_point,
rev_func = rev_func,
remover = True
)
line_draw_anim = AnimationGroup(ShowCreation(line, rate_func = None), walker_anim,
run_time = 2)
anim = Succession(
line_draw_anim,
Transform, line, thin_line
)
return (anim, rebased_winder(1))
wind_so_far = 0
anim = EmptyAnimation()
sides = [
rect.get_top(),
rect.get_right(),
rect.get_bottom(),
rect.get_left()
]
for (start, end) in sides:
(next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far)
anim = Succession(anim, next_anim)
total_wind = round(wind_so_far)
if total_wind == 0:
coords = [
rect.get_top_left(),
rect.get_top_right(),
rect.get_bottom_right(),
rect.get_bottom_left()
]
points = [num_plane.coords_to_point(x, y) for (x, y) in coords]
# TODO: Maybe use diagonal lines or something to fill in rectangles indicating
# their "Nothing here" status?
fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = RED)
return Succession(anim, FadeIn(fill_rect))
else:
(sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split)
sub_rects = [sub_rect1, sub_rect2]
sub_anims = [
Animate2dSolver(
cur_depth = cur_depth + 1,
rect = sub_rect,
dim_to_split = 1 - dim_to_split
)
for sub_rect in sub_rects
]
mid_line_coords = rect.split_line_on_dim(dim_to_split)
mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords]
mid_line = DashedLine(*mid_line_points)
return Succession(anim,
ShowCreation(mid_line),
# FadeOut(mid_line), # TODO: Can change timing so this fades out at just the time it would be overdrawn
AnimationGroup(*sub_anims)
)
lower_x = self.initial_lower_x
upper_x = self.initial_upper_x
lower_y = self.initial_lower_y
upper_y = self.initial_upper_y
x_interval = (lower_x, upper_x)
y_interval = (lower_y, upper_y)
rect = RectangleData(x_interval, y_interval)
anim = Animate2dSolver(
cur_depth = 0,
rect = rect,
dim_to_split = 0,
)
self.play(anim)
self.wait()
#############
# Above are mostly general tools; here, we list, in order, finished or near-finished scenes
class FirstSqrtScene(EquationSolver1d):
CONFIG = {
"x_min" : 0,
"x_max" : 2.5,
"y_min" : 0,
"y_max" : 2.5**2,
"graph_origin" : 2*DOWN + 5 * LEFT,
"x_axis_width" : 12,
"zoom_factor" : 3,
"zoomed_canvas_center" : 2.25 * UP + 1.75 * LEFT,
"func" : lambda x : x**2,
"targetX" : np.sqrt(2),
"targetY" : 2,
"initial_lower_x" : 1,
"initial_upper_x" : 2,
"num_iterations" : 10,
"iteration_at_which_to_start_zoom" : 3,
"graph_label" : "y = x^2",
"show_target_line" : True,
}
# TODO: Pi creatures intrigued
class ComplexPlaneIs2d(Scene):
def construct(self):
com_plane = ComplexPlane()
self.add(com_plane)
# TODO: Add labels to axes, specific complex points
self.wait()
class NumberLineScene(Scene):
def construct(self):
num_line = NumberLine()
self.add(num_line)
# TODO: Add labels, arrows, specific points
self.wait()
border_color = PURPLE_E
inner_color = RED
stroke_width = 10
left_point = num_line.number_to_point(-1)
right_point = num_line.number_to_point(1)
interval_1d = Line(left_point, right_point,
stroke_color = inner_color, stroke_width = stroke_width)
left_dot = Dot(left_point, stroke_width = stroke_width, color = border_color)
right_dot = Dot(right_point, stroke_width = stroke_width, color = border_color)
endpoints_1d = VGroup(left_dot, right_dot)
full_1d = VGroup(interval_1d, endpoints_1d)
self.play(ShowCreation(full_1d))
self.wait()
num_plane = NumberPlane()
random_points = [UP + LEFT, UP + RIGHT, DOWN + RIGHT, DOWN + LEFT]
border_2d = Polygon(
*random_points,
stroke_color = border_color,
stroke_width = stroke_width)
filling_2d = Polygon(
*random_points,
fill_color = inner_color,
fill_opacity = 0.8,
stroke_width = stroke_width)
full_2d = VGroup(filling_2d, border_2d)
self.play(
FadeOut(num_line),
FadeIn(num_plane),
ReplacementTransform(full_1d, full_2d))
self.wait()
class Initial2dFuncScene(Scene):
def setup(self):
left_camera = Camera(**self.camera_config)
right_camera = MappingCamera(
mapping_func = point_func_from_complex_func(lambda c : np.exp(c)),
**self.camera_config)
split_screen_camera = SplitScreenCamera(left_camera, right_camera, **self.camera_config)
self.camera = split_screen_camera
def construct(self):
num_plane = NumberPlane()
num_plane.fade()
self.add(num_plane)
points = [LEFT + DOWN, RIGHT + DOWN, LEFT + UP, RIGHT + UP]
for i in range(len(points) - 1):
line = Line(points[i], points[i + 1], color = RED)
self.play(ShowCreation(line))
# TODO: Illustrations for introducing domain coloring
# TODO: Bunch of Pi walker scenes
# TODO: An odometer scene when introducing winding numbers
class SecondSqrtScene(FirstSqrtScene, ReconfigurableScene):
# TODO: Don't bother with ReconfigurableScene; just use new config from start
def setup(self):
FirstSqrtScene.setup(self)
@ -298,26 +837,6 @@ class SecondSqrtScene(FirstSqrtScene, ReconfigurableScene):
graph_origin = newOrigin)
self.solveEquation()
# TODO: Perhaps have bullets (pulses) fade out and in at ends of line, instead of jarringly
# popping out and in?
#
# TODO: Perhaps have bullets change color corresponding to a function of their coordinates?
class LinePulser(ContinualAnimation):
def __init__(self, line, bullet_template, num_bullets, pulse_time, **kwargs):
self.line = line
self.num_bullets = num_bullets
self.pulse_time = pulse_time
self.bullets = [bullet_template.copy() for i in range(num_bullets)]
ContinualAnimation.__init__(self, VGroup(line, VGroup(*self.bullets)), **kwargs)
def update_mobject(self, dt):
alpha = self.external_time % self.pulse_time
start = self.line.get_start()
end = self.line.get_end()
for i in range(self.num_bullets):
self.bullets[i].move_to(interpolate(start, end,
np.true_divide((i + alpha),(self.num_bullets))))
class LoopSplitScene(Scene):
def PulsedLine(self, start, end, bullet_template, num_bullets = 4, pulse_time = 1, **kwargs):
@ -327,6 +846,8 @@ class LoopSplitScene(Scene):
def construct(self):
num_plane = NumberPlane(color = LIGHT_GREY, stroke_width = 1)
# We actually don't want to highlight
num_plane.axes.set_stroke(color = WHITE, width = 2)
num_plane.fade()
self.add(num_plane)
@ -427,6 +948,7 @@ class LoopSplitScene(Scene):
self.wait()
# Is there a way to abstract this into a general process to derive a new mapped scene from an old scene?
class LoopSplitSceneMapped(LoopSplitScene):
def setup(self):
@ -437,318 +959,67 @@ class LoopSplitSceneMapped(LoopSplitScene):
split_screen_camera = SplitScreenCamera(left_camera, right_camera, **self.camera_config)
self.camera = split_screen_camera
class NumberLineScene(Scene):
def construct(self):
num_line = NumberLine()
self.add(num_line)
self.wait()
interval_1d = Line(num_line.number_to_point(-1), num_line.number_to_point(1),
stroke_color = RED, stroke_width = 10)
self.play(ShowCreation(interval_1d))
self.wait()
num_plane = NumberPlane()
random_points = [UP + LEFT, 2 * UP, RIGHT, DOWN, DOWN + RIGHT, LEFT]
interval_2d = Polygon(
*random_points,
stroke_color = RED,
stroke_width = 10)
# TODO: Turn this into a more complicated, curvy loop?
# TODO: Illustrate borders and filled interiors with a particular color
# on both 1d and 2d region?
self.play(
FadeOut(num_line),
FadeIn(num_plane),
ReplacementTransform(interval_1d, interval_2d))
self.wait()
def color_func(alpha):
alpha = alpha % 1
colors = ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"]
num_colors = len(colors)
beta = (alpha % (1.0/num_colors)) * num_colors
start_index = int(np.floor(num_colors * alpha)) % num_colors
end_index = (start_index + 1) % num_colors
return interpolate_color(colors[start_index], colors[end_index], beta)
class ArrowCircleTest(Scene):
def construct(self):
circle_radius = 3
circle = Circle(radius = circle_radius, color = WHITE)
self.add(circle)
base_arrow = Arrow(circle_radius * 0.7 * RIGHT, circle_radius * 1.3 * RIGHT)
def rev_rotate(x, revs):
x.rotate(revs * 2 * np.pi)
x.set_color(color_func(revs))
return x
num_arrows = 8 * 3
arrows = [rev_rotate(base_arrow.copy(), (np.true_divide(i, num_arrows))) for i in range(num_arrows)]
arrows_vgroup = VGroup(*arrows)
self.play(ShowCreation(arrows_vgroup), run_time = 2.5, rate_func = None)
self.wait()
class FuncRotater(Animation):
# TODO: Perhaps do extra illustration of zooming out and winding around a large circle,
# to illustrate relation between degree and large-scale winding number
class FundThmAlg(EquationSolver2d):
CONFIG = {
"rotate_func" : lambda x : x # Func from alpha to revolutions
"func" : plane_poly_with_roots((1, 2), (-1, 3), (-1, 3)),
"num_iterations" : 1,
}
# Perhaps abstract this out into an "Animation from base object" class
def update_submobject(self, submobject, starting_submobject, alpha):
submobject.points = np.array(starting_submobject.points)
# TODO: Borsuk-Ulam visuals
# Note: May want to do an ordinary square scene, then mapping func it into a circle
# class BorsukUlamScene(PiWalker):
def update_mobject(self, alpha):
Animation.update_mobject(self, alpha)
angle_revs = self.rotate_func(alpha)
self.mobject.rotate(
angle_revs * 2 * np.pi,
)
self.mobject.set_color(color_func(angle_revs))
# 3-way scene of "Good enough"-illustrating odometers; to be composed in Premiere
left_func = lambda x : x**2 - x + 1
diff_func = lambda x : np.cos(1.4 * (x - 0.1) * (np.log(x + 0.1) - 0.3) * TAU)/2.1
class TestRotater(Scene):
def construct(self):
test_line = Line(ORIGIN, RIGHT)
self.play(FuncRotater(
test_line,
rotate_func = lambda x : x % 0.25,
run_time = 10))
class OdometerScene(Scene):
class LeftOdometer(OdometerScene):
CONFIG = {
"rotate_func" : lambda x : np.sin(x * 2 * np.pi),
"run_time" : 5
"rotate_func" : left_func,
"biased_display_start" : 0
}
def construct(self):
base_arrow = Arrow(ORIGIN, RIGHT)
circle = Circle(center = ORIGIN, radius = 1.3)
self.add(circle)
num_display = DecimalNumber(0)
num_display.move_to(2 * DOWN)
self.play(
FuncRotater(base_arrow, rotate_func = self.rotate_func),
ChangingDecimal(num_display, self.rotate_func),
run_time = self.run_time,
rate_func = None)
def point_to_rev((x, y)):
# Warning: np.arctan2 would happily discontinuously returns the value 0 for (0, 0), due to
# design choices in the underlying atan2 library call, but for our purposes, this is
# illegitimate, and all winding number calculations must be set up to avoid this
if (x, y) == (0, 0):
print "Error! Angle of (0, 0) computed!"
return None
return np.true_divide(np.arctan2(y, x), 2 * np.pi)
# Returns the value with the same fractional component as x, closest to m
def resit_near(x, m):
frac_diff = (x - m) % 1
if frac_diff > 0.5:
frac_diff -= 1
return m + frac_diff
# TODO?: Perhaps use modulus of (uniform) continuity instead of num_checkpoints, calculating
# latter as needed from former?
def make_alpha_winder(func, start, end, num_checkpoints):
check_points = [None for i in range(num_checkpoints)]
check_points[0] = func(start)
step_size = np.true_divide(end - start, num_checkpoints)
for i in range(num_checkpoints - 1):
check_points[i + 1] = \
resit_near(
func(start + (i + 1) * step_size),
check_points[i])
def return_func(alpha):
index = clamp(0, num_checkpoints - 1, int(alpha * num_checkpoints))
x = interpolate(start, end, alpha)
return resit_near(func(x), check_points[index])
return return_func
def split_interval((a, b)):
mid = (a + b)/2.0
return ((a, mid), (mid, b))
class RectangleData():
def __init__(self, x_interval, y_interval):
self.rect = (x_interval, y_interval)
def get_top_left(self):
return np.array((self.rect[0][0], self.rect[1][1]))
def get_top_right(self):
return np.array((self.rect[0][1], self.rect[1][1]))
def get_bottom_right(self):
return np.array((self.rect[0][1], self.rect[1][0]))
def get_bottom_left(self):
return np.array((self.rect[0][0], self.rect[1][0]))
def get_top(self):
return (self.get_top_left(), self.get_top_right())
def get_right(self):
return (self.get_top_right(), self.get_bottom_right())
def get_bottom(self):
return (self.get_bottom_right(), self.get_bottom_left())
def get_left(self):
return (self.get_bottom_left(), self.get_top_left())
def splits_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
# TODO: Can refactor the following; will do later
if dim == 0:
return_data = [RectangleData(new_interval, y_interval) for new_interval in split_interval(x_interval)]
elif dim == 1:
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)]
else:
print "RectangleData.splits_on_dim passed illegitimate dimension!"
return tuple(return_data)
def split_line_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
if dim == 0:
sides = (self.get_top(), self.get_bottom())
elif dim == 1:
sides = (self.get_left(), self.get_right())
return tuple([mid(x, y) for (x, y) in sides])
def complex_to_pair(c):
return (c.real, c.imag)
def plane_poly_with_roots(*points):
def f((x, y)):
return complex_to_pair(np.prod([complex(x, y) - complex(a,b) for (a,b) in points]))
return f
def plane_func_from_complex_func(f):
return lambda (x, y) : complex_to_pair(f(complex(x,y)))
empty_animation = Animation(Mobject())
def EmptyAnimation():
return empty_animation
# TODO: Perhaps restructure this to avoid using AnimationGroup/UnsyncedParallels, and instead
# use lists of animations or lists or other such data, to be merged and processed into parallel
# animations later
class EquationSolver2d(Scene):
class RightOdometer(OdometerScene):
CONFIG = {
"func" : plane_poly_with_roots((1, 2), (-1, 3)),
"initial_lower_x" : -5.1,
"initial_upper_x" : 5.1,
"initial_lower_y" : -3.1,
"initial_upper_y" : 3.1,
"num_iterations" : 5,
"num_checkpoints" : 10,
# TODO: Consider adding a "find_all_roots" flag, which could be turned off
# to only explore one of the two candidate subrectangles when both are viable
"rotate_func" : lambda x : left_func(x) + diff_func(x),
"biased_display_start" : 0
}
def construct(self):
num_plane = NumberPlane()
num_plane.fade()
self.add(num_plane)
class DiffOdometer(OdometerScene):
CONFIG = {
"rotate_func" : diff_func,
"dashed_line_angle" : 0.5,
"biased_display_start" : 0
}
rev_func = lambda p : point_to_rev(self.func(p))
# TODO: Brouwer's fixed point theorem visuals
def Animate2dSolver(cur_depth, rect, dim_to_split):
if cur_depth >= self.num_iterations:
return EmptyAnimation()
# TODO: Pi creatures wide-eyed in amazement
def draw_line_return_wind(start, end, start_wind):
alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints)
a0 = alpha_winder(0)
rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind
line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end),
stroke_width = 5,
color = RED)
thin_line = line.copy()
thin_line.set_stroke(width = 1)
anim = Succession(
ShowCreation, line,
Transform, line, thin_line
)
return (anim, rebased_winder(1))
#################
wind_so_far = 0
anim = EmptyAnimation()
sides = [
rect.get_top(),
rect.get_right(),
rect.get_bottom(),
rect.get_left()
]
for (start, end) in sides:
(next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far)
anim = Succession(anim, next_anim)
# TODOs, from easiest to hardest:
total_wind = round(wind_so_far)
# Minor fiddling with little things in each animation; placements, colors, timing
if total_wind == 0:
coords = [
rect.get_top_left(),
rect.get_top_right(),
rect.get_bottom_right(),
rect.get_bottom_left()
]
points = [num_plane.coords_to_point(x, y) for (x, y) in coords]
fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = RED)
return Succession(anim, FadeIn(fill_rect))
else:
(sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split)
sub_rects = [sub_rect1, sub_rect2]
sub_anims = [
Animate2dSolver(
cur_depth = cur_depth + 1,
rect = sub_rect,
dim_to_split = 1 - dim_to_split
)
for sub_rect in sub_rects
]
mid_line_coords = rect.split_line_on_dim(dim_to_split)
mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords]
mid_line = DashedLine(*mid_line_points)
return Succession(anim,
ShowCreation(mid_line),
FadeOut(mid_line),
UnsyncedParallel(*sub_anims)
)
# Odometer/swinging arrows stuff
lower_x = self.initial_lower_x
upper_x = self.initial_upper_x
lower_y = self.initial_lower_y
upper_y = self.initial_upper_y
# Writing new Pi creature walker scenes off of general template
x_interval = (lower_x, upper_x)
y_interval = (lower_y, upper_y)
# Split screen illustration of 2d function (before domain coloring)
rect = RectangleData(x_interval, y_interval)
# Generalizing Pi color walker stuff/making bullets on pulsing lines change colors dynamically according to function traced out
anim = Animate2dSolver(
cur_depth = 0,
rect = rect,
dim_to_split = 0,
)
# ----
self.play(anim)
# Pi creature emotion stuff
self.wait()
# BFT visuals
# Borsuk-Ulam visuals
# Domain coloring
# FIN

File diff suppressed because it is too large Load diff

View file

@ -460,11 +460,3 @@ class AnimationGroup(Animation):
def update_mobject(self, alpha):
for anim in self.sub_anims:
anim.update(alpha)
# Parallel animations where shorter animations are not stretched out to match the longest
class UnsyncedParallel(AnimationGroup):
def __init__(self, *sub_anims, **kwargs):
digest_config(self, kwargs, locals())
self.run_time = max([a.run_time for a in sub_anims])
everything = Mobject(*[a.mobject for a in sub_anims])
Animation.__init__(self, everything, **kwargs)

1
camera/__init__.py Normal file
View file

@ -0,0 +1 @@
from camera import *

View file

@ -454,14 +454,14 @@ class Mobject(object):
**kwargs
)
def match_width(self, mobject):
return self.match_dim(mobject, 0)
def match_width(self, mobject, **kwargs):
return self.match_dim(mobject, 0, **kwargs)
def match_height(self, mobject):
return self.match_dim(mobject, 1)
def match_height(self, mobject, **kwargs):
return self.match_dim(mobject, 1, **kwargs)
def match_depth(self, mobject):
return self.match_dim(mobject, 2)
def match_depth(self, mobject, **kwargs):
return self.match_dim(mobject, 2, **kwargs)
## Color functions

View file

@ -100,12 +100,12 @@ class VMobject(Mobject):
#match styles accordingly
submobs1, submobs2 = self.submobjects, vmobject.submobjects
if len(submobs1) == 0:
return
return self
elif len(submobs2) == 0:
submobs2 = [vmobject]
for sm1, sm2 in zip(*make_even(submobs1, submobs2)):
sm1.match_style(sm2)
return
return self
def fade(self, darkness = 0.5):
for submob in self.submobject_family():
@ -353,7 +353,8 @@ class VMobject(Mobject):
for index in range(num_curves):
curr_bezier_points = self.points[3*index:3*index+4]
num_inter_curves = sum(index_allocation == index)
alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves)
alphas = np.linspace(0, 1, num_inter_curves+1)
# alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves)
for a, b in zip(alphas, alphas[1:]):
new_points = partial_bezier_points(
curr_bezier_points, a, b

View file

@ -18,9 +18,7 @@ def get_sorted_scene_names(module_name):
for line_no in sorted(line_to_scene.keys())
]
def stage_animaions(module_name):
def stage_animations(module_name):
scene_names = get_sorted_scene_names(module_name)
animation_dir = os.path.join(
ANIMATIONS_DIR, module_name.replace(".py", "")
@ -32,19 +30,27 @@ def stage_animaions(module_name):
sorted_files.append(
os.path.join(animation_dir, clip)
)
for f in os.listdir(STAGED_SCENES_DIR):
os.remove(os.path.join(STAGED_SCENES_DIR, f))
for f, count in zip(sorted_files, it.count()):
staged_scenes_dir = os.path.join(animation_dir, "staged_scenes")
count = 0
while True:
staged_scenes_dir = os.path.join(
animation_dir, "staged_scenes_%d"%count
)
if not os.path.exists(staged_scenes_dir):
os.makedirs(staged_scenes_dir)
break
#Otherwise, keep trying new names until
#there is a free one
count += 1
for count, f in enumerate(sorted_files):
symlink_name = os.path.join(
STAGED_SCENES_DIR,
staged_scenes_dir,
"Scene_%03d"%count + f.split(os.sep)[-1]
)
os.symlink(f, symlink_name)
if __name__ == "__main__":
if len(sys.argv) < 2:
raise Exception("No module given.")
module_name = sys.argv[1]
stage_animaions(module_name)
stage_animations(module_name)

View file

@ -443,7 +443,9 @@ class PiCreatureScene(Scene):
added_anims = kwargs.pop("added_anims", [])
anims = []
on_screen_mobjects = self.get_mobjects()
on_screen_mobjects = self.camera.extract_mobject_family_members(
self.get_mobjects()
)
def has_bubble(pi):
return hasattr(pi, "bubble") and \
pi.bubble is not None and \
@ -478,7 +480,7 @@ class PiCreatureScene(Scene):
]
anims += added_anims
self.play(*anims)
self.play(*anims, **kwargs)
def pi_creature_says(self, *args, **kwargs):
self.introduce_bubble(

View file

@ -97,9 +97,6 @@ class Arc(VMobject):
return self
class Circle(Arc):
CONFIG = {
"color" : RED,
@ -356,6 +353,17 @@ class Line(VMobject):
self.shift(new_start - self.get_start())
return self
def insert_n_anchor_points(self, n):
if not self.path_arc:
n_anchors = self.get_num_anchor_points()
new_num_points = 3*(n_anchors + n)+1
self.points = np.array([
self.point_from_proportion(alpha)
for alpha in np.linspace(0, 1, new_num_points)
])
else:
VMobject.insert_n_anchor_points(self, n)
class DashedLine(Line):
CONFIG = {
"dashed_segment_length" : 0.05
@ -531,6 +539,7 @@ class Arrow(Line):
Line.put_start_and_end_on(self, *args, **kwargs)
self.set_tip_points(self.tip, preserve_normal = False)
self.set_rectangular_stem_points()
return self
def scale(self, scale_factor, **kwargs):
Line.scale(self, scale_factor, **kwargs)
@ -540,6 +549,9 @@ class Arrow(Line):
self.set_rectangular_stem_points()
return self
def copy(self):
return self.deepcopy()
class Vector(Arrow):
CONFIG = {
"color" : YELLOW,

View file

@ -200,14 +200,21 @@ class Axes(VGroup):
self.y_axis.point_to_number(point),
)
def get_graph(self, function, num_graph_points = None, **kwargs):
def get_graph(
self, function, num_graph_points = None,
x_min = None,
x_max = None,
**kwargs
):
kwargs["fill_opacity"] = kwargs.get("fill_opacity", 0)
kwargs["num_anchor_points"] = \
num_graph_points or self.default_num_graph_points
x_min = x_min or self.x_min
x_max = x_max or self.x_max
graph = ParametricFunction(
lambda t : self.coords_to_point(t, function(t)),
t_min = self.x_min,
t_max = self.x_max,
t_min = x_min,
t_max = x_max,
**kwargs
)
graph.underlying_function = function