From 1a8266b5a348ab252efaba100ed72c03b36f32ae Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Mon, 26 Feb 2018 16:07:50 -0800 Subject: [PATCH 1/6] Incremental WindingNumber changes (and hopefully fixes to merge snafu now removed frmo history) For reference: There had been a giant merge snafu (a merge on the Master branch of changes from Lighthouse2 that somehow overwrote many other files to older versions), apparently fixed on the Master branch by doing a reset to an older version and then a push -f. Before it was fixed, the merge snafu from Master was pulled into Sridhar's local WindingNumber, and then pushed to the remote WindingNumber as well. This was fixed by also doing a reset on Sridhar's local WindingNumber to the last good version and a push --force-with-lease. (There were some uncommitted changes on WindingNumber.py lost in the process, but luckily, by manually saving the file, those changes were reinserted, in this very commit). --- active_projects/WindingNumber.py | 106 ++++++++++++++++++------------- 1 file changed, 62 insertions(+), 44 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 92eb5759..456a59bb 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -133,19 +133,20 @@ class EquationSolver1d(GraphScene, ZoomedScene): # This is a mess right now (first major animation coded), # but it works; can be refactored later or never def solveEquation(self): - leftBrace = TexMobject("|") # Not using [ and ] because they end up crossing over - leftBrace.set_color(negative_color) - rightBrace = TexMobject("|") - rightBrace.set_color(positive_color) - xBraces = Group(leftBrace, rightBrace) - xBraces.stretch(2, 0) + startBrace = Dot() #TexMobject("[") # Not using [ and ] because they end up crossing over + startBrace.set_color(negative_color) + endBrace = Dot() + endBrace.set_color(positive_color) + genericBraces = Group(startBrace, endBrace) + #genericBraces.scale(1.5) - downBrace = TexMobject("|") - downBrace.set_color(negative_color) - upBrace = TexMobject("|") - upBrace.set_color(positive_color) + leftBrace = startBrace.copy() + rightBrace = endBrace.copy() + xBraces = Group(leftBrace, rightBrace) + + downBrace = startBrace.copy() + upBrace = endBrace.copy() yBraces = Group(downBrace, upBrace) - yBraces.stretch(2, 0) yBraces.rotate(TAU/4) lowerX = self.initial_lower_x @@ -569,7 +570,7 @@ class ColorMappedByFuncScene(Scene): full_hash = hash((func_hash, self.camera.pixel_shape)) self.background_image_file = os.path.join( self.output_directory, "images", - "color_mapped_background_" + str(full_hash) + ".png" + "color_mapped_bg_hash_" + str(full_hash) + ".png" ) self.in_background_pass = not os.path.exists(self.background_image_file) @@ -1284,12 +1285,55 @@ class NumberLineScene(Scene): self.wait() -initial_2d_func = point_func_from_complex_func(lambda c : np.exp(c)) +class Initial2dFuncSceneBase(Scene): + CONFIG = { + "func" : point_func_from_complex_func(lambda c : np.exp(c)) + } -class Initial2dFuncSceneMorphing(Scene): + def show_planes(self): + print "Error! Unimplemented (pure virtual) show_planes" + + def shared_construct(self): + 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.obj_draw(line) + + # def wiggle_around(point): + # radius = 0.2 + # small_circle = Circle(radius = radius) + # small_ + + # wiggle_around(points[-1]) + + def obj_draw(self, input_object): + self.play(ShowCreation(input_object)) + + def construct(self): + self.show_planes() + self.shared_construct() + +# Alternative to the below, using MappingCameras, but no morphing animation +class Initial2dFuncSceneWithoutMorphing(Initial2dFuncSceneBase): + + def setup(self): + left_camera = Camera(**self.camera_config) + right_camera = MappingCamera( + mapping_func = self.func, + **self.camera_config) + split_screen_camera = SplitScreenCamera(left_camera, right_camera, **self.camera_config) + self.camera = split_screen_camera + + def show_planes(self): + self.num_plane = NumberPlane() + self.num_plane.prepare_for_nonlinear_transform() + #num_plane.fade() + self.add(self.num_plane) + +# Alternative to the above, manually implementing split screen with a morphing animation +class Initial2dFuncSceneMorphing(Initial2dFuncSceneBase): CONFIG = { "num_needed_anchor_points" : 10, - "func" : initial_2d_func, } def setup(self): @@ -1317,7 +1361,7 @@ class Initial2dFuncSceneMorphing(Scene): ShowCreation(output_object) ) - def construct(self): + def show_planes(self): right_plane = self.num_plane.copy() right_plane.center() right_plane.prepare_for_nonlinear_transform() @@ -1334,33 +1378,6 @@ class Initial2dFuncSceneMorphing(Scene): run_time = 3 ) - 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.obj_draw(line) - -# Alternative to the above, using MappingCameras, but no morphing animation -class Initial2dFuncSceneWithoutMorphing(Scene): - - def setup(self): - left_camera = Camera(**self.camera_config) - right_camera = MappingCamera( - mapping_func = initial_2d_func, - **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.prepare_for_nonlinear_transform() - #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)) - class DemonstrateColorMapping(ColorMappedObjectsScene): CONFIG = { "show_num_plane" : False @@ -1662,7 +1679,8 @@ class CombineInterval2(Scene): tiny_loop_func = plane_poly_with_roots((-1, -2), (1, 1), (1, 1)) class TinyLoopInInputPlane(ColorMappedByFuncScene): CONFIG = { - "func" : tiny_loop_func + "func" : tiny_loop_func, + "show_num_plane" : False, } def construct(self): From eb09b57356fcd7a81b43a2c81da2a96a7c8b561d Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Wed, 28 Feb 2018 19:48:08 -0800 Subject: [PATCH 2/6] Synced the display of cComplexPlanes to the way NumberPlanes display. However, the code should further be merged at some point --- topics/complex_numbers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/topics/complex_numbers.py b/topics/complex_numbers.py index 27b39f4f..0b9a593a 100644 --- a/topics/complex_numbers.py +++ b/topics/complex_numbers.py @@ -170,7 +170,6 @@ class ComplexPlane(NumberPlane): "unit_size" : 1, "line_frequency" : 1, "faded_line_frequency" : 0.5, - "number_scale_factor" : 0.5, } def __init__(self, **kwargs): digest_config(self, kwargs) @@ -204,6 +203,8 @@ class ComplexPlane(NumberPlane): for y in range(-int(self.y_radius), int(self.y_radius)+1) ] for number in numbers: + if number == complex(0, 0): + continue point = self.number_to_point(number) num_str = str(number).replace("j", "i") if num_str.startswith("0"): @@ -212,9 +213,8 @@ class ComplexPlane(NumberPlane): num_str = num_str.replace("1", "") num_mob = TexMobject(num_str) num_mob.add_background_rectangle() - num_mob.scale(self.number_scale_factor) - vect = DOWN + LEFT - num_mob.next_to(point, vect, SMALL_BUFF) + num_mob.scale_to_fit_height(self.written_coordinate_height) + num_mob.next_to(point, DOWN+LEFT, SMALL_BUFF) result.add(num_mob) return result From 2f63c7762d2da061f53827edb2ead983a07a04fb Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Wed, 28 Feb 2018 19:48:29 -0800 Subject: [PATCH 3/6] Added match_interpolate function to interpolate and inverse_interpolate --- helpers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helpers.py b/helpers.py index 9eddc42e..594cda42 100644 --- a/helpers.py +++ b/helpers.py @@ -359,6 +359,11 @@ def mid(start, end): def inverse_interpolate(start, end, value): return np.true_divide(value - start, end - start) +def match_interpolate(new_start, new_end, old_start, old_end, old_value): + return interpolate( + new_start, new_end, + inverse_interpolate(old_start, old_end, old_value)) + def clamp(lower, upper, val): if val < lower: return lower From 3b0332902d91b18514486c0ab99cf854974d81e7 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Wed, 28 Feb 2018 19:48:39 -0800 Subject: [PATCH 4/6] Incremental --- active_projects/WindingNumber.py | 428 ++++++++++++++++++++++++------- 1 file changed, 342 insertions(+), 86 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 456a59bb..2d204f34 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -29,6 +29,19 @@ from mobject.svg_mobject import * from mobject.tex_mobject import * from topics.graph_scene import * +# Useful constants to play around with +UL = UP + LEFT +UR = UP + RIGHT +DL = DOWN + LEFT +DR = DOWN + RIGHT +standard_rect = np.array([UL, UR, DR, DL]) + +# Used in EquationSolver2d, and a few other places +border_stroke_width = 10 + +# Used for clockwise circling in some scenes +cw_circle = Circle(color = WHITE).stretch(-1, 0) + # TODO/WARNING: There's a lot of refactoring and cleanup to be done in this code, # (and it will be done, but first I'll figure out what I'm doing with all this...) # -SR @@ -74,11 +87,21 @@ def point_to_rev((x, y), allow_origin = False): return return fdiv(np.arctan2(y, x), TAU) +def point_to_size((x, y)): + return np.sqrt(x**2 + y**2) + +# rescaled_size goes from 0 to 1 as size goes from 0 to infinity +# The exact method is arbitrarily chosen to make pleasing color map +# brightness levels +def point_to_rescaled_size(p): + base_size = point_to_size(p) + + return np.sqrt(fdiv(base_size, base_size + 1)) + def point_to_rgba(point): rev = point_to_rev(point, allow_origin = True) rgba = rev_to_rgba(rev) - base_size = np.sqrt(point[0]**2 + point[1]**2) - rescaled_size = np.sqrt(base_size/(base_size + 1)) + rescaled_size = point_to_rescaled_size(point) return rgba * [rescaled_size, rescaled_size, rescaled_size, 1] # Preserve alpha positive_color = rev_to_color(0) @@ -414,27 +437,64 @@ class RectangleData(): return tuple([mid(x, y) for (x, y) in sides]) +# The various inconsistent choices of what datatype to use where are a bit of a mess, +# but I'm more keen to rush this video out now than to sort this out. + def complex_to_pair(c): return np.array((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 complex_poly_with_roots(*roots): +# def poly(c): +# return np.prod([c - z for z in roots]) +# return poly + +# def complex_func_by_critical_points(roots, poles = []): +# numerator = complex_poly_with_roots(*map(pair_to_complex, roots)) +# denominator = complex_poly_with_roots(*map(pair_to_complex, poles)) +# def rational_func(c): +# return numerator(c)/denominator(c) +# return rational_func 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))) - -def point_func_from_plane_func(f): +def point3d_func_from_plane_func(f): def g((x, y, z)): f_val = f((x, y)) return np.array((f_val[0], f_val[1], 0)) return g -test_map_func = point_func_from_complex_func(lambda c: c**2) +# Returns a function from 2-ples to 2-ples +# This function is specified by a list of (x, y, z) tuples, +# and has winding number z (or total of all specified z) around each (x, y) +# +# Can also pass in (x, y) tuples, interpreted as (x, y, 1) +def plane_func_by_wind_spec(*specs): + def embiggen(p): + if len(p) == 3: + return p + elif len(p) == 2: + return (p[0], p[1], 1) + else: + print "Error in plane_func_by_wind_spec embiggen!" + specs = map(embiggen, specs) + + pos_specs = filter(lambda (x, y, z) : z > 0, specs) + neg_specs = filter(lambda (x, y, z) : z < 0, specs) + + neg_specs_made_pos = map (lambda (x, y, z) : (x, y, -z), neg_specs) + + def poly(c, root_specs): + return np.prod([(c - complex(x, y))**z for (x, y, z) in root_specs]) + + def complex_func(c): + return poly(c, pos_specs) * np.conjugate(poly(c, neg_specs_made_pos)) + + return plane_func_from_complex_func(complex_func) + +# Used in Initial2dFunc scenes, VectorField scene, and ExamplePlaneFunc +example_plane_func_spec = [(1.1, 1, 1), (-3, -1.3, 2), (2.8, -2, -1)] +example_plane_func = plane_func_by_wind_spec(*example_plane_func_spec) empty_animation = EmptyAnimation() @@ -446,15 +506,23 @@ class WalkerAnimation(Animation): "coords_to_point" : None } - def __init__(self, walk_func, rev_func, coords_to_point, show_arrows = True, **kwargs): + def __init__(self, walk_func, val_func, coords_to_point, + show_arrows = True, scale_arrows = False, + **kwargs): self.walk_func = walk_func - self.rev_func = rev_func + self.val_func = val_func self.coords_to_point = coords_to_point self.compound_walker = VGroup() self.show_arrows = show_arrows + self.scale_arrows = scale_arrows - base_walker = Dot().scale(5) # PiCreature().scale(0.8) # - self.compound_walker.walker = base_walker.scale(0.35).set_stroke(WHITE, 3) #PiCreature() + if "walker_stroke_color" in kwargs: + walker_stroke_color = kwargs["walker_stroke_color"] + else: + walker_stroke_color = BLACK + + base_walker = Dot().scale(5 * 0.35).set_stroke(walker_stroke_color, 2) # PiCreature().scale(0.8 * 0.35) + self.compound_walker.walker = base_walker if show_arrows: self.compound_walker.arrow = Arrow(ORIGIN, 0.5 * RIGHT, buff = 0) self.compound_walker.arrow.match_style(self.compound_walker.walker) @@ -470,7 +538,8 @@ class WalkerAnimation(Animation): cur_x, cur_y = cur_coords = self.walk_func(alpha) cur_point = self.coords_to_point(cur_x, cur_y) self.mobject.shift(cur_point - self.mobject.walker.get_center()) - rev = self.rev_func(cur_coords) + val = self.val_func(cur_coords) + rev = point_to_rev(val) self.mobject.walker.set_fill(rev_to_color(rev)) if self.show_arrows: self.mobject.arrow.set_fill(rev_to_color(rev)) @@ -479,20 +548,29 @@ class WalkerAnimation(Animation): about_point = self.mobject.arrow.get_start() ) + if self.scale_arrows: + size = point_to_rescaled_size(val) + self.mobject.arrow.scale( + size * 0.3, # Hack constant; we barely use this feature right now + about_point = self.mobject.arrow.get_start() + ) + def walker_animation_with_display( walk_func, - rev_func, + val_func, coords_to_point, number_update_func = None, show_arrows = True, + scale_arrows = False, **kwargs ): walker_anim = WalkerAnimation( walk_func = walk_func, - rev_func = rev_func, + val_func = val_func, coords_to_point = coords_to_point, show_arrows = show_arrows, + scale_arrows = scale_arrows, **kwargs) walker = walker_anim.compound_walker.walker @@ -510,7 +588,7 @@ def walker_animation_with_display( number_update_func, tracked_mobject = walker_anim.compound_walker.walker, **kwargs) - anim_group = AnimationGroup(walker_anim, display_anim) + anim_group = AnimationGroup(walker_anim, display_anim, rate_func = None) return anim_group else: return walker_anim @@ -519,18 +597,20 @@ def LinearWalker( start_coords, end_coords, coords_to_point, - rev_func, + val_func, number_update_func = None, show_arrows = True, + scale_arrows = False, **kwargs ): walk_func = lambda alpha : interpolate(start_coords, end_coords, alpha) return walker_animation_with_display( walk_func = walk_func, coords_to_point = coords_to_point, - rev_func = rev_func, + val_func = val_func, number_update_func = number_update_func, show_arrows = show_arrows, + scale_arrows = scale_arrows, **kwargs) class ColorMappedByFuncScene(Scene): @@ -628,35 +708,84 @@ class PiWalker(ColorMappedByFuncScene): CONFIG = { "walk_coords" : [], "step_run_time" : 1, + "scale_arrows" : False, + "display_size" : False, + "display_odometer" : False, + "color_foreground_not_background" : False, } def construct(self): ColorMappedByFuncScene.construct(self) + + if self.color_foreground_not_background or self.display_odometer: + # Clear background + self.camera.background_image = None + self.camera.init_background() + num_plane = self.num_plane - rev_func = lambda p : point_to_rev(self.func(p)) - walk_coords = self.walk_coords + points = [num_plane.coords_to_point(x, y) for x, y in walk_coords] + polygon = Polygon(*points, color = WHITE) + if self.color_foreground_not_background: + polygon.stroke_width = border_stroke_width + polygon.color_using_background_image(self.background_image_file) + total_run_time = len(points) * self.step_run_time + polygon_anim = ShowCreation(polygon, run_time = total_run_time, rate_func = None) + walker_anim = EmptyAnimation + 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( - ShowCreation(Line(start_point, end_point), rate_func = None), - LinearWalker( - start_coords = start_coords, - end_coords = end_coords, - coords_to_point = num_plane.coords_to_point, + start_coords = walk_coords[i] + end_coords = walk_coords[(i + 1) % len(walk_coords)] + + # We need to do this roundabout default argument thing to get the closure we want, + # so the next iteration changing start_coords, end_coords doesn't change this closure + val_alpha_func = lambda a, start_coords = start_coords, end_coords = end_coords : self.func(interpolate(start_coords, end_coords, a)) + + if self.display_size: + # We need to do this roundabout default argument thing to get the closure we want, + # so the next iteration changing val_alpha_func doesn't change this closure + number_update_func = lambda a, val_alpha_func = val_alpha_func : point_to_rescaled_size(val_alpha_func(a)) # We only use this for diagnostics + else: + number_update_func = None + + new_anim = LinearWalker( + start_coords = start_coords, + end_coords = end_coords, + coords_to_point = num_plane.coords_to_point, + val_func = self.func, + remover = (i < len(walk_coords) - 1), + show_arrows = not self.show_output, + scale_arrows = self.scale_arrows, + number_update_func = number_update_func, + run_time = self.step_run_time, + walker_stroke_color = DARK_GREY if self.color_foreground_not_background else BLACK + ) + + if self.display_odometer: + # Discard above animation and show an odometer instead + + # We need to do this roundabout default argument thing to get the closure we want, + # so the next iteration changing val_alpha_func doesn't change this closure + rev_func = lambda a, val_alpha_func = val_alpha_func : point_to_rev(val_alpha_func(a)) + base_arrow = Arrow(ORIGIN, RIGHT, buff = 0) + new_anim = FuncRotater(base_arrow, rev_func = rev_func, - remover = (i < len(walk_coords) - 1), - show_arrows = not self.show_output - ), - run_time = self.step_run_time) + run_time = self.step_run_time, + rate_func = None, + remover = i < len(walk_coords) - 1, + ) + + walker_anim = Succession(walker_anim, new_anim) # TODO: Allow smooth paths instead of breaking them up into lines, and # use point_from_proportion to get points along the way + if self.display_odometer: + self.play(walker_anim) + else: + self.play(polygon_anim, walker_anim) + self.wait() class PiWalkerRect(PiWalker): @@ -689,7 +818,6 @@ class PiWalkerCircle(PiWalker): 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: Give drawn lines a bit of buffer, so that the rectangle's corners are filled in class EquationSolver2d(ColorMappedObjectsScene): CONFIG = { "camera_config" : {"use_z_coordinate_for_display_order": True}, @@ -703,16 +831,27 @@ class EquationSolver2d(ColorMappedObjectsScene): "use_fancy_lines" : True, # 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 + + # Walker settings + "show_arrows" : True, + "scale_arrows" : False, + + # Special case settings + # These are used to hack UhOhScene, where we display different colors than + # are actually, secretly, guiding the evolution of the EquationSolver2d + # + # replacement_background_image_file has to be manually configured + "show_winding_numbers" : True, + "replacement_background_image_file" : None, } def construct(self): ColorMappedObjectsScene.construct(self) num_plane = self.num_plane - rev_func = lambda p : point_to_rev(self.func(p)) - clockwise_rev_func = lambda p : -rev_func(p) + clockwise_val_func = lambda p : -point_to_rev(self.func(p)) - base_line = Line(UP, RIGHT, stroke_width = 10 if self.use_fancy_lines else 4, color = RED) + base_line = Line(UP, RIGHT, stroke_width = border_stroke_width, color = RED) run_time_base = 1 run_time_with_lingering = run_time_base + 0.2 @@ -727,21 +866,25 @@ class EquationSolver2d(ColorMappedObjectsScene): return empty_animation def draw_line_return_wind(start, end, start_wind, should_linger = False, draw_line = True): - alpha_winder = make_alpha_winder(clockwise_rev_func, start, end, self.num_checkpoints) + alpha_winder = make_alpha_winder(clockwise_val_func, start, end, self.num_checkpoints) a0 = alpha_winder(0) rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind colored_line = Line(num_plane.coords_to_point(*start) + IN, num_plane.coords_to_point(*end) + IN) colored_line.match_style(base_line) if self.use_fancy_lines: - colored_line.color_using_background_image(self.background_image_file) + colored_line.color_using_background_image(self.replacement_background_image_file or self.background_image_file) walker_anim = LinearWalker( start_coords = start, end_coords = end, coords_to_point = num_plane.coords_to_point, - rev_func = rev_func, - number_update_func = rebased_winder, - remover = True + val_func = self.func, + number_update_func = rebased_winder if self.show_winding_numbers else None, + remover = True, + walker_stroke_color = WHITE, + + show_arrows = self.show_arrows, + scale_arrows = self.scale_arrows, ) if should_linger: # Do we need an "and not self.display_in_parallel" here? @@ -787,7 +930,7 @@ class EquationSolver2d(ColorMappedObjectsScene): # TODO: Maybe use diagonal lines or something to fill in rectangles indicating # their "Nothing here" status? # Or draw a large X or something - fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = DARK_BROWN) + fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = DARK_GREY) return Succession(anim, FadeIn(fill_rect)) else: (sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split) @@ -806,8 +949,6 @@ class EquationSolver2d(ColorMappedObjectsScene): ] mid_line_coords = rect.split_line_on_dim(dim_to_split) mid_line_points = [num_plane.coords_to_point(x, y) + 2 * IN for (x, y) in mid_line_coords] - # TODO: Have this match rectangle line style, apart from dashes and thin-ness? - # Though there is also informational value in seeing the dashed line separately from rectangle lines mid_line = DashedLine(*mid_line_points) if self.display_in_parallel: recursive_anim = AnimationGroup(*sub_anims) @@ -872,8 +1013,8 @@ class EquationSolver2d(ColorMappedObjectsScene): self.wait() -# TODO: Perhaps have bullets (pulses) fade out and in at ends of line, instead of jarringly -# popping out and in? +# TODO: Perhaps have option for bullets (pulses) to 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 @@ -925,7 +1066,7 @@ class ArrowCircleTest(Scene): class FuncRotater(Animation): CONFIG = { - "rotate_func" : lambda x : x # Func from alpha to revolutions + "rev_func" : lambda x : x, # Func from alpha to CCW revolutions, } # Perhaps abstract this out into an "Animation updating from original object" class @@ -934,7 +1075,7 @@ class FuncRotater(Animation): def update_mobject(self, alpha): Animation.update_mobject(self, alpha) - angle_revs = -self.rotate_func(alpha) # Negated so we interpret this clockwise + angle_revs = self.rev_func(alpha) self.mobject.rotate( angle_revs * TAU, about_point = ORIGIN @@ -946,7 +1087,7 @@ class TestRotater(Scene): test_line = Line(ORIGIN, RIGHT) self.play(FuncRotater( test_line, - rotate_func = lambda x : x % 0.25, + rev_func = lambda x : x % 0.25, run_time = 10)) # TODO: Be careful about clockwise vs. counterclockwise convention throughout! @@ -954,7 +1095,7 @@ class TestRotater(Scene): class OdometerScene(ColorMappedObjectsScene): CONFIG = { # "func" : lambda p : 100 * p # Full coloring, essentially - "rotate_func" : lambda x : np.sin(x * TAU), + "rotate_func" : lambda x : np.sin(x * TAU), # This is given in terms of CW revs "run_time" : 5, "dashed_line_angle" : None, "biased_display_start" : None @@ -989,7 +1130,7 @@ class OdometerScene(ColorMappedObjectsScene): base_arrow = Arrow(ORIGIN, RIGHT, buff = 0) self.play( - FuncRotater(base_arrow, rotate_func = self.rotate_func), + FuncRotater(base_arrow, rev_func = lambda x : -self.rotate_func(x)), ChangingDecimal(num_display, display_func), run_time = self.run_time, rate_func = None) @@ -1114,7 +1255,7 @@ class SignsExplanation(Scene): class VectorField(Scene): CONFIG = { - "func" : plane_func_from_complex_func(lambda p : p**2 + 2), + "func" : example_plane_func, "granularity" : 10, "arrow_scale_factor" : 0.1, "normalized_arrow_scale_factor" : 5 @@ -1159,7 +1300,8 @@ class HasItsLimitations(Scene): self.wait() - base_point = num_line.number_to_point(3) + OUT + # We arrange to go from 2 to 4, a la the squaring in FirstSqrtScene + base_point = num_line.number_to_point(2) + OUT dot_color = ORANGE @@ -1287,7 +1429,7 @@ class NumberLineScene(Scene): class Initial2dFuncSceneBase(Scene): CONFIG = { - "func" : point_func_from_complex_func(lambda c : np.exp(c)) + "func" : point3d_func_from_plane_func(example_plane_func) } def show_planes(self): @@ -1386,24 +1528,25 @@ class DemonstrateColorMapping(ColorMappedObjectsScene): def construct(self): ColorMappedObjectsScene.construct(self) - circle = Circle() + circle = cw_circle.copy() circle.color_using_background_image(self.background_image_file) - ray = Line(ORIGIN, 10 * RIGHT) + ray = Line(ORIGIN, 10 * LEFT) ray.color_using_background_image(self.background_image_file) self.play(ShowCreation(circle)) self.play(ShowCreation(ray)) - scale_factor = 5 - self.play(ApplyMethod(circle.scale, scale_factor)) + scale_up_factor = 5 + scale_down_factor = 20 + self.play(ApplyMethod(circle.scale, scale_up_factor)) - self.play(ApplyMethod(circle.scale, fdiv(1, scale_factor**2))) + self.play(ApplyMethod(circle.scale, fdiv(1, scale_up_factor * scale_down_factor))) - self.play(ApplyMethod(circle.scale, scale_factor)) + self.play(ApplyMethod(circle.scale, scale_down_factor)) - self.play(Rotating(ray, about_point = ORIGIN)) + self.play(Rotating(ray, about_point = ORIGIN, radians = -TAU)) # TODO: Illustrations for introducing domain coloring @@ -1415,7 +1558,8 @@ class DemonstrateColorMapping(ColorMappedObjectsScene): class LoopSplitScene(Scene): CONFIG = { - "output_func" : plane_poly_with_roots((1, 1)) + # TODO: Change this to something more colorful down the midline + "output_func" : plane_func_by_wind_spec((1.1, 1.1, 1), (-3.1, -1.3, 2), (2.8, -2.1, -1)) } def PulsedLine(self, @@ -1435,16 +1579,11 @@ class LoopSplitScene(Scene): return [VGroup(line, *anim.bullets), anim] 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) - scale_factor = 2 shift_term = 0 + # TODO: Change all this to use a wider than tall loop, made of two squares + # Original loop tl = scale_factor * (UP + LEFT) + shift_term tm = scale_factor * UP + shift_term @@ -1520,8 +1659,9 @@ class LoopSplitScene(Scene): mid_lines = SGroup(mid_line_left, mid_line_right) - highlight_circle = Circle(color = YELLOW_E) # Perhaps make this a dashed circle? + highlight_circle = Circle(color = WHITE) # Perhaps make this a dashed circle? highlight_circle.surround(mid_lines) + highlight_circle.stretch(0.3, 0) self.play(Indicate(mid_lines), ShowCreation(highlight_circle, run_time = 0.5)) self.wait() @@ -1553,12 +1693,20 @@ class LoopSplitSceneMapped(LoopSplitScene): # to illustrate relation between degree and large-scale winding number class FundThmAlg(EquationSolver2d): CONFIG = { - "func" : plane_poly_with_roots((1, 2), (-1, 1.5), (-1, 1.5)), + "func" : plane_func_by_wind_spec((1, 2), (-1, 1.5), (-1, 1.5)), "num_iterations" : 2, "display_in_parallel" : True, "use_fancy_lines" : True, } +class PreviewClip(EquationSolver2d): + CONFIG = { + "func" : example_plane_func, + "num_iterations" : 5, + "display_in_parallel" : True, + "use_fancy_lines" : True, + } + # TODO: Borsuk-Ulam visuals # Note: May want to do an ordinary square scene, then MappingCamera it into a circle # class BorsukUlamScene(PiWalker): @@ -1676,7 +1824,7 @@ class CombineInterval2(Scene): self.wait() -tiny_loop_func = plane_poly_with_roots((-1, -2), (1, 1), (1, 1)) +tiny_loop_func = plane_func_by_wind_spec((-1, -2), (1, 1), (1, 1)) class TinyLoopInInputPlane(ColorMappedByFuncScene): CONFIG = { "func" : tiny_loop_func, @@ -1687,8 +1835,7 @@ class TinyLoopInInputPlane(ColorMappedByFuncScene): ColorMappedByFuncScene.construct(self) self.wait() - circle = Circle(color = WHITE) - circle.scale(0.5) + circle = cw_circle.copy() circle.move_to(UP + RIGHT) self.play(ShowCreation(circle)) @@ -1696,11 +1843,108 @@ class TinyLoopInInputPlane(ColorMappedByFuncScene): class TinyLoopInOutputPlane(TinyLoopInInputPlane): CONFIG = { "camera_class" : MappingCamera, - "camera_config" : {"mapping_func" : point_func_from_plane_func(tiny_loop_func)}, + "camera_config" : {"mapping_func" : point3d_func_from_plane_func(tiny_loop_func)}, "show_output" : True, "show_num_plane" : False, } +class BorderOf2dRegionScene(Scene): + def construct(self): + num_plane = NumberPlane() + self.add(num_plane) + + points = standard_rect + 1.5 * UP + 2 * RIGHT + interior = Polygon(*points, fill_color = neutral_color, fill_opacity = 1, stroke_width = 0) + self.play(FadeIn(interior)) + + border = Polygon(*points, color = negative_color, stroke_width = border_stroke_width) + self.play(ShowCreation(border)) + +big_loop_no_zeros_func = lambda (x, y) : complex_to_pair(np.exp(complex(10, y * np.pi))) + +class BigLoopNoZeros(ColorMappedObjectsScene): + CONFIG = { + "func" : big_loop_no_zeros_func + } + + def construct(self): + ColorMappedObjectsScene.construct(self) + points = 3 * np.array([UL, UR, DR, DL]) + polygon = Polygon(*points) + polygon.color_using_background_image(self.background_image_file) + self.play(ShowCreation(polygon)) + + self.wait() + + polygon2 = polygon.copy() + polygon2.fill_opacity = 1 + self.play(FadeIn(polygon2)) + + self.wait() + +class ExamplePlaneFunc(ColorMappedByFuncScene): + CONFIG = { + "show_num_plane" : False, + "func" : example_plane_func + } + + def construct(self): + ColorMappedByFuncScene.construct(self) + + radius = 0.5 + + nonzero_point = ORIGIN # Manually chosen, not auto-synced with example_plane_func + nonzero_point_circle = cw_circle.copy().scale(radius).move_to(nonzero_point) + self.play(ShowCreation(red_point_circle)) + self.wait() + self.play(FadeOut(red_point_circle)) + self.wait() + + zero_circles = Group() + for spec in example_plane_func_spec: + point = spec[0] * RIGHT + spec[1] * UP + + circle = cw_circle.copy().scale(radius).move_to(point) + self.play(ShowCreation(circle)) + zero_circles.add(circle) + + self.wait() + + # TODO: Fix the code in Fade to automatically propagate correctly + # to subobjects, even with special vectorized object handler. + # Also, remove the special handling from FadeOut, have it implemented + # solely through Fade. + # + # But for now, I'll just take care of this stuff myself here. + self.play(*[FadeOut(zero_circle) for zero_circle in zero_circles]) + self.wait() + +class PiWalkerExamplePlaneFunc(PiWalkerRect): + CONFIG = { + "show_num_plane" : False, + "func" : example_plane_func, + # These are just manually entered, not + # automatically kept in sync with example_plane_func: + "start_x" : -4, + "start_y" : 3, + "walk_width" : 8, + "walk_height" : 6, + } + +class PiWalkerExamplePlaneFuncWithScaling(PiWalkerExamplePlaneFunc): + CONFIG = { + "scale_arrows" : True, + "display_size" : True, + } + +class TinyLoopOfBasicallySameColor(PureColorMap): + def construct(self): + PureColorMap.construct(self) + radius = 0.5 + circle = cw_circle.copy().scale(radius).move_to(UP + RIGHT) + self.play(ShowCreation(circle)) + self.wait() + # TODO: Brouwer's fixed point theorem visuals # class BFTScene(Scene): @@ -1728,9 +1972,6 @@ class TinyLoopInOutputPlane(TinyLoopInInputPlane): # Borsuk-Ulam visuals -# TODO: Add to camera an option for lower-quality (faster-rendered) background than pixel_array, -# helpful for previews - #################### # Random test scenes and test functions go here: @@ -1749,13 +1990,28 @@ class MapPiWalkerRect(PiWalkerRect): class ShowBack(PiWalkerRect): CONFIG = { - "func" : plane_poly_with_roots((1, 2), (-1, 1.5), (-1, 1.5)) + "func" : plane_func_by_wind_spec((1, 2), (-1, 1.5), (-1, 1.5)) } class Diagnostic(Scene): def construct(self): - testList = map( (lambda n : (n, rev_to_rgba(n))), [0, 0.25, 0.5, 0.9]) - print "rev_to_rgbas", testList + testList = map(lambda n : (n, rev_to_rgba(n)), [0, 0.1, 0.2, 0.3, 0.4]) + print "Here you go:", testList self.wait() +class DiagnosticColorMap(PureColorMap): + CONFIG = { + "func" : lambda (x, y) : (25 * x, 25 * y), + "show_num_plane" : False, + } + +class PiWalkerOdometerTest(PiWalkerExamplePlaneFunc): + CONFIG = { + "display_odometer" : True + } + +class PiWalkerFancyLineTest(PiWalkerExamplePlaneFunc): + CONFIG = { + "color_foreground_not_background" : True + } # FIN \ No newline at end of file From 0975f323db2ad36125bb78620ed08f4eb9e703d6 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 1 Mar 2018 15:34:46 -0800 Subject: [PATCH 5/6] Fixed fade to behave over Groups of objects exactly as over the subobjects individually --- animation/transform.py | 3 --- mobject/mobject.py | 28 +++++++++++++++++++++++----- mobject/vectorized_mobject.py | 19 +++++++++---------- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/animation/transform.py b/animation/transform.py index 433650df..d8a9a617 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -162,9 +162,6 @@ class FadeOut(Transform): def __init__(self, mobject, **kwargs): target = mobject.copy() target.fade(1) - if isinstance(mobject, VMobject): - target.set_stroke(width = 0) - target.set_fill(opacity = 0) Transform.__init__(self, mobject, target, **kwargs) def clean_up(self, surrounding_scene = None): diff --git a/mobject/mobject.py b/mobject/mobject.py index 6618d1aa..e45f3bee 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -524,16 +524,34 @@ class Mobject(Container): self.highlight(self.color) return self - def fade_to(self, color, alpha): - for mob in self.family_members_with_points(): - start = color_to_rgb(mob.get_color()) + # Some objects (e.g., VMobjects) have special fading + # behavior. We let every object handle its individual + # fading via fade_no_recurse (notionally a purely internal method), + # and then have fade() itself call this recursively on each submobject + # + # Similarly for fade_to_no_recurse and fade_to, the underlying functions + # used by default for fade()ing + + def fade_to_no_recurse(self, color, alpha): + if self.get_num_points() > 0: + start = color_to_rgb(self.get_color()) end = color_to_rgb(color) new_rgb = interpolate(start, end, alpha) - mob.highlight(Color(rgb = new_rgb), family = False) + self.highlight(Color(rgb = new_rgb), family = False) + return self + + def fade_to(self, color, alpha): + for mob in self.subobject_family(): + mob.fade_to_no_recurse(self, color, alpha) + return self + + def fade_no_recurse(self, darkness): + self.fade_to_no_recurse(BLACK, darkness) return self def fade(self, darkness = 0.5): - self.fade_to(BLACK, darkness) + for submob in self.submobject_family(): + submob.fade_no_recurse(darkness) return self def get_color(self): diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 0e4cb237..a59f1c13 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -113,16 +113,15 @@ class VMobject(Mobject): sm1.match_style(sm2) return self - def fade(self, darkness = 0.5): - for submob in self.submobject_family(): - submob.set_stroke( - width = (1-darkness)*submob.get_stroke_width(), - family = False - ) - submob.set_fill( - opacity = (1-darkness)*submob.get_fill_opacity(), - family = False - ) + def fade_no_recurse(self, darkness): + self.set_stroke( + width = (1-darkness)*self.get_stroke_width(), + family = False + ) + self.set_fill( + opacity = (1-darkness)*self.get_fill_opacity(), + family = False + ) return self def get_fill_rgb(self): From 5a56e344352dcddf18e0dd5fda875fb1e5945e5d Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Mon, 5 Mar 2018 17:58:57 -0800 Subject: [PATCH 6/6] Incremental --- active_projects/WindingNumber.py | 291 ++++++++++++++++++++++--------- 1 file changed, 205 insertions(+), 86 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 2d204f34..4d8e1bf0 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -42,6 +42,9 @@ border_stroke_width = 10 # Used for clockwise circling in some scenes cw_circle = Circle(color = WHITE).stretch(-1, 0) +# Used when walker animations are on black backgrounds, in EquationSolver2d and PiWalker +WALKER_LIGHT_COLOR = DARK_GREY + # TODO/WARNING: There's a lot of refactoring and cleanup to be done in this code, # (and it will be done, but first I'll figure out what I'm doing with all this...) # -SR @@ -108,6 +111,7 @@ positive_color = rev_to_color(0) negative_color = rev_to_color(0.5) neutral_color = rev_to_color(0.25) +# This, the first animation I made, has the messiest code, full of cruft, but so it goes class EquationSolver1d(GraphScene, ZoomedScene): CONFIG = { "camera_config" : @@ -122,7 +126,9 @@ class EquationSolver1d(GraphScene, ZoomedScene): "num_iterations" : 10, "iteration_at_which_to_start_zoom" : None, "graph_label" : None, - "show_target_line" : True + "show_target_line" : True, + "base_line_y" : 0, # The y coordinate at which to draw our x guesses + "show_y_as_deviation" : False, # Displays y-values as deviations from target } def drawGraph(self): @@ -143,11 +149,14 @@ class EquationSolver1d(GraphScene, ZoomedScene): dashed_segment_length = 0.1) self.add(target_line_object) - target_line_label = TexMobject("y = " + str(self.targetY)) + target_label_num = 0 if self.show_y_as_deviation else self.targetY + target_line_label = TexMobject("y = " + str(target_label_num)) target_line_label.next_to(target_line_object.get_left(), UP + RIGHT) self.add(target_line_label) - self.wait() # Give us time to appreciate the graph + self.wait() # Give us time to appreciate the graph + + if self.show_target_line: self.play(FadeOut(target_line_label)) # Reduce clutter print "For reference, graphOrigin: ", self.coords_to_point(0, 0) @@ -156,10 +165,30 @@ class EquationSolver1d(GraphScene, ZoomedScene): # This is a mess right now (first major animation coded), # but it works; can be refactored later or never def solveEquation(self): - startBrace = Dot() #TexMobject("[") # Not using [ and ] because they end up crossing over - startBrace.set_color(negative_color) - endBrace = Dot() - endBrace.set_color(positive_color) + # Under special conditions, used in GuaranteedZeroScene, we let the + # "lower" guesses actually be too high, or vice versa, and color + # everything accordingly + + def color_by_comparison(val, ref): + if val > ref: + return positive_color + elif val < ref: + return negative_color + else: + return neutral_color + + lower_color = color_by_comparison(self.func(self.initial_lower_x), self.targetY) + upper_color = color_by_comparison(self.func(self.initial_upper_x), self.targetY) + + if self.show_y_as_deviation: + y_bias = -self.targetY + else: + y_bias = 0 + + startBrace = TexMobject("|", stroke_width = 10) #TexMobject("[") # Not using [ and ] because they end up crossing over + startBrace.set_color(lower_color) + endBrace = startBrace.copy().stretch(-1, 0) + endBrace.set_color(upper_color) genericBraces = Group(startBrace, endBrace) #genericBraces.scale(1.5) @@ -177,7 +206,7 @@ class EquationSolver1d(GraphScene, ZoomedScene): upperX = self.initial_upper_x upperY = self.func(upperX) - leftBrace.move_to(self.coords_to_point(lowerX, 0)) #, aligned_edge = RIGHT) + leftBrace.move_to(self.coords_to_point(lowerX, self.base_line_y)) #, aligned_edge = RIGHT) leftBraceLabel = DecimalNumber(lowerX) leftBraceLabel.next_to(leftBrace, DOWN + LEFT, buff = SMALL_BUFF) leftBraceLabelAnimation = ContinualChangingDecimal(leftBraceLabel, @@ -185,7 +214,7 @@ class EquationSolver1d(GraphScene, ZoomedScene): tracked_mobject = leftBrace) self.add(leftBraceLabelAnimation) - rightBrace.move_to(self.coords_to_point(upperX, 0)) #, aligned_edge = LEFT) + rightBrace.move_to(self.coords_to_point(upperX, self.base_line_y)) #, aligned_edge = LEFT) rightBraceLabel = DecimalNumber(upperX) rightBraceLabel.next_to(rightBrace, DOWN + RIGHT, buff = SMALL_BUFF) rightBraceLabelAnimation = ContinualChangingDecimal(rightBraceLabel, @@ -197,7 +226,7 @@ class EquationSolver1d(GraphScene, ZoomedScene): downBraceLabel = DecimalNumber(lowerY) downBraceLabel.next_to(downBrace, LEFT + DOWN, buff = SMALL_BUFF) downBraceLabelAnimation = ContinualChangingDecimal(downBraceLabel, - lambda alpha : self.point_to_coords(downBrace.get_center())[1], + lambda alpha : self.point_to_coords(downBrace.get_center())[1] + y_bias, tracked_mobject = downBrace) self.add(downBraceLabelAnimation) @@ -205,28 +234,28 @@ class EquationSolver1d(GraphScene, ZoomedScene): upBraceLabel = DecimalNumber(upperY) upBraceLabel.next_to(upBrace, LEFT + UP, buff = SMALL_BUFF) upBraceLabelAnimation = ContinualChangingDecimal(upBraceLabel, - lambda alpha : self.point_to_coords(upBrace.get_center())[1], + lambda alpha : self.point_to_coords(upBrace.get_center())[1] + y_bias, tracked_mobject = upBrace) self.add(upBraceLabelAnimation) lowerDotPoint = self.input_to_graph_point(lowerX, self.graph) - lowerDotXPoint = self.coords_to_point(lowerX, 0) + lowerDotXPoint = self.coords_to_point(lowerX, self.base_line_y) lowerDotYPoint = self.coords_to_point(0, self.func(lowerX)) - lowerDot = Dot(lowerDotPoint + OUT, color = negative_color) + lowerDot = Dot(lowerDotPoint + OUT, color = lower_color) upperDotPoint = self.input_to_graph_point(upperX, self.graph) - upperDot = Dot(upperDotPoint + OUT, color = positive_color) - upperDotXPoint = self.coords_to_point(upperX, 0) + upperDot = Dot(upperDotPoint + OUT, color = upper_color) + upperDotXPoint = self.coords_to_point(upperX, self.base_line_y) upperDotYPoint = self.coords_to_point(0, self.func(upperX)) - lowerXLine = Line(lowerDotXPoint, lowerDotPoint, color = negative_color) - upperXLine = Line(upperDotXPoint, upperDotPoint, color = positive_color) - lowerYLine = Line(lowerDotYPoint, lowerDotPoint, color = negative_color) - upperYLine = Line(upperDotYPoint, upperDotPoint, color = positive_color) + lowerXLine = Line(lowerDotXPoint, lowerDotPoint, color = lower_color) + upperXLine = Line(upperDotXPoint, upperDotPoint, color = upper_color) + lowerYLine = Line(lowerDotYPoint, lowerDotPoint, color = lower_color) + upperYLine = Line(upperDotYPoint, upperDotPoint, color = upper_color) self.add(lowerXLine, upperXLine, lowerYLine, upperYLine) - self.add(xBraces, yBraces, lowerDot, upperDot) + self.add_foreground_mobjects(xBraces, yBraces, lowerDot, upperDot) - x_guess_line = Line(lowerDotXPoint, upperDotXPoint, color = neutral_color) + x_guess_line = Line(lowerDotXPoint, upperDotXPoint, color = WHITE, stroke_width = 10) self.add(x_guess_line) lowerGroup = Group( @@ -243,11 +272,11 @@ class EquationSolver1d(GraphScene, ZoomedScene): x_guess_line ) - initialLowerXDot = Dot(lowerDotXPoint, color = negative_color) - initialUpperXDot = Dot(upperDotXPoint, color = positive_color) - initialLowerYDot = Dot(lowerDotYPoint, color = negative_color) - initialUpperYDot = Dot(upperDotYPoint, color = positive_color) - self.add(initialLowerXDot, initialUpperXDot, initialLowerYDot, initialUpperYDot) + initialLowerXDot = Dot(lowerDotXPoint + OUT, color = lower_color) + initialUpperXDot = Dot(upperDotXPoint + OUT, color = upper_color) + initialLowerYDot = Dot(lowerDotYPoint + OUT, color = lower_color) + initialUpperYDot = Dot(upperDotYPoint + OUT, color = upper_color) + self.add_foreground_mobjects(initialLowerXDot, initialUpperXDot, initialLowerYDot, initialUpperYDot) for i in range(self.num_iterations): if i == self.iteration_at_which_to_start_zoom: @@ -268,26 +297,36 @@ class EquationSolver1d(GraphScene, ZoomedScene): graphPoint = self.input_to_graph_point(newX, self.graph) dot.move_to(graphPoint) - xAxisPoint = self.coords_to_point(newX, 0) + xAxisPoint = self.coords_to_point(newX, self.base_line_y) xBrace.move_to(xAxisPoint) yAxisPoint = self.coords_to_point(0, newY) yBrace.move_to(yAxisPoint) xLine.put_start_and_end_on(xAxisPoint, graphPoint) yLine.put_start_and_end_on(yAxisPoint, graphPoint) - fixed_guess_point = self.coords_to_point(fixed_guess_x, 0) + fixed_guess_point = self.coords_to_point(fixed_guess_x, self.base_line_y) guess_line.put_start_and_end_on(xAxisPoint, fixed_guess_point) return group return updater midX = (lowerX + upperX)/float(2) midY = self.func(midX) + + # If we run with an interval whose endpoints start off with same sign, + # then nothing after this branching can be trusted to do anything reasonable + # in terms of picking branches or assigning colors in_negative_branch = midY < self.targetY sign_color = negative_color if in_negative_branch else positive_color midCoords = self.coords_to_point(midX, midY) midColor = neutral_color # Hm... even the z buffer isn't helping keep this above x_guess_line - midXPoint = Dot(self.coords_to_point(midX, 0) + OUT, color = midColor) + + midXBrace = startBrace.copy() # Had start and endBrace been asymmetric, we'd do something different here + midXBrace.set_color(midColor) + midXBrace.move_to(self.coords_to_point(midX, self.base_line_y) + OUT) + + # We only actually add this much later + midXPoint = Dot(self.coords_to_point(midX, self.base_line_y) + OUT, color = sign_color) x_guess_label_caption = TextMobject("New guess: x = ", fill_color = midColor) x_guess_label_num = DecimalNumber(midX, fill_color = midColor) @@ -302,11 +341,17 @@ class EquationSolver1d(GraphScene, ZoomedScene): guess_labels = Group(x_guess_label, y_guess_label) self.play( - ReplacementTransform(leftBrace.copy(), midXPoint), - ReplacementTransform(rightBrace.copy(), midXPoint), + ReplacementTransform(leftBrace.copy(), midXBrace), + ReplacementTransform(rightBrace.copy(), midXBrace), FadeIn(x_guess_label)) - midXLine = DashedLine(self.coords_to_point(midX, 0), midCoords, color = midColor) + self.add_foreground_mobject(midXBrace) + + midXLine = DashedLine( + self.coords_to_point(midX, self.base_line_y), + midCoords, + color = midColor + ) self.play(ShowCreation(midXLine)) midDot = Dot(midCoords, color = sign_color) if(self.iteration_at_which_to_start_zoom != None and @@ -317,10 +362,12 @@ class EquationSolver1d(GraphScene, ZoomedScene): self.play( ShowCreation(midYLine), FadeIn(y_guess_label), - ApplyMethod(midXPoint.set_color, sign_color), - ApplyMethod(midXLine.set_color, sign_color)) + ApplyMethod(midXBrace.set_color, sign_color), + ApplyMethod(midXLine.set_color, sign_color), + run_time = 0.25 + ) midYPoint = Dot(self.coords_to_point(0, midY), color = sign_color) - self.add(midYPoint) + self.add(midXPoint, midYPoint) if in_negative_branch: self.play( @@ -346,7 +393,7 @@ class EquationSolver1d(GraphScene, ZoomedScene): upperX = midX upperY = midY #mid_group = Group(midXLine, midDot, midYLine) Removing groups doesn't flatten as expected? - self.remove(midXLine, midDot, midYLine) + self.remove(midXLine, midDot, midYLine, midXBrace) self.wait() @@ -464,6 +511,9 @@ def point3d_func_from_plane_func(f): return np.array((f_val[0], f_val[1], 0)) return g +def point3d_func_from_complex_func(f): + return point3d_func_from_plane_func(plane_func_from_complex_func(f)) + # Returns a function from 2-ples to 2-ples # This function is specified by a list of (x, y, z) tuples, # and has winding number z (or total of all specified z) around each (x, y) @@ -493,7 +543,7 @@ def plane_func_by_wind_spec(*specs): return plane_func_from_complex_func(complex_func) # Used in Initial2dFunc scenes, VectorField scene, and ExamplePlaneFunc -example_plane_func_spec = [(1.1, 1, 1), (-3, -1.3, 2), (2.8, -2, -1)] +example_plane_func_spec = [(-3, -1.3, 2), (0.1, 0.2, 1), (2.8, -2, -1)] example_plane_func = plane_func_by_wind_spec(*example_plane_func_spec) empty_animation = EmptyAnimation() @@ -619,7 +669,9 @@ class ColorMappedByFuncScene(Scene): "num_plane" : NumberPlane(), "show_num_plane" : True, - "show_output" : False + "show_output" : False, + + "hide_background" : False #Background used for color mapped objects, not as background } def setup(self): @@ -675,7 +727,12 @@ class ColorMappedByFuncScene(Scene): self.save_image(self.background_image_file, mode = "RGBA") - self.camera.background_image = self.background_image_file + if self.hide_background: + # Clearing background + self.camera.background_image = None + else: + # Even if we just computed the background, we switch to the file now + self.camera.background_image = self.background_image_file self.camera.init_background() if self.show_num_plane: @@ -694,16 +751,10 @@ class PureColorMap(ColorMappedByFuncScene): # This sets self.background_image_file, but does not display it as the background class ColorMappedObjectsScene(ColorMappedByFuncScene): CONFIG = { - "show_num_plane" : False + "show_num_plane" : False, + "hide_background" : True, } - def construct(self): - ColorMappedByFuncScene.construct(self) - - # Clearing background - self.camera.background_image = None - self.camera.init_background() - class PiWalker(ColorMappedByFuncScene): CONFIG = { "walk_coords" : [], @@ -759,7 +810,7 @@ class PiWalker(ColorMappedByFuncScene): scale_arrows = self.scale_arrows, number_update_func = number_update_func, run_time = self.step_run_time, - walker_stroke_color = DARK_GREY if self.color_foreground_not_background else BLACK + walker_stroke_color = WALKER_LIGHT_COLOR if self.color_foreground_not_background else BLACK ) if self.display_odometer: @@ -851,7 +902,7 @@ class EquationSolver2d(ColorMappedObjectsScene): clockwise_val_func = lambda p : -point_to_rev(self.func(p)) - base_line = Line(UP, RIGHT, stroke_width = border_stroke_width, color = RED) + base_line = Line(UP, RIGHT, stroke_width = border_stroke_width, color = WHITE) run_time_base = 1 run_time_with_lingering = run_time_base + 0.2 @@ -881,7 +932,7 @@ class EquationSolver2d(ColorMappedObjectsScene): val_func = self.func, number_update_func = rebased_winder if self.show_winding_numbers else None, remover = True, - walker_stroke_color = WHITE, + walker_stroke_color = WALKER_LIGHT_COLOR, show_arrows = self.show_arrows, scale_arrows = self.scale_arrows, @@ -1153,27 +1204,43 @@ class FirstSqrtScene(EquationSolver1d): "targetY" : 2, "initial_lower_x" : 1, "initial_upper_x" : 2, - "num_iterations" : 3, + "num_iterations" : 5, "iteration_at_which_to_start_zoom" : 3, "graph_label" : "y = x^2", "show_target_line" : True, } +class TestFirstSqrtScene(FirstSqrtScene): + CONFIG = { + "num_iterations" : 1, + } + FirstSqrtSceneConfig = FirstSqrtScene.CONFIG shiftVal = FirstSqrtSceneConfig["targetY"] class SecondSqrtScene(FirstSqrtScene): CONFIG = { - "func" : lambda x : FirstSqrtSceneConfig["func"](x) - shiftVal, - "targetY" : 0, "graph_label" : FirstSqrtSceneConfig["graph_label"] + " - " + str(shiftVal), - "y_min" : FirstSqrtSceneConfig["y_min"] - shiftVal, - "y_max" : FirstSqrtSceneConfig["y_max"] - shiftVal, - "show_target_line" : False, - # 0.96 hacked in by checking calculations above - "graph_origin" : 0.96 * shiftVal * UP + FirstSqrtSceneConfig["graph_origin"], + "show_y_as_deviation" : True, } +class TestSecondSqrtScene(SecondSqrtScene): + CONFIG = { + "num_iterations" : 1 + } + +class GuaranteedZeroScene(SecondSqrtScene): + CONFIG = { + # Manual config values, not automatically synced to anything above + "initial_lower_x" : 1.75, + "initial_upper_x" : 2 + } + +class TestGuaranteedZeroScene(GuaranteedZeroScene): + CONFIG = { + "num_iterations" : 1 + } + # TODO: Pi creatures intrigued class RewriteEquation(Scene): @@ -1292,6 +1359,9 @@ class VectorField(Scene): self.wait() class HasItsLimitations(Scene): + CONFIG = { + "camera_config" : {"use_z_coordinate_for_display_order": True}, + } def construct(self): num_line = NumberLine() @@ -1304,12 +1374,20 @@ class HasItsLimitations(Scene): base_point = num_line.number_to_point(2) + OUT dot_color = ORANGE + + DOT_Z = OUT + # Note: This z-buffer value is needed for our static scenes, but is + # not sufficient for everything, in that we still need to use + # the foreground_mobjects trick during animations. + # At some point, we should figure out how to have animations + # play well with z coordinates. - input_dot = Dot(base_point, color = dot_color) + input_dot = Dot(base_point + DOT_Z, color = dot_color) input_label = TexMobject("Input", fill_color = dot_color) input_label.next_to(input_dot, UP + LEFT) input_label.add_background_rectangle() - self.add(input_dot, input_label) + self.add_foreground_mobject(input_dot) + self.add(input_label) curved_arrow = Arc(0, color = MAROON_E) curved_arrow.set_bound_angles(np.pi, 0) @@ -1319,12 +1397,13 @@ class HasItsLimitations(Scene): # Could do something smoother, with arrowhead moving along partial arc? self.play(ShowCreation(curved_arrow)) - output_dot = Dot(base_point + 2 * RIGHT, color = dot_color) + output_dot = Dot(base_point + 2 * RIGHT + DOT_Z, color = dot_color) output_label = TexMobject("Output", fill_color = dot_color) output_label.next_to(output_dot, UP + RIGHT) output_label.add_background_rectangle() - self.add(output_dot, output_label) + self.add_foreground_mobject(output_dot) + self.add(output_label) self.wait() num_plane = NumberPlane() @@ -1429,7 +1508,11 @@ class NumberLineScene(Scene): class Initial2dFuncSceneBase(Scene): CONFIG = { - "func" : point3d_func_from_plane_func(example_plane_func) + "func" : point3d_func_from_complex_func(lambda c : c**2 - c**3/5 + 1) + # We don't use example_plane_func because, unfortunately, the sort of examples + # which are good for demonstrating our color mapping haven't turned out to be + # good for visualizing in this manner; the gridlines run over themselves multiple + # times in too confusing a fashion } def show_planes(self): @@ -1441,12 +1524,15 @@ class Initial2dFuncSceneBase(Scene): line = Line(points[i], points[i + 1], color = RED) self.obj_draw(line) - # def wiggle_around(point): - # radius = 0.2 - # small_circle = Circle(radius = radius) - # small_ + def wiggle_around(point): + radius = 0.2 + small_circle = cw_circle.copy() + small_circle.scale(radius) + small_circle.move_to(point + radius * RIGHT) + small_circle.set_color(RED) + self.obj_draw(small_circle) - # wiggle_around(points[-1]) + wiggle_around(points[-1]) def obj_draw(self, input_object): self.play(ShowCreation(input_object)) @@ -1556,10 +1642,11 @@ class DemonstrateColorMapping(ColorMappedObjectsScene): # (Just set up an OdometerScene with function matching the walking of the Pi # creature from previous scene, then place it as a simultaneous inset with Premiere) -class LoopSplitScene(Scene): +class LoopSplitScene(ColorMappedObjectsScene): CONFIG = { # TODO: Change this to something more colorful down the midline - "output_func" : plane_func_by_wind_spec((1.1, 1.1, 1), (-3.1, -1.3, 2), (2.8, -2.1, -1)) + "func" : plane_func_by_wind_spec((0.1/2, 1.1/2, 1), (-4.1/2, -1.3/2, 2), (1.8/2, -2.1/2, -1)), + "use_fancy_lines" : True, } def PulsedLine(self, @@ -1569,16 +1656,20 @@ class LoopSplitScene(Scene): pulse_time = 1, **kwargs): line = Line(start, end, **kwargs) + if self.use_fancy_lines: + line.color_using_background_image(self.background_image_file) anim = LinePulser( line = line, bullet_template = bullet_template, num_bullets = num_bullets, pulse_time = pulse_time, - output_func = self.output_func, + output_func = self.func, **kwargs) return [VGroup(line, *anim.bullets), anim] def construct(self): + ColorMappedObjectsScene.construct(self) + scale_factor = 2 shift_term = 0 @@ -1594,7 +1685,7 @@ class LoopSplitScene(Scene): bl = scale_factor * (DOWN + LEFT) + shift_term lm = scale_factor * LEFT + shift_term - loop_color = BLUE + loop_color = WHITE default_bullet = PiCreature(color = RED) default_bullet.scale(0.15) @@ -1605,10 +1696,10 @@ class LoopSplitScene(Scene): def SGroup(*args): return VGroup(*[arg[0] for arg in args]) - top_line = self.PulsedLine(tl, tr, default_bullet, color = BLUE) - right_line = self.PulsedLine(tr, br, modified_bullet, color = BLUE) - bottom_line = self.PulsedLine(br, bl, default_bullet, color = BLUE) - left_line = self.PulsedLine(bl, tl, default_bullet, color = BLUE) + top_line = self.PulsedLine(tl, tr, default_bullet, color = loop_color) + right_line = self.PulsedLine(tr, br, modified_bullet, color = loop_color) + bottom_line = self.PulsedLine(br, bl, default_bullet, color = loop_color) + left_line = self.PulsedLine(bl, tl, default_bullet, color = loop_color) line_list = [top_line, right_line, bottom_line, left_line] loop = SGroup(*line_list) for line in line_list: @@ -1616,7 +1707,11 @@ class LoopSplitScene(Scene): self.wait() # Splits in middle - split_line = DashedLine(interpolate(tl, tr, 0.5), interpolate(bl, br, 0.5)) + if self.use_fancy_lines: + split_line = Line(tm, bm) + split_line.color_using_background_image(self.background_image_file) + else: + split_line = DashedLine(tm, bm) self.play(ShowCreation(split_line)) self.remove(*split_line) @@ -1893,20 +1988,25 @@ class ExamplePlaneFunc(ColorMappedByFuncScene): radius = 0.5 + def circle_point(point): + circle = cw_circle.copy().scale(radius).move_to(point) + self.play(ShowCreation(circle)) + return circle + + def circle_spec(spec): + point = spec[0] * RIGHT + spec[1] * UP + return circle_point(point) + nonzero_point = ORIGIN # Manually chosen, not auto-synced with example_plane_func - nonzero_point_circle = cw_circle.copy().scale(radius).move_to(nonzero_point) - self.play(ShowCreation(red_point_circle)) + nonzero_point_circle = circle_point(nonzero_point) self.wait() - self.play(FadeOut(red_point_circle)) + self.play(FadeOut(nonzero_point_circle)) self.wait() zero_circles = Group() - for spec in example_plane_func_spec: - point = spec[0] * RIGHT + spec[1] * UP - circle = cw_circle.copy().scale(radius).move_to(point) - self.play(ShowCreation(circle)) - zero_circles.add(circle) + for spec in example_plane_func_spec: + zero_circles.add(circle_spec(spec)) self.wait() @@ -1916,7 +2016,20 @@ class ExamplePlaneFunc(ColorMappedByFuncScene): # solely through Fade. # # But for now, I'll just take care of this stuff myself here. - self.play(*[FadeOut(zero_circle) for zero_circle in zero_circles]) + # self.play(*[FadeOut(zero_circle) for zero_circle in zero_circles]) + self.play(FadeOut(zero_circles)) + self.wait() + + # We can reuse our nonzero point from before for "Output doesn't go through ever color" + # Do re-use in Premiere + + # We can also re-use the first of our zero-circles for "Output does go through every color", + # but just in case it would be useful, here's another one, all on its own + + specific_spec_index = 0 + temp_circle = circle_spec(example_plane_func_spec[specific_spec_index]) + self.play(FadeOut(temp_circle)) + self.wait() class PiWalkerExamplePlaneFunc(PiWalkerRect): @@ -1945,6 +2058,12 @@ class TinyLoopOfBasicallySameColor(PureColorMap): self.play(ShowCreation(circle)) self.wait() +class UhOhScene(EquationSolver2d): + CONFIG = { + "show_winding_numbers" : False, + "replacement_background_image_file" : None + } + # TODO: Brouwer's fixed point theorem visuals # class BFTScene(Scene):