diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 98abe10e..33478038 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -514,7 +514,6 @@ class FuncRotater(Animation): angle_revs * 2 * np.pi, ) self.mobject.set_color(color_func(angle_revs)) - # Will want to have arrow colors change to match direction as well class TestRotater(Scene): def construct(self): @@ -543,6 +542,12 @@ class OdometerScene(Scene): rate_func = None) def point_to_rev((x, y)): + # Warning: np.arctan2 would happily discontinuously returns the value 0 for (0, 0), due to + # design choices in the underlying atan2 library call, but for our purposes, this is + # illegitimate, and all winding number calculations must be set up to avoid this + if (x, y) == (0, 0): + print "Error! Angle of (0, 0) computed!" + return None return np.true_divide(np.arctan2(y, x), 2 * np.pi) # Returns the value with the same fractional component as x, closest to m @@ -578,16 +583,16 @@ class RectangleData(): self.rect = (x_interval, y_interval) def get_top_left(self): - return np.array((self.rect[0][0], self.rect[1][0])) + 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][0])) - - def get_bottom_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][1])) + return np.array((self.rect[0][0], self.rect[1][0])) def get_top(self): return (self.get_top_left(), self.get_top_right()) @@ -611,22 +616,50 @@ class RectangleData(): elif dim == 1: return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)] else: - print "Error!" + print "RectangleData.splits_on_dim passed illegitimate dimension!" return tuple(return_data) + def split_line_on_dim(self, dim): + x_interval = self.rect[0] + y_interval = self.rect[1] + + if dim == 0: + sides = (self.get_top(), self.get_bottom()) + elif dim == 1: + sides = (self.get_left(), self.get_right()) + + return tuple([mid(x, y) for (x, y) in sides]) + def complex_to_pair(c): return (c.real, c.imag) -class iterative_2d_test(Scene): +def plane_poly_with_roots(*points): + def f((x, y)): + return complex_to_pair(np.prod([complex(x, y) - complex(a,b) for (a,b) in points])) + return f + +def plane_func_from_complex_func(f): + return lambda (x, y) : complex_to_pair(f(complex(x,y))) + +empty_animation = Animation(Mobject()) +def EmptyAnimation(): + return empty_animation + +# TODO: Perhaps restructure this to avoid using AnimationGroup/UnsyncedParallels, and instead +# use lists of animations or lists or other such data, to be merged and processed into parallel +# animations later +class EquationSolver2d(Scene): CONFIG = { - "func" : lambda (x, y) : complex_to_pair(complex(x, y)**2 - complex(1, 2)**2), + "func" : plane_poly_with_roots((1, 2), (-1, 3)), "initial_lower_x" : -5.1, "initial_upper_x" : 5.1, "initial_lower_y" : -3.1, "initial_upper_y" : 3.1, - "num_iterations" : 20, - "num_checkpoints" : 10 + "num_iterations" : 5, + "num_checkpoints" : 10, + # TODO: Consider adding a "find_all_roots" flag, which could be turned off + # to only explore one of the two candidate subrectangles when both are viable } def construct(self): @@ -634,8 +667,70 @@ class iterative_2d_test(Scene): num_plane.fade() self.add(num_plane) - num_display = DecimalNumber(0, color = ORANGE) - num_display.move_to(UP + RIGHT) + rev_func = lambda p : point_to_rev(self.func(p)) + + def Animate2dSolver(cur_depth, rect, dim_to_split): + if cur_depth >= self.num_iterations: + return EmptyAnimation() + + def draw_line_return_wind(start, end, start_wind): + alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints) + a0 = alpha_winder(0) + rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind + line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), + stroke_width = 5, + color = RED) + thin_line = line.copy() + thin_line.set_stroke(width = 1) + anim = Succession( + ShowCreation, line, + Transform, line, thin_line + ) + return (anim, rebased_winder(1)) + + wind_so_far = 0 + anim = EmptyAnimation() + sides = [ + rect.get_top(), + rect.get_right(), + rect.get_bottom(), + rect.get_left() + ] + for (start, end) in sides: + (next_anim, wind_so_far) = draw_line_return_wind(start, end, wind_so_far) + anim = Succession(anim, next_anim) + + total_wind = round(wind_so_far) + + if total_wind == 0: + coords = [ + rect.get_top_left(), + rect.get_top_right(), + rect.get_bottom_right(), + rect.get_bottom_left() + ] + points = [num_plane.coords_to_point(x, y) for (x, y) in coords] + fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = RED) + return Succession(anim, FadeIn(fill_rect)) + else: + (sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split) + sub_rects = [sub_rect1, sub_rect2] + sub_anims = [ + Animate2dSolver( + cur_depth = cur_depth + 1, + rect = sub_rect, + dim_to_split = 1 - dim_to_split + ) + for sub_rect in sub_rects + ] + mid_line_coords = rect.split_line_on_dim(dim_to_split) + mid_line_points = [num_plane.coords_to_point(x, y) for (x, y) in mid_line_coords] + mid_line = DashedLine(*mid_line_points) + return Succession(anim, + ShowCreation(mid_line), + FadeOut(mid_line), + UnsyncedParallel(*sub_anims) + ) lower_x = self.initial_lower_x upper_x = self.initial_upper_x @@ -647,80 +742,13 @@ class iterative_2d_test(Scene): rect = RectangleData(x_interval, y_interval) - rev_func = lambda p : point_to_rev(self.func(p)) + anim = Animate2dSolver( + cur_depth = 0, + rect = rect, + dim_to_split = 0, + ) - dim_to_split = 0 # 0 for x, 1 for y - - def draw_line_return_wind(start, end, start_wind): - alpha_winder = make_alpha_winder(rev_func, start, end, self.num_checkpoints) - a0 = alpha_winder(0) - rebased_winder = lambda alpha: alpha_winder(alpha) - a0 + start_wind - line = Line(num_plane.coords_to_point(*start), num_plane.coords_to_point(*end), - stroke_width = 5, - color = "#FF0000") - self.play( - ShowCreation(line), - #ChangingDecimal(num_display, rebased_winder) - ) - line.set_color("#00FF00") - return rebased_winder(1) - - for i in range(self.num_iterations): - (explore_rect, alt_rect) = rect.splits_on_dim(dim_to_split) - - top_wind = draw_line_return_wind( - explore_rect.get_top_left(), - explore_rect.get_top_right(), - 0 - ) - - print(len(self.mobjects)) - - right_wind = draw_line_return_wind( - explore_rect.get_top_right(), - explore_rect.get_bottom_right(), - top_wind - ) - - print(len(self.mobjects)) - - bottom_wind = draw_line_return_wind( - explore_rect.get_bottom_right(), - explore_rect.get_bottom_left(), - right_wind - ) - - print(len(self.mobjects)) - - left_wind = draw_line_return_wind( - explore_rect.get_bottom_left(), - explore_rect.get_top_left(), - bottom_wind - ) - - print(len(self.mobjects)) - - total_wind = round(left_wind) - - if total_wind == 0: - rect = alt_rect - else: - rect = explore_rect - - dim_to_split = 1 - dim_to_split + self.play(anim) self.wait() - -class EquationSolver2d(ZoomedScene): - #TODO - CONFIG = { - "func" : lambda p : p, - "target_input" : (0, 0), - "target_output" : (0, 0), - "initial_top_left_point" : (0, 0), - "initial_guess_dimensions" : (0, 0), - "num_iterations" : 10, - "iteration_at_which_to_start_zoom" : None - } - diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 672de78d..bb703443 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -357,7 +357,7 @@ class Succession(Animation): """ Each arg will either be an animation, or an animation class followed by its arguments (and potentially a dict for - configuraiton). + configuration). For example, Succession( @@ -415,27 +415,36 @@ class Succession(Animation): #might very well mess with it. self.original_run_time = run_time + # 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) + mobject = Group(*[anim.mobject for anim in self.animations]) Animation.__init__(self, mobject, run_time = run_time, **kwargs) + def rewind_to_start(self): + for anim in reversed(self.animations): + anim.update(0) + def update_mobject(self, alpha): - if alpha >= 1.0: - self.animations[-1].update(1) - return - run_times = self.run_times - index = 0 - time = alpha*self.original_run_time - while sum(run_times[:index+1]) < time: - index += 1 - if index > self.last_index: - self.animations[self.last_index].update(1) - self.animations[self.last_index].clean_up() - self.last_index = index - curr_anim = self.animations[index] - sub_alpha = np.clip( - (time - sum(run_times[:index]))/run_times[index], 0, 1 - ) - curr_anim.update(sub_alpha) + self.rewind_to_start() + + for i in range(len(self.animations)): + sub_alpha = inverse_interpolate( + self.critical_alphas[i], + self.critical_alphas[i + 1], + alpha + ) + if sub_alpha < 0: + return + + sub_alpha = clamp(0, 1, sub_alpha) # Could possibly adopt a non-clamping convention here + self.animations[i].update(sub_alpha) + + def clean_up(self, *args, **kwargs): + for anim in self.animations: + anim.clean_up(*args, **kwargs) class AnimationGroup(Animation): CONFIG = { @@ -452,23 +461,10 @@ class AnimationGroup(Animation): for anim in self.sub_anims: anim.update(alpha) - - - - - - - - - - - - - - - - - - - - +# Parallel animations where shorter animations are not stretched out to match the longest +class UnsyncedParallel(AnimationGroup): + def __init__(self, *sub_anims, **kwargs): + digest_config(self, kwargs, locals()) + self.run_time = max([a.run_time for a in sub_anims]) + everything = Mobject(*[a.mobject for a in sub_anims]) + Animation.__init__(self, everything, **kwargs) \ No newline at end of file diff --git a/helpers.py b/helpers.py index 87a04a46..737eb2b7 100644 --- a/helpers.py +++ b/helpers.py @@ -304,6 +304,12 @@ def digest_locals(obj, keys = None): def interpolate(start, end, alpha): return (1-alpha)*start + alpha*end +def mid(start, end): + return (start + end)/2.0 + +def inverse_interpolate(start, end, value): + return np.true_divide(value - start, end - start) + def clamp(lower, upper, val): if val < lower: return lower