from big_ol_pile_of_manim_imports import * import time import mpmath mpmath.mp.dps = 7 # 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) # Used when walker animations are on black backgrounds, in EquationSolver2d and PiWalker WALKER_LIGHT_COLOR = DARK_GREY ODOMETER_RADIUS = 1.5 ODOMETER_STROKE_WIDTH = 20 # 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 # This turns counterclockwise revs into their color. Beware, we use CCW angles # in all display code, but generally think in this video's script in terms of # CW angles def rev_to_rgba(alpha): alpha = (0.5 - alpha) % 1 # For convenience, to go CW from red on left instead of CCW from right # 0 is red, 1/6 is yellow, 1/3 is green, 2/3 is blue hue_list = [0, 0.5/6.0, 1/6.0, 1.1/6.0, 2/6.0, 3/6.0, 4/6.0, 5/6.0] num_hues = len(hue_list) start_index = int(np.floor(num_hues * alpha)) % num_hues end_index = (start_index + 1) % num_hues beta = (alpha % (1.0/num_hues)) * num_hues start_hue = hue_list[start_index] end_hue = hue_list[end_index] if end_hue < start_hue: end_hue = end_hue + 1 hue = interpolate(start_hue, end_hue, beta) return color_to_rgba(Color(hue = hue, saturation = 1, luminance = 0.5)) # alpha = alpha % 1 # colors = colorslist # 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(colors[start_index], colors[end_index], beta) def rev_to_color(alpha): return rgba_to_color(rev_to_rgba(alpha)) def point_to_rev((x, y), allow_origin = False): # 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 not(allow_origin) and (x, y) == (0, 0): print "Error! Angle of (0, 0) computed!" 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) rescaled_size = point_to_rescaled_size(point) return rgba * [rescaled_size, rescaled_size, rescaled_size, 1] # Preserve alpha positive_color = rev_to_color(0) negative_color = rev_to_color(0.5) neutral_color = rev_to_color(0.25) class EquationSolver1d(GraphScene, ZoomedScene): CONFIG = { "camera_config" : { "use_z_coordinate_for_display_order": True, }, "func" : lambda x : x, "targetX" : 0, "targetY" : 0, "initial_lower_x" : 0, "initial_upper_x" : 10, "num_iterations" : 10, "iteration_at_which_to_start_zoom" : None, "graph_label" : None, "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): self.setup_axes() self.graph = self.get_graph(self.func) self.add(self.graph) if self.graph_label != None: curve_label = self.get_graph_label(self.graph, self.graph_label, x_val = 4, direction = LEFT) curve_label.shift(LEFT) self.add(curve_label) if self.show_target_line: target_line_object = DashedLine( self.coords_to_point(self.x_min, self.targetY), self.coords_to_point(self.x_max, self.targetY), dashed_segment_length = 0.1) self.add(target_line_object) 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 if self.show_target_line: self.play(FadeOut(target_line_label)) # Reduce clutter print "For reference, graphOrigin: ", self.coords_to_point(0, 0) print "targetYPoint: ", self.coords_to_point(0, self.targetY) # This is a mess right now (first major animation coded), # but it works; can be refactored later or never def solveEquation(self): # 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) leftBrace = startBrace.copy() rightBrace = endBrace.copy() xBraces = Group(leftBrace, rightBrace) downBrace = startBrace.copy() upBrace = endBrace.copy() yBraces = Group(downBrace, upBrace) yBraces.rotate(TAU/4) lowerX = self.initial_lower_x lowerY = self.func(lowerX) upperX = self.initial_upper_x upperY = self.func(upperX) 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, lambda alpha : self.point_to_coords(leftBrace.get_center())[0], tracked_mobject = leftBrace) 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, lambda alpha : self.point_to_coords(rightBrace.get_center())[0], tracked_mobject = rightBrace) downBrace.move_to(self.coords_to_point(0, lowerY)) #, aligned_edge = UP) 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] + y_bias, tracked_mobject = downBrace) upBrace.move_to(self.coords_to_point(0, upperY)) #, aligned_edge = DOWN) 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] + y_bias, tracked_mobject = upBrace) lowerDotPoint = self.input_to_graph_point(lowerX, self.graph) lowerDotXPoint = self.coords_to_point(lowerX, self.base_line_y) lowerDotYPoint = self.coords_to_point(0, self.func(lowerX)) lowerDot = Dot(lowerDotPoint + OUT, color = lower_color) upperDotPoint = self.input_to_graph_point(upperX, self.graph) 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 = lower_color) upperXLine = Line(upperDotXPoint, upperDotPoint, color = upper_color) lowerYLine = Line(lowerDotPoint, lowerDotYPoint, color = lower_color) upperYLine = Line(upperDotPoint, upperDotYPoint, color = upper_color) x_guess_line = Line(lowerDotXPoint, upperDotXPoint, color = WHITE, stroke_width = 10) lowerGroup = Group( lowerDot, leftBrace, downBrace, lowerXLine, lowerYLine, x_guess_line ) upperGroup = Group( upperDot, rightBrace, upBrace, upperXLine, upperYLine, x_guess_line ) 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) # All the initial adds and ShowCreations are here now: self.play(FadeIn(initialLowerXDot), FadeIn(leftBrace), FadeIn(leftBraceLabel)) self.add_foreground_mobjects(initialLowerXDot, leftBrace) self.add(leftBraceLabelAnimation) self.play(ShowCreation(lowerXLine)) self.add_foreground_mobject(lowerDot) self.play(ShowCreation(lowerYLine)) self.play(FadeIn(initialLowerYDot), FadeIn(downBrace), FadeIn(downBraceLabel)) self.add_foreground_mobjects(initialLowerYDot, downBrace) self.add(downBraceLabelAnimation) self.wait() self.play(FadeIn(initialUpperXDot), FadeIn(rightBrace), FadeIn(rightBraceLabel)) self.add_foreground_mobjects(initialUpperXDot, rightBrace) self.add(rightBraceLabelAnimation) self.play(ShowCreation(upperXLine)) self.add_foreground_mobject(upperDot) self.play(ShowCreation(upperYLine)) self.play(FadeIn(initialUpperYDot), FadeIn(upBrace), FadeIn(upBraceLabel)) self.add_foreground_mobjects(initialUpperYDot, upBrace) self.add(upBraceLabelAnimation) self.wait() self.play(FadeIn(x_guess_line)) self.wait() for i in range(self.num_iterations): if i == self.iteration_at_which_to_start_zoom: self.activate_zooming() self.little_rectangle.move_to( self.coords_to_point(self.targetX, self.targetY)) inverseZoomFactor = 1/float(self.zoom_factor) self.play( lowerDot.scale_in_place, inverseZoomFactor, upperDot.scale_in_place, inverseZoomFactor) def makeUpdater(xAtStart, fixed_guess_x): def updater(group, alpha): dot, xBrace, yBrace, xLine, yLine, guess_line = group newX = interpolate(xAtStart, midX, alpha) newY = self.func(newX) graphPoint = self.input_to_graph_point(newX, self.graph) dot.move_to(graphPoint) 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, 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 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) x_guess_label_num.move_to(0.9 * FRAME_Y_RADIUS * DOWN) x_guess_label_caption.next_to(x_guess_label_num, LEFT) x_guess_label = Group(x_guess_label_caption, x_guess_label_num) y_guess_label_caption = TextMobject(", y = ", fill_color = midColor) y_guess_label_num = DecimalNumber(midY, fill_color = sign_color) y_guess_label_caption.next_to(x_guess_label_num, RIGHT) y_guess_label_num.next_to(y_guess_label_caption, RIGHT) y_guess_label = Group(y_guess_label_caption, y_guess_label_num) guess_labels = Group(x_guess_label, y_guess_label) self.play( ReplacementTransform(leftBrace.copy(), midXBrace), ReplacementTransform(rightBrace.copy(), midXBrace), FadeIn(x_guess_label)) 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 i >= self.iteration_at_which_to_start_zoom): midDot.scale_in_place(inverseZoomFactor) self.add(midDot) midYLine = DashedLine(midCoords, self.coords_to_point(0, midY), color = sign_color) self.play( ShowCreation(midYLine), FadeIn(y_guess_label), 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(midXPoint, midYPoint) if in_negative_branch: self.play( UpdateFromAlphaFunc(lowerGroup, makeUpdater(lowerX, fixed_guess_x = upperX ) ), FadeOut(guess_labels), ) lowerX = midX lowerY = midY else: self.play( UpdateFromAlphaFunc(upperGroup, makeUpdater(upperX, fixed_guess_x = lowerX ) ), FadeOut(guess_labels), ) upperX = midX upperY = midY #mid_group = Group(midXLine, midDot, midYLine) Removing groups doesn't flatten as expected? self.remove(midXLine, midDot, midYLine, midXBrace) self.wait() def construct(self): self.drawGraph() self.solveEquation() # 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? # # "cheap" argument only used for diagnostic testing right now def make_alpha_winder(func, start, end, num_checkpoints, cheap = False): check_points = [None for i in range(num_checkpoints)] check_points[0] = func(start) step_size = fdiv(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): if cheap: return alpha # A test to see if this func is responsible for slowdown index = clamp(0, num_checkpoints - 1, int(alpha * num_checkpoints)) x = interpolate(start, end, alpha) if cheap: return check_points[index] # A more principled test that at least returns a reasonable answer else: return resit_near(func(x), check_points[index]) return return_func # 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_func_from_complex_func(f): return lambda (x, y) : complex_to_pair(f(complex(x,y))) 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 def point3d_func_from_complex_func(f): return point3d_func_from_plane_func(plane_func_from_complex_func(f)) def plane_zeta((x, y)): CLAMP_SIZE = 1000 z = complex(x, y) try: answer = mpmath.zeta(z) except ValueError: return (CLAMP_SIZE, 0) if abs(answer) > CLAMP_SIZE: answer = answer/abs(answer) * CLAMP_SIZE return (float(answer.real), float(answer.imag)) def rescaled_plane_zeta((x, y)): return plane_zeta((x/FRAME_X_RADIUS, 8*y)) # 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) def scale_func(func, scale_factor): return lambda x : func(x) * scale_factor # Used in Initial2dFunc scenes, VectorField scene, and ExamplePlaneFunc 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() 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, val_func, coords_to_point, show_arrows = True, scale_arrows = False, **kwargs): self.walk_func = walk_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 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) 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) cur_point = self.coords_to_point(cur_x, cur_y) self.mobject.shift(cur_point - self.mobject.walker.get_center()) 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)) self.mobject.arrow.rotate( rev * TAU, 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, val_func, coords_to_point, number_update_func = None, show_arrows = True, scale_arrows = False, num_decimal_places = 1, include_background_rectangle = True, **kwargs ): walker_anim = WalkerAnimation( walk_func = walk_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 if number_update_func != None: display = DecimalNumber(0, num_decimal_places = num_decimal_places, fill_color = WHITE if include_background_rectangle else BLACK, include_background_rectangle = include_background_rectangle) if include_background_rectangle: display.background_rectangle.fill_opacity = 0.5 display.background_rectangle.fill_color = GREY display.background_rectangle.scale(1.2) displaycement = 0.5 * DOWN # How about that pun, eh? # display.move_to(walker.get_center() + displaycement) display.next_to(walker, DOWN+RIGHT, SMALL_BUFF) display_anim = ChangingDecimal(display, number_update_func, tracked_mobject = walker_anim.compound_walker.walker, **kwargs) anim_group = AnimationGroup(walker_anim, display_anim, rate_func = None) return anim_group else: return walker_anim def LinearWalker( start_coords, end_coords, coords_to_point, val_func, number_update_func = None, show_arrows = True, scale_arrows = False, include_background_rectangle = True, **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, val_func = val_func, number_update_func = number_update_func, show_arrows = show_arrows, scale_arrows = scale_arrows, include_background_rectangle = include_background_rectangle, **kwargs) class ColorMappedByFuncScene(Scene): CONFIG = { "func" : lambda p : p, "num_plane" : NumberPlane(), "show_num_plane" : True, "show_output" : False, "hide_background" : False #Background used for color mapped objects, not as background } def short_path_to_long_path(self, filename_with_ext): return os.path.join( self.output_directory, "images", filename_with_ext ) def setup(self): # The composition of input_to_pos and pos_to_color # is to be equal to func (which turns inputs into colors) # However, depending on whether we are showing input or output (via a MappingCamera), # we color the background using either func or the identity map if self.show_output: self.input_to_pos_func = self.func self.pos_to_color_func = lambda p : p else: self.input_to_pos_func = lambda p : p self.pos_to_color_func = self.func self.pixel_pos_to_color_func = lambda (x, y) : self.pos_to_color_func( self.num_plane.point_to_coords_cheap(np.array([x, y, 0])) ) jitter_val = 0.1 line_coords = np.linspace(-10, 10) + jitter_val func_hash_points = it.product(line_coords, line_coords) def mini_hasher(p): rgba = point_to_rgba(self.pixel_pos_to_color_func(p)) if rgba[3] != 1.0: print "Warning! point_to_rgba assigns fractional alpha", rgba[3] return tuple(rgba) to_hash = tuple(mini_hasher(p) for p in func_hash_points) func_hash = hash(to_hash) # We hash just based on output image # Thus, multiple scenes with same output image can re-use it # without recomputation full_hash = hash((func_hash, self.camera.get_pixel_width())) self.background_image_file = self.short_path_to_long_path( "color_mapped_bg_hash_" + str(full_hash) + ".png" ) self.in_background_pass = not os.path.exists(self.background_image_file) print "Background file: " + self.background_image_file if self.in_background_pass: print "The background file does not exist yet; this will be a background creation + video pass" else: print "The background file already exists; this will only be a video pass" def construct(self): if self.in_background_pass: self.camera.set_background_from_func( lambda (x, y): point_to_rgba( self.pixel_pos_to_color_func( (x, y) ) ) ) self.save_image(self.background_image_file, mode = "RGBA") 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: self.num_plane.fade() self.add(self.num_plane) class PureColorMap(ColorMappedByFuncScene): CONFIG = { "show_num_plane" : False } def construct(self): ColorMappedByFuncScene.construct(self) self.wait() # This sets self.background_image_file, but does not display it as the background class ColorMappedObjectsScene(ColorMappedByFuncScene): CONFIG = { "show_num_plane" : False, "hide_background" : True, } class PiWalker(ColorMappedByFuncScene): CONFIG = { "walk_coords" : [], "step_run_time" : 1, "scale_arrows" : False, "display_wind" : True, "wind_reset_indices" : [], "display_size" : False, "display_odometer" : False, "color_foreground_not_background" : False, "show_num_plane" : False, "draw_lines" : True, "num_checkpoints" : 10, "num_decimal_places" : 1, "include_background_rectangle" : 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 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 = empty_animation start_wind = 0 for i in range(len(walk_coords)): 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_wind: clockwise_val_func = lambda p : -point_to_rev(self.func(p)) alpha_winder = make_alpha_winder(clockwise_val_func, start_coords, end_coords, self.num_checkpoints) number_update_func = lambda alpha, alpha_winder = alpha_winder, start_wind = start_wind: alpha_winder(alpha) - alpha_winder(0) + start_wind start_wind = 0 if i + 1 in self.wind_reset_indices else number_update_func(1) elif 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 = WALKER_LIGHT_COLOR if self.color_foreground_not_background else BLACK, num_decimal_places = self.num_decimal_places, include_background_rectangle = self.include_background_rectangle, ) 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, 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: color_wheel = Circle(radius = ODOMETER_RADIUS) color_wheel.stroke_width = ODOMETER_STROKE_WIDTH color_wheel.color_using_background_image(self.short_path_to_long_path("pure_color_map.png")) # Manually inserted here; this is unclean self.add(color_wheel) self.play(walker_anim) else: if self.draw_lines: self.play(polygon_anim, walker_anim) else: # (Note: Turns out, play is unhappy playing empty_animation, as had been # previous approach to this toggle; should fix that) self.play(walker_anim) self.wait() class PiWalkerRect(PiWalker): CONFIG = { "start_x" : -1, "start_y" : 1, "walk_width" : 2, "walk_height" : 2, "func" : plane_func_from_complex_func(lambda c: c**2), "double_up" : False, # New default for the scenes using this: "display_wind" : True } 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] if self.double_up: self.walk_coords = self.walk_coords + self.walk_coords 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) def split_interval((a, b)): mid = (a + b)/2.0 return ((a, mid), (mid, b)) # I am surely reinventing some wheel here, but what's done is done... 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 get_center(self): return interpolate(self.get_top_left(), self.get_bottom_right(), 0.5) def get_width(self): return self.rect[0][1] - self.rect[0][0] def get_height(self): return self.rect[1][1] - self.rect[1][0] 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)[::-1]] 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]) class EquationSolver2dNode(object): def __init__(self, first_anim, children = []): self.first_anim = first_anim self.children = children def depth(self): if len(self.children) == 0: return 0 return 1 + max(map(lambda n : n.depth(), self.children)) def nodes_at_depth(self, n): if n == 0: return [self] # Not the efficient way to flatten lists, because Python + is linear in list size, # but we have at most two children, so no big deal here return sum(map(lambda c : c.nodes_at_depth(n - 1), self.children), []) # This is definitely NOT the efficient way to do BFS, but I'm just trying to write something # quick without thinking that gets the job done on small instances for now def hacky_bfs(self): depth = self.depth() # Not the efficient way to flatten lists, because Python + is linear in list size, # but this IS hacky_bfs... return sum(map(lambda i : self.nodes_at_depth(i), range(depth + 1)), []) def display_in_series(self): return Succession(self.first_anim, *map(lambda n : n.display_in_series(), self.children)) def display_in_parallel(self): return Succession(self.first_anim, AnimationGroup(*map(lambda n : n.display_in_parallel(), self.children))) def display_in_bfs(self): bfs_nodes = self.hacky_bfs() return Succession(*map(lambda n : n.first_anim, bfs_nodes)) def play_in_bfs(self, scene, border_anim): bfs_nodes = self.hacky_bfs() print "Number of nodes: ", len(bfs_nodes) if len(bfs_nodes) < 1: print "Less than 1 node! Aborting!" return scene.play(bfs_nodes[0].first_anim, border_anim) for node in bfs_nodes[1:]: scene.play(node.first_anim) class EquationSolver2d(ColorMappedObjectsScene): CONFIG = { "camera_config" : {"use_z_coordinate_for_display_order": True}, "initial_lower_x" : -5, "initial_upper_x" : 5, "initial_lower_y" : -3, "initial_upper_y" : 3, "num_iterations" : 0, "num_checkpoints" : 10, # Should really merge this into one enum-style variable "display_in_parallel" : False, "display_in_bfs" : False, "use_fancy_lines" : True, "line_color" : WHITE, # Only used for non-fancy lines # 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, # Used for UhOhScene; "manual_wind_override" : None, "show_cursor" : True, "linger_parameter" : 0.5, "use_separate_plays" : False, "use_cheap_winding_numbers" : False, # To use this, make num_checkpoints large } def construct(self): if self.num_iterations == 0: print "You forgot to set num_iterations (maybe you meant to subclass something other than EquationSolver2d directly?)" return ColorMappedObjectsScene.construct(self) num_plane = self.num_plane clockwise_val_func = lambda p : -point_to_rev(self.func(p)) base_line = Line(UP, RIGHT, stroke_width = border_stroke_width, color = self.line_color) if self.use_fancy_lines: base_line.color_using_background_image(self.background_image_file) def match_style_with_bg(obj1, obj2): obj1.match_style(obj2) bg = obj2.get_background_image_file() if bg != None: obj1.color_using_background_image(bg) run_time_base = 1 run_time_with_lingering = run_time_base + self.linger_parameter base_rate = lambda t : t linger_rate = squish_rate_func(lambda t : t, 0, fdiv(run_time_base, run_time_with_lingering)) cursor_base = TextMobject("?") cursor_base.scale(2) # Helper functions for manual_wind_override def head(m): if m == None: return None return m[0] def child(m, i): if m == None or m == 0: return None return m[i + 1] def Animate2dSolver(cur_depth, rect, dim_to_split, sides_to_draw = [0, 1, 2, 3], manual_wind_override = None): print "Solver at depth: " + str(cur_depth) if cur_depth >= self.num_iterations: return EquationSolver2dNode(empty_animation) def draw_line_return_wind(start, end, start_wind, should_linger = False, draw_line = True): alpha_winder = make_alpha_winder(clockwise_val_func, start, end, self.num_checkpoints, cheap = self.use_cheap_winding_numbers) 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) match_style_with_bg(colored_line, base_line) walker_anim = LinearWalker( start_coords = start, end_coords = end, coords_to_point = num_plane.coords_to_point, val_func = self.func, # Note: This is the image func, and not logic_func number_update_func = rebased_winder if self.show_winding_numbers else None, remover = True, walker_stroke_color = WALKER_LIGHT_COLOR, show_arrows = self.show_arrows, scale_arrows = self.scale_arrows, ) if should_linger: # Do we need an "and not self.display_in_parallel" here? run_time = run_time_with_lingering rate_func = linger_rate else: run_time = run_time_base rate_func = base_rate opt_line_anim = ShowCreation(colored_line) if draw_line else empty_animation line_draw_anim = AnimationGroup( opt_line_anim, walker_anim, run_time = run_time, rate_func = rate_func) return (line_draw_anim, rebased_winder(1)) wind_so_far = 0 anim = empty_animation sides = [ rect.get_top(), rect.get_right(), rect.get_bottom(), rect.get_left() ] for (i, (start, end)) in enumerate(sides): (next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far, should_linger = i == len(sides) - 1, draw_line = i in sides_to_draw) anim = Succession(anim, next_anim) if self.show_cursor: cursor = cursor_base.copy() center_x, center_y = rect.get_center() width = rect.get_width() height = rect.get_height() cursor.move_to(num_plane.coords_to_point(center_x, center_y) + 10 * IN) cursor.scale(min(width, height)) # Do a quick FadeIn, wait, and quick FadeOut on the cursor, matching rectangle-drawing time cursor_anim = Succession( FadeIn(cursor, run_time = 0.1), Animation(cursor, run_time = 3.8), FadeOut(cursor, run_time = 0.1) ) anim = AnimationGroup(anim, cursor_anim) override_wind = head(manual_wind_override) if override_wind != None: total_wind = override_wind else: 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 = np.array([num_plane.coords_to_point(x, y) for (x, y) in coords]) + 3 * IN # 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_GREY) return EquationSolver2dNode(Succession(anim, FadeIn(fill_rect))) else: (sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split) if dim_to_split == 0: sub_rect_and_sides = [(sub_rect1, 1), (sub_rect2, 3)] else: sub_rect_and_sides = [(sub_rect1, 2), (sub_rect2, 0)] children = [ Animate2dSolver( cur_depth = cur_depth + 1, rect = sub_rect, dim_to_split = 1 - dim_to_split, sides_to_draw = [side_to_draw], manual_wind_override = child(manual_wind_override, index) ) for (index, (sub_rect, side_to_draw)) in enumerate(sub_rect_and_sides) ] 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] mid_line = DashedLine(*mid_line_points) return EquationSolver2dNode(Succession(anim, ShowCreation(mid_line)), children) 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) print "Starting to compute anim" node = Animate2dSolver( cur_depth = 0, rect = rect, dim_to_split = 0, sides_to_draw = [], manual_wind_override = self.manual_wind_override ) print "Done computing anim" if self.display_in_parallel: anim = node.display_in_parallel() elif self.display_in_bfs: anim = node.display_in_bfs() else: anim = node.display_in_series() # Keep timing details here in sync with details above rect_points = [ rect.get_top_left(), rect.get_top_right(), rect.get_bottom_right(), rect.get_bottom_left(), ] border = Polygon(*map(lambda x : num_plane.coords_to_point(*x) + IN, rect_points)) match_style_with_bg(border, base_line) rect_time_without_linger = 4 * run_time_base rect_time_with_linger = 3 * run_time_base + run_time_with_lingering def rect_rate(alpha): time_in = alpha * rect_time_with_linger if time_in < 3 * run_time_base: return fdiv(time_in, 4 * run_time_base) else: time_in_last_leg = time_in - 3 * run_time_base alpha_in_last_leg = fdiv(time_in_last_leg, run_time_with_lingering) return interpolate(0.75, 1, linger_rate(alpha_in_last_leg)) border_anim = ShowCreation( border, run_time = rect_time_with_linger, rate_func = rect_rate ) print "About to do the big Play; for reference, the current time is ", time.strftime("%H:%M:%S") if self.use_separate_plays: node.play_in_bfs(self, border_anim) else: self.play(anim, border_anim) print "All done; for reference, the current time is ", time.strftime("%H:%M:%S") self.wait() # 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 class LinePulser(ContinualAnimation): def __init__(self, line, bullet_template, num_bullets, pulse_time, output_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.output_func = output_func ContinualAnimation.__init__(self, 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, fdiv((i + alpha),(self.num_bullets))) self.bullets[i].move_to(position) if self.output_func: position_2d = (position[0], position[1]) rev = point_to_rev(self.output_func(position_2d)) color = rev_to_color(rev) self.bullets[i].set_color(color) 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, about_point = ORIGIN) x.set_color(rev_to_color(revs)) return x num_arrows = 8 * 3 # 0.5 - fdiv below so as to get a clockwise rotation from left arrows = [rev_rotate(base_arrow.copy(), 0.5 - (fdiv(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 = { "rev_func" : lambda x : x, # Func from alpha to CCW 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.rev_func(alpha) self.mobject.rotate( angle_revs * TAU, about_point = ORIGIN ) self.mobject.set_color(rev_to_color(angle_revs)) class TestRotater(Scene): def construct(self): test_line = Line(ORIGIN, RIGHT) self.play(FuncRotater( test_line, rev_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(ColorMappedObjectsScene): CONFIG = { # "func" : lambda p : 100 * p # Full coloring, essentially "rotate_func" : lambda x : 2 * np.sin(2 * x * TAU), # This is given in terms of CW revs "run_time" : 40, "dashed_line_angle" : None, "biased_display_start" : None, "pure_odometer_background" : False } def construct(self): ColorMappedObjectsScene.construct(self) radius = ODOMETER_RADIUS circle = Circle(center = ORIGIN, radius = radius) circle.stroke_width = ODOMETER_STROKE_WIDTH circle.color_using_background_image(self.background_image_file) self.add(circle) if self.pure_odometer_background: # Just display this background circle, for compositing in Premiere with PiWalker odometers self.wait() return 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, include_background_rectangle = False) num_display.move_to(2 * DOWN) caption = TextMobject("turns clockwise") caption.next_to(num_display, DOWN) self.add(caption) 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, rev_func = lambda x : -self.rotate_func(x)), ChangingDecimal(num_display, display_func), run_time = self.run_time, rate_func = None) ############# # 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.5*DOWN + 5.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" : 5, "iteration_at_which_to_start_zoom" : 3, "graph_label" : "y = x^2", "show_target_line" : True, "x_tick_frequency" : 0.25 } class TestFirstSqrtScene(FirstSqrtScene): CONFIG = { "num_iterations" : 1, } FirstSqrtSceneConfig = FirstSqrtScene.CONFIG shiftVal = FirstSqrtSceneConfig["targetY"] class SecondSqrtScene(FirstSqrtScene): CONFIG = { "graph_label" : FirstSqrtSceneConfig["graph_label"] + " - " + str(shiftVal), "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): def construct(self): # Can maybe use get_center() to perfectly center Groups before and after transform f_old = TexMobject("f(x)") f_new = f_old.copy() equals_old = TexMobject("=") equals_old_2 = equals_old.copy() equals_new = equals_old.copy() g_old = TexMobject("g(x)") g_new = g_old.copy() minus_new = TexMobject("-") zero_new = TexMobject("0") f_old.next_to(equals_old, LEFT) g_old.next_to(equals_old, RIGHT) minus_new.next_to(g_new, LEFT) f_new.next_to(minus_new, LEFT) equals_new.next_to(g_new, RIGHT) zero_new.next_to(equals_new, RIGHT) # where_old = TextMobject("Where does ") # where_old.next_to(f_old, LEFT) # where_new = where_old.copy() # where_new.next_to(f_new, LEFT) # qmark_old = TextMobject("?") # qmark_old.next_to(g_old, RIGHT) # qmark_new = qmark_old.copy() # qmark_new.next_to(zero_new, RIGHT) self.add(f_old, equals_old, equals_old_2, g_old) #, where_old, qmark_old) self.wait() self.play( ReplacementTransform(f_old, f_new), ReplacementTransform(equals_old, equals_new), ReplacementTransform(g_old, g_new), ReplacementTransform(equals_old_2, minus_new), ShowCreation(zero_new), # ReplacementTransform(where_old, where_new), # ReplacementTransform(qmark_old, qmark_new), ) self.wait() class SignsExplanation(Scene): def construct(self): num_line = NumberLine() largest_num = 10 num_line.add_numbers(*range(-largest_num, largest_num + 1)) self.add(num_line) self.wait() pos_num = 3 neg_num = -pos_num pos_arrow = Arrow( num_line.number_to_point(0), num_line.number_to_point(pos_num), buff = 0, color = positive_color) neg_arrow = Arrow( num_line.number_to_point(0), num_line.number_to_point(neg_num), buff = 0, color = negative_color) plus_sign = TexMobject("+", fill_color = positive_color) minus_sign = TexMobject("-", fill_color = negative_color) plus_sign.next_to(pos_arrow, UP) minus_sign.next_to(neg_arrow, UP) #num_line.add_numbers(pos_num) self.play(ShowCreation(pos_arrow), FadeIn(plus_sign)) #num_line.add_numbers(neg_num) self.play(ShowCreation(neg_arrow), FadeIn(minus_sign)) class VectorField(Scene): CONFIG = { "func" : example_plane_func, "granularity" : 10, "arrow_scale_factor" : 0.1, "normalized_arrow_scale_factor" : 5 } def construct(self): num_plane = NumberPlane() self.add(num_plane) x_min, y_min = num_plane.point_to_coords(FRAME_X_RADIUS * LEFT + FRAME_Y_RADIUS * UP) x_max, y_max = num_plane.point_to_coords(FRAME_X_RADIUS * RIGHT + FRAME_Y_RADIUS * DOWN) x_points = np.linspace(x_min, x_max, self.granularity) y_points = np.linspace(y_min, y_max, self.granularity) points = it.product(x_points, y_points) sized_arrows = Group() unsized_arrows = Group() for (x, y) in points: output = self.func((x, y)) output_size = np.sqrt(sum(output**2)) normalized_output = output * fdiv(self.normalized_arrow_scale_factor, output_size) # Assume output has nonzero size here arrow = Vector(output * self.arrow_scale_factor) normalized_arrow = Vector(normalized_output * self.arrow_scale_factor) arrow.move_to(num_plane.coords_to_point(x, y)) normalized_arrow.move_to(arrow) sized_arrows.add(arrow) unsized_arrows.add(normalized_arrow) self.add(sized_arrows) self.wait() self.play(ReplacementTransform(sized_arrows, unsized_arrows)) self.wait() class HasItsLimitations(Scene): CONFIG = { "camera_config" : {"use_z_coordinate_for_display_order": True}, } def construct(self): num_line = NumberLine() num_line.add_numbers() self.add(num_line) self.wait() # 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 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 + DOT_Z, color = dot_color) input_label = TextMobject("Input", fill_color = dot_color) input_label.next_to(input_dot, UP + LEFT) input_label.add_background_rectangle() 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) curved_arrow.generate_points() curved_arrow.add_tip() curved_arrow.move_arc_center_to(base_point + RIGHT) # Could do something smoother, with arrowhead moving along partial arc? self.play(ShowCreation(curved_arrow)) output_dot = Dot(base_point + 2 * RIGHT + DOT_Z, color = dot_color) output_label = TextMobject("Output", fill_color = dot_color) output_label.next_to(output_dot, UP + RIGHT) output_label.add_background_rectangle() self.add_foreground_mobject(output_dot) self.add(output_label) self.wait() num_plane = NumberPlane() num_plane.add_coordinates() new_base_point = base_point + 2 * UP new_input_dot = input_dot.copy().move_to(new_base_point) new_input_label = input_label.copy().next_to(new_input_dot, UP + LEFT) new_curved_arrow = Arc(0).match_style(curved_arrow) new_curved_arrow.set_bound_angles(np.pi * 3/4, 0) new_curved_arrow.generate_points() new_curved_arrow.add_tip() input_diff = input_dot.get_center() - curved_arrow.points[0] output_diff = output_dot.get_center() - curved_arrow.points[-1] new_curved_arrow.shift((new_input_dot.get_center() - new_curved_arrow.points[0]) - input_diff) new_output_dot = output_dot.copy().move_to(new_curved_arrow.points[-1] + output_diff) new_output_label = output_label.copy().next_to(new_output_dot, UP + RIGHT) dot_objects = Group(input_dot, input_label, output_dot, output_label, curved_arrow) new_dot_objects = Group(new_input_dot, new_input_label, new_output_dot, new_output_label, new_curved_arrow) self.play( FadeOut(num_line), FadeIn(num_plane), ReplacementTransform(dot_objects, new_dot_objects), ) self.wait() self.add_foreground_mobject(new_dot_objects) complex_plane = ComplexPlane() complex_plane.add_coordinates() # This looks a little wonky and we may wish to do a crossfade in Premiere instead self.play(FadeOut(num_plane), FadeIn(complex_plane)) self.wait() 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) # TODO: Make this line a thin rectangle interval_1d = Line(left_point, right_point, stroke_color = inner_color, stroke_width = stroke_width) rect_1d = Rectangle(stroke_width = 0, fill_opacity = 1, fill_color = inner_color) rect_1d.replace(interval_1d) rect_1d.stretch_to_fit_height(SMALL_BUFF) 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(rect_1d, endpoints_1d) self.play(ShowCreation(full_1d)) self.wait() # TODO: Can polish the morphing above; have dots become left and right sides, and # only then fill in the top and bottom 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 Initial2dFuncSceneBase(Scene): CONFIG = { "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): 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 = 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]) 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, } def setup(self): split_line = DashedLine(FRAME_Y_RADIUS * UP, FRAME_Y_RADIUS * DOWN) self.num_plane = NumberPlane(x_radius = FRAME_X_RADIUS/2) self.num_plane.to_edge(LEFT, buff = 0) self.num_plane.prepare_for_nonlinear_transform() self.add(self.num_plane, split_line) def squash_onto_left(self, object): object.shift(FRAME_X_RADIUS/2 * LEFT) def squash_onto_right(self, object): object.shift(FRAME_X_RADIUS/2 * RIGHT) def obj_draw(self, input_object): output_object = input_object.copy() if input_object.get_num_anchor_points() < self.num_needed_anchor_points: input_object.insert_n_anchor_points(self.num_needed_anchor_points) output_object.apply_function(self.func) self.squash_onto_left(input_object) self.squash_onto_right(output_object) self.play( ShowCreation(input_object), ShowCreation(output_object) ) def show_planes(self): right_plane = self.num_plane.copy() right_plane.center() right_plane.prepare_for_nonlinear_transform() right_plane.apply_function(self.func) right_plane.shift(FRAME_X_RADIUS/2 * RIGHT) self.right_plane = right_plane crappy_cropper = FullScreenFadeRectangle(fill_opacity = 1) crappy_cropper.stretch_to_fit_width(FRAME_X_RADIUS) crappy_cropper.to_edge(LEFT, buff = 0) self.play( ReplacementTransform(self.num_plane.copy(), right_plane), FadeIn(crappy_cropper), Animation(self.num_plane), run_time = 3 ) class DemonstrateColorMapping(ColorMappedObjectsScene): CONFIG = { "show_num_plane" : False, "show_full_color_map" : True } def construct(self): ColorMappedObjectsScene.construct(self) # Doing this in Premiere now instead # output_plane_label = TextMobject("Output Plane", color = WHITE) # output_plane_label.move_to(3 * UP) # self.add_foreground_mobject(output_plane_label) if self.show_full_color_map: bright_background = Rectangle(width = 2 * FRAME_X_RADIUS + 1, height = 2 * FRAME_Y_RADIUS + 1, fill_opacity = 1) bright_background.color_using_background_image(self.background_image_file) dim_background = bright_background.copy() dim_background.fill_opacity = 0.3 background = bright_background.copy() self.add(background) self.wait() self.play(ReplacementTransform(background, dim_background)) self.wait() ray = Line(ORIGIN, 10 * LEFT) circle = cw_circle.copy() circle.color_using_background_image(self.background_image_file) self.play(ShowCreation(circle)) self.wait() scale_up_factor = 5 scale_down_factor = 20 self.play(ApplyMethod(circle.scale, fdiv(1, scale_down_factor))) self.play(ApplyMethod(circle.scale, scale_up_factor * scale_down_factor)) self.play(ApplyMethod(circle.scale, fdiv(1, scale_up_factor))) self.wait() self.remove(circle) ray = Line(ORIGIN, 10 * LEFT) ray.color_using_background_image(self.background_image_file) self.play(ShowCreation(ray)) self.wait() self.play(Rotating(ray, about_point = ORIGIN, radians = -TAU/2)) self.wait() self.play(Rotating(ray, about_point = ORIGIN, radians = -TAU/2)) self.wait() if self.show_full_color_map: self.play(ReplacementTransform(background, bright_background)) self.wait() # Everything in this is manually kept in sync with WindingNumber_G/TransitionFromPathsToBoundaries class LoopSplitScene(ColorMappedObjectsScene): CONFIG = { "func" : plane_func_by_wind_spec( (-2, 0, 2), (2, 0, 1) ), "use_fancy_lines" : True, } def PulsedLine(self, start, end, bullet_template, num_bullets = 4, pulse_time = 1, **kwargs): line = Line(start, end, color = WHITE, stroke_width = 4, **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.func, **kwargs) return (line, VMobject(*anim.bullets), anim) def construct(self): ColorMappedObjectsScene.construct(self) scale_factor = 2 shift_term = 0 # TODO: Change all this to use a wider than tall loop, made of two squares # Original loop tl = (UP + 2 * LEFT) * scale_factor tm = UP * scale_factor tr = (UP + 2 * RIGHT) * scale_factor bl = (DOWN + 2 * LEFT) * scale_factor bm = DOWN * scale_factor br = (DOWN + 2 * RIGHT) * scale_factor top_line = Line(tl, tr) # Invisible; only used for surrounding circle bottom_line = Line(br, bl) # Invisible; only used for surrounding circle stroke_width = top_line.stroke_width default_bullet = PiCreature() default_bullet.scale(0.15) def pl(a, b): return self.PulsedLine(a, b, default_bullet) def indicate_circle(x, double_horizontal_stretch = False): circle = Circle(color = WHITE, radius = 2 * np.sqrt(2)) circle.move_to(x.get_center()) if x.get_slope() == 0: circle.stretch(0.2, 1) if double_horizontal_stretch: circle.stretch(2, 0) else: circle.stretch(0.2, 0) return circle tl_line_trip = pl(tl, tm) midline_left_trip = pl(tm, bm) bl_line_trip = pl(bm, bl) left_line_trip = pl(bl, tl) left_square_trips = [tl_line_trip, midline_left_trip, bl_line_trip, left_line_trip] left_square_lines = [x[0] for x in left_square_trips] left_square_lines_vmobject = VMobject(*left_square_lines) left_square_bullets = [x[1] for x in left_square_trips] left_square_anims = [x[2] for x in left_square_trips] tr_line_trip = pl(tm, tr) right_line_trip = pl(tr, br) br_line_trip = pl(br, bm) midline_right_trip = pl(bm, tm) right_square_trips = [tr_line_trip, right_line_trip, br_line_trip, midline_right_trip] right_square_lines = [x[0] for x in right_square_trips] right_square_lines_vmobject = VMobject(*right_square_lines) right_square_bullets = [x[1] for x in right_square_trips] right_square_anims = [x[2] for x in right_square_trips] midline_trips = [midline_left_trip, midline_right_trip] midline_lines = [x[0] for x in midline_trips] midline_lines_vmobject = VMobject(*midline_lines) midline_bullets = [x[1] for x in midline_trips] midline_anims = [x[1] for x in midline_trips] left_line = left_line_trip[0] right_line = right_line_trip[0] for b in left_square_bullets + right_square_bullets: b.set_fill(opacity = 0) faded = 0.3 # Workaround for FadeOut/FadeIn not playing well with ContinualAnimations due to # Transforms making copies no longer identified with the ContinualAnimation's tracked mobject def bullet_fade(start, end, mob): return UpdateFromAlphaFunc(mob, lambda m, a : m.set_fill(opacity = interpolate(start, end, a))) def bullet_list_fade(start, end, bullet_list): return map(lambda b : bullet_fade(start, end, b), bullet_list) def line_fade(start, end, mob): return UpdateFromAlphaFunc(mob, lambda m, a : m.set_stroke(width = interpolate(start, end, a) * stroke_width)) def play_combined_fade(start, end, lines_vmobject, bullets): self.play( line_fade(start, end, lines_vmobject), *bullet_list_fade(start, end, bullets) ) def play_fade_left(start, end): play_combined_fade(start, end, left_square_lines_vmobject, left_square_bullets) def play_fade_right(start, end): play_combined_fade(start, end, right_square_lines_vmobject, right_square_bullets) def play_fade_mid(start, end): play_combined_fade(start, end, midline_lines_vmobject, midline_bullets) def flash_circles(circles): self.play(LaggedStart(FadeIn, VGroup(circles))) self.wait() self.play(FadeOut(VGroup(circles))) self.wait() self.add(left_square_lines_vmobject, right_square_lines_vmobject) self.remove(*midline_lines) self.wait() self.play(ShowCreation(midline_lines[0])) self.add(midline_lines_vmobject) self.wait() self.add(*left_square_anims) self.play(line_fade(1, faded, right_square_lines_vmobject), *bullet_list_fade(0, 1, left_square_bullets)) self.wait() flash_circles([indicate_circle(l) for l in left_square_lines]) self.play(line_fade(faded, 1, right_square_lines_vmobject), *bullet_list_fade(1, 0, left_square_bullets)) self.wait() self.add(*right_square_anims) self.play(line_fade(1, faded, left_square_lines_vmobject), *bullet_list_fade(0, 1, right_square_bullets)) self.wait() flash_circles([indicate_circle(l) for l in right_square_lines]) self.play(line_fade(faded, 1, left_square_lines_vmobject), *bullet_list_fade(1, 0, right_square_bullets)) self.wait() self.play(*bullet_list_fade(0, 1, left_square_bullets + right_square_bullets)) self.wait() outside_circlers = [ indicate_circle(left_line), indicate_circle(right_line), indicate_circle(top_line, double_horizontal_stretch = True), indicate_circle(bottom_line, double_horizontal_stretch = True) ] flash_circles(outside_circlers) inner_circle = indicate_circle(midline_lines[0]) self.play(FadeIn(inner_circle)) self.wait() self.play(FadeOut(inner_circle), line_fade(1, 0, midline_lines_vmobject), *bullet_list_fade(1, 0, midline_bullets)) self.wait() # Repeat for effect, goes well with narration self.play(FadeIn(inner_circle), line_fade(0, 1, midline_lines_vmobject), *bullet_list_fade(0, 1, midline_bullets)) self.wait() self.play(FadeOut(inner_circle), line_fade(1, 0, midline_lines_vmobject), *bullet_list_fade(1, 0, midline_bullets)) self.wait() # 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 = { "func" : plane_func_by_wind_spec((1, 2), (-1, 1.5), (-1, 1.5)), "num_iterations" : 2, } class SolveX5MinusXMinus1(EquationSolver2d): CONFIG = { "func" : plane_func_from_complex_func(lambda c : c**5 - c - 1), "num_iterations" : 10, "show_cursor" : True, "display_in_bfs" : True, } class PureColorMapOfX5Thing(PureColorMap): CONFIG = { "func" : plane_func_from_complex_func(lambda c : c**5 - c - 1), } class X5ThingWithRightHalfGreyed(SolveX5MinusXMinus1): CONFIG = { "num_iterations" : 3, "manual_wind_override" : (1, None, (1, (0, None, None), (0, None, None))) } class SolveX5MinusXMinus1_5Iterations(EquationSolver2d): CONFIG = { "func" : plane_func_from_complex_func(lambda c : c**5 - c - 1), "num_iterations" : 5, "show_cursor" : True, "display_in_bfs" : True, "manual_wind_override" : (None, None, (None, (0, None, None), (0, None, None))) } class X5_Monster_Red_Lines(SolveX5MinusXMinus1_5Iterations): CONFIG = { "use_separate_plays" : True, "use_fancy_lines" : False, "line_color" : RED, } class X5_Monster_Green_Lines(X5_Monster_Red_Lines): CONFIG = { "line_color" : GREEN, } class X5_Monster_Red_Lines_Long(X5_Monster_Red_Lines): CONFIG = { "num_iterations" : 6 } class X5_Monster_Green_Lines_Long(X5_Monster_Green_Lines): CONFIG = { "num_iterations" : 6 } class X5_Monster_Red_Lines_Little_More(X5_Monster_Red_Lines_Long): CONFIG = { "num_iterations" : 7 } class X5_Monster_Green_Lines_Little_More(X5_Monster_Green_Lines_Long): CONFIG = { "num_iterations" : 7 } class X5_Monster_Red_Lines_No_Numbers(X5_Monster_Red_Lines): CONFIG = { "num_iterations" : 3, "show_winding_numbers" : False, } class X5_Monster_Green_Lines_No_Numbers(X5_Monster_Green_Lines): CONFIG = { "num_iterations" : 3, "show_winding_numbers" : False, } class SolveX5MinusXMinus1_3Iterations(EquationSolver2d): CONFIG = { "func" : plane_func_from_complex_func(lambda c : c**5 - c - 1), "num_iterations" : 3, "show_cursor" : True, "display_in_bfs" : True, } class Diagnostic(SolveX5MinusXMinus1_3Iterations): CONFIG = { # I think the combination of these two makes things slow "use_separate_plays" : not False, # This one isn't important to set any particular way, so let's leave it like this "use_fancy_lines" : True, # This causes a small slowdown (before rendering, in particular), but not the big one, I think "show_winding_numbers" : True, # This doesn't significantly matter for rendering time, I think "camera_config" : {"use_z_coordinate_for_display_order" : True} } # All above flags False (meaning not db = False): just under 30 it/s # not db = True: 30 # use_fancy_lines = True: 30 at first (if scene.play(bfs_nodes[0].first_anim, border_anim is off), but then drops to 3 (or drops right away if that simultaneous play is on) # use_z_coordinate = True: 30 # show_winding_numbers = True: 10 # winding AND use_fancy_lines: 10 # not db AND fancy_lines AND z_coords = true, winding = false: 3. Not 30, but 3. Slow. # db AND use_fancy: 3. Slow. # fancy AND z_coords: 30. Fast. [Hm, this may have been a mistake; fancy and z_coords is now slow?] # fancy, winding, AND z_coords, but not (not db): 10 # not db, winding, AND z_coords, but not fancy: 10 # class DiagnosticB(Diagnostic): # CONFIG = { # "num_iterations" : 3, # #"num_checkpoints" : 100, # #"show_winding_numbers" : False, # #"use_cheap_winding_numbers" : True, # } class SolveX5MinusXMinus1Parallel(SolveX5MinusXMinus1): CONFIG = { "display_in_parallel" : True } class SolveX5MinusXMinus1BFS(SolveX5MinusXMinus1): CONFIG = { "display_in_bfs" : True } class PreviewClip(EquationSolver2d): CONFIG = { "func" : example_plane_func, "num_iterations" : 5, "display_in_parallel" : True, "use_fancy_lines" : True, } class ParallelClip(EquationSolver2d): CONFIG = { "func" : plane_func_by_wind_spec( (-3, -1.3, 2), (0.1, 0.2, 1), (2.8, -2, 1) ), "num_iterations" : 5, "display_in_parallel" : True, } class EquationSolver2dMatchBreakdown(EquationSolver2d): CONFIG = { "func" : plane_func_by_wind_spec( (-2, 0.3, 2), (2, -0.2, 1) # Not an exact match, because our breakdown function has a zero along midlines... ), "num_iterations" : 5, "display_in_parallel" : True, "show_cursor" : True } class EquationSolver2dMatchBreakdown_parallel(EquationSolver2dMatchBreakdown): CONFIG = { "display_in_parallel" : True, "display_in_bfs" : False, } class EquationSolver2dMatchBreakdown_bfs(EquationSolver2dMatchBreakdown): CONFIG = { "display_in_parallel" : False, "display_in_bfs" : True, } class QuickPreview(PreviewClip): CONFIG = { "num_iterations" : 3, "display_in_parallel" : False, "display_in_bfs" : True, "show_cursor" : True } class LongEquationSolver(EquationSolver2d): CONFIG = { "func" : example_plane_func, "num_iterations" : 10, "display_in_bfs" : True, "linger_parameter" : 0.4, "show_cursor" : True, } class QuickPreviewUnfancy(LongEquationSolver): CONFIG = { # "use_fancy_lines" : False, } # TODO: Borsuk-Ulam visuals # Note: May want to do an ordinary square scene, then MappingCamera it into a circle # class BorsukUlamScene(PiWalker): # 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 } class CombineInterval(Scene): def construct(self): plus_sign = TexMobject("+", fill_color = positive_color) minus_sign = TexMobject("-", fill_color = negative_color) left_point = Dot(LEFT, color = positive_color) right_point = Dot(RIGHT, color = negative_color) line1 = Line(LEFT, RIGHT) interval1 = Group(line1, left_point, right_point) plus_sign.next_to(left_point, UP) minus_sign.next_to(right_point, UP) self.add(interval1, plus_sign, minus_sign) self.wait() self.play( CircleIndicate(plus_sign), CircleIndicate(minus_sign), ) self.wait() mid_point = Dot(ORIGIN, color = GREY) question_mark = TexMobject("?", fill_color = GREY) plus_sign_copy = plus_sign.copy() minus_sign_copy = minus_sign.copy() new_signs = Group(question_mark, plus_sign_copy, minus_sign_copy) for sign in new_signs: sign.next_to(mid_point, UP) self.play(FadeIn(mid_point), FadeIn(question_mark)) self.wait() self.play( ApplyMethod(mid_point.set_color, positive_color), ReplacementTransform(question_mark, plus_sign_copy), ) self.play( CircleIndicate(plus_sign_copy), CircleIndicate(minus_sign), ) self.wait() self.play( ApplyMethod(mid_point.set_color, negative_color), ReplacementTransform(plus_sign_copy, minus_sign_copy), ) self.play( CircleIndicate(minus_sign_copy), CircleIndicate(plus_sign), ) self.wait() class CombineInterval2(Scene): def construct(self): plus_sign = TexMobject("+", fill_color = positive_color) def make_interval(a, b): line = Line(a, b) start_dot = Dot(a, color = positive_color) end_dot = Dot(b, color = positive_color) start_sign = plus_sign.copy().next_to(start_dot, UP) end_sign = plus_sign.copy().next_to(end_dot, UP) return Group(start_sign, end_sign, line, start_dot, end_dot) def pair_indicate(a, b): self.play( CircleIndicate(a), CircleIndicate(b) ) left_interval = make_interval(2 * LEFT, LEFT) right_interval = make_interval(RIGHT, 2 * RIGHT) self.play(FadeIn(left_interval), FadeIn(right_interval)) pair_indicate(left_interval[0], left_interval[1]) pair_indicate(right_interval[0], right_interval[1]) self.play( ApplyMethod(left_interval.shift, RIGHT), ApplyMethod(right_interval.shift, LEFT), ) pair_indicate(left_interval[0], right_interval[1]) self.wait() tiny_loop_func = scale_func(plane_func_by_wind_spec((-1, -2), (1, 1), (1, 1)), 0.3) class TinyLoopScene(ColorMappedByFuncScene): CONFIG = { "func" : tiny_loop_func, "show_num_plane" : False, "loop_point" : ORIGIN, "circle_scale" : 0.7 } def construct(self): ColorMappedByFuncScene.construct(self) circle = cw_circle.copy() circle.scale(self.circle_scale) circle.move_to(self.loop_point) self.play(ShowCreation(circle)) self.wait() class TinyLoopInInputPlaneAroundNonZero(TinyLoopScene): CONFIG = { "loop_point" : 0.5 * RIGHT } class TinyLoopInInputPlaneAroundZero(TinyLoopScene): CONFIG = { "loop_point" : UP + RIGHT } class TinyLoopInOutputPlaneAroundNonZero(TinyLoopInInputPlaneAroundNonZero): CONFIG = { "camera_class" : MappingCamera, "camera_config" : {"mapping_func" : point3d_func_from_plane_func(tiny_loop_func)}, "show_output" : True, "show_num_plane" : False, } class TinyLoopInOutputPlaneAroundZero(TinyLoopInInputPlaneAroundZero): CONFIG = { "camera_class" : MappingCamera, "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 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 = circle_point(nonzero_point) self.wait() self.play(FadeOut(nonzero_point_circle)) self.wait() zero_circles = Group() for spec in example_plane_func_spec: zero_circles.add(circle_spec(spec)) 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.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): 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 NoticeHowOnThisLoop(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" : 0.5, "start_y" : -0.5, "walk_width" : -1, # We trace from bottom-right clockwise on this one, to start at a red point "walk_height" : -1, } class ButOnThisLoopOverHere(NoticeHowOnThisLoop): CONFIG = { # These are just manually entered, not # automatically kept in sync with example_plane_func: "start_x" : -1, "start_y" : 0, "walk_width" : 1, "walk_height" : 1, } 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() def uhOhFunc((x, y)): x = -np.clip(x, -5, 5)/5 y = -np.clip(y, -3, 3)/3 alpha = 0.5 # Most things will return green # These next three things should really be abstracted into some "Interpolated triangle" function if x >= 0 and y >= x and y <= 1: alpha = interpolate(0.5, 1, y - x) if x < 0 and y >= -2 * x and y <= 1: alpha = interpolate(0.5, 1, y + 2 * x) if x >= -1 and y >= 2 * (x + 1) and y <= 1: alpha = interpolate(0.5, 0, y - 2 * (x + 1)) return complex_to_pair(100 * np.exp(complex(0, TAU * (0.5 - alpha)))) class UhOhFuncTest(PureColorMap): CONFIG = { "func" : uhOhFunc } class UhOhScene(EquationSolver2d): CONFIG = { "func" : uhOhFunc, "manual_wind_override" : (1, None, (1, None, (1, None, None))), # Tailored to UhOhFunc above "show_winding_numbers" : False, "num_iterations" : 5, } class UhOhSceneWithWindingNumbers(UhOhScene): CONFIG = { "show_winding_numbers" : True, } class UhOhSceneWithWindingNumbersNoOverride(UhOhSceneWithWindingNumbers): CONFIG = { "manual_wind_override" : None, "num_iterations" : 2 } class UhOhSalientStill(ColorMappedObjectsScene): CONFIG = { "func" : uhOhFunc } def construct(self): ColorMappedObjectsScene.construct(self) new_up = 3 * UP new_left = 5 * LEFT thin_line = Line(UP, RIGHT, color = WHITE) main_points = [new_left + new_up, new_up, ORIGIN, new_left] polygon = Polygon(*main_points, stroke_width = border_stroke_width) thin_polygon = polygon.copy().match_style(thin_line) polygon.color_using_background_image(self.background_image_file) midline = Line(new_up + 0.5 * new_left, 0.5 * new_left, stroke_width = border_stroke_width) thin_midline = midline.copy().match_style(thin_line) midline.color_using_background_image(self.background_image_file) self.add(polygon, midline) self.wait() everything_filler = FullScreenFadeRectangle(fill_opacity = 1) everything_filler.color_using_background_image(self.background_image_file) thin_white_copy = Group(thin_polygon, thin_midline) self.play(FadeIn(everything_filler), FadeIn(thin_white_copy)) self.wait() # TODO: Brouwer's fixed point theorem visuals # class BFTScene(Scene): # TODO: Pi creatures wide-eyed in amazement ################# # TODOs, from easiest to hardest: # Minor fiddling with little things in each animation; placements, colors, timing, text # Initial odometer scene (simple once previous Pi walker scene is decided upon) # Writing new Pi walker scenes by parametrizing general template # (All the above are basically trivial tinkering at this point) # ---- # Pi creature emotion stuff # BFT visuals # Borsuk-Ulam visuals #################### # Random test scenes and test functions go here: def rect_to_circle((x, y, z)): size = np.sqrt(x**2 + y**2) max_abs_size = max(abs(x), abs(y)) return fdiv(np.array((x, y, z)) * max_abs_size, size) class MapPiWalkerRect(PiWalkerRect): CONFIG = { "camera_class" : MappingCamera, "camera_config" : {"mapping_func" : rect_to_circle}, "show_output" : True } class ShowBack(PiWalkerRect): CONFIG = { "func" : plane_func_by_wind_spec((1, 2), (-1, 1.5), (-1, 1.5)) } class PiWalkerOdometerTest(PiWalkerExamplePlaneFunc): CONFIG = { "display_odometer" : True } class PiWalkerFancyLineTest(PiWalkerExamplePlaneFunc): CONFIG = { "color_foreground_not_background" : True } class NotFoundScene(Scene): def construct(self): self.add(TextMobject("SCENE NOT FOUND!")) self.wait() criticalStripYScale = 100 criticalStrip = Axes(x_min = -0.5, x_max = 1.5, x_axis_config = {"unit_size" : FRAME_X_RADIUS, "number_at_center" : 0.5}, y_min = -criticalStripYScale, y_max = criticalStripYScale, y_axis_config = {"unit_size" : fdiv(FRAME_Y_RADIUS, criticalStripYScale)}) class ZetaViz(PureColorMap): CONFIG = { "func" : plane_zeta, #"num_plane" : criticalStrip, "show_num_plane" : True } class TopLabel(Scene): CONFIG = { "text" : "Text" } def construct(self): label = TextMobject(self.text) label.move_to(3 * UP) self.add(label) self.wait() # This is a giant hack that doesn't handle rev wrap-around correctly; should use # make_alpha_winder instead class SpecifiedWinder(PiWalker): CONFIG = { "start_x" : 0, "start_y" : 0, "x_wind" : 1, # Assumed positive "y_wind" : 1, # Assumed positive "step_size" : 0.1 } def setup(self): rev_func = lambda p : point_to_rev(self.func(p)) start_pos = np.array((self.start_x, self.start_y)) cur_pos = start_pos.copy() start_rev = rev_func(start_pos) mid_rev = start_rev while (abs(mid_rev - start_rev) < self.x_wind): cur_pos += (self.step_size, 0) mid_rev = rev_func(cur_pos) print "Reached ", cur_pos, ", with rev ", mid_rev - start_rev mid_pos = cur_pos.copy() end_rev = mid_rev while (abs(end_rev - mid_rev) < self.y_wind): cur_pos -= (0, self.step_size) end_rev = rev_func(cur_pos) end_pos = cur_pos.copy() print "Reached ", cur_pos, ", with rev ", end_rev - mid_rev self.walk_coords = [start_pos, mid_pos, end_pos] print "Walk coords: ", self.walk_coords PiWalker.setup(self) class OneFifthTwoFifthWinder(SpecifiedWinder): CONFIG = { "func" : example_plane_func, "start_x" : -2.0, "start_y" : 1.0, "x_wind" : 0.2, "y_wind" : 0.2, "step_size" : 0.01, "show_num_plane" : False, "step_run_time" : 6, "num_decimal_places" : 2, } class OneFifthOneFifthWinderWithReset(OneFifthTwoFifthWinder): CONFIG = { "wind_reset_indices" : [1] } class OneFifthTwoFifthWinderOdometer(OneFifthTwoFifthWinder): CONFIG = { "display_odometer" : True, } class ForwardBackWalker(PiWalker): CONFIG = { "func" : example_plane_func, "walk_coords" : [np.array((-2, 1)), np.array((1, 1))], "step_run_time" : 3, } class ForwardBackWalkerOdometer(ForwardBackWalker): CONFIG = { "display_odometer" : True, } class PureOdometerBackground(OdometerScene): CONFIG = { "pure_odometer_background" : True } class CWColorWalk(PiWalkerRect): CONFIG = { "func" : example_plane_func, "start_x" : example_plane_func_spec[0][0] - 1, "start_y" : example_plane_func_spec[0][1] + 1, "walk_width" : 2, "walk_height" : 2, "draw_lines" : False, "display_wind" : False, "step_run_time" : 2 } class CWColorWalkOdometer(CWColorWalk): CONFIG = { "display_odometer" : True, } class CCWColorWalk(CWColorWalk): CONFIG = { "start_x" : example_plane_func_spec[2][0] - 1, "start_y" : example_plane_func_spec[2][1] + 1, } class CCWColorWalkOdometer(CCWColorWalk): CONFIG = { "display_odometer" : True, } class ThreeTurnWalker(PiWalkerRect): CONFIG = { "func" : plane_func_from_complex_func(lambda c: c**3 * complex(1, 1)**3), "double_up" : True, "wind_reset_indices" : [4] } class ThreeTurnWalkerOdometer(ThreeTurnWalker): CONFIG = { "display_odometer" : True, } class FourTurnWalker(PiWalkerRect): CONFIG = { "func" : plane_func_by_wind_spec((0, 0, 4)) } class FourTurnWalkerOdometer(FourTurnWalker): CONFIG = { "display_odometer" : True, } class OneTurnWalker(PiWalkerRect): CONFIG = { "func" : plane_func_from_complex_func(lambda c : np.exp(c) + c) } class OneTurnWalkerOdometer(OneTurnWalker): CONFIG = { "display_odometer" : True, } class ZeroTurnWalker(PiWalkerRect): CONFIG = { "func" : plane_func_by_wind_spec((2, 2, 1), (-1, 2, -1)) } class ZeroTurnWalkerOdometer(ZeroTurnWalker): CONFIG = { "display_odometer" : True, } class NegOneTurnWalker(PiWalkerRect): CONFIG = { "step_run_time" : 2, "func" : plane_func_by_wind_spec((0, 0, -1)) } class NegOneTurnWalkerOdometer(NegOneTurnWalker): CONFIG = { "display_odometer" : True, } # FIN