diff --git a/active_projects/WindingNumber.py b/active_projects/WindingNumber.py index 1fed87e4..190891c3 100644 --- a/active_projects/WindingNumber.py +++ b/active_projects/WindingNumber.py @@ -32,64 +32,8 @@ from topics.graph_scene import * # TODO/WARNING: There's a lot of refactoring and cleanup to be done in this code, # (and it will be done, but first I'll figure out what I'm doing with all this...) # -SR - -class DualScene(Scene): - CONFIG = { - "num_needed_anchor_points" : 10 - } - - def setup(self): - split_line = DashedLine(SPACE_HEIGHT * UP, SPACE_HEIGHT * DOWN) - self.num_plane = NumberPlane(x_radius = SPACE_WIDTH/2) - self.num_plane.to_edge(LEFT, buff = 0) - self.num_plane.prepare_for_nonlinear_transform() - self.add(self.num_plane, split_line) - - def apply_function(self, func, run_time = 3): - self.func = func - right_plane = self.num_plane.copy() - right_plane.center() - right_plane.prepare_for_nonlinear_transform() - right_plane.apply_function(func) - right_plane.shift(SPACE_WIDTH/2 * RIGHT) - self.right_plane = right_plane - crappy_cropper = FullScreenFadeRectangle(fill_opacity = 1) - crappy_cropper.stretch_to_fit_width(SPACE_WIDTH) - crappy_cropper.to_edge(LEFT, buff = 0) - self.play( - ReplacementTransform(self.num_plane.copy(), right_plane), - FadeIn(crappy_cropper), - Animation(self.num_plane), - run_time = run_time - ) - - def squash_onto_left(self, object): - object.shift(SPACE_WIDTH/2 * LEFT) - - def squash_onto_right(self, object): - object.shift(SPACE_WIDTH/2 * RIGHT) - - def path_draw(self, input_object, run_time = 3): - output_object = input_object.copy() - if input_object.get_num_anchor_points() < self.num_needed_anchor_points: - input_object.insert_n_anchor_points(self.num_needed_anchor_points) - output_object.apply_function(self.func) - self.squash_onto_left(input_object) - self.squash_onto_right(output_object) - self.play( - ShowCreation(input_object), - ShowCreation(output_object), - run_time = run_time - ) - -class TestDual(DualScene): - def construct(self): - self.force_skipping() - self.apply_function(lambda (x, y, z) : complex_to_R3(complex(x,y)**2)) - self.revert_to_original_skipping_status() - self.path_draw(Line(LEFT + DOWN, RIGHT + DOWN)) -class EquationSolver1d(GraphScene, ZoomedScene, ReconfigurableScene): +class EquationSolver1d(GraphScene, ZoomedScene): CONFIG = { "func" : lambda x : x, "targetX" : 0, @@ -302,7 +246,7 @@ class ArrowCircleTest(Scene): base_arrow = Arrow(circle_radius * 0.7 * RIGHT, circle_radius * 1.3 * RIGHT) def rev_rotate(x, revs): - x.rotate(revs * TAU) + x.rotate(revs * TAU, about_point = ORIGIN) x.set_color(color_func(revs)) return x @@ -560,8 +504,11 @@ class PiWalker(Scene): rev_func = rev_func, remover = (i < len(walk_coords) - 1) ), - ShowCreation(Line(start_point, end_point)), + ShowCreation(Line(start_point, end_point), rate_func = None), run_time = self.step_run_time) + + # TODO: Allow smooth paths instead of brekaing them up into lines, and + # use point_from_proportion to get points along the way self.wait() @@ -626,10 +573,10 @@ class EquationSolver2d(Scene): 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), + flashing_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 = flashing_line.copy() thin_line.set_stroke(width = 1) walker_anim = LinearWalker( start_coords = start, @@ -638,12 +585,12 @@ class EquationSolver2d(Scene): rev_func = rev_func, remover = True ) - line_draw_anim = AnimationGroup(ShowCreation(line, rate_func = None), walker_anim, - run_time = 2) - anim = Succession( - line_draw_anim, - Transform, line, thin_line - ) + line_draw_anim = AnimationGroup( + ShowCreation(thin_line), + #ShowPassingFlash(flashing_line), + walker_anim, + rate_func = None) + anim = line_draw_anim return (anim, rebased_winder(1)) wind_so_far = 0 @@ -736,6 +683,29 @@ class FirstSqrtScene(EquationSolver1d): "show_target_line" : True, } +class SecondSqrtScene(FirstSqrtScene, ReconfigurableScene): +# TODO: Don't bother with ReconfigurableScene; just use new config from start +# (But can also use this as written, and just cut into middle in Premiere) + + def setup(self): + FirstSqrtScene.setup(self) + ReconfigurableScene.setup(self) + + def construct(self): + shiftVal = self.targetY + + self.drawGraph() + newOrigin = self.coords_to_point(0, shiftVal) + self.transition_to_alt_config( + func = lambda x : x**2 - shiftVal, + targetY = 0, + graph_label = "y = x^2 - " + str(shiftVal), + y_min = self.y_min - shiftVal, + y_max = self.y_max - shiftVal, + show_target_line = False, + graph_origin = newOrigin) + self.solveEquation() + # TODO: Pi creatures intrigued class ComplexPlaneIs2d(Scene): @@ -758,15 +728,22 @@ class NumberLineScene(Scene): left_point = num_line.number_to_point(-1) right_point = num_line.number_to_point(1) + # TODO: Make this line a thin rectangle interval_1d = Line(left_point, right_point, stroke_color = inner_color, stroke_width = stroke_width) + rect_1d = Rectangle(stroke_width = 0, fill_opacity = 1, fill_color = inner_color) + rect_1d.replace(interval_1d) + rect_1d.stretch_to_fit_height(SMALL_BUFF) left_dot = Dot(left_point, stroke_width = stroke_width, color = border_color) right_dot = Dot(right_point, stroke_width = stroke_width, color = border_color) endpoints_1d = VGroup(left_dot, right_dot) - full_1d = VGroup(interval_1d, endpoints_1d) + full_1d = VGroup(rect_1d, endpoints_1d) self.play(ShowCreation(full_1d)) self.wait() + # TODO: Can polish the morphing above; have dots become left and right sides, and + # only then fill in the top and bottom + num_plane = NumberPlane() random_points = [UP + LEFT, UP + RIGHT, DOWN + RIGHT, DOWN + LEFT] @@ -790,20 +767,78 @@ class NumberLineScene(Scene): self.wait() -class Initial2dFuncScene(Scene): +initial_2d_func = point_func_from_complex_func(lambda c : np.exp(c)) + +class Initial2dFuncSceneMorphing(Scene): + CONFIG = { + "num_needed_anchor_points" : 10, + "func" : initial_2d_func, + } + + def setup(self): + split_line = DashedLine(SPACE_HEIGHT * UP, SPACE_HEIGHT * DOWN) + self.num_plane = NumberPlane(x_radius = SPACE_WIDTH/2) + self.num_plane.to_edge(LEFT, buff = 0) + self.num_plane.prepare_for_nonlinear_transform() + self.add(self.num_plane, split_line) + + def squash_onto_left(self, object): + object.shift(SPACE_WIDTH/2 * LEFT) + + def squash_onto_right(self, object): + object.shift(SPACE_WIDTH/2 * RIGHT) + + def obj_draw(self, input_object): + output_object = input_object.copy() + if input_object.get_num_anchor_points() < self.num_needed_anchor_points: + input_object.insert_n_anchor_points(self.num_needed_anchor_points) + output_object.apply_function(self.func) + self.squash_onto_left(input_object) + self.squash_onto_right(output_object) + self.play( + ShowCreation(input_object), + ShowCreation(output_object) + ) + + def construct(self): + right_plane = self.num_plane.copy() + right_plane.center() + right_plane.prepare_for_nonlinear_transform() + right_plane.apply_function(self.func) + right_plane.shift(SPACE_WIDTH/2 * RIGHT) + self.right_plane = right_plane + crappy_cropper = FullScreenFadeRectangle(fill_opacity = 1) + crappy_cropper.stretch_to_fit_width(SPACE_WIDTH) + crappy_cropper.to_edge(LEFT, buff = 0) + self.play( + ReplacementTransform(self.num_plane.copy(), right_plane), + FadeIn(crappy_cropper), + Animation(self.num_plane), + run_time = 3 + ) + + points = [LEFT + DOWN, RIGHT + DOWN, LEFT + UP, RIGHT + UP] + for i in range(len(points) - 1): + line = Line(points[i], points[i + 1], color = RED) + self.obj_draw(line) + +# Alternative to the above, using MappingCameras, but no morphing animation +class Initial2dFuncSceneWithoutMorphing(Scene): def setup(self): left_camera = Camera(**self.camera_config) right_camera = MappingCamera( - mapping_func = point_func_from_complex_func(lambda c : np.exp(c)), + mapping_func = initial_2d_func, **self.camera_config) split_screen_camera = SplitScreenCamera(left_camera, right_camera, **self.camera_config) self.camera = split_screen_camera def construct(self): num_plane = NumberPlane() - num_plane.fade() + num_plane.prepare_for_nonlinear_transform() + #num_plane.fade() self.add(num_plane) + points = [LEFT + DOWN, RIGHT + DOWN, LEFT + UP, RIGHT + UP] for i in range(len(points) - 1): line = Line(points[i], points[i + 1], color = RED) @@ -814,28 +849,8 @@ class Initial2dFuncScene(Scene): # TODO: Bunch of Pi walker scenes # TODO: An odometer scene when introducing winding numbers - -class SecondSqrtScene(FirstSqrtScene, ReconfigurableScene): -# TODO: Don't bother with ReconfigurableScene; just use new config from start - - def setup(self): - FirstSqrtScene.setup(self) - ReconfigurableScene.setup(self) - - def construct(self): - shiftVal = self.targetY - - self.drawGraph() - newOrigin = self.coords_to_point(0, shiftVal) - self.transition_to_alt_config( - func = lambda x : x**2 - shiftVal, - targetY = 0, - graph_label = "y = x^2 - " + str(shiftVal), - y_min = self.y_min - shiftVal, - y_max = self.y_max - shiftVal, - show_target_line = False, - graph_origin = newOrigin) - self.solveEquation() +# (Just set up an OdometerScene with function matching the walking of the Pi +# creature from previous scene, then place it as a simultaneous inset with Premiere) class LoopSplitScene(Scene): @@ -963,12 +978,12 @@ class LoopSplitSceneMapped(LoopSplitScene): # to illustrate relation between degree and large-scale winding number class FundThmAlg(EquationSolver2d): CONFIG = { - "func" : plane_poly_with_roots((1, 2), (-1, 3), (-1, 3)), + "func" : plane_poly_with_roots((1, 2), (-1, 2.5), (-1, 2.5)), "num_iterations" : 1, } # TODO: Borsuk-Ulam visuals -# Note: May want to do an ordinary square scene, then mapping func it into a circle +# Note: May want to do an ordinary square scene, then MappingCamera it into a circle # class BorsukUlamScene(PiWalker): # 3-way scene of "Good enough"-illustrating odometers; to be composed in Premiere @@ -1002,15 +1017,16 @@ class DiffOdometer(OdometerScene): # TODOs, from easiest to hardest: -# Minor fiddling with little things in each animation; placements, colors, timing +# Minor fiddling with little things in each animation; placements, colors, timing, text -# Odometer/swinging arrows stuff +# Initial odometer scene (simple once previous Pi walker scene is decided upon) -# Writing new Pi creature walker scenes off of general template +# Writing new Pi walker scenes by parametrizing general template -# Split screen illustration of 2d function (before domain coloring) +# Generalizing Pi walker stuff to make bullets on pulsing lines change colors dynamically according to +# function traced out -# Generalizing Pi color walker stuff/making bullets on pulsing lines change colors dynamically according to function traced out +# Debugging Pi walker stuff added to EquationSolver2d # ---- @@ -1022,4 +1038,6 @@ class DiffOdometer(OdometerScene): # Domain coloring +# TODO: Ask about tracked mobject, which is probably very useful for our animations + # FIN diff --git a/active_projects/basel.py b/active_projects/basel.py index 909fd1cc..a29a14bb 100644 --- a/active_projects/basel.py +++ b/active_projects/basel.py @@ -30,9 +30,9 @@ from mobject.vectorized_mobject import * ## To watch one of these scenes, run the following: ## python extract_scene.py -p file_name -inverse_power_law = lambda maxint,cutoff,exponent: \ - (lambda r: maxint * (cutoff/(r+cutoff))**exponent) -inverse_quadratic = lambda maxint,cutoff: inverse_power_law(maxint,cutoff,2) +inverse_power_law = lambda maxint,scale,cutoff,exponent: \ + (lambda r: maxint * (cutoff/(r/scale+cutoff))**exponent) +inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2) LIGHT_COLOR = YELLOW INDICATOR_RADIUS = 0.7 @@ -44,15 +44,15 @@ FAST_INDICATOR_UPDATE_TIME = 0.1 OPACITY_FOR_UNIT_INTENSITY = 0.2 SWITCH_ON_RUN_TIME = 2.5 FAST_SWITCH_ON_RUN_TIME = 0.1 -LIGHT_CONE_NUM_SECTORS = 30 +NUM_LEVELS = 30 NUM_CONES = 50 # in first lighthouse scene NUM_VISIBLE_CONES = 5 # ibidem ARC_TIP_LENGTH = 0.2 - - -def show_line_length(line): - v = line.points[1] - line.points[0] - print v[0]**2 + v[1]**2 +AMBIENT_FULL = 1.0 +AMBIENT_DIMMED = 0.2 +LIGHT_COLOR = YELLOW +DEGREES = TAU/360 +SWITCH_ON_RUN_TIME = 1.5 class AngleUpdater(ContinualAnimation): @@ -66,7 +66,7 @@ class AngleUpdater(ContinualAnimation): def update_mobject(self, dt): # angle arc new_arc = self.angle_arc.copy().set_bound_angles( - start = self.lc.start_angle, + start = self.lc.start_angle(), stop = self.lc.stop_angle() ) new_arc.generate_points() @@ -77,171 +77,213 @@ class AngleUpdater(ContinualAnimation): -class LightScreen(VMobject): - # A light screen is composed of a VMobject and a light cone. - # It has knowledge of the light source point. - # As the screen changes, it calculates the viewing angle from - # the source and updates the light cone. - def __init__(self, light_source = ORIGIN, screen = None, light_cone = None): - Mobject.__init__(self) - self.light_cone = light_cone - self.light_source = light_source - self.screen = screen - self.light_cone.move_source_to(self.light_source) - self.shadow = VMobject(fill_color = BLACK, stroke_width = 0, fill_opacity = 1.0) - self.add(self.light_cone, self.screen, self.shadow) - self.update_shadow(self.shadow) - def update_light_cone(self,lc): - lower_angle, upper_angle = self.viewing_angles() - self.light_cone.update_opening(start_angle = lower_angle, - stop_angle = upper_angle) - return self + + + + + + + + + + + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0 + } + + def generate_points(self): + + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + for submob in self.submobjects: + self.remove(submob) + + # create annuli + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha + ) + annulus.move_arc_center_to(self.source_point) + self.add(annulus) + + + + def move_source_to(self,point): + self.shift(point - self.source_point) + self.source_point = np.array(point) + # for submob in self.submobjects: + # if type(submob) == Annulus: + # submob.shift(self.source_point - submob.get_center()) + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + +class Spotlight(VMobject): + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r/2+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0, + "screen" : None, + "shadow" : VMobject(fill_color = BLACK, stroke_width = 0, fill_opacity = 1.0) + } + + def track_screen(self): + self.generate_points() + + def generate_points(self): + + for submob in self.submobjects: + self.remove(submob) + + if self.screen != None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + self.radius = float(self.radius) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha, + start_angle = lower_angle, + angle = upper_angle - lower_angle + ) + annular_sector.move_arc_center_to(self.source_point) + self.add(annular_sector) + + self.update_shadow(point = self.source_point) + self.add(self.shadow) + def viewing_angle_of_point(self,point): - distance_vector = point - self.light_source + distance_vector = point - self.source_point angle = angle_of_vector(distance_vector) return angle - def viewing_angles(self): - all_points = [] + def viewing_angles(self,screen): - for submob in self.family_members_with_points(): - all_points.extend(submob.get_anchors()) - - viewing_angles = np.array(map(self.viewing_angle_of_point, self.screen.get_anchors())) - - if len(viewing_angles) == 0: - lower_angle = upper_angle = 0 - else: + viewing_angles = np.array(map(self.viewing_angle_of_point, + screen.get_anchors())) + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: lower_angle = np.min(viewing_angles) upper_angle = np.max(viewing_angles) - + return lower_angle, upper_angle - def update_shadow(self,sh): + def opening_angle(self): + l,u = self.viewing_angles(self.screen) + return u - l + + def start_angle(self): + l,u = self.viewing_angles(self.screen) + return l + + def stop_angle(self): + l,u = self.viewing_angles(self.screen) + return u + + def move_source_to(self,point): + self.source_point = np.array(point) + self.recalculate_sectors(point = point, screen = self.screen) + self.update_shadow(point = point) + + + def recalculate_sectors(self, point = ORIGIN, screen = None): + if screen == None: + return + for submob in self.submobject_family(): + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(screen) + new_submob = AnnularSector( + start_angle = lower_angle, + angle = upper_angle - lower_angle, + inner_radius = submob.inner_radius, + outer_radius = submob.outer_radius + ) + new_submob.move_arc_center_to(point) + submob.points = new_submob.points + + def update_shadow(self,point = ORIGIN): + use_point = point #self.source_point self.shadow.points = self.screen.points - ray1 = self.screen.points[0] - self.light_source - ray2 = self.screen.points[-1] - self.light_source + ray1 = self.screen.points[0] - use_point + ray2 = self.screen.points[-1] - use_point ray1 = ray1/np.linalg.norm(ray1) * 100 - ray1 = rotate_vector(ray1,-TAU/16) + ray1 = rotate_vector(ray1,-TAU/100) ray2 = ray2/np.linalg.norm(ray2) * 100 - ray2 = rotate_vector(ray2,TAU/16) + ray2 = rotate_vector(ray2,TAU/100) outpoint1 = self.screen.points[0] + ray1 outpoint2 = self.screen.points[-1] + ray2 self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]]) self.shadow.mark_paths_closed = True -class LightCone(VGroup): - CONFIG = { - "start_angle": 0, - "angle" : TAU/8, - "radius" : 10, - "brightness" : 1, - "opacity_function" : lambda r : 1./max(r, 0.01), - "num_sectors" : 10, - "color": LIGHT_COLOR, - } + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) - def generate_points(self): - radii = np.linspace(0, self.radius, self.num_sectors+1) - sectors = [ - AnnularSector( - start_angle = self.start_angle, - angle = self.angle, - inner_radius = r1, - outer_radius = r2, - stroke_width = 0, - stroke_color = self.color, - fill_color = self.color, - fill_opacity = self.brightness * self.opacity_function(r1), - ) - for r1, r2 in zip(radii, radii[1:]) - ] - self.add(*sectors) + def change_opacity_function(self,new_f): + self.radius = 120 + self.opacity_function = new_f + dr = self.radius/self.num_levels - def get_source_point(self): - if len(self.submobjects) == 0: - return None - source = self.submobjects[0].get_arc_center() - return source - - def move_source_to(self,point): - if len(self.submobjects) == 0: - return - source = self.submobjects[0].get_arc_center() - self.shift(point - source) - - def update_opening(self, start_angle, stop_angle): - self.start_angle = start_angle - self.angle = stop_angle - start_angle - source_point = self.get_source_point() + sectors = [] for submob in self.submobjects: if type(submob) == AnnularSector: + sectors.append(submob) - submob.start_angle = self.start_angle - submob.angle = self.angle - submob.generate_points() - submob.shift(source_point - submob.get_arc_center()) + print self.num_levels, len(sectors) + for (r,submob) in zip(np.arange(0,self.radius,dr),sectors): + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + alpha = self.opacity_function(r) + submob.set_fill(opacity = alpha) - def set_brightness(self,new_brightness): - self.brightness = new_brightness - radii = np.linspace(0, self.radius, self.num_sectors+1) - for (r1,sector) in zip(radii,self.submobjects): - sector.set_fill(opacity = self.brightness * self.opacity_function(r1)) - - def stop_angle(self): - return self.start_angle + self.angle - - - - - - - -class Candle(VGroup): - CONFIG = { - "radius" : 5, - "brightness" : 1.0, - "opacity_function" : lambda r : 1./max(r, 0.01), - "num_annuli" : 10, - "color": LIGHT_COLOR, - } - - def generate_points(self): - radii = np.linspace(0, self.radius, self.num_annuli+1) - annuli = [ - Annulus( - inner_radius = r1, - outer_radius = r2, - stroke_width = 0, - stroke_color = self.color, - fill_color = self.color, - fill_opacity = self.brightness * self.opacity_function(r1), - ) - for r1, r2 in zip(radii, radii[1:]) - ] - self.add(*annuli) - - def get_source_point(self): - if len(self.submobjects) == 0: - return None - source = self.submobjects[0].get_center() - return source - - def move_source_to(self,point): - if len(self.submobjects) == 0: - return - source = self.submobjects[0].get_center() - self.shift(point - source) - - def set_brightness(self,new_brightness): - self.brightness = new_brightness - radii = np.linspace(0, self.radius, self.num_annuli+1) - for (r1,annulus) in zip(radii,self.submobjects): - annulus.set_fill(opacity = self.brightness * self.opacity_function(r1)) @@ -253,11 +295,42 @@ class SwitchOn(LaggedStart): } def __init__(self, light, **kwargs): - if not isinstance(light,LightCone) and not isinstance(light,Candle): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): raise Exception("Only LightCones and Candles can be switched on") LaggedStart.__init__(self, FadeIn, light, **kwargs) + +class SwitchOff(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + + + + + +class ScreenTracker(ContinualAnimation): + def __init__(self, mobject, **kwargs): + ContinualAnimation.__init__(self, mobject, **kwargs) + + def update_mobject(self, dt): + self.mobject.recalculate_sectors( + point = self.mobject.source_point, + screen = self.mobject.screen) + self.mobject.update_shadow(self.mobject.source_point) + + + class LightHouse(SVGMobject): CONFIG = { "file_name" : "lighthouse", @@ -268,7 +341,8 @@ class LightIndicator(Mobject): CONFIG = { "radius": 0.5, "intensity": 0, - "opacity_for_unit_intensity": 1 + "opacity_for_unit_intensity": 1, + "precision": 3 } def generate_points(self): @@ -278,7 +352,7 @@ class LightIndicator(Mobject): self.foreground.set_stroke(color=INDICATOR_STROKE_COLOR,width=INDICATOR_STROKE_WIDTH) self.add(self.background, self.foreground) - self.reading = DecimalNumber(self.intensity,num_decimal_points = 3) + self.reading = DecimalNumber(self.intensity,num_decimal_points = self.precision) self.reading.set_fill(color=INDICATOR_TEXT_COLOR) self.reading.move_to(self.get_center()) self.add(self.reading) @@ -306,6 +380,28 @@ class UpdateLightIndicator(AnimationGroup): self.mobject = indicator + + + + + + + + + + + + + + + + + + + + + + class IntroScene(PiCreatureScene): CONFIG = { @@ -319,16 +415,10 @@ class IntroScene(PiCreatureScene): randy = self.get_primary_pi_creature() randy.scale(0.7).to_corner(DOWN+RIGHT) - self.force_skipping() - self.build_up_euler_sum() self.build_up_sum_on_number_line() self.show_pi_answer() self.other_pi_formulas() - - self.revert_to_original_skipping_status() - - self.refocus_on_euler_sum() @@ -526,20 +616,41 @@ class IntroScene(PiCreatureScene): ScaleInPlace(pi_squared,2,rate_func = wiggle) ) - q_circle = Circle(color=WHITE,radius=0.8) - q_mark = TexMobject("?") - q_mark.next_to(q_circle) - thought = Group(q_circle, q_mark) - q_mark.height *= 2 - self.pi_creature_thinks(thought,target_mode = "confused", - bubble_kwargs = { "height" : 1.5, "width" : 2 }) - self.wait() + # Morty thinks of a circle + + # q_circle = Circle(color=WHITE,radius=0.8) + # q_mark = TexMobject("?") + # q_mark.next_to(q_circle) + + # thought = Group(q_circle, q_mark) + # q_mark.height *= 2 + # self.pi_creature_thinks(thought,target_mode = "confused", + # bubble_kwargs = { "height" : 1.5, "width" : 2 }) + + # self.wait() -class FirstLightHouseScene(PiCreatureScene): + + + + + + + + + + + + + + + + + +class FirstLighthouseScene(PiCreatureScene): def construct(self): self.remove(self.get_primary_pi_creature()) @@ -598,7 +709,7 @@ class FirstLightHouseScene(PiCreatureScene): lighthouses = [] lighthouse_pos = [] - light_cones = [] + ambient_lights = [] euler_sum_above = TexMobject("1", "+", "{1\over 4}", @@ -617,15 +728,15 @@ class FirstLightHouseScene(PiCreatureScene): for i in range(1,NUM_CONES+1): lighthouse = LightHouse() point = self.number_line.number_to_point(i) - light_cone = Candle( - opacity_function = inverse_quadratic(1,1), - num_annuli = LIGHT_CONE_NUM_SECTORS, - radius = 12) + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(1,2,1), + num_levels = NUM_LEVELS, + radius = 12.0) - light_cone.move_source_to(point) + ambient_light.move_source_to(point) lighthouse.next_to(point,DOWN,0) lighthouses.append(lighthouse) - light_cones.append(light_cone) + ambient_lights.append(ambient_light) lighthouse_pos.append(point) @@ -641,13 +752,13 @@ class FirstLightHouseScene(PiCreatureScene): # slowly switch on visible light cones and increment indicator - for (i,lc) in zip(range(NUM_VISIBLE_CONES),light_cones[:NUM_VISIBLE_CONES]): - indicator_start_time = 0.4 * (i+1) * SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size + for (i,ambient_light) in zip(range(NUM_VISIBLE_CONES),ambient_lights[:NUM_VISIBLE_CONES]): + indicator_start_time = 0.4 * (i+1) * SWITCH_ON_RUN_TIME/ambient_light.radius * self.number_line.unit_size indicator_stop_time = indicator_start_time + INDICATOR_UPDATE_TIME indicator_rate_func = squish_rate_func( smooth,indicator_start_time,indicator_stop_time) self.play( - SwitchOn(lc), + SwitchOn(ambient_light), FadeIn(euler_sum_above[2*i], run_time = SWITCH_ON_RUN_TIME, rate_func = indicator_rate_func), FadeIn(euler_sum_above[2*i - 1], run_time = SWITCH_ON_RUN_TIME, @@ -667,13 +778,13 @@ class FirstLightHouseScene(PiCreatureScene): ) # quickly switch on off-screen light cones and increment indicator - for (i,lc) in zip(range(NUM_VISIBLE_CONES,NUM_CONES),light_cones[NUM_VISIBLE_CONES:NUM_CONES]): - indicator_start_time = 0.5 * (i+1) * FAST_SWITCH_ON_RUN_TIME/lc.radius * self.number_line.unit_size + for (i,ambient_light) in zip(range(NUM_VISIBLE_CONES,NUM_CONES),ambient_lights[NUM_VISIBLE_CONES:NUM_CONES]): + indicator_start_time = 0.5 * (i+1) * FAST_SWITCH_ON_RUN_TIME/ambient_light.radius * self.number_line.unit_size indicator_stop_time = indicator_start_time + FAST_INDICATOR_UPDATE_TIME indicator_rate_func = squish_rate_func(#smooth, 0.8, 0.9) smooth,indicator_start_time,indicator_stop_time) self.play( - SwitchOn(lc, run_time = FAST_SWITCH_ON_RUN_TIME), + SwitchOn(ambient_light, run_time = FAST_SWITCH_ON_RUN_TIME), ChangeDecimalToValue(light_indicator.reading,intensities[i], rate_func = indicator_rate_func, run_time = FAST_SWITCH_ON_RUN_TIME), ApplyMethod(light_indicator.foreground.set_fill,None,opacities[i]) @@ -702,128 +813,259 @@ class FirstLightHouseScene(PiCreatureScene): -class SingleLightHouseScene(PiCreatureScene): + + + + + + + + + + + + + + + + + + +class SingleLighthouseScene(PiCreatureScene): def construct(self): - self.create_light_source_and_creature() + self.setup_elements() + self.setup_trackers() # spotlight and angle msmt change when screen rotates + self.rotate_screen() - def create_light_source_and_creature(self): + def setup_elements(self): SCREEN_SIZE = 3.0 DISTANCE_FROM_LIGHTHOUSE = 10.0 source_point = [-DISTANCE_FROM_LIGHTHOUSE/2,0,0] observer_point = [DISTANCE_FROM_LIGHTHOUSE/2,0,0] + # Lighthouse + lighthouse = LightHouse() - candle = Candle( - opacity_function = inverse_quadratic(1,1), - num_annuli = LIGHT_CONE_NUM_SECTORS, + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(AMBIENT_FULL,2,1), + num_levels = NUM_LEVELS, radius = 10, brightness = 1, ) lighthouse.scale(2).next_to(source_point, DOWN, buff = 0) - candle.move_to(source_point) + ambient_light.move_to(source_point) + + # Pi Creature + morty = self.get_primary_pi_creature() morty.scale(0.5) morty.move_to(observer_point) self.add(lighthouse) self.play( - SwitchOn(candle) + SwitchOn(ambient_light) ) - light_cone = LightCone( - opacity_function = inverse_quadratic(1,1), - num_sectors = LIGHT_CONE_NUM_SECTORS, + # Screen + + self.screen = Line([0,-1,0],[0,1,0]) + self.screen.rotate(-TAU/6) + self.screen.next_to(morty, LEFT, buff = 1) + + # Spotlight + + self.spotlight = Spotlight( + opacity_function = inverse_quadratic(1,2,1), + num_levels = NUM_LEVELS, radius = 10, brightness = 5, + screen = self.screen ) - light_cone.move_source_to(source_point) - screen = Line([0,-1,0],[0,1,0]) - show_line_length(screen) + self.spotlight.move_source_to(source_point) - screen.rotate_in_place(-TAU/6) - show_line_length(screen) - screen.next_to(morty, LEFT, buff = 1) - - light_screen = LightScreen(light_source = source_point, - screen = screen, light_cone = light_cone) - light_screen.screen.color = WHITE - light_screen.screen.fill_opacity = 1 - light_screen.update_light_cone(light_cone) + # Animations + self.play( - FadeIn(light_screen, run_time = 2), - # dim the light that misses the screen - ApplyMethod(candle.set_brightness,0.3), - ApplyMethod(light_screen.update_shadow,light_screen.shadow), - FadeIn(light_cone), + ApplyMethod(ambient_light.dimming,AMBIENT_DIMMED), + FadeIn(self.spotlight) ) + self.add(self.spotlight.shadow) - lc_updater = lambda lc: light_screen.update_light_cone(lc) - sh_updater = lambda sh: light_screen.update_shadow(sh) - - ca1 = ContinualUpdateFromFunc(light_screen.light_cone, - lc_updater) - ca2 = ContinualUpdateFromFunc(light_screen.shadow, - sh_updater) - - self.add(ca1, ca2) self.add_foreground_mobject(morty) - pointing_screen_at_source = ApplyMethod(screen.rotate_in_place,TAU/6) + + + + def setup_trackers(self): + + # Make spotlight follow the screen + + screen_tracker = ScreenTracker(self.spotlight) + + self.add(screen_tracker) + pointing_screen_at_source = Rotate(self.screen,TAU/6) self.play(pointing_screen_at_source) - arc_angle = light_cone.angle + + # angle msmt (arc) + + arc_angle = self.spotlight.opening_angle() # draw arc arrows to show the opening angle - angle_arc = Arc(radius = 5, start_angle = light_cone.start_angle, - angle = light_cone.angle, tip_length = ARC_TIP_LENGTH) + angle_arc = Arc(radius = 5, start_angle = self.spotlight.start_angle(), + angle = self.spotlight.opening_angle(), tip_length = ARC_TIP_LENGTH) #angle_arc.add_tip(at_start = True, at_end = True) - angle_arc.move_arc_center_to(source_point) + angle_arc.move_arc_center_to(self.spotlight.source_point) self.add(angle_arc) + # angle msmt (decimal number) + angle_indicator = DecimalNumber(arc_angle/TAU*360, num_decimal_points = 0, unit = "^\\circ") angle_indicator.next_to(angle_arc,RIGHT) self.add_foreground_mobject(angle_indicator) - angle_update_func = lambda x: light_cone.angle/TAU*360 - ca3 = ContinualChangingDecimal(angle_indicator,angle_update_func) - self.add(ca3) + angle_update_func = lambda x: self.spotlight.opening_angle()/TAU * 360 + ca1 = ContinualChangingDecimal(angle_indicator,angle_update_func) + self.add(ca1) - #ca4 = ContinualUpdateFromFunc(angle_arc,update_angle_arc) - ca4 = AngleUpdater(angle_arc, light_screen.light_cone) - self.add(ca4) + ca2 = AngleUpdater(angle_arc, self.spotlight) + self.add(ca2) + + + def rotate_screen(self): + + # rotating_screen_1 = Rotate(self.screen, + # TAU/8, run_time=1.5) + # #self.wait(2) + # rotating_screen_2 = Rotate(self.screen, + # -TAU/4, run_time=3) + # #self.wait(2) + # rotating_screen_3 = Rotate(self.screen, + # TAU/8, run_time=1.5) + + # self.play(rotating_screen_1) + # self.play(rotating_screen_2) + # self.play(rotating_screen_3) + + rotating_screen_1 = Rotate(self.screen, TAU/8, rate_func = there_and_back) + rotating_screen_2 = Rotate(self.screen, -TAU/8, rate_func = there_and_back) + self.play(rotating_screen_1) + self.play(rotating_screen_2) - rotating_screen = ApplyMethod(light_screen.screen.rotate_in_place, TAU/6, run_time=3, rate_func = wiggle) - self.play(rotating_screen) - #rotating_screen_back = ApplyMethod(light_screen.screen.rotate_in_place, -TAU/6) #, run_time=3, rate_func = wiggle) - #self.play(rotating_screen_back) - self.wait() + #self.wait() + + +### The following is supposed to morph the scene into the Earth scene, +### but it doesn't work + + + # # morph into Earth scene + + # earth = Circle(radius = 3) + # earth.move_to([2,0,0]) + # sun_position = [-100,0,0] + # #self.add(screen_tracker) + # print "tuet" + # self.remove(screen_tracker) + # new_opacity_function = lambda r: 0.5 + # self.play( + # ApplyMethod(lighthouse.move_to,sun_position), + # ApplyMethod(ambient_light.move_to,sun_position), + # ApplyMethod(spotlight.move_source_to,sun_position), + + # ) + # self.play( + # ApplyMethod(spotlight.change_opacity_function,new_opacity_function)) + + # self.add(screen_tracker) - # morph into Earth scene - globe = Circle(radius = 3) - globe.move_to([2,0,0]) - sun_position = [-100,0,0] + + + + + + + + + + + + + + + + + + +class EarthScene(Scene): + + def construct(self): + + radius = 2.5 + center_x = 3 + theta0 = 70 * DEGREES + dtheta = 10 * DEGREES + theta1 = theta0 + dtheta + + + # screen + + screen = Line([center_x - radius * np.cos(theta0),radius * np.sin(theta0),0], + [center_x - radius * np.cos(theta1),radius * np.sin(theta1),0]) + screen.set_stroke(color = RED, width = 5) + + # Earth + + earth = Circle(radius = radius, stroke_width = 0) + earth.move_to([center_x,0,0]) + foreground_earth = earth.copy() # above the shadow + foreground_earth.radius -= 0.2 + foreground_earth.set_stroke(color = WHITE, width = 1) + self.add_foreground_mobject(foreground_earth) + earth.add(screen) + + # Morty + + morty = Mortimer().scale(0.3).next_to(screen, RIGHT, buff = 0.5) + self.add_foreground_mobject(morty) + + + # Light source (far-away Sun) + + sun = Spotlight( + opacity_function = lambda r : 0.5, + num_levels = NUM_LEVELS, + radius = 1100, + brightness = 5, + screen = screen + ) + + sun.move_source_to([-1000,0,0]) + + # Add elements to scene + + self.add(earth,sun,screen) + screen_tracker = ScreenTracker(sun) + self.add(screen_tracker) + + + # move screen to equator + self.play( - ApplyMethod(lighthouse.move_to,sun_position), - ApplyMethod(candle.move_to,sun_position), - ApplyMethod(light_cone.move_source_to,sun_position), - FadeOut(angle_arc), - FadeOut(angle_indicator), - FadeIn(globe), - ApplyMethod(light_screen.move_to,[0,0,0]), - ApplyMethod(morty.move_to,[1,0,0]) - + Rotate(earth,theta0 + dtheta/2,run_time = 3), + ApplyMethod(morty.move_to,[1.5,0,0], run_time = 3), ) @@ -849,6 +1091,281 @@ class SingleLightHouseScene(PiCreatureScene): + +class ScreenShapingScene(Scene): + + def construct(self): + + self.setup_elements() + self.deform_screen() + self.create_brightness_rect() + self.slant_screen() + self.unslant_screen() + self.left_shift_screen_while_showing_light_indicator() + self.add_distance_arrow() + self.right_shift_screen_while_showing_light_indicator_and_distance_arrow() + self.left_shift_again() + self.morph_into_faux_3d() + + + def setup_elements(self): + + self.screen_height = 1.0 + self.brightness_rect_height = 1.0 + + # screen + self.screen = Line([3,-self.screen_height/2,0],[3,self.screen_height/2,0], + path_arc = 0, num_arc_anchors = 10) + + # spotlight + self.spotlight = Spotlight( + opacity_function = inverse_quadratic(1,5,1), + num_levels = NUM_LEVELS, + radius = 10, + brightness = 5, + screen = self.screen + ) + + self.spotlight.move_source_to([-5,0,0]) + screen_tracker = ScreenTracker(self.spotlight) + + # lighthouse + lighthouse = LightHouse() + lighthouse.scale(2).next_to(self.spotlight.source_point,DOWN,buff=0) + + # ambient light + ambient_light = AmbientLight( + opacity_function = inverse_quadratic(AMBIENT_DIMMED,1,1), + num_levels = NUM_LEVELS, + radius = 10, + brightness = 1, + ) + ambient_light.move_source_to(self.spotlight.source_point) + + # Morty + self.morty = Mortimer().scale(0.3).next_to(self.screen, RIGHT, buff = 0.5) + + # Add everything to the scene + self.add(lighthouse, ambient_light,self.spotlight,self.screen) + self.add_foreground_mobject(self.morty) + self.add(screen_tracker) + + + + + def deform_screen(self): + + self.wait() + + self.play(ApplyMethod(self.screen.set_path_arc, 45 * DEGREES)) + self.play(ApplyMethod(self.screen.set_path_arc, -90 * DEGREES)) + self.play(ApplyMethod(self.screen.set_path_arc, 0)) + + + + + def create_brightness_rect(self): + + # in preparation for the slanting, create a rectangle that shows the brightness + + # a rect a zero width overlaying the screen + # so we can morph it into the brightness rect above + brightness_rect0 = Rectangle(width = 0, + height = self.screen_height).move_to(self.screen.get_center()) + self.add_foreground_mobject(brightness_rect0) + + self.brightness_rect = Rectangle(width = self.brightness_rect_height, + height = self.brightness_rect_height, fill_color = YELLOW, fill_opacity = 0.5) + + self.brightness_rect.next_to(self.screen, UP, buff = 1) + + self.play( + ReplacementTransform(brightness_rect0,self.brightness_rect) + ) + + self.original_screen = self.screen.copy() + self.original_brightness_rect = self.brightness_rect.copy() + # for unslanting the screen later + + + def slant_screen(self): + + lower_screen_point, upper_screen_point = self.screen.get_start_and_end() + + lower_slanted_screen_point = interpolate( + lower_screen_point, self.spotlight.source_point, 0.2 + ) + upper_slanted_screen_point = interpolate( + upper_screen_point, self.spotlight.source_point, -0.2 + ) + + self.slanted_brightness_rect = self.brightness_rect.copy() + self.slanted_brightness_rect.width *= 2 + self.slanted_brightness_rect.generate_points() + self.slanted_brightness_rect.set_fill(opacity = 0.25) + + self.slanted_screen = Line(lower_slanted_screen_point,upper_slanted_screen_point, + path_arc = 0, num_arc_anchors = 10) + self.slanted_brightness_rect.move_to(self.brightness_rect.get_center()) + + self.play( + ReplacementTransform(self.screen,self.slanted_screen), + ReplacementTransform(self.brightness_rect,self.slanted_brightness_rect), + ) + + + + def unslant_screen(self): + + self.wait() + + + self.remove(self.slanted_screen) + self.remove(self.slanted_brightness_rect) + + + self.play( + ReplacementTransform(self.screen,self.original_screen), + ReplacementTransform(self.slanted_brightness_rect,self.original_brightness_rect), + ) + + + #self.remove(self.original_brightness_rect) + + + def left_shift_screen_while_showing_light_indicator(self): + + # Scene 5: constant screen size, changing opening angle + + # let's use an actual light indicator instead of just rects + + self.indicator_intensity = 0.25 + indicator_height = 1.25 * self.screen_height + + # indicator0 = Ellipse(width = 0, height = screen_height).move_to(self.screen.get_center()) + # indicator0.set_fill(opacity = 0) + # self.add_foreground_mobject(indicator0) + + self.indicator = LightIndicator(radius = indicator_height/2, + opacity_for_unit_intensity = OPACITY_FOR_UNIT_INTENSITY, + color = LIGHT_COLOR, + precision = 2) + self.indicator.set_intensity(self.indicator_intensity) + + + + self.indicator.move_to(self.original_brightness_rect.get_center()) + + + self.play( + FadeOut(self.original_brightness_rect), + FadeIn(self.indicator) + ) + + self.add_foreground_mobject(self.indicator.reading) + + self.unit_indicator_intensity = 1.0 + + self.left_shift = (self.screen.get_center()[0] - self.spotlight.source_point[0])/2 + + self.play( + ApplyMethod(self.screen.shift,[-self.left_shift,0,0]), + ApplyMethod(self.morty.shift,[-self.left_shift,0,0]), + #ApplyMethod(self.indicator.shift,[-self.left_shift,0,0]), + ApplyMethod(self.indicator.set_intensity,self.unit_indicator_intensity), + ) + + self.remove(self.original_screen) # was still hiding behind the shadow + + + + def add_distance_arrow(self): + + # distance arrow (length 1) + left_x = self.spotlight.source_point[0] + right_x = self.screen.get_center()[0] + arrow_y = -2 + arrow1 = Arrow([left_x,arrow_y,0],[right_x,arrow_y,0]) + arrow2 = Arrow([right_x,arrow_y,0],[left_x,arrow_y,0]) + arrow1.set_fill(color = WHITE) + arrow2.set_fill(color = WHITE) + distance_decimal = Integer(1).next_to(arrow1,DOWN) + self.arrow = VGroup(arrow1, arrow2,distance_decimal) + self.add(self.arrow) + + + # distance arrow (length 2) + # will be morphed into + self.distance_to_source = right_x - left_x + new_right_x = left_x + 2 * self.distance_to_source + new_arrow1 = Arrow([left_x,arrow_y,0],[new_right_x,arrow_y,0]) + new_arrow2 = Arrow([new_right_x,arrow_y,0],[left_x,arrow_y,0]) + new_arrow1.set_fill(color = WHITE) + new_arrow2.set_fill(color = WHITE) + new_distance_decimal = Integer(2).next_to(new_arrow1,DOWN) + self.new_arrow = VGroup(new_arrow1, new_arrow2, new_distance_decimal) + # don't add it yet + + + def right_shift_screen_while_showing_light_indicator_and_distance_arrow(self): + + self.wait() + + self.play( + ReplacementTransform(self.arrow,self.new_arrow), + ApplyMethod(self.screen.shift,[self.distance_to_source,0,0]), + #ApplyMethod(self.indicator.shift,[self.left_shift,0,0]), + + ApplyMethod(self.indicator.set_intensity,self.indicator_intensity), + # this should trigger ChangingDecimal, but it doesn't + # maybe bc it's an anim within an anim? + + ApplyMethod(self.morty.shift,[self.distance_to_source,0,0]), + ) + + + def left_shift_again(self): + + self.wait() + + self.play( + ReplacementTransform(self.new_arrow,self.arrow), + ApplyMethod(self.screen.shift,[-self.distance_to_source,0,0]), + #ApplyMethod(self.indicator.shift,[-self.left_shift,0,0]), + ApplyMethod(self.indicator.set_intensity,self.unit_indicator_intensity), + ApplyMethod(self.morty.shift,[-self.distance_to_source,0,0]), + ) + + def morph_into_faux_3d(self): + + p0_lower, p0_upper = self.screen.get_start_and_end() + + p01 = p0_lower + p02 = p0_lower + p03 = p0_upper + p04 = p0_upper + + screen3d0 = Polygon(p01,p02,p03,p04) + screen3d0.set_stroke(width = 1, color = WHITE) + + screen3d0_edge = VMobject() + screen3d0_edge.set_anchor_points([p03,p04,p01]) + + perspective_v = 0.5 * np.array([1,-1,0]) + p1 = p01 + 0.5 * perspective_v + p2 = p02 - 0.5 * perspective_v + p3 = p03 - 0.5 * perspective_v + p4 = p04 + 0.5 * perspective_v + + screen3d = Polygon(p1,p2,p3,p4) + screen3d.set_fill(color = WHITE, opacity = 0.5) + + screen3d_edge = VMobject() + screen3d_edge.set_anchor_points([p3,p4,p1]) + + self.spotlight.screen = screen3d0_edge + + self.play(Transform(self.spotlight.screen,screen3d_edge)) diff --git a/active_projects/fourier.py b/active_projects/fourier.py index fde2569c..429b5041 100644 --- a/active_projects/fourier.py +++ b/active_projects/fourier.py @@ -4190,15 +4190,11 @@ class FourierEndScreen(PatreonEndScreen): class Thumbnail(Scene): def construct(self): title = TextMobject("Fourier\\\\", "Visualized") - title[1].highlight(YELLOW) - title[1].set_stroke(RED, 1) title.highlight(YELLOW) - title.set_stroke(RED, 1) - + title.set_stroke(RED, 2) title.scale(2.5) title.add_background_rectangle() - # title.to_edge(UP) - # self.add(title) + def func(t): return np.cos(2*TAU*t) + np.cos(3*TAU*t) + np.cos(5*t) fourier = get_fourier_transform(func, -5, 5) @@ -4206,36 +4202,40 @@ class Thumbnail(Scene): graph = FunctionGraph(func, x_min = -5, x_max = 5) graph.highlight(BLUE) fourier_graph = FunctionGraph(fourier, x_min = 0, x_max = 6) - fourier_graph.highlight(RED_C) + fourier_graph.highlight(YELLOW) for g in graph, fourier_graph: g.stretch_to_fit_height(2) g.stretch_to_fit_width(10) g.set_stroke(width = 8) - arrow = Vector( - 2.5*DOWN, - rectangular_stem_width = 0.2, - tip_length = 0.5, - color = WHITE - ) - q_mark = TextMobject("?").scale(2) - q_mark.highlight(YELLOW) - q_mark.set_stroke(RED, 1) - arrows = VGroup(*it.chain(*zip( - [q_mark.copy() for x in range(5)], - [arrow.copy() for x in range(5)] - ))) - # arrows.submobjects.pop() - # arrows.arrange_submobjects(RIGHT, buff = MED_LARGE_BUFF) - group = VGroup(graph, title, fourier_graph) - arrows = VGroup(*[arrow.copy() for x in range(2)]) - arrows.arrange_submobjects(RIGHT, buff = 6*LARGE_BUFF) - # group = VGroup(graph, arrows, fourier_graph) - group.arrange_submobjects(DOWN) - # group.next_to(title, DOWN, MED_LARGE_BUFF) - arrows.move_to(title) - title.shift(MED_SMALL_BUFF*UP) - graph.shift(SMALL_BUFF*UP) - self.add(arrows, group) + + pol_graphs = VGroup() + for f in np.linspace(1.98, 2.02, 7): + pol_graph = ParametricFunction( + lambda t : complex_to_R3( + (2+np.cos(2*TAU*t)+np.cos(3*TAU*t))*np.exp(-complex(0, TAU*f*t)) + ), + t_min = -5, + t_max = 5, + num_graph_points = 200, + ) + pol_graph.match_color(graph) + pol_graph.scale_to_fit_height(2) + pol_graphs.add(pol_graph) + pol_graphs.arrange_submobjects(RIGHT, buff = LARGE_BUFF) + pol_graphs.gradient_highlight(BLUE_C, YELLOW) + pol_graphs.match_width(graph) + pol_graphs.set_stroke(width = 2) + + + self.clear() + title.center().to_edge(UP) + pol_graphs.scale_to_fit_width(2*SPACE_WIDTH - 1) + pol_graphs.center() + title.move_to(pol_graphs) + title.shift(SMALL_BUFF*LEFT) + graph.next_to(title, UP) + fourier_graph.next_to(title, DOWN) + self.add(pol_graphs, title, graph, fourier_graph) diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 411520c5..f50d47c6 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -408,9 +408,8 @@ class Succession(Animation): run_time = kwargs.pop("run_time") else: run_time = sum(self.run_times) - self.num_anims = len(animations) + self.num_anims = len(animations) #TODO: If this is zero, some special handling below self.animations = animations - self.last_index = 0 #Have to keep track of this run_time, because Scene.play #might very well mess with it. self.original_run_time = run_time @@ -420,29 +419,52 @@ class Succession(Animation): 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) + # 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] + self.scene_mobjects_at_time = [None for i in range(self.num_anims + 1)] + self.scene_mobjects_at_time[0] = Group() + for i in range(self.num_anims): + self.scene_mobjects_at_time[i + 1] = self.scene_mobjects_at_time[i].copy() + self.animations[i].clean_up(self.scene_mobjects_at_time[i + 1]) - def rewind_to_start(self): - for anim in reversed(self.animations): - anim.update(0) + self.current_alpha = 0 + self.current_anim_index = 0 #TODO: What if self.num_anims == 0? + + self.mobject = Group() + self.jump_to_start_of_anim(0) + Animation.__init__(self, self.mobject, run_time = run_time, **kwargs) + + def jump_to_start_of_anim(self, index): + self.current_anim_index = index + self.current_alpha = self.critical_alphas[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.animations[index].update(0) def update_mobject(self, alpha): - self.rewind_to_start() + i = 0 + while self.critical_alphas[i + 1] < alpha: + i = i + 1 + # TODO: Special handling if alpha < 0 or alpha > 1, to use + # first or last sub-animation - 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 + # At this point, we should have self.critical_alphas[i] <= alpha <= self.critical_alphas[i +1] - sub_alpha = clamp(0, 1, sub_alpha) # Could possibly adopt a non-clamping convention here - self.animations[i].update(sub_alpha) + self.jump_to_start_of_anim(i) + sub_alpha = inverse_interpolate( + self.critical_alphas[i], + self.critical_alphas[i + 1], + alpha + ) + self.animations[i].update(sub_alpha) def clean_up(self, *args, **kwargs): + # We clean up as though we've played ALL animations, even if + # clean_up is called in middle of things for anim in self.animations: anim.clean_up(*args, **kwargs) @@ -461,8 +483,6 @@ class AnimationGroup(Animation): for anim in self.sub_anims: anim.update(alpha) - def clean_up(self, surrounding_scene = None): - if surrounding_scene is not None: - surrounding_scene.mobjects.remove(self.everything) + def clean_up(self, *args, **kwargs): for anim in self.sub_anims: - anim.clean_up(surrounding_scene = surrounding_scene) + anim.clean_up(*args, **kwargs) diff --git a/ben_playground.py b/ben_playground.py new file mode 100644 index 00000000..c03c8abe --- /dev/null +++ b/ben_playground.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python + +from helpers import * + +from mobject.tex_mobject import TexMobject +from mobject import Mobject +from mobject.image_mobject import ImageMobject +from mobject.vectorized_mobject import * +from mobject.point_cloud_mobject import PointCloudDot + +from animation.animation import Animation +from animation.transform import * +from animation.simple_animations import * +from animation.continual_animation import * +from animation.playground import * + +from topics.geometry import * +from topics.characters import * +from topics.functions import * +from topics.number_line import * +from topics.combinatorics import * +from scene import Scene +from camera import Camera +from mobject.svg_mobject import * +from mobject.tex_mobject import * + +from mobject.vectorized_mobject import * + +## To watch one of these scenes, run the following: +## python extract_scene.py -p file_name + +LIGHT_COLOR = YELLOW +DEGREES = 360/TAU +SWITCH_ON_RUN_TIME = 1.5 + + +class AmbientLight(VMobject): + + # Parameters are: + # * a source point + # * an opacity function + # * a light color + # * a max opacity + # * a radius (larger than the opacity's dropoff length) + # * the number of subdivisions (levels, annuli) + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0 + } + + def generate_points(self): + + # in theory, this method is only called once, right? + # so removing submobs shd not be necessary + for submob in self.submobjects: + self.remove(submob) + + # create annuli + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annulus = Annulus( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha + ) + annulus.move_arc_center_to(self.source_point) + self.add(annulus) + + + + def move_source_to(self,point): + self.shift(point - self.source_point) + self.source_point = np.array(point) + # for submob in self.submobjects: + # if type(submob) == Annulus: + # submob.shift(self.source_point - submob.get_center()) + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + +class Spotlight(VMobject): + + CONFIG = { + "source_point" : ORIGIN, + "opacity_function" : lambda r : 1.0/(r+1.0)**2, + "color" : LIGHT_COLOR, + "max_opacity" : 1.0, + "num_levels" : 10, + "radius" : 5.0, + "screen" : None, + "shadow" : VMobject(fill_color = RED, stroke_width = 0, fill_opacity = 1.0) + } + + def track_screen(self): + self.generate_points() + + def generate_points(self): + + for submob in self.submobjects: + self.remove(submob) + + if self.screen != None: + # look for the screen and create annular sectors + lower_angle, upper_angle = self.viewing_angles(self.screen) + dr = self.radius / self.num_levels + for r in np.arange(0, self.radius, dr): + alpha = self.max_opacity * self.opacity_function(r) + annular_sector = AnnularSector( + inner_radius = r, + outer_radius = r + dr, + color = self.color, + fill_opacity = alpha, + start_angle = lower_angle, + angle = upper_angle - lower_angle + ) + annular_sector.move_arc_center_to(self.source_point) + self.add(annular_sector) + + self.update_shadow(point = self.source_point) + self.add(self.shadow) + + + def viewing_angle_of_point(self,point): + distance_vector = point - self.source_point + angle = angle_of_vector(distance_vector) + return angle + + def viewing_angles(self,screen): + + viewing_angles = np.array(map(self.viewing_angle_of_point, + screen.get_anchors())) + lower_angle = upper_angle = 0 + if len(viewing_angles) != 0: + lower_angle = np.min(viewing_angles) + upper_angle = np.max(viewing_angles) + + return lower_angle, upper_angle + + def move_source_to(self,point): + self.source_point = np.array(point) + self.recalculate_sectors(point = point, screen = self.screen) + self.update_shadow(point = point) + + def recalculate_sectors(self, point = ORIGIN, screen = None): + if screen == None: + return + for submob in self.submobject_family(): + if type(submob) == AnnularSector: + lower_angle, upper_angle = self.viewing_angles(screen) + new_submob = AnnularSector( + start_angle = lower_angle, + angle = upper_angle - lower_angle, + inner_radius = submob.inner_radius, + outer_radius = submob.outer_radius + ) + new_submob.move_arc_center_to(point) + submob.points = new_submob.points + + def update_shadow(self,point = ORIGIN): + print "updating shadow" + use_point = point #self.source_point + self.shadow.points = self.screen.points + ray1 = self.screen.points[0] - use_point + ray2 = self.screen.points[-1] - use_point + ray1 = ray1/np.linalg.norm(ray1) * 100 + ray1 = rotate_vector(ray1,-TAU/16) + ray2 = ray2/np.linalg.norm(ray2) * 100 + ray2 = rotate_vector(ray2,TAU/16) + outpoint1 = self.screen.points[0] + ray1 + outpoint2 = self.screen.points[-1] + ray2 + self.shadow.add_control_points([outpoint2,outpoint1,self.screen.points[0]]) + self.shadow.mark_paths_closed = True + + + def dimming(self,new_alpha): + old_alpha = self.max_opacity + self.max_opacity = new_alpha + for submob in self.submobjects: + if type(submob) != AnnularSector: + # it's the shadow, don't dim it + continue + old_submob_alpha = submob.fill_opacity + new_submob_alpha = old_submob_alpha * new_alpha/old_alpha + submob.set_fill(opacity = new_submob_alpha) + + + + +class SwitchOn(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + LaggedStart.__init__(self, + FadeIn, light, **kwargs) + + +class SwitchOff(LaggedStart): + CONFIG = { + "lag_ratio": 0.2, + "run_time": SWITCH_ON_RUN_TIME + } + + def __init__(self, light, **kwargs): + if not isinstance(light,AmbientLight) and not isinstance(light,Spotlight): + raise Exception("Only LightCones and Candles can be switched on") + light.submobjects = light.submobjects[::-1] + LaggedStart.__init__(self, + FadeOut, light, **kwargs) + light.submobjects = light.submobjects[::-1] + + + + + +class ScreenTracker(ContinualAnimation): + def __init__(self, mobject, **kwargs): + ContinualAnimation.__init__(self, mobject, **kwargs) + + def update_mobject(self, dt): + self.mobject.recalculate_sectors( + point = self.mobject.source_point, + screen = self.mobject.screen) + self.mobject.update_shadow(self.mobject.source_point) + + + +class IntroScene(Scene): + def construct(self): + + screen = Line([2,-2,0],[1,2,0]).shift([1,0,0]) + self.add(screen) + + ambient_light = AmbientLight( + source_point = np.array([-1,1,0]), + max_opacity = 1.0, + opacity_function = lambda r: 1.0/(r/2+1)**2, + num_levels = 4, + ) + + spotlight = Spotlight( + source_point = np.array([-1,1,0]), + max_opacity = 1.0, + opacity_function = lambda r: 1.0/(r/2+1)**2, + num_levels = 4, + screen = screen, + ) + + self.add(spotlight) + + screen_updater = ScreenTracker(spotlight) + #self.add(ca) + + #self.play(SwitchOn(ambient_light)) + #self.play(ApplyMethod(ambient_light.move_source_to,[-3,1,0])) + #self.play(SwitchOn(spotlight)) + + self.add(screen_updater) + self.play(ApplyMethod(spotlight.screen.rotate,TAU/8)) + self.remove(screen_updater) + self.play(ApplyMethod(spotlight.move_source_to,[-3,-1,0])) + self.add(screen_updater) + spotlight.source_point = [-3,-1,0] + + self.play(ApplyMethod(spotlight.dimming,0.2)) + #self.play(ApplyMethod(spotlight.move_source_to,[-4,0,0])) + + #self.wait() + + + diff --git a/camera/camera.py b/camera/camera.py index f5a6f09a..15ff6400 100644 --- a/camera/camera.py +++ b/camera/camera.py @@ -438,11 +438,6 @@ class MappingCamera(Camera): excluded_mobjects = None, ) -# TODO: Put this in different utility/helpers file? Convenient for me (Sridhar); I like it. -class DictAsObject(object): - def __init__(self, dict): - self.__dict__ = dict - # Note: This allows layering of multiple cameras onto the same portion of the pixel array, # the later cameras overwriting the former # diff --git a/container/__init__.py b/container/__init__.py new file mode 100644 index 00000000..de599d96 --- /dev/null +++ b/container/__init__.py @@ -0,0 +1 @@ +from container import * diff --git a/container/container.py b/container/container.py new file mode 100644 index 00000000..2cb3325e --- /dev/null +++ b/container/container.py @@ -0,0 +1,21 @@ +from helpers import * + +# Currently, this is only used by both Scene and MOBject. +# Still, we abstract its functionality here, albeit purely nominally. +# All actual implementation has to be handled by derived classes for now. +# +# Note that although the prototypical instances add and remove MObjects, +# there is also the possibility to add ContinualAnimations to Scenes. Thus, +# in the Container class in general, we do not make any presumptions about +# what types of objects may be added; this is again dependent on the specific +# derived instance. + +class Container(object): + def __init__(self, *submobjects, **kwargs): + digest_config(self, kwargs) + + def add(self, *items): + raise Exception("Container.add is not implemented; it is up to derived classes to implement") + + def remove(self, *items): + raise Exception("Container.remove is not implemented; it is up to derived classes to implement") \ No newline at end of file diff --git a/helpers.py b/helpers.py index 85ad584e..58cfc7c8 100644 --- a/helpers.py +++ b/helpers.py @@ -629,6 +629,12 @@ def angle_of_vector(vector): return 0 return np.angle(complex(*vector[:2])) +def concatenate_lists(*list_of_lists): + return [item for l in list_of_lists for item in l] - +# Occasionally convenient in order to write dict.x instead of more laborious +# (and less in keeping with all other attr accesses) dict["x"] +class DictAsObject(object): + def __init__(self, dict): + self.__dict__ = dict diff --git a/mobject/mobject.py b/mobject/mobject.py index a6dfc7e4..53509d0e 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -7,10 +7,11 @@ from colour import Color from helpers import * +from container import * #TODO: Explain array_attrs -class Mobject(object): +class Mobject(Container): """ Mathematical Object """ @@ -22,7 +23,7 @@ class Mobject(object): "target" : None, } def __init__(self, *submobjects, **kwargs): - digest_config(self, kwargs) + Container.__init__(self, *submobjects, **kwargs) if not all(map(lambda m : isinstance(m, Mobject), submobjects)): raise Exception("All submobjects must be of type Mobject") self.submobjects = list(submobjects) diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index 439851f2..634b5f18 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -353,7 +353,8 @@ class VMobject(Mobject): for index in range(num_curves): curr_bezier_points = self.points[3*index:3*index+4] num_inter_curves = sum(index_allocation == index) - alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves) + alphas = np.linspace(0, 1, num_inter_curves+1) + # alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves) for a, b in zip(alphas, alphas[1:]): new_points = partial_bezier_points( curr_bezier_points, a, b diff --git a/old_projects/nn/network.py b/old_projects/nn/network.py index 3ec390da..2aa3aab4 100644 --- a/old_projects/nn/network.py +++ b/old_projects/nn/network.py @@ -29,6 +29,12 @@ IMAGE_MAP_DATA_FILE = os.path.join(NN_DIRECTORY, "image_map") # DEFAULT_LAYER_SIZES = [28**2, 80, 10] DEFAULT_LAYER_SIZES = [28**2, 16, 16, 10] +try: + xrange # Python 2 +except NameError: + xrange = range # Python 3 + + class Network(object): def __init__(self, sizes, non_linearity = "sigmoid"): """The list ``sizes`` contains the number of neurons in the diff --git a/old_projects/number_line_scene.py b/old_projects/number_line_scene.py index d81c1bcd..5054f455 100644 --- a/old_projects/number_line_scene.py +++ b/old_projects/number_line_scene.py @@ -1,6 +1,10 @@ import numpy as np -from animation.transform import Transform +from animation.transform import ApplyMethod, Transform +from constants import RIGHT, SPACE_WIDTH, UP +from helpers import counterclockwise_path, straight_path from point_cloud_mobject import Point +from scene import Scene +from topics.geometry import Line from topics.number_line import NumberLine class NumberLineScene(Scene): @@ -23,7 +27,7 @@ class NumberLineScene(Scene): number_at_center = number ) new_displayed_numbers = new_number_line.default_numbers_to_display() - new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers) + new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers) transforms = [] additional_mobjects = [] @@ -78,11 +82,3 @@ class NumberLineScene(Scene): ApplyMethod(mob.shift, (num-1)*mob.get_center()[0]*RIGHT, **kwargs) for mob in self.number_mobs ]) - - - - - - - - diff --git a/scene/scene.py b/scene/scene.py index 1f0afc72..9171a45d 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -20,8 +20,9 @@ from animation import Animation from animation.animation import sync_animation_run_times_and_rate_funcs from animation.transform import MoveToTarget from animation.continual_animation import ContinualAnimation +from container import * -class Scene(object): +class Scene(Container): CONFIG = { "camera_class" : Camera, "camera_config" : {}, @@ -41,7 +42,7 @@ class Scene(object): "skip_to_animation_number" : None, } def __init__(self, **kwargs): - digest_config(self, kwargs) + Container.__init__(self, **kwargs) # Perhaps allow passing in a non-empty *mobjects parameter? self.camera = self.camera_class(**self.camera_config) self.mobjects = [] self.continual_animations = [] diff --git a/topics/geometry.py b/topics/geometry.py index 3605d8b6..2312e74b 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -125,6 +125,19 @@ class Dot(Circle): self.shift(point) self.init_colors() +class Ellipse(VMobject): + CONFIG = { + "width" : 2, + "height" : 1 + } + + def generate_points(self): + circle = Circle(radius = 1) + circle = circle.stretch_to_fit_width(self.width) + circle = circle.stretch_to_fit_height(self.height) + self.points = circle.points + + class AnnularSector(VMobject): CONFIG = { "inner_radius" : 1, @@ -174,6 +187,7 @@ class AnnularSector(VMobject): self.shift(v) return self + class Sector(AnnularSector): CONFIG = { "outer_radius" : 1, @@ -200,10 +214,12 @@ class Annulus(Circle): } def generate_points(self): + self.points = [] self.radius = self.outer_radius - Circle.generate_points(self) + outer_circle = Circle(radius = self.outer_radius) inner_circle = Circle(radius=self.inner_radius) inner_circle.flip() + self.points = outer_circle.points self.add_subpath(inner_circle.points) class Line(VMobject): @@ -228,6 +244,10 @@ class Line(VMobject): ]) self.account_for_buff() + def set_path_arc(self,new_value): + self.path_arc = new_value + self.generate_points() + def account_for_buff(self): length = self.get_arc_length() if length < 2*self.buff or self.buff == 0: @@ -333,6 +353,17 @@ class Line(VMobject): self.shift(new_start - self.get_start()) return self + def insert_n_anchor_points(self, n): + if not self.path_arc: + n_anchors = self.get_num_anchor_points() + new_num_points = 3*(n_anchors + n)+1 + self.points = np.array([ + self.point_from_proportion(alpha) + for alpha in np.linspace(0, 1, new_num_points) + ]) + else: + VMobject.insert_n_anchor_points(self, n) + class DashedLine(Line): CONFIG = { "dashed_segment_length" : 0.05 diff --git a/topics/numerals.py b/topics/numerals.py index 44e28323..1bcc61fe 100644 --- a/topics/numerals.py +++ b/topics/numerals.py @@ -12,7 +12,7 @@ class DecimalNumber(VMobject): "num_decimal_points" : 2, "digit_to_digit_buff" : 0.05, "show_ellipsis" : False, - "unit" : None, + "unit" : None, #Aligned to bottom unless it starts with "^" "include_background_rectangle" : False, } def __init__(self, number, **kwargs): @@ -41,8 +41,17 @@ class DecimalNumber(VMobject): if self.show_ellipsis: self.add(TexMobject("\\dots")) - if self.unit is not None: - self.add(TexMobject(self.unit)) + + if num_string.startswith("-"): + minus = self.submobjects[0] + minus.next_to( + self.submobjects[1], LEFT, + buff = self.digit_to_digit_buff + ) + + if self.unit != None: + self.unit_sign = TexMobject(self.unit) + self.add(self.unit_sign) self.arrange_submobjects( buff = self.digit_to_digit_buff, @@ -54,8 +63,8 @@ class DecimalNumber(VMobject): for i, c in enumerate(num_string): if c == "-" and len(num_string) > i+1: self[i].align_to(self[i+1], alignment_vect = UP) - if self.unit == "\\circ": - self[-1].align_to(self, UP) + if self.unit and self.unit.startswith("^"): + self.unit_sign.align_to(self, UP) # if self.include_background_rectangle: self.add_background_rectangle() @@ -80,7 +89,7 @@ class ChangingDecimal(Animation): "num_decimal_points" : None, "show_ellipsis" : None, "position_update_func" : None, - "tracked_mobject" : None + "tracked_mobject" : None, } def __init__(self, decimal_number_mobject, number_update_func, **kwargs): digest_config(self, kwargs, locals())