From 8a5ce2ced8e94348a9e4ffa18c3187698b26517a Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Wed, 31 Jan 2018 17:17:58 -0800 Subject: [PATCH 1/4] Improvements to camera.set_background_from_func, and incremental progress on WindingNumber --- active_projects/WindingNumber.py | 85 +++++++++++++++++++------ animation/simple_animations.py | 2 +- camera/camera.py | 106 +++++++++++++++++++------------ helpers.py | 3 + topics/number_line.py | 6 ++ 5 files changed, 139 insertions(+), 63 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 2b346d5d..d417e0ec 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -211,14 +211,34 @@ def rev_to_color(alpha): return interpolate_color(colors[start_index], colors[end_index], beta) -def point_to_rev((x, y)): +colorslist = map(color_to_rgba, ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"]) + +def rev_to_rgba(alpha): + # TODO: Merge with above + 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 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 (x, y) == (0, 0): + if not(allow_origin) and (x, y) == (0, 0): print "Error! Angle of (0, 0) computed!" - return None - return np.true_divide(np.arctan2(y, x), TAU) + return + return fdiv(np.arctan2(y, x), TAU) + +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)) + return rgba * rescaled_size # Returns the value with the same fractional component as x, closest to m def resit_near(x, m): @@ -232,7 +252,7 @@ def resit_near(x, m): def make_alpha_winder(func, start, end, num_checkpoints): check_points = [None for i in range(num_checkpoints)] check_points[0] = func(start) - step_size = np.true_divide(end - start, num_checkpoints) + step_size = fdiv(end - start, num_checkpoints) for i in range(num_checkpoints - 1): check_points[i + 1] = \ resit_near( @@ -334,7 +354,9 @@ class WalkerAnimation(Animation): self.rev_func = rev_func self.coords_to_point = coords_to_point self.compound_walker = VGroup() - self.compound_walker.walker = PiCreature(color = RED) + dot = Dot() + dot.scale(5) + self.compound_walker.walker = dot #PiCreature() self.compound_walker.walker.scale(scale_factor) self.compound_walker.arrow = Arrow(ORIGIN, RIGHT) #, buff = 0) self.compound_walker.digest_mobject_attrs() @@ -403,21 +425,35 @@ def LinearWalker( number_update_func = number_update_func, **kwargs) -class PiWalker(Scene): +class ColorMappedByFuncScene(Scene): + CONFIG = { + "func" : lambda p : p + } + + def construct(self): + self.num_plane = NumberPlane() + self.num_plane.fade() + self.add(self.num_plane) + self.camera.set_background_from_func( + lambda (x, y): point_to_rgba( + self.func( + self.num_plane.point_to_coords(np.array([x, y, 0])) + ) + ) + ) + +class PiWalker(ColorMappedByFuncScene): CONFIG = { - "func" : plane_func_from_complex_func(lambda c : c**2), "walk_coords" : [], "step_run_time" : 1 } def construct(self): + ColorMappedByFuncScene.construct(self) + num_plane = self.num_plane rev_func = lambda p : point_to_rev(self.func(p)) - num_plane = NumberPlane() - num_plane.fade() - self.add(num_plane) - walk_coords = self.walk_coords for i in range(len(walk_coords)): start_x, start_y = start_coords = walk_coords[i] @@ -425,6 +461,7 @@ class PiWalker(Scene): 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, @@ -432,7 +469,6 @@ class PiWalker(Scene): rev_func = rev_func, remover = (i < len(walk_coords) - 1) ), - ShowCreation(Line(start_point, end_point), rate_func = None), run_time = self.step_run_time) # TODO: Allow smooth paths instead of breaking them up into lines, and @@ -473,9 +509,8 @@ class PiWalkerCircle(PiWalker): # TODO: Perhaps restructure this to avoid using AnimationGroup, and instead # use lists of animations or lists or other such data, to be merged and processed into parallel # animations later -class EquationSolver2d(Scene): +class EquationSolver2d(ColorMappedByFuncScene): CONFIG = { - "func" : plane_poly_with_roots((1, 2), (-1, 3)), "initial_lower_x" : -5.1, "initial_upper_x" : 5.1, "initial_lower_y" : -3.1, @@ -487,9 +522,8 @@ class EquationSolver2d(Scene): } def construct(self): - num_plane = NumberPlane() - num_plane.fade() - self.add(num_plane) + ColorMappedByFuncScene.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) @@ -613,7 +647,7 @@ class LinePulser(ContinualAnimation): end = self.line.get_end() for i in range(self.num_bullets): position = interpolate(start, end, - np.true_divide((i + alpha),(self.num_bullets))) + fdiv((i + alpha),(self.num_bullets))) self.bullets[i].move_to(position) if self.output_func: position_2d = (position[0], position[1]) @@ -635,7 +669,7 @@ class ArrowCircleTest(Scene): return x num_arrows = 8 * 3 - arrows = [rev_rotate(base_arrow.copy(), (np.true_divide(i, num_arrows))) for i in range(num_arrows)] + arrows = [rev_rotate(base_arrow.copy(), (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) @@ -1098,4 +1132,15 @@ class DiffOdometer(OdometerScene): # TODO: Add to camera an option for low-quality background than other rendering, helpful # for previews +#################### + +class PureTest(Scene): + def construct(self): + point_list = [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] + output_list = map(lambda p : (p, point_to_rgba(p)), point_list) + + print output_list + + self.wait() + # FIN diff --git a/animation/simple_animations.py b/animation/simple_animations.py index dafcfab0..9634aff9 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -446,9 +446,9 @@ class Succession(Animation): def jump_to_start_of_anim(self, index): if index != self.current_anim_index: self.mobject.remove(*self.mobject.submobjects) # Should probably have a cleaner "remove_all" method... - self.mobject.add(self.animations[index].mobject) for m in self.scene_mobjects_at_time[index].submobjects: self.mobject.add(m) + self.mobject.add(self.animations[index].mobject) self.current_anim_index = index self.current_alpha = self.critical_alphas[index] diff --git a/camera/camera.py b/camera/camera.py index d97a4d7a..ed1f62c5 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -22,12 +22,13 @@ class Camera(object): "max_allowable_norm" : 2*SPACE_WIDTH, "image_mode" : "RGBA", "n_rgb_coords" : 4, - "background_alpha" : 0, #Out of 255 + "background_alpha" : 0, #Out of color_max_val "pixel_array_dtype" : 'uint8' } def __init__(self, background = None, **kwargs): digest_config(self, kwargs, locals()) + self.color_max_val = np.iinfo(self.pixel_array_dtype).max self.init_background() self.resize_space_shape() self.reset() @@ -75,11 +76,39 @@ class Camera(object): def get_pixel_array(self): return self.pixel_array - def set_pixel_array(self, pixel_array): - self.pixel_array = np.array(pixel_array) + def convert_pixel_array(self, pixel_array, convert_from_floats = False): + retval = np.array(pixel_array) + if convert_from_floats: + retval = np.apply_along_axis( + lambda f : (f * self.color_max_val).astype(self.pixel_array_dtype), + 2, + retval) + return retval - def set_background(self, pixel_array): - self.background = np.array(pixel_array) + def set_pixel_array(self, pixel_array, convert_from_floats = False): + self.pixel_array = self.convert_pixel_array(pixel_array, convert_from_floats) + + def set_background(self, pixel_array, convert_from_floats = False): + self.background = self.convert_pixel_array(pixel_array, convert_from_floats) + + def set_background_from_func(self, coords_to_colors_func): + """ + Sets background by using coords_to_colors_func to determine each pixel's color. Each input + to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not + pixel coordinates), and each output is expected to be an RGBA array of 4 floats. + """ + + print "Starting set_background_from_func" + + coords = self.get_coords_of_all_pixels() + new_background = np.apply_along_axis( + coords_to_colors_func, + 2, + coords + ) + self.set_background(new_background, convert_from_floats = True) + + print "Ending set_background_from_func" def reset(self): self.set_pixel_array(self.background) @@ -173,7 +202,7 @@ class Camera(object): ) fill = aggdraw.Brush( self.color_to_hex_l(self.get_fill_color(vmobject)), - opacity = int(255*vmobject.get_fill_opacity()) + opacity = int(self.color_max_val*vmobject.get_fill_opacity()) ) return (pen, fill) @@ -222,7 +251,7 @@ class Camera(object): ) rgba_len = self.pixel_array.shape[2] - rgbas = (255*rgbas).astype('uint8') + rgbas = (self.color_max_val*rgbas).astype(self.pixel_array_dtype) target_len = len(pixel_coords) factor = target_len/len(rgbas) rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len)) @@ -311,7 +340,7 @@ class Camera(object): def overlay_rgba_array(self, arr): # """ Overlays arr onto self.pixel_array with relevant alphas""" - bg, fg = self.pixel_array/255.0, arr/255.0 + bg, fg = fdiv(self.pixel_array, self.color_max_val), fdiv(arr, self.color_max_val) bga, fga = [arr[:,:,3:] for arr in bg, fg] alpha_sum = fga + (1-fga)*bga with np.errstate(divide = 'ignore', invalid='ignore'): @@ -320,7 +349,7 @@ class Camera(object): np.divide(bg[:,:,:3]*(1-fga)*bga, alpha_sum), ]) bg[:,:,3:] = 1 - (1 - bga)*(1 - fga) - self.pixel_array = (255*bg).astype(self.pixel_array_dtype) + self.pixel_array = (self.color_max_val*bg).astype(self.pixel_array_dtype) def align_points_to_camera(self, points): ## This is where projection should live @@ -382,36 +411,25 @@ class Camera(object): size = pixel_coords.size return pixel_coords.reshape((size/2, 2)) - def get_points_of_all_pixels(self): - """ - Returns an array a such that a[i, j] gives the spatial - coordinates associated with the pixel self.pixel_array[i, j] - """ - shape = self.pixel_array.shape - indices = np.indices(shape[:2], dtype = 'float64') - all_point_coords = np.zeros((shape[0], shape[1], 3)) - for i, space_dim in enumerate([SPACE_HEIGHT, SPACE_WIDTH]): - all_point_coords[:,:,i] = \ - indices[i,:,:]*2*space_dim/shape[i] - space_dim - return all_point_coords + def get_coords_of_all_pixels(self): + uncentered_pixel_indices = np.indices(self.pixel_shape).transpose(1, 2, 0) + uncentered_space_indices = np.true_divide( + uncentered_pixel_indices * self.space_shape, + self.pixel_shape) + # Could structure above line's computation slightly differently, but figured (without much + # thought) multiplying by space_shape first, THEN dividing by pixel_shape, is probably + # better than the other order, for avoiding underflow quantization in the division (whereas + # overflow is unlikely to be a problem) + centered_space_indices = uncentered_space_indices - np.true_divide(self.space_shape, 2) - def set_background_by_color_function(self, point_to_rgba_func): - """ - point_to_rgba_func should take in a point in R^2, an array - of two floats, and output a four element array representing - rgba values, all between 0 and 1. - """ + # Have to account for increasing y now going up instead of down, and also for swapping the + # order of x and y + coords = np.apply_along_axis( + lambda (y, x) : (x, -y), + 2, + centered_space_indices) - # point_to_rgba = lambda p : [1, 1, 0, 0] - def float_rgba_to_int_rgba(rgba): - return (255*np.array(rgba)).astype(self.pixel_array_dtype) - - points_of_all_pixels = self.get_points_of_all_pixels() - self.set_background(np.apply_along_axis( - lambda p : float_rgba_to_int_rgba(point_to_rgba_func(p)), - 2, points_of_all_pixels - )) - self.reset() # Perhaps this really belongs in set_background? + return coords class MovingCamera(Camera): @@ -497,20 +515,24 @@ class MultiCamera(Camera): shifted_camera.start_x:shifted_camera.end_x] \ = shifted_camera.camera.pixel_array - def set_background(self, pixel_array): + def set_background(self, pixel_array, **kwargs): for shifted_camera in self.shifted_cameras: shifted_camera.camera.set_background( pixel_array[ shifted_camera.start_y:shifted_camera.end_y, - shifted_camera.start_x:shifted_camera.end_x]) + shifted_camera.start_x:shifted_camera.end_x], + **kwargs + ) - def set_pixel_array(self, pixel_array): - Camera.set_pixel_array(self, pixel_array) + def set_pixel_array(self, pixel_array, **kwargs): + Camera.set_pixel_array(self, pixel_array, **kwargs) for shifted_camera in self.shifted_cameras: shifted_camera.camera.set_pixel_array( pixel_array[ shifted_camera.start_y:shifted_camera.end_y, - shifted_camera.start_x:shifted_camera.end_x]) + shifted_camera.start_x:shifted_camera.end_x], + **kwargs + ) def init_background(self): Camera.init_background(self) diff --git a/helpers.py b/helpers.py index 58cfc7c8..cf23df03 100644 --- a/helpers.py +++ b/helpers.py @@ -638,3 +638,6 @@ class DictAsObject(object): def __init__(self, dict): self.__dict__ = dict +# Just to have a less heavyweight name for this extremely common operation +def fdiv(a, b): + return np.true_divide(a,b) diff --git a/topics/number_line.py b/topics/number_line.py index 623abf4e..fc94924c 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -321,6 +321,12 @@ class NumberPlane(VMobject): y = new_point[1]/self.get_y_unit_size() return x, y + def point_to_coords_cheap(self, point): + new_point = point - self.center_point + x = new_point[0]/self.x_unit_size + y = new_point[1]/self.y_unit_size + return x, y + def get_x_unit_size(self): return self.axes.get_width() / (2.0*self.x_radius) From 5c0e71a728052828a62edc53f0bd5b3bd06002f8 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 1 Feb 2018 16:29:28 -0800 Subject: [PATCH 2/4] Successions now correctly handle having zero animations --- animation/simple_animations.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 9634aff9..54ab069d 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -411,9 +411,6 @@ class Succession(Animation): else: run_time = sum(self.run_times) self.num_anims = len(animations) - if self.num_anims == 0: - # TODO: Handle this; it should be easy enough, but requires some special cases below - print "Warning! Successions with zero animations are not currently handled!" self.animations = animations #Have to keep track of this run_time, because Scene.play #might very well mess with it. @@ -422,7 +419,7 @@ class Succession(Animation): # critical_alphas[i] is the start alpha of self.animations[i] # critical_alphas[i + 1] is the end alpha of self.animations[i] critical_times = np.concatenate(([0], np.cumsum(self.run_times))) - self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times) + self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times) if self.num_anims > 0 else [0.0] # self.scene_mobjects_at_time[i] is the scene's mobjects at start of self.animations[i] # self.scene_mobjects_at_time[i + 1] is the scene mobjects at end of self.animations[i] @@ -433,9 +430,12 @@ class Succession(Animation): self.animations[i].clean_up(self.scene_mobjects_at_time[i + 1]) self.current_alpha = 0 - self.current_anim_index = 0 #TODO: What if self.num_anims == 0? - self.mobject = self.scene_mobjects_at_time[0] - self.mobject.add(self.animations[0].mobject) + self.current_anim_index = 0 # If self.num_anims == 0, this is an invalid index, but so it goes + if self.num_anims > 0: + self.mobject = self.scene_mobjects_at_time[0] + self.mobject.add(self.animations[0].mobject) + else: + self.mobject = Group() Animation.__init__(self, self.mobject, run_time = run_time, **kwargs) @@ -454,6 +454,9 @@ class Succession(Animation): self.current_alpha = self.critical_alphas[index] def update_mobject(self, alpha): + if self.num_anims == 0: + return + i = 0 while self.critical_alphas[i + 1] < alpha: i = i + 1 From c9e0d8b8392eaf2d7845160e761093fa10c16839 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 1 Feb 2018 16:32:19 -0800 Subject: [PATCH 3/4] Bug fixes on set_background_from_func (now computes screen dimensions correctly) --- camera/camera.py | 40 ++++++++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/camera/camera.py b/camera/camera.py index ed1f62c5..f8c80a5c 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -13,7 +13,14 @@ class Camera(object): CONFIG = { "background_image" : None, "pixel_shape" : (DEFAULT_HEIGHT, DEFAULT_WIDTH), - #this will be resized to match pixel_shape + # Note 1: space_shape will be resized to match pixel_shape + # + # Note 2: While pixel_shape indicates the actual full height + # and width of the pixel array, space_shape indicates only + # half the height and half the width of space (extending from + # -space_height to +space_height vertically and from + # -space_widtdh to +space_width horizontally) + # TODO: Rename these to SPACE_X_RADIUS, SPACE_Y_RADIUS "space_shape" : (SPACE_HEIGHT, SPACE_WIDTH), "space_center" : ORIGIN, "background_color" : BLACK, @@ -132,6 +139,7 @@ class Camera(object): self, mobjects, include_submobjects = True, excluded_mobjects = None, + z_buff_func = lambda m : m.get_center()[2] ): if include_submobjects: mobjects = self.extract_mobject_family_members( @@ -142,7 +150,8 @@ class Camera(object): excluded_mobjects ) mobjects = list_difference_update(mobjects, all_excluded) - return mobjects + + return sorted(mobjects, lambda a, b: cmp(z_buff_func(a), z_buff_func(b))) def capture_mobject(self, mobject, **kwargs): return self.capture_mobjects([mobject], **kwargs) @@ -412,25 +421,28 @@ class Camera(object): return pixel_coords.reshape((size/2, 2)) def get_coords_of_all_pixels(self): - uncentered_pixel_indices = np.indices(self.pixel_shape).transpose(1, 2, 0) - uncentered_space_indices = np.true_divide( - uncentered_pixel_indices * self.space_shape, - self.pixel_shape) + # These are in x, y order, to help me keep things straight + full_space_dims = np.array(self.space_shape)[::-1] * 2 + full_pixel_dims = np.array(self.pixel_shape)[::-1] + + # These are addressed in the same y, x order as in pixel_array, but the values in them + # are listed in x, y order + uncentered_pixel_coords = np.indices(self.pixel_shape)[::-1].transpose(1, 2, 0) + uncentered_space_coords = fdiv( + uncentered_pixel_coords * full_space_dims, + full_pixel_dims) # Could structure above line's computation slightly differently, but figured (without much # thought) multiplying by space_shape first, THEN dividing by pixel_shape, is probably # better than the other order, for avoiding underflow quantization in the division (whereas # overflow is unlikely to be a problem) - centered_space_indices = uncentered_space_indices - np.true_divide(self.space_shape, 2) - # Have to account for increasing y now going up instead of down, and also for swapping the - # order of x and y - coords = np.apply_along_axis( - lambda (y, x) : (x, -y), - 2, - centered_space_indices) + centered_space_coords = (uncentered_space_coords - fdiv(full_space_dims, 2)) - return coords + # Have to also flip the y coordinates to account for pixel array being listed in + # top-to-bottom order, opposite of screen coordinate convention + centered_space_coords = centered_space_coords * (1, -1) + return centered_space_coords class MovingCamera(Camera): """ From b1a62a156c4e17df98181fa2f6a06f838ae390b7 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 1 Feb 2018 16:33:03 -0800 Subject: [PATCH 4/4] Incremental progress on WindingNumber, including adding helpful functions to helpers.py and number_line.py --- active_projects/WindingNumber.py | 151 ++++++++++++++++++------------- helpers.py | 7 ++ topics/number_line.py | 5 + 3 files changed, 99 insertions(+), 64 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index d417e0ec..b59462bd 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -200,21 +200,9 @@ class EquationSolver1d(GraphScene, ZoomedScene): self.drawGraph() self.solveEquation() - -def rev_to_color(alpha): - alpha = alpha % 1 - colors = ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"] - num_colors = len(colors) - beta = (alpha % (1.0/num_colors)) * num_colors - start_index = int(np.floor(num_colors * alpha)) % num_colors - end_index = (start_index + 1) % num_colors - - return interpolate_color(colors[start_index], colors[end_index], beta) - colorslist = map(color_to_rgba, ["#FF0000", ORANGE, YELLOW, "#00FF00", "#0000FF", "#FF00FF"]) def rev_to_rgba(alpha): - # TODO: Merge with above alpha = alpha % 1 colors = colorslist num_colors = len(colors) @@ -224,6 +212,9 @@ def rev_to_rgba(alpha): 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 @@ -304,7 +295,7 @@ class RectangleData(): 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)] + 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!" @@ -337,6 +328,8 @@ def plane_func_from_complex_func(f): def point_func_from_complex_func(f): return lambda (x, y, z): complex_to_R3(f(complex(x, y))) +test_map_func = point_func_from_complex_func(lambda c: c**2) + empty_animation = Animation(Mobject(), run_time = 0) def EmptyAnimation(): return empty_animation @@ -349,16 +342,15 @@ class WalkerAnimation(Animation): "coords_to_point" : None } - def __init__(self, walk_func, rev_func, coords_to_point, scale_factor, **kwargs): + def __init__(self, walk_func, rev_func, coords_to_point, show_arrows = True, **kwargs): self.walk_func = walk_func self.rev_func = rev_func self.coords_to_point = coords_to_point self.compound_walker = VGroup() - dot = Dot() - dot.scale(5) - self.compound_walker.walker = dot #PiCreature() - self.compound_walker.walker.scale(scale_factor) - self.compound_walker.arrow = Arrow(ORIGIN, RIGHT) #, buff = 0) + base_walker = Dot().scale(5) # PiCreature().scale(0.8) # + self.compound_walker.walker = base_walker.scale(0.35).set_stroke(BLACK, 1.5) #PiCreature() + if show_arrows: + self.compound_walker.arrow = Arrow(ORIGIN, 0.5 * RIGHT, buff = 0).set_stroke(BLACK, 1.5) self.compound_walker.digest_mobject_attrs() Animation.__init__(self, self.compound_walker, **kwargs) @@ -370,21 +362,22 @@ class WalkerAnimation(Animation): 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.walker.move_to(cur_point) + self.mobject.shift(cur_point - self.mobject.walker.get_center()) rev = self.rev_func(cur_coords) - self.mobject.walker.set_color(rev_to_color(rev)) - self.mobject.arrow.set_color(rev_to_color(rev)) - self.mobject.arrow.rotate( - rev * TAU, - about_point = ORIGIN #self.mobject.arrow.get_start() - ) + self.mobject.walker.set_fill(rev_to_color(rev)) + if show_arrows: + self.mobject.arrow.set_fill(rev_to_color(rev)) + self.mobject.arrow.rotate( + rev * TAU, + about_point = self.mobject.arrow.get_start() + ) def walker_animation_with_display( walk_func, rev_func, coords_to_point, - number_update_func = None, - scale_factor = 0.35, + number_update_func = None, + show_arrows = True, **kwargs ): @@ -392,13 +385,19 @@ def walker_animation_with_display( walk_func = walk_func, rev_func = rev_func, coords_to_point = coords_to_point, - scale_factor = scale_factor, + show_arrows = show_arrows, **kwargs) walker = walker_anim.compound_walker.walker if number_update_func != None: - display = DecimalNumber(0, include_background_rectangle = True) - displaycement = scale_factor * DOWN # How about that pun, eh? + display = DecimalNumber(0, + num_decimal_points = 1, + fill_color = WHITE, + include_background_rectangle = True) + 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_anim = ChangingDecimal(display, number_update_func, @@ -415,6 +414,7 @@ def LinearWalker( coords_to_point, rev_func, number_update_func = None, + show_arrows = True, **kwargs ): walk_func = lambda alpha : interpolate(start_coords, end_coords, alpha) @@ -423,21 +423,28 @@ def LinearWalker( coords_to_point = coords_to_point, rev_func = rev_func, number_update_func = number_update_func, + show_arrows = show_arrows, **kwargs) class ColorMappedByFuncScene(Scene): CONFIG = { - "func" : lambda p : p + "func" : lambda p : p, + "num_plane" : NumberPlane(), + "display_output_color_map" : False } def construct(self): - self.num_plane = NumberPlane() + display_func = self.func if not self.display_output_color_map else lambda p : p + self.num_plane.fade() self.add(self.num_plane) self.camera.set_background_from_func( lambda (x, y): point_to_rgba( - self.func( - self.num_plane.point_to_coords(np.array([x, y, 0])) + display_func( + # Should be self.num_plane.point_to_coords_cheap(np.array([x, y, 0])), + # but for cheapness, we'll go with just (x, y), having never altered + # any num_plane's from default settings so far + (x, y) ) ) ) @@ -445,7 +452,8 @@ class ColorMappedByFuncScene(Scene): class PiWalker(ColorMappedByFuncScene): CONFIG = { "walk_coords" : [], - "step_run_time" : 1 + "step_run_time" : 1, + "show_arrows" : True } def construct(self): @@ -467,13 +475,13 @@ class PiWalker(ColorMappedByFuncScene): end_coords = end_coords, coords_to_point = num_plane.coords_to_point, rev_func = rev_func, - remover = (i < len(walk_coords) - 1) + remover = (i < len(walk_coords) - 1), + show_arrows = self.show_arrows ), run_time = self.step_run_time) # TODO: Allow smooth paths instead of breaking them up into lines, and # use point_from_proportion to get points along the way - self.wait() @@ -483,6 +491,7 @@ class PiWalkerRect(PiWalker): "start_y" : 1, "walk_width" : 2, "walk_height" : 2, + "func" : plane_func_from_complex_func(lambda c: c**2) } def setup(self): @@ -517,6 +526,7 @@ class EquationSolver2d(ColorMappedByFuncScene): "initial_upper_y" : 3.1, "num_iterations" : 5, "num_checkpoints" : 10, + "display_in_parallel" : 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 } @@ -534,13 +544,14 @@ class EquationSolver2d(ColorMappedByFuncScene): if cur_depth >= self.num_iterations: return EmptyAnimation() - def draw_line_return_wind(start, end, start_wind): + def draw_line_return_wind(start, end, start_wind, should_linger = False): alpha_winder = make_alpha_winder(clockwise_rev_func, start, end, self.num_checkpoints) a0 = alpha_winder(0) rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind thin_line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), stroke_width = 2, color = RED) + walker_anim = LinearWalker( start_coords = start, end_coords = end, @@ -549,12 +560,17 @@ class EquationSolver2d(ColorMappedByFuncScene): number_update_func = rebased_winder, remover = True ) + + if should_linger: # Do we need an "and not self.display_in_parallel" here? + rate_func = lingering + else: + rate_func = None + line_draw_anim = AnimationGroup( ShowCreation(thin_line), walker_anim, - rate_func = None) - anim = line_draw_anim - return (anim, rebased_winder(1)) + rate_func = rate_func) + return (line_draw_anim, rebased_winder(1)) wind_so_far = 0 anim = EmptyAnimation() @@ -564,8 +580,9 @@ class EquationSolver2d(ColorMappedByFuncScene): rect.get_bottom(), rect.get_left() ] - for (start, end) in sides: - (next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far) + 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) anim = Succession(anim, next_anim) total_wind = round(wind_so_far) @@ -595,12 +612,17 @@ class EquationSolver2d(ColorMappedByFuncScene): ] mid_line_coords = rect.split_line_on_dim(dim_to_split) mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords] - mid_line = DashedLine(*mid_line_points) + mid_line = DashedLine(*mid_line_points) # TODO: Have this match rectangle line style, apart from dashes and thin-ness? + if len(sub_anims) > 0: + if self.display_in_parallel: + recursive_anim = AnimationGroup(*sub_anims) + else: + recursive_anim = Succession(*sub_anims) + else: + recursive_anim = empty_animation # Have to do this because Succession doesn't currently handle empty animations return Succession(anim, - ShowCreation(mid_line), - # FadeOut(mid_line), # TODO: Can change timing so this fades out at just the time it would be overdrawn - # TODO: Investigate weirdness with changing z buffer order on mid_line vs. rectangle lines - AnimationGroup(*sub_anims) + ShowCreation(mid_line), + recursive_anim ) lower_x = self.initial_lower_x @@ -724,7 +746,7 @@ class OdometerScene(Scene): dashed_line.rotate(-self.dashed_line_angle * TAU, about_point = ORIGIN) self.add(dashed_line) - num_display = DecimalNumber(0, include_background_rectangle = True) + num_display = DecimalNumber(0, include_background_rectangle = False).set_stroke(1) num_display.move_to(2 * DOWN) display_val_bias = 0 @@ -1074,7 +1096,8 @@ class LoopSplitSceneMapped(LoopSplitScene): class FundThmAlg(EquationSolver2d): CONFIG = { "func" : plane_poly_with_roots((1, 2), (-1, 1.5), (-1, 1.5)), - "num_iterations" : 10, + "num_iterations" : 4, + "display_in_parallel" : False } # TODO: Borsuk-Ulam visuals @@ -1119,6 +1142,10 @@ class DiffOdometer(OdometerScene): # Writing new Pi walker scenes by parametrizing general template +# Domain coloring scenes by parametrizing general template + +# (All the above are basically trivial tinkering at this point) + # ---- # Pi creature emotion stuff @@ -1127,20 +1154,16 @@ class DiffOdometer(OdometerScene): # Borsuk-Ulam visuals -# Domain coloring - -# TODO: Add to camera an option for low-quality background than other rendering, helpful -# for previews +# TODO: Add to camera an option for lower-quality (faster-rendered) background than pixel_array, +# helpful for previews #################### -class PureTest(Scene): - def construct(self): - point_list = [(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)] - output_list = map(lambda p : (p, point_to_rgba(p)), point_list) +class MapPiWalkerRect(PiWalkerRect): + CONFIG = { + "camera_class" : MappingCamera, + "camera_config" : {"mapping_func" : test_map_func}, + "display_output_color_map" : True + } - print output_list - - self.wait() - -# FIN +# FIN \ No newline at end of file diff --git a/helpers.py b/helpers.py index 3f2b6049..34eb032b 100644 --- a/helpers.py +++ b/helpers.py @@ -552,6 +552,13 @@ def squish_rate_func(func, a = 0.4, b = 0.6): return func((t-a)/(b-a)) return result +# Stylistically, should this take parameters (with default values)? +# Ultimately, the functionality is entirely subsumed by squish_rate_func, +# but it may be useful to have a nice name for with nice default params for +# "lingering", different from squish_rate_func's default params +def lingering(t): + return squish_rate_func(lambda t: t, 0, 0.8)(t) + ### Functional Functions ### def composition(func_list): diff --git a/topics/number_line.py b/topics/number_line.py index fc94924c..4dab9f07 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -321,6 +321,11 @@ class NumberPlane(VMobject): y = new_point[1]/self.get_y_unit_size() return x, y + # Does not recompute center, unit_sizes for each call; useful for + # iterating over large lists of points, but does assume these + # attributes are kept accurate. (Could alternatively have a method + # which returns a function dynamically created after a single + # call to each of get_center(), get_x_unit_size(), etc.) def point_to_coords_cheap(self, point): new_point = point - self.center_point x = new_point[0]/self.x_unit_size