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/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() + + + + + + + + + + + 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/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/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 diff --git a/scene/scene.py b/scene/scene.py index 5b515472..cd4892eb 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, @@ -313,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, )) @@ -470,24 +474,31 @@ 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) - if not os.path.exists(path): - os.makedirs(path) + 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) + 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() 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 +508,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)