From 2d34b2e28be0cf16a39d66b6d0e5919506ab11cd Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Thu, 18 Jan 2018 11:51:09 -0800 Subject: [PATCH 1/9] Incremental progress on WindingNumber --- active_projects/WindingNumber.py | 64 +++++++------------------------- 1 file changed, 14 insertions(+), 50 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 98abe10e..95c90d1d 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): @@ -618,7 +617,7 @@ class RectangleData(): def complex_to_pair(c): return (c.real, c.imag) -class iterative_2d_test(Scene): +class Iterative2dTest(Scene): CONFIG = { "func" : lambda (x, y) : complex_to_pair(complex(x, y)**2 - complex(1, 2)**2), "initial_lower_x" : -5.1, @@ -657,50 +656,28 @@ class iterative_2d_test(Scene): 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") + color = RED) self.play( ShowCreation(line), - #ChangingDecimal(num_display, rebased_winder) + ChangingDecimal(num_display, rebased_winder) ) - line.set_color("#00FF00") + line.set_color(GREEN) # Temporary hack to see (some) redraws; TODO: figure out a better approach 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 - ) + wind_so_far = 0 + sides = [ + explore_rect.get_top(), + explore_rect.get_right(), + explore_rect.get_bottom(), + explore_rect.get_left() + ] + for (start, end) in sides: + wind_so_far = draw_line_return_wind(start, end, wind_so_far) - 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) + total_wind = round(wind_so_far) if total_wind == 0: rect = alt_rect @@ -711,16 +688,3 @@ class iterative_2d_test(Scene): 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 - } - From d74127bb7fb221f08788586514d2f7b71c118820 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 13:02:41 -0800 Subject: [PATCH 2/9] Bug fixes to implementation of Succession animations --- animation/simple_animations.py | 36 +++++++++++++++++----------------- helpers.py | 6 ++++++ 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index bac8ffcc..68e4176e 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -329,7 +329,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( @@ -387,27 +387,27 @@ 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 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) + for i in range(len(self.animations)): + sub_alpha = anti_interpolate( + self.critical_alphas[i], + self.critical_alphas[i + 1], + alpha + ) + 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 = { diff --git a/helpers.py b/helpers.py index 87a04a46..57db6db0 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 anti_interpolate(start, end, value): + return np.true_divide(value - start, end - start) + def clamp(lower, upper, val): if val < lower: return lower From 7bcde6713d48f4045c159125dfa1bef700f0c872 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 13:03:12 -0800 Subject: [PATCH 3/9] Incremental progress on WindingNumber --- active_projects/WindingNumber.py | 154 +++++++++++++++++++++---------- 1 file changed, 104 insertions(+), 50 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 95c90d1d..80e5cd7d 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -542,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 @@ -577,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()) @@ -610,22 +616,47 @@ 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 Iterative2dTest(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 + +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" : 10, + "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): @@ -633,8 +664,63 @@ class Iterative2dTest(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), AnimationGroup(*sub_anims)) lower_x = self.initial_lower_x upper_x = self.initial_upper_x @@ -646,45 +732,13 @@ class Iterative2dTest(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 = RED) - self.play( - ShowCreation(line), - ChangingDecimal(num_display, rebased_winder) - ) - line.set_color(GREEN) # Temporary hack to see (some) redraws; TODO: figure out a better approach - return rebased_winder(1) - - for i in range(self.num_iterations): - (explore_rect, alt_rect) = rect.splits_on_dim(dim_to_split) - - wind_so_far = 0 - sides = [ - explore_rect.get_top(), - explore_rect.get_right(), - explore_rect.get_bottom(), - explore_rect.get_left() - ] - for (start, end) in sides: - wind_so_far = draw_line_return_wind(start, end, wind_so_far) - - total_wind = round(wind_so_far) - - if total_wind == 0: - rect = alt_rect - else: - rect = explore_rect - - dim_to_split = 1 - dim_to_split + self.play(anim) self.wait() From 123bae00a67575b01dafbc1ce6cf00c9d3e1cf04 Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 16:52:31 -0800 Subject: [PATCH 4/9] Further fixes to Succession animations --- animation/simple_animations.py | 38 +++++++++++++++------------------- helpers.py | 2 +- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 68e4176e..c7bd8f89 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -395,13 +395,22 @@ class Succession(Animation): 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): + self.rewind_to_start() + for i in range(len(self.animations)): - sub_alpha = anti_interpolate( + 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) @@ -424,23 +433,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 57db6db0..737eb2b7 100644 --- a/helpers.py +++ b/helpers.py @@ -307,7 +307,7 @@ def interpolate(start, end, alpha): def mid(start, end): return (start + end)/2.0 -def anti_interpolate(start, end, value): +def inverse_interpolate(start, end, value): return np.true_divide(value - start, end - start) def clamp(lower, upper, val): From 6e293782ffbb79696facfdc40515fd21d961884c Mon Sep 17 00:00:00 2001 From: Sridhar Ramesh Date: Fri, 19 Jan 2018 16:53:05 -0800 Subject: [PATCH 5/9] Incremental progress on WindingNumber (equation solver 2d) --- active_projects/WindingNumber.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 80e5cd7d..33478038 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -646,6 +646,9 @@ 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" : plane_poly_with_roots((1, 2), (-1, 3)), @@ -653,7 +656,7 @@ class EquationSolver2d(Scene): "initial_upper_x" : 5.1, "initial_lower_y" : -3.1, "initial_upper_y" : 3.1, - "num_iterations" : 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 @@ -679,7 +682,10 @@ class EquationSolver2d(Scene): color = RED) thin_line = line.copy() thin_line.set_stroke(width = 1) - anim = Succession(ShowCreation, line)#, Transform, line, thin_line) + anim = Succession( + ShowCreation, line, + Transform, line, thin_line + ) return (anim, rebased_winder(1)) wind_so_far = 0 @@ -720,7 +726,11 @@ class EquationSolver2d(Scene): 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), AnimationGroup(*sub_anims)) + return Succession(anim, + ShowCreation(mid_line), + FadeOut(mid_line), + UnsyncedParallel(*sub_anims) + ) lower_x = self.initial_lower_x upper_x = self.initial_upper_x From 4e468af6564b1af4a53fea472cc4620b3b8d6e17 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 19 Jan 2018 17:31:31 -0800 Subject: [PATCH 6/9] Update extract_scene.py args to better support previewing, showing in finder, opening output files, etc. --- extract_scene.py | 39 +++++++++++++++++++++++---------------- scene/scene.py | 23 +++++++++++++++-------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/extract_scene.py b/extract_scene.py index 455c7f25..2580ac8f 100644 --- a/extract_scene.py +++ b/extract_scene.py @@ -9,6 +9,7 @@ import inspect import traceback import imp import os +import subprocess as sp from helpers import * from scene import Scene @@ -58,7 +59,8 @@ def get_configuration(): ("-s", "--show_last_frame"), ("-l", "--low_quality"), ("-m", "--medium_quality"), - ("-f", "--save_pngs"), + ("-g", "--save_pngs"), + ("-f", "--show_file_in_finder"), ("-t", "--transparent"), ("-q", "--quiet"), ("-a", "--write_all") @@ -74,11 +76,10 @@ def get_configuration(): config = { "file" : args.file, "scene_name" : args.scene_name, - "camera_config" : PRODUCTION_QUALITY_CAMERA_CONFIG, #TODO - "frame_duration" : PRODUCTION_QUALITY_FRAME_DURATION, #TODO - "preview" : args.preview, - "write_to_movie" : args.write_to_movie, - "save_frames" : args.preview, #Scenes only save frame when previewing + "open_video_upon_completion" : args.preview, + "show_file_in_finder" : args.show_file_in_finder, + #By default, write to file + "write_to_movie" : args.write_to_movie or not args.show_last_frame, "show_last_frame" : args.show_last_frame, "save_pngs" : args.save_pngs, #If -t is passed in (for transparent), this will be RGBA @@ -88,7 +89,7 @@ def get_configuration(): "output_name" : args.output_name, "skip_to_animation_number" : args.skip_to_animation_number, } - if args.low_quality or args.preview: + if args.low_quality: config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG config["frame_duration"] = LOW_QUALITY_FRAME_DURATION elif args.medium_quality: @@ -102,10 +103,6 @@ def get_configuration(): if stan is not None: config["skip_to_animation_number"] = int(stan) - #By default, write to file - actions = ["write_to_movie", "preview", "show_last_frame"] - if not any([config[key] for key in actions]): - config["write_to_movie"] = True config["skip_animations"] = any([ config["show_last_frame"] and not config["write_to_movie"], config["skip_to_animation_number"], @@ -117,12 +114,23 @@ def handle_scene(scene, **config): curr_stdout = sys.stdout sys.stdout = open(os.devnull, "w") - if config["preview"]: - scene.preview() if config["show_last_frame"]: - if not config["write_all"]: - scene.show_frame() scene.save_image(mode = config["saved_image_mode"]) + open_file = any([ + config["show_last_frame"], + config["open_video_upon_completion"], + config["show_file_in_finder"] + ]) + if open_file: + commands = ["open"] + if config["show_file_in_finder"]: + commands.append("-R") + # + if config["show_last_frame"]: + commands.append(scene.get_image_file_path()) + else: + commands.append(scene.get_movie_file_path()) + sp.call(commands) if config["quiet"]: sys.stdout.close() @@ -209,7 +217,6 @@ def main(): "frame_duration", "skip_animations", "write_to_movie", - "save_frames", "output_directory", "save_pngs", "skip_to_animation_number", diff --git a/scene/scene.py b/scene/scene.py index 5b515472..1a01c411 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -33,6 +33,7 @@ class Scene(object): "save_pngs" : False, "pngs_mode" : "RGBA", "output_directory" : ANIMATIONS_DIR, + "movie_file_extension" : ".mp4", "name" : None, "always_continually_update" : False, "random_seed" : 0, @@ -470,24 +471,30 @@ class Scene(object): def preview(self): TkSceneRoot(self) - - def save_image(self, name = None, mode = "RGB", dont_update = False): + + def get_image_file_path(self, name = None, dont_update = False): folder = "images" if dont_update: folder = str(self) - path = os.path.join(self.output_directory, folder) file_name = (name or str(self)) + ".png" - full_path = os.path.join(path, file_name) + return os.path.join(path, file_name) + + def save_image(self, name = None, mode = "RGB", dont_update = False): + path = self.get_image_file_path(name, dont_update) if not os.path.exists(path): os.makedirs(path) if not dont_update: self.update_frame() image = self.get_image() image = image.convert(mode) - image.save(full_path) + image.save(path) - def get_movie_file_path(self, name, extension): + def get_movie_file_path(self, name = None, extension = None): + if extension is None: + extension = self.movie_file_extension + if name is None: + name = self.name file_path = os.path.join(self.output_directory, name) if not file_path.endswith(extension): file_path += extension @@ -497,8 +504,8 @@ class Scene(object): def open_movie_pipe(self): name = str(self) - file_path = self.get_movie_file_path(name, ".mp4") - temp_file_path = file_path.replace(".mp4", "Temp.mp4") + file_path = self.get_movie_file_path(name) + temp_file_path = file_path.replace(name, name + "Temp") print("Writing to %s"%temp_file_path) self.args_to_rename_file = (temp_file_path, file_path) From a28d13fc3e5f2791601eb111ff15f2b26a828b88 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 19 Jan 2018 18:35:58 -0800 Subject: [PATCH 7/9] Finished DrawFrequencyPlot of Fourier --- active_projects/fourier.py | 192 ++++++++++++++++++++++++++++++------- 1 file changed, 157 insertions(+), 35 deletions(-) diff --git a/active_projects/fourier.py b/active_projects/fourier.py index 6537cc2f..0e32265e 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -707,10 +707,7 @@ class FourierMachineScene(Scene): for label in labels: label.scale(self.text_scale_val) time_label.next_to(time_axes.coords_to_point(3.5,0), DOWN) - intensity_label.next_to( - time_axes.y_axis.get_top(), RIGHT, - aligned_edge = UP, - ) + intensity_label.next_to(time_axes.y_axis.get_top(), RIGHT) time_axes.labels = labels time_axes.add(labels) time_axes.to_corner(UP+LEFT) @@ -764,8 +761,10 @@ class FourierMachineScene(Scene): graph = self.time_axes.get_graph(func, **config) return graph - def get_cosine_wave(self, freq = 1): - return self.get_time_graph(lambda t : 1 + 0.5*np.cos(TAU*freq*t)) + def get_cosine_wave(self, freq = 1, shift_val = 1, scale_val = 0.9): + return self.get_time_graph( + lambda t : shift_val + scale_val*np.cos(TAU*freq*t) + ) def get_fourier_transform_graph(self, time_graph, **kwargs): if not hasattr(self, "frequency_axes"): @@ -795,7 +794,6 @@ class FourierMachineScene(Scene): )[0] return fourier_transform - def get_polarized_mobject(self, mobject, freq = 1.0): if not hasattr(self, "circle_plane"): self.get_circle_plane() @@ -958,6 +956,8 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): for x in 1, 2 ]) words = self.get_bps_label() + words.save_state() + words.next_to(axes.coords_to_point(1.5, 0), DOWN, MED_LARGE_BUFF) self.add(axes) self.play(ShowCreation(graph, run_time = 2, rate_func = None)) @@ -967,7 +967,10 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): *map(ShowCreation, v_lines) ) self.wait() - self.play(FadeOut(VGroup(braces, v_lines))) + self.play( + FadeOut(VGroup(braces, v_lines)), + words.restore, + ) self.wait() self.beats_per_second_label = words @@ -1101,7 +1104,7 @@ class WrapCosineGraphAroundCircle(FourierMachineScene): braces = VGroup(*self.get_peak_braces()[3:6]) words = TextMobject("3 beats/second") words.scale_to_fit_width(0.9*braces.get_width()) - words.next_to(braces, UP, SMALL_BUFF) + words.move_to(braces, DOWN) return words class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): @@ -1112,9 +1115,9 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): def construct(self): self.remove(self.pi_creature) self.setup_graph() - # self.indicate_weight_of_wire() + self.indicate_weight_of_wire() self.show_center_of_mass_dot() - # self.change_to_various_frequencies() + self.change_to_various_frequencies() self.introduce_frequency_plot() self.draw_full_frequency_plot() self.recap_objects_on_screen() @@ -1229,6 +1232,7 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): ), t_min = 0, t_max = TAU, ) + flower_path.move_to(self.center_of_mass_dot) self.play( wps_label.move_to, self.circle_plane.get_top(), @@ -1245,6 +1249,8 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): )) self.wait() + self.x_coord_label = x_coord_label + def draw_full_frequency_plot(self): graph = self.graph fourier_graph = self.get_fourier_transform_graph(graph) @@ -1255,51 +1261,129 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): stroke_width = 6, color = fourier_graph.get_color() ) - dot = Dot(color = fourier_graph.get_color()) - def update_dot(dot): - f = self.graph.polarized_mobject.frequency - dot.move_to(self.frequency_axes.input_to_graph_point( - f, fourier_graph - )) - dot_update_anim = UpdateFromFunc(dot, update_dot) self.change_frequency(0.0) - dot_update_anim.update(0) + self.generate_fourier_dot_transform(fourier_graph) self.wait() self.play(ShowCreation(v_line)) - self.play(GrowFromCenter(dot), FadeOut(v_line)) + self.play( + GrowFromCenter(self.fourier_graph_dot), + FadeOut(v_line) + ) f_max = int(self.frequency_axes.x_max) - for freq in range(1, f_max+1): + for freq in [0.2, 1.5, 3.0, 4.0, 5.0]: fourier_graph.restore() self.change_frequency( freq, - added_anims = [ - ShowCreation( - fourier_graph, - rate_func = lambda t : interpolate( - (freq-1.)/f_max, - float(freq)/f_max, - smooth(t) - ), + added_anims = [ShowCreation( + fourier_graph, + rate_func = lambda t : interpolate( + (freq-1.)/f_max, + float(freq)/f_max, + smooth(t) ), - dot_update_anim - ], + )], run_time = 5, ) self.wait() - self.play(FadeOut(dot)) self.fourier_graph = fourier_graph def recap_objects_on_screen(self): rect = FullScreenFadeRectangle() + time_group = VGroup( + self.graph, + self.time_axes, + self.beats_per_second_label, + ).copy() + circle_group = VGroup( + self.graph.polarized_mobject, + self.circle_plane, + self.winding_freq_label, + self.center_of_mass_label, + self.center_of_mass_dot, + ).copy() + frequency_group = VGroup( + self.fourier_graph, + self.frequency_axes, + self.x_coord_label, + ).copy() + groups = [time_group, circle_group, frequency_group] self.play(FadeIn(rect)) + self.wait() + for group in groups: + self.play(FadeIn(group)) + self.play(ShowCreation(group[0])) + self.wait() + self.play(FadeOut(group)) + self.wait() + self.play(FadeOut(rect)) def lower_graph(self): - pass + graph = self.graph + time_axes = self.time_axes + shift_vect = time_axes.coords_to_point(0, 1) + shift_vect -= time_axes.coords_to_point(0, 0) + fourier_graph = self.fourier_graph + new_graph = self.get_cosine_wave( + self.signal_frequency, shift_val = 0 + ) + new_fourier_graph = self.get_fourier_transform_graph(new_graph) + for mob in graph, time_axes, fourier_graph: + mob.save_state() + + new_freq = 0.03 + self.change_frequency(new_freq) + self.wait() + self.play( + time_axes.shift, shift_vect/2, + graph.shift, -shift_vect/2, + self.get_frequency_change_animation( + self.graph, new_freq + ), + self.center_of_mass_dot_anim, + self.get_period_v_lines_update_anim(), + Transform(fourier_graph, new_fourier_graph), + self.fourier_graph_dot.move_to, + self.frequency_axes.coords_to_point(new_freq, 0), + run_time = 2 + ) + self.wait() + self.remove(self.fourier_graph_dot) + self.generate_fourier_dot_transform(new_fourier_graph) + self.change_frequency(3.0, run_time = 15, rate_func = None) + self.wait() + self.play( + graph.restore, + time_axes.restore, + self.get_frequency_change_animation( + self.graph, 3.0 + ), + self.center_of_mass_dot_anim, + self.get_period_v_lines_update_anim(), + fourier_graph.restore, + Animation(self.fourier_graph_dot), + run_time = 2 + ) + self.generate_fourier_dot_transform(self.fourier_graph) + self.wait() + self.play(FocusOn(self.fourier_graph_dot)) + self.wait() def label_as_almost_fourier(self): - pass + x_coord_label = self.x_coord_label + almost_fourier_label = TextMobject( + "``Almost Fourier Transform''", + ) + almost_fourier_label.move_to(x_coord_label, UP+LEFT) + x_coord_label.generate_target() + x_coord_label.target.next_to(almost_fourier_label, DOWN) + + self.play( + MoveToTarget(x_coord_label), + Write(almost_fourier_label) + ) + self.wait(2) ## @@ -1314,6 +1398,18 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): # result += self.circle_plane.get_center() return result + def generate_fourier_dot_transform(self, fourier_graph): + self.fourier_graph_dot = Dot(color = WHITE, radius = 0.05) + def update_dot(dot): + f = self.graph.polarized_mobject.frequency + dot.move_to(self.frequency_axes.input_to_graph_point( + f, fourier_graph + )) + self.fourier_graph_dot_anim = UpdateFromFunc( + self.fourier_graph_dot, update_dot + ) + self.fourier_graph_dot_anim.update(0) + def change_frequency(self, new_freq, **kwargs): kwargs["run_time"] = kwargs.get("run_time", 3) added_anims = kwargs.get("added_anims", []) @@ -1329,14 +1425,40 @@ class DrawFrequencyPlot(WrapCosineGraphAroundCircle, PiCreatureScene): self.get_period_v_lines_update_anim(), ] anims += added_anims - #TODO, conditionals for center of mass if hasattr(self, "center_of_mass_dot"): anims.append(self.center_of_mass_dot_anim) + if hasattr(self, "fourier_graph_dot"): + anims.append(self.fourier_graph_dot_anim) self.play(*anims, **kwargs) def create_pi_creature(self): return Mortimer().to_corner(DOWN+RIGHT) +class StudentsHorrifiedAtScene(TeacherStudentsScene): + def construct(self): + self.change_student_modes( + *3*["horrified"], + look_at_arg = 2*UP + 3*LEFT + ) + self.wait(4) + + +class ShowLinearity(DrawFrequencyPlot): + def construct(self): + self.show_lower_frequency_signal() + self.play_with_lower_frequency_signal() + self.point_out_fourier_spike() + + + + + + + + + + + From 78b660bb923ca5c990b8c163e61f582e4372a5f4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 19 Jan 2018 18:46:23 -0800 Subject: [PATCH 8/9] Fixed bug with creating image directories --- scene/scene.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scene/scene.py b/scene/scene.py index 1a01c411..fb6df9b4 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -482,8 +482,9 @@ class Scene(object): def save_image(self, name = None, mode = "RGB", dont_update = False): path = self.get_image_file_path(name, dont_update) - if not os.path.exists(path): - os.makedirs(path) + directory_path = os.path.dirname(path) + if not os.path.exists(directory_path): + os.makedirs(directory_path) if not dont_update: self.update_frame() image = self.get_image() From e6df91d03e7ae8b714684f1e42c9720367b3ffa2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 19 Jan 2018 18:57:52 -0800 Subject: [PATCH 9/9] Small bug fix to foreground mobject behavior during an animation --- scene/scene.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scene/scene.py b/scene/scene.py index fb6df9b4..cd4892eb 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -314,7 +314,10 @@ class Scene(object): def get_moving_mobjects(self, *animations): moving_mobjects = list(it.chain( - [anim.mobject for anim in animations], + [ + anim.mobject for anim in animations + if anim.mobject not in self.foreground_mobjects + ], [ca.mobject for ca in self.continual_animations], self.foreground_mobjects, ))