├── .gitignore ├── README.md ├── __init__.py ├── animation ├── __init__.py ├── animation.py ├── playground.py ├── simple_animations.py └── transform.py ├── camera.py ├── constants.py ├── eola ├── __init__.py ├── chapter0.py ├── chapter1.py ├── chapter10.py ├── chapter2.py ├── chapter3.py ├── chapter4.py ├── chapter5.py ├── chapter6.py ├── chapter7.py ├── chapter8.py ├── chapter8p2.py ├── chapter9.py ├── footnote.py ├── footnote2.py ├── matrix.py ├── thumbnails.py └── two_d_space.py ├── example_scenes.py ├── extract_scene.py ├── helpers.py ├── mobject ├── __init__.py ├── image_mobject.py ├── mobject.py ├── point_cloud_mobject.py ├── region.py ├── svg_mobject.py ├── tex_mobject.py └── vectorized_mobject.py ├── old_projects ├── __init__.py ├── brachistochrone │ ├── __init__.py │ ├── curves.py │ ├── cycloid.py │ ├── drawing_images.py │ ├── graveyard.py │ ├── light.py │ ├── misc.py │ ├── multilayered.py │ └── wordplay.py ├── complex_multiplication_article.py ├── counting_in_binary.py ├── eulers_characteristic_formula.py ├── generate_logo.py ├── hilbert │ ├── __init__.py │ ├── fractal_porn.py │ ├── section1.py │ ├── section2.py │ └── section3.py ├── inventing_math.py ├── inventing_math_images.py ├── matrix_as_transform_2d.py ├── moser_intro.py ├── moser_main.py ├── music_and_measure.py ├── number_line_scene.py ├── playground_counting_in_binary.py ├── pythagorean_proof.py ├── tau_poem.py └── triangle_of_power │ ├── __init__.py │ ├── end.py │ ├── intro.py │ └── triangle.py ├── requirements.txt ├── scene ├── __init__.py ├── scene.py ├── scene_from_video.py ├── test.py ├── tk_scene.py └── zoomed_scene.py ├── stage_animations.py ├── template.tex ├── text_template.tex └── topics ├── __init__.py ├── arithmetic.py ├── characters.py ├── combinatorics.py ├── complex_numbers.py ├── fractals.py ├── functions.py ├── geometry.py ├── graph_theory.py ├── number_line.py ├── numerals.py └── three_dimensions.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store 3 | homeless.py 4 | ka_playgrounds/ 5 | playground.py 6 | prettiness_hall_of_fame.py 7 | files/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # animations 2 | Animation engine for explanatory math videos 3 | 4 | For those who want to play around with this tool, I should be upfront that I've mostly had my own use cases (i.e. 3b1b videos) in mind while building it, and it might not be the most friendly thing to get up and running. In particular, I have not done a great job tracking requirements, and documentation, to put it euphamistically, almost exclusively takes the form of naming conventions. 5 | 6 | For 9/10 math animation needs, you'd probably be better off using a more well-maintained tool, like mathplotlib or mathematica. I happen to think the program "Grapher" built into osx is really great, and surprisingly versatile for many needs. My own reasons for building this tool and using it for videos are twofold, and I'm not sure how well they apply to other people's use cases. 7 | 8 | 1) If I wish to work with some new type of mathematical thing (e.g. a fractal), or to experiment with a different type of animation, it's easier to work it into the underlying system and manipulate it the same way as more standard objects/animation. Admittedly, though, part of the reason I find this easier is because I'm more familiar with the underlying system here than I am with others. This keeps me from shying away from certain video topics that I would otherwise have no idea how to animate. 9 | 10 | 2) Having my own tool has been a forcing function for having a style which is more original than what I may have otherwise produced. The cost of this was slower video production when the tool was in its early days, and during points when I do some kind of rehaul, but I think the artistic benefit is a real one. If you wish to use this tool and adopt the same style, by all means feel free. In fact, I encourage it. But the tricky part about anything which confers the benefit of originality is that this benefit cannot be easily shared. 11 | 12 | 13 | ## Install requirements 14 | ```sh 15 | pip install -r requirements.txt 16 | ``` 17 | 18 | Requirements to be installed outside of pip: 19 | aggdraw 20 | ffmpeg 21 | latex 22 | 23 | Here are directions that should work on any 64 bit platform (tested on osx 24 | 10.11.4) 25 | 26 | This doesn't install freetype, but I don't think it's required for this project 27 | 28 | ``` 29 | cd $TMPDIR 30 | git clone https://github.com/scottopell/aggdraw-64bits 31 | cd aggdraw-64bits 32 | /usr/local/bin/python setup.py build_ext -i 33 | /usr/local/bin/python setup.py install 34 | ``` 35 | 36 | ## How to Use 37 | Try running the following: 38 | 39 | python extract_scene.py -p example_scenes.py SquareToCircle 40 | 41 | -p gives a preview of an animation, -w will write it to a file, and -s will show/save the final image in the animation. 42 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /animation/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "animation", 3 | "meta_animations", 4 | "simple_animations", 5 | "transform" 6 | ] 7 | 8 | from animation import Animation 9 | -------------------------------------------------------------------------------- /animation/animation.py: -------------------------------------------------------------------------------- 1 | from PIL import Image 2 | from colour import Color 3 | import numpy as np 4 | import warnings 5 | import time 6 | import os 7 | import progressbar 8 | import inspect 9 | from copy import deepcopy 10 | 11 | from helpers import * 12 | from mobject import Mobject 13 | 14 | class Animation(object): 15 | CONFIG = { 16 | "run_time" : DEFAULT_ANIMATION_RUN_TIME, 17 | "rate_func" : smooth, 18 | "name" : None, 19 | #Does this animation add or remove a mobject form the screen 20 | "remover" : False, 21 | #Options are lagged_start, smoothed_lagged_start, 22 | #one_at_a_time, all_at_once 23 | "submobject_mode" : "all_at_once", 24 | "lag_factor" : 2, 25 | } 26 | def __init__(self, mobject, **kwargs): 27 | mobject = instantiate(mobject) 28 | assert(isinstance(mobject, Mobject)) 29 | digest_config(self, kwargs, locals()) 30 | self.starting_mobject = self.mobject.copy() 31 | if self.rate_func is None: 32 | self.rate_func = (lambda x : x) 33 | if self.name is None: 34 | self.name = self.__class__.__name__ + str(self.mobject) 35 | self.update(0) 36 | 37 | def update_config(self, **kwargs): 38 | digest_config(self, kwargs) 39 | if "rate_func" in kwargs and kwargs["rate_func"] is None: 40 | self.rate_func = (lambda x : x) 41 | return self 42 | 43 | def __str__(self): 44 | return self.name 45 | 46 | def copy(self): 47 | return deepcopy(self) 48 | 49 | def update(self, alpha): 50 | if alpha < 0: 51 | alpha = 0.0 52 | if alpha > 1: 53 | alpha = 1.0 54 | self.update_mobject(self.rate_func(alpha)) 55 | 56 | def update_mobject(self, alpha): 57 | families = self.get_all_families_zipped() 58 | for i, mobs in enumerate(families): 59 | sub_alpha = self.get_sub_alpha(alpha, i, len(families)) 60 | self.update_submobject(*list(mobs) + [sub_alpha]) 61 | return self 62 | 63 | def update_submobject(self, submobject, starting_sumobject, alpha): 64 | #Typically ipmlemented by subclass 65 | pass 66 | 67 | def get_all_families_zipped(self): 68 | """ 69 | Ordering must match the ording of arguments to update_submobject 70 | """ 71 | return zip( 72 | self.mobject.submobject_family(), 73 | self.starting_mobject.submobject_family() 74 | ) 75 | 76 | def get_sub_alpha(self, alpha, index, num_submobjects): 77 | if self.submobject_mode in ["lagged_start", "smoothed_lagged_start"]: 78 | prop = float(index)/num_submobjects 79 | if self.submobject_mode is "smoothed_lagged_start": 80 | prop = smooth(prop) 81 | lf = self.lag_factor 82 | return np.clip(lf*alpha - (lf-1)*prop, 0, 1) 83 | elif self.submobject_mode == "one_at_a_time": 84 | lower = float(index)/num_submobjects 85 | upper = float(index+1)/num_submobjects 86 | return np.clip((alpha-lower)/(upper-lower), 0, 1) 87 | elif self.submobject_mode == "all_at_once": 88 | return alpha 89 | raise Exception("Invalid submobject mode") 90 | 91 | def filter_out(self, *filter_functions): 92 | self.filter_functions += filter_functions 93 | return self 94 | 95 | def set_run_time(self, time): 96 | self.run_time = time 97 | return self 98 | 99 | def get_run_time(self): 100 | return self.run_time 101 | 102 | def set_rate_func(self, rate_func): 103 | if rate_func is None: 104 | rate_func = lambda x : x 105 | self.rate_func = rate_func 106 | return self 107 | 108 | def get_rate_func(self): 109 | return self.rate_func 110 | 111 | def set_name(self, name): 112 | self.name = name 113 | return self 114 | 115 | def is_remover(self): 116 | return self.remover 117 | 118 | def clean_up(self): 119 | self.update(1) 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /animation/playground.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import operator as op 3 | 4 | from animation import Animation 5 | from transform import Transform 6 | from mobject import Mobject1D, Mobject 7 | from topics.geometry import Line 8 | 9 | from helpers import * 10 | 11 | class Vibrate(Animation): 12 | CONFIG = { 13 | "spatial_period" : 6, 14 | "temporal_period" : 1, 15 | "overtones" : 4, 16 | "amplitude" : 0.5, 17 | "radius" : SPACE_WIDTH/2, 18 | "run_time" : 3.0, 19 | "rate_func" : None 20 | } 21 | def __init__(self, mobject = None, **kwargs): 22 | if mobject is None: 23 | mobject = Line(3*LEFT, 3*RIGHT) 24 | Animation.__init__(self, mobject, **kwargs) 25 | 26 | def wave_function(self, x, t): 27 | return sum([ 28 | reduce(op.mul, [ 29 | self.amplitude/(k**2), #Amplitude 30 | np.sin(2*np.pi*(k**1.5)*t/self.temporal_period), #Frequency 31 | np.sin(2*np.pi*k*x/self.spatial_period) #Number of waves 32 | ]) 33 | for k in range(1, self.overtones+1) 34 | ]) 35 | 36 | 37 | def update_mobject(self, alpha): 38 | time = alpha*self.run_time 39 | families = map( 40 | Mobject.submobject_family, 41 | [self.mobject, self.starting_mobject] 42 | ) 43 | for mob, start in zip(*families): 44 | mob.points = np.apply_along_axis( 45 | lambda (x, y, z) : (x, y + self.wave_function(x, time), z), 46 | 1, start.points 47 | ) 48 | 49 | 50 | class TurnInsideOut(Transform): 51 | CONFIG = { 52 | "path_func" : path_along_arc(np.pi/2) 53 | } 54 | def __init__(self, mobject, **kwargs): 55 | mobject.sort_points(np.linalg.norm) 56 | mob_copy = mobject.copy() 57 | mob_copy.sort_points(lambda p : -np.linalg.norm(p)) 58 | Transform.__init__(self, mobject, mob_copy, **kwargs) 59 | 60 | 61 | -------------------------------------------------------------------------------- /animation/simple_animations.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | 4 | from helpers import * 5 | 6 | from mobject import Mobject 7 | from mobject.vectorized_mobject import VMobject 8 | from mobject.tex_mobject import TextMobject 9 | from animation import Animation 10 | 11 | 12 | class Rotating(Animation): 13 | CONFIG = { 14 | "axes" : [RIGHT, UP], 15 | "axis" : None, 16 | "radians" : 2*np.pi, 17 | "run_time" : 20.0, 18 | "rate_func" : None, 19 | "in_place" : True, 20 | } 21 | def update_submobject(self, submobject, starting_mobject, alpha): 22 | submobject.points = starting_submobject.points 23 | 24 | def update_mobject(self, alpha): 25 | Animation.update_mobject(self, alpha) 26 | axes = [self.axis] if self.axis is not None else self.axes 27 | if self.in_place: 28 | method = self.mobject.rotate_in_place 29 | else: 30 | method = self.mobject.rotate 31 | method(alpha*self.radians, axes = axes) 32 | 33 | 34 | class ShowPartial(Animation): 35 | def update_submobject(self, submobject, starting_submobject, alpha): 36 | submobject.pointwise_become_partial( 37 | starting_submobject, *self.get_bounds(alpha) 38 | ) 39 | 40 | def get_bounds(self, alpha): 41 | raise Exception("Not Implemented") 42 | 43 | 44 | class ShowCreation(ShowPartial): 45 | CONFIG = { 46 | "submobject_mode" : "one_at_a_time", 47 | } 48 | def get_bounds(self, alpha): 49 | return (0, alpha) 50 | 51 | class Uncreate(ShowCreation): 52 | CONFIG = { 53 | "rate_func" : lambda t : smooth(1-t), 54 | "remover" : True 55 | } 56 | 57 | class Write(ShowCreation): 58 | CONFIG = { 59 | "rate_func" : None, 60 | "submobject_mode" : "lagged_start", 61 | } 62 | def __init__(self, mob_or_text, **kwargs): 63 | digest_config(self, kwargs) 64 | if isinstance(mob_or_text, str): 65 | mobject = TextMobject(mob_or_text) 66 | else: 67 | mobject = mob_or_text 68 | if "run_time" not in kwargs: 69 | self.establish_run_time(mobject) 70 | if "lag_factor" not in kwargs: 71 | self.lag_factor = max(self.run_time - 1, 1) 72 | ShowCreation.__init__(self, mobject, **kwargs) 73 | 74 | def establish_run_time(self, mobject): 75 | num_subs = len(mobject.family_members_with_points()) 76 | if num_subs < 5: 77 | self.run_time = 1 78 | elif num_subs < 15: 79 | self.run_time = 2 80 | else: 81 | self.run_time = 3 82 | 83 | 84 | class ShowPassingFlash(ShowPartial): 85 | CONFIG = { 86 | "time_width" : 0.1 87 | } 88 | def get_bounds(self, alpha): 89 | alpha *= (1+self.time_width) 90 | alpha -= self.time_width/2 91 | lower = max(0, alpha - self.time_width/2) 92 | upper = min(1, alpha + self.time_width/2) 93 | return (lower, upper) 94 | 95 | 96 | class MoveAlongPath(Animation): 97 | def __init__(self, mobject, vmobject, **kwargs): 98 | digest_config(self, kwargs, locals()) 99 | Animation.__init__(self, mobject, **kwargs) 100 | 101 | def update_mobject(self, alpha): 102 | self.mobject.shift( 103 | self.vmobject.point_from_proportion(alpha) - \ 104 | self.mobject.get_center() 105 | ) 106 | 107 | class Homotopy(Animation): 108 | CONFIG = { 109 | "run_time" : 3 110 | } 111 | def __init__(self, homotopy, mobject, **kwargs): 112 | """ 113 | Homotopy a function from (x, y, z, t) to (x', y', z') 114 | """ 115 | def function_at_time_t(t): 116 | return lambda p : homotopy(p[0], p[1], p[2], t) 117 | self.function_at_time_t = function_at_time_t 118 | digest_config(self, kwargs) 119 | Animation.__init__(self, mobject, **kwargs) 120 | 121 | def update_submobject(self, submob, start, alpha): 122 | submob.points = start.points 123 | submob.apply_function(self.function_at_time_t(alpha)) 124 | 125 | def update_mobject(self, alpha): 126 | Animation.update_mobject(self, alpha) 127 | 128 | 129 | 130 | class PhaseFlow(Animation): 131 | CONFIG = { 132 | "virtual_time" : 1, 133 | "rate_func" : None, 134 | } 135 | def __init__(self, function, mobject, **kwargs): 136 | digest_config(self, kwargs, locals()) 137 | Animation.__init__(self, mobject, **kwargs) 138 | 139 | def update_mobject(self, alpha): 140 | if hasattr(self, "last_alpha"): 141 | dt = self.virtual_time*(alpha-self.last_alpha) 142 | self.mobject.apply_function( 143 | lambda p : p + dt*self.function(p) 144 | ) 145 | self.last_alpha = alpha 146 | 147 | class MoveAlongPath(Animation): 148 | def __init__(self, mobject, path, **kwargs): 149 | digest_config(self, kwargs, locals()) 150 | Animation.__init__(self, mobject, **kwargs) 151 | 152 | def update_mobject(self, alpha): 153 | n = self.path.get_num_points()-1 154 | point = self.path.points[int(alpha*n)] 155 | self.mobject.shift(point-self.mobject.get_center()) 156 | 157 | class UpdateFromFunc(Animation): 158 | """ 159 | update_function of the form func(mobject), presumably 160 | to be used when the state of one mobject is dependent 161 | on another simultaneously animated mobject 162 | """ 163 | def __init__(self, mobject, update_function, **kwargs): 164 | digest_config(self, kwargs, locals()) 165 | Animation.__init__(self, mobject, **kwargs) 166 | 167 | def update_mobject(self, alpha): 168 | self.update_function(self.mobject) 169 | 170 | 171 | class MaintainPositionRelativeTo(Animation): 172 | CONFIG = { 173 | "tracked_critical_point" : ORIGIN 174 | } 175 | def __init__(self, mobject, tracked_mobject, **kwargs): 176 | digest_config(self, kwargs, locals()) 177 | tcp = self.tracked_critical_point 178 | self.diff = mobject.get_critical_point(tcp) - \ 179 | tracked_mobject.get_critical_point(tcp) 180 | Animation.__init__(self, mobject, **kwargs) 181 | 182 | def update_mobject(self, alpha): 183 | self.mobject.shift( 184 | self.tracked_mobject.get_critical_point(self.tracked_critical_point) - \ 185 | self.mobject.get_critical_point(self.tracked_critical_point) + \ 186 | self.diff 187 | ) 188 | 189 | 190 | ### Animation modifiers ### 191 | 192 | class ApplyToCenters(Animation): 193 | def __init__(self, AnimationClass, mobjects, **kwargs): 194 | full_kwargs = AnimationClass.CONFIG 195 | full_kwargs.update(kwargs) 196 | full_kwargs["mobject"] = Mobject(*[ 197 | mob.get_point_mobject() 198 | for mob in mobjects 199 | ]) 200 | self.centers_container = AnimationClass(**full_kwargs) 201 | full_kwargs.pop("mobject") 202 | Animation.__init__(self, Mobject(*mobjects), **full_kwargs) 203 | self.name = str(self) + AnimationClass.__name__ 204 | 205 | def update_mobject(self, alpha): 206 | self.centers_container.update_mobject(alpha) 207 | center_mobs = self.centers_container.mobject.split() 208 | mobjects = self.mobject.split() 209 | for center_mob, mobject in zip(center_mobs, mobjects): 210 | mobject.shift( 211 | center_mob.get_center()-mobject.get_center() 212 | ) 213 | 214 | 215 | 216 | class DelayByOrder(Animation): 217 | """ 218 | Modifier of animation. 219 | 220 | Warning: This will not work on all animation types. 221 | """ 222 | CONFIG = { 223 | "max_power" : 5 224 | } 225 | def __init__(self, animation, **kwargs): 226 | digest_locals(self) 227 | self.num_mobject_points = animation.mobject.get_num_points() 228 | kwargs.update(dict([ 229 | (attr, getattr(animation, attr)) 230 | for attr in Animation.CONFIG 231 | ])) 232 | Animation.__init__(self, animation.mobject, **kwargs) 233 | self.name = str(self) + str(self.animation) 234 | 235 | def update_mobject(self, alpha): 236 | dim = self.mobject.DIM 237 | alpha_array = np.array([ 238 | [alpha**power]*dim 239 | for n in range(self.num_mobject_points) 240 | for prop in [(n+1.0)/self.num_mobject_points] 241 | for power in [1+prop*(self.max_power-1)] 242 | ]) 243 | self.animation.update_mobject(alpha_array) 244 | 245 | 246 | class Succession(Animation): 247 | def __init__(self, *animations, **kwargs): 248 | if "run_time" in kwargs: 249 | run_time = kwargs.pop("run_time") 250 | else: 251 | run_time = sum([anim.run_time for anim in animations]) 252 | self.num_anims = len(animations) 253 | self.anims = animations 254 | mobject = animations[0].mobject 255 | Animation.__init__(self, mobject, run_time = run_time, **kwargs) 256 | 257 | def __str__(self): 258 | return self.__class__.__name__ + \ 259 | "".join(map(str, self.anims)) 260 | 261 | def update(self, alpha): 262 | scaled_alpha = alpha*self.num_anims 263 | self.mobject = self.anims 264 | for index in range(len(self.anims)): 265 | self.anims[index].update(scaled_alpha - index) 266 | 267 | 268 | -------------------------------------------------------------------------------- /animation/transform.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | import inspect 4 | import copy 5 | import warnings 6 | 7 | from helpers import * 8 | 9 | from animation import Animation 10 | from simple_animations import DelayByOrder 11 | from mobject import Mobject, Point, VMobject, Group 12 | 13 | class Transform(Animation): 14 | CONFIG = { 15 | "path_arc" : 0, 16 | "path_func" : None, 17 | "submobject_mode" : "all_at_once", 18 | } 19 | def __init__(self, mobject, ending_mobject, **kwargs): 20 | #Copy ending_mobject so as to not mess with caller 21 | ending_mobject = ending_mobject.copy() 22 | digest_config(self, kwargs, locals()) 23 | mobject.align_data(ending_mobject) 24 | self.init_path_func() 25 | 26 | Animation.__init__(self, mobject, **kwargs) 27 | self.name += "To" + str(ending_mobject) 28 | self.mobject.stroke_width = ending_mobject.stroke_width 29 | 30 | def update_config(self, **kwargs): 31 | Animation.update_config(self, **kwargs) 32 | if "path_arc" in kwargs: 33 | self.path_func = path_along_arc(kwargs["path_arc"]) 34 | 35 | def init_path_func(self): 36 | if self.path_func is not None: 37 | return 38 | if self.path_arc == 0: 39 | self.path_func = straight_path 40 | else: 41 | self.path_func = path_along_arc(self.path_arc) 42 | 43 | def get_all_families_zipped(self): 44 | return zip(*map( 45 | Mobject.submobject_family, 46 | [self.mobject, self.starting_mobject, self.ending_mobject] 47 | )) 48 | 49 | def update_submobject(self, submob, start, end, alpha): 50 | submob.interpolate(start, end, alpha, self.path_func) 51 | return self 52 | 53 | 54 | class ClockwiseTransform(Transform): 55 | CONFIG = { 56 | "path_arc" : -np.pi 57 | } 58 | 59 | class CounterclockwiseTransform(Transform): 60 | CONFIG = { 61 | "path_arc" : np.pi 62 | } 63 | 64 | class MoveToTarget(Transform): 65 | def __init__(self, mobject, **kwargs): 66 | if not hasattr(mobject, "target"): 67 | raise Exception("MoveToTarget called on mobject without attribute 'target' ") 68 | Transform.__init__(self, mobject, mobject.target, **kwargs) 69 | 70 | class CyclicReplace(Transform): 71 | CONFIG = { 72 | "path_arc" : np.pi/2 73 | } 74 | def __init__(self, *mobjects, **kwargs): 75 | start = Group(*mobjects) 76 | target = Group(*[ 77 | m1.copy().move_to(m2) 78 | for m1, m2 in adjascent_pairs(start) 79 | ]) 80 | Transform.__init__(self, start, target, **kwargs) 81 | 82 | class Swap(CyclicReplace): 83 | pass #Renaming, more understandable for two entries 84 | 85 | class GrowFromCenter(Transform): 86 | def __init__(self, mobject, **kwargs): 87 | target = mobject.copy() 88 | point = Point(mobject.get_center()) 89 | mobject.replace(point) 90 | mobject.highlight(point.get_color()) 91 | Transform.__init__(self, mobject, target, **kwargs) 92 | 93 | class SpinInFromNothing(GrowFromCenter): 94 | CONFIG = { 95 | "path_func" : counterclockwise_path() 96 | } 97 | 98 | class ShrinkToCenter(Transform): 99 | def __init__(self, mobject, **kwargs): 100 | Transform.__init__( 101 | self, mobject, 102 | Point(mobject.get_center()), 103 | **kwargs 104 | ) 105 | 106 | class ApplyMethod(Transform): 107 | CONFIG = { 108 | "submobject_mode" : "all_at_once" 109 | } 110 | def __init__(self, method, *args, **kwargs): 111 | """ 112 | Method is a method of Mobject. *args is for the method, 113 | **kwargs is for the transform itself. 114 | 115 | Relies on the fact that mobject methods return the mobject 116 | """ 117 | if not inspect.ismethod(method): 118 | raise Exception( 119 | "Whoops, looks like you accidentally invoked " + \ 120 | "the method you want to animate" 121 | ) 122 | assert(isinstance(method.im_self, Mobject)) 123 | target = copy.deepcopy(method)(*args) 124 | Transform.__init__(self, method.im_self, target, **kwargs) 125 | 126 | class FadeOut(Transform): 127 | CONFIG = { 128 | "remover" : True, 129 | } 130 | def __init__(self, mobject, **kwargs): 131 | target = mobject.copy() 132 | target.fade(1) 133 | if isinstance(mobject, VMobject): 134 | target.set_stroke(width = 0) 135 | target.set_fill(opacity = 0) 136 | Transform.__init__(self, mobject, target, **kwargs) 137 | 138 | def clean_up(self): 139 | self.update(0) 140 | 141 | class FadeIn(Transform): 142 | def __init__(self, mobject, **kwargs): 143 | target = mobject.copy() 144 | Transform.__init__(self, mobject, target, **kwargs) 145 | self.starting_mobject.fade(1) 146 | if isinstance(self.starting_mobject, VMobject): 147 | self.starting_mobject.set_stroke(width = 0) 148 | self.starting_mobject.set_fill(opacity = 0) 149 | 150 | 151 | class ShimmerIn(DelayByOrder): 152 | def __init__(self, mobject, **kwargs): 153 | mobject.sort_points(lambda p : np.dot(p, DOWN+RIGHT)) 154 | DelayByOrder.__init__(self, FadeIn(mobject, **kwargs)) 155 | 156 | 157 | class Rotate(ApplyMethod): 158 | CONFIG = { 159 | "in_place" : False, 160 | } 161 | def __init__(self, mobject, angle = np.pi, axis = OUT, **kwargs): 162 | if "path_arc" not in kwargs: 163 | kwargs["path_arc"] = angle 164 | digest_config(self, kwargs, locals()) 165 | if self.in_place: 166 | method = mobject.rotate_in_place 167 | else: 168 | method = mobject.rotate 169 | ApplyMethod.__init__(self, method, angle, axis, **kwargs) 170 | 171 | 172 | class ApplyPointwiseFunction(ApplyMethod): 173 | CONFIG = { 174 | "run_time" : DEFAULT_POINTWISE_FUNCTION_RUN_TIME 175 | } 176 | def __init__(self, function, mobject, **kwargs): 177 | ApplyMethod.__init__( 178 | self, mobject.apply_function, function, **kwargs 179 | ) 180 | 181 | class FadeToColor(ApplyMethod): 182 | def __init__(self, mobject, color, **kwargs): 183 | ApplyMethod.__init__(self, mobject.highlight, color, **kwargs) 184 | 185 | class ScaleInPlace(ApplyMethod): 186 | def __init__(self, mobject, scale_factor, **kwargs): 187 | ApplyMethod.__init__(self, mobject.scale_in_place, scale_factor, **kwargs) 188 | 189 | class ApplyFunction(Transform): 190 | CONFIG = { 191 | "submobject_mode" : "all_at_once", 192 | } 193 | def __init__(self, function, mobject, **kwargs): 194 | Transform.__init__( 195 | self, 196 | mobject, 197 | function(mobject.copy()), 198 | **kwargs 199 | ) 200 | self.name = "ApplyFunctionTo"+str(mobject) 201 | 202 | class ApplyMatrix(ApplyPointwiseFunction): 203 | #Truth be told, I'm not sure if this is useful. 204 | def __init__(self, matrix, mobject, **kwargs): 205 | matrix = np.array(matrix) 206 | if matrix.shape == (2, 2): 207 | new_matrix = np.identity(3) 208 | new_matrix[:2, :2] = matrix 209 | matrix = new_matrix 210 | elif matrix.shape != (3, 3): 211 | raise "Matrix has bad dimensions" 212 | transpose = np.transpose(matrix) 213 | def func(p): 214 | return np.dot(p, transpose) 215 | ApplyPointwiseFunction.__init__(self, func, mobject, **kwargs) 216 | 217 | 218 | class TransformAnimations(Transform): 219 | CONFIG = { 220 | "rate_func" : squish_rate_func(smooth) 221 | } 222 | def __init__(self, start_anim, end_anim, **kwargs): 223 | digest_config(self, kwargs, locals()) 224 | if "run_time" in kwargs: 225 | self.run_time = kwargs.pop("run_time") 226 | else: 227 | self.run_time = max(start_anim.run_time, end_anim.run_time) 228 | for anim in start_anim, end_anim: 229 | anim.set_run_time(self.run_time) 230 | 231 | if start_anim.starting_mobject.get_num_points() != end_anim.starting_mobject.get_num_points(): 232 | start_anim.starting_mobject.align_data(end_anim.starting_mobject) 233 | for anim in start_anim, end_anim: 234 | if hasattr(anim, "ending_mobject"): 235 | anim.starting_mobject.align_data(anim.ending_mobject) 236 | 237 | Transform.__init__(self, start_anim.mobject, end_anim.mobject, **kwargs) 238 | #Rewire starting and ending mobjects 239 | start_anim.mobject = self.starting_mobject 240 | end_anim.mobject = self.ending_mobject 241 | 242 | def update(self, alpha): 243 | self.start_anim.update(alpha) 244 | self.end_anim.update(alpha) 245 | Transform.update(self, alpha) 246 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /camera.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | import os 4 | 5 | from PIL import Image 6 | from colour import Color 7 | import aggdraw 8 | 9 | from helpers import * 10 | from mobject import PMobject, VMobject 11 | 12 | class Camera(object): 13 | CONFIG = { 14 | #background of a different shape will overwrite this 15 | "pixel_shape" : (DEFAULT_HEIGHT, DEFAULT_WIDTH), 16 | #this will be resized to match pixel_shape 17 | "space_shape" : (SPACE_HEIGHT, SPACE_WIDTH), 18 | "space_center" : ORIGIN, 19 | "background_color" : BLACK, 20 | } 21 | 22 | def __init__(self, background = None, **kwargs): 23 | digest_config(self, kwargs, locals()) 24 | self.init_background() 25 | self.resize_space_shape() 26 | self.reset() 27 | 28 | def resize_space_shape(self, fixed_dimension = 0): 29 | """ 30 | Changes space_shape to match the aspect ratio 31 | of pixel_shape, where fixed_dimension determines 32 | whether space_shape[0] (height) or space_shape[1] (width) 33 | remains fixed while the other changes accordingly. 34 | """ 35 | aspect_ratio = float(self.pixel_shape[1])/self.pixel_shape[0] 36 | space_height, space_width = self.space_shape 37 | if fixed_dimension == 0: 38 | space_width = aspect_ratio*space_height 39 | else: 40 | space_height = space_width/aspect_ratio 41 | self.space_shape = (space_height, space_width) 42 | 43 | def init_background(self): 44 | if self.background is not None: 45 | self.pixel_shape = self.background.shape[:2] 46 | else: 47 | background_rgb = color_to_int_rgb(self.background_color) 48 | self.background = np.zeros( 49 | list(self.pixel_shape)+[3], 50 | dtype = 'uint8' 51 | ) 52 | self.background[:,:] = background_rgb 53 | 54 | def get_image(self): 55 | return np.array(self.pixel_array) 56 | 57 | def set_image(self, pixel_array): 58 | self.pixel_array = np.array(pixel_array) 59 | 60 | def reset(self): 61 | self.set_image(np.array(self.background)) 62 | 63 | def capture_mobject(self, mobject): 64 | return self.capture_mobjects([mobject]) 65 | 66 | def capture_mobjects(self, mobjects, include_submobjects = True): 67 | if include_submobjects: 68 | mobjects = it.chain(*[ 69 | mob.family_members_with_points() 70 | for mob in mobjects 71 | ]) 72 | vmobjects = [] 73 | for mobject in mobjects: 74 | if isinstance(mobject, VMobject): 75 | vmobjects.append(mobject) 76 | elif isinstance(mobject, PMobject): 77 | self.display_point_cloud( 78 | mobject.points, mobject.rgbs, 79 | self.adjusted_thickness(mobject.stroke_width) 80 | ) 81 | #TODO, more? Call out if it's unknown? 82 | 83 | image = Image.fromarray(self.pixel_array, mode = "RGB") 84 | canvas = aggdraw.Draw(image) 85 | 86 | for vmobject in vmobjects: 87 | self.display_vectorized(vmobject, canvas) 88 | canvas.flush() 89 | self.pixel_array[:,:] = np.array(image) 90 | 91 | 92 | def display_region(self, region): 93 | (h, w) = self.pixel_shape 94 | scalar = 2*self.space_shape[0] / h 95 | xs = scalar*np.arange(-w/2, w/2)+self.space_center[0] 96 | ys = -scalar*np.arange(-h/2, h/2)+self.space_center[1] 97 | x_array = np.dot(np.ones((h, 1)), xs.reshape((1, w))) 98 | y_array = np.dot(ys.reshape(h, 1), np.ones((1, w))) 99 | covered = region.condition(x_array, y_array) 100 | rgb = np.array(Color(region.color).get_rgb()) 101 | rgb = (255*rgb).astype('uint8') 102 | self.pixel_array[covered] = rgb 103 | 104 | 105 | def display_vectorized(self, vmobject, canvas): 106 | if vmobject.is_subpath: 107 | #Subpath vectorized mobjects are taken care 108 | #of by their parent 109 | return 110 | pen, fill = self.get_pen_and_fill(vmobject) 111 | pathstring = self.get_pathstring(vmobject) 112 | symbol = aggdraw.Symbol(pathstring) 113 | canvas.symbol((0, 0), symbol, pen, fill) 114 | 115 | 116 | def get_pen_and_fill(self, vmobject): 117 | pen = aggdraw.Pen( 118 | vmobject.get_stroke_color().get_hex_l(), 119 | max(vmobject.stroke_width, 0) 120 | ) 121 | fill = aggdraw.Brush( 122 | vmobject.get_fill_color().get_hex_l(), 123 | opacity = int(255*vmobject.get_fill_opacity()) 124 | ) 125 | return (pen, fill) 126 | 127 | def get_pathstring(self, vmobject): 128 | result = "" 129 | for mob in [vmobject]+vmobject.get_subpath_mobjects(): 130 | points = mob.points 131 | if len(points) == 0: 132 | continue 133 | coords = self.points_to_pixel_coords(points) 134 | start = "M%d %d"%tuple(coords[0]) 135 | #(handle1, handle2, anchor) tripletes 136 | triplets = zip(*[ 137 | coords[i+1::3] 138 | for i in range(3) 139 | ]) 140 | cubics = [ 141 | "C" + " ".join(map(str, it.chain(*triplet))) 142 | for triplet in triplets 143 | ] 144 | end = "Z" if vmobject.mark_paths_closed else "" 145 | result += " ".join([start] + cubics + [end]) 146 | return result 147 | 148 | def display_point_cloud(self, points, rgbs, thickness): 149 | if len(points) == 0: 150 | return 151 | points = self.align_points_to_camera(points) 152 | pixel_coords = self.points_to_pixel_coords(points) 153 | pixel_coords = self.thickened_coordinates( 154 | pixel_coords, thickness 155 | ) 156 | 157 | rgbs = (255*rgbs).astype('uint8') 158 | target_len = len(pixel_coords) 159 | factor = target_len/len(rgbs) 160 | rgbs = np.array([rgbs]*factor).reshape((target_len, 3)) 161 | 162 | on_screen_indices = self.on_screen_pixels(pixel_coords) 163 | pixel_coords = pixel_coords[on_screen_indices] 164 | rgbs = rgbs[on_screen_indices] 165 | 166 | ph, pw = self.pixel_shape 167 | 168 | flattener = np.array([1, pw], dtype = 'int') 169 | flattener = flattener.reshape((2, 1)) 170 | indices = np.dot(pixel_coords, flattener)[:,0] 171 | indices = indices.astype('int') 172 | 173 | new_pa = self.pixel_array.reshape((ph*pw, 3)) 174 | new_pa[indices] = rgbs 175 | self.pixel_array = new_pa.reshape((ph, pw, 3)) 176 | 177 | 178 | def align_points_to_camera(self, points): 179 | ## This is where projection should live 180 | return points - self.space_center 181 | 182 | def points_to_pixel_coords(self, points): 183 | result = np.zeros((len(points), 2)) 184 | ph, pw = self.pixel_shape 185 | sh, sw = self.space_shape 186 | width_mult = pw/sw/2 187 | width_add = pw/2 188 | height_mult = ph/sh/2 189 | height_add = ph/2 190 | #Flip on y-axis as you go 191 | height_mult *= -1 192 | 193 | result[:,0] = points[:,0]*width_mult + width_add 194 | result[:,1] = points[:,1]*height_mult + height_add 195 | return result.astype('int') 196 | 197 | def on_screen_pixels(self, pixel_coords): 198 | return reduce(op.and_, [ 199 | pixel_coords[:,0] >= 0, 200 | pixel_coords[:,0] < self.pixel_shape[1], 201 | pixel_coords[:,1] >= 0, 202 | pixel_coords[:,1] < self.pixel_shape[0], 203 | ]) 204 | 205 | def adjusted_thickness(self, thickness): 206 | big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"] 207 | factor = sum(big_shape)/sum(self.pixel_shape) 208 | return 1 + (thickness-1)/factor 209 | 210 | def get_thickening_nudges(self, thickness): 211 | _range = range(-thickness/2+1, thickness/2+1) 212 | return np.array( 213 | list(it.product([0], _range))+ 214 | list(it.product(_range, [0])) 215 | ) 216 | 217 | def thickened_coordinates(self, pixel_coords, thickness): 218 | nudges = self.get_thickening_nudges(thickness) 219 | pixel_coords = np.array([ 220 | pixel_coords + nudge 221 | for nudge in nudges 222 | ]) 223 | size = pixel_coords.size 224 | return pixel_coords.reshape((size/2, 2)) 225 | 226 | 227 | 228 | class MovingCamera(Camera): 229 | """ 230 | Stays in line with the height, width and position 231 | of a given mobject 232 | """ 233 | CONFIG = { 234 | "aligned_dimension" : "width" #or height 235 | } 236 | def __init__(self, mobject, **kwargs): 237 | digest_locals(self) 238 | Camera.__init__(self, **kwargs) 239 | 240 | def capture_mobjects(self, *args, **kwargs): 241 | self.space_center = self.mobject.get_center() 242 | self.realign_space_shape() 243 | Camera.capture_mobjects(self, *args, **kwargs) 244 | 245 | def realign_space_shape(self): 246 | height, width = self.space_shape 247 | if self.aligned_dimension == "height": 248 | self.space_shape = (self.mobject.get_height()/2, width) 249 | else: 250 | self.space_shape = (height, self.mobject.get_width()/2) 251 | self.resize_space_shape( 252 | 0 if self.aligned_dimension == "height" else 1 253 | ) 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | import os 2 | import numpy as np 3 | 4 | DEFAULT_HEIGHT = 1080 5 | DEFAULT_WIDTH = 1920 6 | DEFAULT_FRAME_DURATION = 0.04 7 | 8 | #There might be other configuration than pixel_shape later... 9 | PRODUCTION_QUALITY_CAMERA_CONFIG = { 10 | "pixel_shape" : (DEFAULT_HEIGHT, DEFAULT_WIDTH), 11 | } 12 | 13 | MEDIUM_QUALITY_CAMERA_CONFIG = { 14 | "pixel_shape" : (720, 1280), 15 | } 16 | 17 | LOW_QUALITY_CAMERA_CONFIG = { 18 | "pixel_shape" : (480, 853), 19 | } 20 | 21 | 22 | DEFAULT_POINT_DENSITY_2D = 25 23 | DEFAULT_POINT_DENSITY_1D = 250 24 | 25 | DEFAULT_POINT_THICKNESS = 4 26 | 27 | #TODO, Make sure these are not needed 28 | SPACE_HEIGHT = 4.0 29 | SPACE_WIDTH = SPACE_HEIGHT * DEFAULT_WIDTH / DEFAULT_HEIGHT 30 | 31 | 32 | SMALL_BUFF = 0.1 33 | MED_BUFF = 0.25 34 | LARGE_BUFF = 1 35 | 36 | DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_BUFF 37 | DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = SMALL_BUFF 38 | 39 | 40 | #All in seconds 41 | DEFAULT_ANIMATION_RUN_TIME = 1.0 42 | DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0 43 | DEFAULT_DITHER_TIME = 1.0 44 | 45 | 46 | ORIGIN = np.array(( 0, 0, 0)) 47 | UP = np.array(( 0, 1, 0)) 48 | DOWN = np.array(( 0,-1, 0)) 49 | RIGHT = np.array(( 1, 0, 0)) 50 | LEFT = np.array((-1, 0, 0)) 51 | IN = np.array(( 0, 0,-1)) 52 | OUT = np.array(( 0, 0, 1)) 53 | 54 | TOP = SPACE_HEIGHT*UP 55 | BOTTOM = SPACE_HEIGHT*DOWN 56 | LEFT_SIDE = SPACE_WIDTH*LEFT 57 | RIGHT_SIDE = SPACE_WIDTH*RIGHT 58 | 59 | THIS_DIR = os.path.dirname(os.path.realpath(__file__)) 60 | FILE_DIR = os.path.join(THIS_DIR, "files") 61 | IMAGE_DIR = os.path.join(FILE_DIR, "images") 62 | GIF_DIR = os.path.join(FILE_DIR, "gifs") 63 | MOVIE_DIR = os.path.join(FILE_DIR, "movies") 64 | STAGED_SCENES_DIR = os.path.join(FILE_DIR, "staged_scenes") 65 | TEX_DIR = os.path.join(FILE_DIR, "Tex") 66 | TEX_IMAGE_DIR = os.path.join(IMAGE_DIR, "Tex") 67 | MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects") 68 | IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image") 69 | 70 | for folder in [FILE_DIR, IMAGE_DIR, GIF_DIR, MOVIE_DIR, TEX_DIR, 71 | TEX_IMAGE_DIR, MOBJECT_DIR, IMAGE_MOBJECT_DIR, 72 | STAGED_SCENES_DIR]: 73 | if not os.path.exists(folder): 74 | os.mkdir(folder) 75 | 76 | TEX_TEXT_TO_REPLACE = "YourTextHere" 77 | TEMPLATE_TEX_FILE = os.path.join(THIS_DIR, "template.tex") 78 | TEMPLATE_TEXT_FILE = os.path.join(THIS_DIR, "text_template.tex") 79 | 80 | 81 | LOGO_PATH = os.path.join(IMAGE_DIR, "logo.png") 82 | 83 | FFMPEG_BIN = "ffmpeg" 84 | 85 | 86 | ### Colors ### 87 | 88 | 89 | COLOR_MAP = { 90 | "DARK_BLUE" : "#236B8E", 91 | "DARK_BROWN" : "#8B4513", 92 | "LIGHT_BROWN" : "#CD853F", 93 | "BLUE_E" : "#1C758A", 94 | "BLUE_D" : "#29ABCA", 95 | "BLUE_C" : "#58C4DD", 96 | "BLUE_B" : "#9CDCEB", 97 | "BLUE_A" : "#C7E9F1", 98 | "TEAL_E" : "#49A88F", 99 | "TEAL_D" : "#55C1A7", 100 | "TEAL_C" : "#5CD0B3", 101 | "TEAL_B" : "#76DDC0", 102 | "TEAL_A" : "#ACEAD7", 103 | "GREEN_E" : "#699C52", 104 | "GREEN_D" : "#77B05D", 105 | "GREEN_C" : "#83C167", 106 | "GREEN_B" : "#A6CF8C", 107 | "GREEN_A" : "#C9E2AE", 108 | "YELLOW_E" : "#E8C11C", 109 | "YELLOW_D" : "#F4D345", 110 | "YELLOW_C" : "#FFFF00", 111 | "YELLOW_B" : "#FFEA94", 112 | "YELLOW_A" : "#FFF1B6", 113 | "GOLD_E" : "#C78D46", 114 | "GOLD_D" : "#E1A158", 115 | "GOLD_C" : "#F0AC5F", 116 | "GOLD_B" : "#F9B775", 117 | "GOLD_A" : "#F7C797", 118 | "RED_E" : "#CF5044", 119 | "RED_D" : "#E65A4C", 120 | "RED_C" : "#FC6255", 121 | "RED_B" : "#FF8080", 122 | "RED_A" : "#F7A1A3", 123 | "MAROON_E" : "#94424F", 124 | "MAROON_D" : "#A24D61", 125 | "MAROON_C" : "#C55F73", 126 | "MAROON_B" : "#EC92AB", 127 | "MAROON_A" : "#ECABC1", 128 | "PURPLE_E" : "#644172", 129 | "PURPLE_D" : "#715582", 130 | "PURPLE_C" : "#9A72AC", 131 | "PURPLE_B" : "#B189C6", 132 | "PURPLE_A" : "#CAA3E8", 133 | "WHITE" : "#FFFFFF", 134 | "BLACK" : "#000000", 135 | "GRAY" : "#888888", 136 | "GREY" : "#888888", 137 | "DARK_GREY" : "#444444", 138 | "DARK_GRAY" : "#444444", 139 | "GREY_BROWN" : "#736357", 140 | "PINK" : "#D147BD", 141 | "GREEN_SCREEN": "#00FF00", 142 | "ORANGE" : "#FF862F", 143 | } 144 | PALETTE = COLOR_MAP.values() 145 | locals().update(COLOR_MAP) 146 | for name in filter(lambda s : s.endswith("_C"), COLOR_MAP.keys()): 147 | locals()[name.replace("_C", "")] = locals()[name] 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /eola/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helpsypoo/manim/720f1e9683cd09968eeb84c6f69409d5241a74a2/eola/__init__.py -------------------------------------------------------------------------------- /eola/matrix.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from scene import Scene 4 | from mobject import Mobject 5 | from mobject.vectorized_mobject import VMobject, VGroup 6 | from mobject.tex_mobject import TexMobject, TextMobject 7 | from animation.transform import ApplyPointwiseFunction, Transform, \ 8 | ApplyMethod, FadeOut, ApplyFunction 9 | from animation.simple_animations import ShowCreation, Write 10 | from topics.number_line import NumberPlane, Axes 11 | from topics.geometry import Vector, Line, Circle, Arrow, Dot, BackgroundRectangle 12 | 13 | from helpers import * 14 | 15 | VECTOR_LABEL_SCALE_FACTOR = 0.8 16 | 17 | def matrix_to_tex_string(matrix): 18 | matrix = np.array(matrix).astype("string") 19 | if matrix.ndim == 1: 20 | matrix = matrix.reshape((matrix.size, 1)) 21 | n_rows, n_cols = matrix.shape 22 | prefix = "\\left[ \\begin{array}{%s}"%("c"*n_cols) 23 | suffix = "\\end{array} \\right]" 24 | rows = [ 25 | " & ".join(row) 26 | for row in matrix 27 | ] 28 | return prefix + " \\\\ ".join(rows) + suffix 29 | 30 | 31 | def matrix_to_mobject(matrix): 32 | return TexMobject(matrix_to_tex_string(matrix)) 33 | 34 | def vector_coordinate_label(vector_mob, integer_labels = True, 35 | n_dim = 2, color = WHITE): 36 | vect = np.array(vector_mob.get_end()) 37 | if integer_labels: 38 | vect = np.round(vect).astype(int) 39 | vect = vect[:n_dim] 40 | vect = vect.reshape((n_dim, 1)) 41 | label = Matrix(vect, add_background_rectangles = True) 42 | label.scale(VECTOR_LABEL_SCALE_FACTOR) 43 | 44 | shift_dir = np.array(vector_mob.get_end()) 45 | if shift_dir[0] >= 0: #Pointing right 46 | shift_dir -= label.get_left() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER*LEFT 47 | else: #Pointing left 48 | shift_dir -= label.get_right() + DEFAULT_MOBJECT_TO_MOBJECT_BUFFER*RIGHT 49 | label.shift(shift_dir) 50 | label.highlight(color) 51 | label.rect = BackgroundRectangle(label) 52 | label.add_to_back(label.rect) 53 | return label 54 | 55 | class Matrix(VMobject): 56 | CONFIG = { 57 | "v_buff" : 0.5, 58 | "h_buff" : 1, 59 | "add_background_rectangles" : False 60 | } 61 | def __init__(self, matrix, **kwargs): 62 | """ 63 | Matrix can either either include numbres, tex_strings, 64 | or mobjects 65 | """ 66 | VMobject.__init__(self, **kwargs) 67 | matrix = np.array(matrix) 68 | if matrix.ndim == 1: 69 | matrix = matrix.reshape((matrix.size, 1)) 70 | if not isinstance(matrix[0][0], Mobject): 71 | matrix = matrix.astype("string") 72 | matrix = self.string_matrix_to_mob_matrix(matrix) 73 | self.organize_mob_matrix(matrix) 74 | self.add(*matrix.flatten()) 75 | self.add_brackets() 76 | self.center() 77 | self.mob_matrix = matrix 78 | if self.add_background_rectangles: 79 | for mob in matrix.flatten(): 80 | mob.add_background_rectangle() 81 | 82 | def string_matrix_to_mob_matrix(self, matrix): 83 | return np.array([ 84 | map(TexMobject, row) 85 | for row in matrix 86 | ]) 87 | 88 | def organize_mob_matrix(self, matrix): 89 | for i, row in enumerate(matrix): 90 | for j, elem in enumerate(row): 91 | mob = matrix[i][j] 92 | if i == 0 and j == 0: 93 | continue 94 | elif i == 0: 95 | mob.next_to(matrix[i][j-1], RIGHT, self.h_buff) 96 | else: 97 | mob.next_to(matrix[i-1][j], DOWN, self.v_buff) 98 | return self 99 | 100 | def add_brackets(self): 101 | bracket_pair = TexMobject("\\big[ \\big]") 102 | bracket_pair.scale(2) 103 | bracket_pair.stretch_to_fit_height(self.get_height() + 0.5) 104 | l_bracket, r_bracket = bracket_pair.split() 105 | l_bracket.next_to(self, LEFT) 106 | r_bracket.next_to(self, RIGHT) 107 | self.add(l_bracket, r_bracket) 108 | self.brackets = VMobject(l_bracket, r_bracket) 109 | return self 110 | 111 | def highlight_columns(self, *colors): 112 | for i, color in enumerate(colors): 113 | VMobject(*self.mob_matrix[:,i]).highlight(color) 114 | return self 115 | 116 | def add_background_to_entries(self): 117 | for mob in self.get_entries(): 118 | mob.add_background_rectangle() 119 | return self 120 | 121 | def get_mob_matrix(self): 122 | return self.mob_matrix 123 | 124 | def get_entries(self): 125 | return VMobject(*self.get_mob_matrix().flatten()) 126 | 127 | def get_brackets(self): 128 | return self.brackets 129 | 130 | 131 | 132 | class NumericalMatrixMultiplication(Scene): 133 | CONFIG = { 134 | "left_matrix" : [[1, 2], [3, 4]], 135 | "right_matrix" : [[5, 6], [7, 8]], 136 | "use_parens" : True, 137 | } 138 | def construct(self): 139 | left_string_matrix, right_string_matrix = [ 140 | np.array(matrix).astype("string") 141 | for matrix in self.left_matrix, self.right_matrix 142 | ] 143 | if right_string_matrix.shape[0] != left_string_matrix.shape[1]: 144 | raise Exception("Incompatible shapes for matrix multiplication") 145 | 146 | left = Matrix(left_string_matrix) 147 | right = Matrix(right_string_matrix) 148 | result = self.get_result_matrix( 149 | left_string_matrix, right_string_matrix 150 | ) 151 | 152 | self.organize_matrices(left, right, result) 153 | self.animate_product(left, right, result) 154 | 155 | 156 | def get_result_matrix(self, left, right): 157 | (m, k), n = left.shape, right.shape[1] 158 | mob_matrix = np.array([VMobject()]).repeat(m*n).reshape((m, n)) 159 | for a in range(m): 160 | for b in range(n): 161 | template = "(%s)(%s)" if self.use_parens else "%s%s" 162 | parts = [ 163 | prefix + template%(left[a][c], right[c][b]) 164 | for c in range(k) 165 | for prefix in ["" if c == 0 else "+"] 166 | ] 167 | mob_matrix[a][b] = TexMobject(parts, next_to_buff = 0.1) 168 | return Matrix(mob_matrix) 169 | 170 | def add_lines(self, left, right): 171 | line_kwargs = { 172 | "color" : BLUE, 173 | "stroke_width" : 2, 174 | } 175 | left_rows = [ 176 | VMobject(*row) for row in left.get_mob_matrix() 177 | ] 178 | h_lines = VMobject() 179 | for row in left_rows[:-1]: 180 | h_line = Line(row.get_left(), row.get_right(), **line_kwargs) 181 | h_line.next_to(row, DOWN, buff = left.v_buff/2.) 182 | h_lines.add(h_line) 183 | 184 | right_cols = [ 185 | VMobject(*col) for col in np.transpose(right.get_mob_matrix()) 186 | ] 187 | v_lines = VMobject() 188 | for col in right_cols[:-1]: 189 | v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs) 190 | v_line.next_to(col, RIGHT, buff = right.h_buff/2.) 191 | v_lines.add(v_line) 192 | 193 | self.play(ShowCreation(h_lines)) 194 | self.play(ShowCreation(v_lines)) 195 | self.dither() 196 | self.show_frame() 197 | 198 | def organize_matrices(self, left, right, result): 199 | equals = TexMobject("=") 200 | everything = VMobject(left, right, equals, result) 201 | everything.arrange_submobjects() 202 | everything.scale_to_fit_width(2*SPACE_WIDTH-1) 203 | self.add(everything) 204 | 205 | 206 | def animate_product(self, left, right, result): 207 | l_matrix = left.get_mob_matrix() 208 | r_matrix = right.get_mob_matrix() 209 | result_matrix = result.get_mob_matrix() 210 | circle = Circle( 211 | radius = l_matrix[0][0].get_height(), 212 | color = GREEN 213 | ) 214 | circles = VMobject(*[ 215 | entry.get_point_mobject() 216 | for entry in l_matrix[0][0], r_matrix[0][0] 217 | ]) 218 | (m, k), n = l_matrix.shape, r_matrix.shape[1] 219 | for mob in result_matrix.flatten(): 220 | mob.highlight(BLACK) 221 | lagging_anims = [] 222 | for a in range(m): 223 | for b in range(n): 224 | for c in range(k): 225 | l_matrix[a][c].highlight(YELLOW) 226 | r_matrix[c][b].highlight(YELLOW) 227 | for c in range(k): 228 | start_parts = VMobject( 229 | l_matrix[a][c].copy(), 230 | r_matrix[c][b].copy() 231 | ) 232 | result_entry = result_matrix[a][b].split()[c] 233 | 234 | new_circles = VMobject(*[ 235 | circle.copy().shift(part.get_center()) 236 | for part in start_parts.split() 237 | ]) 238 | self.play(Transform(circles, new_circles)) 239 | self.play( 240 | Transform( 241 | start_parts, 242 | result_entry.copy().highlight(YELLOW), 243 | path_arc = -np.pi/2, 244 | submobject_mode = "all_at_once", 245 | ), 246 | *lagging_anims 247 | ) 248 | result_entry.highlight(YELLOW) 249 | self.remove(start_parts) 250 | lagging_anims = [ 251 | ApplyMethod(result_entry.highlight, WHITE) 252 | ] 253 | 254 | for c in range(k): 255 | l_matrix[a][c].highlight(WHITE) 256 | r_matrix[c][b].highlight(WHITE) 257 | self.play(FadeOut(circles), *lagging_anims) 258 | self.dither() 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | -------------------------------------------------------------------------------- /eola/thumbnails.py: -------------------------------------------------------------------------------- 1 | from mobject.tex_mobject import TexMobject 2 | from mobject import Mobject 3 | from mobject.image_mobject import ImageMobject 4 | from mobject.vectorized_mobject import VMobject 5 | 6 | from animation.animation import Animation 7 | from animation.transform import * 8 | from animation.simple_animations import * 9 | from topics.geometry import * 10 | from topics.characters import * 11 | from topics.functions import * 12 | from topics.number_line import * 13 | from topics.numerals import * 14 | from scene import Scene 15 | from camera import Camera 16 | from mobject.svg_mobject import * 17 | from mobject.tex_mobject import * 18 | from mobject.vectorized_mobject import * 19 | 20 | from eola.matrix import * 21 | from eola.two_d_space import * 22 | from eola.chapter9 import Jennifer, You 23 | 24 | class Chapter0(LinearTransformationScene): 25 | CONFIG = { 26 | "include_background_plane" : False, 27 | "t_matrix" : [[3, 1], [2, -1]] 28 | } 29 | def construct(self): 30 | self.setup() 31 | self.plane.fade() 32 | for mob in self.get_mobjects(): 33 | mob.set_stroke(width = 6) 34 | self.apply_transposed_matrix(self.t_matrix, run_time = 0) 35 | 36 | class Chapter1(Scene): 37 | def construct(self): 38 | arrow = Vector(2*UP+RIGHT) 39 | vs = TextMobject("vs.") 40 | array = Matrix([1, 2]) 41 | array.highlight(TEAL) 42 | everyone = VMobject(arrow, vs, array) 43 | everyone.arrange_submobjects(RIGHT, buff = 0.5) 44 | everyone.scale_to_fit_height(4) 45 | self.add(everyone) 46 | 47 | class Chapter2(LinearTransformationScene): 48 | def construct(self): 49 | self.lock_in_faded_grid() 50 | vectors = VMobject(*[ 51 | Vector([x, y]) 52 | for x in np.arange(-int(SPACE_WIDTH)+0.5, int(SPACE_WIDTH)+0.5) 53 | for y in np.arange(-int(SPACE_HEIGHT)+0.5, int(SPACE_HEIGHT)+0.5) 54 | ]) 55 | vectors.submobject_gradient_highlight(PINK, BLUE_E) 56 | words = TextMobject("Span") 57 | words.scale(3) 58 | words.to_edge(UP) 59 | words.add_background_rectangle() 60 | self.add(vectors, words) 61 | 62 | 63 | class Chapter3(Chapter0): 64 | CONFIG = { 65 | "t_matrix" : [[3, 0], [2, -1]] 66 | } 67 | 68 | class Chapter4p1(Chapter0): 69 | CONFIG = { 70 | "t_matrix" : [[1, 0], [1, 1]] 71 | } 72 | 73 | class Chapter4p2(Chapter0): 74 | CONFIG = { 75 | "t_matrix" : [[1, 2], [-1, 1]] 76 | } 77 | 78 | class Chapter5(LinearTransformationScene): 79 | def construct(self): 80 | self.plane.fade() 81 | self.add_unit_square() 82 | self.plane.set_stroke(width = 6) 83 | VMobject(self.i_hat, self.j_hat).set_stroke(width = 10) 84 | self.square.set_fill(YELLOW, opacity = 0.7) 85 | self.square.set_stroke(width = 0) 86 | self.apply_transposed_matrix(self.t_matrix, run_time = 0) 87 | 88 | class Chapter9(Scene): 89 | def construct(self): 90 | you = You() 91 | jenny = Jennifer() 92 | you.change_mode("erm") 93 | jenny.change_mode("speaking") 94 | you.shift(LEFT) 95 | jenny.shift(2*RIGHT) 96 | 97 | vector = Vector([3, 2]) 98 | vector.center().shift(2*DOWN) 99 | vector.set_stroke(width = 8) 100 | vector.tip.scale_in_place(2) 101 | 102 | you.coords = Matrix([3, 2]) 103 | jenny.coords = Matrix(["5/3", "1/3"]) 104 | for pi in jenny, you: 105 | pi.bubble = pi.get_bubble("speech", width = 3, height = 3) 106 | if pi is you: 107 | pi.bubble.shift(MED_BUFF*RIGHT) 108 | else: 109 | pi.coords.scale(0.8) 110 | pi.bubble.shift(MED_BUFF*LEFT) 111 | pi.bubble.add_content(pi.coords) 112 | pi.add(pi.bubble, pi.coords) 113 | pi.look_at(vector) 114 | 115 | self.add(you, jenny, vector) 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /example_scenes.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from helpers import * 4 | 5 | from mobject.tex_mobject import TexMobject 6 | from mobject import Mobject 7 | from mobject.image_mobject import ImageMobject 8 | from mobject.vectorized_mobject import * 9 | 10 | from animation.animation import Animation 11 | from animation.transform import * 12 | from animation.simple_animations import * 13 | from animation.playground import * 14 | from topics.geometry import * 15 | from topics.characters import * 16 | from topics.functions import * 17 | from topics.number_line import * 18 | from topics.combinatorics import * 19 | from scene import Scene 20 | from camera import Camera 21 | from mobject.svg_mobject import * 22 | from mobject.tex_mobject import * 23 | 24 | from mobject.vectorized_mobject import * 25 | 26 | ## To watch one of these scenes, run the following: 27 | ## python extract_scenes.py -p file_name 28 | 29 | class SquareToCircle(Scene): 30 | def construct(self): 31 | circle = Circle() 32 | square = Square() 33 | square.rotate(np.pi/8) 34 | self.play(ShowCreation(square)) 35 | self.play(Transform(square, circle)) 36 | self.dither() 37 | 38 | class WarpSquare(Scene): 39 | def construct(self): 40 | square = Square() 41 | self.play(ApplyPointwiseFunction( 42 | lambda (x, y, z) : complex_to_R3(np.exp(complex(x, y))), 43 | square 44 | )) 45 | self.dither() 46 | 47 | 48 | class WriteStuff(Scene): 49 | def construct(self): 50 | self.play(Write(TextMobject("Stuff").scale(3))) 51 | 52 | 53 | -------------------------------------------------------------------------------- /extract_scene.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import getopt 5 | import imp 6 | import itertools as it 7 | import inspect 8 | import traceback 9 | import imp 10 | 11 | from helpers import * 12 | from scene import Scene 13 | from camera import Camera 14 | 15 | HELP_MESSAGE = """ 16 | Usage: 17 | python extract_scene.py [] 18 | 19 | -p preview in low quality 20 | -s show and save picture of last frame 21 | -w write result to file [this is default if nothing else is stated] 22 | -l use low quality 23 | -m use medium quality 24 | -a run and save every scene in the script, or all args for the given scene 25 | -q don't pring progress 26 | """ 27 | SCENE_NOT_FOUND_MESSAGE = """ 28 | That scene is not in the script 29 | """ 30 | CHOOSE_NUMBER_MESSAGE = """ 31 | Choose number corresponding to desired scene/arguments. 32 | (Use comma separated list for multiple entries) 33 | 34 | Choice(s): """ 35 | INVALID_NUMBER_MESSAGE = "Fine then, if you don't want to give a valid number I'll just quit" 36 | 37 | NO_SCENE_MESSAGE = """ 38 | There are no scenes inside that module 39 | """ 40 | 41 | 42 | def get_configuration(sys_argv): 43 | try: 44 | opts, args = getopt.getopt(sys_argv[1:], 'hlmpwsqa') 45 | except getopt.GetoptError as err: 46 | print str(err) 47 | sys.exit(2) 48 | config = { 49 | "file" : None, 50 | "scene_name" : "", 51 | "camera_config" : PRODUCTION_QUALITY_CAMERA_CONFIG, 52 | "preview" : False, 53 | "write" : False, 54 | "save_image" : False, 55 | "quiet" : False, 56 | "write_all" : False, 57 | } 58 | for opt, arg in opts: 59 | if opt == '-h': 60 | print HELP_MESSAGE 61 | return 62 | elif opt == '-l': 63 | config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG 64 | elif opt == '-m': 65 | config["camera_config"] = MEDIUM_QUALITY_CAMERA_CONFIG 66 | elif opt == '-p': 67 | config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG 68 | config["preview"] = True 69 | elif opt == '-w': 70 | config["write"] = True 71 | elif opt == '-s': 72 | config["save_image"] = True 73 | elif opt == '-q': 74 | config["quiet"] = True 75 | elif opt == '-a': 76 | config["write_all"] = True 77 | config["quiet"] = True 78 | #By default, write to file 79 | actions = ["write", "preview", "save_image"] 80 | if not any([config[key] for key in actions]): 81 | config["write"] = True 82 | config["skip_animations"] = config["save_image"] and not config["write"] 83 | 84 | if len(args) == 0: 85 | print HELP_MESSAGE 86 | sys.exit() 87 | config["file"] = args[0] 88 | if len(args) > 1: 89 | config["scene_name"] = args[1] 90 | return config 91 | 92 | def handle_scene(scene, **config): 93 | name = str(scene) 94 | if config["quiet"]: 95 | curr_stdout = sys.stdout 96 | sys.stdout = open(os.devnull, "w") 97 | 98 | if config["preview"]: 99 | scene.preview() 100 | if config["save_image"]: 101 | if not config["write_all"]: 102 | scene.show_frame() 103 | path = os.path.join(MOVIE_DIR, config["movie_prefix"]) 104 | scene.save_image(path, name) 105 | if config["write"]: 106 | scene.write_to_movie(os.path.join(config["movie_prefix"], name)) 107 | 108 | if config["quiet"]: 109 | sys.stdout.close() 110 | sys.stdout = curr_stdout 111 | 112 | def is_scene(obj): 113 | if not inspect.isclass(obj): 114 | return False 115 | if not issubclass(obj, Scene): 116 | return False 117 | if obj == Scene: 118 | return False 119 | return True 120 | 121 | def prompt_user_for_choice(name_to_obj): 122 | num_to_name = {} 123 | names = sorted(name_to_obj.keys()) 124 | for count, name in zip(it.count(1), names): 125 | print "%d: %s"%(count, name) 126 | num_to_name[count] = name 127 | try: 128 | user_input = raw_input(CHOOSE_NUMBER_MESSAGE) 129 | return [ 130 | name_to_obj[num_to_name[int(num_str)]] 131 | for num_str in user_input.split(",") 132 | ] 133 | except: 134 | print INVALID_NUMBER_MESSAGE 135 | sys.exit() 136 | 137 | def get_scene_classes(scene_names_to_classes, config): 138 | if len(scene_names_to_classes) == 0: 139 | print NO_SCENE_MESSAGE 140 | return [] 141 | if len(scene_names_to_classes) == 1: 142 | return scene_names_to_classes.values() 143 | if config["scene_name"] in scene_names_to_classes: 144 | return [scene_names_to_classes[config["scene_name"]] ] 145 | if config["scene_name"] != "": 146 | print SCENE_NOT_FOUND_MESSAGE 147 | return [] 148 | if config["write_all"]: 149 | return scene_names_to_classes.values() 150 | return prompt_user_for_choice(scene_names_to_classes) 151 | 152 | def get_module(file_name): 153 | module_name = file_name.replace(".py", "") 154 | last_module = imp.load_module(".", *imp.find_module(".")) 155 | for part in module_name.split(os.sep): 156 | load_args = imp.find_module(part, last_module.__path__) 157 | last_module = imp.load_module(part, *load_args) 158 | return last_module 159 | 160 | def main(): 161 | config = get_configuration(sys.argv) 162 | module = get_module(config["file"]) 163 | scene_names_to_classes = dict( 164 | inspect.getmembers(module, is_scene) 165 | ) 166 | config["movie_prefix"] = config["file"].replace(".py", "") 167 | scene_kwargs = { 168 | "camera_config" : config["camera_config"], 169 | "skip_animations" : config["skip_animations"], 170 | } 171 | for SceneClass in get_scene_classes(scene_names_to_classes, config): 172 | try: 173 | handle_scene(SceneClass(**scene_kwargs), **config) 174 | play_finish_sound() 175 | except: 176 | print "\n\n" 177 | traceback.print_exc() 178 | print "\n\n" 179 | play_error_sound() 180 | 181 | 182 | if __name__ == "__main__": 183 | main() 184 | 185 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /mobject/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "mobject", 3 | "image_mobject", 4 | "tex_mobject", 5 | ] 6 | 7 | from mobject import Mobject, Group 8 | from point_cloud_mobject import Point, Mobject1D, Mobject2D, PMobject 9 | from vectorized_mobject import VMobject, VGroup -------------------------------------------------------------------------------- /mobject/image_mobject.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | import os 4 | from PIL import Image 5 | from random import random 6 | 7 | from helpers import * 8 | from mobject import Mobject 9 | from point_cloud_mobject import PMobject 10 | 11 | class ImageMobject(PMobject): 12 | """ 13 | Automatically filters out black pixels 14 | """ 15 | CONFIG = { 16 | "filter_color" : "black", 17 | "invert" : True, 18 | "use_cache" : True, 19 | "stroke_width" : 1, 20 | "scale_factorue" : 1.0, 21 | "should_center" : True, 22 | } 23 | def __init__(self, image_file, **kwargs): 24 | digest_locals(self) 25 | Mobject.__init__(self, **kwargs) 26 | self.name = to_cammel_case( 27 | os.path.split(image_file)[-1].split(".")[0] 28 | ) 29 | possible_paths = [ 30 | image_file, 31 | os.path.join(IMAGE_DIR, image_file), 32 | os.path.join(IMAGE_DIR, image_file + ".jpg"), 33 | os.path.join(IMAGE_DIR, image_file + ".png"), 34 | os.path.join(IMAGE_DIR, image_file + ".gif"), 35 | ] 36 | for path in possible_paths: 37 | if os.path.exists(path): 38 | self.generate_points_from_file(path) 39 | self.scale(self.scale_factorue) 40 | if self.should_center: 41 | self.center() 42 | return 43 | raise IOError("File not Found") 44 | 45 | def generate_points_from_file(self, path): 46 | if self.use_cache and self.read_in_cached_attrs(path): 47 | return 48 | image = Image.open(path).convert('RGB') 49 | if self.invert: 50 | image = invert_image(image) 51 | self.generate_points_from_image_array(np.array(image)) 52 | self.cache_attrs(path) 53 | 54 | def get_cached_attr_files(self, path, attrs): 55 | #Hash should be unique to (path, invert) pair 56 | unique_hash = str(hash(path+str(self.invert))) 57 | return [ 58 | os.path.join(IMAGE_MOBJECT_DIR, unique_hash)+"."+attr 59 | for attr in attrs 60 | ] 61 | 62 | def read_in_cached_attrs(self, path, 63 | attrs = ("points", "rgbs"), 64 | dtype = "float64"): 65 | cached_attr_files = self.get_cached_attr_files(path, attrs) 66 | if all(map(os.path.exists, cached_attr_files)): 67 | for attr, cache_file in zip(attrs, cached_attr_files): 68 | arr = np.fromfile(cache_file, dtype = dtype) 69 | arr = arr.reshape(arr.size/self.DIM, self.DIM) 70 | setattr(self, attr, arr) 71 | return True 72 | return False 73 | 74 | def cache_attrs(self, path, 75 | attrs = ("points", "rgbs"), 76 | dtype = "float64"): 77 | cached_attr_files = self.get_cached_attr_files(path, attrs) 78 | for attr, cache_file in zip(attrs, cached_attr_files): 79 | getattr(self, attr).astype(dtype).tofile(cache_file) 80 | 81 | 82 | def generate_points_from_image_array(self, image_array): 83 | height, width = image_array.shape[:2] 84 | #Flatten array, and find indices where rgb is not filter_rgb 85 | array = image_array.reshape((height * width, 3)) 86 | filter_rgb = np.array(Color(self.filter_color).get_rgb()) 87 | filter_rgb = 255*filter_rgb.astype('uint8') 88 | bools = array == filter_rgb 89 | bools = bools[:,0]*bools[:,1]*bools[:,2] 90 | indices = np.arange(height * width, dtype = 'int')[~bools] 91 | rgbs = array[indices, :].astype('float') / 255.0 92 | 93 | points = np.zeros((indices.size, 3), dtype = 'float64') 94 | points[:,0] = indices%width - width/2 95 | points[:,1] = -indices/width + height/2 96 | 97 | height, width = map(float, (height, width)) 98 | if height / width > float(DEFAULT_HEIGHT) / DEFAULT_WIDTH: 99 | points *= 2 * SPACE_HEIGHT / height 100 | else: 101 | points *= 2 * SPACE_WIDTH / width 102 | self.add_points(points, rgbs = rgbs) 103 | return self 104 | 105 | 106 | class MobjectFromPixelArray(ImageMobject): 107 | def __init__(self, image_array, **kwargs): 108 | Mobject.__init__(self, **kwargs) 109 | self.generate_points_from_image_array(image_array) 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /mobject/point_cloud_mobject.py: -------------------------------------------------------------------------------- 1 | from .mobject import Mobject 2 | from helpers import * 3 | 4 | class PMobject(Mobject): 5 | def init_points(self): 6 | self.rgbs = np.zeros((0, 3)) 7 | self.points = np.zeros((0, 3)) 8 | return self 9 | 10 | def get_array_attrs(self): 11 | return Mobject.get_array_attrs(self) + ["rgbs"] 12 | 13 | def add_points(self, points, rgbs = None, color = None): 14 | """ 15 | points must be a Nx3 numpy array, as must rgbs if it is not None 16 | """ 17 | if not isinstance(points, np.ndarray): 18 | points = np.array(points) 19 | num_new_points = points.shape[0] 20 | self.points = np.append(self.points, points, axis = 0) 21 | if rgbs is None: 22 | color = Color(color) if color else self.color 23 | rgbs = np.array([color.get_rgb()] * num_new_points) 24 | elif rgbs.shape != points.shape: 25 | raise Exception("points and rgbs must have same shape") 26 | self.rgbs = np.append(self.rgbs, rgbs, axis = 0) 27 | return self 28 | 29 | def highlight(self, color = YELLOW_C, family = True, condition = None): 30 | rgb = Color(color).get_rgb() 31 | mobs = self.family_members_with_points() if family else [self] 32 | for mob in mobs: 33 | if condition: 34 | to_change = np.apply_along_axis(condition, 1, mob.points) 35 | mob.rgbs[to_change, :] = rgb 36 | else: 37 | mob.rgbs[:,:] = rgb 38 | return self 39 | 40 | def gradient_highlight(self, start_color, end_color): 41 | start_rgb, end_rgb = [ 42 | np.array(Color(color).get_rgb()) 43 | for color in start_color, end_color 44 | ] 45 | for mob in self.family_members_with_points(): 46 | num_points = mob.get_num_points() 47 | mob.rgbs = np.array([ 48 | interpolate(start_rgb, end_rgb, alpha) 49 | for alpha in np.arange(num_points)/float(num_points) 50 | ]) 51 | return self 52 | 53 | 54 | def match_colors(self, mobject): 55 | Mobject.align_data(self, mobject) 56 | self.rgbs = np.array(mobject.rgbs) 57 | return self 58 | 59 | def filter_out(self, condition): 60 | for mob in self.family_members_with_points(): 61 | to_eliminate = ~np.apply_along_axis(condition, 1, mob.points) 62 | mob.points = mob.points[to_eliminate] 63 | mob.rgbs = mob.rgbs[to_eliminate] 64 | return self 65 | 66 | def thin_out(self, factor = 5): 67 | """ 68 | Removes all but every nth point for n = factor 69 | """ 70 | for mob in self.family_members_with_points(): 71 | num_points = self.get_num_points() 72 | mob.apply_over_attr_arrays( 73 | lambda arr : arr[ 74 | np.arange(0, num_points, factor) 75 | ] 76 | ) 77 | return self 78 | 79 | def sort_points(self, function = lambda p : p[0]): 80 | """ 81 | function is any map from R^3 to R 82 | """ 83 | for mob in self.family_members_with_points(): 84 | indices = np.argsort( 85 | np.apply_along_axis(function, 1, mob.points) 86 | ) 87 | mob.apply_over_attr_arrays(lambda arr : arr[indices]) 88 | return self 89 | 90 | def fade_to(self, color, alpha): 91 | self.rgbs = interpolate(self.rgbs, np.array(Color(color).rgb), alpha) 92 | for mob in self.submobjects: 93 | mob.fade_to(color, alpha) 94 | return self 95 | 96 | def get_all_rgbs(self): 97 | return self.get_merged_array("rgbs") 98 | 99 | def ingest_submobjects(self): 100 | attrs = self.get_array_attrs() 101 | arrays = map(self.get_merged_array, attrs) 102 | for attr, array in zip(attrs, arrays): 103 | setattr(self, attr, array) 104 | self.submobjects = [] 105 | return self 106 | 107 | def get_color(self): 108 | return Color(rgb = self.rgbs[0, :]) 109 | 110 | def point_from_proportion(self, alpha): 111 | index = alpha*(self.get_num_points()-1) 112 | return self.points[index] 113 | 114 | # Alignment 115 | def align_points_with_larger(self, larger_mobject): 116 | assert(isinstance(larger_mobject, PMobject)) 117 | self.apply_over_attr_arrays( 118 | lambda a : streth_array_to_length( 119 | a, larger_mobject.get_num_points() 120 | ) 121 | ) 122 | 123 | def get_point_mobject(self, center = None): 124 | if center is None: 125 | center = self.get_center() 126 | return Point(center) 127 | 128 | def interpolate_color(self, mobject1, mobject2, alpha): 129 | self.rgbs = interpolate( 130 | mobject1.rgbs, mobject2.rgbs, alpha 131 | ) 132 | 133 | def pointwise_become_partial(self, mobject, a, b): 134 | lower_index, upper_index = [ 135 | int(x * mobject.get_num_points()) 136 | for x in a, b 137 | ] 138 | for attr in self.get_array_attrs(): 139 | full_array = getattr(mobject, attr) 140 | partial_array = full_array[lower_index:upper_index] 141 | setattr(self, attr, partial_array) 142 | 143 | 144 | #TODO, Make the two implementations bellow non-redundant 145 | class Mobject1D(PMobject): 146 | CONFIG = { 147 | "density" : DEFAULT_POINT_DENSITY_1D, 148 | } 149 | def __init__(self, **kwargs): 150 | digest_config(self, kwargs) 151 | self.epsilon = 1.0 / self.density 152 | Mobject.__init__(self, **kwargs) 153 | 154 | 155 | def add_line(self, start, end, color = None): 156 | start, end = map(np.array, [start, end]) 157 | length = np.linalg.norm(end - start) 158 | if length == 0: 159 | points = [start] 160 | else: 161 | epsilon = self.epsilon/length 162 | points = [ 163 | interpolate(start, end, t) 164 | for t in np.arange(0, 1, epsilon) 165 | ] 166 | self.add_points(points, color = color) 167 | 168 | class Mobject2D(PMobject): 169 | CONFIG = { 170 | "density" : DEFAULT_POINT_DENSITY_2D, 171 | } 172 | def __init__(self, **kwargs): 173 | digest_config(self, kwargs) 174 | self.epsilon = 1.0 / self.density 175 | Mobject.__init__(self, **kwargs) 176 | 177 | 178 | 179 | class Point(PMobject): 180 | CONFIG = { 181 | "color" : BLACK, 182 | } 183 | def __init__(self, location = ORIGIN, **kwargs): 184 | PMobject.__init__(self, **kwargs) 185 | self.add_points([location]) 186 | 187 | -------------------------------------------------------------------------------- /mobject/region.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | from PIL import Image 4 | from copy import deepcopy 5 | 6 | from mobject import Mobject 7 | 8 | from helpers import * 9 | 10 | #TODO, this whole class should be something vectorized. 11 | class Region(Mobject): 12 | CONFIG = { 13 | "display_mode" : "region" 14 | } 15 | def __init__(self, condition = (lambda x, y : True), **kwargs): 16 | """ 17 | Condition must be a function which takes in two real 18 | arrays (representing x and y values of space respectively) 19 | and return a boolean array. This can essentially look like 20 | a function from R^2 to {True, False}, but & and | must be 21 | used in place of "and" and "or" 22 | """ 23 | Mobject.__init__(self, **kwargs) 24 | self.condition = condition 25 | 26 | def _combine(self, region, op): 27 | self.condition = lambda x, y : op( 28 | self.condition(x, y), 29 | region.condition(x, y) 30 | ) 31 | 32 | def union(self, region): 33 | self._combine(region, lambda bg1, bg2 : bg1 | bg2) 34 | return self 35 | 36 | def intersect(self, region): 37 | self._combine(region, lambda bg1, bg2 : bg1 & bg2) 38 | return self 39 | 40 | def complement(self): 41 | self.bool_grid = ~self.bool_grid 42 | return self 43 | 44 | class HalfPlane(Region): 45 | def __init__(self, point_pair, upper_left = True, *args, **kwargs): 46 | """ 47 | point_pair of the form [(x_0, y_0,...), (x_1, y_1,...)] 48 | 49 | Pf upper_left is True, the side of the region will be 50 | everything on the upper left side of the line through 51 | the point pair 52 | """ 53 | if not upper_left: 54 | point_pair = list(point_pair) 55 | point_pair.reverse() 56 | (x0, y0), (x1, y1) = point_pair[0][:2], point_pair[1][:2] 57 | def condition(x, y): 58 | return (x1 - x0)*(y - y0) > (y1 - y0)*(x - x0) 59 | Region.__init__(self, condition, *args, **kwargs) 60 | 61 | def region_from_line_boundary(*lines, **kwargs): 62 | reg = Region(**kwargs) 63 | for line in lines: 64 | reg.intersect(HalfPlane(line, **kwargs)) 65 | return reg 66 | 67 | def region_from_polygon_vertices(*vertices, **kwargs): 68 | return region_from_line_boundary(*adjascent_pairs(vertices), **kwargs) 69 | 70 | 71 | def plane_partition(*lines, **kwargs): 72 | """ 73 | A 'line' is a pair of points [(x0, y0,...), (x1, y1,...)] 74 | 75 | Returns the list of regions of the plane cut out by 76 | these lines 77 | """ 78 | result = [] 79 | half_planes = [HalfPlane(line, **kwargs) for line in lines] 80 | complements = [deepcopy(hp).complement() for hp in half_planes] 81 | num_lines = len(lines) 82 | for bool_list in it.product(*[[True, False]]*num_lines): 83 | reg = Region(**kwargs) 84 | for i in range(num_lines): 85 | if bool_list[i]: 86 | reg.intersect(half_planes[i]) 87 | else: 88 | reg.intersect(complements[i]) 89 | if reg.bool_grid.any(): 90 | result.append(reg) 91 | return result 92 | 93 | def plane_partition_from_points(*points, **kwargs): 94 | """ 95 | Returns list of regions cut out by the complete graph 96 | with points from the argument as vertices. 97 | 98 | Each point comes in the form (x, y) 99 | """ 100 | lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)] 101 | return plane_partition(*lines, **kwargs) 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /mobject/svg_mobject.py: -------------------------------------------------------------------------------- 1 | from xml.dom import minidom 2 | import warnings 3 | 4 | from vectorized_mobject import VMobject 5 | from topics.geometry import Rectangle, Circle 6 | from helpers import * 7 | 8 | class SVGMobject(VMobject): 9 | CONFIG = { 10 | "initial_scale_factor" : 1, 11 | "should_center" : True, 12 | } 13 | def __init__(self, svg_file, **kwargs): 14 | digest_config(self, kwargs, locals()) 15 | self.ensure_valid_file() 16 | VMobject.__init__(self, **kwargs) 17 | self.move_into_position() 18 | 19 | def ensure_valid_file(self): 20 | possible_paths = [ 21 | self.svg_file, 22 | os.path.join(IMAGE_DIR, self.svg_file), 23 | os.path.join(IMAGE_DIR, self.svg_file + ".svg"), 24 | ] 25 | for path in possible_paths: 26 | if os.path.exists(path): 27 | self.svg_file = path 28 | return 29 | raise IOError("No file matching %s in image directory"%self.svg_file) 30 | 31 | def generate_points(self): 32 | doc = minidom.parse(self.svg_file) 33 | self.ref_to_element = {} 34 | for svg in doc.getElementsByTagName("svg"): 35 | self.add(*self.get_mobjects_from(svg)) 36 | doc.unlink() 37 | 38 | def get_mobjects_from(self, element): 39 | result = [] 40 | if not isinstance(element, minidom.Element): 41 | return result 42 | if element.tagName == 'defs': 43 | self.update_ref_to_element(element) 44 | elif element.tagName == 'style': 45 | pass #TODO, handle style 46 | elif element.tagName in ['g', 'svg']: 47 | result += it.chain(*[ 48 | self.get_mobjects_from(child) 49 | for child in element.childNodes 50 | ]) 51 | elif element.tagName == 'path': 52 | result.append(self.path_string_to_mobject( 53 | element.getAttribute('d') 54 | )) 55 | elif element.tagName == 'use': 56 | result += self.use_to_mobjects(element) 57 | elif element.tagName == 'rect': 58 | result.append(self.rect_to_mobject(element)) 59 | elif element.tagName == 'circle': 60 | result.append(self.circle_to_mobject(element)) 61 | else: 62 | warnings.warn("Unknown element type: " + element.tagName) 63 | result = filter(lambda m : m is not None, result) 64 | self.handle_transforms(element, VMobject(*result)) 65 | return result 66 | 67 | def g_to_mobjects(self, g_element): 68 | mob = VMobject(*self.get_mobjects_from(g_element)) 69 | self.handle_transforms(g_element, mob) 70 | return mob.submobjects 71 | 72 | def path_string_to_mobject(self, path_string): 73 | return VMobjectFromSVGPathstring(path_string) 74 | 75 | def use_to_mobjects(self, use_element): 76 | #Remove initial "#" character 77 | ref = use_element.getAttribute("xlink:href")[1:] 78 | if ref not in self.ref_to_element: 79 | warnings.warn("%s not recognized"%ref) 80 | return VMobject() 81 | return self.get_mobjects_from( 82 | self.ref_to_element[ref] 83 | ) 84 | 85 | # 86 | 87 | def circle_to_mobject(self, circle_element): 88 | x, y, r = [ 89 | float(circle_element.getAttribute(key)) 90 | if circle_element.hasAttribute(key) 91 | else 0.0 92 | for key in "cx", "cy", "r" 93 | ] 94 | return Circle(radius = r).shift(x*RIGHT+y*DOWN) 95 | 96 | def rect_to_mobject(self, rect_element): 97 | if rect_element.hasAttribute("fill"): 98 | if Color(str(rect_element.getAttribute("fill"))) == Color(WHITE): 99 | return 100 | mob = Rectangle( 101 | width = float(rect_element.getAttribute("width")), 102 | height = float(rect_element.getAttribute("height")), 103 | stroke_width = 0, 104 | fill_color = WHITE, 105 | fill_opacity = 1.0 106 | ) 107 | mob.shift(mob.get_center()-mob.get_corner(UP+LEFT)) 108 | return mob 109 | 110 | def handle_transforms(self, element, mobject): 111 | x, y = 0, 0 112 | try: 113 | x = float(element.getAttribute('x')) 114 | #Flip y 115 | y = -float(element.getAttribute('y')) 116 | except: 117 | pass 118 | mobject.shift(x*RIGHT+y*UP) 119 | #TODO, transforms 120 | 121 | def update_ref_to_element(self, defs): 122 | new_refs = dict([ 123 | (element.getAttribute('id'), element) 124 | for element in defs.childNodes 125 | if isinstance(element, minidom.Element) and element.hasAttribute('id') 126 | ]) 127 | self.ref_to_element.update(new_refs) 128 | 129 | def move_into_position(self): 130 | if self.should_center: 131 | self.center() 132 | self.scale_in_place(self.initial_scale_factor) 133 | 134 | 135 | 136 | 137 | class VMobjectFromSVGPathstring(VMobject): 138 | def __init__(self, path_string, **kwargs): 139 | digest_locals(self) 140 | VMobject.__init__(self, **kwargs) 141 | 142 | def get_path_commands(self): 143 | result = [ 144 | "M", #moveto 145 | "L", #lineto 146 | "H", #horizontal lineto 147 | "V", #vertical lineto 148 | "C", #curveto 149 | "S", #smooth curveto 150 | "Q", #quadratic Bezier curve 151 | "T", #smooth quadratic Bezier curveto 152 | "A", #elliptical Arc 153 | "Z", #closepath 154 | ] 155 | result += map(lambda s : s.lower(), result) 156 | return result 157 | 158 | def generate_points(self): 159 | pattern = "[%s]"%("".join(self.get_path_commands())) 160 | pairs = zip( 161 | re.findall(pattern, self.path_string), 162 | re.split(pattern, self.path_string)[1:] 163 | ) 164 | #Which mobject should new points be added to 165 | self.growing_path = self 166 | for command, coord_string in pairs: 167 | self.handle_command(command, coord_string) 168 | #people treat y-coordinate differently 169 | self.rotate(np.pi, RIGHT) 170 | 171 | def handle_command(self, command, coord_string): 172 | isLower = command.islower() 173 | command = command.upper() 174 | #new_points are the points that will be added to the curr_points 175 | #list. This variable may get modified in the conditionals below. 176 | points = self.growing_path.points 177 | new_points = self.string_to_points(coord_string) 178 | if isLower: 179 | new_points += points[-1] 180 | if command == "M": #moveto 181 | if len(points) > 0: 182 | self.growing_path = self.add_subpath(new_points) 183 | else: 184 | self.growing_path.start_at(new_points[0]) 185 | return 186 | elif command in ["L", "H", "V"]: #lineto 187 | if command == "H": 188 | new_points[0,1] = points[-1,1] 189 | elif command == "V": 190 | new_points[0,1] = new_points[0,0] 191 | new_points[0,0] = points[-1,0] 192 | new_points = new_points[[0, 0, 0]] 193 | elif command == "C": #curveto 194 | pass #Yay! No action required 195 | elif command in ["S", "T"]: #smooth curveto 196 | handle1 = points[-1]+(points[-1]-points[-2]) 197 | new_points = np.append([handle1], new_points, axis = 0) 198 | if command in ["Q", "T"]: #quadratic Bezier curve 199 | #TODO, this is a suboptimal approximation 200 | new_points = np.append([new_points[0]], new_points, axis = 0) 201 | elif command == "A": #elliptical Arc 202 | raise Exception("Not implemented") 203 | elif command == "Z": #closepath 204 | if not is_closed(points): 205 | #Both handles and new anchor are the start 206 | new_points = points[[0, 0, 0]] 207 | # self.mark_paths_closed = True 208 | self.growing_path.add_control_points(new_points) 209 | 210 | def string_to_points(self, coord_string): 211 | coord_string = coord_string.replace("-",",-") 212 | numbers = [ 213 | float(s) 214 | for s in re.split("[ ,]", coord_string) 215 | if s != "" 216 | ] 217 | if len(numbers)%2 == 1: 218 | numbers.append(0) 219 | num_points = len(numbers)/2 220 | result = np.zeros((num_points, self.dim)) 221 | result[:,:2] = np.array(numbers).reshape((num_points, 2)) 222 | return result 223 | 224 | def get_original_path_string(self): 225 | return self.path_string 226 | 227 | -------------------------------------------------------------------------------- /mobject/tex_mobject.py: -------------------------------------------------------------------------------- 1 | from vectorized_mobject import VMobject 2 | from svg_mobject import SVGMobject, VMobjectFromSVGPathstring 3 | from topics.geometry import BackgroundRectangle 4 | from helpers import * 5 | import collections 6 | 7 | TEX_MOB_SCALE_FACTOR = 0.05 8 | TEXT_MOB_SCALE_FACTOR = 0.05 9 | 10 | 11 | class TexSymbol(VMobjectFromSVGPathstring): 12 | def pointwise_become_partial(self, mobject, a, b): 13 | #TODO, this assumes a = 0 14 | if b < 0.5: 15 | b = 2*b 16 | width = 1 17 | opacity = 0 18 | else: 19 | width = 2 - 2*b 20 | opacity = 2*b - 1 21 | b = 1 22 | VMobjectFromSVGPathstring.pointwise_become_partial( 23 | self, mobject, 0, b 24 | ) 25 | self.set_stroke(width = width) 26 | self.set_fill(opacity = opacity) 27 | 28 | 29 | class TexMobject(SVGMobject): 30 | CONFIG = { 31 | "template_tex_file" : TEMPLATE_TEX_FILE, 32 | "stroke_width" : 0, 33 | "fill_opacity" : 1.0, 34 | "fill_color" : WHITE, 35 | "should_center" : True, 36 | "arg_separator" : " ", 37 | "enforce_new_line_structure" : False, 38 | "initial_scale_factor" : TEX_MOB_SCALE_FACTOR, 39 | "organize_left_to_right" : False, 40 | "propogate_style_to_family" : True, 41 | "alignment" : "", 42 | } 43 | def __init__(self, *args, **kwargs): 44 | digest_config(self, kwargs, locals()) 45 | ##TODO, Eventually remove this 46 | if len(args) == 1 and isinstance(args[0], list): 47 | self.args = args[0] 48 | ## 49 | assert(all([isinstance(a, str) for a in self.args])) 50 | self.tex_string = self.get_modified_expression() 51 | VMobject.__init__(self, **kwargs) 52 | self.move_into_position() 53 | if self.organize_left_to_right: 54 | self.organize_submobjects_left_to_right() 55 | 56 | 57 | def path_string_to_mobject(self, path_string): 58 | #Overwrite superclass default to use 59 | #specialized path_string mobject 60 | return TexSymbol(path_string) 61 | 62 | 63 | def generate_points(self): 64 | self.svg_file = tex_to_svg_file( 65 | self.tex_string, 66 | self.template_tex_file 67 | ) 68 | SVGMobject.generate_points(self) 69 | if len(self.args) > 1: 70 | self.handle_multiple_args() 71 | 72 | def get_modified_expression(self): 73 | result = self.arg_separator.join(self.args) 74 | if self.enforce_new_line_structure: 75 | result = result.replace("\n", " \\\\ \n ") 76 | result = " ".join([self.alignment, result]) 77 | result = result.strip() 78 | return result 79 | 80 | def get_tex_string(self): 81 | return self.tex_string 82 | 83 | def handle_multiple_args(self): 84 | new_submobjects = [] 85 | curr_index = 0 86 | self.expression_parts = list(self.args) 87 | for expr in self.args: 88 | model = TexMobject(expr, **self.CONFIG) 89 | new_index = curr_index + len(model.submobjects) 90 | new_submobjects.append(VMobject( 91 | *self.submobjects[curr_index:new_index] 92 | )) 93 | curr_index = new_index 94 | self.submobjects = new_submobjects 95 | return self 96 | 97 | def highlight_by_tex(self, tex, color): 98 | if not hasattr(self, "expression_parts"): 99 | if tex == self.get_tex_string(): 100 | self.highlight(color) 101 | return self 102 | for submob, part_tex in zip(self.split(), self.expression_parts): 103 | if part_tex == tex: 104 | submob.highlight(color) 105 | return self 106 | 107 | def organize_submobjects_left_to_right(self): 108 | self.submobjects.sort( 109 | lambda m1, m2 : int((m1.get_left()-m2.get_left())[0]) 110 | ) 111 | 112 | def add_background_rectangle(self, color = BLACK, opacity = 0.75): 113 | self.background_rectangle = BackgroundRectangle( 114 | self, color = color, 115 | fill_opacity = opacity 116 | ) 117 | letters = VMobject(*self.submobjects) 118 | self.submobjects = [self.background_rectangle, letters] 119 | return self 120 | 121 | class TextMobject(TexMobject): 122 | CONFIG = { 123 | "template_tex_file" : TEMPLATE_TEXT_FILE, 124 | "initial_scale_factor" : TEXT_MOB_SCALE_FACTOR, 125 | "enforce_new_line_structure" : True, 126 | "alignment" : "\\centering", 127 | } 128 | 129 | 130 | 131 | class Brace(TexMobject): 132 | CONFIG = { 133 | "buff" : 0.2, 134 | } 135 | TEX_STRING = "\\underbrace{%s}"%(3*"\\qquad") 136 | def __init__(self, mobject, direction = DOWN, **kwargs): 137 | digest_config(self, kwargs, locals()) 138 | TexMobject.__init__(self, self.TEX_STRING, **kwargs) 139 | angle = -np.arctan2(*direction[:2]) + np.pi 140 | mobject.rotate(-angle) 141 | left = mobject.get_corner(DOWN+LEFT) 142 | right = mobject.get_corner(DOWN+RIGHT) 143 | self.stretch_to_fit_width(right[0]-left[0]) 144 | self.shift(left - self.get_corner(UP+LEFT) + self.buff*DOWN) 145 | for mob in mobject, self: 146 | mob.rotate(angle) 147 | 148 | def put_at_tip(self, mob, **kwargs): 149 | mob.next_to(self, self.direction, **kwargs) 150 | return self 151 | 152 | def get_text(self, *text, **kwargs): 153 | text_mob = TextMobject(*text) 154 | self.put_at_tip(text_mob, **kwargs) 155 | return text_mob 156 | 157 | 158 | 159 | def tex_hash(expression, template_tex_file): 160 | return str(hash(expression + template_tex_file)) 161 | 162 | def tex_to_svg_file(expression, template_tex_file): 163 | image_dir = os.path.join( 164 | TEX_IMAGE_DIR, 165 | tex_hash(expression, template_tex_file) 166 | ) 167 | if os.path.exists(image_dir): 168 | return get_sorted_image_list(image_dir) 169 | tex_file = generate_tex_file(expression, template_tex_file) 170 | dvi_file = tex_to_dvi(tex_file) 171 | return dvi_to_svg(dvi_file) 172 | 173 | 174 | def generate_tex_file(expression, template_tex_file): 175 | result = os.path.join( 176 | TEX_DIR, 177 | tex_hash(expression, template_tex_file) 178 | ) + ".tex" 179 | if not os.path.exists(result): 180 | print "Writing \"%s\" to %s"%( 181 | "".join(expression), result 182 | ) 183 | with open(template_tex_file, "r") as infile: 184 | body = infile.read() 185 | body = body.replace(TEX_TEXT_TO_REPLACE, expression) 186 | with open(result, "w") as outfile: 187 | outfile.write(body) 188 | return result 189 | 190 | def tex_to_dvi(tex_file): 191 | result = tex_file.replace(".tex", ".dvi") 192 | if not os.path.exists(result): 193 | commands = [ 194 | "latex", 195 | "-interaction=batchmode", 196 | "-output-directory=" + TEX_DIR, 197 | tex_file, 198 | "> /dev/null" 199 | ] 200 | #TODO, Error check 201 | os.system(" ".join(commands)) 202 | return result 203 | 204 | def dvi_to_svg(dvi_file, regen_if_exists = False): 205 | """ 206 | Converts a dvi, which potentially has multiple slides, into a 207 | directory full of enumerated pngs corresponding with these slides. 208 | Returns a list of PIL Image objects for these images sorted as they 209 | where in the dvi 210 | """ 211 | result = dvi_file.replace(".dvi", ".svg") 212 | if not os.path.exists(result): 213 | commands = [ 214 | "dvisvgm", 215 | dvi_file, 216 | "-n", 217 | "-v", 218 | "0", 219 | "-o", 220 | result, 221 | "> /dev/null" 222 | ] 223 | os.system(" ".join(commands)) 224 | return result 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /old_projects/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helpsypoo/manim/720f1e9683cd09968eeb84c6f69409d5241a74a2/old_projects/__init__.py -------------------------------------------------------------------------------- /old_projects/brachistochrone/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helpsypoo/manim/720f1e9683cd09968eeb84c6f69409d5241a74a2/old_projects/brachistochrone/__init__.py -------------------------------------------------------------------------------- /old_projects/complex_multiplication_article.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | import itertools as it 5 | from copy import deepcopy 6 | import sys 7 | 8 | 9 | from animation import * 10 | from mobject import * 11 | from constants import * 12 | from mobject.region import * 13 | from scene import Scene 14 | from topics.complex_numbers import * 15 | 16 | DEFAULT_PLANE_CONFIG = { 17 | "stroke_width" : 2*DEFAULT_POINT_THICKNESS 18 | 19 | 20 | class SuccessiveComplexMultiplications(ComplexMultiplication): 21 | args_list = [ 22 | (complex(1, 2), complex(1, -2)), 23 | (complex(-2, 1), complex(-2, -1)), 24 | ] 25 | 26 | @staticmethod 27 | def args_to_string(*multipliers): 28 | return "_".join([str(m)[1:-1] for m in multipliers]) 29 | 30 | @staticmethod 31 | def string_to_args(arg_string): 32 | args_string.replac("i", "j") 33 | return map(copmlex, arg_string.split()) 34 | 35 | def construct(self, *multipliers): 36 | norm = abs(reduce(op.mul, multipliers, 1)) 37 | shrink_factor = SPACE_WIDTH/max(SPACE_WIDTH, norm) 38 | plane_config = { 39 | "density" : norm*DEFAULT_POINT_DENSITY_1D, 40 | "unit_to_spatial_width" : shrink_factor, 41 | "x_radius" : shrink_factor*SPACE_WIDTH, 42 | "y_radius" : shrink_factor*SPACE_HEIGHT, 43 | } 44 | ComplexMultiplication.construct(self, multipliers[0], **plane_config) 45 | 46 | one_dot = self.draw_dot("1", 1, True) 47 | one_dot_copy = deepcopy(one_dot) 48 | 49 | for multiplier, count in zip(multipliers, it.count()): 50 | if multiplier == multipliers[0]: 51 | tex = "z" 52 | elif np.conj(multiplier) == multipliers[0]: 53 | tex = "\\bar z" 54 | else: 55 | tex = "z_%d"%count 56 | self.draw_dot(tex, multiplier) 57 | 58 | for multiplier in multipliers: 59 | self.multiplier = multiplier 60 | self.apply_multiplication() 61 | new_one = deepcopy(one_dot_copy) 62 | self.mobjects_to_move_without_molding.append(new_one) 63 | 64 | 65 | 66 | class ShowComplexPower(SuccessiveComplexMultiplications): 67 | args_list = [ 68 | (complex(0, 1), 1), 69 | (complex(0, 1), 2), 70 | (np.exp(complex(0, 2*np.pi/5)), 1), 71 | (np.exp(complex(0, 2*np.pi/5)), 5), 72 | (np.exp(complex(0, 4*np.pi/5)), 5), 73 | (np.exp(complex(0, -2*np.pi/5)), 5), 74 | (complex(1, np.sqrt(3)), 1), 75 | (complex(1, np.sqrt(3)), 3), 76 | ] 77 | 78 | @staticmethod 79 | def args_to_string(multiplier, num_repeats): 80 | start = ComplexMultiplication.args_to_string(multiplier) 81 | return start + "ToThe%d"%num_repeats 82 | 83 | @staticmethod 84 | def string_to_args(arg_string): 85 | parts = arg_string.split() 86 | if len(parts) < 2 or len(parts) > 3: 87 | raise Exception("Invalid arguments") 88 | multiplier = complex(parts[0]) 89 | num_repeats = int(parts[1]) 90 | return multiplier, num_repeats 91 | 92 | def construct(self, multiplier, num_repeats): 93 | SuccessiveComplexMultiplications.construct( 94 | [multiplier]*num_repeats 95 | ) 96 | 97 | 98 | class ComplexDivision(ComplexMultiplication): 99 | args_list = [ 100 | complex(np.sqrt(3), 1), 101 | complex(1./3, -1./3), 102 | complex(1, 2), 103 | ] 104 | 105 | def construct(self, num): 106 | ComplexMultiplication.construct(self, 1./num) 107 | self.draw_dot("1", 1, False), 108 | self.draw_dot("z", num, True) 109 | self.apply_multiplication() 110 | 111 | class ConjugateDivisionExample(ComplexMultiplication): 112 | args_list = [ 113 | complex(1, 2), 114 | ] 115 | 116 | def construct(self, num): 117 | ComplexMultiplication.construct(self, np.conj(num), radius = 2.5*SPACE_WIDTH) 118 | self.draw_dot("1", 1, True) 119 | self.draw_dot("\\bar z", self.multiplier) 120 | self.apply_multiplication() 121 | self.multiplier = 1./(abs(num)**2) 122 | self.anim_config["path_func"] = straight_path 123 | self.apply_multiplication() 124 | self.dither() 125 | 126 | class DrawSolutionsToZToTheNEqualsW(Scene): 127 | @staticmethod 128 | def args_to_string(n, w): 129 | return str(n) + "_" + complex_string(w) 130 | 131 | @staticmethod 132 | def string_to_args(args_string): 133 | parts = args_string.split() 134 | return int(parts[0]), complex(parts[1]) 135 | 136 | def construct(self, n, w): 137 | w = complex(w) 138 | plane_config = DEFAULT_PLANE_CONFIG.copy() 139 | norm = abs(w) 140 | theta = np.log(w).imag 141 | radius = norm**(1./n) 142 | zoom_value = (SPACE_HEIGHT-0.5)/radius 143 | plane_config["unit_to_spatial_width"] = zoom_value 144 | plane = ComplexPlane(**plane_config) 145 | circle = Circle( 146 | radius = radius*zoom_value, 147 | stroke_width = plane.stroke_width 148 | ) 149 | solutions = [ 150 | radius*np.exp(complex(0, 1)*(2*np.pi*k + theta)/n) 151 | for k in range(n) 152 | ] 153 | points = map(plane.number_to_point, solutions) 154 | dots = [ 155 | Dot(point, color = BLUE_B, radius = 0.1) 156 | for point in points 157 | ] 158 | lines = [Line(*pair) for pair in adjascent_pairs(points)] 159 | 160 | self.add(plane, circle, *dots+lines) 161 | self.add(*plane.get_coordinate_labels()) 162 | 163 | 164 | class DrawComplexAngleAndMagnitude(Scene): 165 | args_list = [ 166 | ( 167 | ("1+i\\sqrt{3}", complex(1, np.sqrt(3)) ), 168 | ("\\frac{\\sqrt{3}}{2} - \\frac{1}{2}i", complex(np.sqrt(3)/2, -1./2)), 169 | ), 170 | (("1+i", complex(1, 1)),), 171 | ] 172 | @staticmethod 173 | def args_to_string(*reps_and_nums): 174 | return "--".join([ 175 | complex_string(num) 176 | for rep, num in reps_and_nums 177 | ]) 178 | 179 | def construct(self, *reps_and_nums): 180 | radius = max([abs(n.imag) for r, n in reps_and_nums]) + 1 181 | plane_config = { 182 | "color" : "grey", 183 | "unit_to_spatial_width" : SPACE_HEIGHT / radius, 184 | } 185 | plane_config.update(DEFAULT_PLANE_CONFIG) 186 | self.plane = ComplexPlane(**plane_config) 187 | coordinates = self.plane.get_coordinate_labels() 188 | # self.plane.add_spider_web() 189 | self.add(self.plane, *coordinates) 190 | for rep, num in reps_and_nums: 191 | self.draw_number(rep, num) 192 | self.add_angle_label(num) 193 | self.add_lines(rep, num) 194 | 195 | def draw_number(self, tex_representation, number): 196 | point = self.plane.number_to_point(number) 197 | dot = Dot(point) 198 | label = TexMobject(tex_representation) 199 | max_width = 0.8*self.plane.unit_to_spatial_width 200 | if label.get_width() > max_width: 201 | label.scale_to_fit_width(max_width) 202 | dot_to_label_dir = RIGHT if point[0] > 0 else LEFT 203 | edge = label.get_edge_center(-dot_to_label_dir) 204 | buff = 0.1 205 | label.shift(point - edge + buff*dot_to_label_dir) 206 | label.highlight(YELLOW) 207 | 208 | self.add_mobjects_among(locals().values()) 209 | 210 | 211 | def add_angle_label(self, number): 212 | arc = Arc( 213 | np.log(number).imag, 214 | radius = 0.2 215 | ) 216 | 217 | self.add_mobjects_among(locals().values()) 218 | 219 | def add_lines(self, tex_representation, number): 220 | point = self.plane.number_to_point(number) 221 | x_line, y_line, num_line = [ 222 | Line( 223 | start, end, 224 | color = color, 225 | stroke_width = self.plane.stroke_width 226 | ) 227 | for start, end, color in zip( 228 | [ORIGIN, point[0]*RIGHT, ORIGIN], 229 | [point[0]*RIGHT, point, point], 230 | [BLUE_D, GOLD_E, WHITE] 231 | ) 232 | ] 233 | # tex_representation.replace("i", "") 234 | # if "+" in tex_representation: 235 | # tex_parts = tex_representation.split("+") 236 | # elif "-" in tex_representation: 237 | # tex_parts = tex_representation.split("-") 238 | # x_label, y_label = map(TexMobject, tex_parts) 239 | # for label in x_label, y_label: 240 | # label.scale_to_fit_height(0.5) 241 | # x_label.next_to(x_line, point[1]*DOWN/abs(point[1])) 242 | # y_label.next_to(y_line, point[0]*RIGHT/abs(point[0])) 243 | norm = np.linalg.norm(point) 244 | brace = Underbrace(ORIGIN, ORIGIN+norm*RIGHT) 245 | if point[1] > 0: 246 | brace.rotate(np.pi, RIGHT) 247 | brace.rotate(np.log(number).imag) 248 | norm_label = TexMobject("%.1f"%abs(number)) 249 | norm_label.scale(0.5) 250 | axis = OUT if point[1] > 0 else IN 251 | norm_label.next_to(brace, rotate_vector(point, np.pi/2, axis)) 252 | 253 | self.add_mobjects_among(locals().values()) 254 | 255 | -------------------------------------------------------------------------------- /old_projects/generate_logo.py: -------------------------------------------------------------------------------- 1 | 2 | from animation.transform import Transform 3 | from mobject import Mobject 4 | from mobject.tex_mobject import TextMobject 5 | from mobject.image_mobject import MobjectFromPixelArray 6 | from topics.geometry import Circle 7 | from topics.three_dimensions import Sphere 8 | from scene import Scene 9 | 10 | from helpers import * 11 | 12 | ## Warning, much of what is in this class 13 | ## likely not supported anymore. 14 | 15 | class LogoGeneration(Scene): 16 | CONFIG = { 17 | "radius" : 1.5, 18 | "inner_radius_ratio" : 0.55, 19 | "circle_density" : 100, 20 | "circle_blue" : "skyblue", 21 | "circle_brown" : DARK_BROWN, 22 | "circle_repeats" : 5, 23 | "sphere_density" : 50, 24 | "sphere_blue" : DARK_BLUE, 25 | "sphere_brown" : LIGHT_BROWN, 26 | "interpolation_factor" : 0.3, 27 | "frame_duration" : 0.03, 28 | "run_time" : 3, 29 | } 30 | def construct(self): 31 | digest_config(self, {}) 32 | ## Usually shouldn't need this... 33 | self.frame_duration = self.CONFIG["frame_duration"] 34 | ## 35 | digest_config(self, {}) 36 | circle = Circle( 37 | density = self.circle_density, 38 | color = self.circle_blue 39 | ) 40 | circle.repeat(self.circle_repeats) 41 | circle.scale(self.radius) 42 | sphere = Sphere( 43 | density = self.sphere_density, 44 | color = self.sphere_blue 45 | ) 46 | sphere.scale(self.radius) 47 | sphere.rotate(-np.pi / 7, [1, 0, 0]) 48 | sphere.rotate(-np.pi / 7) 49 | iris = Mobject() 50 | iris.interpolate( 51 | circle, sphere, 52 | self.interpolation_factor 53 | ) 54 | for mob, color in [(iris, self.sphere_brown), (circle, self.circle_brown)]: 55 | mob.highlight(color, lambda (x, y, z) : x < 0 and y > 0) 56 | mob.highlight( 57 | "black", 58 | lambda point: np.linalg.norm(point) < \ 59 | self.inner_radius_ratio*self.radius 60 | ) 61 | self.name_mob = TextMobject("3Blue1Brown").center() 62 | self.name_mob.highlight("grey") 63 | self.name_mob.shift(2*DOWN) 64 | 65 | self.play(Transform( 66 | circle, iris, 67 | run_time = self.run_time 68 | )) 69 | self.frames = drag_pixels(self.frames) 70 | self.save_image(IMAGE_DIR) 71 | self.logo = MobjectFromPixelArray(self.frames[-1]) 72 | self.add(self.name_mob) 73 | self.dither() 74 | 75 | 76 | -------------------------------------------------------------------------------- /old_projects/hilbert/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helpsypoo/manim/720f1e9683cd09968eeb84c6f69409d5241a74a2/old_projects/hilbert/__init__.py -------------------------------------------------------------------------------- /old_projects/hilbert/fractal_porn.py: -------------------------------------------------------------------------------- 1 | from mobject import Mobject, Point, Mobject1D 2 | from scene import Scene 3 | from animation.transform import \ 4 | Transform, ShimmerIn, FadeIn, FadeOut, ApplyMethod 5 | from animation.simple_animations import \ 6 | ShowCreation, DelayByOrder 7 | from topics.geometry import Line, Arc, Arrow 8 | from mobject.tex_mobject import TexMobject, TextMobject 9 | 10 | from helpers import * 11 | from hilbert.curves import * 12 | 13 | class Intro(TransformOverIncreasingOrders): 14 | @staticmethod 15 | def args_to_string(*args): 16 | return "" 17 | 18 | @staticmethod 19 | def string_to_args(string): 20 | raise Exception("string_to_args Not Implemented!") 21 | 22 | def construct(self): 23 | words1 = TextMobject( 24 | "If you watched my video about Hilbert's space-filling curve\\dots" 25 | ) 26 | words2 = TextMobject( 27 | "\\dots you might be curious to see what a few other space-filling curves look like." 28 | ) 29 | words2.scale(0.8) 30 | for words in words1, words2: 31 | words.to_edge(UP, buff = 0.2) 32 | 33 | self.setup(HilbertCurve) 34 | self.play(ShimmerIn(words1)) 35 | for x in range(4): 36 | self.increase_order() 37 | self.remove(words1) 38 | self.increase_order( 39 | ShimmerIn(words2) 40 | ) 41 | for x in range(4): 42 | self.increase_order() 43 | 44 | 45 | 46 | class BringInPeano(Intro): 47 | def construct(self): 48 | words1 = TextMobject(""" 49 | For each one, see if you can figure out what 50 | the pattern of construction is. 51 | """) 52 | words2 = TextMobject(""" 53 | This one is the Peano curve. 54 | """) 55 | words3 = TextMobject(""" 56 | It is the original space-filling curve. 57 | """) 58 | self.setup(PeanoCurve) 59 | self.play(ShimmerIn(words1)) 60 | self.dither(5) 61 | self.remove(words1) 62 | self.add(words2.to_edge(UP)) 63 | for x in range(3): 64 | self.increase_order() 65 | self.remove(words2) 66 | self.increase_order(ShimmerIn(words3.to_edge(UP))) 67 | for x in range(2): 68 | self.increase_order() 69 | 70 | 71 | class FillOtherShapes(Intro): 72 | def construct(self): 73 | words1 = TextMobject(""" 74 | But of course, there's no reason we should limit 75 | ourselves to filling in squares. 76 | """) 77 | words2 = TextMobject(""" 78 | Here's a simple triangle-filling curve I defined 79 | in a style reflective of a Hilbert curve. 80 | """) 81 | words1.to_edge(UP) 82 | words2.scale(0.8).to_edge(UP, buff = 0.2) 83 | 84 | self.setup(TriangleFillingCurve) 85 | self.play(ShimmerIn(words1)) 86 | for x in range(3): 87 | self.increase_order() 88 | self.remove(words1) 89 | self.add(words2) 90 | for x in range(5): 91 | self.increase_order() 92 | 93 | class SmallerFlowSnake(FlowSnake): 94 | CONFIG = { 95 | "radius" : 4 96 | } 97 | 98 | class MostDelightfulName(Intro): 99 | def construct(self): 100 | words1 = TextMobject(""" 101 | This one has the most delightful name, 102 | thanks to mathematician/programmer Bill Gosper: 103 | """) 104 | words2 = TextMobject("``Flow Snake''") 105 | words3 = TextMobject(""" 106 | What makes this one particularly interesting 107 | is that the boundary itself is a fractal. 108 | """) 109 | for words in words1, words2, words3: 110 | words.to_edge(UP) 111 | 112 | self.setup(SmallerFlowSnake) 113 | self.play(ShimmerIn(words1)) 114 | for x in range(3): 115 | self.increase_order() 116 | self.remove(words1) 117 | self.add(words2) 118 | for x in range(3): 119 | self.increase_order() 120 | self.remove(words2) 121 | self.play(ShimmerIn(words3)) 122 | 123 | 124 | 125 | class SurpriseFractal(Intro): 126 | def construct(self): 127 | words = TextMobject(""" 128 | It might come as a surprise how some well-known 129 | fractals can be described with curves. 130 | """) 131 | words.to_edge(UP) 132 | 133 | self.setup(Sierpinski) 134 | self.add(TextMobject("Speaking of other fractals\\dots")) 135 | self.dither(3) 136 | self.clear() 137 | self.play(ShimmerIn(words)) 138 | for x in range(9): 139 | self.increase_order() 140 | 141 | 142 | class IntroduceKoch(Intro): 143 | def construct(self): 144 | words = map(TextMobject, [ 145 | "This is another famous fractal.", 146 | "The ``Koch Snowflake''", 147 | "Let's finish things off by seeing how to turn \ 148 | this into a space-filling curve" 149 | ]) 150 | for text in words: 151 | text.to_edge(UP) 152 | 153 | self.setup(KochCurve) 154 | self.add(words[0]) 155 | for x in range(3): 156 | self.increase_order() 157 | self.remove(words[0]) 158 | self.add(words[1]) 159 | for x in range(4): 160 | self.increase_order() 161 | self.remove(words[1]) 162 | self.add(words[2]) 163 | self.dither(6) 164 | 165 | class StraightKoch(KochCurve): 166 | CONFIG = { 167 | "axiom" : "A" 168 | } 169 | 170 | class SharperKoch(StraightKoch): 171 | CONFIG = { 172 | "angle" : 0.9*np.pi/2, 173 | } 174 | 175 | class DullerKoch(StraightKoch): 176 | CONFIG = { 177 | "angle" : np.pi/6, 178 | } 179 | 180 | class SpaceFillingKoch(StraightKoch): 181 | CONFIG = { 182 | "angle" : np.pi/2, 183 | } 184 | 185 | 186 | 187 | class FromKochToSpaceFilling(Scene): 188 | def construct(self): 189 | self.max_order = 7 190 | 191 | self.revisit_koch() 192 | self.show_angles() 193 | self.show_change_side_by_side() 194 | 195 | 196 | def revisit_koch(self): 197 | words = map(TextMobject, [ 198 | "First, look at how one section of this curve is made.", 199 | "This pattern of four lines is the ``seed''", 200 | "With each iteration, every straight line is \ 201 | replaced with an appropriately small copy of the seed", 202 | ]) 203 | for text in words: 204 | text.to_edge(UP) 205 | 206 | self.add(words[0]) 207 | curve = StraightKoch(order = self.max_order) 208 | self.play(Transform( 209 | curve, 210 | StraightKoch(order = 1), 211 | run_time = 5 212 | )) 213 | self.remove(words[0]) 214 | self.add(words[1]) 215 | self.dither(4) 216 | self.remove(words[1]) 217 | self.add(words[2]) 218 | self.dither(3) 219 | for order in range(2, self.max_order): 220 | self.play(Transform( 221 | curve, 222 | StraightKoch(order = order) 223 | )) 224 | if order == 2: 225 | self.dither(2) 226 | elif order == 3: 227 | self.dither() 228 | self.clear() 229 | 230 | 231 | 232 | def show_angles(self): 233 | words = TextMobject(""" 234 | Let's see what happens as we change 235 | the angle in this seed 236 | """) 237 | words.to_edge(UP) 238 | koch, sharper_koch, duller_koch = curves = [ 239 | CurveClass(order = 1) 240 | for CurveClass in StraightKoch, SharperKoch, DullerKoch 241 | ] 242 | arcs = [ 243 | Arc( 244 | 2*(np.pi/2 - curve.angle), 245 | radius = r, 246 | start_angle = np.pi+curve.angle 247 | ).shift(curve.points[curve.get_num_points()/2]) 248 | for curve, r in zip(curves, [0.6, 0.7, 0.4]) 249 | ] 250 | theta = TexMobject("\\theta") 251 | theta.shift(arcs[0].get_center()+2.5*DOWN) 252 | arrow = Arrow(theta, arcs[0]) 253 | 254 | self.add(words, koch) 255 | self.play(ShowCreation(arcs[0])) 256 | self.play( 257 | ShowCreation(arrow), 258 | ShimmerIn(theta) 259 | ) 260 | self.dither(2) 261 | self.remove(theta, arrow) 262 | self.play( 263 | Transform(koch, duller_koch), 264 | Transform(arcs[0], arcs[2]), 265 | ) 266 | self.play( 267 | Transform(koch, sharper_koch), 268 | Transform(arcs[0], arcs[1]), 269 | ) 270 | self.clear() 271 | 272 | def show_change_side_by_side(self): 273 | 274 | seed = TextMobject("Seed") 275 | seed.shift(3*LEFT+2*DOWN) 276 | fractal = TextMobject("Fractal") 277 | fractal.shift(3*RIGHT+2*DOWN) 278 | words = map(TextMobject, [ 279 | "A sharper angle results in a richer curve", 280 | "A more obtuse angle gives a sparser curve", 281 | "And as the angle approaches 0\\dots", 282 | "We have a new space-filling curve." 283 | ]) 284 | for text in words: 285 | text.to_edge(UP) 286 | sharper, duller, space_filling = [ 287 | CurveClass(order = 1).shift(3*LEFT) 288 | for CurveClass in SharperKoch, DullerKoch, SpaceFillingKoch 289 | ] 290 | shaper_f, duller_f, space_filling_f = [ 291 | CurveClass(order = self.max_order).shift(3*RIGHT) 292 | for CurveClass in SharperKoch, DullerKoch, SpaceFillingKoch 293 | ] 294 | 295 | self.add(words[0]) 296 | left_curve = SharperKoch(order = 1) 297 | right_curve = SharperKoch(order = 1) 298 | self.play( 299 | Transform(left_curve, sharper), 300 | ApplyMethod(right_curve.shift, 3*RIGHT), 301 | ) 302 | self.play( 303 | Transform( 304 | right_curve, 305 | SharperKoch(order = 2).shift(3*RIGHT) 306 | ), 307 | ShimmerIn(seed), 308 | ShimmerIn(fractal) 309 | ) 310 | for order in range(3, self.max_order): 311 | self.play(Transform( 312 | right_curve, 313 | SharperKoch(order = order).shift(3*RIGHT) 314 | )) 315 | self.remove(words[0]) 316 | self.add(words[1]) 317 | kwargs = { 318 | "run_time" : 4, 319 | } 320 | self.play( 321 | Transform(left_curve, duller, **kwargs), 322 | Transform(right_curve, duller_f, **kwargs) 323 | ) 324 | self.dither() 325 | kwargs["run_time"] = 7 326 | kwargs["rate_func"] = None 327 | self.remove(words[1]) 328 | self.add(words[2]) 329 | self.play( 330 | Transform(left_curve, space_filling, **kwargs), 331 | Transform(right_curve, space_filling_f, **kwargs) 332 | ) 333 | self.remove(words[2]) 334 | self.add(words[3]) 335 | self.dither() 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | -------------------------------------------------------------------------------- /old_projects/hilbert/section3.py: -------------------------------------------------------------------------------- 1 | from mobject import Mobject, Point 2 | from mobject.tex_mobject import \ 3 | TexMobject, TextMobject, Brace 4 | from mobject.image_mobject import \ 5 | ImageMobject, MobjectFromRegion 6 | 7 | from scene import Scene 8 | 9 | from animation import Animation 10 | from animation.transform import \ 11 | Transform, CounterclockwiseTransform, ApplyMethod,\ 12 | GrowFromCenter, ClockwiseTransform, ApplyPointwiseFunction,\ 13 | TransformAnimations, ShimmerIn, FadeOut, FadeIn 14 | from animation.simple_animations import \ 15 | ShowCreation, DelayByOrder 16 | from animation.playground import Vibrate 17 | 18 | from topics.geometry import \ 19 | Line, Dot, Arrow, Grid, Square, Point, Polygon 20 | from topics.characters import \ 21 | ThoughtBubble, SpeechBubble, Mathematician, Mortimer 22 | from topics.number_line import UnitInterval 23 | from topics.three_dimensions import Stars 24 | 25 | from mobject.region import region_from_polygon_vertices 26 | 27 | import displayer as disp 28 | 29 | from hilbert.curves import \ 30 | TransformOverIncreasingOrders, FlowSnake, HilbertCurve, \ 31 | SnakeCurve, Sierpinski 32 | from hilbert.section1 import get_mathy_and_bubble 33 | 34 | 35 | from helpers import * 36 | 37 | 38 | 39 | class SectionThree(Scene): 40 | def construct(self): 41 | self.add(TextMobject("A few words on the usefulness of infinite math")) 42 | self.dither() 43 | 44 | 45 | class InfiniteResultsFiniteWorld(Scene): 46 | def construct(self): 47 | left_words = TextMobject("Infinite result") 48 | right_words = TextMobject("Finite world") 49 | for words in left_words, right_words: 50 | words.scale(0.8) 51 | left_formula = TexMobject( 52 | "\\sum_{n = 0}^{\\infty} 2^n = -1" 53 | ) 54 | right_formula = TexMobject("111\\cdots111") 55 | for formula in left_formula, right_formula: 56 | formula.add( 57 | Brace(formula, UP), 58 | ) 59 | formula.ingest_submobjects() 60 | right_overwords = TextMobject( 61 | "\\substack{\ 62 | \\text{How computers} \\\\ \ 63 | \\text{represent $-1$}\ 64 | }" 65 | ).scale(1.5) 66 | 67 | left_mobs = [left_words, left_formula] 68 | right_mobs = [right_words, right_formula] 69 | for mob in left_mobs: 70 | mob.to_edge(RIGHT, buff = 1) 71 | mob.shift(SPACE_WIDTH*LEFT) 72 | for mob in right_mobs: 73 | mob.to_edge(LEFT, buff = 1) 74 | mob.shift(SPACE_WIDTH*RIGHT) 75 | arrow = Arrow(left_words, right_words) 76 | right_overwords.next_to(right_formula, UP) 77 | 78 | self.play(ShimmerIn(left_words)) 79 | self.play(ShowCreation(arrow)) 80 | self.play(ShimmerIn(right_words)) 81 | self.dither() 82 | self.play( 83 | ShimmerIn(left_formula), 84 | ApplyMethod(left_words.next_to, left_formula, UP) 85 | ) 86 | self.dither() 87 | self.play( 88 | ShimmerIn(right_formula), 89 | Transform(right_words, right_overwords) 90 | ) 91 | self.dither() 92 | self.finite_analog( 93 | Mobject(left_formula, left_words), 94 | arrow, 95 | Mobject(right_formula, right_words) 96 | ) 97 | 98 | 99 | def finite_analog(self, left_mob, arrow, right_mob): 100 | self.clear() 101 | self.add(left_mob, arrow, right_mob) 102 | ex = TextMobject("\\times") 103 | ex.highlight(RED) 104 | # ex.shift(arrow.get_center()) 105 | middle = TexMobject( 106 | "\\sum_{n=0}^N 2^n \\equiv -1 \\mod 2^{N+1}" 107 | ) 108 | finite_analog = TextMobject("Finite analog") 109 | finite_analog.scale(0.8) 110 | brace = Brace(middle, UP) 111 | finite_analog.next_to(brace, UP) 112 | new_left = left_mob.copy().to_edge(LEFT) 113 | new_right = right_mob.copy().to_edge(RIGHT) 114 | left_arrow, right_arrow = [ 115 | Arrow( 116 | mob1.get_right()[0]*RIGHT, 117 | mob2.get_left()[0]*RIGHT, 118 | buff = 0 119 | ) 120 | for mob1, mob2 in [ 121 | (new_left, middle), 122 | (middle, new_right) 123 | ] 124 | ] 125 | for mob in ex, middle: 126 | mob.sort_points(np.linalg.norm) 127 | 128 | self.play(GrowFromCenter(ex)) 129 | self.dither() 130 | self.play( 131 | Transform(left_mob, new_left), 132 | Transform(arrow.copy(), left_arrow), 133 | DelayByOrder(Transform(ex, middle)), 134 | Transform(arrow, right_arrow), 135 | Transform(right_mob, new_right) 136 | ) 137 | self.play( 138 | GrowFromCenter(brace), 139 | ShimmerIn(finite_analog) 140 | ) 141 | self.dither() 142 | self.equivalence( 143 | left_mob, 144 | left_arrow, 145 | Mobject(middle, brace, finite_analog) 146 | ) 147 | 148 | def equivalence(self, left_mob, arrow, right_mob): 149 | self.clear() 150 | self.add(left_mob, arrow, right_mob) 151 | words = TextMobject("is equivalent to") 152 | words.shift(0.25*LEFT) 153 | words.highlight(BLUE) 154 | new_left = left_mob.copy().shift(RIGHT) 155 | new_right = right_mob.copy() 156 | new_right.shift( 157 | (words.get_right()[0]-\ 158 | right_mob.get_left()[0]+\ 159 | 0.5 160 | )*RIGHT 161 | ) 162 | for mob in arrow, words: 163 | mob.sort_points(np.linalg.norm) 164 | 165 | self.play( 166 | ApplyMethod(left_mob.shift, RIGHT), 167 | Transform(arrow, words), 168 | ApplyMethod(right_mob.to_edge, RIGHT) 169 | ) 170 | self.dither() 171 | 172 | 173 | class HilbertCurvesStayStable(Scene): 174 | def construct(self): 175 | scale_factor = 0.9 176 | grid = Grid(4, 4, stroke_width = 1) 177 | curve = HilbertCurve(order = 2) 178 | for mob in grid, curve: 179 | mob.scale(scale_factor) 180 | words = TextMobject(""" 181 | Sequence of curves is stable 182 | $\\leftrightarrow$ existence of limit curve 183 | """, size = "\\normal") 184 | words.scale(1.25) 185 | words.to_edge(UP) 186 | 187 | self.add(curve, grid) 188 | self.dither() 189 | for n in range(3, 7): 190 | if n == 5: 191 | self.play(ShimmerIn(words)) 192 | new_grid = Grid(2**n, 2**n, stroke_width = 1) 193 | new_curve = HilbertCurve(order = n) 194 | for mob in new_grid, new_curve: 195 | mob.scale(scale_factor) 196 | self.play( 197 | ShowCreation(new_grid), 198 | Animation(curve) 199 | ) 200 | self.remove(grid) 201 | grid = new_grid 202 | self.play(Transform(curve, new_curve)) 203 | self.dither() 204 | 205 | 206 | 207 | class InfiniteObjectsEncapsulateFiniteObjects(Scene): 208 | def get_triangles(self): 209 | triangle = Polygon( 210 | LEFT/np.sqrt(3), 211 | UP, 212 | RIGHT/np.sqrt(3), 213 | color = GREEN 214 | ) 215 | triangles = Mobject( 216 | triangle.copy().scale(0.5).shift(LEFT), 217 | triangle, 218 | triangle.copy().scale(0.3).shift(0.5*UP+RIGHT) 219 | ) 220 | triangles.center() 221 | return triangles 222 | 223 | def construct(self): 224 | words =[ 225 | TextMobject(text, size = "\\large") 226 | for text in [ 227 | "Truths about infinite objects", 228 | " encapsulate ", 229 | "facts about finite objects" 230 | ] 231 | ] 232 | 233 | words[0].highlight(RED) 234 | words[1].next_to(words[0]) 235 | words[2].highlight(GREEN).next_to(words[1]) 236 | Mobject(*words).center().to_edge(UP) 237 | infinite_objects = [ 238 | TexMobject( 239 | "\\sum_{n=0}^\\infty", 240 | size = "\\normal" 241 | ).highlight(RED_E), 242 | Sierpinski(order = 8).scale(0.3), 243 | TextMobject( 244 | "$\\exists$ something infinite $\\dots$" 245 | ).highlight(RED_B) 246 | ] 247 | finite_objects = [ 248 | TexMobject( 249 | "\\sum_{n=0}^N", 250 | size = "\\normal" 251 | ).highlight(GREEN_E), 252 | self.get_triangles(), 253 | TextMobject( 254 | "$\\forall$ finite somethings $\\dots$" 255 | ).highlight(GREEN_B) 256 | ] 257 | for infinite, finite, n in zip(infinite_objects, finite_objects, it.count(1, 2)): 258 | infinite.next_to(words[0], DOWN, buff = n) 259 | finite.next_to(words[2], DOWN, buff = n) 260 | 261 | self.play(ShimmerIn(words[0])) 262 | self.dither() 263 | self.play(ShimmerIn(infinite_objects[0])) 264 | self.play(ShowCreation(infinite_objects[1])) 265 | self.play(ShimmerIn(infinite_objects[2])) 266 | self.dither() 267 | self.play(ShimmerIn(words[1]), ShimmerIn(words[2])) 268 | self.play(ShimmerIn(finite_objects[0])) 269 | self.play(ShowCreation(finite_objects[1])) 270 | self.play(ShimmerIn(finite_objects[2])) 271 | self.dither() 272 | 273 | 274 | class StatementRemovedFromReality(Scene): 275 | def construct(self): 276 | mathy, bubble = get_mathy_and_bubble() 277 | bubble.stretch_to_fit_width(4) 278 | mathy.to_corner(DOWN+LEFT) 279 | bubble.pin_to(mathy) 280 | bubble.shift(LEFT) 281 | morty = Mortimer() 282 | morty.to_corner(DOWN+RIGHT) 283 | morty_bubble = SpeechBubble() 284 | morty_bubble.stretch_to_fit_width(4) 285 | morty_bubble.pin_to(morty) 286 | bubble.write(""" 287 | Did you know a curve \\\\ 288 | can fill all space? 289 | """) 290 | morty_bubble.write("Who cares?") 291 | 292 | self.add(mathy, morty) 293 | for bub, buddy in [(bubble, mathy), (morty_bubble, morty)]: 294 | self.play(Transform( 295 | Point(bub.get_tip()), 296 | bub 297 | )) 298 | self.play(ShimmerIn(bub.content)) 299 | self.play(ApplyMethod( 300 | buddy.blink, 301 | rate_func = squish_rate_func(there_and_back) 302 | )) 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | -------------------------------------------------------------------------------- /old_projects/inventing_math_images.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | import itertools as it 5 | from copy import deepcopy 6 | import sys 7 | 8 | 9 | from animation import * 10 | from mobject import * 11 | from constants import * 12 | from mobject.region import * 13 | from scene import Scene 14 | from script_wrapper import command_line_create_scene 15 | from inventing_math import divergent_sum, draw_you 16 | 17 | 18 | 19 | class SimpleText(Scene): 20 | args_list = [ 21 | ("Build the foundation of what we know",), 22 | ("What would that feel like?",), 23 | ("Arbitrary decisions hinder generality",), 24 | ("Section 1: Discovering and Defining Infinite Sums",), 25 | ("Section 2: Seeking Generality",), 26 | ("Section 3: Redefining Distance",), 27 | ("``Approach''?",), 28 | ("Rigor would dictate you ignore these",), 29 | ("dist($A$, $B$) = dist($A+x$, $B+x$) \\quad for all $x$",), 30 | ("How does a useful distance function differ from a random function?",), 31 | ("Pause now, if you like, and see if you can invent your own distance function from this.",), 32 | ("$p$-adic metrics \\\\ ($p$ is any prime number)",), 33 | ("This is not meant to match the history of discoveries",), 34 | ] 35 | @staticmethod 36 | def args_to_string(text): 37 | return initials(filter(lambda c : c in string.letters + " ", text)) 38 | 39 | def construct(self, text): 40 | self.add(TextMobject(text)) 41 | 42 | 43 | class SimpleTex(Scene): 44 | args_list = [ 45 | ( 46 | "\\frac{9}{10}+\\frac{9}{100}+\\frac{9}{1000}+\\cdots = 1", 47 | "SumOf9s" 48 | ), 49 | ( 50 | "0 < p < 1", 51 | "PBetween0And1" 52 | ), 53 | ] 54 | @staticmethod 55 | def args_to_string(expression, words): 56 | return words 57 | 58 | def construct(self, expression, words): 59 | self.add(TexMobject(expression)) 60 | 61 | 62 | class OneMinusOnePoem(Scene): 63 | def construct(self): 64 | verse1 = TextMobject(""" 65 | \\begin{flushleft} 66 | When one takes one from one \\\\ 67 | plus one from one plus one \\\\ 68 | and on and on but ends \\\\ 69 | anon then starts again, \\\\ 70 | then some sums sum to one, \\\\ 71 | to zero other ones. \\\\ 72 | One wonders who'd have won \\\\ 73 | had stopping not been done; \\\\ 74 | had he summed every bit \\\\ 75 | until the infinite. \\\\ 76 | \\end{flushleft} 77 | """).scale(0.5).to_corner(UP+LEFT) 78 | verse2 = TextMobject(""" 79 | \\begin{flushleft} 80 | Lest you should think that such \\\\ 81 | less well-known sums are much \\\\ 82 | ado about nonsense \\\\ 83 | I do give these two cents: \\\\ 84 | The universe has got \\\\ 85 | an answer which is not \\\\ 86 | what most would first surmise, \\\\ 87 | it is a compromise, \\\\ 88 | and though it seems a laugh \\\\ 89 | the universe gives ``half''. \\\\ 90 | \\end{flushleft} 91 | """).scale(0.5).to_corner(DOWN+LEFT) 92 | equation = TexMobject( 93 | "1-1+1-1+\\cdots = \\frac{1}{2}" 94 | ) 95 | self.add(verse1, verse2, equation) 96 | 97 | class DivergentSum(Scene): 98 | def construct(self): 99 | self.add(divergent_sum().scale(0.75)) 100 | 101 | 102 | class PowersOfTwoSmall(Scene): 103 | def construct(self): 104 | you, bubble = draw_you(with_bubble=True) 105 | bubble.write( 106 | "Is there any way in which apparently \ 107 | large powers of two can be considered small?" 108 | ) 109 | self.add(you, bubble, bubble.content) 110 | 111 | 112 | class FinalSlide(Scene): 113 | def construct(self): 114 | self.add(TextMobject(""" 115 | \\begin{flushleft} 116 | Needless to say, what I said here only scratches the 117 | surface of the tip of the iceberg of the p-adic metric. 118 | What is this new form of number I referred to? 119 | Why were distances in the 2-adic metric all powers of 120 | $\\frac{1}{2}$ and not some other base? 121 | Why does it only work for prime numbers? \\\\ 122 | \\quad \\\\ 123 | I highly encourage anyone who has not seen p-adic numbers 124 | to look them up and learn more, but even more edifying than 125 | looking them up will be to explore this idea for yourself directly. 126 | What properties make a distance function useful, and why? 127 | What do I mean by ``useful''? Useful for what purpose? 128 | Can you find infinite sums or sequences which feel like 129 | they should converge in the 2-adic metric, but don't converge 130 | to a rational number? Go on! Search! Invent! 131 | \\end{flushleft} 132 | """, size = "\\small")) 133 | 134 | 135 | 136 | 137 | -------------------------------------------------------------------------------- /old_projects/moser_intro.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | import itertools as it 5 | import operator as op 6 | from copy import deepcopy 7 | 8 | 9 | from animation import * 10 | from mobject import * 11 | from constants import * 12 | from mobject.region import * 13 | from scene import Scene 14 | 15 | RADIUS = SPACE_HEIGHT - 0.1 16 | CIRCLE_DENSITY = DEFAULT_POINT_DENSITY_1D*RADIUS 17 | 18 | 19 | def logo_to_circle(): 20 | from generate_logo import DARK_BROWN, LOGO_RADIUS 21 | sc = Scene() 22 | small_circle = Circle( 23 | density = CIRCLE_DENSITY, 24 | color = 'skyblue' 25 | ).scale(LOGO_RADIUS).highlight( 26 | DARK_BROWN, lambda (x, y, z) : x < 0 and y > 0 27 | ) 28 | big_circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 29 | sc.add(small_circle) 30 | sc.dither()` 31 | sc.animate(Transform(small_circle, big_circle)) 32 | return sc 33 | 34 | def count_sections(*radians): 35 | sc = Scene() 36 | circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 37 | sc.add(circle) 38 | points = [ 39 | (RADIUS * np.cos(angle), RADIUS * np.sin(angle), 0) 40 | for angle in radians 41 | ] 42 | dots = [Dot(point) for point in points] 43 | interior = Region(lambda x, y : x**2 + y**2 < RADIUS**2) 44 | for x in xrange(1, len(points)): 45 | if x == 1: 46 | sc.animate(ShowCreation(dots[0]), ShowCreation(dots[1])) 47 | sc.add(dots[0], dots[1]) 48 | else: 49 | sc.animate(ShowCreation(dots[x])) 50 | sc.add(dots[x]) 51 | new_lines = Mobject(*[ 52 | Line(points[x], points[y]) for y in xrange(x) 53 | ]) 54 | sc.animate(Transform(deepcopy(dots[x]), new_lines, run_time = 2.0)) 55 | sc.add(new_lines) 56 | sc.dither() 57 | regions = plane_partition_from_points(*points[:x+1]) 58 | for reg in regions: 59 | reg.intersect(interior) 60 | regions = filter(lambda reg : reg.bool_grid.any(), regions) 61 | 62 | last_num = None 63 | for reg, count in zip(regions, it.count(1)): 64 | number = TexMobject(str(count)).shift((RADIUS, 3, 0)) 65 | sc.highlight_region(reg) 66 | rt = 1.0 / (x**0.8) 67 | sc.add(number) 68 | sc.remove(last_num) 69 | last_num = number 70 | sc.dither(rt) 71 | sc.reset_background() 72 | sc.remove(last_num) 73 | sc.animate(Transform(last_num, deepcopy(last_num).center())) 74 | sc.dither() 75 | sc.remove(last_num) 76 | return sc 77 | 78 | def summarize_pattern(*radians): 79 | sc = Scene() 80 | circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 81 | sc.add(circle) 82 | points = [ 83 | (RADIUS * np.cos(angle), RADIUS * np.sin(angle), 0) 84 | for angle in radians 85 | ] 86 | dots = [Dot(point) for point in points] 87 | last_num = None 88 | for x in xrange(len(points)): 89 | new_lines = Mobject(*[ 90 | Line(points[x], points[y]) for y in xrange(x) 91 | ]) 92 | num = TexMobject(str(moser_function(x + 1))).center() 93 | sc.animate( 94 | Transform(last_num, num) if last_num else ShowCreation(num), 95 | FadeIn(new_lines), 96 | FadeIn(dots[x]), 97 | run_time = 0.5, 98 | ) 99 | sc.remove(last_num) 100 | last_num = num 101 | sc.add(num, dots[x], new_lines) 102 | sc.dither() 103 | return sc 104 | 105 | def connect_points(*radians): 106 | sc = Scene() 107 | circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 108 | sc.add(circle) 109 | points = [ 110 | (RADIUS * np.cos(angle), RADIUS * np.sin(angle), 0) 111 | for angle in radians 112 | ] 113 | dots = [Dot(point) for point in points] 114 | sc.add(*dots) 115 | anims = [] 116 | all_lines = [] 117 | for x in xrange(len(points)): 118 | lines = [Line(points[x], points[y]) for y in range(len(points))] 119 | lines = Mobject(*lines) 120 | anims.append(Transform(deepcopy(dots[x]), lines, run_time = 3.0)) 121 | all_lines.append(lines) 122 | sc.animate(*anims) 123 | sc.add(*all_lines) 124 | sc.dither() 125 | return sc 126 | 127 | def interesting_problems(): 128 | sc = Scene() 129 | locales = [(6, 2, 0), (6, -2, 0), (-5, -2, 0)] 130 | fermat = Mobject(*TexMobjects(["x^n","+","y^n","=","z^n"])) 131 | fermat.scale(0.5).shift((-2.5, 0.7, 0)) 132 | face = SimpleFace() 133 | tb = ThoughtBubble().shift((-1.5, 1, 0)) 134 | sb = SpeechBubble().shift((-2.4, 1.3, 0)) 135 | fermat_copies, face_copies, tb_copies, sb_copies = ( 136 | Mobject(*[ 137 | deepcopy(mob).scale(0.5).shift(locale) 138 | for locale in locales 139 | ]) 140 | for mob in [fermat, face, tb, sb] 141 | ) 142 | 143 | sc.add(face, tb) 144 | sc.animate(ShowCreation(fermat, run_time = 1)) 145 | sc.add(fermat) 146 | sc.dither() 147 | sc.animate( 148 | Transform( 149 | deepcopy(fermat).repeat(len(locales)), 150 | fermat_copies 151 | ), 152 | FadeIn(face_copies, run_time = 1.0) 153 | ) 154 | sc.animate(FadeIn(tb_copies)) 155 | sc.dither() 156 | sc.animate( 157 | Transform(tb, sb), 158 | Transform(tb_copies, sb_copies) 159 | ) 160 | return sc 161 | 162 | def response_invitation(): 163 | sc = Scene() 164 | video_icon = VideoIcon() 165 | mini_videos = Mobject(*[ 166 | deepcopy(video_icon).scale(0.5).shift((3, y, 0)) 167 | for y in [-2, 0, 2] 168 | ]) 169 | comments = Mobject(*[ 170 | Line((-1.2, y, 0), (1.2, y, 0), color = 'white') 171 | for y in [-1.5, -1.75, -2] 172 | ]) 173 | 174 | sc.add(video_icon) 175 | sc.dither() 176 | sc.animate(Transform(deepcopy(video_icon).repeat(3), mini_videos)) 177 | sc.add(mini_videos) 178 | sc.dither() 179 | sc.animate(ShowCreation(comments, run_time = 1.0)) 180 | return sc 181 | 182 | def different_points(radians1, radians2): 183 | sc = Scene() 184 | circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 185 | sc.add(circle) 186 | points1, points2 = ( 187 | [ 188 | (RADIUS * np.cos(angle), RADIUS * np.sin(angle), 0) 189 | for angle in radians 190 | ] 191 | for radians in (radians1, radians2) 192 | ) 193 | dots1, dots2 = ( 194 | Mobject(*[Dot(point) for point in points]) 195 | for points in (points1, points2) 196 | ) 197 | lines1, lines2 = ( 198 | [ 199 | Line(point1, point2) 200 | for point1, point2 in it.combinations(points, 2) 201 | ] 202 | for points in (points1, points2) 203 | ) 204 | sc.add(dots1, *lines1) 205 | sc.animate( 206 | Transform(dots1, dots2, run_time = 3), 207 | *[ 208 | Transform(line1, line2, run_time = 3) 209 | for line1, line2 in zip(lines1, lines2) 210 | ] 211 | ) 212 | sc.dither() 213 | return sc 214 | 215 | def next_few_videos(*radians): 216 | sc = Scene() 217 | circle = Circle(density = CIRCLE_DENSITY).scale(RADIUS) 218 | points = [ 219 | (RADIUS * np.cos(angle), RADIUS * np.sin(angle), 0) 220 | for angle in radians 221 | ] 222 | dots = Mobject(*[ 223 | Dot(point) for point in points 224 | ]) 225 | lines = Mobject(*[ 226 | Line(point1, point2) 227 | for point1, point2 in it.combinations(points, 2) 228 | ]) 229 | thumbnail = Mobject(circle, dots, lines) 230 | frame = VideoIcon().highlight( 231 | "black", 232 | lambda point : np.linalg.norm(point) < 0.5 233 | ) 234 | big_frame = deepcopy(frame).scale(SPACE_WIDTH) 235 | frame.shift((-5, 0, 0)) 236 | 237 | sc.add(thumbnail) 238 | sc.dither() 239 | sc.animate( 240 | Transform(big_frame, frame), 241 | Transform( 242 | thumbnail, 243 | deepcopy(thumbnail).scale(0.15).shift((-5, 0, 0)) 244 | ) 245 | ) 246 | sc.add(frame, thumbnail) 247 | sc.dither() 248 | last = frame 249 | for x in [-2, 1, 4]: 250 | vi = VideoIcon().shift((x, 0, 0)) 251 | sc.animate( 252 | Transform(deepcopy(last), vi), 253 | Animation(thumbnail)#Keeps it from getting burried 254 | ) 255 | sc.add(vi) 256 | last = vi 257 | return sc 258 | 259 | 260 | 261 | if __name__ == '__main__': 262 | radians = [1, 3, 5, 2, 4, 6] 263 | more_radians = radians + [10, 13, 20, 17, 15, 21, 18.5] 264 | different_radians = [1.7, 4.8, 3.2, 3.5, 2.1, 5.5] 265 | # logo_to_circle().write_to_movie("moser/LogoToCircle") 266 | # count_sections(*radians).write_to_movie("moser/CountingSections") 267 | # summarize_pattern(*radians).write_to_movie("moser/SummarizePattern") 268 | # interesting_problems().write_to_movie("moser/InterestingProblems") 269 | # summarize_pattern(*more_radians).write_to_movie("moser/ExtendedPattern") 270 | # connect_points(*radians).write_to_movie("moser/ConnectPoints") 271 | # response_invitation().write_to_movie("moser/ResponseInvitation") 272 | # different_points(radians, different_radians).write_to_movie("moser/DifferentPoints") 273 | # next_few_videos(*radians).write_to_movie("moser/NextFewVideos") 274 | # summarize_pattern(*different_radians).write_to_movie("moser/PatternWithDifferentPoints") 275 | 276 | #Images 277 | # TexMobject(r""" 278 | # \Underbrace{1, 2, 4, 8, 16, 31, \dots}_\text{What?} 279 | # """).save_image("moser/NumberList31") 280 | # TexMobject(""" 281 | # 1, 2, 4, 8, 16, 32, 63, \dots 282 | # """).save_image("moser/NumberList63") 283 | # TexMobject(""" 284 | # 1, 2, 4, 8, 15, \dots 285 | # """).save_image("moser/NumberList15") 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | -------------------------------------------------------------------------------- /old_projects/number_line_scene.py: -------------------------------------------------------------------------------- 1 | class NumberLineScene(Scene): 2 | def construct(self, **number_line_config): 3 | self.number_line = NumberLine(**number_line_config) 4 | self.displayed_numbers = self.number_line.default_numbers_to_display() 5 | self.number_mobs = self.number_line.get_number_mobjects(*self.displayed_numbers) 6 | self.add(self.number_line, *self.number_mobs) 7 | 8 | def zoom_in_on(self, number, zoom_factor, run_time = 2.0): 9 | unit_length_to_spatial_width = self.number_line.unit_length_to_spatial_width*zoom_factor 10 | radius = SPACE_WIDTH/unit_length_to_spatial_width 11 | tick_frequency = 10**(np.floor(np.log10(radius))) 12 | left_tick = tick_frequency*(np.ceil((number-radius)/tick_frequency)) 13 | new_number_line = NumberLine( 14 | numerical_radius = radius, 15 | unit_length_to_spatial_width = unit_length_to_spatial_width, 16 | tick_frequency = tick_frequency, 17 | leftmost_tick = left_tick, 18 | number_at_center = number 19 | ) 20 | new_displayed_numbers = new_number_line.default_numbers_to_display() 21 | new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers) 22 | 23 | transforms = [] 24 | additional_mobjects = [] 25 | squished_new_line = new_number_line.copy() 26 | squished_new_line.scale(1.0/zoom_factor) 27 | squished_new_line.shift(self.number_line.number_to_point(number)) 28 | squished_new_line.points[:,1] = self.number_line.number_to_point(0)[1] 29 | transforms.append(Transform(squished_new_line, new_number_line)) 30 | for mob, num in zip(new_number_mobs, new_displayed_numbers): 31 | point = Point(self.number_line.number_to_point(num)) 32 | point.shift(new_number_line.get_vertical_number_offset()) 33 | transforms.append(Transform(point, mob)) 34 | for mob in self.mobjects: 35 | if mob == self.number_line: 36 | new_mob = mob.copy() 37 | new_mob.shift(-self.number_line.number_to_point(number)) 38 | new_mob.stretch(zoom_factor, 0) 39 | transforms.append(Transform(mob, new_mob)) 40 | continue 41 | mob_center = mob.get_center() 42 | number_under_center = self.number_line.point_to_number(mob_center) 43 | new_point = new_number_line.number_to_point(number_under_center) 44 | new_point += mob_center[1]*UP 45 | if mob in self.number_mobs: 46 | transforms.append(Transform(mob, Point(new_point))) 47 | else: 48 | transforms.append(ApplyMethod(mob.shift, new_point - mob_center)) 49 | additional_mobjects.append(mob) 50 | line_to_hide_pixelation = Line( 51 | self.number_line.get_left(), 52 | self.number_line.get_right(), 53 | color = self.number_line.get_color() 54 | ) 55 | self.add(line_to_hide_pixelation) 56 | self.play(*transforms, run_time = run_time) 57 | self.clear() 58 | self.number_line = new_number_line 59 | self.displayed_numbers = new_displayed_numbers 60 | self.number_mobs = new_number_mobs 61 | self.add(self.number_line, *self.number_mobs) 62 | self.add(*additional_mobjects) 63 | 64 | def show_multiplication(self, num, **kwargs): 65 | if "path_func" not in kwargs: 66 | if num > 0: 67 | kwargs["path_func"] = straight_path 68 | else: 69 | kwargs["path_func"] = counterclockwise_path() 70 | self.play(*[ 71 | ApplyMethod(self.number_line.stretch, num, 0, **kwargs) 72 | ]+[ 73 | ApplyMethod(mob.shift, (num-1)*mob.get_center()[0]*RIGHT, **kwargs) 74 | for mob in self.number_mobs 75 | ]) 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /old_projects/playground_counting_in_binary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import numpy as np 4 | import itertools as it 5 | from copy import deepcopy 6 | import sys 7 | 8 | 9 | from animation import * 10 | from mobject import * 11 | from constants import * 12 | from mobject.region import * 13 | from scene import Scene, SceneFromVideo 14 | from script_wrapper import command_line_create_scene 15 | 16 | MOVIE_PREFIX = "counting_in_binary/" 17 | 18 | COUNT_TO_FRAME_NUM = { 19 | 0 : 0, 20 | 1 : 53, 21 | 2 : 84, 22 | 3 : 128, 23 | 4 : 169, 24 | 5 : 208, 25 | 6 : 238, 26 | 7 : 281, 27 | 8 : 331, 28 | 9 : 365, 29 | 10 : 395, 30 | 11 : 435, 31 | 12 : 475, 32 | 13 : 518, 33 | 14 : 556, 34 | 15 : 595, 35 | 16 : 636, 36 | 17 : 676, 37 | 18 : 709, 38 | 19 : 753, 39 | 20 : 790, 40 | 21 : 835, 41 | 22 : 869, 42 | 23 : 903, 43 | 24 : 950, 44 | 25 : 988, 45 | 26 : 1027, 46 | 27 : 1065, 47 | 28 : 1104, 48 | 29 : 1145, 49 | 30 : 1181, 50 | 31 : 1224, 51 | 32 : 1239, 52 | } 53 | 54 | class Hand(ImageMobject): 55 | def __init__(self, num, **kwargs): 56 | Mobject2D.__init__(self, **kwargs) 57 | path = os.path.join( 58 | MOVIE_DIR, MOVIE_PREFIX, "images", "Hand%d.png"%num 59 | ) 60 | invert = False 61 | if self.read_in_cached_attrs(path, invert): 62 | return 63 | ImageMobject.__init__(self, path, invert) 64 | center = self.get_center() 65 | self.center() 66 | self.rotate(np.pi, axis = RIGHT+UP) 67 | self.sort_points(lambda p : np.log(complex(*p[:2])).imag) 68 | self.rotate(np.pi, axis = RIGHT+UP) 69 | self.shift(center) 70 | self.cache_attrs(path, invert = False) 71 | 72 | 73 | class EdgeDetection(SceneFromVideo): 74 | args_list = [ 75 | ("CountingInBinary.m4v", 35, 70), 76 | ("CountingInBinary.m4v", 0, 100), 77 | ("CountingInBinary.m4v", 10, 50), 78 | ] 79 | @staticmethod 80 | def args_to_string(filename, t1, t2): 81 | return "-".join([filename.split(".")[0], str(t1), str(t2)]) 82 | 83 | def construct(self, filename, t1, t2): 84 | path = os.path.join(MOVIE_DIR, filename) 85 | SceneFromVideo.construct(self, path) 86 | self.apply_gaussian_blur() 87 | self.apply_edge_detection(t1, t2) 88 | 89 | class BufferedCounting(SceneFromVideo): 90 | def construct(self): 91 | path = os.path.join(MOVIE_DIR, "CountingInBinary.m4v") 92 | time_range = (3, 42) 93 | SceneFromVideo.construct(self, path, time_range = time_range) 94 | self.buffer_pixels(spreads = (3, 2)) 95 | # self.make_all_black_or_white() 96 | 97 | def buffer_pixels(self, spreads = (2, 2)): 98 | ksize = (5, 5) 99 | sigmaX = 10 100 | threshold1 = 35 101 | threshold2 = 70 102 | 103 | matrices = [ 104 | thick_diagonal(dim, spread) 105 | for dim, spread in zip(self.shape, spreads) 106 | ] 107 | for frame, index in zip(self.frames, it.count()): 108 | print index, "of", len(self.frames) 109 | blurred = cv2.GaussianBlur(frame, ksize, sigmaX) 110 | edged = cv2.Canny(blurred, threshold1, threshold2) 111 | buffed = reduce(np.dot, [matrices[0], edged, matrices[1]]) 112 | for i in range(3): 113 | self.frames[index][:,:,i] = buffed 114 | 115 | 116 | def make_all_black_or_white(self): 117 | self.frames = [ 118 | 255*(frame != 0).astype('uint8') 119 | for frame in self.frames 120 | ] 121 | 122 | class ClearLeftSide(SceneFromVideo): 123 | args_list = [ 124 | ("BufferedCounting",), 125 | ] 126 | @staticmethod 127 | def args_to_string(scenename): 128 | return scenename 129 | 130 | def construct(self, scenename): 131 | path = os.path.join(MOVIE_DIR, MOVIE_PREFIX, scenename + ".mp4") 132 | SceneFromVideo.construct(self, path) 133 | self.highlight_region_over_time_range( 134 | Region(lambda x, y : x < -1, shape = self.shape) 135 | ) 136 | 137 | 138 | 139 | class DraggedPixels(SceneFromVideo): 140 | args_list = [ 141 | ("BufferedCounting",), 142 | ("CountingWithLeftClear",), 143 | ] 144 | @staticmethod 145 | def args_to_string(*args): 146 | return args[0] 147 | 148 | def construct(self, video): 149 | path = os.path.join(MOVIE_DIR, MOVIE_PREFIX, video+".mp4") 150 | SceneFromVideo.construct(self, path) 151 | self.drag_pixels() 152 | 153 | def drag_pixels(self, num_frames_to_drag_over = 5): 154 | for index in range(len(self.frames)-1, 0, -1): 155 | self.frames[index] = np.max([ 156 | self.frames[k] 157 | for k in range( 158 | max(index-num_frames_to_drag_over, 0), 159 | index 160 | ) 161 | ], axis = 0) 162 | 163 | 164 | class SaveEachNumber(SceneFromVideo): 165 | def construct(self): 166 | path = os.path.join(MOVIE_DIR, MOVIE_PREFIX, "ClearLeftSideBufferedCounting.mp4") 167 | SceneFromVideo.construct(self, path) 168 | for count in COUNT_TO_FRAME_NUM: 169 | path = os.path.join( 170 | MOVIE_DIR, MOVIE_PREFIX, "images", 171 | "Hand%d.png"%count 172 | ) 173 | Image.fromarray(self.frames[COUNT_TO_FRAME_NUM[count]]).save(path) 174 | 175 | class ShowCounting(SceneFromVideo): 176 | args_list = [ 177 | ("CountingWithLeftClear",), 178 | ("ClearLeftSideBufferedCounting",), 179 | ] 180 | @staticmethod 181 | def args_to_string(filename): 182 | return filename 183 | 184 | def construct(self, filename): 185 | path = os.path.join(MOVIE_DIR, MOVIE_PREFIX, filename + ".mp4") 186 | SceneFromVideo.construct(self, path) 187 | total_time = len(self.frames)*self.frame_duration 188 | for count in range(32): 189 | print count 190 | mob = TexMobject(str(count)).scale(1.5) 191 | mob.shift(0.3*LEFT).to_edge(UP, buff = 0.1) 192 | index_range = range( 193 | max(COUNT_TO_FRAME_NUM[count]-10, 0), 194 | COUNT_TO_FRAME_NUM[count+1]-10 195 | ) 196 | for index in index_range: 197 | self.frames[index] = disp.paint_mobject( 198 | mob, self.frames[index] 199 | ) 200 | 201 | class ShowFrameNum(SceneFromVideo): 202 | args_list = [ 203 | ("ClearLeftSideBufferedCounting",), 204 | ] 205 | @staticmethod 206 | def args_to_string(filename): 207 | return filename 208 | 209 | def construct(self, filename): 210 | path = os.path.join(MOVIE_DIR, MOVIE_PREFIX, filename+".mp4") 211 | SceneFromVideo.construct(self, path) 212 | for frame, count in zip(self.frames, it.count()): 213 | print count, "of", len(self.frames) 214 | mob = Mobject(*[ 215 | TexMobject(char).shift(0.3*x*RIGHT) 216 | for char, x, in zip(str(count), it.count()) 217 | ]) 218 | self.frames[count] = disp.paint_mobject( 219 | mob.to_corner(UP+LEFT), 220 | frame 221 | ) 222 | 223 | 224 | 225 | 226 | 227 | if __name__ == "__main__": 228 | command_line_create_scene(MOVIE_PREFIX) -------------------------------------------------------------------------------- /old_projects/triangle_of_power/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Helpsypoo/manim/720f1e9683cd09968eeb84c6f69409d5241a74a2/old_projects/triangle_of_power/__init__.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | colour==0.1.2 2 | numpy==1.11.0 3 | Pillow==1.7.8 4 | progressbar==2.3 5 | scipy==0.17.1 6 | tqdm==4.7.1 7 | -------------------------------------------------------------------------------- /scene/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "scene" 3 | ] 4 | 5 | from scene import Scene -------------------------------------------------------------------------------- /scene/scene_from_video.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import cv2 3 | import itertools as it 4 | from tqdm import tqdm as show_progress 5 | 6 | from scene import Scene 7 | 8 | 9 | class SceneFromVideo(Scene): 10 | def construct(self, file_name, 11 | freeze_last_frame = True, 12 | time_range = None): 13 | cap = cv2.VideoCapture(file_name) 14 | self.shape = ( 15 | int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)), 16 | int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH)) 17 | ) 18 | fps = cap.get(cv2.cv.CV_CAP_PROP_FPS) 19 | self.frame_duration = 1.0/fps 20 | frame_count = int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_COUNT)) 21 | if time_range is None: 22 | start_frame = 0 23 | end_frame = frame_count 24 | else: 25 | start_frame, end_frame = map(lambda t : fps*t, time_range) 26 | 27 | frame_count = end_frame - start_frame 28 | print "Reading in " + file_name + "..." 29 | for count in show_progress(range(start_frame, end_frame+1)): 30 | returned, frame = cap.read() 31 | if not returned 32 | break 33 | # b, g, r = cv2.split(frame) 34 | # self.frames.append(cv2.merge([r, g, b])) 35 | self.frames.append(frame) 36 | cap.release() 37 | 38 | if freeze_last_frame and len(self.frames) > 0: 39 | self.original_background = self.background = self.frames[-1] 40 | 41 | def apply_gaussian_blur(self, ksize = (5, 5), sigmaX = 5): 42 | self.frames = [ 43 | cv2.GaussianBlur(frame, ksize, sigmaX) 44 | for frame in self.frames 45 | ] 46 | 47 | def apply_edge_detection(self, threshold1 = 50, threshold2 = 100): 48 | edged_frames = [ 49 | cv2.Canny(frame, threshold1, threshold2) 50 | for frame in self.frames 51 | ] 52 | for index in range(len(self.frames)): 53 | for i in range(3): 54 | self.frames[index][:,:,i] = edged_frames[index] 55 | 56 | 57 | -------------------------------------------------------------------------------- /scene/test.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import random 3 | 4 | class CowProblem(): 5 | def __init__(self): 6 | self.reset() 7 | self.directions = [ 8 | (1, 0), 9 | (1, 1), 10 | (0, 1), 11 | (-1, 1), 12 | (-1, 0), 13 | (-1, -1), 14 | (0, -1), 15 | (1, -1), 16 | ] 17 | 18 | def reset(self): 19 | self.field = np.ones((11, 11), dtype = 'bool') 20 | self.cow_x = 0 21 | self.cow_y = 0 22 | 23 | def step(self): 24 | valid_step = False 25 | while not valid_step: 26 | step_x, step_y = random.choice(self.directions) 27 | if self.cow_x + step_x > 0 and self.cow_x + step_x < 11 and self.cow_y + step_y > 0 and self.cow_y + step_y < 11: 28 | valid_step = True 29 | self.cow_x += step_x 30 | self.cow_y += step_y 31 | self.field[self.cow_x, self.cow_y] = False 32 | 33 | def total_grass_after_n_steps(self, n): 34 | for x in range(n): 35 | self.step() 36 | return sum(sum(self.field)) 37 | 38 | def num_steps_for_half_eaten(self): 39 | result = 0 40 | while sum(sum(self.field)) > 121/2: 41 | self.step() 42 | result += 1 43 | return result 44 | 45 | def average_number_of_steps_for_half_eaten(self, sample_size): 46 | run_times = [] 47 | for x in range(sample_size): 48 | run_times.append( 49 | self.num_steps_for_half_eaten() 50 | ) 51 | self.reset() 52 | return np.mean(run_times) 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /scene/tk_scene.py: -------------------------------------------------------------------------------- 1 | import Tkinter 2 | from PIL import ImageTk, Image 3 | import itertools as it 4 | import time 5 | 6 | 7 | class TkSceneRoot(Tkinter.Tk): 8 | def __init__(self, scene): 9 | if scene.frames == []: 10 | raise Exception(str(scene) + " has no frames!") 11 | Tkinter.Tk.__init__(self) 12 | 13 | kwargs = { 14 | "height" : scene.camera.pixel_shape[0], 15 | "width" : scene.camera.pixel_shape[1], 16 | } 17 | self.frame = Tkinter.Frame(self, **kwargs) 18 | self.frame.pack() 19 | self.canvas = Tkinter.Canvas(self.frame, **kwargs) 20 | self.canvas.configure(background='black') 21 | self.canvas.place(x=0,y=0) 22 | 23 | last_time = time.time() 24 | for frame in it.cycle(scene.frames): 25 | try: 26 | self.show_new_image(frame) 27 | except: 28 | break 29 | sleep_time = scene.frame_duration 30 | sleep_time -= time.time() - last_time 31 | time.sleep(max(0, sleep_time)) 32 | last_time = time.time() 33 | self.mainloop() 34 | 35 | def show_new_image(self, frame): 36 | image = Image.fromarray(frame).convert('RGB') 37 | image.resize(self.frame.size()) 38 | photo = ImageTk.PhotoImage(image) 39 | self.canvas.delete(Tkinter.ALL) 40 | self.canvas.create_image( 41 | 0, 0, 42 | image = photo, anchor = Tkinter.NW 43 | ) 44 | self.update() 45 | -------------------------------------------------------------------------------- /scene/zoomed_scene.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from scene import Scene 4 | from mobject import Mobject 5 | from topics.geometry import Rectangle 6 | from camera import MovingCamera, Camera 7 | 8 | from helpers import * 9 | 10 | class ZoomedScene(Scene): 11 | CONFIG = { 12 | "zoomed_canvas_space_shape" : (3, 3), 13 | "zoomed_canvas_center" : None, 14 | "zoomed_canvas_corner" : UP+RIGHT, 15 | "zoomed_camera_background" : None, 16 | "zoom_factor" : 6, 17 | "square_color" : WHITE, 18 | "zoom_activated" : False, 19 | } 20 | def activate_zooming(self): 21 | self.generate_big_rectangle() 22 | self.setup_zoomed_canvas() 23 | self.setup_zoomed_camera() 24 | self.zoom_activated = True 25 | 26 | def disactivate_zooming(self): 27 | self.remove(self.big_rectangle, self.little_rectangle) 28 | self.zoom_activated = False 29 | 30 | def get_zoomed_camera_mobject(self): 31 | return self.little_rectangle 32 | 33 | def get_zoomed_screen(self): 34 | return self.big_rectangle 35 | 36 | def generate_big_rectangle(self): 37 | height, width = self.zoomed_canvas_space_shape 38 | self.big_rectangle = Rectangle( 39 | height = height, 40 | width = width, 41 | color = self.square_color 42 | ) 43 | if self.zoomed_canvas_center is not None: 44 | self.big_rectangle.shift(self.zoomed_canvas_center) 45 | elif self.zoomed_canvas_corner is not None: 46 | self.big_rectangle.to_corner(self.zoomed_canvas_corner) 47 | self.add(self.big_rectangle) 48 | 49 | 50 | def setup_zoomed_canvas(self): 51 | upper_left = self.big_rectangle.get_corner(UP+LEFT) 52 | lower_right = self.big_rectangle.get_corner(DOWN+RIGHT) 53 | pixel_coords = self.camera.points_to_pixel_coords( 54 | np.array([upper_left, lower_right]) 55 | ) 56 | self.zoomed_canvas_pixel_indices = pixel_coords 57 | (up, left), (down, right) = pixel_coords 58 | self.zoomed_canvas_pixel_shape = ( 59 | down-up, 60 | right-left 61 | ) 62 | 63 | def setup_zoomed_camera(self): 64 | self.little_rectangle = self.big_rectangle.copy() 65 | self.little_rectangle.scale(1./self.zoom_factor) 66 | self.little_rectangle.center() 67 | self.zoomed_camera = MovingCamera( 68 | self.little_rectangle, 69 | pixel_shape = self.zoomed_canvas_pixel_shape, 70 | background = self.zoomed_camera_background 71 | ) 72 | self.add(self.little_rectangle) 73 | #TODO, is there a better way to hanld this? 74 | self.zoomed_camera.adjusted_thickness = lambda x : x 75 | 76 | def get_frame(self): 77 | frame = Scene.get_frame(self) 78 | if self.zoom_activated: 79 | (up, left), (down, right) = self.zoomed_canvas_pixel_indices 80 | frame[left:right, up:down, :] = self.zoomed_camera.get_image() 81 | return frame 82 | 83 | def update_frame(self, *args, **kwargs): 84 | Scene.update_frame(self, *args, **kwargs) 85 | if self.zoom_activated: 86 | self.zoomed_camera.reset() 87 | self.zoomed_camera.capture_mobjects(self.mobjects) 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /stage_animations.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import inspect 3 | import os 4 | import shutil 5 | import itertools as it 6 | from extract_scene import is_scene, get_module 7 | from constants import MOVIE_DIR, STAGED_SCENES_DIR 8 | 9 | 10 | def get_sorted_scene_names(module_name): 11 | module = get_module(module_name) 12 | line_to_scene = {} 13 | for name, scene_class in inspect.getmembers(module, is_scene): 14 | lines, line_no = inspect.getsourcelines(scene_class) 15 | line_to_scene[line_no] = name 16 | return [ 17 | line_to_scene[line_no] 18 | for line_no in sorted(line_to_scene.keys()) 19 | ] 20 | 21 | 22 | 23 | def stage_animaions(module_name): 24 | scene_names = get_sorted_scene_names(module_name) 25 | movie_dir = os.path.join( 26 | MOVIE_DIR, module_name.replace(".py", "") 27 | ) 28 | files = os.listdir(movie_dir) 29 | sorted_files = [] 30 | for scene in scene_names: 31 | for clip in filter(lambda f : f.startswith(scene), files): 32 | sorted_files.append( 33 | os.path.join(movie_dir, clip) 34 | ) 35 | for f in os.listdir(STAGED_SCENES_DIR): 36 | os.remove(os.path.join(STAGED_SCENES_DIR, f)) 37 | for f, count in zip(sorted_files, it.count()): 38 | symlink_name = os.path.join( 39 | STAGED_SCENES_DIR, 40 | "Scene_%03d"%count + f.split(os.sep)[-1] 41 | ) 42 | os.symlink(f, symlink_name) 43 | 44 | 45 | 46 | if __name__ == "__main__": 47 | if len(sys.argv) < 2: 48 | raise Exception("No module given.") 49 | module_name = sys.argv[1] 50 | stage_animaions(module_name) -------------------------------------------------------------------------------- /template.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{beamer} 2 | 3 | \usepackage[english]{babel} 4 | \usepackage[utf8x]{inputenc} 5 | \usepackage{amsmath} 6 | \usepackage{amssymb} 7 | \usepackage{dsfont} 8 | \usepackage{setspace} 9 | \usepackage{tipa} 10 | \usepackage{relsize} 11 | 12 | 13 | \mode 14 | { 15 | \usetheme{default} % or try Darmstadt, Madrid, Warsaw, ... 16 | \usecolortheme{default} % or try albatross, beaver, crane, ... 17 | \usefonttheme{serif} % or try serif, structurebold, ... 18 | \setbeamertemplate{navigation symbols}{} 19 | \setbeamertemplate{caption}[numbered] 20 | } 21 | 22 | \begin{document} 23 | \centering 24 | 25 | \begin{frame} 26 | \begin{align*} 27 | YourTextHere 28 | \end{align*} 29 | \end{frame} 30 | \end{document} -------------------------------------------------------------------------------- /text_template.tex: -------------------------------------------------------------------------------- 1 | \documentclass[12pt]{beamer} 2 | 3 | \usepackage[english]{babel} 4 | \usepackage[utf8x]{inputenc} 5 | \usepackage{amsmath} 6 | \usepackage{amssymb} 7 | \usepackage{dsfont} 8 | \usepackage{setspace} 9 | \usepackage{tipa} 10 | \usepackage{relsize} 11 | \usepackage{textcomp} 12 | 13 | \mode 14 | { 15 | \usetheme{default} % or try Darmstadt, Madrid, Warsaw, ... 16 | \usecolortheme{default} % or try albatross, beaver, crane, ... 17 | \usefonttheme{serif} % or try serif, structurebold, ... 18 | \setbeamertemplate{navigation symbols}{} 19 | \setbeamertemplate{caption}[numbered] 20 | } 21 | \begin{document} 22 | 23 | \begin{frame} 24 | YourTextHere 25 | \end{frame} 26 | 27 | \end{document} -------------------------------------------------------------------------------- /topics/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "arithmetic", 3 | "characters", 4 | "combinatorics", 5 | "complex_numbers", 6 | "functions", 7 | "geometry", 8 | "graph_theory", 9 | "number_line", 10 | "three_dimensions", 11 | ] -------------------------------------------------------------------------------- /topics/arithmetic.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | 4 | from helpers import * 5 | from scene import Scene 6 | from animation import Animation 7 | from mobject.tex_mobject import TexMobject 8 | 9 | class RearrangeEquation(Scene): 10 | def construct( 11 | self, 12 | start_terms, 13 | end_terms, 14 | index_map, 15 | path_arc = np.pi, 16 | start_transform = None, 17 | end_transform = None, 18 | leave_start_terms = False, 19 | transform_kwargs = {}, 20 | ): 21 | transform_kwargs["path_func"] = path 22 | start_mobs, end_mobs = self.get_mobs_from_terms( 23 | start_terms, end_terms 24 | ) 25 | if start_transform: 26 | start_mobs = start_transform(Mobject(*start_mobs)).split() 27 | if end_transform: 28 | end_mobs = end_transform(Mobject(*end_mobs)).split() 29 | unmatched_start_indices = set(range(len(start_mobs))) 30 | unmatched_end_indices = set(range(len(end_mobs))) 31 | unmatched_start_indices.difference_update( 32 | [n%len(start_mobs) for n in index_map] 33 | ) 34 | unmatched_end_indices.difference_update( 35 | [n%len(end_mobs) for n in index_map.values()] 36 | ) 37 | mobject_pairs = [ 38 | (start_mobs[a], end_mobs[b]) 39 | for a, b in index_map.iteritems() 40 | ]+ [ 41 | (Point(end_mobs[b].get_center()), end_mobs[b]) 42 | for b in unmatched_end_indices 43 | ] 44 | if not leave_start_terms: 45 | mobject_pairs += [ 46 | (start_mobs[a], Point(start_mobs[a].get_center())) 47 | for a in unmatched_start_indices 48 | ] 49 | 50 | self.add(*start_mobs) 51 | if leave_start_terms: 52 | self.add(Mobject(*start_mobs)) 53 | self.dither() 54 | self.play(*[ 55 | Transform(*pair, **transform_kwargs) 56 | for pair in mobject_pairs 57 | ]) 58 | self.dither() 59 | 60 | 61 | def get_mobs_from_terms(self, start_terms, end_terms): 62 | """ 63 | Need to ensure that all image mobjects for a tex expression 64 | stemming from the same string are point-for-point copies of one 65 | and other. This makes transitions much smoother, and not look 66 | like point-clouds. 67 | """ 68 | num_start_terms = len(start_terms) 69 | all_mobs = np.array( 70 | TexMobject(start_terms).split() + \ 71 | TexMobject(end_terms).split() 72 | ) 73 | all_terms = np.array(start_terms+end_terms) 74 | for term in set(all_terms): 75 | matches = all_terms == term 76 | if sum(matches) > 1: 77 | base_mob = all_mobs[list(all_terms).index(term)] 78 | all_mobs[matches] = [ 79 | base_mob.copy().replace(target_mob) 80 | for target_mob in all_mobs[matches] 81 | ] 82 | return all_mobs[:num_start_terms], all_mobs[num_start_terms:] 83 | 84 | 85 | class FlipThroughSymbols(Animation): 86 | CONFIG = { 87 | "start_center" : ORIGIN, 88 | "end_center" : ORIGIN, 89 | } 90 | def __init__(self, tex_list, **kwargs): 91 | mobject = TexMobject(self.curr_tex).shift(start_center) 92 | Animation.__init__(self, mobject, **kwargs) 93 | 94 | def update_mobject(self, alpha): 95 | new_tex = self.tex_list[np.ceil(alpha*len(self.tex_list))-1] 96 | 97 | if new_tex != self.curr_tex: 98 | self.curr_tex = new_tex 99 | self.mobject = TexMobject(new_tex).shift(self.start_center) 100 | if not all(self.start_center == self.end_center): 101 | self.mobject.center().shift( 102 | (1-alpha)*self.start_center + alpha*self.end_center 103 | ) 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /topics/combinatorics.py: -------------------------------------------------------------------------------- 1 | from helpers import * 2 | 3 | from mobject.vectorized_mobject import VMobject 4 | from mobject.tex_mobject import TexMobject 5 | 6 | from scene import Scene 7 | 8 | 9 | DEFAULT_COUNT_NUM_OFFSET = (SPACE_WIDTH - 1, SPACE_HEIGHT - 1, 0) 10 | DEFAULT_COUNT_RUN_TIME = 5.0 11 | 12 | class CountingScene(Scene): 13 | def count(self, items, item_type = "mobject", *args, **kwargs): 14 | if item_type == "mobject": 15 | self.count_mobjects(items, *args, **kwargs) 16 | elif item_type == "region": 17 | self.count_regions(items, *args, **kwargs) 18 | else: 19 | raise Exception("Unknown item_type, should be mobject or region") 20 | return self 21 | 22 | def count_mobjects( 23 | self, mobjects, mode = "highlight", 24 | color = "red", 25 | display_numbers = True, 26 | num_offset = DEFAULT_COUNT_NUM_OFFSET, 27 | run_time = DEFAULT_COUNT_RUN_TIME): 28 | """ 29 | Note, leaves final number mobject as "number" attribute 30 | 31 | mode can be "highlight", "show_creation" or "show", otherwise 32 | a warning is given and nothing is animating during the count 33 | """ 34 | if len(mobjects) > 50: #TODO 35 | raise Exception("I don't know if you should be counting \ 36 | too many mobjects...") 37 | if len(mobjects) == 0: 38 | raise Exception("Counting mobject list of length 0") 39 | if mode not in ["highlight", "show_creation", "show"]: 40 | raise Warning("Unknown mode") 41 | frame_time = run_time / len(mobjects) 42 | if mode == "highlight": 43 | self.add(*mobjects) 44 | for mob, num in zip(mobjects, it.count(1)): 45 | if display_numbers: 46 | num_mob = TexMobject(str(num)) 47 | num_mob.center().shift(num_offset) 48 | self.add(num_mob) 49 | if mode == "highlight": 50 | original_color = mob.color 51 | mob.highlight(color) 52 | self.dither(frame_time) 53 | mob.highlight(original_color) 54 | if mode == "show_creation": 55 | self.play(ShowCreation(mob, run_time = frame_time)) 56 | if mode == "show": 57 | self.add(mob) 58 | self.dither(frame_time) 59 | if display_numbers: 60 | self.remove(num_mob) 61 | if display_numbers: 62 | self.add(num_mob) 63 | self.number = num_mob 64 | return self 65 | 66 | def count_regions(self, regions, 67 | mode = "one_at_a_time", 68 | num_offset = DEFAULT_COUNT_NUM_OFFSET, 69 | run_time = DEFAULT_COUNT_RUN_TIME, 70 | **unused_kwargsn): 71 | if mode not in ["one_at_a_time", "show_all"]: 72 | raise Warning("Unknown mode") 73 | frame_time = run_time / (len(regions)) 74 | for region, count in zip(regions, it.count(1)): 75 | num_mob = TexMobject(str(count)) 76 | num_mob.center().shift(num_offset) 77 | self.add(num_mob) 78 | self.highlight_region(region) 79 | self.dither(frame_time) 80 | if mode == "one_at_a_time": 81 | self.reset_background() 82 | self.remove(num_mob) 83 | self.add(num_mob) 84 | self.number = num_mob 85 | return self 86 | 87 | class PascalsTriangle(VMobject): 88 | CONFIG = { 89 | "nrows" : 7, 90 | "height" : 2*SPACE_HEIGHT - 1, 91 | "width" : 1.5*SPACE_WIDTH, 92 | "portion_to_fill" : 0.7 93 | } 94 | def generate_points(self): 95 | self.cell_height = self.height / self.nrows 96 | self.cell_width = self.width / self.nrows 97 | self.bottom_left = (self.cell_width * self.nrows / 2.0)*LEFT + \ 98 | (self.cell_height * self.nrows / 2.0)*DOWN 99 | num_to_num_mob = {} 100 | self.coords_to_mobs = {} 101 | self.coords = [ 102 | (n, k) 103 | for n in range(self.nrows) 104 | for k in range(n+1) 105 | ] 106 | for n, k in self.coords: 107 | num = choose(n, k) 108 | center = self.coords_to_center(n, k) 109 | num_mob = TexMobject(str(num)) 110 | scale_factor = min( 111 | 1, 112 | self.portion_to_fill * self.cell_height / num_mob.get_height(), 113 | self.portion_to_fill * self.cell_width / num_mob.get_width(), 114 | ) 115 | num_mob.center().scale(scale_factor).shift(center) 116 | if n not in self.coords_to_mobs: 117 | self.coords_to_mobs[n] = {} 118 | self.coords_to_mobs[n][k] = num_mob 119 | self.add(*[ 120 | self.coords_to_mobs[n][k] 121 | for n, k in self.coords 122 | ]) 123 | return self 124 | 125 | def coords_to_center(self, n, k): 126 | x_offset = self.cell_width * (k+self.nrows/2.0 - n/2.0) 127 | y_offset = self.cell_height * (self.nrows - n) 128 | return self.bottom_left + x_offset*RIGHT + y_offset*UP 129 | 130 | def generate_n_choose_k_mobs(self): 131 | self.coords_to_n_choose_k = {} 132 | for n, k in self.coords: 133 | nck_mob = TexMobject(r"{%d \choose %d}"%(n, k)) 134 | scale_factor = min( 135 | 1, 136 | self.portion_to_fill * self.cell_height / nck_mob.get_height(), 137 | self.portion_to_fill * self.cell_width / nck_mob.get_width(), 138 | ) 139 | center = self.coords_to_mobs[n][k].get_center() 140 | nck_mob.center().scale(scale_factor).shift(center) 141 | if n not in self.coords_to_n_choose_k: 142 | self.coords_to_n_choose_k[n] = {} 143 | self.coords_to_n_choose_k[n][k] = nck_mob 144 | return self 145 | 146 | def fill_with_n_choose_k(self): 147 | if not hasattr(self, "coords_to_n_choose_k"): 148 | self.generate_n_choose_k_mobs() 149 | self.submobjects = [] 150 | self.add(*[ 151 | self.coords_to_n_choose_k[n][k] 152 | for n, k in self.coords 153 | ]) 154 | return self 155 | 156 | def generate_sea_of_zeros(self): 157 | zero = TexMobject("0") 158 | self.sea_of_zeros = [] 159 | for n in range(self.nrows): 160 | for a in range((self.nrows - n)/2 + 1): 161 | for k in (n + a + 1, -a -1): 162 | self.coords.append((n, k)) 163 | mob = zero.copy() 164 | mob.shift(self.coords_to_center(n, k)) 165 | self.coords_to_mobs[n][k] = mob 166 | self.add(mob) 167 | return self 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /topics/complex_numbers.py: -------------------------------------------------------------------------------- 1 | from helpers import * 2 | 3 | from number_line import NumberPlane 4 | from animation.transform import ApplyPointwiseFunction 5 | from animation.simple_animations import Homotopy 6 | from scene import Scene 7 | 8 | 9 | def complex_string(complex_num): 10 | return filter(lambda c : c not in "()", str(complex_num)) 11 | 12 | class ComplexPlane(NumberPlane): 13 | CONFIG = { 14 | "color" : GREEN, 15 | "unit_to_spatial_width" : 1, 16 | "line_frequency" : 1, 17 | "faded_line_frequency" : 0.5, 18 | "number_at_center" : complex(0), 19 | } 20 | def __init__(self, **kwargs): 21 | digest_config(self, kwargs) 22 | kwargs.update({ 23 | "x_unit_to_spatial_width" : self.unit_to_spatial_width, 24 | "y_unit_to_spatial_height" : self.unit_to_spatial_width, 25 | "x_line_frequency" : self.line_frequency, 26 | "x_faded_line_frequency" : self.faded_line_frequency, 27 | "y_line_frequency" : self.line_frequency, 28 | "y_faded_line_frequency" : self.faded_line_frequency, 29 | "num_pair_at_center" : (self.number_at_center.real, 30 | self.number_at_center.imag), 31 | }) 32 | NumberPlane.__init__(self, **kwargs) 33 | 34 | def number_to_point(self, number): 35 | number = complex(number) 36 | return self.num_pair_to_point((number.real, number.imag)) 37 | 38 | def get_coordinate_labels(self, *numbers): 39 | result = [] 40 | nudge = 0.1*(DOWN+RIGHT) 41 | if len(numbers) == 0: 42 | numbers = range(-int(self.x_radius), int(self.x_radius)) 43 | numbers += [ 44 | complex(0, y) 45 | for y in range(-int(self.y_radius), int(self.y_radius)) 46 | ] 47 | for number in numbers: 48 | point = self.number_to_point(number) 49 | if number == 0: 50 | num_str = "0" 51 | else: 52 | num_str = str(number).replace("j", "i") 53 | num = TexMobject(num_str) 54 | num.scale(self.number_scale_factor) 55 | num.shift(point-num.get_corner(UP+LEFT)+nudge) 56 | result.append(num) 57 | return result 58 | 59 | def add_coordinates(self, *numbers): 60 | self.add(*self.get_coordinate_labels(*numbers)) 61 | return self 62 | 63 | def add_spider_web(self, circle_freq = 1, angle_freq = np.pi/6): 64 | self.fade(self.fade_factor) 65 | config = { 66 | "color" : self.color, 67 | "density" : self.density, 68 | } 69 | for radius in np.arange(circle_freq, SPACE_WIDTH, circle_freq): 70 | self.add(Circle(radius = radius, **config)) 71 | for angle in np.arange(0, 2*np.pi, angle_freq): 72 | end_point = np.cos(angle)*RIGHT + np.sin(angle)*UP 73 | end_point *= SPACE_WIDTH 74 | self.add(Line(ORIGIN, end_point, **config)) 75 | return self 76 | 77 | 78 | class ComplexFunction(ApplyPointwiseFunction): 79 | def __init__(self, function, mobject = ComplexPlane, **kwargs): 80 | if "path_func" not in kwargs: 81 | self.path_func = path_along_arc( 82 | np.log(function(complex(1))).imag 83 | ) 84 | ApplyPointwiseFunction.__init__( 85 | self, 86 | lambda (x, y, z) : complex_to_R3(function(complex(x, y))), 87 | instantiate(mobject), 88 | **kwargs 89 | ) 90 | 91 | class ComplexHomotopy(Homotopy): 92 | def __init__(self, complex_homotopy, mobject = ComplexPlane, **kwargs): 93 | """ 94 | Complex Hootopy a function Cx[0, 1] to C 95 | """ 96 | def homotopy((x, y, z, t)): 97 | c = complex_homotopy((complex(x, y), t)) 98 | return (c.real, c.imag, z) 99 | Homotopy.__init__(self, homotopy, mobject, *args, **kwargs) 100 | 101 | 102 | class ComplexMultiplication(Scene): 103 | @staticmethod 104 | def args_to_string(multiplier, mark_one = False): 105 | num_str = complex_string(multiplier) 106 | arrow_str = "MarkOne" if mark_one else "" 107 | return num_str + arrow_str 108 | 109 | @staticmethod 110 | def string_to_args(arg_string): 111 | parts = arg_string.split() 112 | multiplier = complex(parts[0]) 113 | mark_one = len(parts) > 1 and parts[1] == "MarkOne" 114 | return (multiplier, mark_one) 115 | 116 | def construct(self, multiplier, mark_one = False, **plane_config): 117 | norm = np.linalg.norm(multiplier) 118 | arg = np.log(multiplier).imag 119 | plane_config["faded_line_frequency"] = 0 120 | plane_config.update(DEFAULT_PLANE_CONFIG) 121 | if norm > 1 and "density" not in plane_config: 122 | plane_config["density"] = norm*DEFAULT_POINT_DENSITY_1D 123 | if "radius" not in plane_config: 124 | radius = SPACE_WIDTH 125 | if norm > 0 and norm < 1: 126 | radius /= norm 127 | else: 128 | radius = plane_config["radius"] 129 | plane_config["x_radius"] = plane_config["y_radius"] = radius 130 | plane = ComplexPlane(**plane_config) 131 | self.plane = plane 132 | self.add(plane) 133 | # plane.add_spider_web() 134 | self.anim_config = { 135 | "run_time" : 2.0, 136 | "path_func" : path_along_arc(arg) 137 | } 138 | 139 | plane_config["faded_line_frequency"] = 0.5 140 | background = ComplexPlane(color = "grey", **plane_config) 141 | # background.add_spider_web() 142 | labels = background.get_coordinate_labels() 143 | self.paint_into_background(background, *labels) 144 | self.mobjects_to_move_without_molding = [] 145 | if mark_one: 146 | self.draw_dot("1", 1, True) 147 | self.draw_dot("z", multiplier) 148 | 149 | 150 | self.mobjects_to_multiply = [plane] 151 | 152 | self.additional_animations = [] 153 | self.multiplier = multiplier 154 | if self.__class__ == ComplexMultiplication: 155 | self.apply_multiplication() 156 | 157 | def draw_dot(self, tex_string, value, move_dot = False): 158 | dot = Dot( 159 | self.plane.number_to_point(value), 160 | radius = 0.1*self.plane.unit_to_spatial_width, 161 | color = BLUE if value == 1 else YELLOW 162 | ) 163 | label = TexMobject(tex_string) 164 | label.shift(dot.get_center()+1.5*UP+RIGHT) 165 | arrow = Arrow(label, dot) 166 | self.add(label) 167 | self.play(ShowCreation(arrow)) 168 | self.play(ShowCreation(dot)) 169 | self.dither() 170 | 171 | self.remove(label, arrow) 172 | if move_dot: 173 | self.mobjects_to_move_without_molding.append(dot) 174 | return dot 175 | 176 | 177 | def apply_multiplication(self): 178 | def func((x, y, z)): 179 | complex_num = self.multiplier*complex(x, y) 180 | return (complex_num.real, complex_num.imag, z) 181 | mobjects = self.mobjects_to_multiply 182 | mobjects += self.mobjects_to_move_without_molding 183 | mobjects += [anim.mobject for anim in self.additional_animations] 184 | 185 | 186 | self.add(*mobjects) 187 | full_multiplications = [ 188 | ApplyMethod(mobject.apply_function, func, **self.anim_config) 189 | for mobject in self.mobjects_to_multiply 190 | ] 191 | movements_with_plane = [ 192 | ApplyMethod( 193 | mobject.shift, 194 | func(mobject.get_center())-mobject.get_center(), 195 | **self.anim_config 196 | ) 197 | for mobject in self.mobjects_to_move_without_molding 198 | ] 199 | self.dither() 200 | self.play(*reduce(op.add, [ 201 | full_multiplications, 202 | movements_with_plane, 203 | self.additional_animations 204 | ])) 205 | self.dither() 206 | -------------------------------------------------------------------------------- /topics/functions.py: -------------------------------------------------------------------------------- 1 | from scipy import integrate 2 | 3 | from mobject.vectorized_mobject import VMobject 4 | 5 | from helpers import * 6 | 7 | class FunctionGraph(VMobject): 8 | CONFIG = { 9 | "color" : BLUE_D, 10 | "x_min" : -SPACE_WIDTH, 11 | "x_max" : SPACE_WIDTH, 12 | "num_steps" : 20, 13 | } 14 | def __init__(self, function, **kwargs): 15 | self.function = function 16 | VMobject.__init__(self, **kwargs) 17 | 18 | def generate_points(self): 19 | self.set_anchor_points([ 20 | x*RIGHT + self.function(x)*UP 21 | for x in np.linspace(self.x_min, self.x_max, self.num_steps) 22 | ], mode = "smooth") 23 | 24 | 25 | class ParametricFunction(VMobject): 26 | CONFIG = { 27 | "t_min" : 0, 28 | "t_max" : 1, 29 | "epsilon" : 0.1, 30 | } 31 | def __init__(self, function, **kwargs): 32 | self.function = function 33 | VMobject.__init__(self, **kwargs) 34 | 35 | def generate_points(self): 36 | self.set_anchor_points([ 37 | self.function(t) 38 | for t in np.arange( 39 | self.t_min, 40 | self.t_max+self.epsilon, 41 | self.epsilon 42 | ) 43 | ], mode = "smooth") 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /topics/geometry.py: -------------------------------------------------------------------------------- 1 | from helpers import * 2 | 3 | from mobject import Mobject 4 | from mobject.vectorized_mobject import VMobject 5 | 6 | class Arc(VMobject): 7 | CONFIG = { 8 | "radius" : 1.0, 9 | "start_angle" : 0, 10 | "num_anchors" : 8, 11 | "anchors_span_full_range" : True, 12 | } 13 | def __init__(self, angle, **kwargs): 14 | digest_locals(self) 15 | VMobject.__init__(self, **kwargs) 16 | 17 | def generate_points(self): 18 | self.set_anchor_points( 19 | self.get_unscaled_anchor_points(), 20 | mode = "smooth" 21 | ) 22 | self.scale(self.radius) 23 | 24 | def get_unscaled_anchor_points(self): 25 | return [ 26 | np.cos(a)*RIGHT+np.sin(a)*UP 27 | for a in np.linspace( 28 | self.start_angle, 29 | self.start_angle + self.angle, 30 | self.num_anchors 31 | ) 32 | ] 33 | 34 | def add_tip(self): 35 | #TODO, do this a better way 36 | arrow = Arrow(*self.points[-2:]) 37 | self.add(arrow.split()[-1]) 38 | self.highlight(self.get_color()) 39 | return self 40 | 41 | 42 | class Circle(Arc): 43 | CONFIG = { 44 | "color" : RED, 45 | "close_new_points" : True, 46 | "anchors_span_full_range" : False 47 | } 48 | def __init__(self, **kwargs): 49 | Arc.__init__(self, 2*np.pi, **kwargs) 50 | 51 | class Dot(Circle): #Use 1D density, even though 2D 52 | CONFIG = { 53 | "radius" : 0.05, 54 | "stroke_width" : 0, 55 | "fill_opacity" : 1.0, 56 | "color" : WHITE 57 | } 58 | def __init__(self, point = ORIGIN, **kwargs): 59 | Circle.__init__(self, **kwargs) 60 | self.shift(point) 61 | self.init_colors() 62 | 63 | 64 | class Line(VMobject): 65 | CONFIG = { 66 | "buff" : 0, 67 | "considered_smooth" : False, 68 | } 69 | def __init__(self, start, end, **kwargs): 70 | digest_config(self, kwargs) 71 | self.set_start_and_end(start, end) 72 | VMobject.__init__(self, **kwargs) 73 | 74 | def set_start_and_end(self, start, end): 75 | start_to_end = self.pointify(end) - self.pointify(start) 76 | vect = np.zeros(len(start_to_end)) 77 | longer_dim = np.argmax(map(abs, start_to_end)) 78 | vect[longer_dim] = start_to_end[longer_dim] 79 | self.start, self.end = [ 80 | arg.get_edge_center(unit*vect) 81 | if isinstance(arg, Mobject) 82 | else np.array(arg) 83 | for arg, unit in zip([start, end], [1, -1]) 84 | ] 85 | start_to_end = self.end - self.start 86 | length = np.linalg.norm(start_to_end) 87 | if length > 2*self.buff: 88 | dummy = start_to_end / np.linalg.norm(start_to_end) 89 | start_to_end = dummy 90 | #start_to_end /= np.linalg.norm(start_to_end) 91 | self.start = self.start + self.buff*start_to_end 92 | self.end = self.end - self.buff*start_to_end 93 | 94 | def pointify(self, mob_or_point): 95 | if isinstance(mob_or_point, Mobject): 96 | return mob_or_point.get_center() 97 | return np.array(mob_or_point) 98 | 99 | def generate_points(self): 100 | self.set_points_as_corners([self.start, self.end]) 101 | 102 | def get_length(self): 103 | start, end = self.get_start_and_end() 104 | return np.linalg.norm(start - end) 105 | 106 | def get_start_and_end(self): 107 | return self.get_start(), self.get_end() 108 | 109 | def get_start(self): 110 | return self.points[0] 111 | 112 | def get_end(self): 113 | return self.points[-1] 114 | 115 | def get_slope(self): 116 | end = self.get_end() 117 | rise, run = [ 118 | float(end[i] - start[i]) 119 | for i in [1, 0] 120 | ] 121 | return np.inf if run == 0 else rise/run 122 | 123 | def get_angle(self): 124 | start, end = self.get_start_and_end() 125 | return angle_of_vector(end-start) 126 | 127 | def put_start_and_end_on(self, new_start, new_end): 128 | if self.get_length() == 0: 129 | #TODO, this is hacky 130 | self.points[0] += 0.01*LEFT 131 | new_length = np.linalg.norm(new_end - new_start) 132 | new_angle = angle_of_vector(new_end - new_start) 133 | self.scale(new_length / self.get_length()) 134 | self.rotate(new_angle - self.get_angle()) 135 | self.shift(new_start - self.get_start()) 136 | return self 137 | 138 | class DashedLine(Line): 139 | CONFIG = { 140 | "dashed_segment_length" : 0.15 141 | } 142 | def __init__(self, *args, **kwargs): 143 | self.init_kwargs = kwargs 144 | Line.__init__(self, *args, **kwargs) 145 | 146 | def generate_points(self): 147 | length = np.linalg.norm(self.end-self.start) 148 | num_interp_points = int(length/self.dashed_segment_length) 149 | points = [ 150 | interpolate(self.start, self.end, alpha) 151 | for alpha in np.linspace(0, 1, num_interp_points) 152 | ] 153 | includes = it.cycle([True, False]) 154 | for p1, p2, include in zip(points, points[1:], includes): 155 | if include: 156 | self.add(Line(p1, p2, **self.init_kwargs)) 157 | return self 158 | 159 | def get_start(self): 160 | return self[0].points[0] 161 | 162 | def get_end(self): 163 | return self[-1].points[-1] 164 | 165 | 166 | 167 | class Arrow(Line): 168 | CONFIG = { 169 | "color" : YELLOW_C, 170 | "tip_length" : 0.25, 171 | "tip_angle" : np.pi/6, 172 | "buff" : 0.3, 173 | "propogate_style_to_family" : False, 174 | "preserve_tip_size_when_scaling" : True, 175 | } 176 | def __init__(self, *args, **kwargs): 177 | if len(args) == 1: 178 | point = self.pointify(args[0]) 179 | args = (point+UP+LEFT, target) 180 | Line.__init__(self, *args, **kwargs) 181 | self.add_tip() 182 | 183 | def add_tip(self, add_at_end = True): 184 | vect = self.tip_length*RIGHT 185 | vect = rotate_vector(vect, self.get_angle()+np.pi) 186 | start, end = self.get_start_and_end() 187 | if not add_at_end: 188 | start, end = end, start 189 | vect = -vect 190 | tip_points = [ 191 | end+rotate_vector(vect, u*self.tip_angle) 192 | for u in 1, -1 193 | ] 194 | self.tip = VMobject( 195 | close_new_points = True, 196 | mark_paths_closed = True, 197 | fill_color = self.color, 198 | fill_opacity = 1, 199 | stroke_color = self.color, 200 | ) 201 | self.tip.set_anchor_points( 202 | [tip_points[0], end, tip_points[1]], 203 | mode = "corners" 204 | ) 205 | self.add(self.tip) 206 | self.init_colors() 207 | 208 | def get_tip(self): 209 | return self.tip 210 | 211 | def scale(self, scale_factor): 212 | Line.scale(self, scale_factor) 213 | if self.preserve_tip_size_when_scaling: 214 | self.remove(self.tip) 215 | self.add_tip() 216 | return self 217 | 218 | class Vector(Arrow): 219 | CONFIG = { 220 | "color" : YELLOW, 221 | "buff" : 0, 222 | } 223 | def __init__(self, direction, **kwargs): 224 | if len(direction) == 2: 225 | direction = np.append(np.array(direction), 0) 226 | Arrow.__init__(self, ORIGIN, direction, **kwargs) 227 | 228 | class DoubleArrow(Arrow): 229 | def __init__(self, *args, **kwargs): 230 | Arrow.__init__(self, *args, **kwargs) 231 | self.add_tip(add_at_end = False) 232 | 233 | 234 | class Cross(VMobject): 235 | CONFIG = { 236 | "color" : YELLOW, 237 | "radius" : 0.3 238 | } 239 | def generate_points(self): 240 | p1, p2, p3, p4 = self.radius * np.array([ 241 | UP+LEFT, 242 | DOWN+RIGHT, 243 | UP+RIGHT, 244 | DOWN+LEFT, 245 | ]) 246 | self.add(Line(p1, p2), Line(p3, p4)) 247 | self.init_colors() 248 | 249 | class CubicBezier(VMobject): 250 | def __init__(self, points, **kwargs): 251 | VMobject.__init__(self, **kwargs) 252 | self.set_points(points) 253 | 254 | class Polygon(VMobject): 255 | CONFIG = { 256 | "color" : GREEN_D, 257 | "mark_paths_closed" : True, 258 | "close_new_points" : True, 259 | "considered_smooth" : False, 260 | } 261 | def __init__(self, *vertices, **kwargs): 262 | assert len(vertices) > 1 263 | digest_locals(self) 264 | VMobject.__init__(self, **kwargs) 265 | 266 | def generate_points(self): 267 | self.set_anchor_points(self.vertices, mode = "corners") 268 | 269 | def get_vertices(self): 270 | return self.get_anchors_and_handles()[0] 271 | 272 | class RegularPolygon(VMobject): 273 | CONFIG = { 274 | "start_angle" : 0 275 | } 276 | def __init__(self, n = 3, **kwargs): 277 | digest_config(self, kwargs, locals()) 278 | start_vect = rotate_vector(RIGHT, self.start_angle) 279 | vertices = compass_directions(n, start_angle) 280 | Polygon.__init__(self, *vertices, **kwargs) 281 | 282 | 283 | class Rectangle(VMobject): 284 | CONFIG = { 285 | "color" : YELLOW, 286 | "height" : 2.0, 287 | "width" : 4.0, 288 | "mark_paths_closed" : True, 289 | "close_new_points" : True, 290 | "considered_smooth" : False, 291 | } 292 | def generate_points(self): 293 | y, x = self.height/2., self.width/2. 294 | self.set_anchor_points([ 295 | x*LEFT+y*UP, 296 | x*RIGHT+y*UP, 297 | x*RIGHT+y*DOWN, 298 | x*LEFT+y*DOWN 299 | ], mode = "corners") 300 | 301 | class Square(Rectangle): 302 | CONFIG = { 303 | "side_length" : 2.0, 304 | } 305 | def __init__(self, **kwargs): 306 | digest_config(self, kwargs) 307 | Rectangle.__init__( 308 | self, 309 | height = self.side_length, 310 | width = self.side_length, 311 | **kwargs 312 | ) 313 | 314 | class BackgroundRectangle(Rectangle): 315 | CONFIG = { 316 | "color" : BLACK, 317 | "stroke_width" : 0, 318 | "fill_opacity" : 0.75, 319 | } 320 | def __init__(self, mobject, **kwargs): 321 | Rectangle.__init__(self, **kwargs) 322 | self.replace(mobject, stretch = True) 323 | self.original_fill_opacity = self.fill_opacity 324 | 325 | def pointwise_become_partial(self, mobject, a, b): 326 | self.set_fill(opacity = b*self.original_fill_opacity) 327 | return self 328 | 329 | def get_fill_color(self): 330 | return Color(self.color) 331 | 332 | 333 | class Grid(VMobject): 334 | CONFIG = { 335 | "height" : 6.0, 336 | "width" : 6.0, 337 | "considered_smooth" : False, 338 | } 339 | def __init__(self, rows, columns, **kwargs): 340 | digest_config(self, kwargs, locals()) 341 | VMobject.__init__(self, **kwargs) 342 | 343 | def generate_points(self): 344 | x_step = self.width / self.columns 345 | y_step = self.height / self.rows 346 | 347 | for x in np.arange(0, self.width+x_step, x_step): 348 | self.add(Line( 349 | [x-self.width/2., -self.height/2., 0], 350 | [x-self.width/2., self.height/2., 0], 351 | )) 352 | for y in np.arange(0, self.height+y_step, y_step): 353 | self.add(Line( 354 | [-self.width/2., y-self.height/2., 0], 355 | [self.width/2., y-self.height/2., 0] 356 | )) 357 | 358 | 359 | 360 | -------------------------------------------------------------------------------- /topics/number_line.py: -------------------------------------------------------------------------------- 1 | from helpers import * 2 | 3 | from mobject import Mobject1D 4 | from mobject.vectorized_mobject import VMobject 5 | from mobject.tex_mobject import TexMobject 6 | from topics.geometry import Line, Arrow 7 | from scene import Scene 8 | 9 | class NumberLine(VMobject): 10 | CONFIG = { 11 | "color" : BLUE, 12 | "x_min" : -SPACE_WIDTH, 13 | "x_max" : SPACE_WIDTH, 14 | "space_unit_to_num" : 1, 15 | "tick_size" : 0.1, 16 | "tick_frequency" : 1, 17 | "leftmost_tick" : None, #Defaults to ceil(x_min) 18 | "numbers_with_elongated_ticks" : [0], 19 | "longer_tick_multiple" : 2, 20 | "number_at_center" : 0, 21 | "propogate_style_to_family" : True 22 | } 23 | def __init__(self, **kwargs): 24 | digest_config(self, kwargs) 25 | if self.leftmost_tick is None: 26 | self.leftmost_tick = np.ceil(self.x_min) 27 | VMobject.__init__(self, **kwargs) 28 | 29 | def generate_points(self): 30 | self.main_line = Line(self.x_min*RIGHT, self.x_max*RIGHT) 31 | self.tick_marks = VMobject() 32 | self.add(self.main_line, self.tick_marks) 33 | for x in self.get_tick_numbers(): 34 | self.add_tick(x, self.tick_size) 35 | for x in self.numbers_with_elongated_ticks: 36 | self.add_tick(x, self.longer_tick_multiple*self.tick_size) 37 | self.stretch(self.space_unit_to_num, 0) 38 | self.shift(-self.number_to_point(self.number_at_center)) 39 | 40 | def add_tick(self, x, size): 41 | self.tick_marks.add(Line( 42 | x*RIGHT+size*DOWN, 43 | x*RIGHT+size*UP, 44 | )) 45 | return self 46 | 47 | def get_tick_marks(self): 48 | return self.tick_marks 49 | 50 | def get_tick_numbers(self): 51 | return np.arange(self.leftmost_tick, self.x_max, self.tick_frequency) 52 | 53 | def number_to_point(self, number): 54 | return interpolate( 55 | self.main_line.get_left(), 56 | self.main_line.get_right(), 57 | float(number-self.x_min)/(self.x_max - self.x_min) 58 | ) 59 | 60 | def point_to_number(self, point): 61 | dist_from_left = (point[0]-self.main_line.get_left()[0]) 62 | num_dist_from_left = num_dist_from_left/self.space_unit_to_num 63 | return self.x_min + dist_from_left 64 | 65 | def default_numbers_to_display(self): 66 | return np.arange(self.leftmost_tick, self.x_max, 1) 67 | 68 | def get_vertical_number_offset(self, direction = DOWN): 69 | return 4*direction*self.tick_size 70 | 71 | def get_number_mobjects(self, *numbers, **kwargs): 72 | #TODO, handle decimals 73 | if len(numbers) == 0: 74 | numbers = self.default_numbers_to_display() 75 | result = [] 76 | for number in numbers: 77 | mob = TexMobject(str(int(number))) 78 | mob.scale_to_fit_height(3*self.tick_size) 79 | mob.shift( 80 | self.number_to_point(number), 81 | self.get_vertical_number_offset(**kwargs) 82 | ) 83 | result.append(mob) 84 | return result 85 | 86 | def get_numbers(self, *numbers, **kwargs): 87 | ##TODO, this shouldn't exist alongside the above method. 88 | return VMobject(*self.get_number_mobjects(*numbers, **kwargs)) 89 | 90 | def add_numbers(self, *numbers, **kwargs): 91 | self.numbers = self.get_number_mobjects( 92 | *numbers, **kwargs 93 | ) 94 | self.add(*self.numbers) 95 | return self 96 | 97 | class UnitInterval(NumberLine): 98 | CONFIG = { 99 | "x_min" : 0, 100 | "x_max" : 1, 101 | "space_unit_to_num" : 6, 102 | "tick_frequency" : 0.1, 103 | "numbers_with_elongated_ticks" : [0, 1], 104 | "number_at_center" : 0.5, 105 | } 106 | 107 | 108 | class Axes(VMobject): 109 | CONFIG = { 110 | "propogate_style_to_family" : True 111 | } 112 | def generate_points(self, **kwargs): 113 | self.x_axis = NumberLine(**kwargs) 114 | self.y_axis = NumberLine(**kwargs).rotate(np.pi/2) 115 | self.add(self.x_axis, self.y_axis) 116 | 117 | 118 | class NumberPlane(VMobject): 119 | CONFIG = { 120 | "color" : BLUE_D, 121 | "secondary_color" : BLUE_E, 122 | "axes_color" : WHITE, 123 | "secondary_stroke_width" : 1, 124 | "x_radius": SPACE_WIDTH, 125 | "y_radius": SPACE_HEIGHT, 126 | "space_unit_to_x_unit" : 1, 127 | "space_unit_to_y_unit" : 1, 128 | "x_line_frequency" : 1, 129 | "y_line_frequency" : 1, 130 | "secondary_line_ratio" : 1, 131 | "written_coordinate_height" : 0.2, 132 | "written_coordinate_nudge" : 0.1*(DOWN+RIGHT), 133 | "num_pair_at_center" : (0, 0), 134 | "propogate_style_to_family" : False, 135 | } 136 | 137 | def generate_points(self): 138 | self.axes = VMobject() 139 | self.main_lines = VMobject() 140 | self.secondary_lines = VMobject() 141 | tuples = [ 142 | ( 143 | self.x_radius, 144 | self.x_line_frequency, 145 | self.y_radius*DOWN, 146 | self.y_radius*UP, 147 | RIGHT 148 | ), 149 | ( 150 | self.y_radius, 151 | self.y_line_frequency, 152 | self.x_radius*LEFT, 153 | self.x_radius*RIGHT, 154 | UP, 155 | ), 156 | ] 157 | for radius, freq, start, end, unit in tuples: 158 | main_range = np.arange(0, radius, freq) 159 | step = freq/float(freq + self.secondary_line_ratio) 160 | for v in np.arange(0, radius, step): 161 | line1 = Line(start+v*unit, end+v*unit) 162 | line2 = Line(start-v*unit, end-v*unit) 163 | if v == 0: 164 | self.axes.add(line1) 165 | elif v in main_range: 166 | self.main_lines.add(line1, line2) 167 | else: 168 | self.secondary_lines.add(line1, line2) 169 | self.add(self.axes, self.main_lines, self.secondary_lines) 170 | self.stretch(self.space_unit_to_x_unit, 0) 171 | self.stretch(self.space_unit_to_y_unit, 1) 172 | #Put x_axis before y_axis 173 | y_axis, x_axis = self.axes.split() 174 | self.axes = VMobject(x_axis, y_axis) 175 | 176 | def init_colors(self): 177 | VMobject.init_colors(self) 178 | self.axes.set_stroke(self.axes_color, self.stroke_width) 179 | self.main_lines.set_stroke(self.color, self.stroke_width) 180 | self.secondary_lines.set_stroke( 181 | self.secondary_color, self.secondary_stroke_width 182 | ) 183 | return self 184 | 185 | def get_center_point(self): 186 | return self.num_pair_to_point(self.num_pair_at_center) 187 | 188 | def num_pair_to_point(self, pair): 189 | pair = np.array(pair) + self.num_pair_at_center 190 | result = self.get_center() 191 | result[0] += pair[0]*self.space_unit_to_x_unit 192 | result[1] += pair[1]*self.space_unit_to_y_unit 193 | return result 194 | 195 | def point_to_num_pair(self, point): 196 | new_point = point-self.get_center() 197 | center_x, center_y = self.num_pair_at_center 198 | x = center_x + point[0]/self.space_unit_to_x_unit 199 | y = center_y + point[1]/self.space_unit_to_y_unit 200 | return x, y 201 | 202 | def get_coordinate_labels(self, x_vals = None, y_vals = None): 203 | result = [] 204 | if x_vals == None and y_vals == None: 205 | x_vals = range(-int(self.x_radius), int(self.x_radius)) 206 | y_vals = range(-int(self.y_radius), int(self.y_radius)) 207 | for index, vals in enumerate([x_vals, y_vals]): 208 | num_pair = [0, 0] 209 | for val in vals: 210 | num_pair[index] = val 211 | point = self.num_pair_to_point(num_pair) 212 | num = TexMobject(str(val)) 213 | num.scale_to_fit_height( 214 | self.written_coordinate_height 215 | ) 216 | num.shift( 217 | point-num.get_corner(UP+LEFT), 218 | self.written_coordinate_nudge 219 | ) 220 | result.append(num) 221 | return result 222 | 223 | def get_axes(self): 224 | return self.axes 225 | 226 | def get_axis_labels(self, x_label = "x", y_label = "y"): 227 | x_axis, y_axis = self.get_axes().split() 228 | x_label_mob = TexMobject(x_label) 229 | y_label_mob = TexMobject(y_label) 230 | x_label_mob.next_to(x_axis, DOWN) 231 | x_label_mob.to_edge(RIGHT) 232 | y_label_mob.next_to(y_axis, RIGHT) 233 | y_label_mob.to_edge(UP) 234 | return VMobject(x_label_mob, y_label_mob) 235 | 236 | 237 | def add_coordinates(self, x_vals = None, y_vals = None): 238 | self.add(*self.get_coordinate_labels(x_vals, y_vals)) 239 | return self 240 | 241 | def get_vector(self, coords, **kwargs): 242 | point = coords[0]*RIGHT + coords[1]*UP 243 | arrow = Arrow(ORIGIN, coords, **kwargs) 244 | return arrow 245 | 246 | def prepare_for_nonlinear_transform(self, num_inserted_anchor_points = 50): 247 | for mob in self.family_members_with_points(): 248 | mob.insert_n_anchor_points(num_inserted_anchor_points) 249 | mob.make_smooth() 250 | return self 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | -------------------------------------------------------------------------------- /topics/numerals.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from mobject.vectorized_mobject import VMobject 4 | from mobject.tex_mobject import TexMobject 5 | from animation import Animation 6 | from scene import Scene 7 | from helpers import * 8 | 9 | class DecimalNumber(VMobject): 10 | CONFIG = { 11 | "num_decimal_points" : 2, 12 | "digit_to_digit_buff" : 0.05 13 | } 14 | def __init__(self, float_num, **kwargs): 15 | digest_config(self, kwargs) 16 | num_string = '%.*f' % (self.num_decimal_points, float_num) 17 | VMobject.__init__(self, *[ 18 | TexMobject(char) 19 | for char in num_string 20 | ], **kwargs) 21 | self.arrange_submobjects( 22 | buff = self.digit_to_digit_buff, 23 | aligned_edge = DOWN 24 | ) 25 | if float_num < 0: 26 | minus = self.submobjects[0] 27 | minus.next_to( 28 | self.submobjects[1], LEFT, 29 | buff = self.digit_to_digit_buff 30 | ) 31 | 32 | #Todo, this class is now broken 33 | 34 | class RangingValue(Animation): 35 | CONFIG = { 36 | "num_decimal_points" : 2, 37 | "rate_func" : None, 38 | "tracked_mobject" : None, 39 | "tracked_mobject_next_to_kwargs" : {}, 40 | "scale_factor" : None, 41 | "color" : WHITE, 42 | } 43 | def __init__(self, value_function, **kwargs): 44 | """ 45 | Value function should return a real value 46 | depending on the state of the surrounding scene 47 | """ 48 | digest_config(self, kwargs, locals()) 49 | self.update_mobject() 50 | Animation.__init__(self, self.mobject, **kwargs) 51 | 52 | def update_mobject(self, alpha = 0): 53 | mobject = DecimalNumber( 54 | self.value_function(), 55 | num_decimal_points = self.num_decimal_points, 56 | color = self.color, 57 | ) 58 | if not hasattr(self, "mobject"): 59 | self.mobject = mobject 60 | else: 61 | self.mobject.points = mobject.points 62 | self.mobject.submobjects = mobject.submobjects 63 | if self.scale_factor: 64 | self.mobject.scale(self.scale_factor) 65 | elif self.tracked_mobject: 66 | self.mobject.next_to( 67 | self.tracked_mobject, 68 | **self.tracked_mobject_next_to_kwargs 69 | ) 70 | return self 71 | 72 | 73 | class RangingValueScene(Scene): 74 | CONFIG = { 75 | "ranging_values" : [] 76 | } 77 | 78 | def add_ranging_value(self, value_function, **kwargs): 79 | self.ranging_values.append( 80 | RangingValue(value_function, **kwargs) 81 | ) 82 | 83 | def update_frame(self, *args, **kwargs): 84 | for val in self.ranging_values: 85 | self.remove(val.mobject) 86 | val.update_mobject() 87 | self.add(val.mobject) 88 | return Scene.update_frame(self, *args, **kwargs) 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /topics/three_dimensions.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import itertools as it 3 | 4 | from mobject import Mobject, Mobject1D, Mobject2D, Mobject, VMobject 5 | from geometry import Line 6 | from helpers import * 7 | 8 | class Stars(Mobject1D): 9 | CONFIG = { 10 | "stroke_width" : 1, 11 | "radius" : SPACE_WIDTH, 12 | "num_points" : 1000, 13 | } 14 | def generate_points(self): 15 | radii, phis, thetas = [ 16 | scalar*np.random.random(self.num_points) 17 | for scalar in [self.radius, np.pi, 2*np.pi] 18 | ] 19 | self.add_points([ 20 | ( 21 | r * np.sin(phi)*np.cos(theta), 22 | r * np.sin(phi)*np.sin(theta), 23 | r * np.cos(phi) 24 | ) 25 | for r, phi, theta in zip(radii, phis, thetas) 26 | ]) 27 | class StarsCartesian(Mobject2D): 28 | CONFIG = { 29 | "stroke_width" : 1, 30 | "radius" : 2*SPACE_WIDTH, 31 | "num_points" : 1000, 32 | } 33 | def generate_points(self): 34 | xs, ys, zs = [ 35 | scalar*np.random.random(self.num_points) 36 | for scalar in [self.radius, self.radius, self.radius] 37 | ] 38 | self.add_points([ 39 | ( 40 | x - self.radius/2, 41 | y - self.radius/2, 42 | z - self.radius/2 43 | ) 44 | for x, y, z in zip(xs, ys, zs) 45 | ]) 46 | 47 | class CubeWithFaces(Mobject2D): 48 | def generate_points(self): 49 | self.add_points([ 50 | sgn * np.array(coords) 51 | for x in np.arange(-1, 1, self.epsilon) 52 | for y in np.arange(x, 1, self.epsilon) 53 | for coords in it.permutations([x, y, 1]) 54 | for sgn in [-1, 1] 55 | ]) 56 | self.pose_at_angle() 57 | self.set_color(BLUE) 58 | 59 | def unit_normal(self, coords): 60 | return np.array(map(lambda x : 1 if abs(x) == 1 else 0, coords)) 61 | 62 | class Cube(Mobject1D): 63 | def generate_points(self): 64 | self.add_points([ 65 | ([a, b, c][p[0]], [a, b, c][p[1]], [a, b, c][p[2]]) 66 | for p in [(0, 1, 2), (2, 0, 1), (1, 2, 0)] 67 | for a, b, c in it.product([-1, 1], [-1, 1], np.arange(-1, 1, self.epsilon)) 68 | ]) 69 | self.pose_at_angle() 70 | self.set_color(YELLOW) 71 | 72 | class Octohedron(VMobject): 73 | CONFIG = { 74 | 75 | } 76 | def generate_points(self): 77 | # TODO in a more generalized way where you can just define the vertices? 78 | vertex_pairs = [ 79 | (LEFT+UP, RIGHT+UP), 80 | (RIGHT+UP, RIGHT+DOWN), 81 | (RIGHT+DOWN, LEFT+DOWN), 82 | (LEFT+DOWN, LEFT+UP), 83 | 84 | (LEFT+UP, IN*np.sqrt(2)), 85 | (RIGHT+UP, IN*np.sqrt(2)), 86 | (RIGHT+DOWN, IN*np.sqrt(2)), 87 | (LEFT+DOWN, IN*np.sqrt(2)), 88 | 89 | (LEFT+UP, OUT*np.sqrt(2)), 90 | (RIGHT+UP, OUT*np.sqrt(2)), 91 | (RIGHT+DOWN, OUT*np.sqrt(2)), 92 | (LEFT+DOWN, OUT*np.sqrt(2)) 93 | ] 94 | 95 | for pair in vertex_pairs: 96 | self.add(Line(pair[0], pair[1])) 97 | 98 | 99 | self.rotate_in_place(- 3 * np.pi / 7, RIGHT) 100 | self.rotate_in_place(np.pi / 12, UP) 101 | 102 | class Dodecahedron(Mobject1D): 103 | def generate_points(self): 104 | phi = (1 + np.sqrt(5)) / 2 105 | x = np.array([1, 0, 0]) 106 | y = np.array([0, 1, 0]) 107 | z = np.array([0, 0, 1]) 108 | v1, v2 = (phi, 1/phi, 0), (phi, -1/phi, 0) 109 | vertex_pairs = [ 110 | (v1, v2), 111 | (x+y+z, v1), 112 | (x+y-z, v1), 113 | (x-y+z, v2), 114 | (x-y-z, v2), 115 | ] 116 | five_lines_points = Mobject(*[ 117 | Line(pair[0], pair[1], density = 1.0/self.epsilon) 118 | for pair in vertex_pairs 119 | ]).points 120 | #Rotate those 5 edges into all 30. 121 | for i in range(3): 122 | perm = map(lambda j : j%3, range(i, i+3)) 123 | for b in [-1, 1]: 124 | matrix = b*np.array([x[perm], y[perm], z[perm]]) 125 | self.add_points(np.dot(five_lines_points, matrix)) 126 | self.pose_at_angle() 127 | self.set_color(GREEN) 128 | 129 | class Sphere(Mobject2D): 130 | def generate_points(self): 131 | self.add_points([ 132 | ( 133 | np.sin(phi) * np.cos(theta), 134 | np.sin(phi) * np.sin(theta), 135 | np.cos(phi) 136 | ) 137 | for phi in np.arange(self.epsilon, np.pi, self.epsilon) 138 | for theta in np.arange(0, 2 * np.pi, 2 * self.epsilon / np.sin(phi)) 139 | ]) 140 | self.set_color(BLUE) 141 | 142 | def unit_normal(self, coords): 143 | return np.array(coords) / np.linalg.norm(coords) 144 | 145 | --------------------------------------------------------------------------------