diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 46257a5b..1fed87e4 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -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) @@ -261,13 +261,14 @@ class EquationSolver1d(GraphScene, ZoomedScene, ReconfigurableScene): # 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 PiColorWakler +# This could involve some merging of functoinality with PiWalker class LinePulser(ContinualAnimation): - def __init__(self, line, bullet_template, num_bullets, pulse_time, **kwargs): + 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): @@ -275,8 +276,11 @@ class LinePulser(ContinualAnimation): 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)))) + 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): @@ -298,7 +302,7 @@ class ArrowCircleTest(Scene): 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.rotate(revs * TAU) x.set_color(color_func(revs)) return x @@ -322,8 +326,9 @@ class FuncRotater(Animation): 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 * 2 * np.pi, + -angle_revs * TAU, about_point = ORIGIN ) self.mobject.set_color(color_func(angle_revs)) @@ -336,21 +341,40 @@ class TestRotater(Scene): 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 * 2 * np.pi), - "run_time" : 5 + "rotate_func" : lambda x : np.sin(x * TAU), + "run_time" : 5, + "dashed_line_angle" : None, + "biased_display_start" : None } def construct(self): - base_arrow = Arrow(ORIGIN, RIGHT) - circle = Circle(center = ORIGIN, radius = 1.3) + 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, self.rotate_func), + ChangingDecimal(num_display, display_func), run_time = self.run_time, rate_func = None) @@ -361,7 +385,7 @@ def point_to_rev((x, y)): 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) + 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): @@ -441,6 +465,8 @@ class RectangleData(): 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]) @@ -455,10 +481,120 @@ def plane_poly_with_roots(*points): 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 @@ -495,8 +631,17 @@ class EquationSolver2d(Scene): 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( - ShowCreation, line, + line_draw_anim, Transform, line, thin_line ) return (anim, rebased_winder(1)) @@ -523,6 +668,8 @@ class EquationSolver2d(Scene): 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: @@ -541,7 +688,7 @@ class EquationSolver2d(Scene): mid_line = DashedLine(*mid_line_points) return Succession(anim, ShowCreation(mid_line), - FadeOut(mid_line), + # FadeOut(mid_line), # TODO: Can change timing so this fades out at just the time it would be overdrawn AnimationGroup(*sub_anims) ) @@ -565,80 +712,6 @@ class EquationSolver2d(Scene): self.wait() -class PiColorWalker(Scene): - CONFIG = { - "func" : plane_func_from_complex_func(lambda c : c**2), - "start_x" : -1, - "start_y" : 1, - "walk_width" : 2, - "walk_height" : 2, - } - - def construct(self): - - rev_func = lambda p : point_to_rev(self.func(p)) - - num_plane = NumberPlane() - num_plane.fade() - self.add(num_plane) - - class WalkerAnimation(Animation): - CONFIG = { - "remover" : True, - "rate_func" : None, - "start_coords" : None, - "end_coords" : None, - } - - def __init__(self, start_coords, end_coords, **kwargs): - self.start_coords = start_coords - self.end_coords = end_coords - 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 = interpolate(self.start_coords, self.end_coords, alpha) - self.mobject.walker.move_to(num_plane.coords_to_point(cur_x, cur_y)) - rev = 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 * 2 * np.pi, - about_point = ORIGIN #self.mobject.arrow.get_start() - ) - - - 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) - - walk_coords = [TL, TR, BR, BL] - 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( - WalkerAnimation( - start_coords = start_coords, - end_coords = end_coords, - remover = (i < len(walk_coords) - 1) - ), - ShowCreation(Line(start_point, end_point))) - - - self.wait() - ############# # Above are mostly general tools; here, we list, in order, finished or near-finished scenes @@ -679,31 +752,62 @@ class NumberLineScene(Scene): # TODO: Add labels, arrows, specific points 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)) + 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, 2 * UP, RIGHT, DOWN, DOWN + RIGHT, LEFT] + random_points = [UP + LEFT, UP + RIGHT, DOWN + RIGHT, DOWN + LEFT] - interval_2d = Polygon( + border_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? + 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(interval_1d, interval_2d)) + ReplacementTransform(full_1d, full_2d)) self.wait() -# TODO: Split screen illustration of 2d function (before domain coloring) +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 @@ -844,13 +948,13 @@ 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): left_camera = Camera(**self.camera_config) right_camera = MappingCamera( mapping_func = lambda (x, y, z) : complex_to_R3(((complex(x,y) + 3)**1.1) - 3), - allow_object_intrusion = False **self.camera_config) split_screen_camera = SplitScreenCamera(left_camera, right_camera, **self.camera_config) self.camera = split_screen_camera @@ -860,11 +964,35 @@ class LoopSplitSceneMapped(LoopSplitScene): class FundThmAlg(EquationSolver2d): CONFIG = { "func" : plane_poly_with_roots((1, 2), (-1, 3), (-1, 3)), + "num_iterations" : 1, } # TODO: Borsuk-Ulam visuals +# Note: May want to do an ordinary square scene, then mapping func it into a circle +# class BorsukUlamScene(PiWalker): -# TODO: "Good enough" property visuals (swinging arrows and odometer, like in odometer scene above) +# 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 LeftOdometer(OdometerScene): + CONFIG = { + "rotate_func" : left_func, + "biased_display_start" : 0 + } + +class RightOdometer(OdometerScene): + CONFIG = { + "rotate_func" : lambda x : left_func(x) + diff_func(x), + "biased_display_start" : 0 + } + +class DiffOdometer(OdometerScene): + CONFIG = { + "rotate_func" : diff_func, + "dashed_line_angle" : 0.5, + "biased_display_start" : 0 + } # TODO: Brouwer's fixed point theorem visuals @@ -876,16 +1004,18 @@ class FundThmAlg(EquationSolver2d): # Minor fiddling with little things in each animation; placements, colors, timing -# Writing new Pi creature walker scenes off of general template - # Odometer/swinging arrows stuff -# Pi creature emotion stuff +# Writing new Pi creature walker scenes off of general template # Split screen illustration of 2d function (before domain coloring) # Generalizing Pi color walker stuff/making bullets on pulsing lines change colors dynamically according to function traced out +# ---- + +# Pi creature emotion stuff + # BFT visuals # Borsuk-Ulam visuals