From 08a4d22740fae036cfad5dd82ff425a5f23a2c81 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 7 Aug 2017 16:17:35 -0700 Subject: [PATCH] Added slider objects for highD.py --- highD.py | 556 +++++++++++++++++++++++++++++++++++++- stage_animations.py | 50 ++++ topics/complex_numbers.py | 4 +- topics/geometry.py | 2 +- topics/number_line.py | 31 ++- topics/probability.py | 2 - 6 files changed, 618 insertions(+), 27 deletions(-) create mode 100644 stage_animations.py diff --git a/highD.py b/highD.py index 6790ad28..2e0a0b44 100644 --- a/highD.py +++ b/highD.py @@ -29,8 +29,240 @@ from mobject.svg_mobject import * from mobject.tex_mobject import * ########## +#force_skipping #revert_to_original_skipping_status +########## + +class Slider(NumberLine): + CONFIG = { + "color" : WHITE, + "x_min" : -1, + "x_max" : 1, + "unit_size" : 2, + "center_value" : 0, + "number_scale_val" : 0.75, + "numbers_with_elongated_ticks" : [], + "line_to_number_vect" : LEFT, + "line_to_number_buff" : MED_LARGE_BUFF, + "dial_radius" : 0.1, + "dial_color" : YELLOW, + "include_real_estate_ticks" : True, + } + def __init__(self, **kwargs): + NumberLine.__init__(self, **kwargs) + self.rotate(np.pi/2) + self.init_dial() + if self.include_real_estate_ticks: + self.add_real_estate_ticks() + + def init_dial(self): + dial = Dot( + radius = self.dial_radius, + color = self.dial_color, + ) + dial.move_to(self.number_to_point(0)) + re_dial = dial.copy() + re_dial.set_fill(opacity = 0) + self.add(dial, re_dial) + + self.dial = dial + self.re_dial = re_dial + self.last_sign = -1 + + def add_real_estate_ticks( + self, + re_per_tick = 0.05, + colors = [BLUE, RED], + ): + self.real_estate_ticks = VGroup(*[ + self.get_tick(u*np.sqrt(x)) + for x in np.arange(re_per_tick, 1, re_per_tick) + for u in [-1, 1] + ]) + self.real_estate_ticks.set_stroke(width = 3) + self.real_estate_ticks.gradient_highlight(*colors) + self.add(self.real_estate_ticks) + self.add(self.dial) + return self.real_estate_ticks + + def set_value(self, x): + for dial, val in (self.dial, x), (self.re_dial, x**2): + dial.move_to(self.number_to_point(val)) + return self + + def change_real_estate(self, d_re): + left_over = 0 + curr_re = self.get_real_estate() + if d_re < -curr_re: + left_over = d_re + curr_re + d_re = -curr_re + self.set_real_estate(curr_re + d_re) + return left_over + + def set_real_estate(self, target_re): + if target_re < 0: + raise Exception("Cannot set real estate below 0") + self.re_dial.move_to(self.number_to_point(target_re)) + self.update_dial_by_re_dial() + return self + + def get_dial_supplement_animation(self): + return UpdateFromFunc(self.dial, self.update_dial_by_re_dial) + + def update_dial_by_re_dial(self, dial = None): + dial = dial or self.dial + re = self.get_real_estate() + sign = np.sign(self.get_value() - self.center_value) + if sign == 0: + sign = -self.last_sign + self.last_sign *= -1 + dial.move_to(self.number_to_point( + self.center_value + sign*np.sqrt(re) + )) + return dial + + def get_value(self): + return self.point_to_number(self.dial.get_center()) + + def get_real_estate(self): + return self.point_to_number(self.re_dial.get_center()) + + def copy(self): + return self.deepcopy() + +class SliderScene(Scene): + CONFIG = { + "n_sliders" : 4, + "slider_spacing" : MED_LARGE_BUFF, + "slider_config" : {}, + "center_point" : None, + "total_real_estate" : 1, + "ambiently_change_sliders" : False, + "ambient_velocity_magnitude" : 1.0, + "ambient_acceleration_magnitude" : 1.0, + "ambient_jerk_magnitude" : 1.0/2, + } + def setup(self): + if self.center_point is None: + self.center_point = np.zeros(self.n_sliders) + sliders = VGroup(*[ + Slider(center_value = cv, **self.slider_config) + for cv in self.center_point + ]) + sliders.arrange_submobjects(RIGHT, buff = self.slider_spacing) + sliders[0].add_numbers() + sliders[0].set_value(np.sqrt(self.total_real_estate)) + + self.add(sliders) + self.sliders = sliders + + def reset_dials(self, values, fixed_sliders = None): + if fixed_sliders is None: fixed_sliders = [] + unspecified_sliders = [ + self.sliders[i] + for i in range(len(self.sliders)) + if i >= len(values) or values[i] is None + if self.sliders[i] not in fixed_sliders + ] + for value, slider in zip(values, self.sliders): + if value is not None: + slider.set_value(value) + #Readjust sliers with unspecified values that are not fixed + real_estate_diff = self.total_real_estate - self.get_current_total_real_estate() + for i, slider in enumerate(unspecified_sliders): + n_remaining = len(unspecified_sliders[i:]) + d_re = real_estate_diff / n_remaining + left_over = slider.change_real_estate(d_re) + real_estate_diff -= (d_re - left_over) + if real_estate_diff > 0.001: + raise Exception("Overspecified reset") + + def get_vector(self): + return np.array([slider.get_value() for slider in self.sliders]) + + def get_center_point(self): + return np.array([slider.center_value for slider in self.sliders]) + + def get_current_total_real_estate(self): + return sum([ + slider.get_real_estate() + for slider in self.sliders + ]) + + def get_all_dial_supplement_animations(self): + return [ + slider.get_dial_supplement_animation() + for slider in self.sliders + ] + + def initialize_ambiant_slider_movement(self): + self.ambiently_change_sliders = True + self.ambient_change_end_time = np.inf + self.ambient_change_time = 0 + self.ambient_velocity, self.ambient_acceleration, self.ambient_jerk = [ + self.get_random_vector(magnitude) + for magnitude in [ + self.ambient_velocity_magnitude, + self.ambient_acceleration_magnitude, + self.ambient_jerk_magnitude, + ] + ] + self.add_foreground_mobjects(self.sliders) + + def wind_down_ambient_movement(self, time = 1): + self.ambient_change_end_time = self.ambient_change_time + time + + def ambient_slider_movement_update(self): + #Set velocity_magnitude based on start up or wind down + velocity_magnitude = float(self.ambient_velocity_magnitude) + if self.ambient_change_time <= 1: + velocity_magnitude *= smooth(self.ambient_change_time) + time_until_end = self.ambient_change_end_time - self.ambient_change_time + if time_until_end <= 1: + velocity_magnitude *= smooth(time_until_end) + if time_until_end <= 0: + self.ambiently_change_sliders = False + return + + center_point = self.get_center_point() + target_vector = self.get_vector() - center_point + vectors_and_magnitudes = [ + (self.ambient_acceleration, self.ambient_acceleration_magnitude), + (self.ambient_velocity, velocity_magnitude), + (target_vector, np.sqrt(self.total_real_estate)), + ] + jerk = self.get_random_vector(self.ambient_jerk_magnitude) + deriv = jerk + for vect, mag in vectors_and_magnitudes: + vect += self.frame_duration*deriv + if vect is self.ambient_velocity: + unit_r_vect = target_vector / np.linalg.norm(target_vector) + vect -= np.dot(vect, unit_r_vect)*unit_r_vect + vect *= mag/np.linalg.norm(vect) + deriv = vect + + self.reset_dials(target_vector + center_point) + self.ambient_change_time += self.frame_duration + + def get_random_vector(self, magnitude): + result = 2*np.random.random(len(self.sliders)) - 1 + result *= magnitude / np.linalg.norm(result) + return result + + def update_frame(self, *args, **kwargs): + if self.ambiently_change_sliders: + self.ambient_slider_movement_update() + Scene.update_frame(self, *args, **kwargs) + + def dither(self, time = 1): + if self.ambiently_change_sliders: + self.play(Animation(self.sliders, run_time = time)) + else: + self.dither(time) + +########## + class MathIsATease(Scene): def construct(self): randy = Randolph() @@ -102,7 +334,9 @@ class CircleToPairsOfPoints(Scene): background_plane.highlight(GREY) background_plane.fade() circle = Circle(radius = 2, color = YELLOW) - dot = Dot(circle.get_right(), color = LIGHT_GREY) + + x, y = [np.sqrt(2)/2]*2 + dot = Dot(2*x*RIGHT + 2*y*UP, color = LIGHT_GREY) equation = TexMobject("x", "^2", "+", "y", "^2", "=", "1") equation.highlight_by_tex("x", GREEN) @@ -110,9 +344,8 @@ class CircleToPairsOfPoints(Scene): equation.to_corner(UP+LEFT) equation.add_background_rectangle() - x, y = 1, 0 coord_pair = TexMobject("(", "-%.02f"%x, ",", "-%.02f"%y, ")") - fixed_numbers = coord_pair.get_parts_by_tex(".00") + fixed_numbers = coord_pair.get_parts_by_tex("-") fixed_numbers.set_fill(opacity = 0) coord_pair.add_background_rectangle() coord_pair.next_to(dot, UP+RIGHT, SMALL_BUFF) @@ -129,18 +362,23 @@ class CircleToPairsOfPoints(Scene): self.add(background_plane, plane) self.play(ShowCreation(circle)) - self.play(Write(equation)) self.play( - LaggedStart(FadeIn, coord_pair), + FadeIn(coord_pair), + Write(numbers, run_time = 1), ShowCreation(dot), + ) + self.play( + Write(equation), *[ - ReplacementTransform( - equation.get_parts_by_tex(tex).copy(), - number + Transform( + number.copy(), + equation.get_parts_by_tex(tex), + remover = True ) for tex, number in zip("xy", numbers) ] ) + self.play(FocusOn(dot, run_time = 1)) self.play( Rotating( dot, run_time = 7, in_place = False, @@ -179,7 +417,8 @@ class CircleToPairsOfPoints(Scene): words.get_left(), rot_equation.get_bottom(), path_arc = -np.pi/6 ) - randy = Randolph().to_corner(DOWN+LEFT) + randy = Randolph(color = GREY_BROWN) + randy.to_corner(DOWN+LEFT) self.play( Write(rot_equation, run_time = 2), @@ -211,6 +450,305 @@ class CircleToPairsOfPoints(Scene): ) self.dither() +class GreatSourceOfMaterial(TeacherStudentsScene): + def construct(self): + self.teacher_says( + "It's a great source \\\\ of material.", + target_mode = "hooray" + ) + self.change_student_modes(*["happy"]*3) + self.dither(3) + +class CirclesSpheresSumsSquares(ExternallyAnimatedScene): + pass + +class BackAndForth(Scene): + def construct(self): + analytic = TextMobject("Analytic") + analytic.shift(SPACE_WIDTH*LEFT/2) + analytic.to_edge(UP, buff = MED_SMALL_BUFF) + geometric = TextMobject("Geometric") + geometric.shift(SPACE_WIDTH*RIGHT/2) + geometric.to_edge(UP, buff = MED_SMALL_BUFF) + h_line = Line(LEFT, RIGHT).scale(SPACE_WIDTH) + h_line.to_edge(UP, LARGE_BUFF) + v_line = Line(UP, DOWN).scale(SPACE_HEIGHT) + self.add(analytic, geometric, h_line, v_line) + + pair = TexMobject("(", "x", ",", "y", ")") + pair.shift(SPACE_WIDTH*LEFT/2 + SPACE_HEIGHT*UP/3) + triplet = TexMobject("(", "x", ",", "y", ",", "z", ")") + triplet.shift(SPACE_WIDTH*LEFT/2 + SPACE_HEIGHT*DOWN/2) + for mob in pair, triplet: + arrow = DoubleArrow(LEFT, RIGHT) + arrow.move_to(mob) + arrow.shift(2*RIGHT) + mob.arrow = arrow + circle_eq = TexMobject("x", "^2", "+", "y", "^2", "=", "1") + circle_eq.move_to(pair) + sphere_eq = TexMobject("x", "^2", "+", "y", "^2", "+", "z", "^2", "=", "1") + sphere_eq.move_to(triplet) + + plane = NumberPlane(x_unit_size = 2, y_unit_size = 2) + circle = Circle(radius = 2, color = YELLOW) + plane_group = VGroup(plane, circle) + plane_group.scale(0.4) + plane_group.next_to(h_line, DOWN, SMALL_BUFF) + plane_group.shift(SPACE_WIDTH*RIGHT/2) + + + self.play(Write(pair)) + # self.play(ShowCreation(pair.arrow)) + self.play(ShowCreation(plane, run_time = 3)) + self.play(Write(triplet)) + # self.play(ShowCreation(triplet.arrow)) + self.dither(3) + for tup, eq, to_draw in (pair, circle_eq, circle), (triplet, sphere_eq, VMobject()): + for mob in tup, eq: + mob.xyz = VGroup(*filter( + lambda sm : sm is not None, + map(mob.get_part_by_tex, "xyz") + )) + self.play( + ReplacementTransform(tup.xyz, eq.xyz), + FadeOut(VGroup(*filter( + lambda sm : sm not in tup.xyz, tup + ))), + ) + self.play( + Write(VGroup(*filter( + lambda sm : sm not in eq.xyz, eq + ))), + ShowCreation(to_draw) + ) + self.dither(3) + +class SphereForming(ExternallyAnimatedScene): + pass + +class PreviousVideos(Scene): + def construct(self): + titles = VGroup(*map(TextMobject, [ + "Pi hiding in prime regularities", + "Visualizing all possible pythagorean triples", + "Borsuk-Ulam theorem", + ])) + titles.to_edge(UP, buff = MED_SMALL_BUFF) + screen = ScreenRectangle(height = 6) + screen.next_to(titles, DOWN) + + title = titles[0] + self.add(title, screen) + self.dither(2) + for new_title in titles[1:]: + self.play(Transform(title, new_title)) + self.dither(2) + +class TODOTease(TODOStub): + CONFIG = { + "message" : "Tease" + } + +class AskAboutLongerLists(TeacherStudentsScene): + def construct(self): + question = TextMobject( + "What about \\\\", + "$(x_1, x_2, x_3, x_4)?$" + ) + tup = question[1] + alt_tups = map(TextMobject, [ + "$(x_1, x_2, x_3, x_4, x_5)?$", + "$(x_1, x_2, \\dots, x_{99}, x_{100})?$" + ]) + + self.student_says(question, run_time = 1) + self.dither() + for alt_tup in alt_tups: + alt_tup.move_to(tup) + self.play(Transform(tup, alt_tup)) + self.dither() + self.dither() + self.play( + RemovePiCreatureBubble(self.students[1]), + self.teacher.change, "raise_right_hand" + ) + self.change_student_modes( + *["confused"]*3, + look_at_arg = self.teacher.get_top() + 2*UP + ) + self.play(self.teacher.look, UP) + self.dither(5) + self.student_says( + "I...don't see it.", + target_mode = "maybe", + student_index = 0 + ) + self.dither(3) + +class FourDCubeRotation(ExternallyAnimatedScene): + pass + +class HypersphereRotation(ExternallyAnimatedScene): + pass + +class FourDSurfaceRotating(ExternallyAnimatedScene): + pass + +class Professionals(PiCreatureScene): + def construct(self): + self.introduce_characters() + self.add_equation() + self.analogies() + + def add_equation(self): + quaternion = TexMobject( + "\\frac{1}{2}", "+", + "0", "\\textbf{i}", "+", + "\\frac{\\sqrt{6}}{4}", "\\textbf{j}", "+", + "\\frac{\\sqrt{6}}{4}", "\\textbf{k}", + ) + quaternion.shift(SPACE_WIDTH*LEFT/2) + equation = TexMobject( + "\\textbf{i}", "^2", "=", + "\\textbf{j}", "^2", "=", + "\\textbf{k}", "^2", "=", + "\\textbf{i}", "\\textbf{j}", "\\textbf{k}", "=", + "-1" + ) + equation.shift(SPACE_WIDTH*RIGHT/2) + VGroup(quaternion, equation).to_edge(UP) + for mob in quaternion, equation: + mob.highlight_by_tex_to_color_map({ + "i" : RED, + "j" : GREEN, + "k" : BLUE, + }) + + brace = Brace(quaternion, DOWN) + words = brace.get_text("4 numbers") + + self.play( + Write(quaternion), + Write(equation), + GrowFromCenter(brace), + FadeIn(words), + run_time = 2 + ) + self.play(*[ + ApplyMethod(pi.change, "pondering", quaternion) + for pi in self.pi_creatures + ]) + self.dither() + self.play(FadeOut(VGroup(brace, words))) + + + self.quaternion = quaternion + self.equation = equation + + def introduce_characters(self): + titles = VGroup(*map(TextMobject, [ + "Mathematician", + "Computer scientist", + "Physicist", + ])) + self.remove(*self.pi_creatures) + for title, pi in zip(titles, self.pi_creatures): + title.next_to(pi, DOWN) + self.play( + Animation(VectorizedPoint(pi.eyes.get_center())), + FadeIn(pi), + Write(title, run_time = 1), + ) + self.dither() + + def analogies(self): + examples = VGroup() + plane = ComplexPlane( + x_radius = 2.5, + y_radius = 1.5, + ) + plane.add_coordinates() + plane.add(Circle(color = YELLOW)) + plane.scale(0.75) + examples.add(plane) + examples.add(Circle()) + examples.arrange_submobjects(RIGHT, buff = 2) + examples.next_to(self.pi_creatures, UP, MED_LARGE_BUFF) + labels = VGroup(*map(TextMobject, ["2D", "3D"])) + + title = TextMobject("Fly by instruments") + title.scale(1.5) + title.to_edge(UP) + + for label, example in zip(labels, examples): + label.next_to(example, DOWN) + self.play( + ShowCreation(example), + Write(label, run_time = 1) + ) + example.add(label) + self.dither() + self.dither() + self.play( + FadeOut(examples), + VGroup(self.quaternion, self.equation).shift, 2*DOWN, + Write(title, run_time = 2) + ) + self.play(*[ + ApplyMethod( + pi.change, mode, self.equation, + run_time = 2, + rate_func = squish_rate_func(smooth, a, a+0.5) + ) + for pi, mode, a in zip( + self.pi_creatures, + ["confused", "sassy", "erm"], + np.linspace(0, 0.5, len(self.pi_creatures)) + ) + ]) + self.dither() + self.play(Animation(self.quaternion)) + self.dither(2) + + + + ###### + + def create_pi_creatures(self): + mathy = Mathematician() + physy = PiCreature(color = PINK) + compy = PiCreature(color = PURPLE) + pi_creatures = VGroup(mathy, compy, physy) + for pi in pi_creatures: + pi.scale(0.7) + pi_creatures.arrange_submobjects(RIGHT, buff = 3) + pi_creatures.to_edge(DOWN, buff = LARGE_BUFF) + return pi_creatures + +class OfferAHybrid(SliderScene): + def construct(self): + slider = self.sliders[0] + + self.initialize_ambiant_slider_movement() + self.dither(3) + self.play(slider.shift, 3*LEFT) + self.dither(2) + self.wind_down_ambient_movement() + self.dither(2) + + + + + + + + + + + + + + diff --git a/stage_animations.py b/stage_animations.py new file mode 100644 index 00000000..6f129239 --- /dev/null +++ b/stage_animations.py @@ -0,0 +1,50 @@ +import sys +import inspect +import os +import shutil +import itertools as it +from extract_scene import is_scene, get_module +from constants import MOVIE_DIR, STAGED_SCENES_DIR + + +def get_sorted_scene_names(module_name): + module = get_module(module_name) + line_to_scene = {} + for name, scene_class in inspect.getmembers(module, is_scene): + lines, line_no = inspect.getsourcelines(scene_class) + line_to_scene[line_no] = name + return [ + line_to_scene[line_no] + for line_no in sorted(line_to_scene.keys()) + ] + + + +def stage_animaions(module_name): + scene_names = get_sorted_scene_names(module_name) + movie_dir = os.path.join( + MOVIE_DIR, module_name.replace(".py", "") + ) + files = os.listdir(movie_dir) + sorted_files = [] + for scene in scene_names: + for clip in filter(lambda f : f.startswith(scene), files): + sorted_files.append( + os.path.join(movie_dir, clip) + ) + for f in os.listdir(STAGED_SCENES_DIR): + os.remove(os.path.join(STAGED_SCENES_DIR, f)) + for f, count in zip(sorted_files, it.count()): + symlink_name = os.path.join( + STAGED_SCENES_DIR, + "Scene_%03d"%count + f.split(os.sep)[-1] + ) + os.symlink(f, symlink_name) + + + +if __name__ == "__main__": + if len(sys.argv) < 2: + raise Exception("No module given.") + module_name = sys.argv[1] + stage_animaions(module_name) \ No newline at end of file diff --git a/topics/complex_numbers.py b/topics/complex_numbers.py index 1efa41e4..8b998e46 100644 --- a/topics/complex_numbers.py +++ b/topics/complex_numbers.py @@ -196,10 +196,10 @@ class ComplexPlane(NumberPlane): result = VGroup() nudge = 0.1*(DOWN+RIGHT) if len(numbers) == 0: - numbers = range(-int(self.x_radius), int(self.x_radius)) + numbers = range(-int(self.x_radius), int(self.x_radius)+1) numbers += [ complex(0, y) - for y in range(-int(self.y_radius), int(self.y_radius)) + for y in range(-int(self.y_radius), int(self.y_radius)+1) ] for number in numbers: point = self.number_to_point(number) diff --git a/topics/geometry.py b/topics/geometry.py index 414b36bb..37ae0946 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -391,7 +391,7 @@ class PictureInPictureFrame(Rectangle): **kwargs ) self.scale_to_fit_height(height) - + class Cross(VGroup): CONFIG = { "stroke_color" : RED, diff --git a/topics/number_line.py b/topics/number_line.py index 82082e7b..5af5bfc4 100644 --- a/topics/number_line.py +++ b/topics/number_line.py @@ -19,7 +19,10 @@ class NumberLine(VMobject): "numbers_to_show" : None, "longer_tick_multiple" : 2, "number_at_center" : 0, - "propogate_style_to_family" : True + "number_scale_val" : 0.5, + "line_to_number_vect" : DOWN, + "line_to_number_buff" : MED_SMALL_BUFF, + "propogate_style_to_family" : True, } def __init__(self, **kwargs): digest_config(self, kwargs) @@ -38,13 +41,17 @@ class NumberLine(VMobject): self.stretch(self.unit_size, 0) self.shift(-self.number_to_point(self.number_at_center)) - def add_tick(self, x, size): - self.tick_marks.add(Line( - x*RIGHT+size*DOWN, - x*RIGHT+size*UP, - )) + def add_tick(self, x, size = None): + self.tick_marks.add(self.get_tick(x, size)) return self + def get_tick(self, x, size = None): + if size is None: size = self.tick_size + result = Line(size*DOWN, size*UP) + result.rotate(self.main_line.get_angle()) + result.move_to(self.number_to_point(x)) + return result + def get_tick_marks(self): return self.tick_marks @@ -77,10 +84,7 @@ class NumberLine(VMobject): def default_numbers_to_display(self): if self.numbers_to_show is not None: return self.numbers_to_show - return np.arange(self.leftmost_tick, self.x_max, 1) - - def get_vertical_number_offset(self, direction = DOWN): - return 4*direction*self.tick_size + return np.arange(int(self.leftmost_tick), int(self.x_max)+1) def get_number_mobjects(self, *numbers, **kwargs): #TODO, handle decimals @@ -91,10 +95,11 @@ class NumberLine(VMobject): result = VGroup() for number in numbers: mob = TexMobject(str(number)) - mob.scale_to_fit_height(3*self.tick_size) - mob.shift( + mob.scale(self.number_scale_val) + mob.next_to( self.number_to_point(number), - self.get_vertical_number_offset(**kwargs) + self.line_to_number_vect, + self.line_to_number_buff, ) result.add(mob) return result diff --git a/topics/probability.py b/topics/probability.py index 01c8b9c5..36dc35b0 100644 --- a/topics/probability.py +++ b/topics/probability.py @@ -367,8 +367,6 @@ class BarChart(VGroup): return self.deepcopy() - - ### Cards ### class DeckOfCards(VGroup):