From 400aa0aada85a39b5f1fafa1c5572af17165047f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 27 Jun 2015 04:49:10 -0700 Subject: [PATCH] Finished Tau Poem --- animation/animation.py | 39 ++- animation/simple_animations.py | 5 +- animation/transform.py | 5 +- constants.py | 2 +- helpers.py | 14 +- mobject/creatures.py | 78 ++++- mobject/function_graphs.py | 13 + mobject/image_mobject.py | 23 +- mobject/mobject.py | 21 +- sample_script.py | 9 +- scene/scene.py | 27 +- scripts/ecf_graph_scenes.py | 13 +- scripts/generate_logo.py | 90 +++-- scripts/moser_main.py | 52 +-- scripts/tau_poem.py | 591 +++++++++++++++++++++++++++++++++ 15 files changed, 872 insertions(+), 110 deletions(-) create mode 100644 scripts/tau_poem.py diff --git a/animation/animation.py b/animation/animation.py index b7d77eed..d5e1dcd7 100644 --- a/animation/animation.py +++ b/animation/animation.py @@ -68,9 +68,9 @@ class Animation(object): def update(self, alpha): if alpha < 0: - alpha = 0 + alpha = 0.0 if alpha > 1: - alpha = 1 + alpha = 1.0 self.update_mobject(self.alpha_func(alpha)) def filter_out(self, *filter_functions): @@ -111,5 +111,40 @@ class Animation(object): self.update(1) +class Succession(Animation): + def __init__(self, *animations, **kwargs): + if "run_time" in kwargs: + run_time = kwargs.pop("run_time") + else: + run_time = sum([anim.run_time for anim in animations]) + self.num_anims = len(animations) + self.anims = animations + mobject = animations[0].mobject + Animation.__init__(self, mobject, run_time = run_time, **kwargs) + + def __str__(self): + return self.__class__.__name__ + \ + "".join(map(str, self.anims)) + + def update(self, alpha): + scaled_alpha = alpha*self.num_anims + self.mobject = self.anims + for index in range(len(self.anims)): + self.anims[index].update(scaled_alpha - index) + + + + + + + + + + + + + + + diff --git a/animation/simple_animations.py b/animation/simple_animations.py index 75881bf4..3801d3d6 100644 --- a/animation/simple_animations.py +++ b/animation/simple_animations.py @@ -158,12 +158,11 @@ class WalkPiCreature(Animation): class BlinkPiCreature(Transform): - def __init__(self, pi_creature, run_time = 0.2, *args, **kwargs): + def __init__(self, pi_creature, *args, **kwargs): blinked = deepcopy(pi_creature).blink() Transform.__init__( self, pi_creature, blinked, - alpha_func = there_and_back, - run_time = run_time, + alpha_func = squish_alpha_func(there_and_back), *args, **kwargs ) diff --git a/animation/transform.py b/animation/transform.py index 3a28b217..e6f004d3 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -150,11 +150,14 @@ class ComplexFunction(ApplyFunction): #Todo, abstract away function naming' -#Fuck this is cool! class TransformAnimations(Transform): def __init__(self, start_anim, end_anim, alpha_func = squish_alpha_func(high_inflection_0_to_1), **kwargs): + if "run_time" in kwargs: + run_time = kwargs.pop("run_time") + for anim in start_anim, end_anim: + anim.set_run_time(run_time) self.start_anim, self.end_anim = start_anim, end_anim Transform.__init__( self, diff --git a/constants.py b/constants.py index c46246f2..5a59e588 100644 --- a/constants.py +++ b/constants.py @@ -29,7 +29,7 @@ SPACE_WIDTH = SPACE_HEIGHT * DEFAULT_WIDTH / DEFAULT_HEIGHT #All in seconds DEFAULT_FRAME_DURATION = 0.04 -DEFAULT_ANIMATION_RUN_TIME = 3.0 +DEFAULT_ANIMATION_RUN_TIME = 1.0 DEFAULT_TRANSFORM_RUN_TIME = 1.0 DEFAULT_DITHER_TIME = 1.0 diff --git a/helpers.py b/helpers.py index d12560ad..b9d4bf2f 100644 --- a/helpers.py +++ b/helpers.py @@ -106,15 +106,21 @@ def make_even_by_cycling(iterable_1, iterable_2): [cycle2.next() for x in range(length)] ) +### Alpha Functions ### + def sigmoid(x): return 1.0/(1 + np.exp(-x)) -### Alpha Functions ### - def high_inflection_0_to_1(t, inflection = 10.0): error = sigmoid(-inflection / 2) return (sigmoid(inflection*(t - 0.5)) - error) / (1 - 2*error) +def rush_into(t): + return 2*high_inflection_0_to_1(t/2) + +def rush_from(t): + return 2*high_inflection_0_to_1(t/2+0.5) - 1 + def there_and_back(t, inflection = 10.0): new_t = 2*t if t < 0.5 else 2*(1 - t) return high_inflection_0_to_1(new_t, inflection) @@ -128,9 +134,9 @@ def wiggle(t, wiggles = 2): def squish_alpha_func(func, a = 0.4, b = 0.6): def result(t): if t < a: - return 0 + return func(0) elif t > b: - return 1 + return func(1) else: return func((t-a)/(b-a)) return result diff --git a/mobject/creatures.py b/mobject/creatures.py index 33cc0e07..a7e367d9 100644 --- a/mobject/creatures.py +++ b/mobject/creatures.py @@ -9,8 +9,9 @@ from simple_mobjects import * class PiCreature(Mobject): DEFAULT_COLOR = "blue" - def __init__(self, color = DEFAULT_COLOR, **kwargs): - Mobject.__init__(self, color = color, **kwargs) + def __init__(self, **kwargs): + Mobject.__init__(self, **kwargs) + color = self.DEFAULT_COLOR scale_val = 0.5 mouth_to_eyes_distance = 0.25 part_names = [ @@ -43,7 +44,8 @@ class PiCreature(Mobject): self.right_eye.get_center()/2 - (0, mouth_to_eyes_distance, 0) ) - + self.eyes = [self.left_eye, self.right_eye] + self.legs = [self.left_leg, self.right_leg] for part in parts: self.add(part) self.parts = parts @@ -64,11 +66,16 @@ class PiCreature(Mobject): part.rgbs = self.rgbs[curr:curr+n_points,:] curr += n_points + def reload_from_parts(self): + self.rewire_part_attributes(self_from_parts = True) - def highlight(self, color): + def highlight(self, color, condition = None): + if condition is not None: + Mobject.highlight(self, color, condition) + return self for part in set(self.parts).difference(self.white_parts): part.highlight(color) - self.rewire_part_attributes(self_from_parts = True) + self.reload_from_parts() return self def move_to(self, destination): @@ -78,6 +85,7 @@ class PiCreature(Mobject): 0 )) self.shift(destination-bottom) + self.rewire_part_attributes() return self def give_frown(self): @@ -85,7 +93,7 @@ class PiCreature(Mobject): self.mouth.center() self.mouth.apply_function(lambda (x, y, z) : (x, -y, z)) self.mouth.shift(center) - self.rewire_part_attributes(self_from_parts = True) + self.reload_from_parts() return self def give_straight_face(self): @@ -97,7 +105,33 @@ class PiCreature(Mobject): self.parts[self.parts.index(self.mouth)] = new_mouth self.white_parts[self.white_parts.index(self.mouth)] = new_mouth self.mouth = new_mouth - self.rewire_part_attributes(self_from_parts = True) + self.reload_from_parts() + return self + + def get_eye_center(self): + return center_of_mass([ + self.left_eye.get_center(), + self.right_eye.get_center() + ]) + 0.04*RIGHT + 0.02*UP + + def make_mean(self): + eye_x, eye_y = self.get_eye_center()[:2] + def should_delete((x, y, z)): + return y - eye_y > 0.3*abs(x - eye_x) + for eye in self.left_eye, self.right_eye: + eye.highlight("black", should_delete) + self.give_straight_face() + return self + + def make_sad(self): + eye_x, eye_y = self.get_eye_center()[:2] + eye_y += 0.15 + def should_delete((x, y, z)): + return y - eye_y > -0.3*abs(x - eye_x) + for eye in self.left_eye, self.right_eye: + eye.highlight("black", should_delete) + self.give_frown() + self.reload_from_parts() return self def get_step_intermediate(self, pi_creature): @@ -122,22 +156,48 @@ class PiCreature(Mobject): eye.apply_function( lambda (x, y, z) : (x, bottom, z) ) - self.rewire_part_attributes(self_from_parts = True) + self.reload_from_parts() return self + def shift_eyes(self): + for eye in self.left_eye, self.right_eye: + center = eye.get_center() + eye.center() + eye.rotate(np.pi, UP) + eye.shift(center) + self.reload_from_parts() + return self + + def to_symbol(self): + for white_part in self.white_parts: + self.parts.remove(white_part) + self.reload_from_parts() + return self class Randolph(PiCreature): pass #Nothing more than an alternative name class Mortimer(PiCreature): + DEFAULT_COLOR = DARK_BROWN def __init__(self, *args, **kwargs): PiCreature.__init__(self, *args, **kwargs) - self.highlight(DARK_BROWN) + # self.highlight(DARK_BROWN) self.give_straight_face() self.rotate(np.pi, UP) self.rewire_part_attributes() +class TauCreature(PiCreature): + def __init__(self, **kwargs): + leg_shift_val = 0.25 + leg_wag_val = 0.2 + PiCreature.__init__(self, **kwargs) + self.parts.remove(self.right_leg) + self.left_leg.shift(leg_shift_val*RIGHT) + self.left_leg.wag(leg_wag_val*RIGHT, DOWN) + self.leg = self.left_leg + self.reload_from_parts() + diff --git a/mobject/function_graphs.py b/mobject/function_graphs.py index 8884ead5..1525a6ac 100644 --- a/mobject/function_graphs.py +++ b/mobject/function_graphs.py @@ -114,5 +114,18 @@ class NumberLine(Mobject1D): for y in np.arange(-self.tick_size, self.tick_size, self.epsilon) ]) +class Axes(CompoundMobject): + def __init__(self, *args, **kwargs): + x_axis = NumberLine(*args, **kwargs) + y_axis = NumberLine(*args, **kwargs).rotate(np.pi/2, OUT) + CompoundMobject.__init__(self, x_axis, y_axis) + + + + + + + + diff --git a/mobject/image_mobject.py b/mobject/image_mobject.py index 641bfa5c..1c6ac536 100644 --- a/mobject/image_mobject.py +++ b/mobject/image_mobject.py @@ -90,10 +90,19 @@ class SpeechBubble(ImageMobject): def write(self, text): smidgeon = 0.1*UP + 0.2*self.direction + self.clear() self.text = text_mobject(text) self.text.scale(0.75*self.get_width() / self.text.get_width()) self.text.shift(self.get_center() + smidgeon) + self.add(self.text) + def clear(self): + if not hasattr(self, "text"): + return + num_text_points = self.text.points.shape[0] + self.points = self.points[:num_text_points] + self.rgbs = self.rgbs[:num_text_points] + self.text = Mobject() class ThoughtBubble(ImageMobject): def __init__(self, *args, **kwargs): @@ -117,19 +126,15 @@ class VideoIcon(ImageMobject): self.center() def text_mobject(text, size = "\\Large"): - return tex_mobjects(text, size, TEMPLATE_TEXT_FILE) + return tex_mobject(text, size, TEMPLATE_TEXT_FILE) -#Purely redundant function to make singulars and plurals sensible -def tex_mobject(expression, size = "\\Huge"): - return tex_mobjects(expression, size) - -def tex_mobjects(expression, - size = "\\Huge", - template_tex_file = TEMPLATE_TEX_FILE): +def tex_mobject(expression, + size = "\\Huge", + template_tex_file = TEMPLATE_TEX_FILE): images = tex_to_image(expression, size, template_tex_file) if isinstance(images, list): #TODO, is checking listiness really the best here? - return CompoundMobject(*map(ImageMobject, images)).center().split() + return CompoundMobject(*map(ImageMobject, images)).center() else: return ImageMobject(images).center() diff --git a/mobject/mobject.py b/mobject/mobject.py index d665fc6c..8671e745 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -171,14 +171,16 @@ class Mobject(object): return (result.real, result.imag, 0) return self.apply_function(point_map) - def highlight(self, color = "red", condition = lambda x : True): + def highlight(self, color = "red", condition = None): """ Condition is function which takes in one arguments, (x, y, z). """ - #TODO, Should self.color change? - to_change = np.apply_along_axis(condition, 1, self.points) - self.rgbs[to_change, :] *= 0 - self.rgbs[to_change, :] += Color(color).get_rgb() + rgb = Color(color).get_rgb() + if condition: + to_change = np.apply_along_axis(condition, 1, self.points) + self.rgbs[to_change, :] = rgb + else: + self.rgbs[:,:] = rgb return self def fade(self, brightness = 0.5): @@ -191,6 +193,15 @@ class Mobject(object): self.rgbs = self.rgbs[to_eliminate] return self + def sort_points(self, function = lambda p : p[0]): + """ + function is any map from R^3 to R + """ + self.points = np.array(sorted( + self.points, + lambda *points : cmp(*map(function, points)) + )) + ### Getters ### def get_num_points(self): diff --git a/sample_script.py b/sample_script.py index 4ec78064..51f865ba 100644 --- a/sample_script.py +++ b/sample_script.py @@ -16,13 +16,8 @@ from script_wrapper import command_line_create_scene class SampleScene(Scene): def construct(self): - randy = Randolph() - self.add(randy) - self.dither() - self.animate(BlinkPiCreature(randy)) - self.dither() - self.animate(WaveArm(randy)) - self.dither() + tauy = TauCreature() + self.animate(ApplyMethod(tauy.make_sad)) diff --git a/scene/scene.py b/scene/scene.py index a5871096..dc598ef1 100644 --- a/scene/scene.py +++ b/scene/scene.py @@ -115,6 +115,29 @@ class Scene(object): progress_bar.finish() return self + def animate_over_time_range(self, t0, t1, animation): + needed_scene_time = max(abs(t0), abs(t1)) + existing_scene_time = len(self.frames)*self.frame_duration + if existing_scene_time < needed_scene_time: + self.dither(needed_scene_time - existing_scene_time) + existing_scene_time = needed_scene_time + #So negative values may be used + if t0 < 0: + t0 = float(t0)%existing_scene_time + if t1 < 0: + t1 = float(t1)%existing_scene_time + t0, t1 = min(t0, t1), max(t0, t1) + + for t in np.arange(t0, t1, self.frame_duration): + animation.update((t-t0)/(t1 - t0)) + index = int(t/self.frame_duration) + self.frames[index] = disp.paint_mobject( + animation.mobject, self.frames[index] + ) + animation.clean_up() + return self + + def count(self, items, item_type = "mobject", *args, **kwargs): if item_type == "mobject": self.count_mobjects(items, *args, **kwargs) @@ -205,9 +228,7 @@ class Scene(object): self.dither(end_dither_time) disp.write_to_gif(self, name or str(self)) - def write_to_movie(self, name = None, - end_dither_time = DEFAULT_DITHER_TIME): - self.dither(end_dither_time) + def write_to_movie(self, name = None): disp.write_to_movie(self, name or str(self)) def show_frame(self): diff --git a/scripts/ecf_graph_scenes.py b/scripts/ecf_graph_scenes.py index ccfac609..db8397cb 100644 --- a/scripts/ecf_graph_scenes.py +++ b/scripts/ecf_graph_scenes.py @@ -107,9 +107,9 @@ class IntroduceGraph(GraphScene): for pair in [(4, 5), (0, 5), (1, 5), (7, 1), (8, 3)] ] - connected, planar, graph = CompoundMobject(*text_mobject([ + connected, planar, graph = text_mobject([ "Connected ", "Planar ", "Graph" - ])).to_edge(UP).split() + ]).to_edge(UP).split() not_okay = text_mobject("Not Okay").highlight("red") planar_explanation = text_mobject(""" (``Planar'' just means we can draw it without @@ -181,7 +181,7 @@ class PlanarGraphDefinition(Scene): Not, quote, planar, end_quote = text_mobject([ "Not \\\\", "``", "Planar", "''", # "no matter how \\\\ hard you try" - ]) + ]).split() shift_val = CompoundMobject(Not, planar).to_corner().get_center() Not.highlight("red").shift(shift_val) graphs = [ @@ -1073,7 +1073,10 @@ class MortimerCannotTraverseCycle(GraphScene): class TwoPropertiesOfSpanningTree(Scene): def construct(self): - spanning, tree = text_mobject(["Spanning ", "Tree"], size = "\\Huge") + spanning, tree = text_mobject( + ["Spanning ", "Tree"], + size = "\\Huge" + ).split() spanning_explanation = text_mobject(""" Touches every vertex """).shift(spanning.get_center() + 2*DOWN) @@ -1158,7 +1161,7 @@ class FinalSum(Scene): "(\\text{Number of Randolph's Edges}) + 1 &= V \\\\ \n", "(\\text{Number of Mortimer's Edges}) + 1 &= F \\\\ \n", " \\Downarrow \\\\", "E","+","2","&=","V","+","F", - ], size = "\\large") + ], size = "\\large").split() for line in lines[:2] + [CompoundMobject(*lines[2:])]: self.add(line) self.dither() diff --git a/scripts/generate_logo.py b/scripts/generate_logo.py index e7b599d4..ff5e6a54 100644 --- a/scripts/generate_logo.py +++ b/scripts/generate_logo.py @@ -1,44 +1,64 @@ #!/usr/bin/env python -from PIL import Image import numpy as np +import itertools as it +from copy import deepcopy +import sys + from animation import * from mobject import * from constants import * -from helpers import * -from scene import * -import itertools as it -import os +from region import * +from scene import Scene +from script_wrapper import command_line_create_scene + +class LogoGeneration(Scene): + LOGO_RADIUS = 1.5 + INNER_RADIUS_RATIO = 0.55 + CIRCLE_DENSITY = 100 + CIRCLE_BLUE = "skyblue" + SPHERE_DENSITY = 50 + SPHERE_BLUE = DARK_BLUE + CIRCLE_SPHERE_INTERPOLATION = 0.3 + + def construct(self): + circle = Circle( + density = self.CIRCLE_DENSITY, + color = self.CIRCLE_BLUE + ).repeat(5).scale(self.LOGO_RADIUS) + sphere = Sphere( + density = self.SPHERE_DENSITY, + color = self.SPHERE_BLUE + ).scale(self.LOGO_RADIUS) + sphere.rotate(-np.pi / 7, [1, 0, 0]) + sphere.rotate(-np.pi / 7) + alpha = 0.3 + iris = Mobject() + Mobject.interpolate( + circle, sphere, iris, + self.CIRCLE_SPHERE_INTERPOLATION + ) + for mob, color in [(iris, LIGHT_BROWN), (circle, DARK_BROWN)]: + mob.highlight(color, lambda (x, y, z) : x < 0 and y > 0) + mob.highlight( + "black", + lambda point: np.linalg.norm(point) < \ + self.INNER_RADIUS_RATIO*self.LOGO_RADIUS + ) + name = text_mobject("3Blue1Brown").center() + name.highlight("grey") + name.shift(2*DOWN) + + self.animate(Transform( + circle, iris, + run_time = DEFAULT_ANIMATION_RUN_TIME + )) + self.add(name) + self.dither() + print "Dragging pixels..." + self.frames = drag_pixels(self.frames) -LOGO_RADIUS = 1.5 - -if __name__ == '__main__': - circle = Circle(density = 100, color = 'skyblue').repeat(5).scale(LOGO_RADIUS) - sphere = Sphere(density = 50, color = DARK_BLUE).scale(LOGO_RADIUS) - sphere.rotate(-np.pi / 7, [1, 0, 0]) - sphere.rotate(-np.pi / 7) - alpha = 0.3 - iris = Mobject() - Mobject.interpolate(circle, sphere, iris, alpha) - for mob, color in [(iris, LIGHT_BROWN), (circle, DARK_BROWN)]: - mob.highlight(color, lambda (x, y, z) : x < 0 and y > 0) - mob.highlight("black", lambda point: np.linalg.norm(point) < 0.55*LOGO_RADIUS) - - name = tex_mobject(r"\text{3Blue1Brown}").center() - name.highlight("gray") - name.shift((0, -2, 0)) - sc = Scene() - sc.animate(Transform( - circle, iris, - run_time = DEFAULT_ANIMATION_RUN_TIME - )) - sc.add(name) - sc.dither() - sc.frames = drag_pixels(sc.frames) - sc.write_to_movie("LogoGeneration", end_dither_time = 0) - - - # index = int(DEFAULT_ANIMATION_RUN_TIME / DEFAULT_ANIMATION_PAUSE_TIME) - # create_eye.frames[index].save(LOGO_PATH) +if __name__ == "__main__": + command_line_create_scene() diff --git a/scripts/moser_main.py b/scripts/moser_main.py index 916f59d7..318ca0a5 100644 --- a/scripts/moser_main.py +++ b/scripts/moser_main.py @@ -149,7 +149,7 @@ class MoserPattern(CircleScene): def __init__(self, radians, *args, **kwargs): CircleScene.__init__(self, radians, *args, **kwargs) self.remove(*self.dots + self.lines + [self.n_equals]) - n_equals, num = tex_mobjects(["n=", "10"]) + n_equals, num = tex_mobject(["n=", "10"]).split() for mob in n_equals, num: mob.shift((-SPACE_WIDTH + 1.5, SPACE_HEIGHT - 1.5, 0)) self.add(n_equals) @@ -183,7 +183,7 @@ class HardProblemsSimplerQuestions(Scene): for sym in ["n", "2", "3"] ]) # not_that_hard = text_mobject("(maybe not that hard...)").scale(0.5) - fermat2, fermat2_jargon = tex_mobjects([ + fermat2, fermat2_jargon = tex_mobject([ r"&x^2 + y^2 = z^2 \\", r""" &(3, 4, 5) \\ @@ -193,14 +193,14 @@ class HardProblemsSimplerQuestions(Scene): (m^2 - &n^2, 2mn, m^2 + n^2) \\ &\quad \vdots """ - ]) - fermat3, fermat3_jargon = tex_mobjects([ + ]).split() + fermat3, fermat3_jargon = tex_mobject([ r"&x^3 + y^3 = z^3\\", r""" &y^3 = (z - x)(z - \omega x)(z - \omega^2 x) \\ &\mathds{Z}[\omega] \text{ is a UFD...} """ - ]) + ]).split() for mob in [fermat2, fermat3, fermat["2"], fermat["3"], fermat2_jargon, fermat3_jargon]: mob.scale(scale_factor) @@ -332,10 +332,10 @@ class CountIntersectionPoints(CircleScene): scale_factor = 0.4 text = tex_mobject(r"\text{How Many Intersection Points?}", size = size) n = len(radians) - formula, answer = tex_mobjects([ + formula, answer = tex_mobject([ r"{%d \choose 4} = \frac{%d(%d - 1)(%d - 2)(%d-3)}{1\cdot 2\cdot 3 \cdot 4}="%(n, n, n, n, n), str(choose(n, 4)) - ]) + ]).split() text.scale(scale_factor).shift(text_center) self.add(text) self.count(intersection_dots, mode="show", num_offset = ORIGIN) @@ -492,7 +492,7 @@ class IllustrateNChooseK(Scene): Scene.__init__(self, *args, **kwargs) nrange = range(1, n+1) tuples = list(it.combinations(nrange, k)) - nrange_mobs = tex_mobjects([str(n) + r'\;' for n in nrange]) + nrange_mobs = tex_mobject([str(n) + r'\;' for n in nrange]).split() tuple_mobs = tex_mobjects( [ (r'\\&' if c%(20//k) == 0 else r'\;\;') + str(p) @@ -831,10 +831,10 @@ class CannotDirectlyApplyEulerToMoser(CircleScene): def __init__(self, radians, *args, **kwargs): CircleScene.__init__(self, radians, *args, **kwargs) self.remove(self.n_equals) - n_equals, intersection_count = tex_mobjects([ + n_equals, intersection_count = tex_mobject([ r"&n = %d\\"%len(radians), r"&{%d \choose 4} = %d"%(len(radians), choose(len(radians), 4)) - ]) + ]).split() shift_val = self.n_equals.get_center() - n_equals.get_center() for mob in n_equals, intersection_count: mob.shift(shift_val) @@ -877,10 +877,10 @@ class ShowMoserGraphLines(CircleScene): radians = list(set(map(lambda x : x%(2*np.pi), radians))) radians.sort() CircleScene.__init__(self, radians, *args, **kwargs) - n, plus_n_choose_4 = tex_mobjects(["n", "+{n \\choose 4}"]) - n_choose_2, plus_2_n_choose_4, plus_n = tex_mobjects([ + n, plus_n_choose_4 = tex_mobject(["n", "+{n \\choose 4}"]).split() + n_choose_2, plus_2_n_choose_4, plus_n = tex_mobject([ r"{n \choose 2}",r"&+2{n \choose 4}\\",r"&+n" - ]) + ]).split() for mob in n, plus_n_choose_4, n_choose_2, plus_2_n_choose_4, plus_n: mob.shift((SPACE_WIDTH - 2, SPACE_HEIGHT-1, 0)) self.chop_lines_at_intersection_points() @@ -1028,19 +1028,19 @@ class ApplyEulerToMoser(CircleScene): equals, two, two1, n, n1, nc2, nc4, nc41] V[1], minus[1], E[1], plus[1], F[1], equals[1], two[1] = \ - tex_mobjects(["V", "-", "E", "+", "F", "=", "2"]) + tex_mobject(["V", "-", "E", "+", "F", "=", "2"]).split() F[2], equals[2], E[2], minus[2], V[2], plus[2], two[2] = \ - tex_mobjects(["F", "=", "E", "-", "V", "+", "2"]) + tex_mobject(["F", "=", "E", "-", "V", "+", "2"]).split() F[3], equals[3], E[3], minus[3], n[3], minus1[3], nc4[3], plus[3], two[3] = \ - tex_mobjects(["F", "=", "E", "-", "n", "-", r"{n \choose 4}", "+", "2"]) + tex_mobject(["F", "=", "E", "-", "n", "-", r"{n \choose 4}", "+", "2"]).split() F[4], equals[4], nc2[4], plus1[4], two1[4], nc41[4], plus2[4], n1[4], minus[4], n[4], minus1[4], nc4[4], plus[4], two[4] = \ - tex_mobjects(["F", "=", r"{n \choose 2}", "+", "2", r"{n \choose 4}", "+", "n","-", "n", "-", r"{n \choose 4}", "+", "2"]) + tex_mobject(["F", "=", r"{n \choose 2}", "+", "2", r"{n \choose 4}", "+", "n","-", "n", "-", r"{n \choose 4}", "+", "2"]).split() F[5], equals[5], nc2[5], plus1[5], two1[5], nc41[5], minus1[5], nc4[5], plus[5], two[5] = \ - tex_mobjects(["F", "=", r"{n \choose 2}", "+", "2", r"{n \choose 4}", "-", r"{n \choose 4}", "+", "2"]) + tex_mobject(["F", "=", r"{n \choose 2}", "+", "2", r"{n \choose 4}", "-", r"{n \choose 4}", "+", "2"]).split() F[6], equals[6], nc2[6], plus1[6], nc4[6], plus[6], two[6] = \ - tex_mobjects(["F", "=", r"{n \choose 2}", "+", r"{n \choose 4}", "+", "2"]) + tex_mobject(["F", "=", r"{n \choose 2}", "+", r"{n \choose 4}", "+", "2"]).split() F[7], equals[7], two[7], plus[7], nc2[7], plus1[7], nc4[7] = \ - tex_mobjects(["F", "=", "2", "+", r"{n \choose 2}", "+", r"{n \choose 4}"]) + tex_mobject(["F", "=", "2", "+", r"{n \choose 2}", "+", r"{n \choose 4}"]).split() shift_val = (0, 3, 0) for d in dicts: if not d: @@ -1423,9 +1423,9 @@ class MoserSolutionInPascal(PascalsTriangleScene): term_color = "green" self.generate_n_choose_k_mobs() self.remove(*[self.coords_to_mobs[n0][k0] for n0, k0 in self.coords]) - terms = one, plus0, n_choose_2, plus1, n_choose_4 = tex_mobjects([ + terms = one, plus0, n_choose_2, plus1, n_choose_4 = tex_mobject([ "1", "+", r"{%d \choose 2}"%n, "+", r"{%d \choose 4}"%n - ]) + ]).split() target_terms = [] for k in range(len(terms)): if k%2 == 0 and k <= n: @@ -1537,9 +1537,9 @@ class ExplainNChoose2Formula(Scene): nums[x].shift((0, x*height, 0)) nums_compound = CompoundMobject(*nums) nums_compound.shift(a_mob.get_center() - nums[0].get_center()) - n_mob, n_minus_1, over_2 = tex_mobjects([ + n_mob, n_minus_1, over_2 = tex_mobject([ str(n), "(%d-1)"%n, r"\over{2}" - ]) + ]).split() for part in n_mob, n_minus_1, over_2: part.shift((SPACE_WIDTH-1.5, SPACE_HEIGHT-1, 0)) @@ -1603,10 +1603,10 @@ class ExplainNChoose4Formula(Scene): ) quad_mobs = tuple_mobs[1::2] parens = CompoundMobject(*tuple_mobs[0::2]) - form_mobs = tex_mobjects([ + form_mobs = tex_mobject([ str(n), "(%d-1)"%n, "(%d-2)"%n,"(%d-3)"%n, r"\over {4 \cdot 3 \cdot 2 \cdot 1}" - ]) + ]).split() form_mobs = CompoundMobject(*form_mobs).scale(0.7).shift((4, 3, 0)).split() nums = [tex_mobject(str(k)) for k in range(1, n+1)] height = 1.5*nums[0].get_height() diff --git a/scripts/tau_poem.py b/scripts/tau_poem.py new file mode 100644 index 00000000..34e4aaae --- /dev/null +++ b/scripts/tau_poem.py @@ -0,0 +1,591 @@ +#!/usr/bin/env python + +import numpy as np +import itertools as it +from copy import deepcopy +import sys + + +from animation import * +from mobject import * +from constants import * +from region import * +from scene import Scene +from script_wrapper import command_line_create_scene +from generate_logo import LogoGeneration + +POEM_LINES = """Fixed poorly in notation with that two, +you shine so loud that you deserve a name. +Late though we are to make a change, it's true, +We can extol you 'til you have pi's fame. +One might object, ``Conventions matter not! +Great formulae cast truths transcending names.'' +I've noticed, though, how language molds my thoughts; +the natural terms make heart and head the same. +So lose the two inside your autograph, +then guide our thoughts without your ``better'' half. +Wonders math imparts become so neat +when phrased with you, and pi remains off-screen. +Sine and exp both cycle to your beat. +Jive with Fourier, and forms are clean. +``Wait! Area of circles'', pi would say, +``sticks oddly to one half when tau's preferred.'' +More to you then! For write it in this way, +then links to triangles can be inferred. +Nix pi, then all within geometry +shines clean and clear, as if by poetry.""".split("\n") + +DIGIT_TO_WORD = { + '0' : "Zero", + '1' : "One", + '2' : "Two", + '3' : "Three", + '4' : "Four", + '5' : "Five", + '6' : "Six", + '7' : "Seven", + '8' : "Eight", + '9' : "Nine", +} + +FORMULAE = [ + "e^{x + \\tau i} = e^{x}", + "&\\Leftrightarrow", + "e^{x + 2\\pi i} = e^{x} \\\\", + "A = \\frac{1}{2} \\tau r^2", + "&\\Leftrightarrow", + "A = \\pi r^2 \\\\", + "n! \\sim \\sqrt{\\tau n}\\left(\\frac{n}{e}\\right)^n", + "&\\Leftrightarrow", + "n! \\sim \\sqrt{2\\pi n}\\left(\\frac{n}{e}\\right)^n \\\\", + # "\\sum_{n = 0}^\\infty \\frac{(-1)^n}{2n+1} = \\frac{\\tau}{8}", + # "&\\Leftrightarrow", + # "\\sum_{n = 0}^\\infty \\frac{(-1)^n}{2n+1} = \\frac{\\pi}{4} \\\\", +] + +DIGITS = map(str, list("62831853071795864769")) +DIGITS[1] = "." + DIGITS[1] #2->.2 + +BUFF = 1.0 + +MOVIE_PREFIX = "tau_poem/" + +class Welcome(LogoGeneration): + def construct(self): + text = "Happy $\\tau$ Day, from 3Blue1Brown!" + self.add(text_mobject(text).to_edge(UP)) + LogoGeneration.construct(self) + +class HappyTauDayWords(Scene): + def construct(self): + words = text_mobject("Happy Tau Day Everybody!").scale(2) + tau = TauCreature().move_to(2*LEFT + UP) + pi = PiCreature().move_to(2*RIGHT + 3*DOWN) + pi.highlight("red") + self.add(words, tau, pi) + self.dither() + self.animate(BlinkPiCreature(tau)) + self.animate(BlinkPiCreature(pi)) + +class TauPoem(Scene): + args_list = map(lambda x : (x,), range(len(POEM_LINES))) + @staticmethod + def args_to_string(line_num, *ignore): + return str(line_num) + + def __init__(self, line_num, *args, **kwargs): + self.line_num = line_num + self.anim_kwargs = { + "run_time" : 4.0, + } + self.line_num_to_method = { + 0 : self.line0, + 1 : self.line1, + 2 : self.line2, + 3 : self.line3, + 4 : self.line4, + 5 : self.line5, + 6 : self.line6, + 7 : self.line7, + 8 : self.line8, + 9 : self.line9, + 10 : self.line10, + 11 : self.line11, + 12 : self.line12, + 13 : self.line13, + 14 : self.line14, + 15 : self.line15, + 16 : self.line16, + 17 : self.line17, + 18 : self.line18, + 19 : self.line19, + } + Scene.__init__(self, *args, **kwargs) + + def construct(self): + self.add_line_and_number() + self.line_num_to_method[self.line_num]() + self.first_word_to_last_digit() + + def add_line_and_number(self): + self.first_digits, new_digit, last_digits = tex_mobject([ + "".join(DIGITS[:self.line_num]), + DIGITS[self.line_num], + "".join(DIGITS[(self.line_num+1):]), + ]).to_edge(UP, buff=0.2).split() + line_str = POEM_LINES[self.line_num] + if self.line_num == 0: + index = line_str.index("ed ") + elif self.line_num == 10: + index = line_str.index("ders") + else: + index = line_str.index(" ") + first_word, rest_of_line = text_mobject( + [line_str[:index], line_str[index:]] + ).to_edge(UP).shift(BUFF*DOWN).split() + first_word.shift(0.15*RIGHT) #Stupid + number_word = text_mobject(DIGIT_TO_WORD[DIGITS[self.line_num][-1]]) + number_word.shift(first_word.get_center()) + number_word.shift(BUFF * UP / 2) + + kwargs = { + "alpha_func" : squish_alpha_func(high_inflection_0_to_1), + } + self.add(first_word, rest_of_line, self.first_digits) + self.first_word = first_word + self.number_word = number_word + self.new_digit = new_digit + + def first_word_to_last_digit(self): + if self.line_num == 19: + shift_val = SPACE_HEIGHT*DOWN + self.new_digit.shift(shift_val) + self.animate(ApplyMethod( + self.first_digits.shift, shift_val, run_time = 2.0 + )) + self.dither(2) + self.animate_over_time_range(0, 2, + Transform( + deepcopy(self.first_word), self.number_word, + alpha_func = squish_alpha_func(high_inflection_0_to_1) + ) + ) + self.animate_over_time_range(2, 4, + Transform( + self.number_word, self.new_digit, + alpha_func = squish_alpha_func(high_inflection_0_to_1) + ) + ) + + def line0(self): + two, pi = tex_mobject(["2", "\\pi"]).scale(2).split() + self.add(two, pi) + two_copy = deepcopy(two).rotate(np.pi/10).highlight("yellow") + self.animate(Transform( + two, two_copy, + alpha_func = squish_alpha_func( + lambda t : wiggle(t), + 0.4, 0.9, + ), + **self.anim_kwargs + )) + + def line1(self): + two_pi = tex_mobject(["2", "\\pi"]).scale(2) + tau = TauCreature() + tau.to_symbol() + sphere = Mobject() + Mobject.interpolate( + two_pi, + Sphere().highlight("yellow"), + sphere, + 0.8 + ) + self.add(two_pi) + self.dither() + self.animate(SemiCircleTransform( + two_pi, sphere, + alpha_func = lambda t : 2*high_inflection_0_to_1(t/2) + )) + self.remove(two_pi) + self.animate(SemiCircleTransform( + sphere, tau, + alpha_func = lambda t : 2*(high_inflection_0_to_1(t/2+0.5)-0.5) + )) + self.remove(sphere) + self.add(tau) + self.dither() + + def line2(self): + tau = TauCreature() + tau.make_sad() + tau.mouth.points = np.array(sorted( + tau.mouth.points, + lambda p0, p1 : cmp(p0[0], p1[0]) + )) + blinked = deepcopy(tau).blink() + for eye in blinked.eyes: + eye.highlight("black") + self.add(*set(tau.parts).difference(tau.white_parts)) + self.animate(*[ + Transform(*eyes) + for eyes in zip(blinked.eyes, tau.eyes) + ]) + self.animate(ShowCreation(tau.mouth)) + self.dither(2) + + def line3(self): + tau = TauCreature().make_sad() + pi = PiCreature() + self.add(*tau.parts) + self.dither() + self.animate( + Transform(tau.leg, pi.left_leg), + ShowCreation(pi.right_leg), + run_time = 1.0, + ) + self.animate(*[ + Transform(*parts) + for parts in zip(tau.white_parts, pi.white_parts) + ]) + self.remove(*tau.parts + pi.parts) + self.animate(BlinkPiCreature(pi)) + + def pi_speaking(self, text): + pi = PiCreature() + pi.highlight("red").give_straight_face() + pi.shift(3*DOWN + LEFT) + bubble = SpeechBubble().speak_from(pi) + bubble.write(text) + return pi, bubble + + def line4(self): + pi, bubble = self.pi_speaking("Conventions matter \\\\ not!") + self.add(pi) + self.dither() + self.animate(Transform( + Point(bubble.tip).highlight("black"), + bubble + )) + + + def line5(self): + pi, bubble = self.pi_speaking(""" + Great formulae cast \\\\ + truths transcending \\\\ + names. + """) + self.add(pi, bubble) + + formulae = tex_mobject(FORMULAE, size = "\\small") + formulae.scale(1.25) + formulae.to_corner([1, -1, 0]) + self.animate(FadeIn(formulae)) + + def line6(self): + bubble = ThoughtBubble() + self.animate(ApplyFunction( + lambda p : 2 * p / np.linalg.norm(p), + bubble, + alpha_func = wiggle, + run_time = 3.0, + )) + + def line7(self): + bubble = ThoughtBubble() + heart = ImageMobject("heart") + heart.scale(0.5).shift(DOWN).highlight("red") + for mob in bubble, heart: + mob.sort_points(np.linalg.norm) + + self.add(bubble) + self.dither() + self.remove(bubble) + bubble_copy = deepcopy(bubble) + self.animate(SemiCircleTransform(bubble_copy, heart)) + self.dither() + self.remove(bubble_copy) + self.animate(SemiCircleTransform(heart, bubble)) + self.dither() + + + def line8(self): + pi = PiCreature().give_straight_face() + tau = TauCreature() + two = ImageMobject("big2").scale(0.5).shift(1.6*LEFT + 0.1*DOWN) + + self.add(two, *pi.parts) + self.dither() + self.animate( + Transform(pi.left_leg, tau.leg), + Transform( + pi.right_leg, + Point(pi.right_leg.points[0,:]).highlight("black") + ), + Transform(pi.mouth, tau.mouth), + SemiCircleTransform( + two, + Dot(two.get_center()).highlight("black") + ) + ) + + def line9(self): + tau = TauCreature() + pi = PiCreature().highlight("red").give_straight_face() + pi.scale(0.2).move_to(tau.arm.points[-1,:]) + point = Point(pi.get_center()).highlight("black") + vanish_local = 3*(LEFT + UP) + new_pi = deepcopy(pi) + new_pi.scale(0.01) + new_pi.rotate(np.pi) + new_pi.shift(vanish_local) + Mobject.highlight(new_pi, "black") + + self.add(tau) + self.dither() + self.animate(Transform(point, pi)) + self.remove(point) + self.add(pi) + self.animate(WaveArm(tau),Transform(pi, new_pi)) + + def line10(self): + formulae = tex_mobject(FORMULAE, "\\small") + formulae.scale(1.5).to_edge(DOWN) + self.add(formulae) + + def line11(self): + formulae = tex_mobject(FORMULAE, "\\small") + formulae.scale(1.5).to_edge(DOWN) + formulae = formulae.split() + f_copy = deepcopy(formulae) + for mob, count in zip(f_copy, it.count()): + if count%3 == 0: + mob.to_edge(LEFT).shift(RIGHT*(SPACE_WIDTH-1)) + else: + mob.shift(2*SPACE_WIDTH*RIGHT) + self.animate(*[ + Transform(*mobs, run_time = 2.0) + for mobs in zip(formulae, f_copy) + ]) + + def line12(self): + interval_size = 0.5 + axes_center = SPACE_WIDTH*LEFT/2 + grid_center = SPACE_WIDTH*RIGHT/2 + radius = SPACE_WIDTH / 2.0 + axes = Axes( + radius = radius, + interval_size = interval_size + ) + axes.shift(axes_center) + def sine_curve(t): + t += 1 + result = np.array((-np.pi*t, np.sin(np.pi*t), 0)) + result *= interval_size + result += axes_center + return result + sine = ParametricFunction(sine_curve) + sine_period = Line( + axes_center, + axes_center + 2*np.pi*interval_size*RIGHT + ) + grid = Grid(radius = radius).shift(grid_center) + circle = Circle().scale(interval_size).shift(grid_center) + grid.add(tex_mobject("e^{ix}").shift(grid_center+UP+RIGHT)) + circle.highlight("white") + tau_line = Line( + *[np.pi*interval_size*vect for vect in LEFT, RIGHT], + density = 5*DEFAULT_POINT_DENSITY_1D + ) + tau_line.highlight("red") + tau = tex_mobject("\\tau") + tau.shift(tau_line.get_center() + 0.5*UP) + + self.add(axes, grid) + self.animate( + TransformAnimations( + ShowCreation(sine), + ShowCreation(deepcopy(sine).shift(2*np.pi*interval_size*RIGHT)), + run_time = 2.0, + alpha_func = high_inflection_0_to_1 + ), + ShowCreation(circle) + ) + self.animate( + SemiCircleTransform(sine_period, tau_line), + SemiCircleTransform(circle, deepcopy(tau_line)), + FadeOut(axes), + FadeOut(grid), + FadeOut(sine), + FadeIn(tau), + ) + self.dither() + + + def line13(self): + formula = form_start, two_pi, form_end = tex_mobject([ + "\\hat{f^{(n)}}(\\xi) = (", + "2\\pi", + "i\\xi)^n \\hat{f}(\\xi)" + ]).shift(DOWN).split() + tau = TauCreature().center() + tau.scale(two_pi.get_width()/tau.get_width()) + tau.shift(two_pi.get_center()+0.2*UP) + tau.rewire_part_attributes() + + self.add(*formula) + self.dither() + self.animate(SemiCircleTransform(two_pi, tau)) + self.remove(two_pi) + self.animate(BlinkPiCreature(tau)) + self.dither() + + def line14(self): + pi, bubble = self.pi_speaking( + "Wait! Area \\\\ of circles" + ) + self.add(pi) + self.animate( + Transform(Point(bubble.tip).highlight("black"), bubble) + ) + + def line15(self): + pi, bubble = self.pi_speaking( + "sticks oddly \\\\ to one half when \\\\ tau's preferred." + ) + formula = form_start, half, form_end = tex_mobject([ + "A = ", + "\\frac{1}{2}", + "\\tau r^2" + ]).split() + + self.add(pi, bubble, *formula) + self.dither(2) + self.animate(ApplyMethod(half.highlight, "yellow")) + + def line16(self): + self.add(tex_mobject( + "\\frac{1}{2}\\tau r^2" + ).scale(2).shift(DOWN)) + + def line17(self): + circle = Dot( + radius = 1, + density = 4*DEFAULT_POINT_DENSITY_1D + ) + blue_rgb = np.array(Color("blue").get_rgb()) + white_rgb = np.ones(3) + circle.rgbs = np.array([ + alpha * blue_rgb + (1 - alpha) * white_rgb + for alpha in np.arange(0, 1, 1.0/len(circle.rgbs)) + ]) + for index in range(circle.points.shape[0]): + circle.rgbs + def trianglify((x, y, z)): + norm = np.linalg.norm((x, y, z)) + comp = complex(x, y)*complex(0, 1) + return ( + norm * np.log(comp).imag, + -norm, + 0 + ) + tau_r = tex_mobject("\\tau r").shift(1.3*DOWN) + r = tex_mobject("r").shift(0.2*RIGHT + 0.7*DOWN) + lines = [ + Line(DOWN+np.pi*LEFT, DOWN+np.pi*RIGHT), + Line(ORIGIN, DOWN) + ] + for line in lines: + line.highlight("red") + + self.animate(ApplyFunction(trianglify, circle, run_time = 2.0)) + self.add(tau_r, r) + self.animate(*[ + ShowCreation(line, run_time = 1.0) + for line in lines + ]) + self.dither() + + def line18(self): + tau = TauCreature() + tau.shift_eyes() + tau.move_to(DOWN) + pi = PiCreature() + pi.highlight("red") + pi.move_to(DOWN + 3*LEFT) + mad_tau = deepcopy(tau).make_mean() + mad_tau.arm.wag(0.5*UP, LEFT, 2.0) + sad_pi = deepcopy(pi).shift_eyes().make_sad() + blinked_tau = deepcopy(tau).blink() + blinked_pi = deepcopy(pi).blink() + + self.add(*pi.parts + tau.parts) + self.dither(0.8) + self.animate(*[ + Transform(*eyes, run_time = 0.2, alpha_func = rush_into) + for eyes in [ + (tau.left_eye, blinked_tau.left_eye), + (tau.right_eye, blinked_tau.right_eye), + ] + ]) + self.remove(tau.left_eye, tau.right_eye) + self.animate(*[ + Transform(*eyes, run_time = 0.2, alpha_func = rush_from) + for eyes in [ + (blinked_tau.left_eye, mad_tau.left_eye), + (blinked_tau.right_eye, mad_tau.right_eye), + ] + ]) + self.remove(blinked_tau.left_eye, blinked_tau.right_eye) + self.add(mad_tau.left_eye, mad_tau.right_eye) + self.animate( + Transform(tau.arm, mad_tau.arm), + Transform(tau.mouth, mad_tau.mouth), + run_time = 0.5 + ) + self.remove(*tau.parts + blinked_tau.parts) + self.add(*mad_tau.parts) + + self.animate(*[ + Transform(*eyes, run_time = 0.2, alpha_func = rush_into) + for eyes in [ + (pi.left_eye, blinked_pi.left_eye), + (pi.right_eye, blinked_pi.right_eye), + ] + ]) + self.remove(pi.left_eye, pi.right_eye) + self.animate(*[ + Transform(*eyes, run_time = 0.2, alpha_func = rush_from) + for eyes in [ + (blinked_pi.left_eye, sad_pi.left_eye), + (blinked_pi.right_eye, sad_pi.right_eye), + ] + ] + [ + Transform(pi.mouth, sad_pi.mouth, run_time = 0.2) + ]) + self.remove(*blinked_pi.parts + pi.parts + sad_pi.parts) + self.animate( + WalkPiCreature(sad_pi, DOWN+4*LEFT), + run_time = 1.0 + ) + self.dither() + + def line19(self): + pass + + +if __name__ == "__main__": + command_line_create_scene(MOVIE_PREFIX) + + + + + + + + + + + + +