Merge pull request #197 from 3b1b/PEP8

Changed all files to (mostly) conform to PEP8
This commit is contained in:
Grant Sanderson
2018-04-06 14:00:57 -07:00
committed by GitHub
71 changed files with 3736 additions and 3604 deletions

View File

@@ -7,28 +7,30 @@ from utils.rate_functions import smooth
from utils.config_ops import instantiate
from utils.config_ops import digest_config
class Animation(object):
CONFIG = {
"run_time" : DEFAULT_ANIMATION_RUN_TIME,
"rate_func" : smooth,
"name" : None,
#Does this animation add or remove a mobject form the screen
"remover" : False,
#Options are lagged_start, smoothed_lagged_start,
#one_at_a_time, all_at_once
"submobject_mode" : "all_at_once",
"lag_factor" : 2,
"run_time": DEFAULT_ANIMATION_RUN_TIME,
"rate_func": smooth,
"name": None,
# Does this animation add or remove a mobject form the screen
"remover": False,
# Options are lagged_start, smoothed_lagged_start,
# one_at_a_time, all_at_once
"submobject_mode": "all_at_once",
"lag_factor": 2,
# Used by EmptyAnimation to announce itself ignorable
# in Successions and AnimationGroups
"empty" : False
"empty": False
}
def __init__(self, mobject, **kwargs):
mobject = instantiate(mobject)
assert(isinstance(mobject, Mobject))
digest_config(self, kwargs, locals())
self.starting_mobject = self.mobject.copy()
if self.rate_func is None:
self.rate_func = (lambda x : x)
self.rate_func = (lambda x: x)
if self.name is None:
self.name = self.__class__.__name__ + str(self.mobject)
self.all_families_zipped = self.get_all_families_zipped()
@@ -37,7 +39,7 @@ class Animation(object):
def update_config(self, **kwargs):
digest_config(self, kwargs)
if "rate_func" in kwargs and kwargs["rate_func"] is None:
self.rate_func = (lambda x : x)
self.rate_func = (lambda x: x)
return self
def __str__(self):
@@ -58,13 +60,13 @@ class Animation(object):
return self
def update_submobject(self, submobject, starting_sumobject, alpha):
#Typically ipmlemented by subclass
# Typically ipmlemented by subclass
pass
def get_all_mobjects(self):
"""
Ordering must match the ording of arguments to update_submobject
"""
"""
return self.mobject, self.starting_mobject
def get_all_families_zipped(self):
@@ -75,15 +77,15 @@ class Animation(object):
def get_sub_alpha(self, alpha, index, num_submobjects):
if self.submobject_mode in ["lagged_start", "smoothed_lagged_start"]:
prop = float(index)/num_submobjects
prop = float(index) / num_submobjects
if self.submobject_mode is "smoothed_lagged_start":
prop = smooth(prop)
lf = self.lag_factor
return np.clip(lf*alpha - (lf-1)*prop, 0, 1)
return np.clip(lf * alpha - (lf - 1) * prop, 0, 1)
elif self.submobject_mode == "one_at_a_time":
lower = float(index)/num_submobjects
upper = float(index+1)/num_submobjects
return np.clip((alpha-lower)/(upper-lower), 0, 1)
lower = float(index) / num_submobjects
upper = float(index + 1) / num_submobjects
return np.clip((alpha - lower) / (upper - lower), 0, 1)
elif self.submobject_mode == "all_at_once":
return alpha
raise Exception("Invalid submobject mode")
@@ -101,7 +103,8 @@ class Animation(object):
def set_rate_func(self, rate_func):
if rate_func is None:
rate_func = lambda x : x
def rate_func(x):
return x
self.rate_func = rate_func
return self
@@ -115,7 +118,7 @@ class Animation(object):
def is_remover(self):
return self.remover
def clean_up(self, surrounding_scene = None):
def clean_up(self, surrounding_scene=None):
self.update(1)
if surrounding_scene is not None:
if self.is_remover():
@@ -123,16 +126,3 @@ class Animation(object):
else:
surrounding_scene.add(self.mobject)
return self

View File

@@ -14,25 +14,28 @@ from utils.bezier import inverse_interpolate
from utils.config_ops import digest_config
from utils.rate_functions import squish_rate_func
class EmptyAnimation(Animation):
CONFIG = {
"run_time" : 0,
"empty" : True
"run_time": 0,
"empty": True
}
def __init__(self, *args, **kwargs):
return Animation.__init__(self, Group(), *args, **kwargs)
class Succession(Animation):
CONFIG = {
"rate_func" : None,
"rate_func": None,
}
def __init__(self, *args, **kwargs):
"""
Each arg will either be an animation, or an animation class
followed by its arguments (and potentially a dict for
Each arg will either be an animation, or an animation class
followed by its arguments (and potentially a dict for
configuration).
For example,
For example,
Succession(
ShowCreation(circle),
Transform, circle, square,
@@ -42,16 +45,17 @@ class Succession(Animation):
"""
animations = []
state = {
"animations" : animations,
"curr_class" : None,
"curr_class_args" : [],
"curr_class_config" : {},
"animations": animations,
"curr_class": None,
"curr_class_args": [],
"curr_class_config": {},
}
def invoke_curr_class(state):
if state["curr_class"] is None:
return
anim = state["curr_class"](
*state["curr_class_args"],
*state["curr_class_args"],
**state["curr_class_config"]
)
state["animations"].append(anim)
@@ -76,43 +80,47 @@ class Succession(Animation):
for anim in animations:
anim.update(0)
animations = filter (lambda x : not(x.empty), animations)
animations = filter(lambda x: not(x.empty), animations)
self.run_times = [anim.run_time for anim in animations]
if "run_time" in kwargs:
run_time = kwargs.pop("run_time")
warnings.warn("Succession doesn't currently support explicit run_time.")
warnings.warn(
"Succession doesn't currently support explicit run_time.")
run_time = sum(self.run_times)
self.num_anims = len(animations)
if self.num_anims == 0:
self.empty = True
self.animations = animations
#Have to keep track of this run_time, because Scene.play
#might very well mess with it.
# Have to keep track of this run_time, because Scene.play
# might very well mess with it.
self.original_run_time = run_time
# critical_alphas[i] is the start alpha of self.animations[i]
# critical_alphas[i + 1] is the end alpha of self.animations[i]
critical_times = np.concatenate(([0], np.cumsum(self.run_times)))
self.critical_alphas = map (lambda x : np.true_divide(x, run_time), critical_times) if self.num_anims > 0 else [0.0]
self.critical_alphas = map(lambda x: np.true_divide(
x, run_time), critical_times) if self.num_anims > 0 else [0.0]
# self.scene_mobjects_at_time[i] is the scene's mobjects at start of self.animations[i]
# self.scene_mobjects_at_time[i + 1] is the scene mobjects at end of self.animations[i]
self.scene_mobjects_at_time = [None for i in range(self.num_anims + 1)]
self.scene_mobjects_at_time[0] = Group()
for i in range(self.num_anims):
self.scene_mobjects_at_time[i + 1] = self.scene_mobjects_at_time[i].copy()
self.scene_mobjects_at_time[i +
1] = self.scene_mobjects_at_time[i].copy()
self.animations[i].clean_up(self.scene_mobjects_at_time[i + 1])
self.current_alpha = 0
self.current_anim_index = 0 # If self.num_anims == 0, this is an invalid index, but so it goes
# If self.num_anims == 0, this is an invalid index, but so it goes
self.current_anim_index = 0
if self.num_anims > 0:
self.mobject = self.scene_mobjects_at_time[0]
self.mobject.add(self.animations[0].mobject)
else:
self.mobject = Group()
Animation.__init__(self, self.mobject, run_time = run_time, **kwargs)
Animation.__init__(self, self.mobject, run_time=run_time, **kwargs)
# Beware: This does NOT take care of calling update(0) on the subanimation.
# This was important to avoid a pernicious possibility in which subanimations were called
@@ -120,7 +128,8 @@ class Succession(Animation):
# continuing exponentially.
def jump_to_start_of_anim(self, index):
if index != self.current_anim_index:
self.mobject.remove(*self.mobject.submobjects) # Should probably have a cleaner "remove_all" method...
# Should probably have a cleaner "remove_all" method...
self.mobject.remove(*self.mobject.submobjects)
self.mobject.add(*self.scene_mobjects_at_time[index].submobjects)
self.mobject.add(self.animations[index].mobject)
@@ -138,18 +147,18 @@ class Succession(Animation):
return
gt_alpha_iter = it.ifilter(
lambda i : self.critical_alphas[i+1] >= alpha,
lambda i: self.critical_alphas[i + 1] >= alpha,
range(self.num_anims)
)
i = next(gt_alpha_iter, None)
if i == None:
# In this case, we assume what is happening is that alpha is 1.0,
if i is None:
# In this case, we assume what is happening is that alpha is 1.0,
# but that rounding error is causing us to overshoot the end of
# self.critical_alphas (which is also 1.0)
if not abs(alpha - 1) < 0.001:
warnings.warn(
"Rounding error not near alpha=1 in Succession.update_mobject," + \
"instead alpha = %f"%alpha
"Rounding error not near alpha=1 in Succession.update_mobject," +
"instead alpha = %f" % alpha
)
print self.critical_alphas, alpha
i = self.num_anims - 1
@@ -158,8 +167,8 @@ class Succession(Animation):
self.jump_to_start_of_anim(i)
sub_alpha = inverse_interpolate(
self.critical_alphas[i],
self.critical_alphas[i + 1],
self.critical_alphas[i],
self.critical_alphas[i + 1],
alpha
)
self.animations[i].update(sub_alpha)
@@ -171,14 +180,16 @@ class Succession(Animation):
for anim in self.animations:
anim.clean_up(*args, **kwargs)
class AnimationGroup(Animation):
CONFIG = {
"rate_func" : None
"rate_func": None
}
def __init__(self, *sub_anims, **kwargs):
sub_anims = filter (lambda x : not(x.empty), sub_anims)
sub_anims = filter(lambda x: not(x.empty), sub_anims)
digest_config(self, locals())
self.update_config(**kwargs) # Handles propagation to self.sub_anims
self.update_config(**kwargs) # Handles propagation to self.sub_anims
if len(sub_anims) == 0:
self.empty = True
@@ -198,7 +209,7 @@ class AnimationGroup(Animation):
def update_config(self, **kwargs):
Animation.update_config(self, **kwargs)
# If AnimationGroup is called with any configuration,
# it is propagated to the sub_animations
for anim in self.sub_anims:
@@ -206,30 +217,33 @@ class AnimationGroup(Animation):
# Variants on mappin an animation over submobjectsg
class LaggedStart(Animation):
CONFIG = {
"run_time" : 2,
"lag_ratio" : 0.5,
"run_time": 2,
"lag_ratio": 0.5,
}
def __init__(self, AnimationClass, mobject, arg_creator = None, **kwargs):
def __init__(self, AnimationClass, mobject, arg_creator=None, **kwargs):
digest_config(self, kwargs)
for key in "rate_func", "run_time", "lag_ratio":
if key in kwargs:
kwargs.pop(key)
if arg_creator is None:
arg_creator = lambda mobject : (mobject,)
def arg_creator(mobject):
return (mobject,)
self.subanimations = [
AnimationClass(
*arg_creator(submob),
run_time = self.run_time,
rate_func = squish_rate_func(
run_time=self.run_time,
rate_func=squish_rate_func(
self.rate_func, beta, beta + self.lag_ratio
),
**kwargs
)
for submob, beta in zip(
mobject,
np.linspace(0, 1-self.lag_ratio, len(mobject))
mobject,
np.linspace(0, 1 - self.lag_ratio, len(mobject))
)
]
Animation.__init__(self, mobject, **kwargs)
@@ -243,6 +257,7 @@ class LaggedStart(Animation):
for anim in self.subanimations:
anim.clean_up(*args, **kwargs)
class ApplyToCenters(Animation):
def __init__(self, AnimationClass, mobjects, **kwargs):
full_kwargs = AnimationClass.CONFIG
@@ -259,9 +274,8 @@ class ApplyToCenters(Animation):
def update_mobject(self, alpha):
self.centers_container.update_mobject(alpha)
center_mobs = self.centers_container.mobject.split()
mobjects = self.mobject.split()
mobjects = self.mobject.split()
for center_mob, mobject in zip(center_mobs, mobjects):
mobject.shift(
center_mob.get_center()-mobject.get_center()
center_mob.get_center() - mobject.get_center()
)

View File

@@ -8,9 +8,7 @@ from animation.animation import Animation
from animation.movement import Homotopy
from animation.creation import ShowPartial
from animation.transform import Transform
from mobject.mobject import Group
from mobject.mobject import Mobject
from mobject.types.vectorized_mobject import VMobject
from mobject.geometry import Circle
from mobject.geometry import Dot
from utils.config_ops import digest_config
@@ -20,31 +18,34 @@ from utils.rate_functions import there_and_back
class FocusOn(Transform):
CONFIG = {
"opacity" : 0.2,
"color" : GREY,
"run_time" : 2,
"remover" : True,
"opacity": 0.2,
"color": GREY,
"run_time": 2,
"remover": True,
}
def __init__(self, mobject_or_point, **kwargs):
digest_config(self, kwargs)
big_dot = Dot(
radius = FRAME_X_RADIUS+FRAME_Y_RADIUS,
stroke_width = 0,
fill_color = self.color,
fill_opacity = 0,
radius=FRAME_X_RADIUS + FRAME_Y_RADIUS,
stroke_width=0,
fill_color=self.color,
fill_opacity=0,
)
little_dot = Dot(radius = 0)
little_dot.set_fill(self.color, opacity = self.opacity)
little_dot = Dot(radius=0)
little_dot.set_fill(self.color, opacity=self.opacity)
little_dot.move_to(mobject_or_point)
Transform.__init__(self, big_dot, little_dot, **kwargs)
class Indicate(Transform):
CONFIG = {
"rate_func" : there_and_back,
"scale_factor" : 1.2,
"color" : YELLOW,
"rate_func": there_and_back,
"scale_factor": 1.2,
"color": YELLOW,
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
target = mobject.copy()
@@ -52,27 +53,31 @@ class Indicate(Transform):
target.set_color(self.color)
Transform.__init__(self, mobject, target, **kwargs)
class CircleIndicate(Indicate):
CONFIG = {
"rate_func" : squish_rate_func(there_and_back, 0, 0.8),
"remover" : True
"rate_func": squish_rate_func(there_and_back, 0, 0.8),
"remover": True
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
circle = Circle(color = self.color, **kwargs)
circle = Circle(color=self.color, **kwargs)
circle.surround(mobject)
Indicate.__init__(self, circle, **kwargs)
class ShowPassingFlash(ShowPartial):
CONFIG = {
"time_width" : 0.1,
"remover" : True,
"time_width": 0.1,
"remover": True,
}
def get_bounds(self, alpha):
alpha *= (1+self.time_width)
alpha -= self.time_width/2.0
lower = max(0, alpha - self.time_width/2.0)
upper = min(1, alpha + self.time_width/2.0)
alpha *= (1 + self.time_width)
alpha -= self.time_width / 2.0
lower = max(0, alpha - self.time_width / 2.0)
upper = min(1, alpha + self.time_width / 2.0)
return (lower, upper)
def clean_up(self, *args, **kwargs):
@@ -80,43 +85,49 @@ class ShowPassingFlash(ShowPartial):
for submob, start_submob in self.get_all_families_zipped():
submob.pointwise_become_partial(start_submob, 0, 1)
class ShowCreationThenDestruction(ShowPassingFlash):
CONFIG = {
"time_width" : 2.0,
"run_time" : 1,
"time_width": 2.0,
"run_time": 1,
}
class ApplyWave(Homotopy):
CONFIG = {
"direction" : DOWN,
"amplitude" : 0.2,
"run_time" : 1,
"apply_function_kwargs" : {
"maintain_smoothness" : False,
"direction": DOWN,
"amplitude": 0.2,
"run_time": 1,
"apply_function_kwargs": {
"maintain_smoothness": False,
},
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs, locals())
left_x = mobject.get_left()[0]
right_x = mobject.get_right()[0]
vect = self.amplitude*self.direction
vect = self.amplitude * self.direction
def homotopy(x, y, z, t):
start_point = np.array([x, y, z])
alpha = (x-left_x)/(right_x-left_x)
power = np.exp(2*(alpha-0.5))
alpha = (x - left_x) / (right_x - left_x)
power = np.exp(2 * (alpha - 0.5))
nudge = there_and_back(t**power)
return np.array([x, y, z]) + nudge*vect
return np.array([x, y, z]) + nudge * vect
Homotopy.__init__(self, homotopy, mobject, **kwargs)
class WiggleOutThenIn(Animation):
CONFIG = {
"scale_value" : 1.1,
"rotation_angle" : 0.01*TAU,
"n_wiggles" : 6,
"run_time" : 2,
"scale_about_point" : None,
"rotate_about_point" : None,
"scale_value": 1.1,
"rotation_angle": 0.01 * TAU,
"n_wiggles": 6,
"run_time": 2,
"scale_about_point": None,
"rotate_about_point": None,
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
if self.scale_about_point is None:
@@ -126,62 +137,65 @@ class WiggleOutThenIn(Animation):
Animation.__init__(self, mobject, **kwargs)
def update_submobject(self, submobject, starting_sumobject, alpha):
submobject.points[:,:] = starting_sumobject.points
submobject.points[:, :] = starting_sumobject.points
submobject.scale(
interpolate(1, self.scale_value, there_and_back(alpha)),
about_point = self.scale_about_point
about_point=self.scale_about_point
)
submobject.rotate(
wiggle(alpha, self.n_wiggles)*self.rotation_angle,
about_point = self.rotate_about_point
wiggle(alpha, self.n_wiggles) * self.rotation_angle,
about_point=self.rotate_about_point
)
class Vibrate(Animation):
CONFIG = {
"spatial_period" : 6,
"temporal_period" : 1,
"overtones" : 4,
"amplitude" : 0.5,
"radius" : FRAME_X_RADIUS/2,
"run_time" : 3.0,
"rate_func" : None
"spatial_period": 6,
"temporal_period": 1,
"overtones": 4,
"amplitude": 0.5,
"radius": FRAME_X_RADIUS / 2,
"run_time": 3.0,
"rate_func": None
}
def __init__(self, mobject = None, **kwargs):
def __init__(self, mobject=None, **kwargs):
if mobject is None:
mobject = Line(3*LEFT, 3*RIGHT)
mobject = Line(3 * LEFT, 3 * RIGHT)
Animation.__init__(self, mobject, **kwargs)
def wave_function(self, x, t):
return sum([
reduce(op.mul, [
self.amplitude/(k**2), #Amplitude
np.sin(2*np.pi*(k**1.5)*t/self.temporal_period), #Frequency
np.sin(2*np.pi*k*x/self.spatial_period) #Number of waves
self.amplitude / (k**2), # Amplitude
np.sin(2 * np.pi * (k**1.5) * t / \
self.temporal_period), # Frequency
# Number of waves
np.sin(2 * np.pi * k * x / self.spatial_period)
])
for k in range(1, self.overtones+1)
for k in range(1, self.overtones + 1)
])
def update_mobject(self, alpha):
time = alpha*self.run_time
time = alpha * self.run_time
families = map(
Mobject.submobject_family,
[self.mobject, self.starting_mobject]
)
for mob, start in zip(*families):
mob.points = np.apply_along_axis(
lambda (x, y, z) : (x, y + self.wave_function(x, time), z),
lambda (x, y, z): (x, y + self.wave_function(x, time), z),
1, start.points
)
class TurnInsideOut(Transform):
CONFIG = {
"path_arc" : TAU/4,
"path_arc": TAU / 4,
}
def __init__(self, mobject, **kwargs):
mobject.sort_points(np.linalg.norm)
mob_copy = mobject.copy()
mob_copy.sort_points(lambda p : -np.linalg.norm(p))
mob_copy.sort_points(lambda p: -np.linalg.norm(p))
Transform.__init__(self, mobject, mob_copy, **kwargs)

View File

@@ -2,22 +2,22 @@ from __future__ import absolute_import
from constants import *
import warnings
from animation.animation import Animation
from utils.config_ops import digest_config
class Homotopy(Animation):
CONFIG = {
"run_time" : 3,
"apply_function_kwargs" : {},
"run_time": 3,
"apply_function_kwargs": {},
}
def __init__(self, homotopy, mobject, **kwargs):
"""
Homotopy a function from (x, y, z, t) to (x', y', z')
"""
def function_at_time_t(t):
return lambda p : homotopy(p[0], p[1], p[2], t)
return lambda p: homotopy(p[0], p[1], p[2], t)
self.function_at_time_t = function_at_time_t
digest_config(self, kwargs)
Animation.__init__(self, mobject, **kwargs)
@@ -29,11 +29,13 @@ class Homotopy(Animation):
**self.apply_function_kwargs
)
class SmoothedVectorizedHomotopy(Homotopy):
def update_submobject(self, submob, start, alpha):
Homotopy.update_submobject(self, submob, start, alpha)
submob.make_smooth()
class ComplexHomotopy(Homotopy):
def __init__(self, complex_homotopy, mobject, **kwargs):
"""
@@ -48,21 +50,23 @@ class ComplexHomotopy(Homotopy):
class PhaseFlow(Animation):
CONFIG = {
"virtual_time" : 1,
"rate_func" : None,
"virtual_time": 1,
"rate_func": None,
}
def __init__(self, function, mobject, **kwargs):
digest_config(self, kwargs, locals())
digest_config(self, kwargs, locals())
Animation.__init__(self, mobject, **kwargs)
def update_mobject(self, alpha):
if hasattr(self, "last_alpha"):
dt = self.virtual_time*(alpha-self.last_alpha)
dt = self.virtual_time * (alpha - self.last_alpha)
self.mobject.apply_function(
lambda p : p + dt*self.function(p)
lambda p: p + dt * self.function(p)
)
self.last_alpha = alpha
class MoveAlongPath(Animation):
def __init__(self, mobject, path, **kwargs):
digest_config(self, kwargs, locals())
@@ -71,4 +75,3 @@ class MoveAlongPath(Animation):
def update_mobject(self, alpha):
point = self.path.point_from_proportion(alpha)
self.mobject.move_to(point)

View File

@@ -7,13 +7,15 @@ from mobject.numbers import DecimalNumber
from utils.bezier import interpolate
from utils.config_ops import digest_config
class ChangingDecimal(Animation):
CONFIG = {
"num_decimal_points" : None,
"show_ellipsis" : None,
"position_update_func" : None,
"tracked_mobject" : None,
"num_decimal_points": None,
"show_ellipsis": None,
"position_update_func": None,
"tracked_mobject": None,
}
def __init__(self, decimal_number_mobject, number_update_func, **kwargs):
digest_config(self, kwargs, locals())
self.decimal_number_config = dict(
@@ -52,10 +54,14 @@ class ChangingDecimal(Animation):
if self.position_update_func is not None:
self.position_update_func(self.decimal_number_mobject)
elif self.tracked_mobject is not None:
self.decimal_number_mobject.move_to(self.tracked_mobject.get_center() + self.diff_from_tracked_mobject)
self.decimal_number_mobject.move_to(
self.tracked_mobject.get_center() + self.diff_from_tracked_mobject)
class ChangeDecimalToValue(ChangingDecimal):
def __init__(self, decimal_number_mobject, target_number, **kwargs):
start_number = decimal_number_mobject.number
func = lambda alpha : interpolate(start_number, target_number, alpha)
def func(alpha):
return interpolate(start_number, target_number, alpha)
ChangingDecimal.__init__(self, decimal_number_mobject, func, **kwargs)

View File

@@ -1,26 +1,25 @@
from __future__ import absolute_import
import itertools as it
import numpy as np
from constants import *
import warnings
from animation.animation import Animation
from animation.transform import Transform
from utils.config_ops import digest_config
class Rotating(Animation):
CONFIG = {
"axis" : OUT,
"radians" : 2*np.pi,
"run_time" : 5,
"rate_func" : None,
"in_place" : True,
"about_point" : None,
"about_edge" : None,
"axis": OUT,
"radians": 2 * np.pi,
"run_time": 5,
"rate_func": None,
"in_place": True,
"about_point": None,
"about_edge": None,
}
def update_submobject(self, submobject, starting_submobject, alpha):
submobject.points = np.array(starting_submobject.points)
@@ -28,22 +27,24 @@ class Rotating(Animation):
Animation.update_mobject(self, alpha)
about_point = None
if self.about_point is not None:
about_point = self.about_point
elif self.in_place: #This is superseeded
self.about_point = about_point
elif self.in_place: # This is superseeded
self.about_point = self.mobject.get_center()
self.mobject.rotate(
alpha*self.radians,
axis = self.axis,
about_point = self.about_point,
about_edge = self.about_edge,
alpha * self.radians,
axis=self.axis,
about_point=self.about_point,
about_edge=self.about_edge,
)
class Rotate(Transform):
CONFIG = {
"in_place" : False,
"about_point" : None,
"in_place": False,
"about_point": None,
}
def __init__(self, mobject, angle = np.pi, axis = OUT, **kwargs):
def __init__(self, mobject, angle=np.pi, axis=OUT, **kwargs):
if "path_arc" not in kwargs:
kwargs["path_arc"] = angle
if "path_arc_axis" not in kwargs:
@@ -53,9 +54,8 @@ class Rotate(Transform):
if self.in_place:
self.about_point = mobject.get_center()
target.rotate(
angle,
axis = axis,
about_point = self.about_point,
angle,
axis=axis,
about_point=self.about_point,
)
Transform.__init__(self, mobject, target, **kwargs)

View File

@@ -11,57 +11,60 @@ from mobject.types.vectorized_mobject import VGroup
from mobject.geometry import Circle
from utils.config_ops import digest_config
class MoveCar(ApplyMethod):
CONFIG = {
"moving_forward" : True,
"moving_forward": True,
}
def __init__(self, car, target_point, **kwargs):
assert isinstance(car, Car)
ApplyMethod.__init__(self, car.move_to, target_point, **kwargs)
displacement = self.target_mobject.get_right()-self.starting_mobject.get_right()
displacement = self.target_mobject.get_right() - self.starting_mobject.get_right()
distance = np.linalg.norm(displacement)
if not self.moving_forward:
distance *= -1
tire_radius = car.get_tires()[0].get_width()/2
self.total_tire_radians = -distance/tire_radius
tire_radius = car.get_tires()[0].get_width() / 2
self.total_tire_radians = -distance / tire_radius
def update_mobject(self, alpha):
ApplyMethod.update_mobject(self, alpha)
if alpha == 0:
return
radians = alpha*self.total_tire_radians
radians = alpha * self.total_tire_radians
for tire in self.mobject.get_tires():
tire.rotate_in_place(radians)
class Broadcast(LaggedStart):
CONFIG = {
"small_radius" : 0.0,
"big_radius" : 5,
"n_circles" : 5,
"start_stroke_width" : 8,
"color" : WHITE,
"remover" : True,
"lag_ratio" : 0.7,
"run_time" : 3,
"remover" : True,
"small_radius": 0.0,
"big_radius": 5,
"n_circles": 5,
"start_stroke_width": 8,
"color": WHITE,
"remover": True,
"lag_ratio": 0.7,
"run_time": 3,
"remover": True,
}
def __init__(self, focal_point, **kwargs):
digest_config(self, kwargs)
circles = VGroup()
for x in range(self.n_circles):
circle = Circle(
radius = self.big_radius,
stroke_color = BLACK,
stroke_width = 0,
radius=self.big_radius,
stroke_color=BLACK,
stroke_width=0,
)
circle.move_to(focal_point)
circle.save_state()
circle.scale_to_fit_width(self.small_radius*2)
circle.scale_to_fit_width(self.small_radius * 2)
circle.set_stroke(self.color, self.start_stroke_width)
circles.add(circle)
LaggedStart.__init__(
self, ApplyMethod, circles,
lambda c : (c.restore,),
lambda c: (c.restore,),
**kwargs
)

View File

@@ -17,16 +17,18 @@ from utils.rate_functions import smooth
from utils.rate_functions import squish_rate_func
from utils.space_ops import complex_to_R3
class Transform(Animation):
CONFIG = {
"path_arc" : 0,
"path_arc_axis" : OUT,
"path_func" : None,
"submobject_mode" : "all_at_once",
"replace_mobject_with_target_in_scene" : False,
"path_arc": 0,
"path_arc_axis": OUT,
"path_func": None,
"submobject_mode": "all_at_once",
"replace_mobject_with_target_in_scene": False,
}
def __init__(self, mobject, target_mobject, **kwargs):
#Copy target_mobject so as to not mess with caller
# Copy target_mobject so as to not mess with caller
self.original_target_mobject = target_mobject
target_mobject = target_mobject.copy()
mobject.align_data(target_mobject)
@@ -35,7 +37,7 @@ class Transform(Animation):
self.init_path_func()
Animation.__init__(self, mobject, **kwargs)
self.name += "To" + str(target_mobject)
self.name += "To" + str(target_mobject)
def update_config(self, **kwargs):
Animation.update_config(self, **kwargs)
@@ -63,38 +65,45 @@ class Transform(Animation):
submob.interpolate(start, end, alpha, self.path_func)
return self
def clean_up(self, surrounding_scene = None):
def clean_up(self, surrounding_scene=None):
Animation.clean_up(self, surrounding_scene)
if self.replace_mobject_with_target_in_scene and surrounding_scene is not None:
surrounding_scene.remove(self.mobject)
if not self.remover:
surrounding_scene.add(self.original_target_mobject)
class ReplacementTransform(Transform):
CONFIG = {
"replace_mobject_with_target_in_scene" : True,
"replace_mobject_with_target_in_scene": True,
}
class ClockwiseTransform(Transform):
CONFIG = {
"path_arc" : -np.pi
"path_arc": -np.pi
}
class CounterclockwiseTransform(Transform):
CONFIG = {
"path_arc" : np.pi
"path_arc": np.pi
}
class MoveToTarget(Transform):
def __init__(self, mobject, **kwargs):
if not hasattr(mobject, "target"):
raise Exception("MoveToTarget called on mobject without attribute 'target' ")
raise Exception(
"MoveToTarget called on mobject without attribute 'target' ")
Transform.__init__(self, mobject, mobject.target, **kwargs)
class ApplyMethod(Transform):
CONFIG = {
"submobject_mode" : "all_at_once"
"submobject_mode": "all_at_once"
}
def __init__(self, method, *args, **kwargs):
"""
Method is a method of Mobject. *args is for the method,
@@ -104,11 +113,11 @@ class ApplyMethod(Transform):
"""
if not inspect.ismethod(method):
raise Exception(
"Whoops, looks like you accidentally invoked " + \
"the method you want to animate"
)
"Whoops, looks like you accidentally invoked " +
"the method you want to animate"
)
assert(isinstance(method.im_self, Mobject))
args = list(args) #So that args.pop() works
args = list(args) # So that args.pop() works
if "method_kwargs" in kwargs:
method_kwargs = kwargs["method_kwargs"]
elif len(args) > 0 and isinstance(args[-1], dict):
@@ -119,38 +128,46 @@ class ApplyMethod(Transform):
method.im_func(target, *args, **method_kwargs)
Transform.__init__(self, method.im_self, target, **kwargs)
class ApplyPointwiseFunction(ApplyMethod):
CONFIG = {
"run_time" : DEFAULT_POINTWISE_FUNCTION_RUN_TIME
"run_time": DEFAULT_POINTWISE_FUNCTION_RUN_TIME
}
def __init__(self, function, mobject, **kwargs):
ApplyMethod.__init__(
self, mobject.apply_function, function, **kwargs
)
class FadeToColor(ApplyMethod):
def __init__(self, mobject, color, **kwargs):
ApplyMethod.__init__(self, mobject.set_color, color, **kwargs)
class ScaleInPlace(ApplyMethod):
def __init__(self, mobject, scale_factor, **kwargs):
ApplyMethod.__init__(self, mobject.scale_in_place, scale_factor, **kwargs)
ApplyMethod.__init__(self, mobject.scale_in_place,
scale_factor, **kwargs)
class ApplyFunction(Transform):
CONFIG = {
"submobject_mode" : "all_at_once",
"submobject_mode": "all_at_once",
}
def __init__(self, function, mobject, **kwargs):
Transform.__init__(
self,
mobject,
self,
mobject,
function(mobject.copy()),
**kwargs
)
self.name = "ApplyFunctionTo"+str(mobject)
self.name = "ApplyFunctionTo" + str(mobject)
class ApplyMatrix(ApplyPointwiseFunction):
#Truth be told, I'm not sure if this is useful.
# Truth be told, I'm not sure if this is useful.
def __init__(self, matrix, mobject, **kwargs):
matrix = np.array(matrix)
if matrix.shape == (2, 2):
@@ -160,10 +177,12 @@ class ApplyMatrix(ApplyPointwiseFunction):
elif matrix.shape != (3, 3):
raise "Matrix has bad dimensions"
transpose = np.transpose(matrix)
def func(p):
return np.dot(p, transpose)
ApplyPointwiseFunction.__init__(self, func, mobject, **kwargs)
class ComplexFunction(ApplyPointwiseFunction):
def __init__(self, function, mobject, **kwargs):
if "path_func" not in kwargs:
@@ -172,17 +191,19 @@ class ComplexFunction(ApplyPointwiseFunction):
)
ApplyPointwiseFunction.__init__(
self,
lambda (x, y, z) : complex_to_R3(function(complex(x, y))),
lambda (x, y, z): complex_to_R3(function(complex(x, y))),
instantiate(mobject),
**kwargs
)
###
class CyclicReplace(Transform):
CONFIG = {
"path_arc" : np.pi/2
"path_arc": np.pi / 2
}
def __init__(self, *mobjects, **kwargs):
start = Group(*mobjects)
target = Group(*[
@@ -191,14 +212,18 @@ class CyclicReplace(Transform):
])
Transform.__init__(self, start, target, **kwargs)
class Swap(CyclicReplace):
pass #Renaming, more understandable for two entries
#TODO: Um...does this work
class Swap(CyclicReplace):
pass # Renaming, more understandable for two entries
# TODO: Um...does this work
class TransformAnimations(Transform):
CONFIG = {
"rate_func" : squish_rate_func(smooth)
"rate_func": squish_rate_func(smooth)
}
def __init__(self, start_anim, end_anim, **kwargs):
digest_config(self, kwargs, locals())
if "run_time" in kwargs:
@@ -207,15 +232,16 @@ class TransformAnimations(Transform):
self.run_time = max(start_anim.run_time, end_anim.run_time)
for anim in start_anim, end_anim:
anim.set_run_time(self.run_time)
if start_anim.starting_mobject.get_num_points() != end_anim.starting_mobject.get_num_points():
start_anim.starting_mobject.align_data(end_anim.starting_mobject)
for anim in start_anim, end_anim:
if hasattr(anim, "target_mobject"):
anim.starting_mobject.align_data(anim.target_mobject)
Transform.__init__(self, start_anim.mobject, end_anim.mobject, **kwargs)
#Rewire starting and ending mobjects
Transform.__init__(self, start_anim.mobject,
end_anim.mobject, **kwargs)
# Rewire starting and ending mobjects
start_anim.mobject = self.starting_mobject
end_anim.mobject = self.target_mobject
@@ -223,4 +249,3 @@ class TransformAnimations(Transform):
self.start_anim.update(alpha)
self.end_anim.update(alpha)
Transform.update(self, alpha)

View File

@@ -12,6 +12,7 @@ class UpdateFromFunc(Animation):
to be used when the state of one mobject is dependent
on another simultaneously animated mobject
"""
def __init__(self, mobject, update_function, **kwargs):
digest_config(self, kwargs, locals())
Animation.__init__(self, mobject, **kwargs)
@@ -19,29 +20,27 @@ class UpdateFromFunc(Animation):
def update_mobject(self, alpha):
self.update_function(self.mobject)
class UpdateFromAlphaFunc(UpdateFromFunc):
def update_mobject(self, alpha):
self.update_function(self.mobject, alpha)
class MaintainPositionRelativeTo(Animation):
CONFIG = {
"tracked_critical_point" : ORIGIN
"tracked_critical_point": ORIGIN
}
def __init__(self, mobject, tracked_mobject, **kwargs):
digest_config(self, kwargs, locals())
tcp = self.tracked_critical_point
self.diff = mobject.get_critical_point(tcp) - \
tracked_mobject.get_critical_point(tcp)
tracked_mobject.get_critical_point(tcp)
Animation.__init__(self, mobject, **kwargs)
def update_mobject(self, alpha):
self.mobject.shift(
self.tracked_mobject.get_critical_point(self.tracked_critical_point) - \
self.mobject.get_critical_point(self.tracked_critical_point) + \
self.tracked_mobject.get_critical_point(self.tracked_critical_point) -
self.mobject.get_critical_point(self.tracked_critical_point) +
self.diff
)

View File

@@ -1,12 +1,12 @@
"""
I won't pretend like this is best practice, by in creating animations for a video,
it can be very nice to simply have all of the Mobjects, Animations, Scenes, etc.
it can be very nice to simply have all of the Mobjects, Animations, Scenes, etc.
of manim available without having to worry about what namespace they come from.
Rather than having a large pile of "from <module> import *" at the top of every such
script, the intent of this file is to make it so that one can just include
script, the intent of this file is to make it so that one can just include
"from big_ol_pile_of_manim_imports import *". The effects of adding more modules
or refactoring the library on current or older scene scripts should be entirely
or refactoring the library on current or older scene scripts should be entirely
addressible by changing this file.
Note: One should NOT import from this file for main library code, it is meant only

View File

@@ -1,6 +1,5 @@
import itertools as it
import numpy as np
import os
import aggdraw
import copy
@@ -23,28 +22,29 @@ from utils.iterables import list_difference_update
from utils.iterables import remove_list_redundancies
from utils.simple_functions import fdiv
class Camera(object):
CONFIG = {
"background_image" : None,
"pixel_shape" : (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
"background_image": None,
"pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
# Note: frame_shape will be resized to match pixel_shape
"frame_shape" : (FRAME_HEIGHT, FRAME_WIDTH),
"space_center" : ORIGIN,
"background_color" : BLACK,
#Points in vectorized mobjects with norm greater
#than this value will be rescaled.
"max_allowable_norm" : FRAME_WIDTH,
"image_mode" : "RGBA",
"n_rgb_coords" : 4,
"background_alpha" : 0, #Out of rgb_max_val
"pixel_array_dtype" : 'uint8',
"use_z_coordinate_for_display_order" : False,
"frame_shape": (FRAME_HEIGHT, FRAME_WIDTH),
"space_center": ORIGIN,
"background_color": BLACK,
# Points in vectorized mobjects with norm greater
# than this value will be rescaled.
"max_allowable_norm": FRAME_WIDTH,
"image_mode": "RGBA",
"n_rgb_coords": 4,
"background_alpha": 0, # Out of rgb_max_val
"pixel_array_dtype": 'uint8',
"use_z_coordinate_for_display_order": False,
# z_buff_func is only used if the flag above is set to True.
# round z coordinate to nearest hundredth when comparring
"z_buff_func" : lambda m : np.round(m.get_center()[2], 2),
"z_buff_func": lambda m: np.round(m.get_center()[2], 2),
}
def __init__(self, background = None, **kwargs):
def __init__(self, background=None, **kwargs):
digest_config(self, kwargs, locals())
self.rgb_max_val = np.iinfo(self.pixel_array_dtype).max
self.init_background()
@@ -55,22 +55,22 @@ class Camera(object):
# This is to address a strange bug where deepcopying
# will result in a segfault, which is somehow related
# to the aggdraw library
self.canvas = None
self.canvas = None
return copy.copy(self)
def resize_frame_shape(self, fixed_dimension = 0):
def resize_frame_shape(self, fixed_dimension=0):
"""
Changes frame_shape to match the aspect ratio
Changes frame_shape to match the aspect ratio
of pixel_shape, where fixed_dimension determines
whether frame_shape[0] (height) or frame_shape[1] (width)
remains fixed while the other changes accordingly.
"""
aspect_ratio = float(self.pixel_shape[1])/self.pixel_shape[0]
aspect_ratio = float(self.pixel_shape[1]) / self.pixel_shape[0]
frame_width, frame_height = self.frame_shape
if fixed_dimension == 0:
frame_height = aspect_ratio*frame_width
frame_height = aspect_ratio * frame_width
else:
frame_width = frame_height/aspect_ratio
frame_width = frame_height / aspect_ratio
self.frame_shape = (frame_width, frame_height)
def init_background(self):
@@ -78,53 +78,56 @@ class Camera(object):
path = get_full_raster_image_path(self.background_image)
image = Image.open(path).convert(self.image_mode)
height, width = self.pixel_shape
#TODO, how to gracefully handle backgrounds
#with different sizes?
# TODO, how to gracefully handle backgrounds
# with different sizes?
self.background = np.array(image)[:height, :width]
self.background = self.background.astype(self.pixel_array_dtype)
else:
background_rgba = color_to_int_rgba(
self.background_color, alpha = self.background_alpha
self.background_color, alpha=self.background_alpha
)
self.background = np.zeros(
list(self.pixel_shape)+[self.n_rgb_coords],
dtype = self.pixel_array_dtype
list(self.pixel_shape) + [self.n_rgb_coords],
dtype=self.pixel_array_dtype
)
self.background[:,:] = background_rgba
self.background[:, :] = background_rgba
def get_image(self):
return Image.fromarray(
self.pixel_array,
mode = self.image_mode
mode=self.image_mode
)
def get_pixel_array(self):
return self.pixel_array
def convert_pixel_array(self, pixel_array, convert_from_floats = False):
def convert_pixel_array(self, pixel_array, convert_from_floats=False):
retval = np.array(pixel_array)
if convert_from_floats:
retval = np.apply_along_axis(
lambda f : (f * self.rgb_max_val).astype(self.pixel_array_dtype),
lambda f: (
f * self.rgb_max_val).astype(self.pixel_array_dtype),
2,
retval)
return retval
def set_pixel_array(self, pixel_array, convert_from_floats = False):
converted_array = self.convert_pixel_array(pixel_array, convert_from_floats)
if not (hasattr(self, "pixel_array") and self.pixel_array.shape == converted_array.shape):
def set_pixel_array(self, pixel_array, convert_from_floats=False):
converted_array = self.convert_pixel_array(
pixel_array, convert_from_floats)
if not (hasattr(self, "pixel_array") and self.pixel_array.shape == converted_array.shape):
self.pixel_array = converted_array
else:
#Set in place
self.pixel_array[:,:,:] = converted_array[:,:,:]
# Set in place
self.pixel_array[:, :, :] = converted_array[:, :, :]
def set_background(self, pixel_array, convert_from_floats = False):
self.background = self.convert_pixel_array(pixel_array, convert_from_floats)
def set_background(self, pixel_array, convert_from_floats=False):
self.background = self.convert_pixel_array(
pixel_array, convert_from_floats)
def make_background_from_func(self, coords_to_colors_func):
"""
Sets background by using coords_to_colors_func to determine each pixel's color. Each input
to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
Sets background by using coords_to_colors_func to determine each pixel's color. Each input
to coords_to_colors_func is an (x, y) pair in space (in ordinary space coordinates; not
pixel coordinates), and each output is expected to be an RGBA array of 4 floats.
"""
@@ -137,17 +140,18 @@ class Camera(object):
)
print "Ending set_background; for reference, the current time is ", time.strftime("%H:%M:%S")
return self.convert_pixel_array(new_background, convert_from_floats = True)
return self.convert_pixel_array(new_background, convert_from_floats=True)
def set_background_from_func(self, coords_to_colors_func):
self.set_background(self.make_background_from_func(coords_to_colors_func))
self.set_background(
self.make_background_from_func(coords_to_colors_func))
def reset(self):
self.set_pixel_array(self.background)
####
def extract_mobject_family_members(self, mobjects, only_those_with_points = False):
def extract_mobject_family_members(self, mobjects, only_those_with_points=False):
if only_those_with_points:
method = Mobject.family_members_with_points
else:
@@ -161,13 +165,13 @@ class Camera(object):
))
def get_mobjects_to_display(
self, mobjects,
include_submobjects = True,
excluded_mobjects = None,
):
self, mobjects,
include_submobjects=True,
excluded_mobjects=None,
):
if include_submobjects:
mobjects = self.extract_mobject_family_members(
mobjects, only_those_with_points = True
mobjects, only_those_with_points=True
)
if excluded_mobjects:
all_excluded = self.extract_mobject_family_members(
@@ -192,14 +196,15 @@ class Camera(object):
def capture_mobjects(self, mobjects, **kwargs):
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
# Organize this list into batches of the same type, and
# Organize this list into batches of the same type, and
# apply corresponding function to those batches
type_func_pairs = [
(VMobject, self.display_multiple_vectorized_mobjects),
(PMobject, self.display_multiple_point_cloud_mobjects),
(ImageMobject, self.display_multiple_image_mobjects),
(Mobject, lambda batch : batch), #Do nothing
(PMobject, self.display_multiple_point_cloud_mobjects),
(ImageMobject, self.display_multiple_image_mobjects),
(Mobject, lambda batch: batch), # Do nothing
]
def get_mobject_type(mobject):
for mobject_type, func in type_func_pairs:
if isinstance(mobject, mobject_type):
@@ -209,14 +214,14 @@ class Camera(object):
)
batch_type_pairs = batch_by_property(mobjects, get_mobject_type)
#Display in these batches
# Display in these batches
for batch, batch_type in batch_type_pairs:
#check what the type is, and call the appropriate function
# check what the type is, and call the appropriate function
for mobject_type, func in type_func_pairs:
if batch_type == mobject_type:
func(batch)
## Methods associated with svg rendering
# Methods associated with svg rendering
def get_aggdraw_canvas(self):
if not hasattr(self, "canvas") or not self.canvas:
@@ -224,15 +229,15 @@ class Camera(object):
return self.canvas
def reset_aggdraw_canvas(self):
image = Image.fromarray(self.pixel_array, mode = self.image_mode)
image = Image.fromarray(self.pixel_array, mode=self.image_mode)
self.canvas = aggdraw.Draw(image)
def display_multiple_vectorized_mobjects(self, vmobjects):
if len(vmobjects) == 0:
return
batch_file_pairs = batch_by_property(
vmobjects,
lambda vm : vm.get_background_image_file()
vmobjects,
lambda vm: vm.get_background_image_file()
)
for batch, file_name in batch_file_pairs:
if file_name:
@@ -247,10 +252,10 @@ class Camera(object):
self.display_vectorized(vmobject, canvas)
canvas.flush()
def display_vectorized(self, vmobject, canvas = None):
def display_vectorized(self, vmobject, canvas=None):
if vmobject.is_subpath:
#Subpath vectorized mobjects are taken care
#of by their parent
# Subpath vectorized mobjects are taken care
# of by their parent
return
canvas = canvas or self.get_aggdraw_canvas()
pen, fill = self.get_pen_and_fill(vmobject)
@@ -267,7 +272,7 @@ class Camera(object):
stroke_hex = rgb_to_hex(stroke_rgb)
pen = aggdraw.Pen(stroke_hex, stroke_width)
fill_opacity = int(self.rgb_max_val*vmobject.get_fill_opacity())
fill_opacity = int(self.rgb_max_val * vmobject.get_fill_opacity())
if fill_opacity == 0:
fill = None
else:
@@ -291,27 +296,28 @@ class Camera(object):
def get_pathstring(self, vmobject):
result = ""
for mob in [vmobject]+vmobject.get_subpath_mobjects():
for mob in [vmobject] + vmobject.get_subpath_mobjects():
points = mob.points
# points = self.adjust_out_of_range_points(points)
# points = self.adjust_out_of_range_points(points)
if len(points) == 0:
continue
aligned_points = self.align_points_to_camera(points)
coords = self.points_to_pixel_coords(aligned_points)
coord_strings = coords.flatten().astype(str)
#Start new path string with M
# Start new path string with M
coord_strings[0] = "M" + coord_strings[0]
#The C at the start of every 6th number communicates
#that the following 6 define a cubic Bezier
coord_strings[2::6] = map(lambda s : "C" + str(s), coord_strings[2::6])
#Possibly finish with "Z"
# The C at the start of every 6th number communicates
# that the following 6 define a cubic Bezier
coord_strings[2::6] = map(
lambda s: "C" + str(s), coord_strings[2::6])
# Possibly finish with "Z"
if vmobject.mark_paths_closed:
coord_strings[-1] = coord_strings[-1] + " Z"
result += " ".join(coord_strings)
return result
def get_background_colored_vmobject_displayer(self):
#Quite wordy to type out a bunch
# Quite wordy to type out a bunch
long_name = "background_colored_vmobject_displayer"
if not hasattr(self, long_name):
setattr(self, long_name, BackgroundColoredVMobjectDisplayer(self))
@@ -323,7 +329,7 @@ class Camera(object):
self.overlay_rgba_array(cvmobject_pixel_array)
return self
## Methods for other rendering
# Methods for other rendering
def display_multiple_point_cloud_mobjects(self, pmobjects):
for pmobject in pmobjects:
@@ -343,23 +349,23 @@ class Camera(object):
)
rgba_len = self.pixel_array.shape[2]
rgbas = (self.rgb_max_val*rgbas).astype(self.pixel_array_dtype)
rgbas = (self.rgb_max_val * rgbas).astype(self.pixel_array_dtype)
target_len = len(pixel_coords)
factor = target_len/len(rgbas)
rgbas = np.array([rgbas]*factor).reshape((target_len, rgba_len))
factor = target_len / len(rgbas)
rgbas = np.array([rgbas] * factor).reshape((target_len, rgba_len))
on_screen_indices = self.on_screen_pixels(pixel_coords)
pixel_coords = pixel_coords[on_screen_indices]
on_screen_indices = self.on_screen_pixels(pixel_coords)
pixel_coords = pixel_coords[on_screen_indices]
rgbas = rgbas[on_screen_indices]
ph, pw = self.pixel_shape
flattener = np.array([1, pw], dtype = 'int')
flattener = np.array([1, pw], dtype='int')
flattener = flattener.reshape((2, 1))
indices = np.dot(pixel_coords, flattener)[:,0]
indices = np.dot(pixel_coords, flattener)[:, 0]
indices = indices.astype('int')
new_pa = self.pixel_array.reshape((ph*pw, rgba_len))
new_pa = self.pixel_array.reshape((ph * pw, rgba_len))
new_pa[indices] = rgbas
self.pixel_array = new_pa.reshape((ph, pw, rgba_len))
@@ -375,47 +381,46 @@ class Camera(object):
impa = image_mobject.pixel_array
oh, ow = self.pixel_array.shape[:2] #Outer width and height
ih, iw = impa.shape[:2] #inner with and height
oh, ow = self.pixel_array.shape[:2] # Outer width and height
ih, iw = impa.shape[:2] # inner with and height
rgb_len = self.pixel_array.shape[2]
image = np.zeros((oh, ow, rgb_len), dtype = self.pixel_array_dtype)
image = np.zeros((oh, ow, rgb_len), dtype=self.pixel_array_dtype)
if right_vect[1] == 0 and down_vect[0] == 0:
rv0 = right_vect[0]
dv1 = down_vect[1]
x_indices = np.arange(rv0, dtype = 'int')*iw/rv0
y_indices = np.arange(dv1, dtype = 'int')*ih/dv1
stretched_impa = impa[y_indices][:,x_indices]
x_indices = np.arange(rv0, dtype='int') * iw / rv0
y_indices = np.arange(dv1, dtype='int') * ih / dv1
stretched_impa = impa[y_indices][:, x_indices]
x0, x1 = ul_coords[0], ur_coords[0]
x0, x1 = ul_coords[0], ur_coords[0]
y0, y1 = ul_coords[1], dl_coords[1]
if x0 >= ow or x1 < 0 or y0 >= oh or y1 < 0:
return
siy0 = max(-y0, 0) #stretched_impa y0
siy1 = dv1 - max(y1-oh, 0)
siy0 = max(-y0, 0) # stretched_impa y0
siy1 = dv1 - max(y1 - oh, 0)
six0 = max(-x0, 0)
six1 = rv0 - max(x1-ow, 0)
six1 = rv0 - max(x1 - ow, 0)
x0 = max(x0, 0)
y0 = max(y0, 0)
image[y0:y1, x0:x1] = stretched_impa[siy0:siy1, six0:six1]
else:
# Alternate (slower) tactic if image is tilted
# List of all coordinates of pixels, given as (x, y),
# List of all coordinates of pixels, given as (x, y),
# which matches the return type of points_to_pixel_coords,
# even though np.array indexing naturally happens as (y, x)
all_pixel_coords = np.zeros((oh*ow, 2), dtype = 'int')
a = np.arange(oh*ow, dtype = 'int')
all_pixel_coords[:,0] = a%ow
all_pixel_coords[:,1] = a/ow
all_pixel_coords = np.zeros((oh * ow, 2), dtype='int')
a = np.arange(oh * ow, dtype='int')
all_pixel_coords[:, 0] = a % ow
all_pixel_coords[:, 1] = a / ow
recentered_coords = all_pixel_coords - ul_coords
coord_norms = np.linalg.norm(recentered_coords, axis = 1)
with np.errstate(divide = 'ignore'):
with np.errstate(divide='ignore'):
ix_coords, iy_coords = [
np.divide(
dim*np.dot(recentered_coords, vect),
dim * np.dot(recentered_coords, vect),
np.dot(vect, vect),
)
for vect, dim in (right_vect, iw), (down_vect, ih)
@@ -424,12 +429,12 @@ class Camera(object):
ix_coords >= 0, ix_coords < iw,
iy_coords >= 0, iy_coords < ih,
])
n_to_change = np.sum(to_change)
inner_flat_coords = iw*iy_coords[to_change] + ix_coords[to_change]
flat_impa = impa.reshape((iw*ih, rgb_len))
inner_flat_coords = iw * \
iy_coords[to_change] + ix_coords[to_change]
flat_impa = impa.reshape((iw * ih, rgb_len))
target_rgbas = flat_impa[inner_flat_coords, :]
image = image.reshape((ow*oh, rgb_len))
image = image.reshape((ow * oh, rgb_len))
image[to_change] = target_rgbas
image = image.reshape((oh, ow, rgb_len))
self.overlay_rgba_array(image)
@@ -439,27 +444,27 @@ class Camera(object):
bg = self.pixel_array
# rgba_max_val = self.rgb_max_val
src_rgb, src_a, dst_rgb, dst_a = [
a.astype(np.float32)/self.rgb_max_val
for a in fg[...,:3], fg[...,3], bg[...,:3], bg[...,3]
a.astype(np.float32) / self.rgb_max_val
for a in fg[..., :3], fg[..., 3], bg[..., :3], bg[..., 3]
]
out_a = src_a + dst_a*(1.0-src_a)
out_a = src_a + dst_a * (1.0 - src_a)
# When the output alpha is 0 for full transparency,
# When the output alpha is 0 for full transparency,
# we have a choice over what RGB value to use in our
# output representation. We choose 0 here.
out_rgb = fdiv(
src_rgb*src_a[..., None] + \
dst_rgb*dst_a[..., None]*(1.0-src_a[..., None]),
src_rgb * src_a[..., None] +
dst_rgb * dst_a[..., None] * (1.0 - src_a[..., None]),
out_a[..., None],
zero_over_zero_value = 0
zero_over_zero_value=0
)
self.pixel_array[..., :3] = out_rgb*self.rgb_max_val
self.pixel_array[..., 3] = out_a*self.rgb_max_val
self.pixel_array[..., :3] = out_rgb * self.rgb_max_val
self.pixel_array[..., 3] = out_a * self.rgb_max_val
def align_points_to_camera(self, points):
## This is where projection should live
# This is where projection should live
return points - self.space_center
def adjust_out_of_range_points(self, points):
@@ -467,10 +472,10 @@ class Camera(object):
return points
norms = np.apply_along_axis(np.linalg.norm, 1, points)
violator_indices = norms > self.max_allowable_norm
violators = points[violator_indices,:]
violators = points[violator_indices, :]
violator_norms = norms[violator_indices]
reshaped_norms = np.repeat(
violator_norms.reshape((len(violator_norms), 1)),
violator_norms.reshape((len(violator_norms), 1)),
points.shape[1], 1
)
rescaled = self.max_allowable_norm * violators / reshaped_norms
@@ -481,32 +486,32 @@ class Camera(object):
result = np.zeros((len(points), 2))
ph, pw = self.pixel_shape
sh, sw = self.frame_shape
width_mult = pw/sw
width_add = pw/2
height_mult = ph/sh
height_add = ph/2
#Flip on y-axis as you go
width_mult = pw / sw
width_add = pw / 2
height_mult = ph / sh
height_add = ph / 2
# Flip on y-axis as you go
height_mult *= -1
result[:,0] = points[:,0]*width_mult + width_add
result[:,1] = points[:,1]*height_mult + height_add
result[:, 0] = points[:, 0] * width_mult + width_add
result[:, 1] = points[:, 1] * height_mult + height_add
return result.astype('int')
def on_screen_pixels(self, pixel_coords):
return reduce(op.and_, [
pixel_coords[:,0] >= 0,
pixel_coords[:,0] < self.pixel_shape[1],
pixel_coords[:,1] >= 0,
pixel_coords[:,1] < self.pixel_shape[0],
pixel_coords[:, 0] >= 0,
pixel_coords[:, 0] < self.pixel_shape[1],
pixel_coords[:, 1] >= 0,
pixel_coords[:, 1] < self.pixel_shape[0],
])
def adjusted_thickness(self, thickness):
big_shape = PRODUCTION_QUALITY_CAMERA_CONFIG["pixel_shape"]
factor = sum(big_shape)/sum(self.pixel_shape)
return 1 + (thickness-1)/factor
factor = sum(big_shape) / sum(self.pixel_shape)
return 1 + (thickness - 1) / factor
def get_thickening_nudges(self, thickness):
_range = range(-thickness/2+1, thickness/2+1)
_range = range(-thickness / 2 + 1, thickness / 2 + 1)
return np.array(list(it.product(_range, _range)))
def thickened_coordinates(self, pixel_coords, thickness):
@@ -516,7 +521,7 @@ class Camera(object):
for nudge in nudges
])
size = pixel_coords.size
return pixel_coords.reshape((size/2, 2))
return pixel_coords.reshape((size / 2, 2))
def get_coords_of_all_pixels(self):
# These are in x, y order, to help me keep things straight
@@ -525,23 +530,26 @@ class Camera(object):
# These are addressed in the same y, x order as in pixel_array, but the values in them
# are listed in x, y order
uncentered_pixel_coords = np.indices(self.pixel_shape)[::-1].transpose(1, 2, 0)
uncentered_pixel_coords = np.indices(self.pixel_shape)[
::-1].transpose(1, 2, 0)
uncentered_space_coords = fdiv(
uncentered_pixel_coords * full_space_dims,
uncentered_pixel_coords * full_space_dims,
full_pixel_dims)
# Could structure above line's computation slightly differently, but figured (without much
# thought) multiplying by frame_shape first, THEN dividing by pixel_shape, is probably
# better than the other order, for avoiding underflow quantization in the division (whereas
# Could structure above line's computation slightly differently, but figured (without much
# thought) multiplying by frame_shape first, THEN dividing by pixel_shape, is probably
# better than the other order, for avoiding underflow quantization in the division (whereas
# overflow is unlikely to be a problem)
centered_space_coords = (uncentered_space_coords - fdiv(full_space_dims, 2))
centered_space_coords = (
uncentered_space_coords - fdiv(full_space_dims, 2))
# Have to also flip the y coordinates to account for pixel array being listed in
# Have to also flip the y coordinates to account for pixel array being listed in
# top-to-bottom order, opposite of screen coordinate convention
centered_space_coords = centered_space_coords * (1, -1)
return centered_space_coords
class BackgroundColoredVMobjectDisplayer(object):
def __init__(self, camera):
self.camera = camera
@@ -551,20 +559,20 @@ class BackgroundColoredVMobjectDisplayer(object):
def init_canvas(self):
self.pixel_array = np.zeros(
self.camera.pixel_array.shape,
dtype = self.camera.pixel_array_dtype,
dtype=self.camera.pixel_array_dtype,
)
self.reset_canvas()
def reset_canvas(self):
image = Image.fromarray(self.pixel_array, mode = self.camera.image_mode)
image = Image.fromarray(self.pixel_array, mode=self.camera.image_mode)
self.canvas = aggdraw.Draw(image)
def resize_background_array(
self, background_array,
new_width, new_height,
mode = "RGBA"
):
image = Image.fromarray(background_array, mode = mode)
self, background_array,
new_width, new_height,
mode="RGBA"
):
image = Image.fromarray(background_array, mode=mode)
resized_image = image.resize((new_width, new_height))
return np.array(resized_image)
@@ -582,14 +590,15 @@ class BackgroundColoredVMobjectDisplayer(object):
camera = self.camera
if not np.all(camera.pixel_array.shape == array.shape):
array = self.resize_background_array_to_match(array, camera.pixel_array)
array = self.resize_background_array_to_match(
array, camera.pixel_array)
self.file_name_to_pixel_array_map[file_name] = array
return array
def display(self, *cvmobjects):
batch_image_file_pairs = batch_by_property(
cvmobjects, lambda cv : cv.get_background_image_file()
cvmobjects, lambda cv: cv.get_background_image_file()
)
curr_array = None
for batch, image_file in batch_image_file_pairs:
@@ -598,14 +607,13 @@ class BackgroundColoredVMobjectDisplayer(object):
self.camera.display_vectorized(cvmobject, self.canvas)
self.canvas.flush()
new_array = np.array(
(background_array*self.pixel_array.astype('float')/255),
dtype = self.camera.pixel_array_dtype
(background_array * self.pixel_array.astype('float') / 255),
dtype=self.camera.pixel_array_dtype
)
if curr_array is None:
curr_array = new_array
else:
curr_array = np.maximum(curr_array, new_array)
self.pixel_array[:,:] = 0
self.pixel_array[:, :] = 0
self.reset_canvas()
return curr_array

View File

@@ -1,21 +1,26 @@
from __future__ import absolute_import
import numpy as np
from camera.camera import Camera
from mobject.types.vectorized_mobject import VMobject
from utils.config_ops import DictAsObject
from utils.config_ops import digest_config
# TODO: Add an attribute to mobjects under which they can specify that they should just
# TODO: Add an attribute to mobjects under which they can specify that they should just
# map their centers but remain otherwise undistorted (useful for labels, etc.)
class MappingCamera(Camera):
CONFIG = {
"mapping_func" : lambda p : p,
"min_anchor_points" : 50,
"allow_object_intrusion" : False
"mapping_func": lambda p: p,
"min_anchor_points": 50,
"allow_object_intrusion": False
}
def points_to_pixel_coords(self, points):
return Camera.points_to_pixel_coords(self, np.apply_along_axis(self.mapping_func, 1, points))
def capture_mobjects(self, mobjects, **kwargs):
mobjects = self.get_mobjects_to_display(mobjects, **kwargs)
if self.allow_object_intrusion:
@@ -24,30 +29,32 @@ class MappingCamera(Camera):
mobject_copies = [mobject.copy() for mobject in mobjects]
for mobject in mobject_copies:
if isinstance(mobject, VMobject) and \
0 < mobject.get_num_anchor_points() < self.min_anchor_points:
0 < mobject.get_num_anchor_points() < self.min_anchor_points:
mobject.insert_n_anchor_points(self.min_anchor_points)
Camera.capture_mobjects(
self, mobject_copies,
include_submobjects = False,
excluded_mobjects = None,
self, mobject_copies,
include_submobjects=False,
excluded_mobjects=None,
)
# Note: This allows layering of multiple cameras onto the same portion of the pixel array,
# the later cameras overwriting the former
#
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
# TODO: Add optional separator borders between cameras (or perhaps peel this off into a
# CameraPlusOverlay class)
class MultiCamera(Camera):
def __init__(self, *cameras_with_start_positions, **kwargs):
self.shifted_cameras = [
DictAsObject(
{
"camera" : camera_with_start_positions[0],
"start_x" : camera_with_start_positions[1][1],
"start_y" : camera_with_start_positions[1][0],
"end_x" : camera_with_start_positions[1][1] + camera_with_start_positions[0].pixel_shape[1],
"end_y" : camera_with_start_positions[1][0] + camera_with_start_positions[0].pixel_shape[0],
})
{
"camera": camera_with_start_positions[0],
"start_x": camera_with_start_positions[1][1],
"start_y": camera_with_start_positions[1][0],
"end_x": camera_with_start_positions[1][1] + camera_with_start_positions[0].pixel_shape[1],
"end_y": camera_with_start_positions[1][0] + camera_with_start_positions[0].pixel_shape[0],
})
for camera_with_start_positions in cameras_with_start_positions
]
Camera.__init__(self, **kwargs)
@@ -55,17 +62,17 @@ class MultiCamera(Camera):
def capture_mobjects(self, mobjects, **kwargs):
for shifted_camera in self.shifted_cameras:
shifted_camera.camera.capture_mobjects(mobjects, **kwargs)
self.pixel_array[
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_x:shifted_camera.end_x] \
= shifted_camera.camera.pixel_array
= shifted_camera.camera.pixel_array
def set_background(self, pixel_array, **kwargs):
for shifted_camera in self.shifted_cameras:
shifted_camera.camera.set_background(
pixel_array[
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_x:shifted_camera.end_x],
**kwargs
)
@@ -75,7 +82,7 @@ class MultiCamera(Camera):
for shifted_camera in self.shifted_cameras:
shifted_camera.camera.set_pixel_array(
pixel_array[
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_y:shifted_camera.end_y,
shifted_camera.start_x:shifted_camera.end_x],
**kwargs
)
@@ -87,18 +94,21 @@ class MultiCamera(Camera):
# A MultiCamera which, when called with two full-size cameras, initializes itself
# as a splitscreen, also taking care to resize each individual camera within it
class SplitScreenCamera(MultiCamera):
def __init__(self, left_camera, right_camera, **kwargs):
digest_config(self, kwargs)
self.left_camera = left_camera
self.right_camera = right_camera
half_width = self.pixel_shape[1] / 2
for camera in [self.left_camera, self.right_camera]:
camera.pixel_shape = (self.pixel_shape[0], half_width) # TODO: Round up on one if width is odd
# TODO: Round up on one if width is odd
camera.pixel_shape = (self.pixel_shape[0], half_width)
camera.init_background()
camera.resize_frame_shape()
camera.reset()
MultiCamera.__init__(self, (left_camera, (0, 0)), (right_camera, (0, half_width)))
MultiCamera.__init__(self, (left_camera, (0, 0)),
(right_camera, (0, half_width)))

View File

@@ -2,17 +2,19 @@ from __future__ import absolute_import
from camera.camera import Camera
class MovingCamera(Camera):
"""
Stays in line with the height, width and position
of a given mobject
"""
CONFIG = {
"aligned_dimension" : "width" #or height
"aligned_dimension": "width" # or height
}
def __init__(self, frame, **kwargs):
"""
frame is a Mobject, (should be a rectangle) determining
frame is a Mobject, (should be a rectangle) determining
which region of space the camera displys
"""
self.frame = frame

View File

@@ -12,39 +12,43 @@ from utils.bezier import interpolate
from utils.space_ops import rotation_about_z
from utils.space_ops import rotation_matrix
class CameraWithPerspective(Camera):
CONFIG = {
"camera_distance" : 20,
"camera_distance": 20,
}
def points_to_pixel_coords(self, points):
distance_ratios = np.divide(
self.camera_distance,
self.camera_distance - points[:,2]
self.camera_distance - points[:, 2]
)
scale_factors = interpolate(0, 1, distance_ratios)
adjusted_points = np.array(points)
for i in 0, 1:
adjusted_points[:,i] *= scale_factors
adjusted_points[:, i] *= scale_factors
return Camera.points_to_pixel_coords(self, adjusted_points)
class ThreeDCamera(CameraWithPerspective):
CONFIG = {
"sun_vect" : 5*UP+LEFT,
"shading_factor" : 0.2,
"distance" : 5.,
"default_distance" : 5.,
"phi" : 0, #Angle off z axis
"theta" : -TAU/4, #Rotation about z axis
"sun_vect": 5 * UP + LEFT,
"shading_factor": 0.2,
"distance": 5.,
"default_distance": 5.,
"phi": 0, # Angle off z axis
"theta": -TAU / 4, # Rotation about z axis
}
def __init__(self, *args, **kwargs):
Camera.__init__(self, *args, **kwargs)
self.unit_sun_vect = self.sun_vect/np.linalg.norm(self.sun_vect)
## rotation_mobject lives in the phi-theta-distance space
## TODO, use ValueTracker for this instead
self.unit_sun_vect = self.sun_vect / np.linalg.norm(self.sun_vect)
# rotation_mobject lives in the phi-theta-distance space
# TODO, use ValueTracker for this instead
self.rotation_mobject = VectorizedPoint()
## moving_center lives in the x-y-z space
## It representes the center of rotation
# moving_center lives in the x-y-z space
# It representes the center of rotation
self.moving_center = VectorizedPoint(self.space_center)
self.set_position(self.phi, self.theta, self.distance)
@@ -63,30 +67,31 @@ class ThreeDCamera(CameraWithPerspective):
def get_shaded_rgb(self, rgb, normal_vect):
brightness = np.dot(normal_vect, self.unit_sun_vect)**2
if brightness > 0:
alpha = self.shading_factor*brightness
alpha = self.shading_factor * brightness
return interpolate(rgb, np.ones(3), alpha)
else:
alpha = -self.shading_factor*brightness
alpha = -self.shading_factor * brightness
return interpolate(rgb, np.zeros(3), alpha)
def get_unit_normal_vect(self, vmobject):
anchors = vmobject.get_anchors()
if len(anchors) < 3:
return OUT
normal = np.cross(anchors[1]-anchors[0], anchors[2]-anchors[1])
normal = np.cross(anchors[1] - anchors[0], anchors[2] - anchors[1])
if normal[2] < 0:
normal = -normal
length = np.linalg.norm(normal)
if length == 0:
return OUT
return normal/length
return normal / length
def display_multiple_vectorized_mobjects(self, vmobjects):
camera_point = self.spherical_coords_to_point(
*self.get_spherical_coords()
)
def z_cmp(*vmobs):
# Compare to three dimensional mobjects based on
# Compare to three dimensional mobjects based on
# how close they are to the camera
# return cmp(*[
# -np.linalg.norm(vm.get_center()-camera_point)
@@ -103,22 +108,26 @@ class ThreeDCamera(CameraWithPerspective):
else:
return 0
Camera.display_multiple_vectorized_mobjects(
self, sorted(vmobjects, cmp = z_cmp)
self, sorted(vmobjects, cmp=z_cmp)
)
def get_spherical_coords(self, phi = None, theta = None, distance = None):
def get_spherical_coords(self, phi=None, theta=None, distance=None):
curr_phi, curr_theta, curr_d = self.rotation_mobject.points[0]
if phi is None: phi = curr_phi
if theta is None: theta = curr_theta
if distance is None: distance = curr_d
if phi is None:
phi = curr_phi
if theta is None:
theta = curr_theta
if distance is None:
distance = curr_d
return np.array([phi, theta, distance])
def get_cartesian_coords(self, phi = None, theta = None, distance = None):
spherical_coords_array = self.get_spherical_coords(phi,theta,distance)
def get_cartesian_coords(self, phi=None, theta=None, distance=None):
spherical_coords_array = self.get_spherical_coords(
phi, theta, distance)
phi2 = spherical_coords_array[0]
theta2 = spherical_coords_array[1]
d2 = spherical_coords_array[2]
return self.spherical_coords_to_point(phi2,theta2,d2)
return self.spherical_coords_to_point(phi2, theta2, d2)
def get_phi(self):
return self.get_spherical_coords()[0]
@@ -130,13 +139,13 @@ class ThreeDCamera(CameraWithPerspective):
return self.get_spherical_coords()[2]
def spherical_coords_to_point(self, phi, theta, distance):
return distance*np.array([
np.sin(phi)*np.cos(theta),
np.sin(phi)*np.sin(theta),
return distance * np.array([
np.sin(phi) * np.cos(theta),
np.sin(phi) * np.sin(theta),
np.cos(phi)
])
def get_center_of_rotation(self, x = None, y = None, z = None):
def get_center_of_rotation(self, x=None, y=None, z=None):
curr_x, curr_y, curr_z = self.moving_center.points[0]
if x is None:
x = curr_x
@@ -146,19 +155,20 @@ class ThreeDCamera(CameraWithPerspective):
z = curr_z
return np.array([x, y, z])
def set_position(self, phi = None, theta = None, distance = None,
center_x = None, center_y = None, center_z = None):
def set_position(self, phi=None, theta=None, distance=None,
center_x=None, center_y=None, center_z=None):
point = self.get_spherical_coords(phi, theta, distance)
self.rotation_mobject.move_to(point)
self.phi, self.theta, self.distance = point
center_of_rotation = self.get_center_of_rotation(center_x, center_y, center_z)
center_of_rotation = self.get_center_of_rotation(
center_x, center_y, center_z)
self.moving_center.move_to(center_of_rotation)
self.space_center = self.moving_center.points[0]
def get_view_transformation_matrix(self):
return (self.default_distance / self.get_distance()) * np.dot(
rotation_matrix(self.get_phi(), LEFT),
rotation_about_z(-self.get_theta() - np.pi/2),
rotation_about_z(-self.get_theta() - np.pi / 2),
)
def points_to_pixel_coords(self, points):

View File

@@ -3,31 +3,34 @@ import numpy as np
# Things anyone wishing to use this repository for their
# own use will want to change this
MEDIA_DIR = os.path.join(os.path.expanduser('~'), "Dropbox (3Blue1Brown)/3Blue1Brown Team Folder")
MEDIA_DIR = os.path.join(
os.path.expanduser('~'),
"Dropbox (3Blue1Brown)/3Blue1Brown Team Folder"
)
#
DEFAULT_PIXEL_HEIGHT = 1080
DEFAULT_PIXEL_WIDTH = 1920
DEFAULT_PIXEL_WIDTH = 1920
LOW_QUALITY_FRAME_DURATION = 1./15
MEDIUM_QUALITY_FRAME_DURATION = 1./30
PRODUCTION_QUALITY_FRAME_DURATION = 1./60
LOW_QUALITY_FRAME_DURATION = 1. / 15
MEDIUM_QUALITY_FRAME_DURATION = 1. / 30
PRODUCTION_QUALITY_FRAME_DURATION = 1. / 60
#There might be other configuration than pixel_shape later...
# There might be other configuration than pixel_shape later...
PRODUCTION_QUALITY_CAMERA_CONFIG = {
"pixel_shape" : (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
"pixel_shape": (DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH),
}
MEDIUM_QUALITY_CAMERA_CONFIG = {
"pixel_shape" : (720, 1280),
"pixel_shape": (720, 1280),
}
LOW_QUALITY_CAMERA_CONFIG = {
"pixel_shape" : (480, 854),
"pixel_shape": (480, 854),
}
DEFAULT_POINT_DENSITY_2D = 25
DEFAULT_POINT_DENSITY_2D = 25
DEFAULT_POINT_DENSITY_1D = 250
DEFAULT_POINT_THICKNESS = 4
@@ -35,8 +38,8 @@ DEFAULT_POINT_THICKNESS = 4
FRAME_HEIGHT = 8.0
FRAME_WIDTH = FRAME_HEIGHT * DEFAULT_PIXEL_WIDTH / DEFAULT_PIXEL_HEIGHT
FRAME_Y_RADIUS = FRAME_HEIGHT/2
FRAME_X_RADIUS = FRAME_WIDTH/2
FRAME_Y_RADIUS = FRAME_HEIGHT / 2
FRAME_X_RADIUS = FRAME_WIDTH / 2
SMALL_BUFF = 0.1
MED_SMALL_BUFF = 0.25
@@ -47,49 +50,49 @@ DEFAULT_MOBJECT_TO_EDGE_BUFFER = MED_LARGE_BUFF
DEFAULT_MOBJECT_TO_MOBJECT_BUFFER = MED_SMALL_BUFF
#All in seconds
# All in seconds
DEFAULT_ANIMATION_RUN_TIME = 1.0
DEFAULT_POINTWISE_FUNCTION_RUN_TIME = 3.0
DEFAULT_WAIT_TIME = 1.0
ORIGIN = np.array(( 0., 0., 0.))
UP = np.array(( 0., 1., 0.))
DOWN = np.array(( 0.,-1., 0.))
RIGHT = np.array(( 1., 0., 0.))
LEFT = np.array((-1., 0., 0.))
IN = np.array(( 0., 0.,-1.))
OUT = np.array(( 0., 0., 1.))
X_AXIS = np.array(( 1., 0., 0.))
Y_AXIS = np.array(( 0., 1., 0.))
Z_AXIS = np.array(( 0., 0., 1.))
ORIGIN = np.array((0., 0., 0.))
UP = np.array((0., 1., 0.))
DOWN = np.array((0., -1., 0.))
RIGHT = np.array((1., 0., 0.))
LEFT = np.array((-1., 0., 0.))
IN = np.array((0., 0., -1.))
OUT = np.array((0., 0., 1.))
X_AXIS = np.array((1., 0., 0.))
Y_AXIS = np.array((0., 1., 0.))
Z_AXIS = np.array((0., 0., 1.))
TOP = FRAME_Y_RADIUS*UP
BOTTOM = FRAME_Y_RADIUS*DOWN
LEFT_SIDE = FRAME_X_RADIUS*LEFT
RIGHT_SIDE = FRAME_X_RADIUS*RIGHT
TOP = FRAME_Y_RADIUS * UP
BOTTOM = FRAME_Y_RADIUS * DOWN
LEFT_SIDE = FRAME_X_RADIUS * LEFT
RIGHT_SIDE = FRAME_X_RADIUS * RIGHT
TAU = 2*np.pi
DEGREES = TAU/360
TAU = 2 * np.pi
DEGREES = TAU / 360
ANIMATIONS_DIR = os.path.join(MEDIA_DIR, "animations")
RASTER_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "raster_images")
SVG_IMAGE_DIR = os.path.join(MEDIA_DIR, "designs", "svg_images")
#TODO, staged scenes should really go into a subdirectory of a given scenes directory
STAGED_SCENES_DIR = os.path.join(ANIMATIONS_DIR, "staged_scenes")
# TODO, staged scenes should really go into a subdirectory of a given scenes directory
STAGED_SCENES_DIR = os.path.join(ANIMATIONS_DIR, "staged_scenes")
###
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(THIS_DIR, "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = TEX_DIR #TODO, What is this doing?
#These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
THIS_DIR = os.path.dirname(os.path.realpath(__file__))
FILE_DIR = os.path.join(THIS_DIR, "files")
TEX_DIR = os.path.join(FILE_DIR, "Tex")
TEX_IMAGE_DIR = TEX_DIR # TODO, What is this doing?
# These two may be depricated now.
MOBJECT_DIR = os.path.join(FILE_DIR, "mobjects")
IMAGE_MOBJECT_DIR = os.path.join(MOBJECT_DIR, "image")
if not os.path.exists(MEDIA_DIR):
raise Exception("""
Redefine MEDIA_DIR in constants.py to point to
a valid directory where movies and images will
Redefine MEDIA_DIR in constants.py to point to
a valid directory where movies and images will
be written
""")
for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, ANIMATIONS_DIR, TEX_DIR,
@@ -99,82 +102,72 @@ for folder in [FILE_DIR, RASTER_IMAGE_DIR, SVG_IMAGE_DIR, ANIMATIONS_DIR, TEX_DI
os.makedirs(folder)
TEX_TEXT_TO_REPLACE = "YourTextHere"
TEMPLATE_TEX_FILE = os.path.join(THIS_DIR, "template.tex")
TEMPLATE_TEX_FILE = os.path.join(THIS_DIR, "template.tex")
TEMPLATE_TEXT_FILE = os.path.join(THIS_DIR, "text_template.tex")
FFMPEG_BIN = "ffmpeg"
### Colors ###
# Colors
COLOR_MAP = {
"DARK_BLUE" : "#236B8E",
"DARK_BROWN" : "#8B4513",
"LIGHT_BROWN" : "#CD853F",
"BLUE_E" : "#1C758A",
"BLUE_D" : "#29ABCA",
"BLUE_C" : "#58C4DD",
"BLUE_B" : "#9CDCEB",
"BLUE_A" : "#C7E9F1",
"TEAL_E" : "#49A88F",
"TEAL_D" : "#55C1A7",
"TEAL_C" : "#5CD0B3",
"TEAL_B" : "#76DDC0",
"TEAL_A" : "#ACEAD7",
"GREEN_E" : "#699C52",
"GREEN_D" : "#77B05D",
"GREEN_C" : "#83C167",
"GREEN_B" : "#A6CF8C",
"GREEN_A" : "#C9E2AE",
"YELLOW_E" : "#E8C11C",
"YELLOW_D" : "#F4D345",
"YELLOW_C" : "#FFFF00",
"YELLOW_B" : "#FFEA94",
"YELLOW_A" : "#FFF1B6",
"GOLD_E" : "#C78D46",
"GOLD_D" : "#E1A158",
"GOLD_C" : "#F0AC5F",
"GOLD_B" : "#F9B775",
"GOLD_A" : "#F7C797",
"RED_E" : "#CF5044",
"RED_D" : "#E65A4C",
"RED_C" : "#FC6255",
"RED_B" : "#FF8080",
"RED_A" : "#F7A1A3",
"MAROON_E" : "#94424F",
"MAROON_D" : "#A24D61",
"MAROON_C" : "#C55F73",
"MAROON_B" : "#EC92AB",
"MAROON_A" : "#ECABC1",
"PURPLE_E" : "#644172",
"PURPLE_D" : "#715582",
"PURPLE_C" : "#9A72AC",
"PURPLE_B" : "#B189C6",
"PURPLE_A" : "#CAA3E8",
"WHITE" : "#FFFFFF",
"BLACK" : "#000000",
"LIGHT_GRAY" : "#BBBBBB",
"LIGHT_GREY" : "#BBBBBB",
"GRAY" : "#888888",
"GREY" : "#888888",
"DARK_GREY" : "#444444",
"DARK_GRAY" : "#444444",
"GREY_BROWN" : "#736357",
"PINK" : "#D147BD",
"DARK_BLUE": "#236B8E",
"DARK_BROWN": "#8B4513",
"LIGHT_BROWN": "#CD853F",
"BLUE_E": "#1C758A",
"BLUE_D": "#29ABCA",
"BLUE_C": "#58C4DD",
"BLUE_B": "#9CDCEB",
"BLUE_A": "#C7E9F1",
"TEAL_E": "#49A88F",
"TEAL_D": "#55C1A7",
"TEAL_C": "#5CD0B3",
"TEAL_B": "#76DDC0",
"TEAL_A": "#ACEAD7",
"GREEN_E": "#699C52",
"GREEN_D": "#77B05D",
"GREEN_C": "#83C167",
"GREEN_B": "#A6CF8C",
"GREEN_A": "#C9E2AE",
"YELLOW_E": "#E8C11C",
"YELLOW_D": "#F4D345",
"YELLOW_C": "#FFFF00",
"YELLOW_B": "#FFEA94",
"YELLOW_A": "#FFF1B6",
"GOLD_E": "#C78D46",
"GOLD_D": "#E1A158",
"GOLD_C": "#F0AC5F",
"GOLD_B": "#F9B775",
"GOLD_A": "#F7C797",
"RED_E": "#CF5044",
"RED_D": "#E65A4C",
"RED_C": "#FC6255",
"RED_B": "#FF8080",
"RED_A": "#F7A1A3",
"MAROON_E": "#94424F",
"MAROON_D": "#A24D61",
"MAROON_C": "#C55F73",
"MAROON_B": "#EC92AB",
"MAROON_A": "#ECABC1",
"PURPLE_E": "#644172",
"PURPLE_D": "#715582",
"PURPLE_C": "#9A72AC",
"PURPLE_B": "#B189C6",
"PURPLE_A": "#CAA3E8",
"WHITE": "#FFFFFF",
"BLACK": "#000000",
"LIGHT_GRAY": "#BBBBBB",
"LIGHT_GREY": "#BBBBBB",
"GRAY": "#888888",
"GREY": "#888888",
"DARK_GREY": "#444444",
"DARK_GRAY": "#444444",
"GREY_BROWN": "#736357",
"PINK": "#D147BD",
"GREEN_SCREEN": "#00FF00",
"ORANGE" : "#FF862F",
"ORANGE": "#FF862F",
}
PALETTE = COLOR_MAP.values()
locals().update(COLOR_MAP)
for name in filter(lambda s : s.endswith("_C"), COLOR_MAP.keys()):
for name in filter(lambda s: s.endswith("_C"), COLOR_MAP.keys()):
locals()[name.replace("_C", "")] = locals()[name]

View File

@@ -1,22 +1,24 @@
from constants import *
from utils.config_ops import digest_config
# Currently, this is only used by both Scene and MOBject.
# Still, we abstract its functionality here, albeit purely nominally.
# All actual implementation has to be handled by derived classes for now.
#
# Note that although the prototypical instances add and remove MObjects,
# there is also the possibility to add ContinualAnimations to Scenes. Thus,
# in the Container class in general, we do not make any presumptions about
# Note that although the prototypical instances add and remove MObjects,
# there is also the possibility to add ContinualAnimations to Scenes. Thus,
# in the Container class in general, we do not make any presumptions about
# what types of objects may be added; this is again dependent on the specific
# derived instance.
class Container(object):
def __init__(self, *submobjects, **kwargs):
digest_config(self, kwargs)
def add(self, *items):
raise Exception("Container.add is not implemented; it is up to derived classes to implement")
raise Exception(
"Container.add is not implemented; it is up to derived classes to implement")
def remove(self, *items):
raise Exception("Container.remove is not implemented; it is up to derived classes to implement")
raise Exception(
"Container.remove is not implemented; it is up to derived classes to implement")

View File

@@ -6,12 +6,14 @@ from mobject.mobject import Mobject
from utils.config_ops import digest_config
from utils.config_ops import instantiate
class ContinualAnimation(object):
CONFIG = {
"start_up_time" : 1,
"wind_down_time" : 1,
"end_time" : np.inf,
"start_up_time": 1,
"wind_down_time": 1,
"end_time": np.inf,
}
def __init__(self, mobject, **kwargs):
mobject = instantiate(mobject)
assert(isinstance(mobject, Mobject))
@@ -22,42 +24,45 @@ class ContinualAnimation(object):
self.update(0)
def setup(self):
#To implement in subclass
# To implement in subclass
pass
def begin_wind_down(self, wind_down_time = None):
def begin_wind_down(self, wind_down_time=None):
if wind_down_time is not None:
self.wind_down_time = wind_down_time
self.end_time = self.external_time + self.wind_down_time
def update(self, dt):
#TODO, currenty time moves slower for a
#continual animation during its start up
#to help smooth things out. Does this have
#unwanted consequences?
# TODO, currenty time moves slower for a
# continual animation during its start up
# to help smooth things out. Does this have
# unwanted consequences?
self.external_time += dt
if self.external_time < self.start_up_time:
dt *= float(self.external_time)/self.start_up_time
dt *= float(self.external_time) / self.start_up_time
elif self.external_time > self.end_time - self.wind_down_time:
dt *= np.clip(
float(self.end_time - self.external_time)/self.wind_down_time,
float(self.end_time - self.external_time) /
self.wind_down_time,
0, 1
)
self.internal_time += dt
self.update_mobject(dt)
def update_mobject(self, dt):
#To implement in subclass
# To implement in subclass
pass
def copy(self):
return copy.deepcopy(self)
class ContinualAnimationGroup(ContinualAnimation):
CONFIG = {
"start_up_time" : 0,
"wind_down_time" : 0,
"start_up_time": 0,
"wind_down_time": 0,
}
def __init__(self, *continual_animations, **kwargs):
digest_config(self, kwargs, locals())
self.group = Group(*[ca.mobject for ca in continual_animations])
@@ -67,12 +72,13 @@ class ContinualAnimationGroup(ContinualAnimation):
for continual_animation in self.continual_animations:
continual_animation.update(dt)
class ContinualRotation(ContinualAnimation):
CONFIG = {
"axis" : OUT,
"rate" : np.pi/12, #Radians per second
"in_place" : True,
"about_point" : None,
"axis": OUT,
"rate": np.pi / 12, # Radians per second
"in_place": True,
"about_point": None,
}
def update_mobject(self, dt):
@@ -83,23 +89,16 @@ class ContinualRotation(ContinualAnimation):
else:
about_point = ORIGIN
self.mobject.rotate(
dt*self.rate, axis = self.axis,
about_point = about_point
dt * self.rate, axis=self.axis,
about_point=about_point
)
class ContinualMovement(ContinualAnimation):
CONFIG = {
"direction" : RIGHT,
"rate" : 0.05, #Units per second
"direction": RIGHT,
"rate": 0.05, # Units per second
}
def update_mobject(self, dt):
self.mobject.shift(dt*self.rate*self.direction)
self.mobject.shift(dt * self.rate * self.direction)

View File

@@ -2,20 +2,23 @@ from __future__ import absolute_import
from continual_animation.continual_animation import ContinualAnimation
class NormalAnimationAsContinualAnimation(ContinualAnimation):
CONFIG = {
"start_up_time" : 0,
"wind_down_time" : 0,
"start_up_time": 0,
"wind_down_time": 0,
}
def __init__(self, animation, **kwargs):
self.animation = animation
ContinualAnimation.__init__(self, animation.mobject, **kwargs)
def update_mobject(self, dt):
self.animation.update(
min(float(self.internal_time)/self.animation.run_time, 1)
min(float(self.internal_time) / self.animation.run_time, 1)
)
class CycleAnimation(ContinualAnimation):
def __init__(self, animation, **kwargs):
self.animation = animation
@@ -23,6 +26,5 @@ class CycleAnimation(ContinualAnimation):
def update_mobject(self, dt):
mod_value = self.internal_time % self.animation.run_time
alpha = mod_value/float(self.animation.run_time)
alpha = mod_value / float(self.animation.run_time)
self.animation.update(alpha)

View File

@@ -3,6 +3,7 @@ from __future__ import absolute_import
from continual_animation.from_animation import NormalAnimationAsContinualAnimation
from animation.numbers import ChangingDecimal
class ContinualChangingDecimal(NormalAnimationAsContinualAnimation):
def __init__(self, *args, **kwargs):
NormalAnimationAsContinualAnimation.__init__(

View File

@@ -6,8 +6,9 @@ from animation.update import MaintainPositionRelativeTo
class ContinualUpdateFromFunc(ContinualAnimation):
CONFIG = {
"function_depends_on_dt" : False
"function_depends_on_dt": False
}
def __init__(self, mobject, func, **kwargs):
self.func = func
ContinualAnimation.__init__(self, mobject, **kwargs)
@@ -18,16 +19,19 @@ class ContinualUpdateFromFunc(ContinualAnimation):
else:
self.func(self.mobject)
class ContinualUpdateFromTimeFunc(ContinualUpdateFromFunc):
CONFIG = {
"function_depends_on_dt" : True
"function_depends_on_dt": True
}
class ContinualMaintainPositionRelativeTo(ContinualAnimation):
# TODO: Possibly reimplement using CycleAnimation?
def __init__(self, mobject, tracked_mobject, **kwargs):
self.anim = MaintainPositionRelativeTo(mobject, tracked_mobject, **kwargs)
self.anim = MaintainPositionRelativeTo(
mobject, tracked_mobject, **kwargs)
ContinualAnimation.__init__(self, mobject, **kwargs)
def update_mobject(self, dt):
self.anim.update(0) # 0 is arbitrary
self.anim.update(0) # 0 is arbitrary

View File

@@ -4,8 +4,8 @@ from big_ol_pile_of_manim_imports import *
# To watch one of these scenes, run the following:
# python extract_scene.py file_name <SceneName> -p
#
# Use the flat -l for a faster rendering at a lower
#
# Use the flat -l for a faster rendering at a lower
# quality, use -s to skip to the end and just show
# the final frame, and use -n <number> to skip ahead
# to the n'th animation of a scene.
@@ -16,17 +16,18 @@ class SquareToCircle(Scene):
circle = Circle()
square = Square()
square.flip(RIGHT)
square.rotate(-3*TAU/8)
square.rotate(-3 * TAU / 8)
self.play(ShowCreation(square))
self.play(Transform(square, circle))
self.play(FadeOut(square))
class WarpSquare(Scene):
def construct(self):
square = Square()
self.play(ApplyPointwiseFunction(
lambda (x, y, z) : complex_to_R3(np.exp(complex(x, y))),
lambda (x, y, z): complex_to_R3(np.exp(complex(x, y))),
square
))
self.wait()
@@ -42,8 +43,8 @@ class Rotation3d(ThreeDScene):
# STEP 1
# Build two cube in the 3D scene, one for around the origin,
# the other shifted along the vector RIGHT + UP + OUT
cube_origin = Cube(fill_opacity = 0.8, stroke_width = 1.,
side_length = 1., fill_color = WHITE)
cube_origin = Cube(fill_opacity=0.8, stroke_width=1.,
side_length=1., fill_color=WHITE)
# RIGHT side: Red
# UP side: Green
@@ -51,15 +52,15 @@ class Rotation3d(ThreeDScene):
orientations = [IN, OUT, LEFT, RIGHT, UP, DOWN]
for face, orient in zip(cube_origin.family_members_with_points(), orientations):
if np.array_equal(orient, RIGHT):
face.set_style_data(fill_color = RED)
face.set_style_data(fill_color=RED)
elif np.array_equal(orient, UP):
face.set_style_data(fill_color = GREEN)
face.set_style_data(fill_color=GREEN)
elif np.array_equal(orient, OUT):
face.set_style_data(fill_color = BLUE)
face.set_style_data(fill_color=BLUE)
cube_shifted = Cube(fill_opacity = 0.8, stroke_width = 1.,
side_length = 1., fill_color = BLUE)
shift_vec = 2*(RIGHT + UP + OUT)
cube_shifted = Cube(fill_opacity=0.8, stroke_width=1.,
side_length=1., fill_color=BLUE)
shift_vec = 2 * (RIGHT + UP + OUT)
cube_shifted.shift(shift_vec)
# STEP 2
@@ -71,8 +72,8 @@ class Rotation3d(ThreeDScene):
# Setup the camera position
phi, theta, distance = ThreeDCamera().get_spherical_coords()
angle_factor = 0.9
phi += 2*np.pi/4*angle_factor
theta += 3*2*np.pi/8
phi += 2 * np.pi / 4 * angle_factor
theta += 3 * 2 * np.pi / 8
self.set_camera_position(phi, theta, distance)
self.wait()
@@ -80,23 +81,23 @@ class Rotation3d(ThreeDScene):
# Animation
# Animation 1: rotation around the Z-axis with the ORIGIN of the space
# as center of rotation
theta += 2*np.pi
theta += 2 * np.pi
self.move_camera(phi, theta, distance,
run_time = 5)
run_time=5)
# Animation 2: shift the space in order of to get the center of the shifted cube
# as the next center of rotation
cube_center = cube_shifted.get_center()
self.move_camera(center_x = cube_center[0],
center_y = cube_center[1],
center_z = cube_center[2],
run_time = 2)
self.move_camera(center_x=cube_center[0],
center_y=cube_center[1],
center_z=cube_center[2],
run_time=2)
# Animation 3: rotation around the Z-axis with the center of the shifted cube
# Animation 3: rotation around the Z-axis with the center of the shifted cube
# as center of rotation
theta += 2*np.pi
theta += 2 * np.pi
self.move_camera(phi, theta, distance,
run_time = 5)
run_time=5)
class SpinAroundCube(ThreeDScene):
@@ -112,28 +113,28 @@ class SpinAroundCube(ThreeDScene):
def construct(self):
axes = ThreeDAxes()
cube = Cube(
fill_opacity = 1,
stroke_color = LIGHT_GREY,
stroke_width = 1,
fill_opacity=1,
stroke_color=LIGHT_GREY,
stroke_width=1,
)
# The constant OUT is np.array([0, 0, 1])
cube.next_to(ORIGIN, UP+RIGHT+OUT)
cube.next_to(ORIGIN, UP + RIGHT + OUT)
self.add(axes, cube)
# The camera starts positioned with phi=0, meaning it
# is directly above the xy-plane, and theta = -TAU/4,
# is directly above the xy-plane, and theta = -TAU/4,
# which makes the "down" direction of the screen point
# in the negative y direction.
# This animates a camera movement
self.move_camera(
# Tilted 20 degrees off xy plane (70 degrees off the vertical)
phi = (70./360.)*TAU,
phi=(70. / 360.) * TAU,
# Positioned above the third quadrant of
# the xy-plane
theta = (-110./360.)*TAU,
theta=(-110. / 360.) * TAU,
# pass in animation config just like a .play call
run_time = 3
run_time=3
)
self.wait()
# If you want the camera to slowly rotate about
@@ -142,33 +143,17 @@ class SpinAroundCube(ThreeDScene):
self.wait(4)
self.play(FadeOut(cube))
text = TextMobject("Your ad here")
text.rotate(TAU/4, axis = RIGHT)
text.rotate(TAU / 4, axis=RIGHT)
text.next_to(cube, OUT)
self.play(Write(text))
# If you want to play animations while moving the camera,
# include them in an "added_anims" list to move_camera
self.move_camera(
theta = -0.2*TAU,
added_anims = [
text.shift, 3*OUT,
text.set_fill, {"opacity" : 1},
theta=-0.2 * TAU,
added_anims=[
text.shift, 3 * OUT,
text.set_fill, {"opacity": 1},
]
)
self.wait(4)

View File

@@ -3,7 +3,6 @@
import sys
import argparse
import imp
import imp
import inspect
import itertools as it
import os
@@ -12,7 +11,6 @@ import traceback
from constants import *
from camera.camera import Camera
from scene.scene import Scene
from utils.sounds import play_error_sound
from utils.sounds import play_finish_sound
@@ -45,169 +43,178 @@ NO_SCENE_MESSAGE = """
def get_configuration():
try:
parser = argparse.ArgumentParser()
parser.add_argument(
"file", help = "path to file holding the python code for the scene"
)
parser.add_argument(
"scene_name", help = "Name of the Scene class you want to see"
)
optional_args = [
("-p", "--preview"),
("-w", "--write_to_movie"),
("-s", "--show_last_frame"),
("-l", "--low_quality"),
("-m", "--medium_quality"),
("-g", "--save_pngs"),
("-f", "--show_file_in_finder"),
("-t", "--transparent"),
("-q", "--quiet"),
("-a", "--write_all")
]
for short_arg, long_arg in optional_args:
parser.add_argument(short_arg, long_arg, action = "store_true")
parser.add_argument("-o", "--output_name")
parser.add_argument("-n", "--start_at_animation_number")
args = parser.parse_args()
if args.output_name != None:
output_name_root, output_name_ext = os.path.splitext(args.output_name)
expected_ext = '.png' if args.show_last_frame else '.mp4'
if not output_name_ext in ['', expected_ext]:
print "WARNING: The output will be to (doubly-dotted) %s%s"%output_name_root%expected_ext
try:
parser = argparse.ArgumentParser()
parser.add_argument(
"file", help="path to file holding the python code for the scene"
)
parser.add_argument(
"scene_name", help="Name of the Scene class you want to see"
)
optional_args = [
("-p", "--preview"),
("-w", "--write_to_movie"),
("-s", "--show_last_frame"),
("-l", "--low_quality"),
("-m", "--medium_quality"),
("-g", "--save_pngs"),
("-f", "--show_file_in_finder"),
("-t", "--transparent"),
("-q", "--quiet"),
("-a", "--write_all")
]
for short_arg, long_arg in optional_args:
parser.add_argument(short_arg, long_arg, action="store_true")
parser.add_argument("-o", "--output_name")
parser.add_argument("-n", "--start_at_animation_number")
args = parser.parse_args()
if args.output_name is not None:
output_name_root, output_name_ext = os.path.splitext(
args.output_name)
expected_ext = '.png' if args.show_last_frame else '.mp4'
if output_name_ext not in ['', expected_ext]:
print "WARNING: The output will be to (doubly-dotted) %s%s" % output_name_root % expected_ext
output_name = args.output_name
else:
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
output_name = output_name_root
else:
output_name = args.output_name
else:
# If anyone wants .mp4.mp4 and is surprised to only get .mp4, or such... Well, too bad.
output_name = output_name_root
else:
output_name = args.output_name
except argparse.ArgumentError as err:
print(str(err))
sys.exit(2)
config = {
"file" : args.file,
"scene_name" : args.scene_name,
"open_video_upon_completion" : args.preview,
"show_file_in_finder" : args.show_file_in_finder,
#By default, write to file
"write_to_movie" : args.write_to_movie or not args.show_last_frame,
"show_last_frame" : args.show_last_frame,
"save_pngs" : args.save_pngs,
#If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension" : ".mov" if args.transparent else ".mp4",
"quiet" : args.quiet or args.write_all,
"ignore_waits" : args.preview,
"write_all" : args.write_all,
"output_name" : output_name,
"start_at_animation_number" : args.start_at_animation_number,
"end_at_animation_number" : None,
}
if args.low_quality:
config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG
config["frame_duration"] = LOW_QUALITY_FRAME_DURATION
elif args.medium_quality:
config["camera_config"] = MEDIUM_QUALITY_CAMERA_CONFIG
config["frame_duration"] = MEDIUM_QUALITY_FRAME_DURATION
else:
config["camera_config"] = PRODUCTION_QUALITY_CAMERA_CONFIG
config["frame_duration"] = PRODUCTION_QUALITY_FRAME_DURATION
except argparse.ArgumentError as err:
print(str(err))
sys.exit(2)
config = {
"file": args.file,
"scene_name": args.scene_name,
"open_video_upon_completion": args.preview,
"show_file_in_finder": args.show_file_in_finder,
# By default, write to file
"write_to_movie": args.write_to_movie or not args.show_last_frame,
"show_last_frame": args.show_last_frame,
"save_pngs": args.save_pngs,
# If -t is passed in (for transparent), this will be RGBA
"saved_image_mode": "RGBA" if args.transparent else "RGB",
"movie_file_extension": ".mov" if args.transparent else ".mp4",
"quiet": args.quiet or args.write_all,
"ignore_waits": args.preview,
"write_all": args.write_all,
"output_name": output_name,
"start_at_animation_number": args.start_at_animation_number,
"end_at_animation_number": None,
}
if args.low_quality:
config["camera_config"] = LOW_QUALITY_CAMERA_CONFIG
config["frame_duration"] = LOW_QUALITY_FRAME_DURATION
elif args.medium_quality:
config["camera_config"] = MEDIUM_QUALITY_CAMERA_CONFIG
config["frame_duration"] = MEDIUM_QUALITY_FRAME_DURATION
else:
config["camera_config"] = PRODUCTION_QUALITY_CAMERA_CONFIG
config["frame_duration"] = PRODUCTION_QUALITY_FRAME_DURATION
stan = config["start_at_animation_number"]
if stan is not None:
if "," in stan:
start, end = stan.split(",")
config["start_at_animation_number"] = int(start)
config["end_at_animation_number"] = int(end)
else:
config["start_at_animation_number"] = int(stan)
stan = config["start_at_animation_number"]
if stan is not None:
if "," in stan:
start, end = stan.split(",")
config["start_at_animation_number"] = int(start)
config["end_at_animation_number"] = int(end)
else:
config["start_at_animation_number"] = int(stan)
config["skip_animations"] = any([
config["show_last_frame"] and not config["write_to_movie"],
config["start_at_animation_number"],
])
return config
config["skip_animations"] = any([
config["show_last_frame"] and not config["write_to_movie"],
config["start_at_animation_number"],
])
return config
def handle_scene(scene, **config):
import platform
if config["quiet"]:
curr_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")
import platform
if config["quiet"]:
curr_stdout = sys.stdout
sys.stdout = open(os.devnull, "w")
if config["show_last_frame"]:
scene.save_image(mode = config["saved_image_mode"])
open_file = any([
config["show_last_frame"],
config["open_video_upon_completion"],
config["show_file_in_finder"]
])
if open_file:
commands = ["open"]
if (platform.system() == "Linux"):
commands = ["xdg-open"]
if config["show_last_frame"]:
scene.save_image(mode=config["saved_image_mode"])
open_file = any([
config["show_last_frame"],
config["open_video_upon_completion"],
config["show_file_in_finder"]
])
if open_file:
commands = ["open"]
if (platform.system() == "Linux"):
commands = ["xdg-open"]
if config["show_file_in_finder"]:
commands.append("-R")
#
if config["show_last_frame"]:
commands.append(scene.get_image_file_path())
else:
commands.append(scene.get_movie_file_path())
FNULL = open(os.devnull, 'w')
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
FNULL.close()
if config["show_file_in_finder"]:
commands.append("-R")
#
if config["show_last_frame"]:
commands.append(scene.get_image_file_path())
else:
commands.append(scene.get_movie_file_path())
FNULL = open(os.devnull, 'w')
sp.call(commands, stdout=FNULL, stderr=sp.STDOUT)
FNULL.close()
if config["quiet"]:
sys.stdout.close()
sys.stdout = curr_stdout
if config["quiet"]:
sys.stdout.close()
sys.stdout = curr_stdout
def is_scene(obj):
if not inspect.isclass(obj):
return False
if not issubclass(obj, Scene):
return False
if obj == Scene:
return False
return True
if not inspect.isclass(obj):
return False
if not issubclass(obj, Scene):
return False
if obj == Scene:
return False
return True
def prompt_user_for_choice(name_to_obj):
num_to_name = {}
names = sorted(name_to_obj.keys())
for count, name in zip(it.count(1), names):
print("%d: %s"%(count, name))
num_to_name[count] = name
try:
user_input = raw_input(CHOOSE_NUMBER_MESSAGE)
return [
name_to_obj[num_to_name[int(num_str)]]
for num_str in user_input.split(",")
]
except:
print(INVALID_NUMBER_MESSAGE)
sys.exit()
num_to_name = {}
names = sorted(name_to_obj.keys())
for count, name in zip(it.count(1), names):
print("%d: %s" % (count, name))
num_to_name[count] = name
try:
user_input = raw_input(CHOOSE_NUMBER_MESSAGE)
return [
name_to_obj[num_to_name[int(num_str)]]
for num_str in user_input.split(",")
]
except:
print(INVALID_NUMBER_MESSAGE)
sys.exit()
def get_scene_classes(scene_names_to_classes, config):
if len(scene_names_to_classes) == 0:
print(NO_SCENE_MESSAGE)
return []
if len(scene_names_to_classes) == 1:
return scene_names_to_classes.values()
if config["scene_name"] in scene_names_to_classes:
return [scene_names_to_classes[config["scene_name"]] ]
if config["scene_name"] != "":
print(SCENE_NOT_FOUND_MESSAGE)
return []
if config["write_all"]:
return scene_names_to_classes.values()
return prompt_user_for_choice(scene_names_to_classes)
if len(scene_names_to_classes) == 0:
print(NO_SCENE_MESSAGE)
return []
if len(scene_names_to_classes) == 1:
return scene_names_to_classes.values()
if config["scene_name"] in scene_names_to_classes:
return [scene_names_to_classes[config["scene_name"]]]
if config["scene_name"] != "":
print(SCENE_NOT_FOUND_MESSAGE)
return []
if config["write_all"]:
return scene_names_to_classes.values()
return prompt_user_for_choice(scene_names_to_classes)
def get_module_windows(file_name):
module_name = file_name.replace(".py", "")
last_module = imp.load_module("__init__", *imp.find_module("__init__", ['.']))
for part in module_name.split(os.sep):
load_args = imp.find_module(part, [os.path.dirname(last_module.__file__)])
last_module = imp.load_module(part, *load_args)
return last_module
module_name = file_name.replace(".py", "")
last_module = imp.load_module(
"__init__", *imp.find_module("__init__", ['.']))
for part in module_name.split(os.sep):
load_args = imp.find_module(
part, [os.path.dirname(last_module.__file__)])
last_module = imp.load_module(part, *load_args)
return last_module
def get_module_posix(file_name):
module_name = file_name.replace(".py", "")
@@ -217,54 +224,54 @@ def get_module_posix(file_name):
last_module = imp.load_module(part, *load_args)
return last_module
def get_module(file_name):
if os.name == 'nt':
return get_module_windows(file_name)
return get_module_posix(file_name)
def main():
config = get_configuration()
module = get_module(config["file"])
scene_names_to_classes = dict(
inspect.getmembers(module, is_scene)
)
config = get_configuration()
module = get_module(config["file"])
scene_names_to_classes = dict(inspect.getmembers(module, is_scene))
config["output_directory"] = os.path.join(
ANIMATIONS_DIR,
config["file"].replace(".py", "")
)
config["output_directory"] = os.path.join(
ANIMATIONS_DIR,
config["file"].replace(".py", "")
)
scene_kwargs = dict([
(key, config[key])
for key in [
"camera_config",
"frame_duration",
"skip_animations",
"write_to_movie",
"output_directory",
"save_pngs",
"movie_file_extension",
"start_at_animation_number",
"end_at_animation_number",
]
])
scene_kwargs["name"] = config["output_name"]
if config["save_pngs"]:
print "We are going to save a PNG sequence as well..."
scene_kwargs["save_pngs"] = True
scene_kwargs["pngs_mode"] = config["saved_image_mode"]
for SceneClass in get_scene_classes(scene_names_to_classes, config):
try:
handle_scene(SceneClass(**scene_kwargs), **config)
play_finish_sound()
except:
print("\n\n")
traceback.print_exc()
print("\n\n")
play_error_sound()
scene_kwargs = dict([
(key, config[key])
for key in [
"camera_config",
"frame_duration",
"skip_animations",
"write_to_movie",
"output_directory",
"save_pngs",
"movie_file_extension",
"start_at_animation_number",
"end_at_animation_number",
]
])
scene_kwargs["name"] = config["output_name"]
if config["save_pngs"]:
print "We are going to save a PNG sequence as well..."
scene_kwargs["save_pngs"] = True
scene_kwargs["pngs_mode"] = config["saved_image_mode"]
for SceneClass in get_scene_classes(scene_names_to_classes, config):
try:
handle_scene(SceneClass(**scene_kwargs), **config)
play_finish_sound()
except:
print("\n\n")
traceback.print_exc()
print("\n\n")
play_error_sound()
if __name__ == "__main__":
main()
main()

View File

@@ -8,10 +8,8 @@ from animation.animation import Animation
from animation.composition import LaggedStart
from animation.creation import DrawBorderThenFill
from animation.creation import Write
from animation.transform import ApplyMethod
from animation.creation import FadeIn
from animation.creation import FadeOut
from mobject.svg.tex_mobject import TexMobject
from mobject.svg.tex_mobject import TextMobject
from mobject.types.vectorized_mobject import VGroup
from scene.scene import Scene
@@ -23,42 +21,45 @@ from mobject.geometry import Rectangle
from mobject.geometry import Square
from mobject.svg.drawings import PatreonLogo
class OpeningQuote(Scene):
CONFIG = {
"quote" : [],
"quote_arg_separator" : " ",
"highlighted_quote_terms" : {},
"author" : "",
"fade_in_kwargs" : {
"submobject_mode" : "lagged_start",
"rate_func" : None,
"lag_factor" : 4,
"run_time" : 5,
"quote": [],
"quote_arg_separator": " ",
"highlighted_quote_terms": {},
"author": "",
"fade_in_kwargs": {
"submobject_mode": "lagged_start",
"rate_func": None,
"lag_factor": 4,
"run_time": 5,
},
}
def construct(self):
self.quote = self.get_quote()
self.author = self.get_author(self.quote)
self.play(FadeIn(self.quote, **self.fade_in_kwargs))
self.wait(2)
self.play(Write(self.author, run_time = 3))
self.play(Write(self.author, run_time=3))
self.wait()
def get_quote(self, max_width = FRAME_WIDTH-1):
def get_quote(self, max_width=FRAME_WIDTH - 1):
text_mobject_kwargs = {
"alignment" : "",
"arg_separator" : self.quote_arg_separator,
"alignment": "",
"arg_separator": self.quote_arg_separator,
}
if isinstance(self.quote, str):
quote = TextMobject("``%s''"%self.quote.strip(), **text_mobject_kwargs)
quote = TextMobject("``%s''" %
self.quote.strip(), **text_mobject_kwargs)
else:
words = ["\\Large ``"] + list(self.quote) + ["''"]
quote = TextMobject(*words, **text_mobject_kwargs)
##TODO, make less hacky
# TODO, make less hacky
if self.quote_arg_separator == " ":
quote[0].shift(0.2*RIGHT)
quote[-1].shift(0.2*LEFT)
quote[0].shift(0.2 * RIGHT)
quote[-1].shift(0.2 * LEFT)
for term, color in self.set_colored_quote_terms.items():
quote.set_color_by_tex(term, color)
quote.to_edge(UP)
@@ -72,12 +73,14 @@ class OpeningQuote(Scene):
author.set_color(YELLOW)
return author
class PatreonThanks(Scene):
CONFIG = {
"specific_patrons" : [],
"max_patron_group_size" : 20,
"patron_scale_val" : 0.8,
"specific_patrons": [],
"max_patron_group_size": 20,
"patron_scale_val": 0.8,
}
def construct(self):
morty = Mortimer()
morty.next_to(ORIGIN, DOWN)
@@ -89,17 +92,17 @@ class PatreonThanks(Scene):
patrons = map(TextMobject, self.specific_patrons)
num_groups = float(len(patrons)) / self.max_patron_group_size
proportion_range = np.linspace(0, 1, num_groups + 1)
indices = (len(patrons)*proportion_range).astype('int')
indices = (len(patrons) * proportion_range).astype('int')
patron_groups = [
VGroup(*patrons[i:j])
for i, j in zip(indices, indices[1:])
]
]
for i, group in enumerate(patron_groups):
left_group = VGroup(*group[:len(group)/2])
right_group = VGroup(*group[len(group)/2:])
left_group = VGroup(*group[:len(group) / 2])
right_group = VGroup(*group[len(group) / 2:])
for subgroup, vect in (left_group, LEFT), (right_group, RIGHT):
subgroup.arrange_submobjects(DOWN, aligned_edge = LEFT)
subgroup.arrange_submobjects(DOWN, aligned_edge=LEFT)
subgroup.scale(self.patron_scale_val)
subgroup.to_edge(vect)
@@ -109,7 +112,7 @@ class PatreonThanks(Scene):
if last_group is not None:
self.play(
FadeOut(last_group),
morty.look, UP+LEFT
morty.look, UP + LEFT
)
else:
anims += [
@@ -117,25 +120,27 @@ class PatreonThanks(Scene):
]
self.play(
LaggedStart(
FadeIn, group,
run_time = 2,
FadeIn, group,
run_time=2,
),
morty.change, "gracious", group.get_corner(UP+LEFT),
morty.change, "gracious", group.get_corner(UP + LEFT),
*anims
)
self.play(morty.look_at, group.get_corner(DOWN+LEFT))
self.play(morty.look_at, group.get_corner(UP+RIGHT))
self.play(morty.look_at, group.get_corner(DOWN+RIGHT))
self.play(morty.look_at, group.get_corner(DOWN + LEFT))
self.play(morty.look_at, group.get_corner(UP + RIGHT))
self.play(morty.look_at, group.get_corner(DOWN + RIGHT))
self.play(Blink(morty))
last_group = group
class PatreonEndScreen(PatreonThanks):
CONFIG = {
"n_patron_columns" : 3,
"max_patron_width" : 3,
"run_time" : 20,
"randomize_order" : True,
"n_patron_columns": 3,
"max_patron_width": 3,
"run_time": 20,
"randomize_order": True,
}
def construct(self):
if self.randomize_order:
random.shuffle(self.specific_patrons)
@@ -145,32 +150,32 @@ class PatreonEndScreen(PatreonThanks):
def add_title(self):
title = self.title = TextMobject("Clicky Stuffs")
title.scale(1.5)
title.to_edge(UP, buff = MED_SMALL_BUFF)
title.to_edge(UP, buff=MED_SMALL_BUFF)
randy, morty = self.pi_creatures = VGroup(Randolph(), Mortimer())
for pi, vect in (randy, LEFT), (morty, RIGHT):
pi.scale_to_fit_height(title.get_height())
pi.change_mode("thinking")
pi.look(DOWN)
pi.next_to(title, vect, buff = MED_LARGE_BUFF)
pi.next_to(title, vect, buff=MED_LARGE_BUFF)
self.add_foreground_mobjects(title, randy, morty)
def scroll_through_patrons(self):
logo_box = Square(side_length = 2.5)
logo_box.to_corner(DOWN+LEFT, buff = MED_LARGE_BUFF)
logo_box = Square(side_length=2.5)
logo_box.to_corner(DOWN + LEFT, buff=MED_LARGE_BUFF)
total_width = FRAME_X_RADIUS - logo_box.get_right()[0]
black_rect = Rectangle(
fill_color = BLACK,
fill_opacity = 1,
stroke_width = 0,
width = FRAME_WIDTH,
height = 1.1*FRAME_Y_RADIUS
fill_color=BLACK,
fill_opacity=1,
stroke_width=0,
width=FRAME_WIDTH,
height=1.1 * FRAME_Y_RADIUS
)
black_rect.to_edge(UP, buff = 0)
line = DashedLine(FRAME_X_RADIUS*LEFT, FRAME_X_RADIUS*RIGHT)
black_rect.to_edge(UP, buff=0)
line = DashedLine(FRAME_X_RADIUS * LEFT, FRAME_X_RADIUS * RIGHT)
line.move_to(black_rect, DOWN)
line.shift(SMALL_BUFF*SMALL_BUFF*DOWN)
line.shift(SMALL_BUFF * SMALL_BUFF * DOWN)
self.add(line)
patrons = VGroup(*map(TextMobject, self.specific_patrons))
@@ -181,34 +186,36 @@ class PatreonEndScreen(PatreonThanks):
columns = VGroup(*[
VGroup(
*patrons[i::self.n_patron_columns]
).arrange_submobjects(DOWN, buff = MED_SMALL_BUFF)
).arrange_submobjects(DOWN, buff=MED_SMALL_BUFF)
for i in range(self.n_patron_columns)
])
columns.arrange_submobjects(
RIGHT, buff = LARGE_BUFF,
aligned_edge = UP,
RIGHT, buff=LARGE_BUFF,
aligned_edge=UP,
)
columns.scale_to_fit_width(total_width - 1)
columns.next_to(black_rect, DOWN, 3*LARGE_BUFF)
columns.next_to(black_rect, DOWN, 3 * LARGE_BUFF)
columns.to_edge(RIGHT)
self.play(
columns.next_to, FRAME_Y_RADIUS*DOWN, UP, LARGE_BUFF,
columns.to_edge, RIGHT,
columns.next_to, FRAME_Y_RADIUS * DOWN, UP, LARGE_BUFF,
columns.to_edge, RIGHT,
Animation(black_rect),
rate_func = None,
run_time = self.run_time,
rate_func=None,
run_time=self.run_time,
)
class ExternallyAnimatedScene(Scene):
def construct(self):
raise Exception("Don't actually run this class.")
class TODOStub(Scene):
CONFIG = {
"message" : ""
"message": ""
}
def construct(self):
self.add(TextMobject("TODO: %s"%self.message))
self.wait()
def construct(self):
self.add(TextMobject("TODO: %s" % self.message))
self.wait()

View File

@@ -18,46 +18,49 @@ from utils.rate_functions import there_and_back
PI_CREATURE_DIR = os.path.join(MEDIA_DIR, "designs", "PiCreature")
PI_CREATURE_SCALE_FACTOR = 0.5
LEFT_EYE_INDEX = 0
RIGHT_EYE_INDEX = 1
LEFT_PUPIL_INDEX = 2
LEFT_EYE_INDEX = 0
RIGHT_EYE_INDEX = 1
LEFT_PUPIL_INDEX = 2
RIGHT_PUPIL_INDEX = 3
BODY_INDEX = 4
MOUTH_INDEX = 5
BODY_INDEX = 4
MOUTH_INDEX = 5
class PiCreature(SVGMobject):
CONFIG = {
"color" : BLUE_E,
"file_name_prefix" : "PiCreatures",
"stroke_width" : 0,
"stroke_color" : BLACK,
"fill_opacity" : 1.0,
"propagate_style_to_family" : True,
"height" : 3,
"corner_scale_factor" : 0.75,
"flip_at_start" : False,
"is_looking_direction_purposeful" : False,
"start_corner" : None,
#Range of proportions along body where arms are
"right_arm_range" : [0.55, 0.7],
"left_arm_range" : [.34, .462],
"color": BLUE_E,
"file_name_prefix": "PiCreatures",
"stroke_width": 0,
"stroke_color": BLACK,
"fill_opacity": 1.0,
"propagate_style_to_family": True,
"height": 3,
"corner_scale_factor": 0.75,
"flip_at_start": False,
"is_looking_direction_purposeful": False,
"start_corner": None,
# Range of proportions along body where arms are
"right_arm_range": [0.55, 0.7],
"left_arm_range": [.34, .462],
}
def __init__(self, mode = "plain", **kwargs):
def __init__(self, mode="plain", **kwargs):
digest_config(self, kwargs)
self.parts_named = False
try:
svg_file = os.path.join(
PI_CREATURE_DIR,
"%s_%s.svg"%(self.file_name_prefix, mode)
PI_CREATURE_DIR,
"%s_%s.svg" % (self.file_name_prefix, mode)
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
SVGMobject.__init__(self, file_name=svg_file, **kwargs)
except:
warnings.warn("No %s design with mode %s"%(self.file_name_prefix, mode))
warnings.warn("No %s design with mode %s" %
(self.file_name_prefix, mode))
svg_file = os.path.join(
FILE_DIR,
FILE_DIR,
"PiCreatures_plain.svg",
)
SVGMobject.__init__(self, file_name = svg_file, **kwargs)
SVGMobject.__init__(self, file_name=svg_file, **kwargs)
if self.flip_at_start:
self.flip()
@@ -82,10 +85,10 @@ class PiCreature(SVGMobject):
SVGMobject.init_colors(self)
if not self.parts_named:
self.name_parts()
self.mouth.set_fill(BLACK, opacity = 1)
self.body.set_fill(self.color, opacity = 1)
self.pupils.set_fill(BLACK, opacity = 1)
self.eyes.set_fill(WHITE, opacity = 1)
self.mouth.set_fill(BLACK, opacity=1)
self.body.set_fill(self.color, opacity=1)
self.pupils.set_fill(BLACK, opacity=1)
self.eyes.set_fill(WHITE, opacity=1)
return self
def copy(self):
@@ -99,8 +102,8 @@ class PiCreature(SVGMobject):
def change_mode(self, mode):
new_self = self.__class__(
mode = mode,
color = self.color
mode=mode,
color=self.color
)
new_self.scale_to_fit_height(self.get_height())
if self.is_flipped() ^ new_self.is_flipped():
@@ -118,16 +121,16 @@ class PiCreature(SVGMobject):
direction /= norm
self.purposeful_looking_direction = direction
for pupil, eye in zip(self.pupils.split(), self.eyes.split()):
pupil_radius = pupil.get_width()/2.
eye_radius = eye.get_width()/2.
pupil_radius = pupil.get_width() / 2.
eye_radius = eye.get_width() / 2.
pupil.move_to(eye)
if direction[1] < 0:
pupil.shift(pupil_radius*DOWN/3)
pupil.shift(direction*(eye_radius-pupil_radius))
pupil.shift(pupil_radius * DOWN / 3)
pupil.shift(direction * (eye_radius - pupil_radius))
bottom_diff = eye.get_bottom()[1] - pupil.get_bottom()[1]
if bottom_diff > 0:
pupil.shift(bottom_diff*UP)
#TODO, how to handle looking up...
pupil.shift(bottom_diff * UP)
# TODO, how to handle looking up...
# top_diff = eye.get_top()[1]-pupil.get_top()[1]
# if top_diff < 0:
# pupil.shift(top_diff*UP)
@@ -141,7 +144,7 @@ class PiCreature(SVGMobject):
self.look(point - self.eyes.get_center())
return self
def change(self, new_mode, look_at_arg = None):
def change(self, new_mode, look_at_arg=None):
self.change_mode(new_mode)
if look_at_arg is not None:
self.look_at(look_at_arg)
@@ -150,27 +153,27 @@ class PiCreature(SVGMobject):
def get_looking_direction(self):
return np.sign(np.round(
self.pupils.get_center() - self.eyes.get_center(),
decimals = 2
decimals=2
))
def is_flipped(self):
return self.eyes.submobjects[0].get_center()[0] > \
self.eyes.submobjects[1].get_center()[0]
self.eyes.submobjects[1].get_center()[0]
def blink(self):
eye_parts = self.eye_parts
eye_bottom_y = eye_parts.get_bottom()[1]
eye_parts.apply_function(
lambda p : [p[0], eye_bottom_y, p[2]]
lambda p: [p[0], eye_bottom_y, p[2]]
)
return self
def to_corner(self, vect = None, **kwargs):
def to_corner(self, vect=None, **kwargs):
if vect is not None:
SVGMobject.to_corner(self, vect, **kwargs)
else:
self.scale(self.corner_scale_factor)
self.to_corner(DOWN+LEFT, **kwargs)
self.to_corner(DOWN + LEFT, **kwargs)
return self
def get_bubble(self, *content, **kwargs):
@@ -196,8 +199,8 @@ class PiCreature(SVGMobject):
def shrug(self):
self.change_mode("shruggie")
top_mouth_point, bottom_mouth_point = [
self.mouth.points[np.argmax(self.mouth.points[:,1])],
self.mouth.points[np.argmin(self.mouth.points[:,1])]
self.mouth.points[np.argmax(self.mouth.points[:, 1])],
self.mouth.points[np.argmin(self.mouth.points[:, 1])]
]
self.look(top_mouth_point - bottom_mouth_point)
return self
@@ -209,9 +212,10 @@ class PiCreature(SVGMobject):
for alpha_range in self.right_arm_range, self.left_arm_range
])
def get_all_pi_creature_modes():
result = []
prefix = "%s_"%PiCreature.CONFIG["file_name_prefix"]
prefix = "%s_" % PiCreature.CONFIG["file_name_prefix"]
suffix = ".svg"
for file in os.listdir(PI_CREATURE_DIR):
if file.startswith(prefix) and file.endswith(suffix):
@@ -220,69 +224,78 @@ def get_all_pi_creature_modes():
)
return result
class Randolph(PiCreature):
pass #Nothing more than an alternative name
pass # Nothing more than an alternative name
class Mortimer(PiCreature):
CONFIG = {
"color" : GREY_BROWN,
"flip_at_start" : True,
"color": GREY_BROWN,
"flip_at_start": True,
}
class Mathematician(PiCreature):
CONFIG = {
"color" : GREY,
"color": GREY,
}
class BabyPiCreature(PiCreature):
CONFIG = {
"scale_factor" : 0.5,
"eye_scale_factor" : 1.2,
"pupil_scale_factor" : 1.3
"scale_factor": 0.5,
"eye_scale_factor": 1.2,
"pupil_scale_factor": 1.3
}
def __init__(self, *args, **kwargs):
PiCreature.__init__(self, *args, **kwargs)
self.scale(self.scale_factor)
self.shift(LEFT)
self.to_edge(DOWN, buff = LARGE_BUFF)
self.to_edge(DOWN, buff=LARGE_BUFF)
eyes = VGroup(self.eyes, self.pupils)
eyes_bottom = eyes.get_bottom()
eyes.scale(self.eye_scale_factor)
eyes.move_to(eyes_bottom, aligned_edge = DOWN)
eyes.move_to(eyes_bottom, aligned_edge=DOWN)
looking_direction = self.get_looking_direction()
for pupil in self.pupils:
pupil.scale_in_place(self.pupil_scale_factor)
self.look(looking_direction)
class TauCreature(PiCreature):
CONFIG = {
"file_name_prefix" : "TauCreatures"
"file_name_prefix": "TauCreatures"
}
class ThreeLeggedPiCreature(PiCreature):
CONFIG = {
"file_name_prefix" : "ThreeLeggedPiCreatures"
"file_name_prefix": "ThreeLeggedPiCreatures"
}
class Eyes(VMobject):
CONFIG = {
"height" : 0.3,
"thing_looked_at" : None,
"mode" : "plain",
"height": 0.3,
"thing_looked_at": None,
"mode": "plain",
}
def __init__(self, mobject, **kwargs):
VMobject.__init__(self, **kwargs)
self.mobject = mobject
self.submobjects = self.get_eyes().submobjects
def get_eyes(self, mode = None, thing_to_look_at = None):
def get_eyes(self, mode=None, thing_to_look_at=None):
mode = mode or self.mode
if thing_to_look_at is None:
thing_to_look_at = self.thing_looked_at
pi = Randolph(mode = mode)
pi = Randolph(mode=mode)
eyes = VGroup(pi.eyes, pi.pupils)
pi.scale(self.height/eyes.get_height())
pi.scale(self.height / eyes.get_height())
if self.submobjects:
eyes.move_to(self, DOWN)
else:
@@ -293,12 +306,12 @@ class Eyes(VMobject):
def change_mode_anim(self, mode, **kwargs):
self.mode = mode
return Transform(self, self.get_eyes(mode = mode), **kwargs)
return Transform(self, self.get_eyes(mode=mode), **kwargs)
def look_at_anim(self, point_or_mobject, **kwargs):
self.thing_looked_at = point_or_mobject
return Transform(
self, self.get_eyes(thing_to_look_at = point_or_mobject),
self, self.get_eyes(thing_to_look_at=point_or_mobject),
**kwargs
)
@@ -307,10 +320,8 @@ class Eyes(VMobject):
bottom_y = self.get_bottom()[1]
for submob in target:
submob.apply_function(
lambda p : [p[0], bottom_y, p[2]]
lambda p: [p[0], bottom_y, p[2]]
)
if "rate_func" not in kwargs:
kwargs["rate_func"] = squish_rate_func(there_and_back)
return Transform(self, target, **kwargs)

View File

@@ -16,30 +16,34 @@ from utils.config_ops import digest_config
from utils.rate_functions import squish_rate_func
from utils.rate_functions import there_and_back
class Blink(ApplyMethod):
CONFIG = {
"rate_func" : squish_rate_func(there_and_back)
"rate_func": squish_rate_func(there_and_back)
}
def __init__(self, pi_creature, **kwargs):
ApplyMethod.__init__(self, pi_creature.blink, **kwargs)
class PiCreatureBubbleIntroduction(AnimationGroup):
CONFIG = {
"target_mode" : "speaking",
"bubble_class" : SpeechBubble,
"change_mode_kwargs" : {},
"bubble_creation_class" : ShowCreation,
"bubble_creation_kwargs" : {},
"bubble_kwargs" : {},
"content_introduction_class" : Write,
"content_introduction_kwargs" : {},
"look_at_arg" : None,
"target_mode": "speaking",
"bubble_class": SpeechBubble,
"change_mode_kwargs": {},
"bubble_creation_class": ShowCreation,
"bubble_creation_kwargs": {},
"bubble_kwargs": {},
"content_introduction_class": Write,
"content_introduction_kwargs": {},
"look_at_arg": None,
}
def __init__(self, pi_creature, *content, **kwargs):
digest_config(self, kwargs)
bubble = pi_creature.get_bubble(
*content,
bubble_class = self.bubble_class,
bubble_class=self.bubble_class,
**self.bubble_kwargs
)
Group(bubble, bubble.content).shift_onto_screen()
@@ -61,18 +65,21 @@ class PiCreatureBubbleIntroduction(AnimationGroup):
**kwargs
)
class PiCreatureSays(PiCreatureBubbleIntroduction):
CONFIG = {
"target_mode" : "speaking",
"bubble_class" : SpeechBubble,
"target_mode": "speaking",
"bubble_class": SpeechBubble,
}
class RemovePiCreatureBubble(AnimationGroup):
CONFIG = {
"target_mode" : "plain",
"look_at_arg" : None,
"remover" : True,
"target_mode": "plain",
"look_at_arg": None,
"remover": True,
}
def __init__(self, pi_creature, **kwargs):
assert hasattr(pi_creature, "bubble")
digest_config(self, kwargs, locals())
@@ -89,7 +96,7 @@ class RemovePiCreatureBubble(AnimationGroup):
FadeOut(pi_creature.bubble.content),
)
def clean_up(self, surrounding_scene = None):
def clean_up(self, surrounding_scene=None):
AnimationGroup.clean_up(self, surrounding_scene)
self.pi_creature.bubble = None
if surrounding_scene is not None:

View File

@@ -12,51 +12,52 @@ from mobject.svg.tex_mobject import TexMobject
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from utils.config_ops import digest_config
from utils.space_ops import R3_to_complex
from utils.space_ops import angle_of_vector
from utils.space_ops import complex_to_R3
#TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene
# TODO: There should be much more code reuse between Axes, NumberPlane and GraphScene
class Axes(VGroup):
CONFIG = {
"propagate_style_to_family" : True,
"three_d" : False,
"number_line_config" : {
"color" : LIGHT_GREY,
"include_tip" : True,
"propagate_style_to_family": True,
"three_d": False,
"number_line_config": {
"color": LIGHT_GREY,
"include_tip": True,
},
"x_axis_config" : {},
"y_axis_config" : {},
"z_axis_config" : {},
"x_min" : -FRAME_X_RADIUS,
"x_max" : FRAME_X_RADIUS,
"y_min" : -FRAME_Y_RADIUS,
"y_max" : FRAME_Y_RADIUS,
"z_min" : -3.5,
"z_max" : 3.5,
"z_normal" : DOWN,
"default_num_graph_points" : 100,
"x_axis_config": {},
"y_axis_config": {},
"z_axis_config": {},
"x_min": -FRAME_X_RADIUS,
"x_max": FRAME_X_RADIUS,
"y_min": -FRAME_Y_RADIUS,
"y_max": FRAME_Y_RADIUS,
"z_min": -3.5,
"z_max": 3.5,
"z_normal": DOWN,
"default_num_graph_points": 100,
}
def __init__(self, **kwargs):
VGroup.__init__(self, **kwargs)
self.x_axis = self.get_axis(self.x_min, self.x_max, self.x_axis_config)
self.y_axis = self.get_axis(self.y_min, self.y_max, self.y_axis_config)
self.y_axis.rotate(np.pi/2, about_point = ORIGIN)
self.y_axis.rotate(np.pi / 2, about_point=ORIGIN)
self.add(self.x_axis, self.y_axis)
if self.three_d:
self.z_axis = self.get_axis(self.z_min, self.z_max, self.z_axis_config)
self.z_axis.rotate(-np.pi/2, UP, about_point = ORIGIN)
self.z_axis = self.get_axis(
self.z_min, self.z_max, self.z_axis_config)
self.z_axis.rotate(-np.pi / 2, UP, about_point=ORIGIN)
self.z_axis.rotate(
angle_of_vector(self.z_normal), OUT,
about_point = ORIGIN
about_point=ORIGIN
)
self.add(self.z_axis)
def get_axis(self, min_val, max_val, extra_config):
config = dict(self.number_line_config)
config.update(extra_config)
return NumberLine(x_min = min_val, x_max = max_val, **config)
return NumberLine(x_min=min_val, x_max=max_val, **config)
def coords_to_point(self, x, y):
origin = self.x_axis.number_to_point(0)
@@ -66,25 +67,25 @@ class Axes(VGroup):
def point_to_coords(self, point):
return (
self.x_axis.point_to_number(point),
self.x_axis.point_to_number(point),
self.y_axis.point_to_number(point),
)
def get_graph(
self, function, num_graph_points = None,
x_min = None,
x_max = None,
self, function, num_graph_points=None,
x_min=None,
x_max=None,
**kwargs
):
):
kwargs["fill_opacity"] = kwargs.get("fill_opacity", 0)
kwargs["num_anchor_points"] = \
num_graph_points or self.default_num_graph_points
x_min = x_min or self.x_min
x_max = x_max or self.x_max
graph = ParametricFunction(
lambda t : self.coords_to_point(t, function(t)),
t_min = x_min,
t_max = x_max,
lambda t: self.coords_to_point(t, function(t)),
t_min=x_min,
t_max=x_max,
**kwargs
)
graph.underlying_function = function
@@ -94,7 +95,7 @@ class Axes(VGroup):
if hasattr(graph, "underlying_function"):
return self.coords_to_point(x, graph.underlying_function(x))
else:
#binary search
# binary search
lh, rh = 0, 1
while abs(lh - rh) > 0.001:
mh = np.mean([lh, rh])
@@ -115,66 +116,69 @@ class Axes(VGroup):
return points[1]
return self.coords_to_point(x, graph.underlying_function(x))
class ThreeDAxes(Axes):
CONFIG = {
"x_min" : -5.5,
"x_max" : 5.5,
"y_min" : -4.5,
"y_max" : 4.5,
"three_d" : True,
"x_min": -5.5,
"x_max": 5.5,
"y_min": -4.5,
"y_max": 4.5,
"three_d": True,
}
class NumberPlane(VMobject):
CONFIG = {
"color" : BLUE_D,
"secondary_color" : BLUE_E,
"axes_color" : WHITE,
"secondary_stroke_width" : 1,
"color": BLUE_D,
"secondary_color": BLUE_E,
"axes_color": WHITE,
"secondary_stroke_width": 1,
# TODO: Allow coordinate center of NumberPlane to not be at (0, 0)
"x_radius": None,
"y_radius": None,
"x_unit_size" : 1,
"y_unit_size" : 1,
"center_point" : ORIGIN,
"x_line_frequency" : 1,
"y_line_frequency" : 1,
"secondary_line_ratio" : 1,
"written_coordinate_height" : 0.2,
"propagate_style_to_family" : False,
"make_smooth_after_applying_functions" : True,
"x_unit_size": 1,
"y_unit_size": 1,
"center_point": ORIGIN,
"x_line_frequency": 1,
"y_line_frequency": 1,
"secondary_line_ratio": 1,
"written_coordinate_height": 0.2,
"propagate_style_to_family": False,
"make_smooth_after_applying_functions": True,
}
def generate_points(self):
if self.x_radius is None:
center_to_edge = (FRAME_X_RADIUS + abs(self.center_point[0]))
center_to_edge = (FRAME_X_RADIUS + abs(self.center_point[0]))
self.x_radius = center_to_edge / self.x_unit_size
if self.y_radius is None:
center_to_edge = (FRAME_Y_RADIUS + abs(self.center_point[1]))
center_to_edge = (FRAME_Y_RADIUS + abs(self.center_point[1]))
self.y_radius = center_to_edge / self.y_unit_size
self.axes = VMobject()
self.main_lines = VMobject()
self.secondary_lines = VMobject()
tuples = [
(
self.x_radius,
self.x_line_frequency,
self.y_radius*DOWN,
self.y_radius*UP,
self.x_radius,
self.x_line_frequency,
self.y_radius * DOWN,
self.y_radius * UP,
RIGHT
),
(
self.y_radius,
self.y_line_frequency,
self.x_radius*LEFT,
self.x_radius*RIGHT,
self.y_radius,
self.y_line_frequency,
self.x_radius * LEFT,
self.x_radius * RIGHT,
UP,
),
]
for radius, freq, start, end, unit in tuples:
main_range = np.arange(0, radius, freq)
step = freq/float(freq + self.secondary_line_ratio)
step = freq / float(freq + self.secondary_line_ratio)
for v in np.arange(0, radius, step):
line1 = Line(start+v*unit, end+v*unit)
line2 = Line(start-v*unit, end-v*unit)
line1 = Line(start + v * unit, end + v * unit)
line2 = Line(start - v * unit, end - v * unit)
if v == 0:
self.axes.add(line1)
elif v in main_range:
@@ -185,7 +189,7 @@ class NumberPlane(VMobject):
self.stretch(self.x_unit_size, 0)
self.stretch(self.y_unit_size, 1)
self.shift(self.center_point)
#Put x_axis before y_axis
# Put x_axis before y_axis
y_axis, x_axis = self.axes.split()
self.axes = VMobject(x_axis, y_axis)
@@ -204,39 +208,39 @@ class NumberPlane(VMobject):
def coords_to_point(self, x, y):
x, y = np.array([x, y])
result = self.axes.get_center()
result += x*self.get_x_unit_size()*RIGHT
result += y*self.get_y_unit_size()*UP
result += x * self.get_x_unit_size() * RIGHT
result += y * self.get_y_unit_size() * UP
return result
def point_to_coords(self, point):
new_point = point - self.axes.get_center()
x = new_point[0]/self.get_x_unit_size()
y = new_point[1]/self.get_y_unit_size()
x = new_point[0] / self.get_x_unit_size()
y = new_point[1] / self.get_y_unit_size()
return x, y
# Does not recompute center, unit_sizes for each call; useful for
# iterating over large lists of points, but does assume these
# iterating over large lists of points, but does assume these
# attributes are kept accurate. (Could alternatively have a method
# which returns a function dynamically created after a single
# which returns a function dynamically created after a single
# call to each of get_center(), get_x_unit_size(), etc.)
def point_to_coords_cheap(self, point):
new_point = point - self.center_point
x = new_point[0]/self.x_unit_size
y = new_point[1]/self.y_unit_size
x = new_point[0] / self.x_unit_size
y = new_point[1] / self.y_unit_size
return x, y
def get_x_unit_size(self):
return self.axes.get_width() / (2.0*self.x_radius)
return self.axes.get_width() / (2.0 * self.x_radius)
def get_y_unit_size(self):
return self.axes.get_height() / (2.0*self.y_radius)
return self.axes.get_height() / (2.0 * self.y_radius)
def get_coordinate_labels(self, x_vals = None, y_vals = None):
def get_coordinate_labels(self, x_vals=None, y_vals=None):
coordinate_labels = VGroup()
if x_vals == None:
x_vals = range(-int(self.x_radius), int(self.x_radius)+1)
if y_vals == None:
y_vals = range(-int(self.y_radius), int(self.y_radius)+1)
if x_vals is None:
x_vals = range(-int(self.x_radius), int(self.x_radius) + 1)
if y_vals is None:
y_vals = range(-int(self.y_radius), int(self.y_radius) + 1)
for index, vals in enumerate([x_vals, y_vals]):
num_pair = [0, 0]
for val in vals:
@@ -249,7 +253,7 @@ class NumberPlane(VMobject):
num.scale_to_fit_height(
self.written_coordinate_height
)
num.next_to(point, DOWN+LEFT, buff = SMALL_BUFF)
num.next_to(point, DOWN + LEFT, buff=SMALL_BUFF)
coordinate_labels.add(num)
self.coordinate_labels = coordinate_labels
return coordinate_labels
@@ -257,7 +261,7 @@ class NumberPlane(VMobject):
def get_axes(self):
return self.axes
def get_axis_labels(self, x_label = "x", y_label = "y"):
def get_axis_labels(self, x_label="x", y_label="y"):
x_axis, y_axis = self.get_axes().split()
quads = [
(x_axis, x_label, UP, RIGHT),
@@ -273,39 +277,42 @@ class NumberPlane(VMobject):
self.axis_labels = labels
return labels
def add_coordinates(self, x_vals = None, y_vals = None):
def add_coordinates(self, x_vals=None, y_vals=None):
self.add(*self.get_coordinate_labels(x_vals, y_vals))
return self
def get_vector(self, coords, **kwargs):
point = coords[0]*RIGHT + coords[1]*UP
arrow = Arrow(ORIGIN, coords, **kwargs)
point = coords[0] * RIGHT + coords[1] * UP
arrow = Arrow(ORIGIN, point, **kwargs)
return arrow
def prepare_for_nonlinear_transform(self, num_inserted_anchor_points = 50):
def prepare_for_nonlinear_transform(self, num_inserted_anchor_points=50):
for mob in self.family_members_with_points():
num_anchors = mob.get_num_anchor_points()
if num_inserted_anchor_points > num_anchors:
mob.insert_n_anchor_points(num_inserted_anchor_points-num_anchors)
mob.insert_n_anchor_points(
num_inserted_anchor_points - num_anchors)
mob.make_smooth()
return self
class ComplexPlane(NumberPlane):
CONFIG = {
"color" : BLUE,
"unit_size" : 1,
"line_frequency" : 1,
"faded_line_frequency" : 0.5,
"color": BLUE,
"unit_size": 1,
"line_frequency": 1,
"faded_line_frequency": 0.5,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
kwargs.update({
"x_unit_size" : self.unit_size,
"y_unit_size" : self.unit_size,
"x_line_frequency" : self.line_frequency,
"x_faded_line_frequency" : self.faded_line_frequency,
"y_line_frequency" : self.line_frequency,
"y_faded_line_frequency" : self.faded_line_frequency,
"x_unit_size": self.unit_size,
"y_unit_size": self.unit_size,
"x_line_frequency": self.line_frequency,
"x_faded_line_frequency": self.faded_line_frequency,
"y_line_frequency": self.line_frequency,
"y_faded_line_frequency": self.faded_line_frequency,
})
NumberPlane.__init__(self, **kwargs)
@@ -321,12 +328,11 @@ class ComplexPlane(NumberPlane):
# TODO: Should merge this with the code from NumberPlane.get_coordinate_labels
result = VGroup()
nudge = 0.1*(DOWN+RIGHT)
if len(numbers) == 0:
numbers = range(-int(self.x_radius), int(self.x_radius)+1)
numbers = range(-int(self.x_radius), int(self.x_radius) + 1)
numbers += [
complex(0, y)
for y in range(-int(self.y_radius), int(self.y_radius)+1)
for y in range(-int(self.y_radius), int(self.y_radius) + 1)
]
for number in numbers:
if number == complex(0, 0):
@@ -340,7 +346,7 @@ class ComplexPlane(NumberPlane):
num_mob = TexMobject(num_str)
num_mob.add_background_rectangle()
num_mob.scale_to_fit_height(self.written_coordinate_height)
num_mob.next_to(point, DOWN+LEFT, SMALL_BUFF)
num_mob.next_to(point, DOWN + LEFT, SMALL_BUFF)
result.add(num_mob)
self.coordinate_labels = result
return result
@@ -348,4 +354,3 @@ class ComplexPlane(NumberPlane):
def add_coordinates(self, *numbers):
self.add(*self.get_coordinate_labels(*numbers))
return self

View File

@@ -4,32 +4,38 @@ from constants import *
from mobject.geometry import Rectangle
from utils.config_ops import digest_config
class ScreenRectangle(Rectangle):
CONFIG = {
"width_to_height_ratio" : 16.0/9.0,
"height" : 4,
"width_to_height_ratio": 16.0 / 9.0,
"height": 4,
}
def generate_points(self):
self.width = self.width_to_height_ratio * self.height
Rectangle.generate_points(self)
class FullScreenRectangle(ScreenRectangle):
CONFIG = {
"height" : FRAME_HEIGHT,
"height": FRAME_HEIGHT,
}
class FullScreenFadeRectangle(FullScreenRectangle):
CONFIG = {
"stroke_width" : 0,
"fill_color" : BLACK,
"fill_opacity" : 0.7,
"stroke_width": 0,
"fill_color": BLACK,
"fill_opacity": 0.7,
}
class PictureInPictureFrame(Rectangle):
CONFIG = {
"height" : 3,
"aspect_ratio" : (16, 9)
"height": 3,
"aspect_ratio": (16, 9)
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
height = self.height
@@ -37,8 +43,8 @@ class PictureInPictureFrame(Rectangle):
kwargs.pop("height")
Rectangle.__init__(
self,
width = self.aspect_ratio[0],
height = self.aspect_ratio[1],
width=self.aspect_ratio[0],
height=self.aspect_ratio[1],
**kwargs
)
self.scale_to_fit_height(height)

View File

@@ -8,57 +8,46 @@ from utils.config_ops import digest_config
class ParametricFunction(VMobject):
CONFIG = {
"t_min" : 0,
"t_max" : 1,
"num_anchor_points" : 100,
"t_min": 0,
"t_max": 1,
"num_anchor_points": 100,
}
def __init__(self, function, **kwargs):
self.function = function
VMobject.__init__(self, **kwargs)
def generate_points(self):
n_points = 3*self.num_anchor_points - 2
n_points = 3 * self.num_anchor_points - 2
self.points = np.zeros((n_points, self.dim))
self.points[:,0] = np.linspace(
self.points[:, 0] = np.linspace(
self.t_min, self.t_max, n_points
)
#VMobject.apply_function takes care of preserving
#desirable tangent line properties at anchor points
self.apply_function(lambda p : self.function(p[0]))
# VMobject.apply_function takes care of preserving
# desirable tangent line properties at anchor points
self.apply_function(lambda p: self.function(p[0]))
class FunctionGraph(ParametricFunction):
CONFIG = {
"color" : YELLOW,
"x_min" : -FRAME_X_RADIUS,
"x_max" : FRAME_X_RADIUS,
"color": YELLOW,
"x_min": -FRAME_X_RADIUS,
"x_max": FRAME_X_RADIUS,
}
def __init__(self, function, **kwargs):
digest_config(self, kwargs)
parametric_function = lambda t : t*RIGHT + function(t)*UP
def parametric_function(t):
return t * RIGHT + function(t) * UP
ParametricFunction.__init__(
self,
self,
parametric_function,
t_min = self.x_min,
t_max = self.x_max,
t_min=self.x_min,
t_max=self.x_max,
**kwargs
)
self.function = function
def get_function(self):
return self.function

View File

@@ -17,156 +17,159 @@ from utils.space_ops import center_of_mass
from utils.space_ops import compass_directions
from utils.space_ops import rotate_vector
class Arc(VMobject):
CONFIG = {
"radius" : 1.0,
"start_angle" : 0,
"num_anchors" : 9,
"anchors_span_full_range" : True,
"radius": 1.0,
"start_angle": 0,
"num_anchors": 9,
"anchors_span_full_range": True,
}
def __init__(self, angle, **kwargs):
self.angle = angle
VMobject.__init__(self, **kwargs)
def generate_points(self):
anchors = np.array([
np.cos(a)*RIGHT+np.sin(a)*UP
np.cos(a) * RIGHT + np.sin(a) * UP
for a in np.linspace(
self.start_angle,
self.start_angle + self.angle,
self.start_angle,
self.start_angle + self.angle,
self.num_anchors
)
])
#Figure out which control points will give the
#Appropriate tangent lines to the circle
d_theta = self.angle/(self.num_anchors-1.0)
# Figure out which control points will give the
# Appropriate tangent lines to the circle
d_theta = self.angle / (self.num_anchors - 1.0)
tangent_vectors = np.zeros(anchors.shape)
tangent_vectors[:,1] = anchors[:,0]
tangent_vectors[:,0] = -anchors[:,1]
handles1 = anchors[:-1] + (d_theta/3)*tangent_vectors[:-1]
handles2 = anchors[1:] - (d_theta/3)*tangent_vectors[1:]
tangent_vectors[:, 1] = anchors[:, 0]
tangent_vectors[:, 0] = -anchors[:, 1]
handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1]
handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:]
self.set_anchors_and_handles(
anchors, handles1, handles2
)
self.scale(self.radius, about_point = ORIGIN)
self.scale(self.radius, about_point=ORIGIN)
def add_tip(self, tip_length = 0.25, at_start = False, at_end = True):
def add_tip(self, tip_length=0.25, at_start=False, at_end=True):
# clear out any old tips
for submob in self.submobjects:
if submob.mark_paths_closed == True: # is a tip
if submob.mark_paths_closed:
self.remove(submob)
#TODO, do this a better way
# TODO, do this a better way
p1 = p2 = p3 = p4 = None
start_arrow = end_arrow = None
if at_end:
p1, p2 = self.points[-3:-1]
# self.points[-2:] did overshoot
start_arrow = Arrow(
p1, 2*p2 - p1,
tip_length = tip_length,
max_tip_length_to_length_ratio = 2.0
p1, 2 * p2 - p1,
tip_length=tip_length,
max_tip_length_to_length_ratio=2.0
)
self.add(start_arrow.split()[-1]) # just the tip
self.add(start_arrow.split()[-1]) # just the tip
if at_start:
p4, p3 = self.points[1:3]
# self.points[:2] did overshoot
end_arrow = Arrow(
p3, 2*p4 - p3,
tip_length = tip_length,
max_tip_length_to_length_ratio = 2.0
p3, 2 * p4 - p3,
tip_length=tip_length,
max_tip_length_to_length_ratio=2.0
)
self.add(end_arrow.split()[-1])
self.set_color(self.get_color())
return self
def get_arc_center(self):
first_point = self.points[0]
radial_unit_vector = np.array([np.cos(self.start_angle),np.sin(self.start_angle),0])
radial_unit_vector = np.array(
[np.cos(self.start_angle), np.sin(self.start_angle), 0])
arc_center = first_point - self.radius * radial_unit_vector
return arc_center
def move_arc_center_to(self,point):
def move_arc_center_to(self, point):
v = point - self.get_arc_center()
self.shift(v)
return self
def stop_angle(self):
return self.start_angle + self.angle
def set_bound_angles(self,start=0,stop=np.pi):
def set_bound_angles(self, start=0, stop=np.pi):
self.start_angle = start
self.angle = stop - start
return self
class ArcBetweenPoints(Arc):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
def __init__(self, start_point, end_point, angle=TAU / 4, **kwargs):
if angle == 0:
raise Exception("Arc with zero curve angle: use Line instead.")
midpoint = 0.5 * (start_point + end_point)
distance_vector = end_point - start_point
normal_vector = np.array([-distance_vector[1], distance_vector[0],0])
normal_vector = np.array([-distance_vector[1], distance_vector[0], 0])
distance = np.linalg.norm(normal_vector)
normal_vector /= distance
if angle < 0:
normal_vector *= -1
radius = distance/2 / np.sin(0.5 * np.abs(angle))
l = distance/2 / np.tan(0.5 * np.abs(angle))
arc_center = midpoint + l * normal_vector
radius = distance / 2 / np.sin(0.5 * np.abs(angle))
length = distance / 2 / np.tan(0.5 * np.abs(angle))
arc_center = midpoint + length * normal_vector
w = start_point - arc_center
if w[0] != 0:
start_angle = np.arctan2(w[1],w[0])
start_angle = np.arctan2(w[1], w[0])
else:
start_angle = np.pi/2
start_angle = np.pi / 2
Arc.__init__(self, angle,
radius = radius,
start_angle = start_angle,
**kwargs)
radius=radius,
start_angle=start_angle,
**kwargs)
self.move_arc_center_to(arc_center)
class CurvedArrow(ArcBetweenPoints):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
def __init__(self, start_point, end_point, angle=TAU / 4, **kwargs):
# I know this is in reverse, but it works
if angle >= 0:
ArcBetweenPoints.__init__(self, start_point, end_point, angle = angle, **kwargs)
self.add_tip(at_start = True, at_end = False)
ArcBetweenPoints.__init__(
self, start_point, end_point, angle=angle, **kwargs)
self.add_tip(at_start=True, at_end=False)
else:
ArcBetweenPoints.__init__(self, end_point, start_point, angle = -angle, **kwargs)
self.add_tip(at_start = False, at_end = True)
ArcBetweenPoints.__init__(
self, end_point, start_point, angle=-angle, **kwargs)
self.add_tip(at_start=False, at_end=True)
class CurvedDoubleArrow(ArcBetweenPoints):
def __init__(self, start_point, end_point, angle = TAU/4, **kwargs):
ArcBetweenPoints.__init__(self, start_point, end_point, angle = angle, **kwargs)
self.add_tip(at_start = True, at_end = True)
def __init__(self, start_point, end_point, angle=TAU / 4, **kwargs):
ArcBetweenPoints.__init__(
self, start_point, end_point, angle=angle, **kwargs)
self.add_tip(at_start=True, at_end=True)
class Circle(Arc):
CONFIG = {
"color" : RED,
"close_new_points" : True,
"anchors_span_full_range" : False
"color": RED,
"close_new_points": True,
"anchors_span_full_range": False
}
def __init__(self, **kwargs):
Arc.__init__(self, 2*np.pi, **kwargs)
def surround(self, mobject, dim_to_match = 0, stretch = False, buffer_factor = 1.2):
def __init__(self, **kwargs):
Arc.__init__(self, 2 * np.pi, **kwargs)
def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2):
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
@@ -174,54 +177,60 @@ class Circle(Arc):
# TODO: Figure out and fix
self.replace(mobject, dim_to_match, stretch)
self.scale_to_fit_width(np.sqrt(mobject.get_width()**2 + mobject.get_height()**2))
self.scale_to_fit_width(
np.sqrt(mobject.get_width()**2 + mobject.get_height()**2))
self.scale(buffer_factor)
class Dot(Circle):
CONFIG = {
"radius" : 0.08,
"stroke_width" : 0,
"fill_opacity" : 1.0,
"color" : WHITE
"radius": 0.08,
"stroke_width": 0,
"fill_opacity": 1.0,
"color": WHITE
}
def __init__(self, point = ORIGIN, **kwargs):
def __init__(self, point=ORIGIN, **kwargs):
Circle.__init__(self, **kwargs)
self.shift(point)
self.init_colors()
class Ellipse(VMobject):
CONFIG = {
"width" : 2,
"height" : 1
"width": 2,
"height": 1
}
def generate_points(self):
circle = Circle(radius = 1)
circle = Circle(radius=1)
circle = circle.stretch_to_fit_width(self.width)
circle = circle.stretch_to_fit_height(self.height)
self.points = circle.points
class AnnularSector(VMobject):
CONFIG = {
"inner_radius" : 1,
"outer_radius" : 2,
"angle" : TAU/4,
"start_angle" : 0,
"fill_opacity" : 1,
"stroke_width" : 0,
"color" : WHITE,
"mark_paths_closed" : True,
"inner_radius": 1,
"outer_radius": 2,
"angle": TAU / 4,
"start_angle": 0,
"fill_opacity": 1,
"stroke_width": 0,
"color": WHITE,
"mark_paths_closed": True,
}
def generate_points(self):
arc1 = Arc(
angle = self.angle,
start_angle = self.start_angle,
radius = self.inner_radius,
angle=self.angle,
start_angle=self.start_angle,
radius=self.inner_radius,
)
arc2 = Arc(
angle = -1*self.angle,
start_angle = self.start_angle+self.angle,
radius = self.outer_radius,
angle=-1 * self.angle,
start_angle=self.start_angle + self.angle,
radius=self.outer_radius,
)
a1_to_a2_points = np.array([
interpolate(arc1.points[-1], arc2.points[0], alpha)
@@ -236,24 +245,24 @@ class AnnularSector(VMobject):
self.add_control_points(arc2.points[1:])
self.add_control_points(a2_to_a1_points[1:])
def get_arc_center(self):
first_point = self.points[0]
last_point = self.points[-2]
v = last_point - first_point
radial_unit_vector = v/np.linalg.norm(v)
radial_unit_vector = v / np.linalg.norm(v)
arc_center = first_point - self.inner_radius * radial_unit_vector
return arc_center
def move_arc_center_to(self,point):
def move_arc_center_to(self, point):
v = point - self.get_arc_center()
self.shift(v)
return self
class Sector(AnnularSector):
CONFIG = {
"outer_radius" : 1,
"inner_radius" : 0
"outer_radius": 1,
"inner_radius": 0
}
@property
@@ -261,35 +270,38 @@ class Sector(AnnularSector):
return self.outer_radius
@radius.setter
def radius(self,new_radius):
def radius(self, new_radius):
self.outer_radius = new_radius
class Annulus(Circle):
CONFIG = {
"inner_radius": 1,
"outer_radius": 2,
"fill_opacity" : 1,
"stroke_width" : 0,
"color" : WHITE,
"mark_paths_closed" : False,
"propagate_style_to_family" : True
"fill_opacity": 1,
"stroke_width": 0,
"color": WHITE,
"mark_paths_closed": False,
"propagate_style_to_family": True
}
def generate_points(self):
self.points = []
self.radius = self.outer_radius
outer_circle = Circle(radius = self.outer_radius)
outer_circle = Circle(radius=self.outer_radius)
inner_circle = Circle(radius=self.inner_radius)
inner_circle.flip()
self.points = outer_circle.points
self.add_subpath(inner_circle.points)
class Line(VMobject):
CONFIG = {
"buff" : 0,
"path_arc" : None, # angle of arc specified here
"n_arc_anchors" : 10, #Only used if path_arc is not None
"buff": 0,
"path_arc": None, # angle of arc specified here
"n_arc_anchors": 10, # Only used if path_arc is not None
}
def __init__(self, start, end, **kwargs):
digest_config(self, kwargs)
self.set_start_and_end(start, end)
@@ -306,13 +318,13 @@ class Line(VMobject):
])
self.account_for_buff()
def set_path_arc(self,new_value):
def set_path_arc(self, new_value):
self.path_arc = new_value
self.generate_points()
def account_for_buff(self):
length = self.get_arc_length()
if length < 2*self.buff or self.buff == 0:
if length < 2 * self.buff or self.buff == 0:
return
buff_proportion = self.buff / length
self.pointwise_become_partial(
@@ -325,7 +337,7 @@ class Line(VMobject):
longer_dim = np.argmax(map(abs, start_to_end))
vect[longer_dim] = start_to_end[longer_dim]
self.start, self.end = [
arg.get_edge_center(unit*vect)
arg.get_edge_center(unit * vect)
if isinstance(arg, Mobject)
else np.array(arg)
for arg, unit in zip([start, end], [1, -1])
@@ -344,7 +356,7 @@ class Line(VMobject):
if self.path_arc:
anchors = self.get_anchors()
return sum([
np.linalg.norm(a2-a1)
np.linalg.norm(a2 - a1)
for a1, a2 in zip(anchors, anchors[1:])
])
else:
@@ -368,11 +380,11 @@ class Line(VMobject):
float(end[i] - start[i])
for i in [1, 0]
]
return np.inf if run == 0 else rise/run
return np.inf if run == 0 else rise / run
def get_angle(self):
start, end = self.get_start_and_end()
return angle_of_vector(end-start)
return angle_of_vector(end - start)
# def put_start_and_end_on(self, new_start, new_end):
# self.set_start_and_end(new_start, new_end)
@@ -396,7 +408,7 @@ class Line(VMobject):
target_norm = np.linalg.norm(target_vect)
if target_norm == 0:
epsilon = 0.001
self.scale(epsilon/curr_norm)
self.scale(epsilon / curr_norm)
self.move_to(new_start)
return
unit_target = target_vect / target_norm
@@ -410,7 +422,7 @@ class Line(VMobject):
angle_diff = np.arccos(
np.clip(np.dot(unit_target, unit_curr), -1, 1)
)
self.scale(target_norm/curr_norm)
self.scale(target_norm / curr_norm)
self.rotate(-angle_diff, normal)
self.shift(new_start - self.get_start())
return self
@@ -418,7 +430,7 @@ class Line(VMobject):
def insert_n_anchor_points(self, n):
if not self.path_arc:
n_anchors = self.get_num_anchor_points()
new_num_points = 3*(n_anchors + n)-2
new_num_points = 3 * (n_anchors + n) - 2
self.points = np.array([
self.point_from_proportion(alpha)
for alpha in np.linspace(0, 1, new_num_points)
@@ -426,17 +438,19 @@ class Line(VMobject):
else:
VMobject.insert_n_anchor_points(self, n)
class DashedLine(Line):
CONFIG = {
"dashed_segment_length" : 0.05
"dashed_segment_length": 0.05
}
def __init__(self, *args, **kwargs):
self.init_kwargs = kwargs
Line.__init__(self, *args, **kwargs)
def generate_points(self):
length = np.linalg.norm(self.end-self.start)
num_interp_points = int(length/self.dashed_segment_length)
length = np.linalg.norm(self.end - self.start)
num_interp_points = int(length / self.dashed_segment_length)
points = [
interpolate(self.start, self.end, alpha)
for alpha in np.linspace(0, 1, num_interp_points)
@@ -462,23 +476,25 @@ class DashedLine(Line):
else:
return self.end
class Arrow(Line):
CONFIG = {
"tip_length" : 0.25,
"tip_width_to_length_ratio" : 1,
"max_tip_length_to_length_ratio" : 0.35,
"max_stem_width_to_tip_width_ratio" : 0.3,
"buff" : MED_SMALL_BUFF,
"propagate_style_to_family" : False,
"preserve_tip_size_when_scaling" : True,
"normal_vector" : OUT,
"use_rectangular_stem" : True,
"rectangular_stem_width" : 0.05,
"tip_length": 0.25,
"tip_width_to_length_ratio": 1,
"max_tip_length_to_length_ratio": 0.35,
"max_stem_width_to_tip_width_ratio": 0.3,
"buff": MED_SMALL_BUFF,
"propagate_style_to_family": False,
"preserve_tip_size_when_scaling": True,
"normal_vector": OUT,
"use_rectangular_stem": True,
"rectangular_stem_width": 0.05,
}
def __init__(self, *args, **kwargs):
points = map(self.pointify, args)
if len(args) == 1:
args = (points[0]+UP+LEFT, points[0])
args = (points[0] + UP + LEFT, points[0])
Line.__init__(self, *args, **kwargs)
self.init_tip()
if self.use_rectangular_stem and not hasattr(self, "rect"):
@@ -488,17 +504,17 @@ class Arrow(Line):
def init_tip(self):
self.add_tip()
def add_tip(self, add_at_end = True):
def add_tip(self, add_at_end=True):
tip = VMobject(
close_new_points = True,
mark_paths_closed = True,
fill_color = self.color,
fill_opacity = 1,
stroke_color = self.color,
stroke_width = 0,
close_new_points=True,
mark_paths_closed=True,
fill_color=self.color,
fill_opacity=1,
stroke_color=self.color,
stroke_width=0,
)
tip.add_at_end = add_at_end
self.set_tip_points(tip, add_at_end, preserve_normal = False)
self.set_tip_points(tip, add_at_end, preserve_normal=False)
self.add(tip)
if not hasattr(self, 'tip'):
self.tip = VGroup()
@@ -508,17 +524,16 @@ class Arrow(Line):
def add_rectangular_stem(self):
self.rect = Rectangle(
stroke_width = 0,
fill_color = self.tip.get_fill_color(),
fill_opacity = self.tip.get_fill_opacity()
stroke_width=0,
fill_color=self.tip.get_fill_color(),
fill_opacity=self.tip.get_fill_opacity()
)
self.add_to_back(self.rect)
self.set_stroke(width = 0)
self.set_stroke(width=0)
self.set_rectangular_stem_points()
def set_rectangular_stem_points(self):
start, end = self.get_start_and_end()
vect = end - start
tip_base_points = self.tip[0].get_anchors()[1:]
tip_base = center_of_mass(tip_base_points)
tbp1, tbp2 = tip_base_points
@@ -528,35 +543,35 @@ class Arrow(Line):
perp_vect /= tip_base_width
width = min(
self.rectangular_stem_width,
self.max_stem_width_to_tip_width_ratio*tip_base_width,
self.max_stem_width_to_tip_width_ratio * tip_base_width,
)
if hasattr(self, "second_tip"):
start = center_of_mass(
self.second_tip.get_anchors()[1:]
)
self.rect.set_points_as_corners([
tip_base + perp_vect*width/2,
start + perp_vect*width/2,
start - perp_vect*width/2,
tip_base - perp_vect*width/2,
tip_base + perp_vect * width / 2,
start + perp_vect * width / 2,
start - perp_vect * width / 2,
tip_base - perp_vect * width / 2,
])
return self
def set_tip_points(
self, tip,
add_at_end = True,
tip_length = None,
preserve_normal = True,
):
add_at_end=True,
tip_length=None,
preserve_normal=True,
):
if tip_length is None:
tip_length = self.tip_length
if preserve_normal:
normal_vector = self.get_normal_vector()
else:
normal_vector = self.normal_vector
line_length = np.linalg.norm(self.points[-1]-self.points[0])
line_length = np.linalg.norm(self.points[-1] - self.points[0])
tip_length = min(
tip_length, self.max_tip_length_to_length_ratio*line_length
tip_length, self.max_tip_length_to_length_ratio * line_length
)
indices = (-2, -1) if add_at_end else (1, 0)
@@ -569,12 +584,12 @@ class Arrow(Line):
for v in vect, perp_vect:
if np.linalg.norm(v) == 0:
v[0] = 1
v *= tip_length/np.linalg.norm(v)
v *= tip_length / np.linalg.norm(v)
ratio = self.tip_width_to_length_ratio
tip.set_points_as_corners([
end_point,
end_point-vect+perp_vect*ratio/2,
end_point-vect-perp_vect*ratio/2,
end_point - vect + perp_vect * ratio / 2,
end_point - vect - perp_vect * ratio / 2,
])
return self
@@ -586,7 +601,7 @@ class Arrow(Line):
if norm == 0:
return self.normal_vector
else:
return result/norm
return result / norm
def reset_normal_vector(self):
self.normal_vector = self.get_normal_vector()
@@ -603,7 +618,7 @@ class Arrow(Line):
def put_start_and_end_on(self, *args, **kwargs):
Line.put_start_and_end_on(self, *args, **kwargs)
self.set_tip_points(self.tip[0], preserve_normal = False)
self.set_tip_points(self.tip[0], preserve_normal=False)
self.set_rectangular_stem_points()
return self
@@ -619,92 +634,106 @@ class Arrow(Line):
def copy(self):
return self.deepcopy()
class Vector(Arrow):
CONFIG = {
"color" : YELLOW,
"buff" : 0,
"color": YELLOW,
"buff": 0,
}
def __init__(self, direction, **kwargs):
if len(direction) == 2:
direction = np.append(np.array(direction), 0)
Arrow.__init__(self, ORIGIN, direction, **kwargs)
class DoubleArrow(Arrow):
def init_tip(self):
self.tip = VGroup()
for b in True, False:
t = self.add_tip(add_at_end = b)
t = self.add_tip(add_at_end=b)
t.add_at_end = b
self.tip.add(t)
self.tip.match_style(self.tip[0])
class CubicBezier(VMobject):
def __init__(self, points, **kwargs):
VMobject.__init__(self, **kwargs)
self.set_points(points)
class Polygon(VMobject):
CONFIG = {
"color" : GREEN_D,
"mark_paths_closed" : True,
"close_new_points" : True,
"color": GREEN_D,
"mark_paths_closed": True,
"close_new_points": True,
}
def __init__(self, *vertices, **kwargs):
assert len(vertices) > 1
digest_locals(self)
VMobject.__init__(self, **kwargs)
def generate_points(self):
self.set_anchor_points(self.vertices, mode = "corners")
self.set_anchor_points(self.vertices, mode="corners")
def get_vertices(self):
return self.get_anchors_and_handles()[0]
class RegularPolygon(Polygon):
CONFIG = {
"start_angle" : 0
"start_angle": 0
}
def __init__(self, n = 3, **kwargs):
def __init__(self, n=3, **kwargs):
digest_config(self, kwargs, locals())
start_vect = rotate_vector(RIGHT, self.start_angle)
vertices = compass_directions(n, start_vect)
Polygon.__init__(self, *vertices, **kwargs)
class Rectangle(VMobject):
CONFIG = {
"color" : WHITE,
"height" : 2.0,
"width" : 4.0,
"mark_paths_closed" : True,
"close_new_points" : True,
"color": WHITE,
"height": 2.0,
"width": 4.0,
"mark_paths_closed": True,
"close_new_points": True,
}
def generate_points(self):
y, x = self.height/2., self.width/2.
y, x = self.height / 2., self.width / 2.
self.set_anchor_points([
x*LEFT+y*UP,
x*RIGHT+y*UP,
x*RIGHT+y*DOWN,
x*LEFT+y*DOWN
], mode = "corners")
x * LEFT + y * UP,
x * RIGHT + y * UP,
x * RIGHT + y * DOWN,
x * LEFT + y * DOWN
], mode="corners")
class Square(Rectangle):
CONFIG = {
"side_length" : 2.0,
"side_length": 2.0,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
Rectangle.__init__(
self,
height = self.side_length,
width = self.side_length,
self,
height=self.side_length,
width=self.side_length,
**kwargs
)
class Grid(VMobject):
CONFIG = {
"height" : 6.0,
"width" : 6.0,
"height": 6.0,
"width": 6.0,
}
def __init__(self, rows, columns, **kwargs):
digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs)
@@ -713,15 +742,13 @@ class Grid(VMobject):
x_step = self.width / self.columns
y_step = self.height / self.rows
for x in np.arange(0, self.width+x_step, x_step):
for x in np.arange(0, self.width + x_step, x_step):
self.add(Line(
[x-self.width/2., -self.height/2., 0],
[x-self.width/2., self.height/2., 0],
[x - self.width / 2., -self.height / 2., 0],
[x - self.width / 2., self.height / 2., 0],
))
for y in np.arange(0, self.height+y_step, y_step):
for y in np.arange(0, self.height + y_step, y_step):
self.add(Line(
[-self.width/2., y-self.height/2., 0],
[self.width/2., y-self.height/2., 0]
[-self.width / 2., y - self.height / 2., 0],
[self.width / 2., y - self.height / 2., 0]
))

View File

@@ -4,7 +4,6 @@ import numpy as np
import operator as op
import os
from PIL import Image
from colour import Color
from constants import *
@@ -16,28 +15,28 @@ from utils.color import interpolate_color
from utils.iterables import list_update
from utils.iterables import remove_list_redundancies
from utils.paths import straight_path
from utils.space_ops import R3_to_complex
from utils.space_ops import angle_of_vector
from utils.space_ops import complex_to_R3
from utils.space_ops import rotation_matrix
#TODO: Explain array_attrs
# TODO: Explain array_attrs
class Mobject(Container):
"""
Mathematical Object
"""
CONFIG = {
"color" : WHITE,
"stroke_width" : DEFAULT_POINT_THICKNESS,
"name" : None,
"dim" : 3,
"target" : None,
"color": WHITE,
"stroke_width": DEFAULT_POINT_THICKNESS,
"name": None,
"dim": 3,
"target": None,
}
def __init__(self, *submobjects, **kwargs):
Container.__init__(self, *submobjects, **kwargs)
if not all(map(lambda m : isinstance(m, Mobject), submobjects)):
if not all(map(lambda m: isinstance(m, Mobject), submobjects)):
raise Exception("All submobjects must be of type Mobject")
self.submobjects = list(submobjects)
self.color = Color(self.color)
@@ -54,11 +53,11 @@ class Mobject(Container):
self.points = np.zeros((0, self.dim))
def init_colors(self):
#For subclasses
# For subclasses
pass
def generate_points(self):
#Typically implemented in subclass, unless purposefully left blank
# Typically implemented in subclass, unless purposefully left blank
pass
def add(self, *mobjects):
@@ -87,7 +86,7 @@ class Mobject(Container):
in the submobjects list.
"""
mobject_attrs = filter(
lambda x : isinstance(x, Mobject),
lambda x: isinstance(x, Mobject),
self.__dict__.values()
)
self.submobjects = list_update(self.submobjects, mobject_attrs)
@@ -98,24 +97,24 @@ class Mobject(Container):
setattr(self, attr, func(getattr(self, attr)))
return self
def get_image(self, camera = None):
def get_image(self, camera=None):
if camera is None:
from camera.camera import Camera
camera = Camera()
camera.capture_mobject(self)
return camera.get_image()
def show(self, camera = None):
self.get_image(camera = camera).show()
def show(self, camera=None):
self.get_image(camera=camera).show()
def save_image(self, name = None):
def save_image(self, name=None):
self.get_image().save(
os.path.join(ANIMATIONS_DIR, (name or str(self)) + ".png")
)
def copy(self):
#TODO, either justify reason for shallow copy, or
#remove this redundancy everywhere
# TODO, either justify reason for shallow copy, or
# remove this redundancy everywhere
return self.deepcopy()
copy_mobject = copy.copy(self)
@@ -132,8 +131,8 @@ class Mobject(Container):
def deepcopy(self):
return copy.deepcopy(self)
def generate_target(self, use_deepcopy = False):
self.target = None #Prevent exponential explosion
def generate_target(self, use_deepcopy=False):
self.target = None # Prevent exponential explosion
if use_deepcopy:
self.target = self.deepcopy()
else:
@@ -149,8 +148,8 @@ class Mobject(Container):
def shift(self, *vectors):
total_vector = reduce(op.add, vectors)
for mob in self.family_members_with_points():
mob.points = mob.points.astype('float')
mob.points += total_vector
mob.points = mob.points.astype('float')
mob.points += total_vector
return self
def scale(self, scale_factor, **kwargs):
@@ -164,61 +163,61 @@ class Mobject(Container):
respect to that point.
"""
self.apply_points_function_about_point(
lambda points : scale_factor*points, **kwargs
lambda points: scale_factor * points, **kwargs
)
return self
def rotate_about_origin(self, angle, axis = OUT, axes = []):
return self.rotate(angle, axis, about_point = ORIGIN)
def rotate_about_origin(self, angle, axis=OUT, axes=[]):
return self.rotate(angle, axis, about_point=ORIGIN)
def rotate(self, angle, axis = OUT, **kwargs):
def rotate(self, angle, axis=OUT, **kwargs):
rot_matrix = rotation_matrix(angle, axis)
self.apply_points_function_about_point(
lambda points : np.dot(points, rot_matrix.T),
lambda points: np.dot(points, rot_matrix.T),
**kwargs
)
return self
def flip(self, axis = UP, **kwargs):
return self.rotate(TAU/2, axis, **kwargs)
def flip(self, axis=UP, **kwargs):
return self.rotate(TAU / 2, axis, **kwargs)
def stretch(self, factor, dim, **kwargs):
def func(points):
points[:,dim] *= factor
points[:, dim] *= factor
return points
self.apply_points_function_about_point(func, **kwargs)
return self
def apply_function(self, function, **kwargs):
#Default to applying matrix about the origin, not mobjects center
# Default to applying matrix about the origin, not mobjects center
if len(kwargs) == 0:
kwargs["about_point"] = ORIGIN
self.apply_points_function_about_point(
lambda points : np.apply_along_axis(function, 1, points),
lambda points: np.apply_along_axis(function, 1, points),
**kwargs
)
return self
def apply_matrix(self, matrix, **kwargs):
#Default to applying matrix about the origin, not mobjects center
# Default to applying matrix about the origin, not mobjects center
if len(kwargs) == 0:
kwargs["about_point"] = ORIGIN
full_matrix = np.identity(self.dim)
matrix = np.array(matrix)
full_matrix[:matrix.shape[0],:matrix.shape[1]] = matrix
full_matrix[:matrix.shape[0], :matrix.shape[1]] = matrix
self.apply_points_function_about_point(
lambda points : np.dot(points, full_matrix.T),
lambda points: np.dot(points, full_matrix.T),
**kwargs
)
return self
def apply_complex_function(self, function, **kwargs):
return self.apply_function(
lambda (x, y, z) : complex_to_R3(function(complex(x, y))),
lambda (x, y, z): complex_to_R3(function(complex(x, y))),
**kwargs
)
def wag(self, direction = RIGHT, axis = DOWN, wag_factor = 1.0):
def wag(self, direction=RIGHT, axis=DOWN, wag_factor=1.0):
for mob in self.family_members_with_points():
alphas = np.dot(mob.points, np.transpose(axis))
alphas -= min(alphas)
@@ -233,7 +232,7 @@ class Mobject(Container):
def reverse_points(self):
for mob in self.family_members_with_points():
mob.apply_over_attr_arrays(
lambda arr : np.array(list(reversed(arr)))
lambda arr: np.array(list(reversed(arr)))
)
return self
@@ -243,18 +242,18 @@ class Mobject(Container):
"""
def repeat_array(array):
return reduce(
lambda a1, a2 : np.append(a1, a2, axis = 0),
[array]*count
lambda a1, a2: np.append(a1, a2, axis=0),
[array] * count
)
for mob in self.family_members_with_points():
mob.apply_over_attr_arrays(repeat_array)
return self
#### In place operations ######
#Note, much of these are now redundant with default behavior of
#above methods
# Note, much of these are now redundant with default behavior of
# above methods
def apply_points_function_about_point(self, func, about_point = None, about_edge = ORIGIN):
def apply_points_function_about_point(self, func, about_point=None, about_edge=ORIGIN):
if about_point is None:
about_point = self.get_critical_point(about_edge)
for mob in self.family_members_with_points():
@@ -263,20 +262,20 @@ class Mobject(Container):
mob.points += about_point
return self
def rotate_in_place(self, angle, axis = OUT):
def rotate_in_place(self, angle, axis=OUT):
# redundant with default behavior of rotate now.
return self.rotate(angle, axis = axis)
return self.rotate(angle, axis=axis)
def scale_in_place(self, scale_factor, **kwargs):
#Redundant with default behavior of scale now.
# Redundant with default behavior of scale now.
return self.scale(scale_factor, **kwargs)
def scale_about_point(self, scale_factor, point):
#Redundant with default behavior of scale now.
return self.scale(scale_factor, about_point = point)
# Redundant with default behavior of scale now.
return self.scale(scale_factor, about_point=point)
def pose_at_angle(self, **kwargs):
self.rotate(TAU/14, RIGHT+UP, **kwargs)
self.rotate(TAU / 14, RIGHT + UP, **kwargs)
return self
#### Positioning methods ####
@@ -285,7 +284,7 @@ class Mobject(Container):
self.shift(-self.get_center())
return self
def align_on_border(self, direction, buff = DEFAULT_MOBJECT_TO_EDGE_BUFFER):
def align_on_border(self, direction, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
"""
Direction just needs to be a vector pointing towards side or
corner in the 2d plane.
@@ -297,19 +296,19 @@ class Mobject(Container):
self.shift(shift_val)
return self
def to_corner(self, corner = LEFT+DOWN, buff = DEFAULT_MOBJECT_TO_EDGE_BUFFER):
def to_corner(self, corner=LEFT + DOWN, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
return self.align_on_border(corner, buff)
def to_edge(self, edge = LEFT, buff = DEFAULT_MOBJECT_TO_EDGE_BUFFER):
def to_edge(self, edge=LEFT, buff=DEFAULT_MOBJECT_TO_EDGE_BUFFER):
return self.align_on_border(edge, buff)
def next_to(self, mobject_or_point,
direction = RIGHT,
buff = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
aligned_edge = ORIGIN,
submobject_to_align = None,
index_of_submobject_to_align = None,
coor_mask = np.array([1,1,1]),
direction=RIGHT,
buff=DEFAULT_MOBJECT_TO_MOBJECT_BUFFER,
aligned_edge=ORIGIN,
submobject_to_align=None,
index_of_submobject_to_align=None,
coor_mask=np.array([1, 1, 1]),
):
if isinstance(mobject_or_point, Mobject):
mob = mobject_or_point
@@ -329,10 +328,11 @@ class Mobject(Container):
else:
aligner = self
point_to_align = aligner.get_critical_point(aligned_edge - direction)
self.shift((target_point - point_to_align + buff*direction)*coor_mask)
self.shift((target_point - point_to_align +
buff * direction) * coor_mask)
return self
def align_to(self, mobject_or_point, direction = ORIGIN, alignment_vect = UP):
def align_to(self, mobject_or_point, direction=ORIGIN, alignment_vect=UP):
"""
Examples:
mob1.align_to(mob2, UP) moves mob1 vertically so that its
@@ -349,12 +349,12 @@ class Mobject(Container):
target_point = mobject_or_point
direction_norm = np.linalg.norm(direction)
if direction_norm > 0:
alignment_vect = np.array(direction)/direction_norm
alignment_vect = np.array(direction) / direction_norm
reference_point = self.get_critical_point(direction)
else:
reference_point = self.get_center()
diff = target_point - reference_point
self.shift(alignment_vect*np.dot(diff, alignment_vect))
self.shift(alignment_vect * np.dot(diff, alignment_vect))
return self
def shift_onto_screen(self, **kwargs):
@@ -380,57 +380,57 @@ class Mobject(Container):
return False
def stretch_about_point(self, factor, dim, point):
return self.stretch(factor, dim, about_point = point)
return self.stretch(factor, dim, about_point=point)
def stretch_in_place(self, factor, dim):
#Now redundant with stretch
# Now redundant with stretch
return self.stretch(factor, dim)
def rescale_to_fit(self, length, dim, stretch = False, **kwargs):
def rescale_to_fit(self, length, dim, stretch=False, **kwargs):
old_length = self.length_over_dim(dim)
if old_length == 0:
return self
if stretch:
self.stretch(length/old_length, dim, **kwargs)
self.stretch(length / old_length, dim, **kwargs)
else:
self.scale(length/old_length, **kwargs)
self.scale(length / old_length, **kwargs)
return self
def stretch_to_fit_width(self, width, **kwargs):
return self.rescale_to_fit(width, 0, stretch = True, **kwargs)
return self.rescale_to_fit(width, 0, stretch=True, **kwargs)
def stretch_to_fit_height(self, height, **kwargs):
return self.rescale_to_fit(height, 1, stretch = True, **kwargs)
return self.rescale_to_fit(height, 1, stretch=True, **kwargs)
def stretch_to_fit_depth(self, depth, **kwargs):
return self.rescale_to_fit(depth, 1, stretch = True, **kwargs)
return self.rescale_to_fit(depth, 1, stretch=True, **kwargs)
def scale_to_fit_width(self, width, **kwargs):
return self.rescale_to_fit(width, 0, stretch = False, **kwargs)
return self.rescale_to_fit(width, 0, stretch=False, **kwargs)
def scale_to_fit_height(self, height, **kwargs):
return self.rescale_to_fit(height, 1, stretch = False, **kwargs)
return self.rescale_to_fit(height, 1, stretch=False, **kwargs)
def scale_to_fit_depth(self, depth, **kwargs):
return self.rescale_to_fit(depth, 2, stretch = False, **kwargs)
return self.rescale_to_fit(depth, 2, stretch=False, **kwargs)
def space_out_submobjects(self, factor = 1.5, **kwargs):
def space_out_submobjects(self, factor=1.5, **kwargs):
self.scale(factor, **kwargs)
for submob in self.submobjects:
submob.scale(1./factor)
submob.scale(1. / factor)
return self
def move_to(self, point_or_mobject, aligned_edge = ORIGIN,
coor_mask = np.array([1,1,1])):
def move_to(self, point_or_mobject, aligned_edge=ORIGIN,
coor_mask=np.array([1, 1, 1])):
if isinstance(point_or_mobject, Mobject):
target = point_or_mobject.get_critical_point(aligned_edge)
else:
target = point_or_mobject
point_to_align = self.get_critical_point(aligned_edge)
self.shift((target - point_to_align)*coor_mask)
self.shift((target - point_to_align) * coor_mask)
return self
def replace(self, mobject, dim_to_match = 0, stretch = False):
def replace(self, mobject, dim_to_match=0, stretch=False):
if not mobject.get_num_points() and not mobject.submobjects:
raise Warning("Attempting to replace mobject with no points")
return self
@@ -441,12 +441,12 @@ class Mobject(Container):
self.rescale_to_fit(
mobject.length_over_dim(dim_to_match),
dim_to_match,
stretch = False
stretch=False
)
self.shift(mobject.get_center() - self.get_center())
return self
def surround(self, mobject, dim_to_match = 0, stretch = False, buffer_factor = 1.2):
def surround(self, mobject, dim_to_match=0, stretch=False, buffer_factor=1.2):
self.replace(mobject, dim_to_match, stretch)
self.scale_in_place(buffer_factor)
@@ -455,15 +455,15 @@ class Mobject(Container):
if np.all(curr_vect == 0):
raise Exception("Cannot position endpoints of closed loop")
target_vect = end - start
self.scale(np.linalg.norm(target_vect)/np.linalg.norm(curr_vect))
self.scale(np.linalg.norm(target_vect) / np.linalg.norm(curr_vect))
self.rotate(
angle_of_vector(target_vect) - \
angle_of_vector(target_vect) -
angle_of_vector(curr_vect)
)
self.shift(start-self.points[0])
self.shift(start - self.points[0])
return self
## Match other mobvject properties
# Match other mobvject properties
def match_color(self, mobject):
return self.set_color(mobject.get_color())
@@ -483,9 +483,9 @@ class Mobject(Container):
def match_depth(self, mobject, **kwargs):
return self.match_dim(mobject, 2, **kwargs)
## Color functions
# Color functions
def set_color(self, color = YELLOW_C, family = True):
def set_color(self, color=YELLOW_C, family=True):
"""
Condition is function which takes in one arguments, (x, y, z).
Here it just recurses to submobjects, but in subclasses this
@@ -494,7 +494,7 @@ class Mobject(Container):
"""
if family:
for submob in self.submobjects:
submob.set_color(color, family = family)
submob.set_color(color, family=family)
self.color = color
return self
@@ -502,8 +502,9 @@ class Mobject(Container):
self.set_submobject_colors_by_gradient(*colors)
return self
def set_colors_by_radial_gradient(self, center = None, radius = 1, inner_color = WHITE, outer_color = BLACK):
self.set_submobject_colors_by_radial_gradient(center, radius, inner_color, outer_color)
def set_colors_by_radial_gradient(self, center=None, radius=1, inner_color=WHITE, outer_color=BLACK):
self.set_submobject_colors_by_radial_gradient(
center, radius, inner_color, outer_color)
return self
def set_submobject_colors_by_gradient(self, *colors):
@@ -516,19 +517,19 @@ class Mobject(Container):
new_colors = color_gradient(colors, len(mobs))
for mob, color in zip(mobs, new_colors):
mob.set_color(color, family = False)
mob.set_color(color, family=False)
return self
def set_submobject_colors_by_radial_gradient(self, center = None, radius = 1, inner_color = WHITE, outer_color = BLACK):
def set_submobject_colors_by_radial_gradient(self, center=None, radius=1, inner_color=WHITE, outer_color=BLACK):
mobs = self.family_members_with_points()
if center == None:
center = self.get_center()
for mob in self.family_members_with_points():
t = np.linalg.norm(mob.get_center() - center)/radius
t = min(t,1)
t = np.linalg.norm(mob.get_center() - center) / radius
t = min(t, 1)
mob_color = interpolate_color(inner_color, outer_color, t)
mob.set_color(mob_color, family = False)
mob.set_color(mob_color, family=False)
return self
@@ -538,7 +539,7 @@ class Mobject(Container):
# Some objects (e.g., VMobjects) have special fading
# behavior. We let every object handle its individual
# fading via fade_no_recurse (notionally a purely internal method),
# fading via fade_no_recurse (notionally a purely internal method),
# and then have fade() itself call this recursively on each submobject
#
# Similarly for fade_to_no_recurse and fade_to, the underlying functions
@@ -549,7 +550,7 @@ class Mobject(Container):
start = color_to_rgb(self.get_color())
end = color_to_rgb(color)
new_rgb = interpolate(start, end, alpha)
self.set_color(Color(rgb = new_rgb), family = False)
self.set_color(Color(rgb=new_rgb), family=False)
return self
def fade_to(self, color, alpha):
@@ -561,7 +562,7 @@ class Mobject(Container):
self.fade_to_no_recurse(BLACK, darkness)
return self
def fade(self, darkness = 0.5):
def fade(self, darkness=0.5):
for submob in self.submobject_family():
submob.fade_no_recurse(darkness)
return self
@@ -570,9 +571,9 @@ class Mobject(Container):
return self.color
##
def save_state(self, use_deepcopy = False):
def save_state(self, use_deepcopy=False):
if hasattr(self, "saved_state"):
#Prevent exponential growth of data
# Prevent exponential growth of data
self.saved_state = None
if use_deepcopy:
self.saved_state = self.deepcopy()
@@ -634,7 +635,7 @@ class Mobject(Container):
max_point = self.reduce_across_dimension(np.max, np.max, dim)
if direction[dim] == 0:
result[dim] = (max_point+min_point)/2
result[dim] = (max_point + min_point) / 2
elif direction[dim] < 0:
result[dim] = min_point
else:
@@ -694,8 +695,7 @@ class Mobject(Container):
def point_from_proportion(self, alpha):
raise Exception("Not implemented")
## Family matters
# Family matters
def __getitem__(self, value):
self_list = self.split()
@@ -724,22 +724,22 @@ class Mobject(Container):
def family_members_with_points(self):
return filter(
lambda m : m.get_num_points() > 0,
lambda m: m.get_num_points() > 0,
self.submobject_family()
)
def arrange_submobjects(self, direction = RIGHT, center = True, **kwargs):
def arrange_submobjects(self, direction=RIGHT, center=True, **kwargs):
for m1, m2 in zip(self.submobjects, self.submobjects[1:]):
m2.next_to(m1, direction, **kwargs)
if center:
self.center()
return self
def arrange_submobjects_in_grid(self, n_rows = None, n_cols = None, **kwargs):
def arrange_submobjects_in_grid(self, n_rows=None, n_cols=None, **kwargs):
submobs = self.submobjects
if n_rows is None and n_cols is None:
n_cols = int(np.sqrt(len(submobs)))
if n_rows is not None:
v1 = RIGHT
v2 = DOWN
@@ -749,35 +749,35 @@ class Mobject(Container):
v2 = RIGHT
n = len(submobs) / n_cols
Group(*[
Group(*submobs[i:i+n]).arrange_submobjects(v1, **kwargs)
Group(*submobs[i:i + n]).arrange_submobjects(v1, **kwargs)
for i in range(0, len(submobs), n)
]).arrange_submobjects(v2, **kwargs)
return self
def sort_submobjects(self, point_to_num_func = lambda p : p[0]):
def sort_submobjects(self, point_to_num_func=lambda p: p[0]):
self.submobjects.sort(
lambda *mobs : cmp(*[
lambda *mobs: cmp(*[
point_to_num_func(mob.get_center())
for mob in mobs
])
)
return self
def print_submobject_family(self, n_tabs = 0):
def print_submobject_family(self, n_tabs=0):
"""For debugging purposes"""
print "\t"*n_tabs, self, id(self)
print "\t" * n_tabs, self, id(self)
for submob in self.submobjects:
submob.print_mobject_family(n_tabs + 1)
## Alignment
# Alignment
def align_data(self, mobject):
self.align_submobjects(mobject)
self.align_points(mobject)
#Recurse
# Recurse
for m1, m2 in zip(self.submobjects, mobject.submobjects):
m1.align_data(m2)
def get_point_mobject(self, center = None):
def get_point_mobject(self, center=None):
"""
The simplest mobject to be transformed to or from self.
Should by a point of the appropriate type
@@ -797,8 +797,8 @@ class Mobject(Container):
raise Exception("Not implemented")
def align_submobjects(self, mobject):
#If one is empty, and the other is not,
#push it into its submobject list
# If one is empty, and the other is not,
# push it into its submobject list
self_has_points, mob_has_points = [
mob.get_num_points() > 0
for mob in self, mobject
@@ -809,7 +809,7 @@ class Mobject(Container):
self.null_point_align(mobject)
self_count = len(self.submobjects)
mob_count = len(mobject.submobjects)
diff = self_count-mob_count
diff = self_count - mob_count
if diff < 0:
self.add_n_more_submobjects(-diff)
elif diff > 0:
@@ -840,7 +840,7 @@ class Mobject(Container):
self.add(self.copy())
n -= 1
curr += 1
indices = curr*np.arange(curr+n)/(curr+n)
indices = curr * np.arange(curr + n) / (curr + n)
new_submobjects = []
for index in indices:
submob = self.submobjects[index]
@@ -854,7 +854,7 @@ class Mobject(Container):
return submob.copy()
def interpolate(self, mobject1, mobject2,
alpha, path_func = straight_path):
alpha, path_func=straight_path):
"""
Turns self into an interpolation between mobject1
and mobject2.
@@ -865,7 +865,7 @@ class Mobject(Container):
self.interpolate_color(mobject1, mobject2, alpha)
def interpolate_color(self, mobject1, mobject2, alpha):
pass #To implement in subclass
pass # To implement in subclass
def become_partial(self, mobject, a, b):
"""
@@ -874,15 +874,16 @@ class Mobject(Container):
Inputs 0 <= a < b <= 1 determine what portion
of mobject to become.
"""
pass #To implement in subclasses
pass # To implement in subclasses
#TODO, color?
# TODO, color?
def pointwise_become_partial(self, mobject, a, b):
pass #To implement in subclass
pass # To implement in subclass
class Group(Mobject):
#Alternate name to improve readibility in cases where
#the mobject is used primarily for its submobject housing
#functionality.
# Alternate name to improve readibility in cases where
# the mobject is used primarily for its submobject housing
# functionality.
pass

View File

@@ -10,49 +10,50 @@ from mobject.geometry import Line
from utils.bezier import interpolate
from utils.config_ops import digest_config
class NumberLine(VMobject):
CONFIG = {
"color" : BLUE,
"x_min" : -FRAME_X_RADIUS,
"x_max" : FRAME_X_RADIUS,
"unit_size" : 1,
"tick_size" : 0.1,
"tick_frequency" : 1,
"leftmost_tick" : None, #Defaults to value near x_min s.t. 0 is a tick
"numbers_with_elongated_ticks" : [0],
"numbers_to_show" : None,
"longer_tick_multiple" : 2,
"number_at_center" : 0,
"number_scale_val" : 0.75,
"label_direction" : DOWN,
"line_to_number_buff" : MED_SMALL_BUFF,
"include_tip" : False,
"propagate_style_to_family" : True,
"color": BLUE,
"x_min": -FRAME_X_RADIUS,
"x_max": FRAME_X_RADIUS,
"unit_size": 1,
"tick_size": 0.1,
"tick_frequency": 1,
"leftmost_tick": None, # Defaults to value near x_min s.t. 0 is a tick
"numbers_with_elongated_ticks": [0],
"numbers_to_show": None,
"longer_tick_multiple": 2,
"number_at_center": 0,
"number_scale_val": 0.75,
"label_direction": DOWN,
"line_to_number_buff": MED_SMALL_BUFF,
"include_tip": False,
"propagate_style_to_family": True,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
if self.leftmost_tick is None:
tf = self.tick_frequency
self.leftmost_tick = tf*np.ceil(self.x_min/tf)
self.leftmost_tick = tf * np.ceil(self.x_min / tf)
VMobject.__init__(self, **kwargs)
if self.include_tip:
self.add_tip()
def generate_points(self):
self.main_line = Line(self.x_min*RIGHT, self.x_max*RIGHT)
self.main_line = Line(self.x_min * RIGHT, self.x_max * RIGHT)
self.tick_marks = VGroup()
self.add(self.main_line, self.tick_marks)
rounding_value = int(-np.log10(0.1*self.tick_frequency))
rounding_value = int(-np.log10(0.1 * self.tick_frequency))
rounded_numbers_with_elongated_ticks = np.round(
self.numbers_with_elongated_ticks,
self.numbers_with_elongated_ticks,
rounding_value
)
for x in self.get_tick_numbers():
rounded_x = np.round(x, rounding_value)
if rounded_x in rounded_numbers_with_elongated_ticks:
tick_size_used = self.longer_tick_multiple*self.tick_size
tick_size_used = self.longer_tick_multiple * self.tick_size
else:
tick_size_used = self.tick_size
self.add_tick(x, tick_size_used)
@@ -60,13 +61,14 @@ class NumberLine(VMobject):
self.stretch(self.unit_size, 0)
self.shift(-self.number_to_point(self.number_at_center))
def add_tick(self, x, size = None):
def add_tick(self, x, size=None):
self.tick_marks.add(self.get_tick(x, size))
return self
def get_tick(self, x, size = None):
if size is None: size = self.tick_size
result = Line(size*DOWN, size*UP)
def get_tick(self, x, size=None):
if size is None:
size = self.tick_size
result = Line(size * DOWN, size * UP)
result.rotate(self.main_line.get_angle())
result.move_to(self.number_to_point(x))
return result
@@ -77,12 +79,12 @@ class NumberLine(VMobject):
def get_tick_numbers(self):
epsilon = 0.001
return np.arange(
self.leftmost_tick, self.x_max+epsilon,
self.leftmost_tick, self.x_max + epsilon,
self.tick_frequency
)
def number_to_point(self, number):
alpha = float(number-self.x_min)/(self.x_max - self.x_min)
alpha = float(number - self.x_min) / (self.x_max - self.x_min)
return interpolate(
self.main_line.get_start(),
self.main_line.get_end(),
@@ -91,22 +93,23 @@ class NumberLine(VMobject):
def point_to_number(self, point):
left_point, right_point = self.main_line.get_start_and_end()
full_vect = right_point-left_point
full_vect = right_point - left_point
def distance_from_left(p):
return np.dot(p-left_point, full_vect)/np.linalg.norm(full_vect)
return np.dot(p - left_point, full_vect) / np.linalg.norm(full_vect)
return interpolate(
self.x_min, self.x_max,
distance_from_left(point)/distance_from_left(right_point)
self.x_min, self.x_max,
distance_from_left(point) / distance_from_left(right_point)
)
def default_numbers_to_display(self):
if self.numbers_to_show is not None:
return self.numbers_to_show
return np.arange(int(self.leftmost_tick), int(self.x_max)+1)
return np.arange(int(self.leftmost_tick), int(self.x_max) + 1)
def get_number_mobjects(self, *numbers, **kwargs):
#TODO, handle decimals
# TODO, handle decimals
if len(numbers) == 0:
numbers = self.default_numbers_to_display()
if "force_integers" in kwargs and kwargs["force_integers"]:
@@ -135,20 +138,20 @@ class NumberLine(VMobject):
def add_tip(self):
start, end = self.main_line.get_start_and_end()
vect = (end - start)/np.linalg.norm(end-start)
arrow = Arrow(start, end + MED_SMALL_BUFF*vect, buff = 0)
vect = (end - start) / np.linalg.norm(end - start)
arrow = Arrow(start, end + MED_SMALL_BUFF * vect, buff=0)
tip = arrow.tip
tip.set_color(self.color)
self.tip = tip
self.add(tip)
class UnitInterval(NumberLine):
CONFIG = {
"x_min" : 0,
"x_max" : 1,
"unit_size" : 6,
"tick_frequency" : 0.1,
"numbers_with_elongated_ticks" : [0, 1],
"number_at_center" : 0.5,
"x_min": 0,
"x_max": 1,
"unit_size": 6,
"tick_frequency": 0.1,
"numbers_with_elongated_ticks": [0, 1],
"number_at_center": 0.5,
}

View File

@@ -7,29 +7,31 @@ from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from mobject.shape_matchers import BackgroundRectangle
class DecimalNumber(VMobject):
CONFIG = {
"num_decimal_points" : 2,
"digit_to_digit_buff" : 0.05,
"show_ellipsis" : False,
"unit" : None, #Aligned to bottom unless it starts with "^"
"include_background_rectangle" : False,
"num_decimal_points": 2,
"digit_to_digit_buff": 0.05,
"show_ellipsis": False,
"unit": None, # Aligned to bottom unless it starts with "^"
"include_background_rectangle": False,
}
def __init__(self, number, **kwargs):
VMobject.__init__(self, **kwargs)
self.number = number
ndp = self.num_decimal_points
#Build number string
# Build number string
if isinstance(number, complex):
num_string = '%.*f%s%.*fi'%(
ndp, number.real,
num_string = '%.*f%s%.*fi' % (
ndp, number.real,
"-" if number.imag < 0 else "+",
ndp, abs(number.imag)
)
else:
num_string = '%.*f'%(ndp, number)
negative_zero_string = "-%.*f"%(ndp, 0.)
num_string = '%.*f' % (ndp, number)
negative_zero_string = "-%.*f" % (ndp, 0.)
if num_string == negative_zero_string:
num_string = num_string[1:]
self.add(*[
@@ -37,32 +39,31 @@ class DecimalNumber(VMobject):
for char in num_string
])
#Add non-numerical bits
# Add non-numerical bits
if self.show_ellipsis:
self.add(TexMobject("\\dots"))
if num_string.startswith("-"):
minus = self.submobjects[0]
minus.next_to(
self.submobjects[1], LEFT,
buff = self.digit_to_digit_buff
buff=self.digit_to_digit_buff
)
if self.unit != None:
if self.unit is not None:
self.unit_sign = TexMobject(self.unit)
self.add(self.unit_sign)
self.arrange_submobjects(
buff = self.digit_to_digit_buff,
aligned_edge = DOWN
buff=self.digit_to_digit_buff,
aligned_edge=DOWN
)
#Handle alignment of parts that should be aligned
#to the bottom
# Handle alignment of parts that should be aligned
# to the bottom
for i, c in enumerate(num_string):
if c == "-" and len(num_string) > i+1:
self[i].align_to(self[i+1], alignment_vect = UP)
if c == "-" and len(num_string) > i + 1:
self[i].align_to(self[i + 1], alignment_vect=UP)
if self.unit and self.unit.startswith("^"):
self.unit_sign.align_to(self, UP)
#
@@ -70,8 +71,8 @@ class DecimalNumber(VMobject):
self.add_background_rectangle()
def add_background_rectangle(self):
#TODO, is this the best way to handle
#background rectangles?
# TODO, is this the best way to handle
# background rectangles?
self.background_rectangle = BackgroundRectangle(self)
self.submobjects = [
self.background_rectangle,
@@ -79,7 +80,8 @@ class DecimalNumber(VMobject):
]
return self
class Integer(DecimalNumber):
CONFIG = {
"num_decimal_points" : 0,
"num_decimal_points": 0,
}

View File

@@ -15,29 +15,31 @@ from utils.iterables import tuplify
EPSILON = 0.0001
class SampleSpace(Rectangle):
CONFIG = {
"height" : 3,
"width" : 3,
"fill_color" : DARK_GREY,
"fill_opacity" : 1,
"stroke_width" : 0.5,
"stroke_color" : LIGHT_GREY,
"height": 3,
"width": 3,
"fill_color": DARK_GREY,
"fill_opacity": 1,
"stroke_width": 0.5,
"stroke_color": LIGHT_GREY,
##
"default_label_scale_val" : 1,
"default_label_scale_val": 1,
}
def add_title(self, title = "Sample space", buff = MED_SMALL_BUFF):
##TODO, should this really exist in SampleSpaceScene
def add_title(self, title="Sample space", buff=MED_SMALL_BUFF):
# TODO, should this really exist in SampleSpaceScene
title_mob = TextMobject(title)
if title_mob.get_width() > self.get_width():
title_mob.scale_to_fit_width(self.get_width())
title_mob.next_to(self, UP, buff = buff)
title_mob.next_to(self, UP, buff=buff)
self.title = title_mob
self.add(title_mob)
def add_label(self, label):
self.label = label
def complete_p_list(self, p_list):
new_p_list = list(tuplify(p_list))
remainder = 1.0 - sum(new_p_list)
@@ -54,7 +56,7 @@ class SampleSpace(Rectangle):
for factor, color in zip(p_list, colors):
part = SampleSpace()
part.set_fill(color, 1)
part.replace(self, stretch = True)
part.replace(self, stretch=True)
part.stretch(factor, dim)
part.move_to(last_point, -vect)
last_point = part.get_edge_center(vect)
@@ -62,17 +64,17 @@ class SampleSpace(Rectangle):
return parts
def get_horizontal_division(
self, p_list,
colors = [GREEN_E, BLUE_E],
vect = DOWN
):
self, p_list,
colors=[GREEN_E, BLUE_E],
vect=DOWN
):
return self.get_division_along_dimension(p_list, 1, colors, vect)
def get_vertical_division(
self, p_list,
colors = [MAROON_B, YELLOW],
vect = RIGHT
):
self, p_list,
colors=[MAROON_B, YELLOW],
vect=RIGHT
):
return self.get_division_along_dimension(p_list, 0, colors, vect)
def divide_horizontally(self, *args, **kwargs):
@@ -85,16 +87,16 @@ class SampleSpace(Rectangle):
def get_subdivision_braces_and_labels(
self, parts, labels, direction,
buff = SMALL_BUFF,
min_num_quads = 1
):
buff=SMALL_BUFF,
min_num_quads=1
):
label_mobs = VGroup()
braces = VGroup()
for label, part in zip(labels, parts):
brace = Brace(
part, direction,
min_num_quads = min_num_quads,
buff = buff
part, direction,
min_num_quads=min_num_quads,
buff=buff
)
if isinstance(label, Mobject):
label_mob = label
@@ -108,13 +110,13 @@ class SampleSpace(Rectangle):
parts.braces = braces
parts.labels = label_mobs
parts.label_kwargs = {
"labels" : label_mobs.copy(),
"direction" : direction,
"buff" : buff,
"labels": label_mobs.copy(),
"direction": direction,
"buff": buff,
}
return VGroup(parts.braces, parts.labels)
def get_side_braces_and_labels(self, labels, direction = LEFT, **kwargs):
def get_side_braces_and_labels(self, labels, direction=LEFT, **kwargs):
assert(hasattr(self, "horizontal_parts"))
parts = self.horizontal_parts
return self.get_subdivision_braces_and_labels(parts, labels, direction, **kwargs)
@@ -145,21 +147,23 @@ class SampleSpace(Rectangle):
return self.vertical_parts[index]
return self.split()[index]
class BarChart(VGroup):
CONFIG = {
"height" : 4,
"width" : 6,
"n_ticks" : 4,
"tick_width" : 0.2,
"label_y_axis" : True,
"y_axis_label_height" : 0.25,
"max_value" : 1,
"bar_colors" : [BLUE, YELLOW],
"bar_fill_opacity" : 0.8,
"bar_stroke_width" : 3,
"bar_names" : [],
"bar_label_scale_val" : 0.75,
"height": 4,
"width": 6,
"n_ticks": 4,
"tick_width": 0.2,
"label_y_axis": True,
"y_axis_label_height": 0.25,
"max_value": 1,
"bar_colors": [BLUE, YELLOW],
"bar_fill_opacity": 0.8,
"bar_stroke_width": 3,
"bar_names": [],
"bar_label_scale_val": 0.75,
}
def __init__(self, values, **kwargs):
VGroup.__init__(self, **kwargs)
if self.max_value is None:
@@ -170,15 +174,15 @@ class BarChart(VGroup):
self.center()
def add_axes(self):
x_axis = Line(self.tick_width*LEFT/2, self.width*RIGHT)
y_axis = Line(MED_LARGE_BUFF*DOWN, self.height*UP)
x_axis = Line(self.tick_width * LEFT / 2, self.width * RIGHT)
y_axis = Line(MED_LARGE_BUFF * DOWN, self.height * UP)
ticks = VGroup()
heights = np.linspace(0, self.height, self.n_ticks+1)
values = np.linspace(0, self.max_value, self.n_ticks+1)
heights = np.linspace(0, self.height, self.n_ticks + 1)
values = np.linspace(0, self.max_value, self.n_ticks + 1)
for y, value in zip(heights, values):
tick = Line(LEFT, RIGHT)
tick.scale_to_fit_width(self.tick_width)
tick.move_to(y*UP)
tick.move_to(y * UP)
ticks.add(tick)
y_axis.add(ticks)
@@ -195,18 +199,17 @@ class BarChart(VGroup):
self.y_axis_labels = labels
self.add(labels)
def add_bars(self, values):
buff = float(self.width) / (2*len(values) + 1)
buff = float(self.width) / (2 * len(values) + 1)
bars = VGroup()
for i, value in enumerate(values):
bar = Rectangle(
height = (value/self.max_value)*self.height,
width = buff,
stroke_width = self.bar_stroke_width,
fill_opacity = self.bar_fill_opacity,
height=(value / self.max_value) * self.height,
width=buff,
stroke_width=self.bar_stroke_width,
fill_opacity=self.bar_fill_opacity,
)
bar.move_to((2*i+1)*buff*RIGHT, DOWN+LEFT)
bar.move_to((2 * i + 1) * buff * RIGHT, DOWN + LEFT)
bars.add(bar)
bars.set_color_by_gradient(*self.bar_colors)
@@ -225,27 +228,9 @@ class BarChart(VGroup):
for bar, value in zip(self.bars, values):
bar_bottom = bar.get_bottom()
bar.stretch_to_fit_height(
(value/self.max_value)*self.height
(value / self.max_value) * self.height
)
bar.move_to(bar_bottom, DOWN)
def copy(self):
return self.deepcopy()

View File

@@ -8,31 +8,35 @@ from mobject.types.vectorized_mobject import VGroup
from utils.config_ops import digest_config
from utils.color import Color
class SurroundingRectangle(Rectangle):
CONFIG = {
"color" : YELLOW,
"buff" : SMALL_BUFF,
"color": YELLOW,
"buff": SMALL_BUFF,
}
def __init__(self, mobject, **kwargs):
digest_config(self, kwargs)
kwargs["width"] = mobject.get_width() + 2*self.buff
kwargs["height"] = mobject.get_height() + 2*self.buff
kwargs["width"] = mobject.get_width() + 2 * self.buff
kwargs["height"] = mobject.get_height() + 2 * self.buff
Rectangle.__init__(self, **kwargs)
self.move_to(mobject)
class BackgroundRectangle(SurroundingRectangle):
CONFIG = {
"color" : BLACK,
"stroke_width" : 0,
"fill_opacity" : 0.75,
"buff" : 0
"color": BLACK,
"stroke_width": 0,
"fill_opacity": 0.75,
"buff": 0
}
def __init__(self, mobject, **kwargs):
SurroundingRectangle.__init__(self, mobject, **kwargs)
self.original_fill_opacity = self.fill_opacity
def pointwise_become_partial(self, mobject, a, b):
self.set_fill(opacity = b*self.original_fill_opacity)
self.set_fill(opacity=b * self.original_fill_opacity)
return self
def set_color(self):
@@ -42,15 +46,17 @@ class BackgroundRectangle(SurroundingRectangle):
def get_fill_color(self):
return Color(self.color)
class Cross(VGroup):
CONFIG = {
"stroke_color" : RED,
"stroke_width" : 6,
"stroke_color": RED,
"stroke_width": 6,
}
def __init__(self, mobject, **kwargs):
VGroup.__init__(self,
Line(UP+LEFT, DOWN+RIGHT),
Line(UP+RIGHT, DOWN+LEFT),
)
self.replace(mobject, stretch = True)
VGroup.__init__(self,
Line(UP + LEFT, DOWN + RIGHT),
Line(UP + RIGHT, DOWN + LEFT),
)
self.replace(mobject, stretch=True)
self.set_stroke(self.stroke_color, self.stroke_width)

View File

@@ -12,35 +12,37 @@ from mobject.svg.tex_mobject import TextMobject
from mobject.types.vectorized_mobject import VMobject
from utils.config_ops import digest_config
class Brace(TexMobject):
CONFIG = {
"buff" : 0.2,
"width_multiplier" : 2,
"max_num_quads" : 15,
"min_num_quads" : 0,
"buff": 0.2,
"width_multiplier": 2,
"max_num_quads": 15,
"min_num_quads": 0,
}
def __init__(self, mobject, direction = DOWN, **kwargs):
def __init__(self, mobject, direction=DOWN, **kwargs):
digest_config(self, kwargs, locals())
angle = -np.arctan2(*direction[:2]) + np.pi
mobject.rotate(-angle, about_point = ORIGIN)
left = mobject.get_corner(DOWN+LEFT)
right = mobject.get_corner(DOWN+RIGHT)
target_width = right[0]-left[0]
mobject.rotate(-angle, about_point=ORIGIN)
left = mobject.get_corner(DOWN + LEFT)
right = mobject.get_corner(DOWN + RIGHT)
target_width = right[0] - left[0]
## Adding int(target_width) qquads gives approximately the right width
# Adding int(target_width) qquads gives approximately the right width
num_quads = np.clip(
int(self.width_multiplier*target_width),
int(self.width_multiplier * target_width),
self.min_num_quads, self.max_num_quads
)
tex_string = "\\underbrace{%s}"%(num_quads*"\\qquad")
tex_string = "\\underbrace{%s}" % (num_quads * "\\qquad")
TexMobject.__init__(self, tex_string, **kwargs)
self.tip_point_index = np.argmin(self.get_all_points()[:,1])
self.tip_point_index = np.argmin(self.get_all_points()[:, 1])
self.stretch_to_fit_width(target_width)
self.shift(left - self.get_corner(UP+LEFT) + self.buff*DOWN)
self.shift(left - self.get_corner(UP + LEFT) + self.buff * DOWN)
for mob in mobject, self:
mob.rotate(angle, about_point = ORIGIN)
mob.rotate(angle, about_point=ORIGIN)
def put_at_tip(self, mob, use_next_to = True, **kwargs):
def put_at_tip(self, mob, use_next_to=True, **kwargs):
if use_next_to:
mob.next_to(
self.get_tip(),
@@ -50,8 +52,8 @@ class Brace(TexMobject):
else:
mob.move_to(self.get_tip())
buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_MOBJECT_BUFFER)
shift_distance = mob.get_width()/2.0+buff
mob.shift(self.get_direction()*shift_distance)
shift_distance = mob.get_width() / 2.0 + buff
mob.shift(self.get_direction() * shift_distance)
return self
def get_text(self, *text, **kwargs):
@@ -72,32 +74,38 @@ class Brace(TexMobject):
def get_direction(self):
vect = self.get_tip() - self.get_center()
return vect/np.linalg.norm(vect)
return vect / np.linalg.norm(vect)
class BraceLabel(VMobject):
CONFIG = {
"label_constructor" : TexMobject,
"label_scale" : 1,
"label_constructor": TexMobject,
"label_scale": 1,
}
def __init__(self, obj, text, brace_direction = DOWN, **kwargs):
def __init__(self, obj, text, brace_direction=DOWN, **kwargs):
VMobject.__init__(self, **kwargs)
self.brace_direction = brace_direction
if isinstance(obj, list): obj = VMobject(*obj)
if isinstance(obj, list):
obj = VMobject(*obj)
self.brace = Brace(obj, brace_direction, **kwargs)
if isinstance(text, tuple) or isinstance(text, list):
self.label = self.label_constructor(*text, **kwargs)
else: self.label = self.label_constructor(str(text))
if self.label_scale != 1: self.label.scale(self.label_scale)
else:
self.label = self.label_constructor(str(text))
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.submobjects = [self.brace, self.label]
def creation_anim(self, label_anim = FadeIn, brace_anim = GrowFromCenter):
def creation_anim(self, label_anim=FadeIn, brace_anim=GrowFromCenter):
return AnimationGroup(brace_anim(self.brace), label_anim(self.label))
def shift_brace(self, obj, **kwargs):
if isinstance(obj, list): obj = VMobject(*obj)
if isinstance(obj, list):
obj = VMobject(*obj)
self.brace = Brace(obj, self.brace_direction, **kwargs)
self.brace.put_at_tip(self.label)
self.submobjects[0] = self.brace
@@ -105,7 +113,8 @@ class BraceLabel(VMobject):
def change_label(self, *text, **kwargs):
self.label = self.label_constructor(*text, **kwargs)
if self.label_scale != 1: self.label.scale(self.label_scale)
if self.label_scale != 1:
self.label.scale(self.label_scale)
self.brace.put_at_tip(self.label)
self.submobjects[1] = self.label
@@ -124,7 +133,8 @@ class BraceLabel(VMobject):
return copy_mobject
class BraceText(BraceLabel):
CONFIG = {
"label_constructor" : TextMobject
"label_constructor": TextMobject
}

File diff suppressed because it is too large Load Diff

View File

@@ -14,28 +14,31 @@ from utils.config_ops import digest_locals
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
def string_to_numbers(num_string):
num_string = num_string.replace("-",",-")
num_string = num_string.replace("e,-","e-")
num_string = num_string.replace("-", ",-")
num_string = num_string.replace("e,-", "e-")
return [
float(s)
for s in re.split("[ ,]", num_string)
if s != ""
]
class SVGMobject(VMobject):
CONFIG = {
"should_center" : True,
"height" : 2,
"width" : None,
#Must be filled in in a subclass, or when called
"file_name" : None,
"unpack_groups" : True, # if False, creates a hierarchy of VGroups
"stroke_width" : 0,
"fill_opacity" : 1,
"should_center": True,
"height": 2,
"width": None,
# Must be filled in in a subclass, or when called
"file_name": None,
"unpack_groups": True, # if False, creates a hierarchy of VGroups
"stroke_width": 0,
"fill_opacity": 1,
# "fill_color" : LIGHT_GREY,
"propagate_style_to_family" : True,
"propagate_style_to_family": True,
}
def __init__(self, **kwargs):
digest_config(self, kwargs, locals())
self.ensure_valid_file()
@@ -54,15 +57,18 @@ class SVGMobject(VMobject):
if os.path.exists(path):
self.file_path = path
return
raise IOError("No file matching %s in image directory"%self.file_name)
raise IOError("No file matching %s in image directory" %
self.file_name)
def generate_points(self):
doc = minidom.parse(self.file_path)
self.ref_to_element = {}
for svg in doc.getElementsByTagName("svg"):
mobjects = self.get_mobjects_from(svg)
if self.unpack_groups: self.add(*mobjects)
else: self.add(*mobjects[0].submobjects)
if self.unpack_groups:
self.add(*mobjects)
else:
self.add(*mobjects[0].submobjects)
doc.unlink()
def get_mobjects_from(self, element):
@@ -72,7 +78,7 @@ class SVGMobject(VMobject):
if element.tagName == 'defs':
self.update_ref_to_element(element)
elif element.tagName == 'style':
pass #TODO, handle style
pass # TODO, handle style
elif element.tagName in ['g', 'svg']:
result += it.chain(*[
self.get_mobjects_from(child)
@@ -93,9 +99,9 @@ class SVGMobject(VMobject):
elif element.tagName in ['polygon', 'polyline']:
result.append(self.polygon_to_mobject(element))
else:
pass ##TODO
pass # TODO
# warnings.warn("Unknown element type: " + element.tagName)
result = filter(lambda m : m is not None, result)
result = filter(lambda m: m is not None, result)
self.handle_transforms(element, VMobject(*result))
if len(result) > 1 and not self.unpack_groups:
result = [VGroup(*result)]
@@ -111,17 +117,17 @@ class SVGMobject(VMobject):
return VMobjectFromSVGPathstring(path_string)
def use_to_mobjects(self, use_element):
#Remove initial "#" character
# Remove initial "#" character
ref = use_element.getAttribute("xlink:href")[1:]
if ref not in self.ref_to_element:
warnings.warn("%s not recognized"%ref)
warnings.warn("%s not recognized" % ref)
return VMobject()
return self.get_mobjects_from(
self.ref_to_element[ref]
)
def polygon_to_mobject(self, polygon_element):
#TODO, This seems hacky...
# TODO, This seems hacky...
path_string = polygon_element.getAttribute("points")
for digit in string.digits:
path_string = path_string.replace(" " + digit, " L" + digit)
@@ -137,7 +143,7 @@ class SVGMobject(VMobject):
else 0.0
for key in "cx", "cy", "r"
]
return Circle(radius = r).shift(x*RIGHT+y*DOWN)
return Circle(radius=r).shift(x * RIGHT + y * DOWN)
def ellipse_to_mobject(self, circle_element):
x, y, rx, ry = [
@@ -146,74 +152,77 @@ class SVGMobject(VMobject):
else 0.0
for key in "cx", "cy", "rx", "ry"
]
return Circle().scale(rx*RIGHT + ry*UP).shift(x*RIGHT+y*DOWN)
return Circle().scale(rx * RIGHT + ry * UP).shift(x * RIGHT + y * DOWN)
def rect_to_mobject(self, rect_element):
if rect_element.hasAttribute("fill"):
if Color(str(rect_element.getAttribute("fill"))) == Color(WHITE):
return
mob = Rectangle(
width = float(rect_element.getAttribute("width")),
height = float(rect_element.getAttribute("height")),
stroke_width = 0,
fill_color = WHITE,
fill_opacity = 1.0
width=float(rect_element.getAttribute("width")),
height=float(rect_element.getAttribute("height")),
stroke_width=0,
fill_color=WHITE,
fill_opacity=1.0
)
mob.shift(mob.get_center()-mob.get_corner(UP+LEFT))
mob.shift(mob.get_center() - mob.get_corner(UP + LEFT))
return mob
def handle_transforms(self, element, mobject):
x, y = 0, 0
try:
x = float(element.getAttribute('x'))
#Flip y
# Flip y
y = -float(element.getAttribute('y'))
mobject.shift(x*RIGHT+y*UP)
mobject.shift(x * RIGHT + y * UP)
except:
pass
transform = element.getAttribute('transform')
try: # transform matrix
try: # transform matrix
prefix = "matrix("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix): raise Exception()
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
transform = string_to_numbers(transform)
transform = np.array(transform).reshape([3,2])
transform = np.array(transform).reshape([3, 2])
x = transform[2][0]
y = -transform[2][1]
matrix = np.identity(self.dim)
matrix[:2,:2] = transform[:2,:]
matrix[:2, :2] = transform[:2, :]
matrix[1] *= -1
matrix[:,1] *= -1
matrix[:, 1] *= -1
for mob in mobject.family_members_with_points():
mob.points = np.dot(mob.points, matrix)
mobject.shift(x*RIGHT+y*UP)
mobject.shift(x * RIGHT + y * UP)
except:
pass
try: # transform scale
try: # transform scale
prefix = "scale("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix): raise Exception()
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
scale_x, scale_y = string_to_numbers(transform)
mobject.scale(np.array([scale_x, scale_y, 1]))
except:
pass
try: # transform translate
try: # transform translate
prefix = "translate("
suffix = ")"
if not transform.startswith(prefix) or not transform.endswith(suffix): raise Exception()
if not transform.startswith(prefix) or not transform.endswith(suffix):
raise Exception()
transform = transform[len(prefix):-len(suffix)]
x, y = string_to_numbers(transform)
mobject.shift(x*RIGHT + y*DOWN)
mobject.shift(x * RIGHT + y * DOWN)
except:
pass
#TODO, ...
# TODO, ...
def update_ref_to_element(self, defs):
new_refs = dict([
@@ -231,6 +240,7 @@ class SVGMobject(VMobject):
if self.width is not None:
self.scale_to_fit_width(self.width)
class VMobjectFromSVGPathstring(VMobject):
def __init__(self, path_string, **kwargs):
digest_locals(self)
@@ -238,42 +248,42 @@ class VMobjectFromSVGPathstring(VMobject):
def get_path_commands(self):
result = [
"M", #moveto
"L", #lineto
"H", #horizontal lineto
"V", #vertical lineto
"C", #curveto
"S", #smooth curveto
"Q", #quadratic Bezier curve
"T", #smooth quadratic Bezier curveto
"A", #elliptical Arc
"Z", #closepath
"M", # moveto
"L", # lineto
"H", # horizontal lineto
"V", # vertical lineto
"C", # curveto
"S", # smooth curveto
"Q", # quadratic Bezier curve
"T", # smooth quadratic Bezier curveto
"A", # elliptical Arc
"Z", # closepath
]
result += map(lambda s : s.lower(), result)
result += map(lambda s: s.lower(), result)
return result
def generate_points(self):
pattern = "[%s]"%("".join(self.get_path_commands()))
pattern = "[%s]" % ("".join(self.get_path_commands()))
pairs = zip(
re.findall(pattern, self.path_string),
re.split(pattern, self.path_string)[1:]
)
#Which mobject should new points be added to
# Which mobject should new points be added to
self.growing_path = self
for command, coord_string in pairs:
self.handle_command(command, coord_string)
#people treat y-coordinate differently
self.rotate(np.pi, RIGHT, about_point = ORIGIN)
# people treat y-coordinate differently
self.rotate(np.pi, RIGHT, about_point=ORIGIN)
def handle_command(self, command, coord_string):
isLower = command.islower()
command = command.upper()
#new_points are the points that will be added to the curr_points
#list. This variable may get modified in the conditionals below.
# new_points are the points that will be added to the curr_points
# list. This variable may get modified in the conditionals below.
points = self.growing_path.points
new_points = self.string_to_points(coord_string)
if command == "M": #moveto
if command == "M": # moveto
if isLower and len(points) > 0:
new_points[0] += points[-1]
if len(points) > 0:
@@ -281,7 +291,8 @@ class VMobjectFromSVGPathstring(VMobject):
else:
self.growing_path.start_at(new_points[0])
if len(new_points) <= 1: return
if len(new_points) <= 1:
return
points = self.growing_path.points
new_points = new_points[1:]
@@ -290,47 +301,47 @@ class VMobjectFromSVGPathstring(VMobject):
if isLower and len(points) > 0:
new_points += points[-1]
if command in ["L", "H", "V"]: #lineto
if command in ["L", "H", "V"]: # lineto
if command == "H":
new_points[0,1] = points[-1,1]
new_points[0, 1] = points[-1, 1]
elif command == "V":
if isLower:
new_points[0,0] -= points[-1,0]
new_points[0,0] += points[-1,1]
new_points[0,1] = new_points[0,0]
new_points[0,0] = points[-1,0]
new_points = new_points.repeat(3, axis = 0)
elif command == "C": #curveto
pass #Yay! No action required
elif command in ["S", "T"]: #smooth curveto
handle1 = points[-1]+(points[-1]-points[-2])
new_points = np.append([handle1], new_points, axis = 0)
if command in ["Q", "T"]: #quadratic Bezier curve
#TODO, this is a suboptimal approximation
new_points = np.append([new_points[0]], new_points, axis = 0)
elif command == "A": #elliptical Arc
new_points[0, 0] -= points[-1, 0]
new_points[0, 0] += points[-1, 1]
new_points[0, 1] = new_points[0, 0]
new_points[0, 0] = points[-1, 0]
new_points = new_points.repeat(3, axis=0)
elif command == "C": # curveto
pass # Yay! No action required
elif command in ["S", "T"]: # smooth curveto
handle1 = points[-1] + (points[-1] - points[-2])
new_points = np.append([handle1], new_points, axis=0)
if command in ["Q", "T"]: # quadratic Bezier curve
# TODO, this is a suboptimal approximation
new_points = np.append([new_points[0]], new_points, axis=0)
elif command == "A": # elliptical Arc
raise Exception("Not implemented")
elif command == "Z": #closepath
elif command == "Z": # closepath
if not is_closed(points):
#Both handles and new anchor are the start
# Both handles and new anchor are the start
new_points = points[[0, 0, 0]]
# self.mark_paths_closed = True
#Handle situations where there's multiple relative control points
# Handle situations where there's multiple relative control points
if isLower and len(new_points) > 3:
for i in range(3, len(new_points), 3):
new_points[i:i+3] -= points[-1]
new_points[i:i+3] += new_points[i-1]
new_points[i:i + 3] -= points[-1]
new_points[i:i + 3] += new_points[i - 1]
self.growing_path.add_control_points(new_points)
def string_to_points(self, coord_string):
numbers = string_to_numbers(coord_string)
if len(numbers)%2 == 1:
if len(numbers) % 2 == 1:
numbers.append(0)
num_points = len(numbers)/2
num_points = len(numbers) / 2
result = np.zeros((num_points, self.dim))
result[:,:2] = np.array(numbers).reshape((num_points, 2))
result[:, :2] = np.array(numbers).reshape((num_points, 2))
return result
def get_original_path_string(self):

View File

@@ -8,49 +8,50 @@ from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from mobject.types.vectorized_mobject import VectorizedPoint
import collections
import operator as op
import sys
TEX_MOB_SCALE_FACTOR = 0.05
class TexSymbol(VMobjectFromSVGPathstring):
def pointwise_become_partial(self, mobject, a, b):
#TODO, this assumes a = 0
# TODO, this assumes a = 0
if b < 0.5:
b = 2*b
b = 2 * b
added_width = 1
opacity = 0
else:
added_width = 2 - 2*b
opacity = 2*b - 1
added_width = 2 - 2 * b
opacity = 2 * b - 1
b = 1
VMobjectFromSVGPathstring.pointwise_become_partial(
self, mobject, 0, b
)
self.set_stroke(width = added_width + mobject.get_stroke_width())
self.set_fill(opacity = opacity)
self.set_stroke(width=added_width + mobject.get_stroke_width())
self.set_fill(opacity=opacity)
class TexMobject(SVGMobject):
CONFIG = {
"template_tex_file" : TEMPLATE_TEX_FILE,
"stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE,
"should_center" : True,
"arg_separator" : " ",
"height" : None,
"organize_left_to_right" : False,
"propagate_style_to_family" : True,
"alignment" : "",
"template_tex_file": TEMPLATE_TEX_FILE,
"stroke_width": 0,
"fill_opacity": 1.0,
"fill_color": WHITE,
"should_center": True,
"arg_separator": " ",
"height": None,
"organize_left_to_right": False,
"propagate_style_to_family": True,
"alignment": "",
}
def __init__(self, *args, **kwargs):
digest_config(self, kwargs, locals())
if "color" in kwargs.keys() and not "fill_color" in kwargs.keys():
if "color" in kwargs.keys() and "fill_color" not in kwargs.keys():
self.fill_color = kwargs["color"]
##TODO, Eventually remove this
# TODO, Eventually remove this
if len(args) == 1 and isinstance(args[0], list):
self.args = args[0]
##
@@ -60,14 +61,14 @@ class TexMobject(SVGMobject):
self.tex_string,
self.template_tex_file
)
SVGMobject.__init__(self, file_name = file_name, **kwargs)
SVGMobject.__init__(self, file_name=file_name, **kwargs)
self.scale(TEX_MOB_SCALE_FACTOR)
if self.organize_left_to_right:
self.organize_submobjects_left_to_right()
def path_string_to_mobject(self, path_string):
#Overwrite superclass default to use
#specialized path_string mobject
# Overwrite superclass default to use
# specialized path_string mobject
return TexSymbol(path_string)
def generate_points(self):
@@ -86,7 +87,7 @@ class TexMobject(SVGMobject):
def modify_special_strings(self, tex):
tex = self.remove_stray_braces(tex)
if tex in ["\\over", "\\overline"]:
#fraction line needs something to be over
# fraction line needs something to be over
tex += "\\,"
if tex == "\\sqrt":
tex += "{\\quad}"
@@ -132,7 +133,7 @@ class TexMobject(SVGMobject):
self.expression_parts = list(self.args)
for expr in self.args:
sub_tex_mob = TexMobject(expr, **self.CONFIG)
sub_tex_mob.tex_string = expr ##Want it unmodified
sub_tex_mob.tex_string = expr # Want it unmodified
num_submobs = len(sub_tex_mob.submobjects)
new_index = curr_index + num_submobs
if num_submobs == 0:
@@ -150,7 +151,7 @@ class TexMobject(SVGMobject):
self.submobjects = new_submobjects
return self
def get_parts_by_tex(self, tex, substring = True, case_sensitive = True):
def get_parts_by_tex(self, tex, substring=True, case_sensitive=True):
def test(tex1, tex2):
if not case_sensitive:
tex1 = tex1.lower()
@@ -161,13 +162,13 @@ class TexMobject(SVGMobject):
return tex1 == tex2
tex_submobjects = filter(
lambda m : isinstance(m, TexMobject),
lambda m: isinstance(m, TexMobject),
self.submobject_family()
)
if hasattr(self, "expression_parts"):
tex_submobjects.remove(self)
return VGroup(*filter(
lambda m : test(tex, m.get_tex_string()),
lambda m: test(tex, m.get_tex_string()),
tex_submobjects
))
@@ -204,7 +205,7 @@ class TexMobject(SVGMobject):
return self.index_of_part(part)
def organize_submobjects_left_to_right(self):
self.sort_submobjects(lambda p : p[0])
self.sort_submobjects(lambda p: p[0])
return self
def sort_submobjects_alphabetically(self):
@@ -215,30 +216,33 @@ class TexMobject(SVGMobject):
self.submobjects.sort(alphabetical_cmp)
return self
def add_background_rectangle(self, color = BLACK, opacity = 0.75, **kwargs):
def add_background_rectangle(self, color=BLACK, opacity=0.75, **kwargs):
self.background_rectangle = BackgroundRectangle(
self, color = color,
fill_opacity = opacity,
self, color=color,
fill_opacity=opacity,
**kwargs
)
letters = VMobject(*self.submobjects)
self.submobjects = [self.background_rectangle, letters]
return self
class TextMobject(TexMobject):
CONFIG = {
"template_tex_file" : TEMPLATE_TEXT_FILE,
"alignment" : "\\centering",
"template_tex_file": TEMPLATE_TEXT_FILE,
"alignment": "\\centering",
}
class BulletedList(TextMobject):
CONFIG = {
"buff" : MED_LARGE_BUFF,
"dot_scale_factor" : 2,
#Have to include because of handle_multiple_args implementation
"template_tex_file" : TEMPLATE_TEXT_FILE,
"alignment" : "",
"buff": MED_LARGE_BUFF,
"dot_scale_factor": 2,
# Have to include because of handle_multiple_args implementation
"template_tex_file": TEMPLATE_TEXT_FILE,
"alignment": "",
}
def __init__(self, *items, **kwargs):
line_separated_items = [s + "\\\\" for s in items]
TextMobject.__init__(self, *line_separated_items, **kwargs)
@@ -247,12 +251,12 @@ class BulletedList(TextMobject):
dot.next_to(part[0], LEFT, SMALL_BUFF)
part.add_to_back(dot)
self.arrange_submobjects(
DOWN,
aligned_edge = LEFT,
buff = self.buff
DOWN,
aligned_edge=LEFT,
buff=self.buff
)
def fade_all_but(self, index_or_string, opacity = 0.5):
def fade_all_but(self, index_or_string, opacity=0.5):
arg = index_or_string
if isinstance(arg, str):
part = self.get_part_by_tex(arg)
@@ -262,15 +266,17 @@ class BulletedList(TextMobject):
raise Exception("Expected int or string, got {0}".format(arg))
for other_part in self.submobjects:
if other_part is part:
other_part.set_fill(opacity = 1)
other_part.set_fill(opacity=1)
else:
other_part.set_fill(opacity = opacity)
other_part.set_fill(opacity=opacity)
##########
def tex_hash(expression, template_tex_file):
return str(hash(expression + template_tex_file))
def tex_to_svg_file(expression, template_tex_file):
image_dir = os.path.join(
TEX_IMAGE_DIR,
@@ -282,13 +288,14 @@ def tex_to_svg_file(expression, template_tex_file):
dvi_file = tex_to_dvi(tex_file)
return dvi_to_svg(dvi_file)
def generate_tex_file(expression, template_tex_file):
result = os.path.join(
TEX_DIR,
tex_hash(expression, template_tex_file)
) + ".tex"
if not os.path.exists(result):
print("Writing \"%s\" to %s"%(
print("Writing \"%s\" to %s" % (
"".join(expression), result
))
with open(template_tex_file, "r") as infile:
@@ -298,11 +305,13 @@ def generate_tex_file(expression, template_tex_file):
outfile.write(body)
return result
def get_null():
if os.name == "nt":
return "NUL"
return "/dev/null"
def tex_to_dvi(tex_file):
result = tex_file.replace(".tex", ".dvi")
if not os.path.exists(result):
@@ -327,7 +336,8 @@ def tex_to_dvi(tex_file):
"See log output above or the log file: %s" % log_file)
return result
def dvi_to_svg(dvi_file, regen_if_exists = False):
def dvi_to_svg(dvi_file, regen_if_exists=False):
"""
Converts a dvi, which potentially has multiple slides, into a
directory full of enumerated pngs corresponding with these slides.

View File

@@ -9,67 +9,51 @@ from utils.space_ops import z_to_vector
##############
def should_shade_in_3d(mobject):
return hasattr(mobject, "shade_in_3d") and mobject.shade_in_3d
def shade_in_3d(mobject):
for submob in mobject.submobject_family():
submob.shade_in_3d = True
def turn_off_3d_shading(mobject):
for submob in mobject.submobject_family():
submob.shade_in_3d = False
class ThreeDMobject(VMobject):
def __init__(self, *args, **kwargs):
VMobject.__init__(self, *args, **kwargs)
shade_in_3d(self)
class Cube(ThreeDMobject):
CONFIG = {
"fill_opacity" : 0.75,
"fill_color" : BLUE,
"stroke_width" : 0,
"propagate_style_to_family" : True,
"side_length" : 2,
"fill_opacity": 0.75,
"fill_color": BLUE,
"stroke_width": 0,
"propagate_style_to_family": True,
"side_length": 2,
}
def generate_points(self):
for vect in IN, OUT, LEFT, RIGHT, UP, DOWN:
face = Square(side_length = self.side_length)
face.shift(self.side_length*OUT/2.0)
face.apply_function(lambda p : np.dot(p, z_to_vector(vect).T))
face = Square(side_length=self.side_length)
face.shift(self.side_length * OUT / 2.0)
face.apply_function(lambda p: np.dot(p, z_to_vector(vect).T))
self.add(face)
class Prism(Cube):
CONFIG = {
"dimensions" : [3, 2, 1]
"dimensions": [3, 2, 1]
}
def generate_points(self):
Cube.generate_points(self)
for dim, value in enumerate(self.dimensions):
self.rescale_to_fit(value, dim, stretch = True)
self.rescale_to_fit(value, dim, stretch=True)

View File

@@ -1,33 +1,31 @@
from __future__ import absolute_import
import itertools as it
import numpy as np
import os
from PIL import Image
from random import random
from constants import *
from mobject.mobject import Mobject
from utils.bezier import interpolate
from utils.color import color_to_int_rgb
from utils.color import interpolate_color
from utils.config_ops import digest_config
from utils.images import get_full_raster_image_path
class ImageMobject(Mobject):
"""
Automatically filters out black pixels
"""
CONFIG = {
"filter_color" : "black",
"invert" : False,
"filter_color": "black",
"invert": False,
# "use_cache" : True,
"height": 2.0,
"image_mode" : "RGBA",
"pixel_array_dtype" : "uint8",
"image_mode": "RGBA",
"pixel_array_dtype": "uint8",
}
def __init__(self, filename_or_array, **kwargs):
digest_config(self, kwargs)
if isinstance(filename_or_array, str):
@@ -38,7 +36,7 @@ class ImageMobject(Mobject):
self.pixel_array = np.array(filename_or_array)
self.change_to_rgba_array()
if self.invert:
self.pixel_array[:,:,:3] = 255-self.pixel_array[:,:,:3]
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
Mobject.__init__(self, **kwargs)
def change_to_rgba_array(self):
@@ -46,43 +44,43 @@ class ImageMobject(Mobject):
if len(pa.shape) == 2:
pa = pa.reshape(list(pa.shape) + [1])
if pa.shape[2] == 1:
pa = pa.repeat(3, axis = 2)
pa = pa.repeat(3, axis=2)
if pa.shape[2] == 3:
alphas = 255*np.ones(
list(pa.shape[:2])+[1],
dtype = self.pixel_array_dtype
alphas = 255 * np.ones(
list(pa.shape[:2]) + [1],
dtype=self.pixel_array_dtype
)
pa = np.append(pa, alphas, axis = 2)
pa = np.append(pa, alphas, axis=2)
self.pixel_array = pa
def set_color(self, color, alpha = None, family = True):
def set_color(self, color, alpha=None, family=True):
rgb = color_to_int_rgb(color)
self.pixel_array[:,:,:3] = rgb
self.pixel_array[:, :, :3] = rgb
if alpha is not None:
self.pixel_array[:,:,3] = int(255*alpha)
self.pixel_array[:, :, 3] = int(255 * alpha)
for submob in self.submobjects:
submob.set_color(color, alpha, family)
self.color = color
return self
def init_points(self):
#Corresponding corners of image are fixed to these
#Three points
# Corresponding corners of image are fixed to these
# Three points
self.points = np.array([
UP+LEFT,
UP+RIGHT,
DOWN+LEFT,
UP + LEFT,
UP + RIGHT,
DOWN + LEFT,
])
self.center()
self.scale_to_fit_height(self.height)
h, w = self.pixel_array.shape[:2]
self.stretch_to_fit_width(self.height*w/h)
self.stretch_to_fit_width(self.height * w / h)
def set_opacity(self, alpha):
self.pixel_array[:,:,3] = int(255*alpha)
self.pixel_array[:, :, 3] = int(255 * alpha)
return self
def fade(self, darkness = 0.5):
def fade(self, darkness=0.5):
self.set_opacity(1 - darkness)
return self
@@ -94,5 +92,3 @@ class ImageMobject(Mobject):
def copy(self):
return self.deepcopy()

View File

@@ -1,16 +1,16 @@
from __future__ import absolute_import
from mobject.mobject import Mobject
from constants import *
from mobject.mobject import Mobject
from utils.bezier import interpolate
from utils.color import color_gradient
from utils.color import color_to_rgb
from utils.color import color_to_rgba
from utils.color import interpolate_color
from utils.color import rgba_to_color
from utils.config_ops import digest_config
from utils.iterables import stretch_array_to_length
class PMobject(Mobject):
def init_points(self):
self.rgbas = np.zeros((0, 4))
@@ -20,38 +20,38 @@ class PMobject(Mobject):
def get_array_attrs(self):
return Mobject.get_array_attrs(self) + ["rgbas"]
def add_points(self, points, rgbas = None, color = None, alpha = 1):
def add_points(self, points, rgbas=None, color=None, alpha=1):
"""
points must be a Nx3 numpy array, as must rgbas if it is not None
"""
if not isinstance(points, np.ndarray):
points = np.array(points)
num_new_points = len(points)
self.points = np.append(self.points, points, axis = 0)
self.points = np.append(self.points, points, axis=0)
if rgbas is None:
color = Color(color) if color else self.color
rgbas = np.repeat(
[color_to_rgba(color, alpha)],
num_new_points,
axis = 0
axis=0
)
elif len(rgbas) != len(points):
raise Exception("points and rgbas must have same shape")
self.rgbas = np.append(self.rgbas, rgbas, axis = 0)
self.rgbas = np.append(self.rgbas, rgbas, axis=0)
return self
def set_color(self, color = YELLOW_C, family = True):
def set_color(self, color=YELLOW_C, family=True):
rgba = color_to_rgba(color)
mobs = self.family_members_with_points() if family else [self]
for mob in mobs:
mob.rgbas[:,:] = rgba
mob.rgbas[:, :] = rgba
self.color = color
return self
# def set_color_by_gradient(self, start_color, end_color):
def set_color_by_gradient(self, *colors):
self.rgbas = np.array(map(
color_to_rgba,
color_to_rgba,
color_gradient(colors, len(self.points))
))
return self
@@ -61,21 +61,21 @@ class PMobject(Mobject):
num_points = mob.get_num_points()
mob.rgbas = np.array([
interpolate(start_rgba, end_rgba, alpha)
for alpha in np.arange(num_points)/float(num_points)
for alpha in np.arange(num_points) / float(num_points)
])
return self
def set_colors_by_radial_gradient(self, center = None, radius = 1, inner_color = WHITE, outer_color = BLACK):
def set_colors_by_radial_gradient(self, center=None, radius=1, inner_color=WHITE, outer_color=BLACK):
start_rgba, end_rgba = map(color_to_rgba, [start_color, end_color])
if center == None:
if center is None:
center = self.get_center()
for mob in self.family_members_with_points():
num_points = mob.get_num_points()
t = min(1,np.abs(mob.get_center() - center)/radius)
t = min(1, np.abs(mob.get_center() - center) / radius)
mob.rgbas = np.array(
[ interpolate(start_rgba, end_rgba, t) ] * num_points
)
[interpolate(start_rgba, end_rgba, t)] * num_points
)
return self
def match_colors(self, mobject):
@@ -90,20 +90,20 @@ class PMobject(Mobject):
mob.rgbas = mob.rgbas[to_eliminate]
return self
def thin_out(self, factor = 5):
def thin_out(self, factor=5):
"""
Removes all but every nth point for n = factor
"""
for mob in self.family_members_with_points():
num_points = self.get_num_points()
mob.apply_over_attr_arrays(
lambda arr : arr[
lambda arr: arr[
np.arange(0, num_points, factor)
]
)
return self
def sort_points(self, function = lambda p : p[0]):
def sort_points(self, function=lambda p: p[0]):
"""
function is any map from R^3 to R
"""
@@ -111,7 +111,7 @@ class PMobject(Mobject):
indices = np.argsort(
np.apply_along_axis(function, 1, mob.points)
)
mob.apply_over_attr_arrays(lambda arr : arr[indices])
mob.apply_over_attr_arrays(lambda arr: arr[indices])
return self
def fade_to(self, color, alpha):
@@ -135,19 +135,19 @@ class PMobject(Mobject):
return rgba_to_color(self.rgbas[0, :])
def point_from_proportion(self, alpha):
index = alpha*(self.get_num_points()-1)
index = alpha * (self.get_num_points() - 1)
return self.points[index]
# Alignment
def align_points_with_larger(self, larger_mobject):
assert(isinstance(larger_mobject, PMobject))
self.apply_over_attr_arrays(
lambda a : stretch_array_to_length(
lambda a: stretch_array_to_length(
a, larger_mobject.get_num_points()
)
)
def get_point_mobject(self, center = None):
def get_point_mobject(self, center=None):
if center is None:
center = self.get_center()
return Point(center)
@@ -165,65 +165,70 @@ class PMobject(Mobject):
for attr in self.get_array_attrs():
full_array = getattr(mobject, attr)
partial_array = full_array[lower_index:upper_index]
setattr(self, attr, partial_array)
setattr(self, attr, partial_array)
#TODO, Make the two implementations bellow non-redundant
# TODO, Make the two implementations bellow non-redundant
class Mobject1D(PMobject):
CONFIG = {
"density" : DEFAULT_POINT_DENSITY_1D,
"density": DEFAULT_POINT_DENSITY_1D,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.epsilon = 1.0 / self.density
self.epsilon = 1.0 / self.density
Mobject.__init__(self, **kwargs)
def add_line(self, start, end, color = None):
def add_line(self, start, end, color=None):
start, end = map(np.array, [start, end])
length = np.linalg.norm(end - start)
if length == 0:
points = [start]
else:
epsilon = self.epsilon/length
epsilon = self.epsilon / length
points = [
interpolate(start, end, t)
for t in np.arange(0, 1, epsilon)
]
self.add_points(points, color = color)
self.add_points(points, color=color)
class Mobject2D(PMobject):
CONFIG = {
"density" : DEFAULT_POINT_DENSITY_2D,
"density": DEFAULT_POINT_DENSITY_2D,
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.epsilon = 1.0 / self.density
self.epsilon = 1.0 / self.density
Mobject.__init__(self, **kwargs)
class PointCloudDot(Mobject1D):
CONFIG = {
"radius" : 0.075,
"stroke_width" : 2,
"density" : DEFAULT_POINT_DENSITY_1D,
"color" : YELLOW,
"radius": 0.075,
"stroke_width": 2,
"density": DEFAULT_POINT_DENSITY_1D,
"color": YELLOW,
}
def __init__(self, center = ORIGIN, **kwargs):
def __init__(self, center=ORIGIN, **kwargs):
Mobject1D.__init__(self, **kwargs)
self.shift(center)
def generate_points(self):
self.add_points([
r*(np.cos(theta)*RIGHT + np.sin(theta)*UP)
r * (np.cos(theta) * RIGHT + np.sin(theta) * UP)
for r in np.arange(0, self.radius, self.epsilon)
for theta in np.arange(0, 2*np.pi, self.epsilon/r)
for theta in np.arange(0, 2 * np.pi, self.epsilon / r)
])
class Point(PMobject):
CONFIG = {
"color" : BLACK,
"color": BLACK,
}
def __init__(self, location = ORIGIN, **kwargs):
def __init__(self, location=ORIGIN, **kwargs):
PMobject.__init__(self, **kwargs)
self.add_points([location])

View File

@@ -1,7 +1,5 @@
from __future__ import absolute_import
import re
from colour import Color
from mobject.mobject import Mobject
@@ -12,38 +10,36 @@ from utils.bezier import interpolate
from utils.bezier import is_closed
from utils.bezier import partial_bezier_points
from utils.color import color_to_rgb
from utils.color import interpolate_color
from utils.iterables import make_even
class VMobject(Mobject):
CONFIG = {
"fill_color" : None,
"fill_opacity" : 0.0,
"stroke_color" : None,
#Indicates that it will not be displayed, but
#that it should count in parent mobject's path
"is_subpath" : False,
"close_new_points" : False,
"mark_paths_closed" : False,
"propagate_style_to_family" : False,
"pre_function_handle_to_anchor_scale_factor" : 0.01,
"make_smooth_after_applying_functions" : False,
"background_image_file" : None,
"fill_color": None,
"fill_opacity": 0.0,
"stroke_color": None,
# Indicates that it will not be displayed, but
# that it should count in parent mobject's path
"is_subpath": False,
"close_new_points": False,
"mark_paths_closed": False,
"propagate_style_to_family": False,
"pre_function_handle_to_anchor_scale_factor": 0.01,
"make_smooth_after_applying_functions": False,
"background_image_file": None,
}
def get_group_class(self):
return VGroup
## Colors
# Colors
def init_colors(self):
self.set_style_data(
stroke_color = self.stroke_color or self.color,
stroke_width = self.stroke_width,
fill_color = self.fill_color or self.color,
fill_opacity = self.fill_opacity,
family = self.propagate_style_to_family
stroke_color=self.stroke_color or self.color,
stroke_width=self.stroke_width,
fill_color=self.fill_color or self.color,
fill_opacity=self.fill_opacity,
family=self.propagate_style_to_family
)
return self
@@ -51,12 +47,12 @@ class VMobject(Mobject):
for mob in self.submobject_family():
setattr(mob, attr, value)
def set_style_data(self,
stroke_color = None,
stroke_width = None,
fill_color = None,
fill_opacity = None,
family = True
def set_style_data(self,
stroke_color=None,
stroke_width=None,
fill_color=None,
fill_opacity=None,
family=True
):
if stroke_color is not None:
self.stroke_rgb = color_to_rgb(stroke_color)
@@ -69,48 +65,48 @@ class VMobject(Mobject):
if family:
for mob in self.submobjects:
mob.set_style_data(
stroke_color = stroke_color,
stroke_width = stroke_width,
fill_color = fill_color,
fill_opacity = fill_opacity,
family = family
stroke_color=stroke_color,
stroke_width=stroke_width,
fill_color=fill_color,
fill_opacity=fill_opacity,
family=family
)
return self
def set_fill(self, color = None, opacity = None, family = True):
def set_fill(self, color=None, opacity=None, family=True):
return self.set_style_data(
fill_color = color,
fill_opacity = opacity,
family = family
fill_color=color,
fill_opacity=opacity,
family=family
)
def set_stroke(self, color = None, width = None, family = True):
def set_stroke(self, color=None, width=None, family=True):
return self.set_style_data(
stroke_color = color,
stroke_width = width,
family = family
stroke_color=color,
stroke_width=width,
family=family
)
def set_color(self, color, family = True):
def set_color(self, color, family=True):
self.set_style_data(
stroke_color = color,
fill_color = color,
family = family
stroke_color=color,
fill_color=color,
family=family
)
self.color = color
return self
def match_style(self, vmobject):
self.set_style_data(
stroke_color = vmobject.get_stroke_color(),
stroke_width = vmobject.get_stroke_width(),
fill_color = vmobject.get_fill_color(),
fill_opacity = vmobject.get_fill_opacity(),
family = False
stroke_color=vmobject.get_stroke_color(),
stroke_width=vmobject.get_stroke_width(),
fill_color=vmobject.get_fill_color(),
fill_opacity=vmobject.get_fill_opacity(),
family=False
)
#Does its best to match up submobject lists, and
#match styles accordingly
# Does its best to match up submobject lists, and
# match styles accordingly
submobs1, submobs2 = self.submobjects, vmobject.submobjects
if len(submobs1) == 0:
return self
@@ -122,12 +118,12 @@ class VMobject(Mobject):
def fade_no_recurse(self, darkness):
self.set_stroke(
width = (1-darkness)*self.get_stroke_width(),
family = False
width=(1 - darkness) * self.get_stroke_width(),
family=False
)
self.set_fill(
opacity = (1-darkness)*self.get_fill_opacity(),
family = False
opacity=(1 - darkness) * self.get_fill_opacity(),
family=False
)
return self
@@ -137,7 +133,7 @@ class VMobject(Mobject):
def get_fill_color(self):
try:
self.fill_rgb = np.clip(self.fill_rgb, 0.0, 1.0)
return Color(rgb = self.fill_rgb)
return Color(rgb=self.fill_rgb)
except:
return Color(WHITE)
@@ -150,7 +146,7 @@ class VMobject(Mobject):
def get_stroke_color(self):
try:
self.stroke_rgb = np.clip(self.stroke_rgb, 0, 1)
return Color(rgb = self.stroke_rgb)
return Color(rgb=self.stroke_rgb)
except:
return Color(WHITE)
@@ -176,7 +172,7 @@ class VMobject(Mobject):
self.color_using_background_image(vmobject.get_background_image_file())
return self
## Drawing
# Drawing
def start_at(self, point):
if len(self.points) == 0:
self.points = np.zeros((1, 3))
@@ -188,7 +184,7 @@ class VMobject(Mobject):
self.points = np.append(
self.points,
control_points,
axis = 0
axis=0
)
return self
@@ -196,14 +192,14 @@ class VMobject(Mobject):
return is_closed(self.points)
def set_anchors_and_handles(self, anchors, handles1, handles2):
assert(len(anchors) == len(handles1)+1)
assert(len(anchors) == len(handles2)+1)
total_len = 3*(len(anchors)-1) + 1
assert(len(anchors) == len(handles1) + 1)
assert(len(anchors) == len(handles2) + 1)
total_len = 3 * (len(anchors) - 1) + 1
self.points = np.zeros((total_len, self.dim))
self.points[0] = anchors[0]
arrays = [handles1, handles2, anchors[1:]]
for index, array in enumerate(arrays):
self.points[index+1::3] = array
self.points[index + 1::3] = array
return self.points
def set_points_as_corners(self, points):
@@ -212,7 +208,7 @@ class VMobject(Mobject):
points = np.array(points)
self.set_anchors_and_handles(points, *[
interpolate(points[:-1], points[1:], alpha)
for alpha in 1./3, 2./3
for alpha in 1. / 3, 2. / 3
])
return self
@@ -227,11 +223,11 @@ class VMobject(Mobject):
self.points = np.array(points)
return self
def set_anchor_points(self, points, mode = "smooth"):
def set_anchor_points(self, points, mode="smooth"):
if not isinstance(points, np.ndarray):
points = np.array(points)
if self.close_new_points and not is_closed(points):
points = np.append(points, [points[0]], axis = 0)
points = np.append(points, [points[0]], axis=0)
if mode == "smooth":
self.set_points_smoothly(points)
elif mode == "corners":
@@ -243,7 +239,7 @@ class VMobject(Mobject):
def change_anchor_mode(self, mode):
for submob in self.family_members_with_points():
anchors, h1, h2 = submob.get_anchors_and_handles()
submob.set_anchor_points(anchors, mode = mode)
submob.set_anchor_points(anchors, mode=mode)
return self
def make_smooth(self):
@@ -262,7 +258,7 @@ class VMobject(Mobject):
but will be tracked in a separate special list for when
it comes time to display.
"""
subpath_mobject = self.copy() ##Really helps to be of the same class
subpath_mobject = self.copy() # Really helps to be of the same class
subpath_mobject.submobjects = []
subpath_mobject.is_subpath = True
subpath_mobject.set_points(points)
@@ -277,12 +273,12 @@ class VMobject(Mobject):
self.start_at(new_points[0])
self.add_control_points(new_points[1:])
else:
self.add_control_points(2*[new_points[0]] + new_points)
self.add_control_points(2 * [new_points[0]] + new_points)
return self
def get_subpath_mobjects(self):
return filter(
lambda m : hasattr(m, 'is_subpath') and m.is_subpath,
lambda m: hasattr(m, 'is_subpath') and m.is_subpath,
self.submobjects
)
@@ -290,7 +286,7 @@ class VMobject(Mobject):
factor = self.pre_function_handle_to_anchor_scale_factor
self.scale_handle_to_anchor_distances(factor)
Mobject.apply_function(self, function)
self.scale_handle_to_anchor_distances(1./factor)
self.scale_handle_to_anchor_distances(1. / factor)
if self.make_smooth_after_applying_functions:
self.make_smooth()
return self
@@ -300,8 +296,8 @@ class VMobject(Mobject):
If the distance between a given handle point H and its associated
anchor point A is d, then it changes H to be a distances factor*d
away from A, but so that the line from A to H doesn't change.
This is mostly useful in the context of applying a (differentiable)
function, to preserve tangency properties. One would pull all the
This is mostly useful in the context of applying a (differentiable)
function, to preserve tangency properties. One would pull all the
handles closer to their anchors, apply the function then push them out
again.
"""
@@ -311,27 +307,27 @@ class VMobject(Mobject):
# print len(anchors), len(handles1), len(handles2)
a_to_h1 = handles1 - anchors[:-1]
a_to_h2 = handles2 - anchors[1:]
handles1 = anchors[:-1] + factor*a_to_h1
handles2 = anchors[1:] + factor*a_to_h2
handles1 = anchors[:-1] + factor * a_to_h1
handles2 = anchors[1:] + factor * a_to_h2
self.set_anchors_and_handles(anchors, handles1, handles2)
## Information about line
# Information about line
def component_curves(self):
for n in range(self.get_num_anchor_points()-1):
for n in range(self.get_num_anchor_points() - 1):
yield self.get_nth_curve(n)
def get_nth_curve(self, n):
return bezier(self.points[3*n:3*n+4])
return bezier(self.points[3 * n:3 * n + 4])
def get_num_anchor_points(self):
return (len(self.points) - 1)/3 + 1
return (len(self.points) - 1) / 3 + 1
def point_from_proportion(self, alpha):
num_cubics = self.get_num_anchor_points()-1
interpoint_alpha = num_cubics*(alpha % (1./num_cubics))
index = min(3*int(alpha*num_cubics), 3*num_cubics)
cubic = bezier(self.points[index:index+4])
num_cubics = self.get_num_anchor_points() - 1
interpoint_alpha = num_cubics * (alpha % (1. / num_cubics))
index = min(3 * int(alpha * num_cubics), 3 * num_cubics)
cubic = bezier(self.points[index:index + 4])
return cubic(interpoint_alpha)
def get_anchors_and_handles(self):
@@ -346,8 +342,7 @@ class VMobject(Mobject):
def get_points_defining_boundary(self):
return self.get_anchors()
## Alignment
# Alignment
def align_points(self, mobject):
Mobject.align_points(self, mobject)
is_subpath = self.is_subpath or mobject.is_subpath
@@ -355,61 +350,62 @@ class VMobject(Mobject):
mark_closed = self.mark_paths_closed and mobject.mark_paths_closed
self.mark_paths_closed = mobject.mark_paths_closed = mark_closed
return self
def align_points_with_larger(self, larger_mobject):
assert(isinstance(larger_mobject, VMobject))
self.insert_n_anchor_points(
larger_mobject.get_num_anchor_points()-\
larger_mobject.get_num_anchor_points() -
self.get_num_anchor_points()
)
return self
def insert_n_anchor_points(self, n):
curr = self.get_num_anchor_points()
if curr == 0:
self.points = np.zeros((1, 3))
n = n-1
n = n - 1
if curr == 1:
self.points = np.repeat(self.points, 3*n+1, axis = 0)
self.points = np.repeat(self.points, 3 * n + 1, axis=0)
return self
points = np.array([self.points[0]])
num_curves = curr-1
#Curves in self are buckets, and we need to know
#how many new anchor points to put into each one.
#Each element of index_allocation is like a bucket,
#and its value tells you the appropriate index of
#the smaller curve.
index_allocation = (np.arange(curr+n-1) * num_curves)/(curr+n-1)
num_curves = curr - 1
# Curves in self are buckets, and we need to know
# how many new anchor points to put into each one.
# Each element of index_allocation is like a bucket,
# and its value tells you the appropriate index of
# the smaller curve.
index_allocation = (np.arange(curr + n - 1) *
num_curves) / (curr + n - 1)
for index in range(num_curves):
curr_bezier_points = self.points[3*index:3*index+4]
curr_bezier_points = self.points[3 * index:3 * index + 4]
num_inter_curves = sum(index_allocation == index)
alphas = np.linspace(0, 1, num_inter_curves+1)
alphas = np.linspace(0, 1, num_inter_curves + 1)
# alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves)
for a, b in zip(alphas, alphas[1:]):
new_points = partial_bezier_points(
curr_bezier_points, a, b
)
points = np.append(
points, new_points[1:], axis = 0
points, new_points[1:], axis=0
)
self.set_points(points)
return self
def get_point_mobject(self, center = None):
def get_point_mobject(self, center=None):
if center is None:
center = self.get_center()
return VectorizedPoint(center)
def repeat_submobject(self, submobject):
if submobject.is_subpath:
return VectorizedPoint(submobject.points[0])
return submobject.copy()
def interpolate_color(self, mobject1, mobject2, alpha):
attrs = [
"stroke_rgb",
"stroke_width",
"fill_rgb",
"stroke_rgb",
"stroke_width",
"fill_rgb",
"fill_opacity",
]
for attr in attrs:
@@ -421,31 +417,31 @@ class VMobject(Mobject):
if alpha == 1.0:
# print getattr(mobject2, attr)
setattr(self, attr, getattr(mobject2, attr))
def pointwise_become_partial(self, mobject, a, b):
assert(isinstance(mobject, VMobject))
#Partial curve includes three portions:
#- A middle section, which matches the curve exactly
#- A start, which is some ending portion of an inner cubic
#- An end, which is the starting portion of a later inner cubic
# Partial curve includes three portions:
# - A middle section, which matches the curve exactly
# - A start, which is some ending portion of an inner cubic
# - An end, which is the starting portion of a later inner cubic
if a <= 0 and b >= 1:
self.set_points(mobject.points)
self.mark_paths_closed = mobject.mark_paths_closed
return self
self.mark_paths_closed = False
num_cubics = mobject.get_num_anchor_points()-1
lower_index = int(a*num_cubics)
upper_index = int(b*num_cubics)
num_cubics = mobject.get_num_anchor_points() - 1
lower_index = int(a * num_cubics)
upper_index = int(b * num_cubics)
points = np.array(
mobject.points[3*lower_index:3*upper_index+4]
mobject.points[3 * lower_index:3 * upper_index + 4]
)
if len(points) > 1:
a_residue = (num_cubics*a)%1
b_residue = (num_cubics*b)%1
a_residue = (num_cubics * a) % 1
b_residue = (num_cubics * b) % 1
if b == 1:
b_residue = 1
elif lower_index == upper_index:
b_residue = (b_residue - a_residue)/(1-a_residue)
b_residue = (b_residue - a_residue) / (1 - a_residue)
points[:4] = partial_bezier_points(
points[:4], a_residue, 1
@@ -456,6 +452,7 @@ class VMobject(Mobject):
self.set_points(points)
return self
class VGroup(VMobject):
def __init__(self, *args, **kwargs):
if len(args) == 1 and isinstance(args[0], (tuple, list)):
@@ -465,19 +462,22 @@ class VGroup(VMobject):
for arg in args:
if isinstance(arg, (tuple, list)):
packed_args.append(VGroup(arg))
else: packed_args.append(arg)
else:
packed_args.append(arg)
VMobject.__init__(self, *packed_args, **kwargs)
class VectorizedPoint(VMobject):
CONFIG = {
"color" : BLACK,
"fill_opacity" : 0,
"stroke_width" : 0,
"artificial_width" : 0.01,
"artificial_height" : 0.01,
"color": BLACK,
"fill_opacity": 0,
"stroke_width": 0,
"artificial_width": 0.01,
"artificial_height": 0.01,
}
def __init__(self, location = ORIGIN, **kwargs):
def __init__(self, location=ORIGIN, **kwargs):
VMobject.__init__(self, **kwargs)
self.set_points(np.array([location]))
@@ -490,24 +490,25 @@ class VectorizedPoint(VMobject):
def get_location(self):
return self.get_anchors()[0]
def set_location(self,new_loc):
def set_location(self, new_loc):
self.set_points(np.array([new_loc]))
class DashedMobject(VMobject):
CONFIG = {
"dashes_num" : 15,
"spacing" : 0.5,
"color" : WHITE
"dashes_num": 15,
"spacing": 0.5,
"color": WHITE
}
def __init__(self, mobject, **kwargs):
VMobject.__init__(self, **kwargs)
buff = float(self.spacing) / self.dashes_num
for i in range(self.dashes_num):
a = ((1+buff) * i)/self.dashes_num
b = 1-((1+buff) * (self.dashes_num-1-i)) / self.dashes_num
dash = VMobject(color = self.color)
a = ((1 + buff) * i) / self.dashes_num
b = 1 - ((1 + buff) * (self.dashes_num - 1 - i)) / self.dashes_num
dash = VMobject(color=self.color)
dash.pointwise_become_partial(mobject, a, b)
self.submobjects.append(dash)

View File

@@ -8,14 +8,16 @@ from mobject.types.vectorized_mobject import VectorizedPoint
# TODO: Rather than using VectorizedPoint, there should be some UndisplayedPointSet type
class ValueTracker(VectorizedPoint):
"""
Note meant to be displayed. Instead the position encodes some
Note meant to be displayed. Instead the position encodes some
number, often one which another animation or continual_animation
uses for its update function, and by treating it as a mobject it can
still be animated and manipulated just like anything else.
"""
def __init__(self, value = 0, **kwargs):
def __init__(self, value=0, **kwargs):
VectorizedPoint.__init__(self, **kwargs)
self.set_value(value)
@@ -23,21 +25,23 @@ class ValueTracker(VectorizedPoint):
return self.get_center()[0]
def set_value(self, value):
self.move_to(value*RIGHT)
self.move_to(value * RIGHT)
return self
def increment_value(self, d_value):
self.set_value(self.get_value() + d_value)
class ExponentialValueTracker(ValueTracker):
"""
Operates just like ValueTracker, except it encodes the value as the
exponential of a position coordinate, which changes how interpolation
exponential of a position coordinate, which changes how interpolation
behaves
"""
def get_value(self):
return np.exp(self.get_center()[0])
def set_value(self, value):
self.move_to(np.log(value)*RIGHT)
self.move_to(np.log(value) * RIGHT)
return self

View File

@@ -1,4 +1,3 @@
import itertools as it
import numpy as np
from animation.animation import Animation
@@ -6,18 +5,19 @@ from constants import *
from mobject.svg.tex_mobject import TexMobject
from scene.scene import Scene
class RearrangeEquation(Scene):
def construct(
self,
start_terms,
end_terms,
self,
start_terms,
end_terms,
index_map,
path_arc = np.pi,
start_transform = None,
end_transform = None,
leave_start_terms = False,
transform_kwargs = {},
):
path_arc=np.pi,
start_transform=None,
end_transform=None,
leave_start_terms=False,
transform_kwargs={},
):
transform_kwargs["path_func"] = path
start_mobs, end_mobs = self.get_mobs_from_terms(
start_terms, end_terms
@@ -27,17 +27,17 @@ class RearrangeEquation(Scene):
if end_transform:
end_mobs = end_transform(Mobject(*end_mobs)).split()
unmatched_start_indices = set(range(len(start_mobs)))
unmatched_end_indices = set(range(len(end_mobs)))
unmatched_end_indices = set(range(len(end_mobs)))
unmatched_start_indices.difference_update(
[n%len(start_mobs) for n in index_map]
[n % len(start_mobs) for n in index_map]
)
unmatched_end_indices.difference_update(
[n%len(end_mobs) for n in index_map.values()]
[n % len(end_mobs) for n in index_map.values()]
)
mobject_pairs = [
(start_mobs[a], end_mobs[b])
for a, b in index_map.iteritems()
]+ [
] + [
(Point(end_mobs[b].get_center()), end_mobs[b])
for b in unmatched_end_indices
]
@@ -45,7 +45,7 @@ class RearrangeEquation(Scene):
mobject_pairs += [
(start_mobs[a], Point(start_mobs[a].get_center()))
for a in unmatched_start_indices
]
]
self.add(*start_mobs)
if leave_start_terms:
@@ -57,7 +57,6 @@ class RearrangeEquation(Scene):
])
self.wait()
def get_mobs_from_terms(self, start_terms, end_terms):
"""
Need to ensure that all image mobjects for a tex expression
@@ -67,10 +66,10 @@ class RearrangeEquation(Scene):
"""
num_start_terms = len(start_terms)
all_mobs = np.array(
TexMobject(start_terms).split() + \
TexMobject(start_terms).split() +
TexMobject(end_terms).split()
)
all_terms = np.array(start_terms+end_terms)
all_terms = np.array(start_terms + end_terms)
for term in set(all_terms):
matches = all_terms == term
if sum(matches) > 1:
@@ -84,23 +83,21 @@ class RearrangeEquation(Scene):
class FlipThroughSymbols(Animation):
CONFIG = {
"start_center" : ORIGIN,
"end_center" : ORIGIN,
"start_center": ORIGIN,
"end_center": ORIGIN,
}
def __init__(self, tex_list, **kwargs):
mobject = TexMobject(self.curr_tex).shift(start_center)
Animation.__init__(self, mobject, **kwargs)
def update_mobject(self, alpha):
new_tex = self.tex_list[np.ceil(alpha*len(self.tex_list))-1]
new_tex = self.tex_list[np.ceil(alpha * len(self.tex_list)) - 1]
if new_tex != self.curr_tex:
self.curr_tex = new_tex
self.mobject = TexMobject(new_tex).shift(self.start_center)
if not all(self.start_center == self.end_center):
self.mobject.center().shift(
(1-alpha)*self.start_center + alpha*self.end_center
(1 - alpha) * self.start_center + alpha * self.end_center
)

View File

@@ -2,29 +2,30 @@ from constants import *
from animation.animation import Animation
from animation.movement import SmoothedVectorizedHomotopy
from animation.transform import ApplyPointwiseFunction
from animation.transform import MoveToTarget
from mobject.coordinate_systems import ComplexPlane
from mobject.types.vectorized_mobject import VGroup
from scene.scene import Scene
class ComplexTransformationScene(Scene):
CONFIG = {
"plane_config" : {},
"background_fade_factor" : 0.5,
"use_multicolored_plane" : False,
"vert_start_color" : BLUE, ##TODO
"vert_end_color" : BLUE,
"horiz_start_color" : BLUE,
"horiz_end_color" : BLUE,
"num_anchors_to_add_per_line" : 50,
"post_transformation_stroke_width" : None,
"default_apply_complex_function_kwargs" : {
"run_time" : 5,
"plane_config": {},
"background_fade_factor": 0.5,
"use_multicolored_plane": False,
"vert_start_color": BLUE, # TODO
"vert_end_color": BLUE,
"horiz_start_color": BLUE,
"horiz_end_color": BLUE,
"num_anchors_to_add_per_line": 50,
"post_transformation_stroke_width": None,
"default_apply_complex_function_kwargs": {
"run_time": 5,
},
"background_label_scale_val" : 0.5,
"include_coordinate_labels" : True,
"background_label_scale_val": 0.5,
"include_coordinate_labels": True,
}
def setup(self):
self.foreground_mobjects = []
self.transformable_mobjects = []
@@ -44,12 +45,12 @@ class ComplexTransformationScene(Scene):
Scene.add(self, *mobjects)
def add(self, *mobjects):
Scene.add(self, *list(mobjects)+self.foreground_mobjects)
Scene.add(self, *list(mobjects) + self.foreground_mobjects)
def play(self, *animations, **kwargs):
Scene.play(
self,
*list(animations)+map(Animation, self.foreground_mobjects),
*list(animations) + map(Animation, self.foreground_mobjects),
**kwargs
)
@@ -67,7 +68,7 @@ class ComplexTransformationScene(Scene):
self.plane = self.get_transformable_plane()
self.add(self.plane)
def get_transformable_plane(self, x_range = None, y_range = None):
def get_transformable_plane(self, x_range=None, y_range=None):
"""
x_range and y_range would be tuples (min, max)
"""
@@ -76,11 +77,11 @@ class ComplexTransformationScene(Scene):
if x_range is not None:
x_min, x_max = x_range
plane_config["x_radius"] = x_max - x_min
shift_val += (x_max+x_min)*RIGHT/2.
shift_val += (x_max + x_min) * RIGHT / 2.
if y_range is not None:
y_min, y_max = y_range
plane_config["y_radius"] = y_max - y_min
shift_val += (y_max+y_min)*UP/2.
shift_val += (y_max + y_min) * UP / 2.
plane = ComplexPlane(**plane_config)
plane.shift(shift_val)
if self.use_multicolored_plane:
@@ -92,7 +93,7 @@ class ComplexTransformationScene(Scene):
mob.prepare_for_nonlinear_transform(
self.num_anchors_to_add_per_line
)
#TODO...
# TODO...
def paint_plane(self, plane):
for lines in plane.main_lines, plane.secondary_lines:
@@ -109,7 +110,7 @@ class ComplexTransformationScene(Scene):
def z_to_point(self, z):
return self.background.number_to_point(z)
def get_transformer(self, **kwargs):
transform_kwargs = dict(self.default_apply_complex_function_kwargs)
transform_kwargs.update(kwargs)
@@ -117,16 +118,15 @@ class ComplexTransformationScene(Scene):
self.prepare_for_transformation(plane)
transformer = VGroup(
plane, *self.transformable_mobjects
)
)
return transformer, transform_kwargs
def apply_complex_function(self, func, added_anims = [], **kwargs):
def apply_complex_function(self, func, added_anims=[], **kwargs):
transformer, transform_kwargs = self.get_transformer(**kwargs)
transformer.generate_target()
#Rescale, apply function, scale back
# Rescale, apply function, scale back
transformer.target.shift(-self.background.get_center_point())
transformer.target.scale(1./self.background.unit_size)
transformer.target.scale(1. / self.background.unit_size)
transformer.target.apply_complex_function(func)
transformer.target.scale(self.background.unit_size)
transformer.target.shift(self.background.get_center_point())
@@ -135,14 +135,16 @@ class ComplexTransformationScene(Scene):
for mob in transformer.target[0].family_members_with_points():
mob.make_smooth()
if self.post_transformation_stroke_width is not None:
transformer.target.set_stroke(width = self.post_transformation_stroke_width)
transformer.target.set_stroke(
width=self.post_transformation_stroke_width)
self.play(
MoveToTarget(transformer, **transform_kwargs),
*added_anims
)
def apply_complex_homotopy(self, complex_homotopy, added_anims = [], **kwargs):
def apply_complex_homotopy(self, complex_homotopy, added_anims=[], **kwargs):
transformer, transform_kwargs = self.get_transformer(**kwargs)
def homotopy(x, y, z, t):
output = complex_homotopy(complex(x, y), t)
rescaled_output = self.z_to_point(output)
@@ -155,24 +157,3 @@ class ComplexTransformationScene(Scene):
),
*added_anims
)

View File

@@ -1,10 +1,7 @@
from constants import *
from mobject.mobject import Mobject
from mobject.svg.tex_mobject import TexMobject
from mobject.svg.tex_mobject import TextMobject
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from animation.creation import ShowCreation
from animation.creation import FadeIn
@@ -18,17 +15,18 @@ from scene.scene import Scene
class CountingScene(Scene):
CONFIG = {
"digit_place_colors" : [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D],
"counting_dot_starting_position" : (FRAME_X_RADIUS-1)*RIGHT + (FRAME_Y_RADIUS-1)*UP,
"count_dot_starting_radius" : 0.5,
"dot_configuration_height" : 2,
"ones_configuration_location" : UP+2*RIGHT,
"num_scale_factor" : 2,
"num_start_location" : 2*DOWN,
"digit_place_colors": [YELLOW, MAROON_B, RED, GREEN, BLUE, PURPLE_D],
"counting_dot_starting_position": (FRAME_X_RADIUS - 1) * RIGHT + (FRAME_Y_RADIUS - 1) * UP,
"count_dot_starting_radius": 0.5,
"dot_configuration_height": 2,
"ones_configuration_location": UP + 2 * RIGHT,
"num_scale_factor": 2,
"num_start_location": 2 * DOWN,
}
def setup(self):
self.dots = VGroup()
self.number = 0
self.number = 0
self.max_place = 0
self.number_mob = VGroup(TexMobject(str(self.number)))
self.number_mob.scale(self.num_scale_factor)
@@ -43,26 +41,25 @@ class CountingScene(Scene):
self.add(self.number_mob)
def get_template_configuration(self, place):
#This should probably be replaced for non-base-10 counting scenes
down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN
# This should probably be replaced for non-base-10 counting scenes
down_right = (0.5) * RIGHT + (np.sqrt(3) / 2) * DOWN
result = []
for down_right_steps in range(5):
for left_steps in range(down_right_steps):
result.append(
down_right_steps*down_right + left_steps*LEFT
down_right_steps * down_right + left_steps * LEFT
)
return reversed(result[:self.get_place_max(place)])
def get_dot_template(self, place):
#This should be replaced for non-base-10 counting scenes
down_right = (0.5)*RIGHT + (np.sqrt(3)/2)*DOWN
# This should be replaced for non-base-10 counting scenes
dots = VGroup(*[
Dot(
point,
radius = 0.25,
fill_opacity = 0,
stroke_width = 2,
stroke_color = WHITE,
point,
radius=0.25,
fill_opacity=0,
stroke_width=2,
stroke_color=WHITE,
)
for point in self.get_template_configuration(place)
])
@@ -72,9 +69,9 @@ class CountingScene(Scene):
def add_configuration(self):
new_template = self.get_dot_template(len(self.dot_templates))
new_template.move_to(self.ones_configuration_location)
left_vect = (new_template.get_width()+LARGE_BUFF)*LEFT
left_vect = (new_template.get_width() + LARGE_BUFF) * LEFT
new_template.shift(
left_vect*len(self.dot_templates)
left_vect * len(self.dot_templates)
)
self.dot_templates.append(new_template)
self.dot_template_iterators.append(
@@ -82,27 +79,27 @@ class CountingScene(Scene):
)
self.curr_configurations.append(VGroup())
def count(self, max_val, run_time_per_anim = 1):
def count(self, max_val, run_time_per_anim=1):
for x in range(max_val):
self.increment(run_time_per_anim)
def increment(self, run_time_per_anim = 1):
def increment(self, run_time_per_anim=1):
moving_dot = Dot(
self.counting_dot_starting_position,
radius = self.count_dot_starting_radius,
color = self.digit_place_colors[0],
radius=self.count_dot_starting_radius,
color=self.digit_place_colors[0],
)
moving_dot.generate_target()
moving_dot.set_fill(opacity = 0)
moving_dot.set_fill(opacity=0)
kwargs = {
"run_time" : run_time_per_anim
"run_time": run_time_per_anim
}
continue_rolling_over = True
first_move = True
place = 0
while continue_rolling_over:
added_anims = []
added_anims = []
if first_move:
added_anims += self.get_digit_increment_animations()
first_move = False
@@ -112,26 +109,25 @@ class CountingScene(Scene):
self.play(MoveToTarget(moving_dot), *added_anims, **kwargs)
self.curr_configurations[place].add(moving_dot)
if len(self.curr_configurations[place].split()) == self.get_place_max(place):
full_configuration = self.curr_configurations[place]
self.curr_configurations[place] = VGroup()
place += 1
center = full_configuration.get_center_of_mass()
radius = 0.6*max(
radius = 0.6 * max(
full_configuration.get_width(),
full_configuration.get_height(),
)
circle = Circle(
radius = radius,
stroke_width = 0,
fill_color = self.digit_place_colors[place],
fill_opacity = 0.5,
radius=radius,
stroke_width=0,
fill_color=self.digit_place_colors[place],
fill_opacity=0.5,
)
circle.move_to(center)
moving_dot = VGroup(circle, full_configuration)
moving_dot.generate_target()
moving_dot[0].set_fill(opacity = 0)
moving_dot[0].set_fill(opacity=0)
else:
continue_rolling_over = False
@@ -139,23 +135,24 @@ class CountingScene(Scene):
result = []
self.number += 1
is_next_digit = self.is_next_digit()
if is_next_digit: self.max_place += 1
if is_next_digit:
self.max_place += 1
new_number_mob = self.get_number_mob(self.number)
new_number_mob.move_to(self.number_mob, RIGHT)
if is_next_digit:
self.add_configuration()
place = len(new_number_mob.split())-1
place = len(new_number_mob.split()) - 1
result.append(FadeIn(self.dot_templates[place]))
arrow = Arrow(
new_number_mob[place].get_top(),
self.dot_templates[place].get_bottom(),
color = self.digit_place_colors[place]
color=self.digit_place_colors[place]
)
self.arrows.add(arrow)
result.append(ShowCreation(arrow))
result.append(Transform(
self.number_mob, new_number_mob,
submobject_mode = "lagged_start"
submobject_mode="lagged_start"
))
return result
@@ -169,35 +166,42 @@ class CountingScene(Scene):
self.digit_place_colors += self.digit_place_colors
digit.set_color(self.digit_place_colors[place])
digit.scale(self.num_scale_factor)
digit.next_to(result, LEFT, buff = SMALL_BUFF, aligned_edge = DOWN)
digit.next_to(result, LEFT, buff=SMALL_BUFF, aligned_edge=DOWN)
result.add(digit)
place += 1
return result
def is_next_digit(self):
return False
def get_place_num(self, num, place):
return 0
def get_place_max(self, place):
return 0
class PowerCounter(CountingScene):
def is_next_digit(self):
number = self.number
while number > 1:
if number%self.base != 0:
if number % self.base != 0:
return False
number /= self.base
return True
def get_place_max(self, place):
return self.base
def get_place_num(self, num, place):
return (num / (self.base ** place)) % self.base
class CountInDecimal(PowerCounter):
CONFIG = {
"base" : 10,
"base": 10,
}
def construct(self):
for x in range(11):
self.increment()
@@ -206,35 +210,41 @@ class CountInDecimal(PowerCounter):
for x in range(20):
self.increment()
class CountInTernary(PowerCounter):
CONFIG = {
"base" : 3,
"dot_configuration_height" : 1,
"ones_configuration_location" : UP+4*RIGHT
"base": 3,
"dot_configuration_height": 1,
"ones_configuration_location": UP + 4 * RIGHT
}
def construct(self):
self.count(27)
# def get_template_configuration(self):
# return [ORIGIN, UP]
class CountInBinaryTo256(PowerCounter):
CONFIG = {
"base" : 2,
"dot_configuration_height" : 1,
"ones_configuration_location" : UP+5*RIGHT
"base": 2,
"dot_configuration_height": 1,
"ones_configuration_location": UP + 5 * RIGHT
}
def construct(self):
self.count(128, 0.3)
def get_template_configuration(self):
return [ORIGIN, UP]
class FactorialBase(CountingScene):
CONFIG = {
"dot_configuration_height" : 1,
"ones_configuration_location" : UP+4*RIGHT
"dot_configuration_height": 1,
"ones_configuration_location": UP + 4 * RIGHT
}
def construct(self):
self.count(30, 0.4)
@@ -248,15 +258,7 @@ class FactorialBase(CountingScene):
return (num / self.factorial(place + 1)) % self.get_place_max(place)
def factorial(self, n):
if (n == 1): return 1
else: return n * self.factorial(n - 1)
if (n == 1):
return 1
else:
return n * self.factorial(n - 1)

View File

@@ -1,17 +1,11 @@
from animation.creation import ShowCreation
from animation.transform import Transform
from for_3b1b_videos.pi_creature import PiCreature
from for_3b1b_videos.pi_creature import Randolph
from for_3b1b_videos.pi_creature import get_all_pi_creature_modes
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from mobject.types.vectorized_mobject import VectorizedPoint
from scene.scene import Scene
from mobject.geometry import Circle
from mobject.geometry import Line
from mobject.geometry import Polygon
from mobject.geometry import RegularPolygon
from mobject.geometry import Square
from utils.bezier import interpolate
from utils.color import color_gradient
from utils.config_ops import digest_config
@@ -22,64 +16,70 @@ from utils.space_ops import rotation_matrix
from constants import *
def rotate(points, angle = np.pi, axis = OUT):
def rotate(points, angle=np.pi, axis=OUT):
if axis is None:
return points
matrix = rotation_matrix(angle, axis)
points = np.dot(points, np.transpose(matrix))
return points
def fractalify(vmobject, order = 3, *args, **kwargs):
def fractalify(vmobject, order=3, *args, **kwargs):
for x in range(order):
fractalification_iteration(vmobject)
return vmobject
def fractalification_iteration(vmobject, dimension = 1.05, num_inserted_anchors_range = range(1, 4)):
def fractalification_iteration(vmobject, dimension=1.05, num_inserted_anchors_range=range(1, 4)):
num_points = vmobject.get_num_points()
if num_points > 0:
# original_anchors = vmobject.get_anchors()
original_anchors = [
vmobject.point_from_proportion(x)
for x in np.linspace(0, 1-1./num_points, num_points)
for x in np.linspace(0, 1 - 1. / num_points, num_points)
]
new_anchors = []
for p1, p2, in zip(original_anchors, original_anchors[1:]):
num_inserts = random.choice(num_inserted_anchors_range)
inserted_points = [
interpolate(p1, p2, alpha)
for alpha in np.linspace(0, 1, num_inserts+2)[1:-1]
for alpha in np.linspace(0, 1, num_inserts + 2)[1:-1]
]
mass_scaling_factor = 1./(num_inserts+1)
length_scaling_factor = mass_scaling_factor**(1./dimension)
target_length = np.linalg.norm(p1-p2)*length_scaling_factor
curr_length = np.linalg.norm(p1-p2)*mass_scaling_factor
#offset^2 + curr_length^2 = target_length^2
mass_scaling_factor = 1. / (num_inserts + 1)
length_scaling_factor = mass_scaling_factor**(1. / dimension)
target_length = np.linalg.norm(p1 - p2) * length_scaling_factor
curr_length = np.linalg.norm(p1 - p2) * mass_scaling_factor
# offset^2 + curr_length^2 = target_length^2
offset_len = np.sqrt(target_length**2 - curr_length**2)
unit_vect = (p1-p2)/np.linalg.norm(p1-p2)
offset_unit_vect = rotate_vector(unit_vect, np.pi/2)
unit_vect = (p1 - p2) / np.linalg.norm(p1 - p2)
offset_unit_vect = rotate_vector(unit_vect, np.pi / 2)
inserted_points = [
point + u*offset_len*offset_unit_vect
point + u * offset_len * offset_unit_vect
for u, point in zip(it.cycle([-1, 1]), inserted_points)
]
new_anchors += [p1] + inserted_points
new_anchors.append(original_anchors[-1])
vmobject.set_points_as_corners(new_anchors)
vmobject.submobjects = [
fractalification_iteration(submob, dimension, num_inserted_anchors_range)
fractalification_iteration(
submob, dimension, num_inserted_anchors_range)
for submob in vmobject.submobjects
]
return vmobject
class SelfSimilarFractal(VMobject):
CONFIG = {
"order" : 5,
"num_subparts" : 3,
"height" : 4,
"colors" : [RED, WHITE],
"stroke_width" : 1,
"fill_opacity" : 1,
"propagate_style_to_family" : True,
"order": 5,
"num_subparts": 3,
"height": 4,
"colors": [RED, WHITE],
"stroke_width": 1,
"fill_opacity": 1,
"propagate_style_to_family": True,
}
def init_colors(self):
VMobject.init_colors(self)
self.set_color_by_gradient(*self.colors)
@@ -114,45 +114,51 @@ class SelfSimilarFractal(VMobject):
def arrange_subparts(self, *subparts):
raise Exception("Not implemented")
class Sierpinski(SelfSimilarFractal):
def get_seed_shape(self):
return Polygon(
RIGHT, np.sqrt(3)*UP, LEFT,
RIGHT, np.sqrt(3) * UP, LEFT,
)
def arrange_subparts(self, *subparts):
tri1, tri2, tri3 = subparts
tri1.move_to(tri2.get_corner(DOWN+LEFT), UP)
tri3.move_to(tri2.get_corner(DOWN+RIGHT), UP)
tri1.move_to(tri2.get_corner(DOWN + LEFT), UP)
tri3.move_to(tri2.get_corner(DOWN + RIGHT), UP)
class DiamondFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 4,
"height" : 4,
"colors" : [GREEN_E, YELLOW],
"num_subparts": 4,
"height": 4,
"colors": [GREEN_E, YELLOW],
}
def get_seed_shape(self):
return RegularPolygon(n = 4)
return RegularPolygon(n=4)
def arrange_subparts(self, *subparts):
# VGroup(*subparts).rotate(np.pi/4)
for part, vect in zip(subparts, compass_directions(start_vect = UP+RIGHT)):
part.next_to(ORIGIN, vect, buff = 0)
VGroup(*subparts).rotate(np.pi/4, about_point = ORIGIN)
for part, vect in zip(subparts, compass_directions(start_vect=UP + RIGHT)):
part.next_to(ORIGIN, vect, buff=0)
VGroup(*subparts).rotate(np.pi / 4, about_point=ORIGIN)
class PentagonalFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 5,
"colors" : [MAROON_B, YELLOW, RED],
"height" : 6,
"num_subparts": 5,
"colors": [MAROON_B, YELLOW, RED],
"height": 6,
}
def get_seed_shape(self):
return RegularPolygon(n = 5, start_angle = np.pi/2)
return RegularPolygon(n=5, start_angle=np.pi / 2)
def arrange_subparts(self, *subparts):
for x, part in enumerate(subparts):
part.shift(0.95*part.get_height()*UP)
part.rotate(2*np.pi*x/5, about_point = ORIGIN)
part.shift(0.95 * part.get_height() * UP)
part.rotate(2 * np.pi * x / 5, about_point=ORIGIN)
class PentagonalPiCreatureFractal(PentagonalFractal):
def init_colors(self):
@@ -165,30 +171,32 @@ class PentagonalPiCreatureFractal(PentagonalFractal):
colors = color_gradient(self.colors, len(internal_pis))
for pi, color in zip(internal_pis, colors):
pi.init_colors()
pi.body.set_stroke(color, width = 0.5)
pi.body.set_stroke(color, width=0.5)
pi.set_color(color)
def get_seed_shape(self):
return Randolph(mode = "shruggie")
return Randolph(mode="shruggie")
def arrange_subparts(self, *subparts):
for part in subparts:
part.rotate(2*np.pi/5, about_point = ORIGIN)
part.rotate(2 * np.pi / 5, about_point=ORIGIN)
PentagonalFractal.arrange_subparts(self, *subparts)
class PiCreatureFractal(VMobject):
CONFIG = {
"order" : 7,
"scale_val" : 2.5,
"start_mode" : "hooray",
"height" : 6,
"colors" : [
"order": 7,
"scale_val": 2.5,
"start_mode": "hooray",
"height": 6,
"colors": [
BLUE_D, BLUE_B, MAROON_B, MAROON_D, GREY,
YELLOW, RED, GREY_BROWN, RED, RED_E,
],
"random_seed" : 0,
"stroke_width" : 0,
"random_seed": 0,
"stroke_width": 0,
}
def init_colors(self):
VMobject.init_colors(self)
internal_pis = [
@@ -200,13 +208,12 @@ class PiCreatureFractal(VMobject):
for pi in reversed(internal_pis):
color = random.choice(self.colors)
pi.set_color(color)
pi.set_stroke(color, width = 0)
pi.set_stroke(color, width=0)
def generate_points(self):
random.seed(self.random_seed)
modes = get_all_pi_creature_modes()
seed = PiCreature(mode = self.start_mode)
seed = PiCreature(mode=self.start_mode)
seed.scale_to_fit_height(self.height)
seed.to_edge(DOWN)
creatures = [seed]
@@ -216,15 +223,15 @@ class PiCreatureFractal(VMobject):
for creature in creatures:
for eye, vect in zip(creature.eyes, [LEFT, RIGHT]):
new_creature = PiCreature(
mode = random.choice(modes)
mode=random.choice(modes)
)
new_creature.scale_to_fit_height(
self.scale_val*eye.get_height()
self.scale_val * eye.get_height()
)
new_creature.next_to(
eye, vect,
buff = 0,
aligned_edge = DOWN
eye, vect,
buff=0,
aligned_edge=DOWN
)
new_creatures.append(new_creature)
creature.look_at(random.choice(new_creatures))
@@ -235,70 +242,77 @@ class PiCreatureFractal(VMobject):
# VMobject.init_colors(self)
# self.set_color_by_gradient(*self.colors)
class WonkyHexagonFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 7
"num_subparts": 7
}
def get_seed_shape(self):
return RegularPolygon(n=6)
def arrange_subparts(self, *subparts):
for i, piece in enumerate(subparts):
piece.rotate(i*np.pi/12, about_point = ORIGIN)
piece.rotate(i * np.pi / 12, about_point=ORIGIN)
p1, p2, p3, p4, p5, p6, p7 = subparts
center_row = VGroup(p1, p4, p7)
center_row.arrange_submobjects(RIGHT, buff = 0)
center_row.arrange_submobjects(RIGHT, buff=0)
for p in p2, p3, p5, p6:
p.scale_to_fit_width(p1.get_width())
p2.move_to(p1.get_top(), DOWN+LEFT)
p3.move_to(p1.get_bottom(), UP+LEFT)
p5.move_to(p4.get_top(), DOWN+LEFT)
p6.move_to(p4.get_bottom(), UP+LEFT)
p2.move_to(p1.get_top(), DOWN + LEFT)
p3.move_to(p1.get_bottom(), UP + LEFT)
p5.move_to(p4.get_top(), DOWN + LEFT)
p6.move_to(p4.get_bottom(), UP + LEFT)
class CircularFractal(SelfSimilarFractal):
CONFIG = {
"num_subparts" : 3,
"colors" : [GREEN, BLUE, GREY]
"num_subparts": 3,
"colors": [GREEN, BLUE, GREY]
}
def get_seed_shape(self):
return Circle()
def arrange_subparts(self, *subparts):
if not hasattr(self, "been_here"):
self.num_subparts = 3+self.order
self.num_subparts = 3 + self.order
self.been_here = True
for i, part in enumerate(subparts):
theta = np.pi/self.num_subparts
theta = np.pi / self.num_subparts
part.next_to(
ORIGIN, UP,
buff = self.height/(2*np.tan(theta))
buff=self.height / (2 * np.tan(theta))
)
part.rotate(i*2*np.pi/self.num_subparts, about_point = ORIGIN)
part.rotate(i * 2 * np.pi / self.num_subparts, about_point=ORIGIN)
self.num_subparts -= 1
######## Space filling curves ############
class JaggedCurvePiece(VMobject):
def insert_n_anchor_points(self, n):
if self.get_num_anchor_points() == 0:
self.points = np.zeros((1, 3))
anchors = self.get_anchors()
indices = np.linspace(0, len(anchors)-1, n+len(anchors)).astype('int')
indices = np.linspace(0, len(anchors) - 1, n +
len(anchors)).astype('int')
self.set_points_as_corners(anchors[indices])
class FractalCurve(VMobject):
CONFIG = {
"radius" : 3,
"order" : 5,
"colors" : [RED, GREEN],
"num_submobjects" : 20,
"monochromatic" : False,
"order_to_stroke_width_map" : {
3 : 3,
4 : 2,
5 : 1,
"radius": 3,
"order": 5,
"colors": [RED, GREEN],
"num_submobjects": 20,
"monochromatic": False,
"order_to_stroke_width_map": {
3: 3,
4: 2,
5: 1,
},
"propagate_style_to_family" : True,
"propagate_style_to_family": True,
}
def generate_points(self):
@@ -319,19 +333,20 @@ class FractalCurve(VMobject):
self.set_color_by_gradient(*self.colors)
for order in sorted(self.order_to_stroke_width_map.keys()):
if self.order >= order:
self.set_stroke(width = self.order_to_stroke_width_map[order])
self.set_stroke(width=self.order_to_stroke_width_map[order])
def get_anchor_points(self):
raise Exception("Not implemented")
class LindenmayerCurve(FractalCurve):
CONFIG = {
"axiom" : "A",
"rule" : {},
"scale_factor" : 2,
"radius" : 3,
"start_step" : RIGHT,
"angle" : np.pi/2,
"axiom": "A",
"rule": {},
"scale_factor": 2,
"radius": 3,
"start_step": RIGHT,
"angle": np.pi / 2,
}
def expand_command_string(self, command):
@@ -350,7 +365,7 @@ class LindenmayerCurve(FractalCurve):
return result
def get_anchor_points(self):
step = float(self.radius) * self.start_step
step = float(self.radius) * self.start_step
step /= (self.scale_factor**self.order)
curr = np.zeros(3)
result = [curr]
@@ -364,14 +379,16 @@ class LindenmayerCurve(FractalCurve):
result.append(curr)
return np.array(result) - center_of_mass(result)
class SelfSimilarSpaceFillingCurve(FractalCurve):
CONFIG = {
"offsets" : [],
#keys must awkwardly be in string form...
"offset_to_rotation_axis" : {},
"scale_factor" : 2,
"radius_scale_factor" : 0.5,
"offsets": [],
# keys must awkwardly be in string form...
"offset_to_rotation_axis": {},
"scale_factor": 2,
"radius_scale_factor": 0.5,
}
def transform(self, points, offset):
"""
How to transform the copy of points shifted by
@@ -380,11 +397,11 @@ class SelfSimilarSpaceFillingCurve(FractalCurve):
copy = np.array(points)
if str(offset) in self.offset_to_rotation_axis:
copy = rotate(
copy,
axis = self.offset_to_rotation_axis[str(offset)]
copy,
axis=self.offset_to_rotation_axis[str(offset)]
)
copy /= self.scale_factor,
copy += offset*self.radius*self.radius_scale_factor
copy += offset * self.radius * self.radius_scale_factor
return copy
def refine_into_subparts(self, points):
@@ -393,11 +410,10 @@ class SelfSimilarSpaceFillingCurve(FractalCurve):
for offset in self.offsets
]
return reduce(
lambda a, b : np.append(a, b, axis = 0),
lambda a, b: np.append(a, b, axis=0),
transformed_copies
)
def get_anchor_points(self):
points = np.zeros((1, 3))
for count in range(self.order):
@@ -407,95 +423,100 @@ class SelfSimilarSpaceFillingCurve(FractalCurve):
def generate_grid(self):
raise Exception("Not implemented")
class HilbertCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"offsets" : [
LEFT+DOWN,
LEFT+UP,
RIGHT+UP,
RIGHT+DOWN,
"offsets": [
LEFT + DOWN,
LEFT + UP,
RIGHT + UP,
RIGHT + DOWN,
],
"offset_to_rotation_axis" : {
str(LEFT+DOWN) : RIGHT+UP,
str(RIGHT+DOWN) : RIGHT+DOWN,
"offset_to_rotation_axis": {
str(LEFT + DOWN): RIGHT + UP,
str(RIGHT + DOWN): RIGHT + DOWN,
},
}
}
class HilbertCurve3D(SelfSimilarSpaceFillingCurve):
CONFIG = {
"offsets" : [
RIGHT+DOWN+IN,
LEFT+DOWN+IN,
LEFT+DOWN+OUT,
RIGHT+DOWN+OUT,
RIGHT+UP+OUT,
LEFT+UP+OUT,
LEFT+UP+IN,
RIGHT+UP+IN,
"offsets": [
RIGHT + DOWN + IN,
LEFT + DOWN + IN,
LEFT + DOWN + OUT,
RIGHT + DOWN + OUT,
RIGHT + UP + OUT,
LEFT + UP + OUT,
LEFT + UP + IN,
RIGHT + UP + IN,
],
"offset_to_rotation_axis_and_angle" : {
str(RIGHT+DOWN+IN) : (LEFT+UP+OUT , 2*np.pi/3),
str(LEFT+DOWN+IN) : (RIGHT+DOWN+IN, 2*np.pi/3),
str(LEFT+DOWN+OUT) : (RIGHT+DOWN+IN, 2*np.pi/3),
str(RIGHT+DOWN+OUT) : (UP , np.pi ),
str(RIGHT+UP+OUT) : (UP , np.pi ),
str(LEFT+UP+OUT) : (LEFT+DOWN+OUT, 2*np.pi/3),
str(LEFT+UP+IN) : (LEFT+DOWN+OUT, 2*np.pi/3),
str(RIGHT+UP+IN) : (RIGHT+UP+IN , 2*np.pi/3),
"offset_to_rotation_axis_and_angle": {
str(RIGHT + DOWN + IN): (LEFT + UP + OUT, 2 * np.pi / 3),
str(LEFT + DOWN + IN): (RIGHT + DOWN + IN, 2 * np.pi / 3),
str(LEFT + DOWN + OUT): (RIGHT + DOWN + IN, 2 * np.pi / 3),
str(RIGHT + DOWN + OUT): (UP, np.pi),
str(RIGHT + UP + OUT): (UP, np.pi),
str(LEFT + UP + OUT): (LEFT + DOWN + OUT, 2 * np.pi / 3),
str(LEFT + UP + IN): (LEFT + DOWN + OUT, 2 * np.pi / 3),
str(RIGHT + UP + IN): (RIGHT + UP + IN, 2 * np.pi / 3),
},
}
# Rewrote transform method to include the rotation angle
def transform(self, points, offset):
copy = np.array(points)
copy = rotate(
copy,
axis = self.offset_to_rotation_axis_and_angle[str(offset)][0],
angle = self.offset_to_rotation_axis_and_angle[str(offset)][1],
copy,
axis=self.offset_to_rotation_axis_and_angle[str(offset)][0],
angle=self.offset_to_rotation_axis_and_angle[str(offset)][1],
)
copy /= self.scale_factor,
copy += offset*self.radius*self.radius_scale_factor
copy += offset * self.radius * self.radius_scale_factor
return copy
class PeanoCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors" : [PURPLE, TEAL],
"offsets" : [
LEFT+DOWN,
"colors": [PURPLE, TEAL],
"offsets": [
LEFT + DOWN,
LEFT,
LEFT+UP,
LEFT + UP,
UP,
ORIGIN,
DOWN,
RIGHT+DOWN,
RIGHT + DOWN,
RIGHT,
RIGHT+UP,
RIGHT + UP,
],
"offset_to_rotation_axis" : {
str(LEFT) : UP,
str(UP) : RIGHT,
str(ORIGIN) : LEFT+UP,
str(DOWN) : RIGHT,
str(RIGHT) : UP,
"offset_to_rotation_axis": {
str(LEFT): UP,
str(UP): RIGHT,
str(ORIGIN): LEFT + UP,
str(DOWN): RIGHT,
str(RIGHT): UP,
},
"scale_factor" : 3,
"radius_scale_factor" : 2.0/3,
"scale_factor": 3,
"radius_scale_factor": 2.0 / 3,
}
class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors" : [MAROON, YELLOW],
"offsets" : [
LEFT/4.+DOWN/6.,
"colors": [MAROON, YELLOW],
"offsets": [
LEFT / 4. + DOWN / 6.,
ORIGIN,
RIGHT/4.+DOWN/6.,
UP/3.,
RIGHT / 4. + DOWN / 6.,
UP / 3.,
],
"offset_to_rotation_axis" : {
"offset_to_rotation_axis": {
str(ORIGIN): RIGHT,
str(UP/3.) : UP,
str(UP / 3.): UP,
},
"scale_factor" : 2,
"radius_scale_factor" : 1.5,
"scale_factor": 2,
"radius_scale_factor": 1.5,
}
# class HexagonFillingCurve(SelfSimilarSpaceFillingCurve):
@@ -505,7 +526,7 @@ class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
# "axis_offset_pairs" : [
# (None, 1.5*DOWN + 0.5*np.sqrt(3)*LEFT),
# (UP+np.sqrt(3)*RIGHT, 1.5*DOWN + 0.5*np.sqrt(3)*RIGHT),
# (np.sqrt(3)*UP+RIGHT, ORIGIN),
# (np.sqrt(3)*UP+RIGHT, ORIGIN),
# ((UP, RIGHT), np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*LEFT),
# (None, 1.5*UP + 0.5*np.sqrt(3)*RIGHT),
@@ -524,135 +545,132 @@ class TriangleFillingCurve(SelfSimilarSpaceFillingCurve):
class UtahFillingCurve(SelfSimilarSpaceFillingCurve):
CONFIG = {
"colors" : [WHITE, BLUE_D],
"axis_offset_pairs" : [
"colors": [WHITE, BLUE_D],
"axis_offset_pairs": [
],
"scale_factor" : 3,
"radius_scale_factor" : 2/(3*np.sqrt(3)),
"scale_factor": 3,
"radius_scale_factor": 2 / (3 * np.sqrt(3)),
}
class FlowSnake(LindenmayerCurve):
CONFIG = {
"colors" : [YELLOW, GREEN],
"axiom" : "A",
"rule" : {
"A" : "A-B--B+A++AA+B-",
"B" : "+A-BB--B-A++A+B",
"colors": [YELLOW, GREEN],
"axiom": "A",
"rule": {
"A": "A-B--B+A++AA+B-",
"B": "+A-BB--B-A++A+B",
},
"radius" : 6, #TODO, this is innaccurate
"scale_factor" : np.sqrt(7),
"start_step" : RIGHT,
"angle" : -np.pi/3,
"radius": 6, # TODO, this is innaccurate
"scale_factor": np.sqrt(7),
"start_step": RIGHT,
"angle": -np.pi / 3,
}
def __init__(self, **kwargs):
LindenmayerCurve.__init__(self, **kwargs)
self.rotate(-self.order*np.pi/9, about_point = ORIGIN)
self.rotate(-self.order * np.pi / 9, about_point=ORIGIN)
class SierpinskiCurve(LindenmayerCurve):
CONFIG = {
"colors" : [RED, WHITE],
"axiom" : "B",
"rule" : {
"A" : "+B-A-B+",
"B" : "-A+B+A-",
"colors": [RED, WHITE],
"axiom": "B",
"rule": {
"A": "+B-A-B+",
"B": "-A+B+A-",
},
"radius" : 6, #TODO, this is innaccurate
"scale_factor" : 2,
"start_step" : RIGHT,
"angle" : -np.pi/3,
"radius": 6, # TODO, this is innaccurate
"scale_factor": 2,
"start_step": RIGHT,
"angle": -np.pi / 3,
}
class KochSnowFlake(LindenmayerCurve):
CONFIG = {
"colors" : [BLUE_D, WHITE, BLUE_D],
"axiom" : "A--A--A--",
"rule" : {
"A" : "A+A--A+A"
"colors": [BLUE_D, WHITE, BLUE_D],
"axiom": "A--A--A--",
"rule": {
"A": "A+A--A+A"
},
"radius" : 4,
"scale_factor" : 3,
"start_step" : RIGHT,
"angle" : np.pi/3,
"order_to_stroke_width_map" : {
3 : 3,
5 : 2,
6 : 1,
"radius": 4,
"scale_factor": 3,
"start_step": RIGHT,
"angle": np.pi / 3,
"order_to_stroke_width_map": {
3: 3,
5: 2,
6: 1,
},
}
def __init__(self, **kwargs):
digest_config(self, kwargs)
self.scale_factor = 2*(1+np.cos(self.angle))
self.scale_factor = 2 * (1 + np.cos(self.angle))
LindenmayerCurve.__init__(self, **kwargs)
class KochCurve(KochSnowFlake):
CONFIG = {
"axiom" : "A--"
"axiom": "A--"
}
class QuadraticKoch(LindenmayerCurve):
CONFIG = {
"colors" : [YELLOW, WHITE, MAROON_B],
"axiom" : "A",
"rule" : {
"A" : "A+A-A-AA+A+A-A"
"colors": [YELLOW, WHITE, MAROON_B],
"axiom": "A",
"rule": {
"A": "A+A-A-AA+A+A-A"
},
"radius" : 4,
"scale_factor" : 4,
"start_step" : RIGHT,
"angle" : np.pi/2
"radius": 4,
"scale_factor": 4,
"start_step": RIGHT,
"angle": np.pi / 2
}
class QuadraticKochIsland(QuadraticKoch):
CONFIG = {
"axiom" : "A+A+A+A"
"axiom": "A+A+A+A"
}
class StellarCurve(LindenmayerCurve):
CONFIG = {
"start_color" : RED,
"end_color" : BLUE_E,
"rule" : {
"A" : "+B-A-B+A-B+",
"B" : "-A+B+A-B+A-",
"start_color": RED,
"end_color": BLUE_E,
"rule": {
"A": "+B-A-B+A-B+",
"B": "-A+B+A-B+A-",
},
"scale_factor" : 3,
"angle" : 2*np.pi/5,
"scale_factor": 3,
"angle": 2 * np.pi / 5,
}
class SnakeCurve(FractalCurve):
CONFIG = {
"start_color" : BLUE,
"end_color" : YELLOW,
"start_color": BLUE,
"end_color": YELLOW,
}
def get_anchor_points(self):
result = []
resolution = 2**self.order
step = 2.0*self.radius / resolution
step = 2.0 * self.radius / resolution
lower_left = ORIGIN + \
LEFT*(self.radius - step/2) + \
DOWN*(self.radius - step/2)
LEFT * (self.radius - step / 2) + \
DOWN * (self.radius - step / 2)
for y in range(resolution):
x_range = range(resolution)
if y%2 == 0:
if y % 2 == 0:
x_range.reverse()
for x in x_range:
result.append(
lower_left + x*step*RIGHT + y*step*UP
lower_left + x * step * RIGHT + y * step * UP
)
return result

View File

@@ -2,8 +2,6 @@ import itertools as it
import numpy as np
import operator as op
from random import random
from constants import *
from scene.scene import Scene
@@ -13,14 +11,14 @@ from utils.space_ops import center_of_mass
class Graph():
def __init__(self):
#List of points in R^3
vertices = []
#List of pairs of indices of vertices
edges = []
#List of tuples of indices of vertices. The last should
#be a cycle whose interior is the entire graph, and when
#regions are computed its complement will be taken.
region_cycles = []
# List of points in R^3
vertices = []
# List of pairs of indices of vertices
edges = []
# List of tuples of indices of vertices. The last should
# be a cycle whose interior is the entire graph, and when
# regions are computed its complement will be taken.
region_cycles = []
self.construct()
@@ -30,18 +28,20 @@ class Graph():
def __str__(self):
return self.__class__.__name__
class CubeGraph(Graph):
"""
5 7
12
03
12
03
4 6
"""
def construct(self):
self.vertices = [
(x, y, 0)
for r in (1, 2)
for x, y in it.product([-r,r], [-r, r])
for x, y in it.product([-r, r], [-r, r])
]
self.edges = [
(0, 1),
@@ -63,9 +63,10 @@ class CubeGraph(Graph):
[4, 6, 2, 0],
[6, 7, 3, 2],
[7, 5, 1, 3],
[4, 6, 7, 5],#By convention, last region will be "outside"
[4, 6, 7, 5], # By convention, last region will be "outside"
]
class SampleGraph(Graph):
"""
4 2 3 8
@@ -73,17 +74,18 @@ class SampleGraph(Graph):
7
5 6
"""
def construct(self):
self.vertices = [
( 0, 0, 0),
( 2, 0, 0),
( 1, 2, 0),
( 3, 2, 0),
(0, 0, 0),
(2, 0, 0),
(1, 2, 0),
(3, 2, 0),
(-1, 2, 0),
(-2,-2, 0),
( 2,-2, 0),
( 4,-1, 0),
( 6, 2, 0),
(-2, -2, 0),
(2, -2, 0),
(4, -1, 0),
(6, 2, 0),
]
self.edges = [
(0, 1),
@@ -113,19 +115,21 @@ class SampleGraph(Graph):
(4, 5, 6, 7, 8, 3, 2),
]
class OctohedronGraph(Graph):
"""
3
1 0
2
4 5
"""
def construct(self):
self.vertices = [
(r*np.cos(angle), r*np.sin(angle)-1, 0)
(r * np.cos(angle), r * np.sin(angle) - 1, 0)
for r, s in [(1, 0), (3, 3)]
for angle in (np.pi/6) * np.array([s, 4 + s, 8 + s])
for angle in (np.pi / 6) * np.array([s, 4 + s, 8 + s])
]
self.edges = [
(0, 1),
@@ -152,35 +156,38 @@ class OctohedronGraph(Graph):
(3, 4, 5),
]
class CompleteGraph(Graph):
def __init__(self, num_vertices, radius = 3):
def __init__(self, num_vertices, radius=3):
self.num_vertices = num_vertices
self.radius = radius
Graph.__init__(self)
def construct(self):
self.vertices = [
(self.radius*np.cos(theta), self.radius*np.sin(theta), 0)
(self.radius * np.cos(theta), self.radius * np.sin(theta), 0)
for x in range(self.num_vertices)
for theta in [2*np.pi*x / self.num_vertices]
for theta in [2 * np.pi * x / self.num_vertices]
]
self.edges = it.combinations(range(self.num_vertices), 2)
def __str__(self):
return Graph.__str__(self) + str(self.num_vertices)
class GraphScene(Scene):
args_list = [
(CubeGraph(),),
(SampleGraph(),),
(OctohedronGraph(),),
]
@staticmethod
def args_to_string(*args):
return str(args[0])
def __init__(self, graph, *args, **kwargs):
#See CubeGraph() above for format of graph
# See CubeGraph() above for format of graph
self.graph = graph
Scene.__init__(self, *args, **kwargs)
@@ -198,19 +205,19 @@ class GraphScene(Scene):
self.region_from_cycle(cycle)
for cycle in self.graph.region_cycles
]
regions[-1].complement()#Outer region painted outwardly...
regions[-1].complement() # Outer region painted outwardly...
self.regions = regions
def region_from_cycle(self, cycle):
point_pairs = [
[
self.points[cycle[i]],
self.points[cycle[(i+1)%len(cycle)]]
self.points[cycle[i]],
self.points[cycle[(i + 1) % len(cycle)]]
]
for i in range(len(cycle))
]
return region_from_line_boundary(
*point_pairs, shape = self.shape
*point_pairs, shape=self.shape
)
def draw_vertices(self, **kwargs):
@@ -219,7 +226,7 @@ class GraphScene(Scene):
def draw_edges(self):
self.play(*[
ShowCreation(edge, run_time = 1.0)
ShowCreation(edge, run_time=1.0)
for edge in self.edges
])
@@ -227,17 +234,16 @@ class GraphScene(Scene):
self.remove(*self.vertices)
start = Mobject(*self.vertices)
end = Mobject(*[
Dot(point, radius = 3*Dot.DEFAULT_RADIUS, color = "lightgreen")
Dot(point, radius=3 * Dot.DEFAULT_RADIUS, color="lightgreen")
for point in self.points
])
self.play(Transform(
start, end, rate_func = there_and_back,
start, end, rate_func=there_and_back,
**kwargs
))
self.remove(start)
self.add(*self.vertices)
def replace_vertices_with(self, mobject):
mobject.center()
diameter = max(mobject.get_height(), mobject.get_width())
@@ -255,7 +261,7 @@ class GraphScene(Scene):
for edge in self.edges
])
def annotate_edges(self, mobject, fade_in = True, **kwargs):
def annotate_edges(self, mobject, fade_in=True, **kwargs):
angles = map(np.arctan, map(Line.get_slope, self.edges))
self.edge_annotations = [
mobject.copy().rotate(angle).move_to(edge.get_center())
@@ -267,22 +273,22 @@ class GraphScene(Scene):
for ann in self.edge_annotations
])
def trace_cycle(self, cycle = None, color = "yellow", run_time = 2.0):
def trace_cycle(self, cycle=None, color="yellow", run_time=2.0):
if cycle == None:
cycle = self.graph.region_cycles[0]
time_per_edge = run_time / len(cycle)
next_in_cycle = it.cycle(cycle)
next_in_cycle.next()#jump one ahead
next_in_cycle.next() # jump one ahead
self.traced_cycle = Mobject(*[
Line(self.points[i], self.points[j]).set_color(color)
for i, j in zip(cycle, next_in_cycle)
])
self.play(
ShowCreation(self.traced_cycle),
run_time = run_time
ShowCreation(self.traced_cycle),
run_time=run_time
)
def generate_spanning_tree(self, root = 0, color = "yellow"):
def generate_spanning_tree(self, root=0, color="yellow"):
self.spanning_tree_root = 0
pairs = deepcopy(self.graph.edges)
pairs += [tuple(reversed(pair)) for pair in pairs]
@@ -311,10 +317,10 @@ class GraphScene(Scene):
y_sep = 2
if not hasattr(self, "spanning_tree"):
self.generate_spanning_tree()
root = self.spanning_tree_root
root = self.spanning_tree_root
color = self.spanning_tree.get_color()
indices = range(len(self.points))
#Build dicts
# Build dicts
parent_of = dict([
tuple(reversed(pair))
for pair in self.spanning_tree_index_pairs
@@ -323,12 +329,12 @@ class GraphScene(Scene):
for child in parent_of:
children_of[parent_of[child]].append(child)
x_coord_of = {root : 0}
y_coord_of = {root : bottom}
#width to allocate to a given node, computed as
#the maxium number of decendents in a single generation,
#minus 1, multiplied by x_sep
width_of = {}
x_coord_of = {root: 0}
y_coord_of = {root: bottom}
# width to allocate to a given node, computed as
# the maxium number of decendents in a single generation,
# minus 1, multiplied by x_sep
width_of = {}
for index in indices:
next_generation = children_of[index]
curr_max = max(1, len(next_generation))
@@ -345,9 +351,9 @@ class GraphScene(Scene):
if index not in y_coord_of:
y_coord_of[index] = y_sep + y_coord_of[parent_of[index]]
children = children_of[index]
left_hand = x_coord_of[index]-width_of[index]/2.0
left_hand = x_coord_of[index] - width_of[index] / 2.0
for child in children:
x_coord_of[child] = left_hand + width_of[child]/2.0
x_coord_of[child] = left_hand + width_of[child] / 2.0
left_hand += width_of[child] + x_sep
to_process += children
@@ -365,7 +371,7 @@ class GraphScene(Scene):
])
def generate_dual_graph(self):
point_at_infinity = np.array([np.inf]*3)
point_at_infinity = np.array([np.inf] * 3)
cycles = self.graph.region_cycles
self.dual_points = [
center_of_mass([
@@ -378,7 +384,8 @@ class GraphScene(Scene):
Dot(point).set_color("green")
for point in self.dual_points
]
self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS + FRAME_Y_RADIUS)
self.dual_vertices[-1] = Circle().scale(FRAME_X_RADIUS +
FRAME_Y_RADIUS)
self.dual_points[-1] = point_at_infinity
self.dual_edges = []
@@ -388,43 +395,20 @@ class GraphScene(Scene):
if not (pair[0] in cycle and pair[1] in cycle):
continue
index1, index2 = cycle.index(pair[0]), cycle.index(pair[1])
if abs(index1 - index2) in [1, len(cycle)-1]:
if abs(index1 - index2) in [1, len(cycle) - 1]:
dual_point_pair.append(
self.dual_points[cycles.index(cycle)]
)
assert(len(dual_point_pair) == 2)
for i in 0, 1:
if all(dual_point_pair[i] == point_at_infinity):
new_point = np.array(dual_point_pair[1-i])
new_point = np.array(dual_point_pair[1 - i])
vect = center_of_mass([
self.points[pair[0]],
self.points[pair[1]]
]) - new_point
new_point += FRAME_X_RADIUS*vect/np.linalg.norm(vect)
new_point += FRAME_X_RADIUS * vect / np.linalg.norm(vect)
dual_point_pair[i] = new_point
self.dual_edges.append(
Line(*dual_point_pair).set_color()
)

View File

@@ -1,34 +1,22 @@
from constants import *
from mobject.geometry import AnnularSector
from mobject.geometry import Arc
from mobject.geometry import Annulus
from mobject.mobject import Mobject
from mobject.svg.svg_mobject import SVGMobject
from mobject.svg.tex_mobject import TexMobject
from mobject.types.vectorized_mobject import VGroup
from mobject.types.vectorized_mobject import VMobject
from mobject.types.vectorized_mobject import VectorizedPoint
from continual_animation.continual_animation import ContinualAnimation
from animation.animation import Animation
from animation.composition import LaggedStart
from animation.transform import ApplyMethod
from animation.transform import Transform
from animation.creation import FadeIn
from animation.creation import FadeOut
from camera.camera import Camera
from scene.scene import Scene
from camera.three_d_camera import ThreeDCamera
from scene.three_d_scene import ThreeDScene
from utils.space_ops import angle_between
from utils.space_ops import angle_between_vectors
from utils.space_ops import project_along_vector
from utils.space_ops import rotate_vector
from utils.space_ops import rotation_matrix
from utils.space_ops import z_to_vector
from scipy.spatial import ConvexHull
@@ -39,8 +27,8 @@ SHADOW_COLOR = BLACK
SWITCH_ON_RUN_TIME = 1.5
FAST_SWITCH_ON_RUN_TIME = 0.1
NUM_LEVELS = 30
NUM_CONES = 7 # in first lighthouse scene
NUM_VISIBLE_CONES = 5 # ibidem
NUM_CONES = 7 # in first lighthouse scene
NUM_VISIBLE_CONES = 5 # ibidem
ARC_TIP_LENGTH = 0.2
AMBIENT_FULL = 0.8
AMBIENT_DIMMED = 0.5
@@ -48,11 +36,15 @@ SPOTLIGHT_FULL = 0.8
SPOTLIGHT_DIMMED = 0.5
LIGHTHOUSE_HEIGHT = 0.8
DEGREES = TAU/360
DEGREES = TAU / 360
inverse_power_law = lambda maxint,scale,cutoff,exponent: \
(lambda r: maxint * (cutoff/(r/scale+cutoff))**exponent)
inverse_quadratic = lambda maxint,scale,cutoff: inverse_power_law(maxint,scale,cutoff,2)
def inverse_power_law(maxint, scale, cutoff, exponent):
return (lambda r: maxint * (cutoff / (r / scale + cutoff))**exponent)
def inverse_quadratic(maxint, scale, cutoff):
return inverse_power_law(maxint, scale, cutoff, 2)
class SwitchOn(LaggedStart):
@@ -62,12 +54,14 @@ class SwitchOn(LaggedStart):
}
def __init__(self, light, **kwargs):
if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)):
raise Exception("Only AmbientLights and Spotlights can be switched on")
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
raise Exception(
"Only AmbientLights and Spotlights can be switched on")
LaggedStart.__init__(
self, FadeIn, light, **kwargs
)
class SwitchOff(LaggedStart):
CONFIG = {
"lag_ratio": 0.2,
@@ -75,23 +69,26 @@ class SwitchOff(LaggedStart):
}
def __init__(self, light, **kwargs):
if (not isinstance(light,AmbientLight) and not isinstance(light,Spotlight)):
raise Exception("Only AmbientLights and Spotlights can be switched off")
if (not isinstance(light, AmbientLight) and not isinstance(light, Spotlight)):
raise Exception(
"Only AmbientLights and Spotlights can be switched off")
light.submobjects = light.submobjects[::-1]
LaggedStart.__init__(self,
FadeOut, light, **kwargs)
FadeOut, light, **kwargs)
light.submobjects = light.submobjects[::-1]
class Lighthouse(SVGMobject):
CONFIG = {
"file_name" : "lighthouse",
"height" : LIGHTHOUSE_HEIGHT,
"fill_color" : WHITE,
"fill_opacity" : 1.0,
"file_name": "lighthouse",
"height": LIGHTHOUSE_HEIGHT,
"fill_color": WHITE,
"fill_opacity": 1.0,
}
def move_to(self,point):
self.next_to(point, DOWN, buff = 0)
def move_to(self, point):
self.next_to(point, DOWN, buff=0)
class AmbientLight(VMobject):
@@ -104,18 +101,18 @@ class AmbientLight(VMobject):
# * the number of subdivisions (levels, annuli)
CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"opacity_function" : lambda r : 1.0/(r+1.0)**2,
"color" : LIGHT_COLOR,
"max_opacity" : 1.0,
"num_levels" : NUM_LEVELS,
"radius" : 5.0
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"opacity_function": lambda r: 1.0 / (r + 1.0)**2,
"color": LIGHT_COLOR,
"max_opacity": 1.0,
"num_levels": NUM_LEVELS,
"radius": 5.0
}
def generate_points(self):
# in theory, this method is only called once, right?
# so removing submobs shd not be necessary
#
#
# Note: Usually, yes, it is only called within Mobject.__init__,
# but there is no strong guarantee of that, and you may want certain
# update functions to regenerate points here and there.
@@ -130,71 +127,61 @@ class AmbientLight(VMobject):
for r in np.arange(0, self.radius, dr):
alpha = self.max_opacity * self.opacity_function(r)
annulus = Annulus(
inner_radius = r,
outer_radius = r + dr,
color = self.color,
fill_opacity = alpha
inner_radius=r,
outer_radius=r + dr,
color=self.color,
fill_opacity=alpha
)
annulus.move_to(self.get_source_point())
self.add(annulus)
def move_source_to(self,point):
#old_source_point = self.get_source_point()
#self.shift(point - old_source_point)
def move_source_to(self, point):
# old_source_point = self.get_source_point()
# self.shift(point - old_source_point)
self.move_to(point)
return self
def get_source_point(self):
return self.source_point.get_location()
def dimming(self,new_alpha):
def dimming(self, new_alpha):
old_alpha = self.max_opacity
self.max_opacity = new_alpha
for submob in self.submobjects:
old_submob_alpha = submob.fill_opacity
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
submob.set_fill(opacity = new_submob_alpha)
submob.set_fill(opacity=new_submob_alpha)
class Spotlight(VMobject):
CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"opacity_function" : lambda r : 1.0/(r/2+1.0)**2,
"color" : GREEN, # LIGHT_COLOR,
"max_opacity" : 1.0,
"num_levels" : 10,
"radius" : 10.0,
"screen" : None,
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"opacity_function": lambda r: 1.0 / (r / 2 + 1.0)**2,
"color": GREEN, # LIGHT_COLOR,
"max_opacity": 1.0,
"num_levels": 10,
"radius": 10.0,
"screen": None,
"camera_mob": None
}
def projection_direction(self):
# Note: This seems reasonable, though for it to work you'd
# need to be sure that any 3d scene including a spotlight
# somewhere assigns that spotlights "camera" attribute
# need to be sure that any 3d scene including a spotlight
# somewhere assigns that spotlights "camera" attribute
# to be the camera associated with that scene.
if self.camera_mob == None:
if self.camera_mob is None:
return OUT
else:
[phi, theta, r] = self.camera_mob.get_center()
v = np.array([np.sin(phi)*np.cos(theta), np.sin(phi)*np.sin(theta), np.cos(phi)])
return v #/np.linalg.norm(v)
v = np.array([np.sin(phi) * np.cos(theta),
np.sin(phi) * np.sin(theta), np.cos(phi)])
return v # /np.linalg.norm(v)
def project(self,point):
def project(self, point):
v = self.projection_direction()
w = project_along_vector(point,v)
w = project_along_vector(point, v)
return w
def get_source_point(self):
@@ -205,7 +192,7 @@ class Spotlight(VMobject):
self.add(self.source_point)
if self.screen != None:
if self.screen is not None:
# look for the screen and create annular sectors
lower_angle, upper_angle = self.viewing_angles(self.screen)
self.radius = float(self.radius)
@@ -213,18 +200,18 @@ class Spotlight(VMobject):
lower_ray, upper_ray = self.viewing_rays(self.screen)
for r in np.arange(0, self.radius, dr):
new_sector = self.new_sector(r,dr,lower_angle,upper_angle)
new_sector = self.new_sector(r, dr, lower_angle, upper_angle)
self.add(new_sector)
def new_sector(self,r,dr,lower_angle,upper_angle):
def new_sector(self, r, dr, lower_angle, upper_angle):
alpha = self.max_opacity * self.opacity_function(r)
annular_sector = AnnularSector(
inner_radius = r,
outer_radius = r + dr,
color = self.color,
fill_opacity = alpha,
start_angle = lower_angle,
angle = upper_angle - lower_angle
inner_radius=r,
outer_radius=r + dr,
color=self.color,
fill_opacity=alpha,
start_angle=lower_angle,
angle=upper_angle - lower_angle
)
# rotate (not project) it into the viewing plane
rotation_matrix = z_to_vector(self.projection_direction())
@@ -232,13 +219,13 @@ class Spotlight(VMobject):
# now rotate it inside that plane
rotated_RIGHT = np.dot(RIGHT, rotation_matrix.T)
projected_RIGHT = self.project(RIGHT)
omega = angle_between_vectors(rotated_RIGHT,projected_RIGHT)
annular_sector.rotate(omega, axis = self.projection_direction())
omega = angle_between_vectors(rotated_RIGHT, projected_RIGHT)
annular_sector.rotate(omega, axis=self.projection_direction())
annular_sector.move_arc_center_to(self.get_source_point())
return annular_sector
def viewing_angle_of_point(self,point):
def viewing_angle_of_point(self, point):
# as measured from the positive x-axis
v1 = self.project(RIGHT)
v2 = self.project(np.array(point) - self.get_source_point())
@@ -247,63 +234,66 @@ class Spotlight(VMobject):
# choice of orientation. That choice is set by the camera
# position, i. e. projection direction
if np.dot(self.projection_direction(),np.cross(v1, v2)) > 0:
if np.dot(self.projection_direction(), np.cross(v1, v2)) > 0:
return absolute_angle
else:
return -absolute_angle
def viewing_angles(self,screen):
def viewing_angles(self, screen):
screen_points = screen.get_anchors()
projected_screen_points = map(self.project,screen_points)
projected_screen_points = map(self.project, screen_points)
viewing_angles = np.array(map(self.viewing_angle_of_point,
projected_screen_points))
projected_screen_points))
lower_angle = upper_angle = 0
if len(viewing_angles) != 0:
lower_angle = np.min(viewing_angles)
upper_angle = np.max(viewing_angles)
if upper_angle - lower_angle > TAU/2:
if upper_angle - lower_angle > TAU / 2:
lower_angle, upper_angle = upper_angle, lower_angle + TAU
return lower_angle, upper_angle
def viewing_rays(self,screen):
def viewing_rays(self, screen):
lower_angle, upper_angle = self.viewing_angles(screen)
projected_RIGHT = self.project(RIGHT)/np.linalg.norm(self.project(RIGHT))
lower_ray = rotate_vector(projected_RIGHT,lower_angle, axis = self.projection_direction())
upper_ray = rotate_vector(projected_RIGHT,upper_angle, axis = self.projection_direction())
projected_RIGHT = self.project(
RIGHT) / np.linalg.norm(self.project(RIGHT))
lower_ray = rotate_vector(
projected_RIGHT, lower_angle, axis=self.projection_direction())
upper_ray = rotate_vector(
projected_RIGHT, upper_angle, axis=self.projection_direction())
return lower_ray, upper_ray
def opening_angle(self):
l,u = self.viewing_angles(self.screen)
l, u = self.viewing_angles(self.screen)
return u - l
def start_angle(self):
l,u = self.viewing_angles(self.screen)
l, u = self.viewing_angles(self.screen)
return l
def stop_angle(self):
l,u = self.viewing_angles(self.screen)
l, u = self.viewing_angles(self.screen)
return u
def move_source_to(self,point):
def move_source_to(self, point):
self.source_point.set_location(np.array(point))
#self.source_point.move_to(np.array(point))
#self.move_to(point)
# self.source_point.move_to(np.array(point))
# self.move_to(point)
self.update_sectors()
return self
def update_sectors(self):
if self.screen == None:
if self.screen is None:
return
for submob in self.submobjects:
if type(submob) == AnnularSector:
lower_angle, upper_angle = self.viewing_angles(self.screen)
#dr = submob.outer_radius - submob.inner_radius
# dr = submob.outer_radius - submob.inner_radius
dr = self.radius / self.num_levels
new_submob = self.new_sector(
submob.inner_radius, dr, lower_angle, upper_angle
@@ -312,7 +302,7 @@ class Spotlight(VMobject):
# submob.set_fill(opacity = 10 * self.opacity_function(submob.outer_radius))
Transform(submob, new_submob).update(1)
def dimming(self,new_alpha):
def dimming(self, new_alpha):
old_alpha = self.max_opacity
self.max_opacity = new_alpha
for submob in self.submobjects:
@@ -322,26 +312,28 @@ class Spotlight(VMobject):
# it's the shadow, don't dim it
continue
old_submob_alpha = submob.fill_opacity
new_submob_alpha = old_submob_alpha * new_alpha/old_alpha
submob.set_fill(opacity = new_submob_alpha)
new_submob_alpha = old_submob_alpha * new_alpha / old_alpha
submob.set_fill(opacity=new_submob_alpha)
def change_opacity_function(self,new_f):
def change_opacity_function(self, new_f):
self.opacity_function = new_f
dr = self.radius/self.num_levels
dr = self.radius / self.num_levels
sectors = []
for submob in self.submobjects:
if type(submob) == AnnularSector:
sectors.append(submob)
for (r,submob) in zip(np.arange(0,self.radius,dr),sectors):
for (r, submob) in zip(np.arange(0, self.radius, dr), sectors):
if type(submob) != AnnularSector:
# it's the shadow, don't dim it
continue
alpha = self.opacity_function(r)
submob.set_fill(opacity = alpha)
submob.set_fill(opacity=alpha)
# Warning: This class is likely quite buggy.
class LightSource(VMobject):
# combines:
# a lighthouse
@@ -349,12 +341,12 @@ class LightSource(VMobject):
# a spotlight
# and a shadow
CONFIG = {
"source_point": VectorizedPoint(location = ORIGIN, stroke_width = 0, fill_opacity = 0),
"source_point": VectorizedPoint(location=ORIGIN, stroke_width=0, fill_opacity=0),
"color": LIGHT_COLOR,
"num_levels": 10,
"radius": 10.0,
"screen": None,
"opacity_function": inverse_quadratic(1,2,1),
"opacity_function": inverse_quadratic(1, 2, 1),
"max_opacity_ambient": AMBIENT_FULL,
"max_opacity_spotlight": SPOTLIGHT_FULL,
"camera_mob": None
@@ -366,39 +358,41 @@ class LightSource(VMobject):
self.lighthouse = Lighthouse()
self.ambient_light = AmbientLight(
source_point = VectorizedPoint(location = self.get_source_point()),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_ambient
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_ambient
)
if self.has_screen():
self.spotlight = Spotlight(
source_point = VectorizedPoint(location = self.get_source_point()),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
screen = self.screen,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_spotlight,
camera_mob = self.camera_mob
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
screen=self.screen,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_spotlight,
camera_mob=self.camera_mob
)
else:
self.spotlight = Spotlight()
self.shadow = VMobject(fill_color = SHADOW_COLOR, fill_opacity = 1.0, stroke_color = BLACK)
self.lighthouse.next_to(self.get_source_point(),DOWN,buff = 0)
self.shadow = VMobject(fill_color=SHADOW_COLOR,
fill_opacity=1.0, stroke_color=BLACK)
self.lighthouse.next_to(self.get_source_point(), DOWN, buff=0)
self.ambient_light.move_source_to(self.get_source_point())
if self.has_screen():
self.spotlight.move_source_to(self.get_source_point())
self.update_shadow()
self.add(self.ambient_light,self.spotlight,self.lighthouse, self.shadow)
self.add(self.ambient_light, self.spotlight,
self.lighthouse, self.shadow)
def has_screen(self):
if self.screen == None:
if self.screen is None:
return False
elif np.size(self.screen.points) == 0:
return False
@@ -408,18 +402,18 @@ class LightSource(VMobject):
def dim_ambient(self):
self.set_max_opacity_ambient(AMBIENT_DIMMED)
def set_max_opacity_ambient(self,new_opacity):
def set_max_opacity_ambient(self, new_opacity):
self.max_opacity_ambient = new_opacity
self.ambient_light.dimming(new_opacity)
def dim_spotlight(self):
self.set_max_opacity_spotlight(SPOTLIGHT_DIMMED)
def set_max_opacity_spotlight(self,new_opacity):
def set_max_opacity_spotlight(self, new_opacity):
self.max_opacity_spotlight = new_opacity
self.spotlight.dimming(new_opacity)
def set_camera_mob(self,new_cam_mob):
def set_camera_mob(self, new_cam_mob):
self.camera_mob = new_cam_mob
self.spotlight.camera_mob = new_cam_mob
@@ -432,40 +426,40 @@ class LightSource(VMobject):
camera_mob = self.spotlight.camera_mob
self.remove(self.spotlight)
self.spotlight = Spotlight(
source_point = VectorizedPoint(location = self.get_source_point()),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
screen = new_screen,
camera_mob = self.camera_mob,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_spotlight,
source_point=VectorizedPoint(location=self.get_source_point()),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
screen=new_screen,
camera_mob=self.camera_mob,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_spotlight,
)
self.spotlight.move_source_to(self.get_source_point())
# Note: This line will make spotlight show up at the end
# of the submojects list, which can make it show up on
# top of the shadow. To make it show up in the
# same spot, you could try the following line,
# same spot, you could try the following line,
# where "index" is what I defined above:
self.submobjects.insert(index, self.spotlight)
#self.add(self.spotlight)
# self.add(self.spotlight)
# in any case
self.screen = new_screen
def move_source_to(self,point):
def move_source_to(self, point):
apoint = np.array(point)
v = apoint - self.get_source_point()
# Note: As discussed, things stand to behave better if source
# point is a submobject, so that it automatically interpolates
# during an animation, and other updates can be defined wrt
# during an animation, and other updates can be defined wrt
# that source point's location
self.source_point.set_location(apoint)
#self.lighthouse.next_to(apoint,DOWN,buff = 0)
#self.ambient_light.move_source_to(apoint)
# self.lighthouse.next_to(apoint,DOWN,buff = 0)
# self.ambient_light.move_source_to(apoint)
self.lighthouse.shift(v)
#self.ambient_light.shift(v)
# self.ambient_light.shift(v)
self.ambient_light.move_source_to(apoint)
if self.has_screen():
self.spotlight.move_source_to(apoint)
@@ -474,8 +468,8 @@ class LightSource(VMobject):
def change_spotlight_opacity_function(self, new_of):
self.spotlight.change_opacity_function(new_of)
def set_radius(self,new_radius):
def set_radius(self, new_radius):
self.radius = new_radius
self.ambient_light.radius = new_radius
self.spotlight.radius = new_radius
@@ -496,12 +490,12 @@ class LightSource(VMobject):
def update_ambient(self):
new_ambient_light = AmbientLight(
source_point = VectorizedPoint(location = ORIGIN),
color = self.color,
num_levels = self.num_levels,
radius = self.radius,
opacity_function = self.opacity_function,
max_opacity = self.max_opacity_ambient
source_point=VectorizedPoint(location=ORIGIN),
color=self.color,
num_levels=self.num_levels,
radius=self.radius,
opacity_function=self.opacity_function,
max_opacity=self.max_opacity_ambient
)
new_ambient_light.apply_matrix(self.rotation_matrix())
new_ambient_light.move_source_to(self.get_source_point())
@@ -512,13 +506,12 @@ class LightSource(VMobject):
def rotation_matrix(self):
if self.camera_mob == None:
if self.camera_mob is None:
return np.eye(3)
phi = self.camera_mob.get_center()[0]
theta = self.camera_mob.get_center()[1]
R1 = np.array([
[1, 0, 0],
[0, np.cos(phi), -np.sin(phi)],
@@ -526,8 +519,8 @@ class LightSource(VMobject):
])
R2 = np.array([
[np.cos(theta + TAU/4), -np.sin(theta + TAU/4), 0],
[np.sin(theta + TAU/4), np.cos(theta + TAU/4), 0],
[np.cos(theta + TAU / 4), -np.sin(theta + TAU / 4), 0],
[np.sin(theta + TAU / 4), np.cos(theta + TAU / 4), 0],
[0, 0, 1]
])
@@ -542,28 +535,31 @@ class LightSource(VMobject):
for point in self.screen.get_anchors():
projected_screen_points.append(self.spotlight.project(point))
projected_source = project_along_vector(self.get_source_point(),self.spotlight.projection_direction())
projected_source = project_along_vector(
self.get_source_point(), self.spotlight.projection_direction())
projected_point_cloud_3d = np.append(
projected_screen_points,
np.reshape(projected_source,(1,3)),
axis = 0
np.reshape(projected_source, (1, 3)),
axis=0
)
rotation_matrix = self.rotation_matrix() # z_to_vector(self.spotlight.projection_direction())
back_rotation_matrix = rotation_matrix.T # i. e. its inverse
# z_to_vector(self.spotlight.projection_direction())
rotation_matrix = self.rotation_matrix()
back_rotation_matrix = rotation_matrix.T # i. e. its inverse
rotated_point_cloud_3d = np.dot(projected_point_cloud_3d,back_rotation_matrix.T)
rotated_point_cloud_3d = np.dot(
projected_point_cloud_3d, back_rotation_matrix.T)
# these points now should all have z = 0
point_cloud_2d = rotated_point_cloud_3d[:,:2]
point_cloud_2d = rotated_point_cloud_3d[:, :2]
# now we can compute the convex hull
hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw
hull_2d = ConvexHull(point_cloud_2d) # guaranteed to run ccw
hull = []
# we also need the projected source point
source_point_2d = np.dot(self.spotlight.project(self.get_source_point()),back_rotation_matrix.T)[:2]
source_point_2d = np.dot(self.spotlight.project(
self.get_source_point()), back_rotation_matrix.T)[:2]
index = 0
for point in point_cloud_2d[hull_2d.vertices]:
if np.all(np.abs(point - source_point_2d) < 1.0e-6):
@@ -574,35 +570,35 @@ class LightSource(VMobject):
hull.append(point_3d)
index += 1
hull_mobject = VMobject()
hull_mobject.set_points_as_corners(hull)
hull_mobject.apply_matrix(rotation_matrix)
anchors = hull_mobject.get_anchors()
# add two control points for the outer cone
if np.size(anchors) == 0:
if np.size(anchors) == 0:
self.shadow.points = []
return
ray1 = anchors[source_index - 1] - projected_source
ray1 = ray1/np.linalg.norm(ray1) * 100
ray1 = ray1 / np.linalg.norm(ray1) * 100
ray2 = anchors[source_index] - projected_source
ray2 = ray2/np.linalg.norm(ray2) * 100
ray2 = ray2 / np.linalg.norm(ray2) * 100
outpoint1 = anchors[source_index - 1] + ray1
outpoint2 = anchors[source_index] + ray2
new_anchors = anchors[:source_index]
new_anchors = np.append(new_anchors,np.array([outpoint1, outpoint2]),axis = 0)
new_anchors = np.append(new_anchors,anchors[source_index:],axis = 0)
new_anchors = np.append(new_anchors, np.array(
[outpoint1, outpoint2]), axis=0)
new_anchors = np.append(new_anchors, anchors[source_index:], axis=0)
self.shadow.set_points_as_corners(new_anchors)
# shift it closer to the camera so it is in front of the spotlight
self.shadow.mark_paths_closed = True
class ScreenTracker(ContinualAnimation):
def __init__(self, light_source, **kwargs):
self.light_source = light_source
@@ -611,20 +607,3 @@ class ScreenTracker(ContinualAnimation):
def update_mobject(self, dt):
self.light_source.update()

View File

@@ -1,5 +1,7 @@
import numpy as np
from constants import *
from animation.creation import ShowCreation
from animation.transform import ApplyMethod
from animation.creation import FadeOut
@@ -11,12 +13,14 @@ from scene.scene import Scene
from mobject.geometry import Circle
from mobject.geometry import Line
class NumericalMatrixMultiplication(Scene):
CONFIG = {
"left_matrix" : [[1, 2], [3, 4]],
"right_matrix" : [[5, 6], [7, 8]],
"use_parens" : True,
"left_matrix": [[1, 2], [3, 4]],
"right_matrix": [[5, 6], [7, 8]],
"use_parens": True,
}
def construct(self):
left_string_matrix, right_string_matrix = [
np.array(matrix).astype("string")
@@ -34,25 +38,24 @@ class NumericalMatrixMultiplication(Scene):
self.organize_matrices(left, right, result)
self.animate_product(left, right, result)
def get_result_matrix(self, left, right):
(m, k), n = left.shape, right.shape[1]
mob_matrix = np.array([VGroup()]).repeat(m*n).reshape((m, n))
mob_matrix = np.array([VGroup()]).repeat(m * n).reshape((m, n))
for a in range(m):
for b in range(n):
template = "(%s)(%s)" if self.use_parens else "%s%s"
parts = [
prefix + template%(left[a][c], right[c][b])
prefix + template % (left[a][c], right[c][b])
for c in range(k)
for prefix in ["" if c == 0 else "+"]
]
mob_matrix[a][b] = TexMobject(parts, next_to_buff = 0.1)
mob_matrix[a][b] = TexMobject(parts, next_to_buff=0.1)
return Matrix(mob_matrix)
def add_lines(self, left, right):
line_kwargs = {
"color" : BLUE,
"stroke_width" : 2,
"color": BLUE,
"stroke_width": 2,
}
left_rows = [
VGroup(*row) for row in left.get_mob_matrix()
@@ -60,7 +63,7 @@ class NumericalMatrixMultiplication(Scene):
h_lines = VGroup()
for row in left_rows[:-1]:
h_line = Line(row.get_left(), row.get_right(), **line_kwargs)
h_line.next_to(row, DOWN, buff = left.v_buff/2.)
h_line.next_to(row, DOWN, buff=left.v_buff / 2.)
h_lines.add(h_line)
right_cols = [
@@ -69,7 +72,7 @@ class NumericalMatrixMultiplication(Scene):
v_lines = VGroup()
for col in right_cols[:-1]:
v_line = Line(col.get_top(), col.get_bottom(), **line_kwargs)
v_line.next_to(col, RIGHT, buff = right.h_buff/2.)
v_line.next_to(col, RIGHT, buff=right.h_buff / 2.)
v_lines.add(v_line)
self.play(ShowCreation(h_lines))
@@ -81,17 +84,16 @@ class NumericalMatrixMultiplication(Scene):
equals = TexMobject("=")
everything = VGroup(left, right, equals, result)
everything.arrange_submobjects()
everything.scale_to_fit_width(FRAME_WIDTH-1)
everything.scale_to_fit_width(FRAME_WIDTH - 1)
self.add(everything)
def animate_product(self, left, right, result):
l_matrix = left.get_mob_matrix()
r_matrix = right.get_mob_matrix()
result_matrix = result.get_mob_matrix()
circle = Circle(
radius = l_matrix[0][0].get_height(),
color = GREEN
radius=l_matrix[0][0].get_height(),
color=GREEN
)
circles = VGroup(*[
entry.get_point_mobject()
@@ -120,10 +122,10 @@ class NumericalMatrixMultiplication(Scene):
self.play(Transform(circles, new_circles))
self.play(
Transform(
start_parts,
result_entry.copy().set_color(YELLOW),
path_arc = -np.pi/2,
submobject_mode = "all_at_once",
start_parts,
result_entry.copy().set_color(YELLOW),
path_arc=-np.pi / 2,
submobject_mode="all_at_once",
),
*lagging_anims
)
@@ -137,4 +139,4 @@ class NumericalMatrixMultiplication(Scene):
l_matrix[a][c].set_color(WHITE)
r_matrix[c][b].set_color(WHITE)
self.play(FadeOut(circles), *lagging_anims)
self.wait()
self.wait()

View File

@@ -1,6 +1,4 @@
import numpy as np
import itertools as it
from PIL import Image
from copy import deepcopy
from mobject.mobject import Mobject
@@ -10,11 +8,13 @@ from constants import *
# Warning: This is all now pretty depricated, and should not be expected to work
class Region(Mobject):
CONFIG = {
"display_mode" : "region"
"display_mode": "region"
}
def __init__(self, condition = (lambda x, y : True), **kwargs):
def __init__(self, condition=(lambda x, y: True), **kwargs):
"""
Condition must be a function which takes in two real
arrays (representing x and y values of space respectively)
@@ -26,25 +26,26 @@ class Region(Mobject):
self.condition = condition
def _combine(self, region, op):
self.condition = lambda x, y : op(
self.condition = lambda x, y: op(
self.condition(x, y),
region.condition(x, y)
)
def union(self, region):
self._combine(region, lambda bg1, bg2 : bg1 | bg2)
self._combine(region, lambda bg1, bg2: bg1 | bg2)
return self
def intersect(self, region):
self._combine(region, lambda bg1, bg2 : bg1 & bg2)
self._combine(region, lambda bg1, bg2: bg1 & bg2)
return self
def complement(self):
self.bool_grid = ~self.bool_grid
return self
class HalfPlane(Region):
def __init__(self, point_pair, upper_left = True, *args, **kwargs):
def __init__(self, point_pair, upper_left=True, *args, **kwargs):
"""
point_pair of the form [(x_0, y_0,...), (x_1, y_1,...)]
@@ -56,16 +57,19 @@ class HalfPlane(Region):
point_pair = list(point_pair)
point_pair.reverse()
(x0, y0), (x1, y1) = point_pair[0][:2], point_pair[1][:2]
def condition(x, y):
return (x1 - x0)*(y - y0) > (y1 - y0)*(x - x0)
return (x1 - x0) * (y - y0) > (y1 - y0) * (x - x0)
Region.__init__(self, condition, *args, **kwargs)
def region_from_line_boundary(*lines, **kwargs):
reg = Region(**kwargs)
for line in lines:
reg.intersect(HalfPlane(line, **kwargs))
return reg
def region_from_polygon_vertices(*vertices, **kwargs):
return region_from_line_boundary(*adjacent_pairs(vertices), **kwargs)
@@ -81,7 +85,7 @@ def plane_partition(*lines, **kwargs):
half_planes = [HalfPlane(line, **kwargs) for line in lines]
complements = [deepcopy(hp).complement() for hp in half_planes]
num_lines = len(lines)
for bool_list in it.product(*[[True, False]]*num_lines):
for bool_list in it.product(*[[True, False]] * num_lines):
reg = Region(**kwargs)
for i in range(num_lines):
if bool_list[i]:
@@ -92,6 +96,7 @@ def plane_partition(*lines, **kwargs):
result.append(reg)
return result
def plane_partition_from_points(*points, **kwargs):
"""
Returns list of regions cut out by the complete graph
@@ -101,9 +106,3 @@ def plane_partition_from_points(*points, **kwargs):
"""
lines = [[p1, p2] for (p1, p2) in it.combinations(points, 2)]
return plane_partition(*lines, **kwargs)

132
primes.py
View File

@@ -1,5 +1,6 @@
from big_ol_pile_of_manim_imports import *
def is_prime(n):
for i in primes(n**0.5):
@@ -8,6 +9,7 @@ def is_prime(n):
return True
def primes(max_n):
if max_n < 2:
@@ -23,13 +25,14 @@ def primes(max_n):
return p
def prime_factors(n):
if is_prime(n):
return [n]
i = 0
primes_list = primes(n/2)
primes_list = primes(n / 2)
factors = []
r = n
@@ -38,7 +41,7 @@ def prime_factors(n):
p = primes_list[i]
if r % p == 0:
factors.append(p)
r = r/p
r = r / p
else:
i += 1
@@ -48,55 +51,58 @@ def prime_factors(n):
RUN_TIME = 0.5
DOWN_SHIFT = 0.0 * DOWN
class Primes(Scene):
def construct(self):
N = 100
primes_list = np.array(primes(N))
palette = ["#FBA125", "#76CD42", "#30CCF5", "#9377C4", "#F95137",
# 2 3 5 7 11
"#1B442E", TEAL_E, MAROON_A, DARK_BROWN, PINK,
# 13 17 19 23 29
"#9C25FB", GREEN_E, MAROON_E, GOLD_E, GREEN_E,
# 31 37 41 43 47 # last prime to occur in a factorization
LIGHT_BROWN, DARK_BLUE, GREY_BROWN, GREEN_C, BLUE_C,
# 53 59 61 67 71
PURPLE_C, RED_C, YELLOW_E, TEAL_C, MAROON_C]
palette = ["#FBA125", "#76CD42", "#30CCF5", "#9377C4", "#F95137",
# 2 3 5 7 11
"#1B442E", TEAL_E, MAROON_A, DARK_BROWN, PINK,
# 13 17 19 23 29
"#9C25FB", GREEN_E, MAROON_E, GOLD_E, GREEN_E,
# 31 37 41 43 47 # last prime to occur in a factorization
LIGHT_BROWN, DARK_BLUE, GREY_BROWN, GREEN_C, BLUE_C,
# 53 59 61 67 71
PURPLE_C, RED_C, YELLOW_E, TEAL_C, MAROON_C]
# 73 79 83 89 97
nb_primes = len(primes_list)
print nb_primes
prime_points_radius = 3.2
angles = np.arange(TAU/4, -3*TAU/4, -TAU/float(nb_primes))
angles = np.arange(TAU / 4, -3 * TAU / 4, -TAU / float(nb_primes))
print len(angles), angles
prime_points = [prime_points_radius * (np.cos(theta) * RIGHT
+ np.sin(theta) * UP)
for theta in angles]
+ np.sin(theta) * UP)
for theta in angles]
print len(prime_points)
wheel = Wheel()
angles = [TAU]
colors = [LIGHT_GREY]
wheel.update_sectors(angles, colors)
wheel.rotate(-TAU/4).shift(DOWN_SHIFT)
wheel.rotate(-TAU / 4).shift(DOWN_SHIFT)
self.add(wheel)
number = DecimalNumber(1, num_decimal_points = 0).scale(2).shift(DOWN_SHIFT)
number = DecimalNumber(1, num_decimal_points=0).scale(
2).shift(DOWN_SHIFT)
self.add(number)
self.wait(RUN_TIME)
j = 0
for i in range(2,N+1):
for i in range(2, N + 1):
factors = prime_factors(i)
factor_indices = [np.where(primes_list == x)[0][0] for x in factors]
factor_indices = [np.where(primes_list == x)[0][0]
for x in factors]
nb_sectors = float(len(factor_indices))
new_angles = np.ones(nb_sectors) / nb_sectors * TAU
@@ -104,16 +110,16 @@ class Primes(Scene):
new_colors = []
for index in factor_indices:
new_colors.append(palette[index])
self.play(
UpdateAngles(wheel, new_angles = new_angles, new_colors = new_colors,
run_time = RUN_TIME),
ChangeDecimalToValue(number, i, run_time = RUN_TIME)
UpdateAngles(wheel, new_angles=new_angles, new_colors=new_colors,
run_time=RUN_TIME),
ChangeDecimalToValue(number, i, run_time=RUN_TIME)
)
self.wait(RUN_TIME)
if is_prime(i):
full_wheel = VGroup(wheel,number).copy()
full_wheel = VGroup(wheel, number).copy()
full_wheel_copy = full_wheel.copy()
full_wheel_copy.scale(0.15).move_to(prime_points[j])
print j
@@ -123,47 +129,45 @@ class Primes(Scene):
)
class Wheel(VMobject):
CONFIG = {
"inner_radius" : 1.2,
"outer_radius" : 2.4,
"nb_sectors" : 25,
"colors" : [BLACK] * 25
"inner_radius": 1.2,
"outer_radius": 2.4,
"nb_sectors": 25,
"colors": [BLACK] * 25
}
def generate_points(self):
angle = TAU/self.nb_sectors
angle_range = np.arange(0,TAU,angle)
angle = TAU / self.nb_sectors
angle_range = np.arange(0, TAU, angle)
for j in range(self.nb_sectors - len(angle_range)):
angle_range = np.append(angle_range, TAU)
self.colors.append(BLACK)
for (i,theta) in enumerate(angle_range):
for (i, theta) in enumerate(angle_range):
if theta != TAU:
use_angle = angle
else:
use_angle = 0
sector = AnnularSector(
inner_radius = self.inner_radius,
outer_radius = self.outer_radius,
angle = use_angle,
start_angle = theta,
fill_color = self.colors[i],
fill_opacity = 1,
stroke_color = WHITE,
stroke_width = 5
).rotate_about_origin(TAU/2, axis = UP).shift(DOWN_SHIFT)
inner_radius=self.inner_radius,
outer_radius=self.outer_radius,
angle=use_angle,
start_angle=theta,
fill_color=self.colors[i],
fill_opacity=1,
stroke_color=WHITE,
stroke_width=5
).rotate_about_origin(TAU / 2, axis=UP).shift(DOWN_SHIFT)
self.add(sector)
def update_sectors(self, new_angles, new_colors):
if len(new_angles) > self.nb_sectors:
raise "More angles than sectors!"
for i in range(len(new_angles), self.nb_sectors):
new_angles = np.append(new_angles, 0)
new_colors.append(BLACK)
@@ -172,30 +176,28 @@ class Wheel(VMobject):
new_start_angles = -np.cumsum(new_angles) + new_angles
for (i,sector) in enumerate(self.submobjects):
for (i, sector) in enumerate(self.submobjects):
sector.angle = new_angles[i]
sector.start_angle = new_start_angles[i]
sector.set_fill(color = new_colors[i])
sector.set_fill(color=new_colors[i])
sector.generate_points()
sector.rotate_about_origin(TAU/2, axis = UP).shift(DOWN_SHIFT)
sector.rotate_about_origin(TAU / 2, axis=UP).shift(DOWN_SHIFT)
class UpdateAngles(Animation):
def __init__(self,mobject,**kwargs):
def __init__(self, mobject, **kwargs):
self.old_angles = []
for (i, sector) in enumerate(mobject.submobjects):
self.old_angles.append(sector.angle)
self.old_angles = np.array(self.old_angles)
self.old_start_angles = np.cumsum(self.old_angles) - self.old_angles + TAU/4
self.old_start_angles = np.cumsum(
self.old_angles) - self.old_angles + TAU / 4
digest_config(self, kwargs)
Animation.__init__(self,mobject,**kwargs)
Animation.__init__(self, mobject, **kwargs)
def update_submobject(self, submobject, starting_submobject, alpha):
@@ -211,7 +213,8 @@ class UpdateAngles(Animation):
self.new_angles = np.append(self.new_angles, 0)
self.new_colors.append(BLACK)
self.new_start_angles = np.cumsum(self.new_angles) - self.new_angles + TAU/4
self.new_start_angles = np.cumsum(
self.new_angles) - self.new_angles + TAU / 4
# this should be in __init__!
# but has no effect there
@@ -223,30 +226,15 @@ class UpdateAngles(Animation):
self.new_start_angles[i], alpha
)
interpolated_color = interpolate_color(
self.mobject.colors[i],
self.new_colors[i],
alpha
)
submobject.set_fill(color = interpolated_color)
submobject.set_fill(color=interpolated_color)
submobject.generate_points()
submobject.rotate_about_origin(TAU/2, axis = UP).shift(DOWN_SHIFT)
submobject.rotate_about_origin(TAU / 2, axis=UP).shift(DOWN_SHIFT)
if alpha > 0.95:
self.mobject.colors[i] = self.new_colors[i]

View File

@@ -22,101 +22,103 @@ from utils.space_ops import angle_of_vector
# TODO, this should probably reimplemented entirely, especially so as to
# better reuse code from mobject/coordinate_systems
class GraphScene(Scene):
CONFIG = {
"x_min" : -1,
"x_max" : 10,
"x_axis_width" : 9,
"x_tick_frequency" : 1,
"x_leftmost_tick" : None, #Change if different from x_min
"x_labeled_nums" : None,
"x_axis_label" : "$x$",
"y_min" : -1,
"y_max" : 10,
"y_axis_height" : 6,
"y_tick_frequency" : 1,
"y_bottom_tick" : None, #Change if different from y_min
"y_labeled_nums" : None,
"y_axis_label" : "$y$",
"axes_color" : GREY,
"graph_origin" : 2.5*DOWN + 4*LEFT,
"exclude_zero_label" : True,
"num_graph_anchor_points" : 25,
"default_graph_colors" : [BLUE, GREEN, YELLOW],
"default_derivative_color" : GREEN,
"default_input_color" : YELLOW,
"default_riemann_start_color" : BLUE,
"default_riemann_end_color" : GREEN,
"x_min": -1,
"x_max": 10,
"x_axis_width": 9,
"x_tick_frequency": 1,
"x_leftmost_tick": None, # Change if different from x_min
"x_labeled_nums": None,
"x_axis_label": "$x$",
"y_min": -1,
"y_max": 10,
"y_axis_height": 6,
"y_tick_frequency": 1,
"y_bottom_tick": None, # Change if different from y_min
"y_labeled_nums": None,
"y_axis_label": "$y$",
"axes_color": GREY,
"graph_origin": 2.5 * DOWN + 4 * LEFT,
"exclude_zero_label": True,
"num_graph_anchor_points": 25,
"default_graph_colors": [BLUE, GREEN, YELLOW],
"default_derivative_color": GREEN,
"default_input_color": YELLOW,
"default_riemann_start_color": BLUE,
"default_riemann_end_color": GREEN,
}
def setup(self):
self.default_graph_colors_cycle = it.cycle(self.default_graph_colors)
def setup_axes(self, animate = False):
##TODO, once eoc is done, refactor this to be less redundant.
def setup_axes(self, animate=False):
# TODO, once eoc is done, refactor this to be less redundant.
x_num_range = float(self.x_max - self.x_min)
self.space_unit_to_x = self.x_axis_width/x_num_range
self.space_unit_to_x = self.x_axis_width / x_num_range
if self.x_labeled_nums is None:
self.x_labeled_nums = []
if self.x_leftmost_tick is None:
self.x_leftmost_tick = self.x_min
x_axis = NumberLine(
x_min = self.x_min,
x_max = self.x_max,
unit_size = self.space_unit_to_x,
tick_frequency = self.x_tick_frequency,
leftmost_tick = self.x_leftmost_tick,
numbers_with_elongated_ticks = self.x_labeled_nums,
color = self.axes_color
x_min=self.x_min,
x_max=self.x_max,
unit_size=self.space_unit_to_x,
tick_frequency=self.x_tick_frequency,
leftmost_tick=self.x_leftmost_tick,
numbers_with_elongated_ticks=self.x_labeled_nums,
color=self.axes_color
)
x_axis.shift(self.graph_origin - x_axis.number_to_point(0))
x_axis.shift(self.graph_origin - x_axis.number_to_point(0))
if len(self.x_labeled_nums) > 0:
if self.exclude_zero_label:
self.x_labeled_nums = filter(
lambda x : x != 0,
lambda x: x != 0,
self.x_labeled_nums
)
x_axis.add_numbers(*self.x_labeled_nums)
if self.x_axis_label:
x_label = TextMobject(self.x_axis_label)
x_label.next_to(
x_axis.get_tick_marks(), UP+RIGHT,
buff = SMALL_BUFF
x_axis.get_tick_marks(), UP + RIGHT,
buff=SMALL_BUFF
)
x_label.shift_onto_screen()
x_axis.add(x_label)
self.x_axis_label_mob = x_label
y_num_range = float(self.y_max - self.y_min)
self.space_unit_to_y = self.y_axis_height/y_num_range
self.space_unit_to_y = self.y_axis_height / y_num_range
if self.y_labeled_nums is None:
self.y_labeled_nums = []
if self.y_bottom_tick is None:
self.y_bottom_tick = self.y_min
y_axis = NumberLine(
x_min = self.y_min,
x_max = self.y_max,
unit_size = self.space_unit_to_y,
tick_frequency = self.y_tick_frequency,
leftmost_tick = self.y_bottom_tick,
numbers_with_elongated_ticks = self.y_labeled_nums,
color = self.axes_color,
line_to_number_vect = LEFT,
x_min=self.y_min,
x_max=self.y_max,
unit_size=self.space_unit_to_y,
tick_frequency=self.y_tick_frequency,
leftmost_tick=self.y_bottom_tick,
numbers_with_elongated_ticks=self.y_labeled_nums,
color=self.axes_color,
line_to_number_vect=LEFT,
)
y_axis.shift(self.graph_origin-y_axis.number_to_point(0))
y_axis.rotate(np.pi/2, about_point = y_axis.number_to_point(0))
y_axis.shift(self.graph_origin - y_axis.number_to_point(0))
y_axis.rotate(np.pi / 2, about_point=y_axis.number_to_point(0))
if len(self.y_labeled_nums) > 0:
if self.exclude_zero_label:
self.y_labeled_nums = filter(
lambda y : y != 0,
lambda y: y != 0,
self.y_labeled_nums
)
y_axis.add_numbers(*self.y_labeled_nums)
if self.y_axis_label:
y_label = TextMobject(self.y_axis_label)
y_label.next_to(
y_axis.get_tick_marks(), UP+RIGHT,
buff = SMALL_BUFF
y_axis.get_tick_marks(), UP + RIGHT,
buff=SMALL_BUFF
)
y_label.shift_onto_screen()
y_axis.add(y_label)
@@ -131,20 +133,20 @@ class GraphScene(Scene):
def coords_to_point(self, x, y):
assert(hasattr(self, "x_axis") and hasattr(self, "y_axis"))
result = self.x_axis.number_to_point(x)[0]*RIGHT
result += self.y_axis.number_to_point(y)[1]*UP
result = self.x_axis.number_to_point(x)[0] * RIGHT
result += self.y_axis.number_to_point(y)[1] * UP
return result
def point_to_coords(self, point):
return (self.x_axis.point_to_number(point),
return (self.x_axis.point_to_number(point),
self.y_axis.point_to_number(point))
def get_graph(
self, func,
color = None,
x_min = None,
x_max = None,
):
self, func,
color=None,
x_min=None,
x_max=None,
):
if color is None:
color = self.default_graph_colors_cycle.next()
if x_min is None:
@@ -160,9 +162,9 @@ class GraphScene(Scene):
return self.coords_to_point(x, y)
graph = ParametricFunction(
parameterized_function,
color = color,
num_anchor_points = self.num_graph_anchor_points,
parameterized_function,
color=color,
num_anchor_points=self.num_graph_anchor_points,
)
graph.underlying_function = func
return graph
@@ -170,34 +172,36 @@ class GraphScene(Scene):
def input_to_graph_point(self, x, graph):
return self.coords_to_point(x, graph.underlying_function(x))
def angle_of_tangent(self, x, graph, dx = 0.01):
vect = self.input_to_graph_point(x + dx, graph) - self.input_to_graph_point(x, graph)
def angle_of_tangent(self, x, graph, dx=0.01):
vect = self.input_to_graph_point(
x + dx, graph) - self.input_to_graph_point(x, graph)
return angle_of_vector(vect)
def slope_of_tangent(self, *args, **kwargs):
return np.tan(self.angle_of_tangent(*args, **kwargs))
def get_derivative_graph(self, graph, dx = 0.01, **kwargs):
def get_derivative_graph(self, graph, dx=0.01, **kwargs):
if "color" not in kwargs:
kwargs["color"] = self.default_derivative_color
def deriv(x):
return self.slope_of_tangent(x, graph, dx) / self.space_unit_to_y
return self.get_graph(deriv, **kwargs)
def get_graph_label(
self,
graph,
label = "f(x)",
x_val = None,
direction = RIGHT,
buff = MED_SMALL_BUFF,
color = None,
):
self,
graph,
label="f(x)",
x_val=None,
direction=RIGHT,
buff=MED_SMALL_BUFF,
color=None,
):
label = TexMobject(label)
color = color or graph.get_color()
label.set_color(color)
if x_val is None:
#Search from right to left
# Search from right to left
for x in np.linspace(self.x_max, self.x_min, 100):
point = self.input_to_graph_point(x, graph)
if point[1] < FRAME_Y_RADIUS:
@@ -206,26 +210,26 @@ class GraphScene(Scene):
label.next_to(
self.input_to_graph_point(x_val, graph),
direction,
buff = buff
buff=buff
)
label.shift_onto_screen()
return label
def get_riemann_rectangles(
self,
self,
graph,
x_min = None,
x_max = None,
dx = 0.1,
input_sample_type = "left",
stroke_width = 1,
stroke_color = BLACK,
fill_opacity = 1,
start_color = None,
end_color = None,
show_signed_area = True,
width_scale_factor = 1.001
):
x_min=None,
x_max=None,
dx=0.1,
input_sample_type="left",
stroke_width=1,
stroke_color=BLACK,
fill_opacity=1,
start_color=None,
end_color=None,
show_signed_area=True,
width_scale_factor=1.001
):
x_min = x_min if x_min is not None else self.x_min
x_max = x_max if x_max is not None else self.x_max
if start_color is None:
@@ -233,47 +237,47 @@ class GraphScene(Scene):
if end_color is None:
end_color = self.default_riemann_end_color
rectangles = VGroup()
x_range = np.arange(x_min, x_max, dx)
x_range = np.arange(x_min, x_max, dx)
colors = color_gradient([start_color, end_color], len(x_range))
for x, color in zip(x_range, colors):
if input_sample_type == "left":
sample_input = x
elif input_sample_type == "right":
sample_input = x+dx
sample_input = x + dx
else:
raise Exception("Invalid input sample type")
graph_point = self.input_to_graph_point(sample_input, graph)
points = VGroup(*map(VectorizedPoint, [
self.coords_to_point(x, 0),
self.coords_to_point(x+width_scale_factor*dx, 0),
self.coords_to_point(x + width_scale_factor * dx, 0),
graph_point
]))
rect = Rectangle()
rect.replace(points, stretch = True)
rect.replace(points, stretch=True)
if graph_point[1] < self.graph_origin[1] and show_signed_area:
fill_color = invert_color(color)
else:
fill_color = color
rect.set_fill(fill_color, opacity = fill_opacity)
rect.set_stroke(stroke_color, width = stroke_width)
rect.set_fill(fill_color, opacity=fill_opacity)
rect.set_stroke(stroke_color, width=stroke_width)
rectangles.add(rect)
return rectangles
def get_riemann_rectangles_list(
self,
self,
graph,
n_iterations,
max_dx = 0.5,
power_base = 2,
stroke_width = 1,
max_dx=0.5,
power_base=2,
stroke_width=1,
**kwargs
):
):
return [
self.get_riemann_rectangles(
graph = graph,
dx = float(max_dx)/(power_base**n),
stroke_width = float(stroke_width)/(power_base**n),
graph=graph,
dx=float(max_dx) / (power_base**n),
stroke_width=float(stroke_width) / (power_base**n),
**kwargs
)
for n in range(n_iterations)
@@ -281,17 +285,17 @@ class GraphScene(Scene):
def transform_between_riemann_rects(self, curr_rects, new_rects, **kwargs):
transform_kwargs = {
"run_time" : 2,
"submobject_mode" : "lagged_start"
"run_time": 2,
"submobject_mode": "lagged_start"
}
added_anims = kwargs.get("added_anims", [])
transform_kwargs.update(kwargs)
curr_rects.align_submobjects(new_rects)
x_coords = set() #Keep track of new repetitions
x_coords = set() # Keep track of new repetitions
for rect in curr_rects:
x = rect.get_center()[0]
if x in x_coords:
rect.set_fill(opacity = 0)
rect.set_fill(opacity=0)
else:
x_coords.add(x)
self.play(
@@ -302,24 +306,24 @@ class GraphScene(Scene):
def get_vertical_line_to_graph(
self,
x, graph,
line_class = Line,
line_class=Line,
**line_kwargs
):
):
if "color" not in line_kwargs:
line_kwargs["color"] = graph.get_color()
return line_class(
self.coords_to_point(x, 0),
self.input_to_graph_point(x, graph),
**line_kwargs
)
)
def get_vertical_lines_to_graph(
self, graph,
x_min = None,
x_max = None,
num_lines = 20,
x_min=None,
x_max=None,
num_lines=20,
**kwargs
):
):
x_min = x_min or self.x_min
x_max = x_max or self.x_max
return VGroup(*[
@@ -328,20 +332,20 @@ class GraphScene(Scene):
])
def get_secant_slope_group(
self,
x, graph,
dx = None,
dx_line_color = None,
df_line_color = None,
dx_label = None,
df_label = None,
include_secant_line = True,
secant_line_color = None,
secant_line_length = 10,
):
self,
x, graph,
dx=None,
dx_line_color=None,
df_line_color=None,
dx_label=None,
df_label=None,
include_secant_line=True,
secant_line_color=None,
secant_line_length=10,
):
"""
Resulting group is of the form VGroup(
dx_line,
dx_line,
df_line,
dx_label, (if applicable)
df_label, (if applicable)
@@ -354,21 +358,21 @@ class GraphScene(Scene):
group = VGroup()
group.kwargs = kwargs
dx = dx or float(self.x_max - self.x_min)/10
dx = dx or float(self.x_max - self.x_min) / 10
dx_line_color = dx_line_color or self.default_input_color
df_line_color = df_line_color or graph.get_color()
p1 = self.input_to_graph_point(x, graph)
p2 = self.input_to_graph_point(x+dx, graph)
interim_point = p2[0]*RIGHT + p1[1]*UP
p2 = self.input_to_graph_point(x + dx, graph)
interim_point = p2[0] * RIGHT + p1[1] * UP
group.dx_line = Line(
p1, interim_point,
color = dx_line_color
color=dx_line_color
)
group.df_line = Line(
interim_point, p2,
color = df_line_color
color=df_line_color
)
group.add(group.dx_line, group.df_line)
@@ -383,8 +387,8 @@ class GraphScene(Scene):
group.add(group.df_label)
if len(labels) > 0:
max_width = 0.8*group.dx_line.get_width()
max_height = 0.8*group.df_line.get_height()
max_width = 0.8 * group.dx_line.get_width()
max_height = 0.8 * group.df_line.get_height()
if labels.get_width() > max_width:
labels.scale_to_fit_width(max_width)
if labels.get_height() > max_height:
@@ -392,40 +396,41 @@ class GraphScene(Scene):
if dx_label is not None:
group.dx_label.next_to(
group.dx_line,
np.sign(dx)*DOWN,
buff = group.dx_label.get_height()/2
group.dx_line,
np.sign(dx) * DOWN,
buff=group.dx_label.get_height() / 2
)
group.dx_label.set_color(group.dx_line.get_color())
if df_label is not None:
group.df_label.next_to(
group.df_line,
np.sign(dx)*RIGHT,
buff = group.df_label.get_height()/2
group.df_line,
np.sign(dx) * RIGHT,
buff=group.df_label.get_height() / 2
)
group.df_label.set_color(group.df_line.get_color())
if include_secant_line:
secant_line_color = secant_line_color or self.default_derivative_color
group.secant_line = Line(p1, p2, color = secant_line_color)
group.secant_line = Line(p1, p2, color=secant_line_color)
group.secant_line.scale_in_place(
secant_line_length/group.secant_line.get_length()
secant_line_length / group.secant_line.get_length()
)
group.add(group.secant_line)
return group
def animate_secant_slope_group_change(
self, secant_slope_group,
target_dx = None,
target_x = None,
run_time = 3,
added_anims = None,
self, secant_slope_group,
target_dx=None,
target_x=None,
run_time=3,
added_anims=None,
**anim_kwargs
):
):
if target_dx is None and target_x is None:
raise Exception("At least one of target_x and target_dx must not be None")
raise Exception(
"At least one of target_x and target_dx must not be None")
if added_anims is None:
added_anims = []
@@ -435,6 +440,7 @@ class GraphScene(Scene):
target_dx = start_dx
if target_x is None:
target_x = start_x
def update_func(group, alpha):
dx = interpolate(start_dx, target_dx, alpha)
x = interpolate(start_x, target_x, alpha)
@@ -448,31 +454,10 @@ class GraphScene(Scene):
self.play(
UpdateFromAlphaFunc(
secant_slope_group, update_func,
run_time = run_time,
run_time=run_time,
**anim_kwargs
),
*added_anims
)
secant_slope_group.kwargs["x"] = target_x
secant_slope_group.kwargs["dx"] = target_dx

View File

@@ -6,10 +6,11 @@ from scene.scene import Scene
from camera.moving_camera import MovingCamera
from mobject.frame import ScreenRectangle
class MovingCameraScene(Scene):
def setup(self):
self.camera_frame = ScreenRectangle(height = FRAME_HEIGHT)
self.camera_frame.set_stroke(width = 0)
self.camera_frame = ScreenRectangle(height=FRAME_HEIGHT)
self.camera_frame.set_stroke(width=0)
self.camera = MovingCamera(
self.camera_frame, **self.camera_config
)
@@ -22,4 +23,4 @@ class MovingCameraScene(Scene):
# When the camera is moving, so is everything,
return self.mobjects
else:
return moving_mobjects
return moving_mobjects

View File

@@ -1,27 +1,27 @@
from __future__ import absolute_import
import numpy as np
from scene.scene import Scene
from animation.transform import Transform
from mobject.mobject import Mobject
from constants import *
class ReconfigurableScene(Scene):
CONFIG = {
"allow_recursion" : True,
"allow_recursion": True,
}
def setup(self):
self.states = []
self.num_recursions = 0
def transition_to_alt_config(
self,
return_to_original_configuration = True,
transformation_kwargs = None,
self,
return_to_original_configuration=True,
transformation_kwargs=None,
**new_config
):
):
if transformation_kwargs is None:
transformation_kwargs = {}
original_state = self.get_state()
@@ -30,41 +30,37 @@ class ReconfigurableScene(Scene):
if not self.allow_recursion:
return
alt_scene = self.__class__(
skip_animations = True,
allow_recursion = False,
skip_animations=True,
allow_recursion=False,
**new_config
)
alt_state = alt_scene.states[len(self.states)-1]
alt_state = alt_scene.states[len(self.states) - 1]
if return_to_original_configuration:
self.clear()
self.transition_between_states(
state_copy, alt_state,
state_copy, alt_state,
**transformation_kwargs
)
self.transition_between_states(
state_copy, original_state,
state_copy, original_state,
**transformation_kwargs
)
self.clear()
self.add(*original_state)
else:
self.transition_between_states(
original_state, alt_state,
original_state, alt_state,
**transformation_kwargs
)
self.__dict__.update(new_config)
def get_state(self):
# Want to return a mobject that maintains the most
# Want to return a mobject that maintains the most
# structure. The way to do that is to extract only
# those that aren't inside another.
top_level_mobjects = self.get_top_level_mobjects()
return Mobject(*self.get_top_level_mobjects())
def transition_between_states(self, start_state, target_state, **kwargs):
self.play(Transform(start_state, target_state, **kwargs))
self.wait()

View File

@@ -21,10 +21,10 @@ class SampleSpaceScene(Scene):
def get_division_change_animations(
self, sample_space, parts, p_list,
dimension = 1,
new_label_kwargs = None,
dimension=1,
new_label_kwargs=None,
**kwargs
):
):
if new_label_kwargs is None:
new_label_kwargs = {}
anims = []
@@ -34,12 +34,12 @@ class SampleSpaceScene(Scene):
vect = DOWN if dimension == 1 else RIGHT
parts.generate_target()
for part, p in zip(parts.target, p_list):
part.replace(space_copy, stretch = True)
part.replace(space_copy, stretch=True)
part.stretch(p, dimension)
parts.target.arrange_submobjects(vect, buff = 0)
parts.target.arrange_submobjects(vect, buff=0)
parts.target.move_to(space_copy)
anims.append(MoveToTarget(parts))
if hasattr(parts, "labels") and parts.labels is not None:
if hasattr(parts, "labels") and parts.labels is not None:
label_kwargs = parts.label_kwargs
label_kwargs.update(new_label_kwargs)
new_braces, new_labels = sample_space.get_subdivision_braces_and_labels(
@@ -55,7 +55,7 @@ class SampleSpaceScene(Scene):
assert(hasattr(self.sample_space, "horizontal_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.horizontal_parts, p_list,
dimension = 1,
dimension=1,
**kwargs
)
@@ -63,19 +63,19 @@ class SampleSpaceScene(Scene):
assert(hasattr(self.sample_space, "vertical_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.vertical_parts, p_list,
dimension = 0,
dimension=0,
**kwargs
)
def get_conditional_change_anims(
self, sub_sample_space_index, value, post_rects = None,
self, sub_sample_space_index, value, post_rects=None,
**kwargs
):
):
parts = self.sample_space.horizontal_parts
sub_sample_space = parts[sub_sample_space_index]
anims = self.get_division_change_animations(
sub_sample_space, sub_sample_space.vertical_parts, value,
dimension = 0,
dimension=0,
**kwargs
)
if post_rects is not None:
@@ -94,10 +94,10 @@ class SampleSpaceScene(Scene):
for i in range(2)
])
def get_posterior_rectangles(self, buff = MED_LARGE_BUFF):
def get_posterior_rectangles(self, buff=MED_LARGE_BUFF):
prior_rects = self.get_prior_rectangles()
areas = [
rect.get_width()*rect.get_height()
rect.get_width() * rect.get_height()
for rect in prior_rects
]
total_area = sum(areas)
@@ -105,19 +105,19 @@ class SampleSpaceScene(Scene):
post_rects = prior_rects.copy()
for rect, area in zip(post_rects, areas):
rect.stretch_to_fit_height(total_height * area/total_area)
rect.stretch_to_fit_height(total_height * area / total_area)
rect.stretch_to_fit_width(
area/rect.get_height()
area / rect.get_height()
)
post_rects.arrange_submobjects(DOWN, buff = 0)
post_rects.arrange_submobjects(DOWN, buff=0)
post_rects.next_to(
self.sample_space, RIGHT, buff
)
return post_rects
def get_posterior_rectangle_braces_and_labels(
self, post_rects, labels, direction = RIGHT, **kwargs
):
self, post_rects, labels, direction=RIGHT, **kwargs
):
return self.sample_space.get_subdivision_braces_and_labels(
post_rects, labels, direction, **kwargs
)
@@ -132,7 +132,7 @@ class SampleSpaceScene(Scene):
def get_posterior_rectangle_change_anims(self, post_rects):
def update_rects(rects):
new_rects = self.get_posterior_rectangles()
new_rects = self.get_posterior_rectangles()
Transform(rects, new_rects).update(1)
if hasattr(rects, "braces"):
self.update_posterior_braces(rects)

View File

@@ -1,4 +1,3 @@
import copy
import inspect
import itertools as it
import numpy as np
@@ -6,11 +5,8 @@ import os
import random
import shutil
import subprocess as sp
import time
import warnings
from PIL import Image
from colour import Color
from tqdm import tqdm as ProgressDisplay
from constants import *
@@ -20,11 +16,11 @@ from animation.transform import MoveToTarget
from camera.camera import Camera
from continual_animation.continual_animation import ContinualAnimation
from mobject.mobject import Mobject
from mobject.types.vectorized_mobject import VMobject
from utils.iterables import list_update
from container.container import Container
def add_extension_if_not_present(file_name, extension):
# This could conceivably be smarter about handling existing differing extensions
if(file_name[-len(extension):] != extension):
@@ -32,29 +28,32 @@ def add_extension_if_not_present(file_name, extension):
else:
return file_name
class Scene(Container):
CONFIG = {
"camera_class" : Camera,
"camera_config" : {},
"frame_duration" : LOW_QUALITY_FRAME_DURATION,
"construct_args" : [],
"skip_animations" : False,
"ignore_waits" : False,
"write_to_movie" : False,
"save_frames" : False,
"save_pngs" : False,
"pngs_mode" : "RGBA",
"output_directory" : ANIMATIONS_DIR,
"movie_file_extension" : ".mp4",
"name" : None,
"always_continually_update" : False,
"random_seed" : 0,
"start_at_animation_number" : None,
"end_at_animation_number" : None,
"include_render_quality_in_output_directory" : True,
"camera_class": Camera,
"camera_config": {},
"frame_duration": LOW_QUALITY_FRAME_DURATION,
"construct_args": [],
"skip_animations": False,
"ignore_waits": False,
"write_to_movie": False,
"save_frames": False,
"save_pngs": False,
"pngs_mode": "RGBA",
"output_directory": ANIMATIONS_DIR,
"movie_file_extension": ".mp4",
"name": None,
"always_continually_update": False,
"random_seed": 0,
"start_at_animation_number": None,
"end_at_animation_number": None,
"include_render_quality_in_output_directory": True,
}
def __init__(self, **kwargs):
Container.__init__(self, **kwargs) # Perhaps allow passing in a non-empty *mobjects parameter?
# Perhaps allow passing in a non-empty *mobjects parameter?
Container.__init__(self, **kwargs)
self.camera = self.camera_class(**self.camera_config)
self.mobjects = []
self.continual_animations = []
@@ -86,7 +85,7 @@ class Scene(Container):
if self.write_to_movie:
self.close_movie_pipe()
print("Played a total of %d animations"%self.num_plays)
print("Played a total of %d animations" % self.num_plays)
def setup(self):
"""
@@ -101,7 +100,7 @@ class Scene(Container):
base.setup(self)
def construct(self):
pass #To be implemented in subclasses
pass # To be implemented in subclasses
def __str__(self):
return self.name
@@ -128,7 +127,7 @@ class Scene(Container):
def get_attrs(self, *keys):
return [getattr(self, key) for key in keys]
### Only these methods should touch the camera
# Only these methods should touch the camera
def set_camera(self, camera):
self.camera = camera
@@ -152,12 +151,12 @@ class Scene(Container):
self.camera.capture_mobjects(mobjects, **kwargs)
def update_frame(
self,
mobjects = None,
background = None,
include_submobjects = True,
dont_update_when_skipping = True,
**kwargs):
self,
mobjects=None,
background=None,
include_submobjects=True,
dont_update_when_skipping=True,
**kwargs):
if self.skip_animations and dont_update_when_skipping:
return
if mobjects is None:
@@ -179,7 +178,7 @@ class Scene(Container):
self.clear()
###
def continual_update(self, dt = None):
def continual_update(self, dt=None):
if dt is None:
dt = self.frame_duration
for continual_animation in self.continual_animations:
@@ -190,10 +189,10 @@ class Scene(Container):
for continual_animation in continual_animations:
continual_animation.begin_wind_down(wind_down_time)
self.wait(wind_down_time)
#TODO, this is not done with the remove method so as to
#keep the relevant mobjects. Better way?
# TODO, this is not done with the remove method so as to
# keep the relevant mobjects. Better way?
self.continual_animations = filter(
lambda ca : ca in continual_animations,
lambda ca: ca in continual_animations,
self.continual_animations
)
@@ -207,6 +206,7 @@ class Scene(Container):
# of another mobject from the scene
mobjects = self.get_mobjects()
families = [m.submobject_family() for m in mobjects]
def is_top_level(mobject):
num_families = sum([
(mobject in family)
@@ -239,7 +239,7 @@ class Scene(Container):
mobjects, continual_animations = self.separate_mobjects_and_continual_animations(
mobjects_or_continual_animations
)
self.restructure_mobjects(to_remove = mobjects)
self.restructure_mobjects(to_remove=mobjects)
self.mobjects += mobjects
self.continual_animations += continual_animations
return self
@@ -249,7 +249,7 @@ class Scene(Container):
So a scene can just add all mobjects it's defined up to that point
by calling add_mobjects_among(locals().values())
"""
mobjects = filter(lambda x : isinstance(x, Mobject), values)
mobjects = filter(lambda x: isinstance(x, Mobject), values)
self.add(*mobjects)
return self
@@ -263,17 +263,17 @@ class Scene(Container):
self.restructure_mobjects(mobjects, list_name, False)
self.continual_animations = filter(
lambda ca : ca not in continual_animations and \
ca.mobject not in to_remove,
lambda ca: ca not in continual_animations and
ca.mobject not in to_remove,
self.continual_animations
)
return self
def restructure_mobjects(
self, to_remove,
mobject_list_name = "mobjects",
extract_families = True
):
self, to_remove,
mobject_list_name="mobjects",
extract_families=True
):
"""
In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one
of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects
@@ -289,6 +289,7 @@ class Scene(Container):
def get_restructured_mobject_list(self, mobjects, to_remove):
new_mobjects = []
def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
for mob in list_to_examine:
if mob in set_to_remove:
@@ -364,7 +365,7 @@ class Scene(Container):
run_time = np.max([animation.run_time for animation in animations])
time_progression = self.get_time_progression(run_time)
time_progression.set_description("".join([
"Animation %d: "%self.num_plays,
"Animation %d: " % self.num_plays,
str(animations[0]),
(", etc." if len(animations) > 1 else ""),
]))
@@ -383,17 +384,18 @@ class Scene(Container):
"""
animations = []
state = {
"curr_method" : None,
"last_method" : None,
"method_args" : [],
"curr_method": None,
"last_method": None,
"method_args": [],
}
def compile_method(state):
if state["curr_method"] is None:
return
mobject = state["curr_method"].im_self
if state["last_method"] and state["last_method"].im_self is mobject:
animations.pop()
#method should already have target then.
# method should already have target then.
else:
mobject.generate_target()
#
@@ -402,7 +404,7 @@ class Scene(Container):
else:
method_kwargs = {}
state["curr_method"].im_func(
mobject.target,
mobject.target,
*state["method_args"],
**method_kwargs
)
@@ -453,7 +455,7 @@ class Scene(Container):
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
self.update_frame(excluded_mobjects = moving_mobjects)
self.update_frame(excluded_mobjects=moving_mobjects)
static_image = self.get_frame()
for t in self.get_animation_time_progression(animations):
for animation in animations:
@@ -484,27 +486,28 @@ class Scene(Container):
return self.mobjects_from_last_animation
return []
def wait(self, duration = DEFAULT_WAIT_TIME):
def wait(self, duration=DEFAULT_WAIT_TIME):
if self.should_continually_update():
for t in self.get_time_progression(duration):
self.continual_update()
self.update_frame()
self.add_frames(self.get_frame())
elif self.skip_animations:
#Do nothing
# Do nothing
return self
else:
self.update_frame()
self.add_frames(*[self.get_frame()]*int(duration / self.frame_duration))
self.add_frames(*[self.get_frame()] *
int(duration / self.frame_duration))
return self
def wait_to(self, time, assert_positive = True):
if self.ignore_waits:
def wait_to(self, time, assert_positive=True):
if self.ignore_waits:
return
time -= self.current_scene_time
if assert_positive:
if assert_positive:
assert(time >= 0)
elif time < 0:
elif time < 0:
return
self.wait(time)
@@ -522,23 +525,24 @@ class Scene(Container):
def add_frames(self, *frames):
if self.skip_animations:
return
self.current_scene_time += len(frames)*self.frame_duration
self.current_scene_time += len(frames) * self.frame_duration
if self.write_to_movie:
for frame in frames:
if self.save_pngs:
self.save_image("frame" + str(self.frame_num), self.pngs_mode, True)
self.save_image(
"frame" + str(self.frame_num), self.pngs_mode, True)
self.frame_num = self.frame_num + 1
self.writing_process.stdin.write(frame.tostring())
if self.save_frames:
self.saved_frames += list(frames)
#Display methods
# Display methods
def show_frame(self):
self.update_frame(dont_update_when_skipping = False)
self.update_frame(dont_update_when_skipping=False)
self.get_image().show()
def get_image_file_path(self, name = None, dont_update = False):
def get_image_file_path(self, name=None, dont_update=False):
folder = "images"
if dont_update:
folder = str(self)
@@ -546,23 +550,23 @@ class Scene(Container):
file_name = add_extension_if_not_present(name or str(self), ".png")
return os.path.join(path, file_name)
def save_image(self, name = None, mode = "RGB", dont_update = False):
def save_image(self, name=None, mode="RGB", dont_update=False):
path = self.get_image_file_path(name, dont_update)
directory_path = os.path.dirname(path)
if not os.path.exists(directory_path):
os.makedirs(directory_path)
if not dont_update:
self.update_frame(dont_update_when_skipping = False)
self.update_frame(dont_update_when_skipping=False)
image = self.get_image()
image = image.convert(mode)
image.save(path)
def get_movie_file_path(self, name = None, extension = None):
def get_movie_file_path(self, name=None, extension=None):
directory = self.output_directory
if self.include_render_quality_in_output_directory:
sub_dir = "%dp%d"%(
sub_dir = "%dp%d" % (
self.camera.pixel_shape[0],
int(1.0/self.frame_duration)
int(1.0 / self.frame_duration)
)
directory = os.path.join(directory, sub_dir)
if not os.path.exists(directory):
@@ -581,21 +585,21 @@ class Scene(Container):
name = str(self)
file_path = self.get_movie_file_path(name)
temp_file_path = file_path.replace(name, name + "Temp")
print("Writing to %s"%temp_file_path)
print("Writing to %s" % temp_file_path)
self.args_to_rename_file = (temp_file_path, file_path)
fps = int(1/self.frame_duration)
fps = int(1 / self.frame_duration)
height, width = self.camera.pixel_shape
command = [
FFMPEG_BIN,
'-y', # overwrite output file if it exists
'-y', # overwrite output file if it exists
'-f', 'rawvideo',
'-s', '%dx%d'%(width, height), # size of one frame
'-s', '%dx%d' % (width, height), # size of one frame
'-pix_fmt', 'rgba',
'-r', str(fps), # frames per second
'-i', '-', # The imput comes from a pipe
'-an', # Tells FFMPEG not to expect any audio
'-r', str(fps), # frames per second
'-i', '-', # The imput comes from a pipe
'-an', # Tells FFMPEG not to expect any audio
'-loglevel', 'error',
]
if self.movie_file_extension == ".mov":
@@ -603,7 +607,7 @@ class Scene(Container):
# should be transparent.
command += [
'-vcodec', 'png',
]
]
else:
command += [
'-vcodec', 'libx264',
@@ -621,14 +625,6 @@ class Scene(Container):
else:
os.rename(*self.args_to_rename_file)
class EndSceneEarlyException(Exception):
pass

View File

@@ -1,8 +1,6 @@
from __future__ import absolute_import
import cv2
import itertools as it
import numpy as np
from tqdm import tqdm as show_progress
@@ -11,25 +9,25 @@ from scene.scene import Scene
class SceneFromVideo(Scene):
def construct(self, file_name,
freeze_last_frame = True,
time_range = None):
freeze_last_frame=True,
time_range=None):
cap = cv2.VideoCapture(file_name)
self.shape = (
int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_HEIGHT)),
int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_WIDTH))
)
fps = cap.get(cv2.cv.CV_CAP_PROP_FPS)
self.frame_duration = 1.0/fps
self.frame_duration = 1.0 / fps
frame_count = int(cap.get(cv2.cv.CV_CAP_PROP_FRAME_COUNT))
if time_range is None:
start_frame = 0
end_frame = frame_count
else:
start_frame, end_frame = map(lambda t : fps*t, time_range)
start_frame, end_frame = map(lambda t: fps * t, time_range)
frame_count = end_frame - start_frame
print("Reading in " + file_name + "...")
for count in show_progress(range(start_frame, end_frame+1)):
for count in show_progress(range(start_frame, end_frame + 1)):
returned, frame = cap.read()
if not returned:
break
@@ -41,18 +39,17 @@ class SceneFromVideo(Scene):
if freeze_last_frame and len(self.frames) > 0:
self.original_background = self.background = self.frames[-1]
def apply_gaussian_blur(self, ksize = (5, 5), sigmaX = 5):
def apply_gaussian_blur(self, ksize=(5, 5), sigmaX=5):
self.frames = [
cv2.GaussianBlur(frame, ksize, sigmaX)
for frame in self.frames
]
def apply_edge_detection(self, threshold1 = 50, threshold2 = 100):
def apply_edge_detection(self, threshold1=50, threshold2=100):
edged_frames = [
cv2.Canny(frame, threshold1, threshold2)
for frame in self.frames
]
for index in range(len(self.frames)):
for i in range(3):
self.frames[index][:,:,i] = edged_frames[index]
self.frames[index][:, :, i] = edged_frames[index]

View File

@@ -5,8 +5,6 @@ from constants import *
from continual_animation.continual_animation import ContinualMovement
from animation.transform import ApplyMethod
from camera.three_d_camera import ThreeDCamera
from mobject.types.vectorized_mobject import VGroup
from mobject.three_dimensions import should_shade_in_3d
from scene.scene import Scene
from utils.iterables import list_update
@@ -14,19 +12,20 @@ from utils.iterables import list_update
class ThreeDScene(Scene):
CONFIG = {
"camera_class" : ThreeDCamera,
"ambient_camera_rotation" : None,
"camera_class": ThreeDCamera,
"ambient_camera_rotation": None,
}
def set_camera_position(self, phi = None, theta = None, distance = None,
center_x = None, center_y = None, center_z = None):
self.camera.set_position(phi, theta, distance, center_x, center_y, center_z)
def set_camera_position(self, phi=None, theta=None, distance=None,
center_x=None, center_y=None, center_z=None):
self.camera.set_position(phi, theta, distance,
center_x, center_y, center_z)
def begin_ambient_camera_rotation(self, rate = 0.01):
def begin_ambient_camera_rotation(self, rate=0.01):
self.ambient_camera_rotation = ContinualMovement(
self.camera.rotation_mobject,
direction = UP,
rate = rate
direction=UP,
rate=rate
)
self.add(self.ambient_camera_rotation)
@@ -37,18 +36,19 @@ class ThreeDScene(Scene):
def move_camera(
self,
phi = None, theta = None, distance = None,
center_x = None, center_y = None, center_z = None,
added_anims = [],
phi=None, theta=None, distance=None,
center_x=None, center_y=None, center_z=None,
added_anims=[],
**kwargs
):
):
target_point = self.camera.get_spherical_coords(phi, theta, distance)
movement = ApplyMethod(
self.camera.rotation_mobject.move_to,
target_point,
**kwargs
)
target_center = self.camera.get_center_of_rotation(center_x, center_y, center_z)
target_center = self.camera.get_center_of_rotation(
center_x, center_y, center_z)
movement_center = ApplyMethod(
self.camera.moving_center.move_to,
target_center,

View File

@@ -4,29 +4,29 @@ import numpy as np
from scene.scene import Scene
from animation.creation import FadeIn
from camera.camera import Camera
from camera.moving_camera import MovingCamera
from mobject.mobject import Mobject
from mobject.geometry import Rectangle
from constants import *
class ZoomedScene(Scene):
"""
Move around self.little_rectangle to determine
which part of the screen is zoomed in on.
"""
CONFIG = {
"zoomed_canvas_frame_shape" : (3, 3),
"zoomed_canvas_center" : None,
"zoomed_canvas_corner" : UP+RIGHT,
"zoomed_canvas_corner_buff" : DEFAULT_MOBJECT_TO_EDGE_BUFFER,
"zoomed_camera_background" : None,
"little_rectangle_start_position" : ORIGIN,
"zoom_factor" : 6,
"square_color" : WHITE,
"zoom_activated" : False,
"zoomed_canvas_frame_shape": (3, 3),
"zoomed_canvas_center": None,
"zoomed_canvas_corner": UP + RIGHT,
"zoomed_canvas_corner_buff": DEFAULT_MOBJECT_TO_EDGE_BUFFER,
"zoomed_camera_background": None,
"little_rectangle_start_position": ORIGIN,
"zoom_factor": 6,
"square_color": WHITE,
"zoom_activated": False,
}
def activate_zooming(self):
self.generate_big_rectangle()
self.setup_zoomed_canvas()
@@ -52,47 +52,46 @@ class ZoomedScene(Scene):
def generate_big_rectangle(self):
height, width = self.zoomed_canvas_frame_shape
self.big_rectangle = Rectangle(
height = height,
width = width,
color = self.square_color
height=height,
width=width,
color=self.square_color
)
if self.zoomed_canvas_center is not None:
self.big_rectangle.shift(self.zoomed_canvas_center)
elif self.zoomed_canvas_corner is not None:
self.big_rectangle.to_corner(
self.zoomed_canvas_corner,
buff = self.zoomed_canvas_corner_buff
buff=self.zoomed_canvas_corner_buff
)
self.add(self.big_rectangle)
def setup_zoomed_canvas(self):
upper_left = self.big_rectangle.get_corner(UP+LEFT)
lower_right = self.big_rectangle.get_corner(DOWN+RIGHT)
upper_left = self.big_rectangle.get_corner(UP + LEFT)
lower_right = self.big_rectangle.get_corner(DOWN + RIGHT)
pixel_coords = self.camera.points_to_pixel_coords(
np.array([upper_left, lower_right])
)
self.zoomed_canvas_pixel_indices = pixel_coords
(up, left), (down, right) = pixel_coords
self.zoomed_canvas_pixel_shape = (
right-left,
down-up,
right - left,
down - up,
)
def setup_zoomed_camera(self):
self.little_rectangle = self.big_rectangle.copy()
self.little_rectangle.scale(1./self.zoom_factor)
self.little_rectangle.scale(1. / self.zoom_factor)
self.little_rectangle.move_to(
self.little_rectangle_start_position
)
self.zoomed_camera = MovingCamera(
self.little_rectangle,
pixel_shape = self.zoomed_canvas_pixel_shape,
background = self.zoomed_camera_background
pixel_shape=self.zoomed_canvas_pixel_shape,
background=self.zoomed_camera_background
)
self.add(self.little_rectangle)
#TODO, is there a better way to hanld this?
self.zoomed_camera.adjusted_thickness = lambda x : x
# TODO, is there a better way to hanld this?
self.zoomed_camera.adjusted_thickness = lambda x: x
def get_frame(self):
frame = Scene.get_frame(self)
@@ -105,11 +104,12 @@ class ZoomedScene(Scene):
self.camera.set_pixel_array(pixel_array)
if self.zoom_activated:
(up, left), (down, right) = self.zoomed_canvas_pixel_indices
self.zoomed_camera.set_pixel_array(pixel_array[left:right, up:down])
self.zoomed_camera.set_pixel_array(
pixel_array[left:right, up:down])
def set_camera_background(self, background):
self.set_camera_pixel_array(self, background)
#TODO, check this...
# TODO, check this...
def reset_camera(self):
self.camera.reset()
@@ -125,7 +125,7 @@ class ZoomedScene(Scene):
self.zoomed_camera.capture_mobjects(
mobjects, **kwargs
)
def get_moving_mobjects(self, *animations):
moving_mobjects = Scene.get_moving_mobjects(self, *animations)
if self.zoom_activated and self.little_rectangle in moving_mobjects:
@@ -133,8 +133,3 @@ class ZoomedScene(Scene):
return self.mobjects
else:
return moving_mobjects

View File

@@ -5,18 +5,20 @@ from utils.simple_functions import choose
CLOSED_THRESHOLD = 0.0
def bezier(points):
n = len(points) - 1
return lambda t : sum([
((1-t)**(n-k))*(t**k)*choose(n, k)*point
return lambda t: sum([
((1 - t)**(n - k)) * (t**k) * choose(n, k) * point
for k, point in enumerate(points)
])
def partial_bezier_points(points, a, b):
"""
Given an array of points which define
Given an array of points which define
a bezier curve, and two numbers 0<=a<b<=1,
return an array of the same size, which
return an array of the same size, which
describes the portion of the original bezier
curve on the interval [a, b].
@@ -27,7 +29,7 @@ def partial_bezier_points(points, a, b):
for i in range(len(points))
])
return np.array([
bezier(a_to_1[:i+1])((b-a)/(1.-a))
bezier(a_to_1[:i + 1])((b - a) / (1. - a))
for i in range(len(points))
])
@@ -35,20 +37,24 @@ def partial_bezier_points(points, a, b):
# Linear interpolation variants
def interpolate(start, end, alpha):
return (1-alpha)*start + alpha*end
return (1 - alpha) * start + alpha * end
def mid(start, end):
return (start + end)/2.0
return (start + end) / 2.0
def inverse_interpolate(start, end, value):
return np.true_divide(value - start, end - start)
def match_interpolate(new_start, new_end, old_start, old_end, old_value):
return interpolate(
new_start, new_end,
new_start, new_end,
inverse_interpolate(old_start, old_end, old_value)
)
def clamp(lower, upper, val):
if val < lower:
return lower
@@ -58,70 +64,75 @@ def clamp(lower, upper, val):
# Figuring out which bezier curves most smoothly connect a sequence of points
def get_smooth_handle_points(points):
points = np.array(points)
num_handles = len(points) - 1
dim = points.shape[1]
dim = points.shape[1]
if num_handles < 1:
return np.zeros((0, dim)), np.zeros((0, dim))
#Must solve 2*num_handles equations to get the handles.
#l and u are the number of lower an upper diagonal rows
#in the matrix to solve.
l, u = 2, 1
#diag is a representation of the matrix in diagonal form
#See https://www.particleincell.com/2012/bezier-splines/
#for how to arive at these equations
diag = np.zeros((l+u+1, 2*num_handles))
diag[0,1::2] = -1
diag[0,2::2] = 1
diag[1,0::2] = 2
diag[1,1::2] = 1
diag[2,1:-2:2] = -2
diag[3,0:-3:2] = 1
#last
diag[2,-2] = -1
diag[1,-1] = 2
#This is the b as in Ax = b, where we are solving for x,
#and A is represented using diag. However, think of entries
#to x and b as being points in space, not numbers
b = np.zeros((2*num_handles, dim))
b[1::2] = 2*points[1:]
# Must solve 2*num_handles equations to get the handles.
# l and u are the number of lower an upper diagonal rows
# in the matrix to solve.
l, u = 2, 1
# diag is a representation of the matrix in diagonal form
# See https://www.particleincell.com/2012/bezier-splines/
# for how to arive at these equations
diag = np.zeros((l + u + 1, 2 * num_handles))
diag[0, 1::2] = -1
diag[0, 2::2] = 1
diag[1, 0::2] = 2
diag[1, 1::2] = 1
diag[2, 1:-2:2] = -2
diag[3, 0:-3:2] = 1
# last
diag[2, -2] = -1
diag[1, -1] = 2
# This is the b as in Ax = b, where we are solving for x,
# and A is represented using diag. However, think of entries
# to x and b as being points in space, not numbers
b = np.zeros((2 * num_handles, dim))
b[1::2] = 2 * points[1:]
b[0] = points[0]
b[-1] = points[-1]
solve_func = lambda b : linalg.solve_banded(
(l, u), diag, b
)
def solve_func(b):
return linalg.solve_banded((l, u), diag, b)
if is_closed(points):
#Get equations to relate first and last points
# Get equations to relate first and last points
matrix = diag_to_matrix((l, u), diag)
#last row handles second derivative
# last row handles second derivative
matrix[-1, [0, 1, -2, -1]] = [2, -1, 1, -2]
#first row handles first derivative
matrix[0,:] = np.zeros(matrix.shape[1])
matrix[0,[0, -1]] = [1, 1]
b[0] = 2*points[0]
# first row handles first derivative
matrix[0, :] = np.zeros(matrix.shape[1])
matrix[0, [0, -1]] = [1, 1]
b[0] = 2 * points[0]
b[-1] = np.zeros(dim)
solve_func = lambda b : linalg.solve(matrix, b)
handle_pairs = np.zeros((2*num_handles, dim))
def solve_func(b):
return linalg.solve(matrix, b)
handle_pairs = np.zeros((2 * num_handles, dim))
for i in range(dim):
handle_pairs[:,i] = solve_func(b[:,i])
handle_pairs[:, i] = solve_func(b[:, i])
return handle_pairs[0::2], handle_pairs[1::2]
def diag_to_matrix(l_and_u, diag):
"""
Converts array whose rows represent diagonal
Converts array whose rows represent diagonal
entries of a matrix into the matrix itself.
See scipy.linalg.solve_banded
"""
l, u = l_and_u
dim = diag.shape[1]
matrix = np.zeros((dim, dim))
for i in range(l+u+1):
for i in range(l + u + 1):
np.fill_diagonal(
matrix[max(0,i-u):,max(0,u-i):],
diag[i,max(0,u-i):]
matrix[max(0, i - u):, max(0, u - i):],
diag[i, max(0, u - i):]
)
return matrix
def is_closed(points):
return np.linalg.norm(points[0] - points[-1]) < CLOSED_THRESHOLD
return np.linalg.norm(points[0] - points[-1]) < CLOSED_THRESHOLD

View File

@@ -7,33 +7,42 @@ from constants import PALETTE
from utils.bezier import interpolate
def color_to_rgb(color):
return np.array(Color(color).get_rgb())
def color_to_rgba(color, alpha = 1):
def color_to_rgba(color, alpha=1):
return np.append(color_to_rgb(color), [alpha])
def rgb_to_color(rgb):
try:
return Color(rgb = rgb)
return Color(rgb=rgb)
except:
return Color(WHITE)
def rgba_to_color(rgba):
return rgb_to_color(rgba[:3])
def rgb_to_hex(rgb):
return "#" + "".join('%02x'%int(255*x) for x in rgb)
return "#" + "".join('%02x' % int(255 * x) for x in rgb)
def invert_color(color):
return rgb_to_color(1.0 - color_to_rgb(color))
def color_to_int_rgb(color):
return (255*color_to_rgb(color)).astype('uint8')
def color_to_int_rgba(color, alpha = 255):
def color_to_int_rgb(color):
return (255 * color_to_rgb(color)).astype('uint8')
def color_to_int_rgba(color, alpha=255):
return np.append(color_to_int_rgb(color), alpha)
def color_gradient(reference_colors, length_of_output):
if length_of_output == 0:
return reference_colors[0]
@@ -41,30 +50,34 @@ def color_gradient(reference_colors, length_of_output):
alphas = np.linspace(0, (len(rgbs) - 1), length_of_output)
floors = alphas.astype('int')
alphas_mod1 = alphas % 1
#End edge case
# End edge case
alphas_mod1[-1] = 1
floors[-1] = len(rgbs) - 2
return [
rgb_to_color(interpolate(rgbs[i], rgbs[i+1], alpha))
rgb_to_color(interpolate(rgbs[i], rgbs[i + 1], alpha))
for i, alpha in zip(floors, alphas_mod1)
]
def interpolate_color(color1, color2, alpha):
rgb = interpolate(color_to_rgb(color1), color_to_rgb(color2), alpha)
return rgb_to_color(rgb)
def average_color(*colors):
rgbs = np.array(map(color_to_rgb, colors))
mean_rgb = np.apply_along_axis(np.mean, 0, rgbs)
return rgb_to_color(mean_rgb)
def random_bright_color():
color = random_color()
curr_rgb = color_to_rgb(color)
new_rgb = interpolate(
curr_rgb, np.ones(len(curr_rgb)), 0.5
)
return Color(rgb = new_rgb)
return Color(rgb=new_rgb)
def random_color():
return random.choice(PALETTE)
return random.choice(PALETTE)

View File

@@ -1,14 +1,16 @@
import inspect
import operator as op
def instantiate(obj):
"""
Useful so that classes or instance of those classes can be
Useful so that classes or instance of those classes can be
included in configuration, which can prevent defaults from
getting created during compilation/importing
"""
return obj() if isinstance(obj, type) else obj
def get_all_descendent_classes(Class):
awaiting_review = [Class]
result = []
@@ -18,6 +20,7 @@ def get_all_descendent_classes(Class):
result.append(Child)
return result
def filtered_locals(caller_locals):
result = caller_locals.copy()
ignored_local_args = ["self", "kwargs"]
@@ -25,12 +28,13 @@ def filtered_locals(caller_locals):
result.pop(arg, caller_locals)
return result
def digest_config(obj, kwargs, caller_locals = {}):
def digest_config(obj, kwargs, caller_locals={}):
"""
Sets init args and CONFIG values as local variables
The purpose of this function is to ensure that all
configuration of any object is inheritable, able to
The purpose of this function is to ensure that all
configuration of any object is inheritable, able to
be easily passed into instantiation, and is attached
as an attribute of the object.
"""
@@ -44,16 +48,17 @@ def digest_config(obj, kwargs, caller_locals = {}):
if hasattr(Class, "CONFIG"):
static_configs.append(Class.CONFIG)
#Order matters a lot here, first dicts have higher priority
# Order matters a lot here, first dicts have higher priority
caller_locals = filtered_locals(caller_locals)
all_dicts = [kwargs, caller_locals, obj.__dict__]
all_dicts += static_configs
all_new_dicts = [kwargs, caller_locals] + static_configs
obj.__dict__ = merge_config(all_dicts)
#Keep track of the configuration of objects upon
#instantiation
# Keep track of the configuration of objects upon
# instantiation
obj.initial_config = merge_config(all_new_dicts)
def merge_config(all_dicts):
all_config = reduce(op.add, [d.items() for d in all_dicts])
config = dict()
@@ -62,11 +67,12 @@ def merge_config(all_dicts):
if not key in config:
config[key] = value
else:
#When two dictionaries have the same key, they are merged.
# When two dictionaries have the same key, they are merged.
if isinstance(value, dict) and isinstance(config[key], dict):
config[key] = merge_config([config[key], value])
return config
def soft_dict_update(d1, d2):
"""
Adds key values pairs of d2 to d1 only when d1 doesn't
@@ -76,7 +82,8 @@ def soft_dict_update(d1, d2):
if key not in d1:
d1[key] = value
def digest_locals(obj, keys = None):
def digest_locals(obj, keys=None):
caller_locals = filtered_locals(
inspect.currentframe().f_back.f_locals
)
@@ -85,31 +92,10 @@ def digest_locals(obj, keys = None):
for key in keys:
setattr(obj, key, caller_locals[key])
# Occasionally convenient in order to write dict.x instead of more laborious
# Occasionally convenient in order to write dict.x instead of more laborious
# (and less in keeping with all other attr accesses) dict["x"]
class DictAsObject(object):
def __init__(self, dict):
self.__dict__ = dict
self.__dict__ = dict

View File

@@ -4,6 +4,7 @@ import os
from PIL import Image
from constants import RASTER_IMAGE_DIR
def get_full_raster_image_path(image_file_name):
possible_paths = [
image_file_name,
@@ -15,7 +16,8 @@ def get_full_raster_image_path(image_file_name):
for path in possible_paths:
if os.path.exists(path):
return path
raise IOError("File %s not Found"%image_file_name)
raise IOError("File %s not Found" % image_file_name)
def drag_pixels(frames):
curr = frames[0]
@@ -25,7 +27,8 @@ def drag_pixels(frames):
new_frames.append(np.array(curr))
return new_frames
def invert_image(image):
arr = np.array(image)
arr = (255 * np.ones(arr.shape)).astype(arr.dtype) - arr
return Image.fromarray(arr)
return Image.fromarray(arr)

View File

@@ -1,6 +1,7 @@
import itertools as it
import numpy as np
def remove_list_redundancies(l):
"""
Used instead of list(set(l)) to maintain order
@@ -9,27 +10,32 @@ def remove_list_redundancies(l):
reversed_result = []
used = set()
for x in reversed(l):
if not x in used:
if x not in used:
reversed_result.append(x)
used.add(x)
reversed_result.reverse()
return reversed_result
def list_update(l1, l2):
"""
Used instead of list(set(l1).update(l2)) to maintain order,
making sure duplicates are removed from l1, not l2.
"""
return filter(lambda e : e not in l2, l1) + list(l2)
return filter(lambda e: e not in l2, l1) + list(l2)
def list_difference_update(l1, l2):
return filter(lambda e : e not in l2, l1)
return filter(lambda e: e not in l2, l1)
def all_elements_are_instances(iterable, Class):
return all(map(lambda e : isinstance(e, Class), iterable))
return all(map(lambda e: isinstance(e, Class), iterable))
def adjacent_pairs(objects):
return zip(objects, list(objects[1:])+[objects[0]])
return zip(objects, list(objects[1:]) + [objects[0]])
def batch_by_property(items, property_func):
"""
@@ -39,6 +45,7 @@ def batch_by_property(items, property_func):
batches together would give the original list.
"""
batch_prop_pairs = []
def add_batch_prop_pair(batch):
if len(batch) > 0:
batch_prop_pairs.append(
@@ -57,6 +64,7 @@ def batch_by_property(items, property_func):
add_batch_prop_pair(curr_batch)
return batch_prop_pairs
def tuplify(obj):
if isinstance(obj, str):
return (obj,)
@@ -65,14 +73,17 @@ def tuplify(obj):
except:
return (obj,)
def stretch_array_to_length(nparray, length):
curr_len = len(nparray)
if curr_len > length:
raise Warning("Trying to stretch array to a length shorter than its own")
indices = np.arange(length)/ float(length)
raise Warning(
"Trying to stretch array to a length shorter than its own")
indices = np.arange(length) / float(length)
indices *= curr_len
return nparray[indices.astype('int')]
def make_even(iterable_1, iterable_2):
list_1, list_2 = list(iterable_1), list(iterable_2)
length = max(len(list_1), len(list_2))
@@ -81,6 +92,7 @@ def make_even(iterable_1, iterable_2):
[list_2[(n * len(list_2)) / length] for n in xrange(length)]
)
def make_even_by_cycling(iterable_1, iterable_2):
length = max(len(iterable_1), len(iterable_2))
cycle1 = it.cycle(iterable_1)
@@ -90,24 +102,23 @@ def make_even_by_cycling(iterable_1, iterable_2):
[cycle2.next() for x in range(length)]
)
def composition(func_list):
"""
func_list should contain elements of the form (f, args)
"""
return reduce(
lambda (f1, args1), (f2, args2) : (lambda x : f1(f2(x, *args2), *args1)),
lambda (f1, args1), (f2, args2): (lambda x: f1(f2(x, *args2), *args1)),
func_list,
lambda x : x
lambda x: x
)
def remove_nones(sequence):
return filter(lambda x : x, sequence)
## Note this is redundant with it.chain
def remove_nones(sequence):
return filter(lambda x: x, sequence)
# Note this is redundant with it.chain
def concatenate_lists(*list_of_lists):
return [item for l in list_of_lists for item in l]

View File

@@ -6,6 +6,7 @@ from utils.space_ops import rotation_matrix
STRAIGHT_PATH_THRESHOLD = 0.01
def straight_path(start_points, end_points, alpha):
"""
Same function as interpolate, but renamed to reflect
@@ -15,27 +16,31 @@ def straight_path(start_points, end_points, alpha):
"""
return interpolate(start_points, end_points, alpha)
def path_along_arc(arc_angle, axis = OUT):
def path_along_arc(arc_angle, axis=OUT):
"""
If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
perpendicular to vect in the left direction.
"""
if abs(arc_angle) < STRAIGHT_PATH_THRESHOLD:
return straight_path
if np.linalg.norm(axis) == 0:
axis = OUT
unit_axis = axis/np.linalg.norm(axis)
unit_axis = axis / np.linalg.norm(axis)
def path(start_points, end_points, alpha):
vects = end_points - start_points
centers = start_points + 0.5*vects
centers = start_points + 0.5 * vects
if arc_angle != np.pi:
centers += np.cross(unit_axis, vects/2.0)/np.tan(arc_angle/2)
rot_matrix = rotation_matrix(alpha*arc_angle, unit_axis)
return centers + np.dot(start_points-centers, rot_matrix.T)
centers += np.cross(unit_axis, vects / 2.0) / np.tan(arc_angle / 2)
rot_matrix = rotation_matrix(alpha * arc_angle, unit_axis)
return centers + np.dot(start_points - centers, rot_matrix.T)
return path
def clockwise_path():
return path_along_arc(-np.pi)
def counterclockwise_path():
return path_along_arc(np.pi)
return path_along_arc(np.pi)

View File

@@ -3,49 +3,60 @@ import numpy as np
from utils.bezier import bezier
from utils.simple_functions import sigmoid
def smooth(t, inflection = 10.0):
def smooth(t, inflection=10.0):
error = sigmoid(-inflection / 2)
return (sigmoid(inflection*(t - 0.5)) - error) / (1 - 2*error)
return (sigmoid(inflection * (t - 0.5)) - error) / (1 - 2 * error)
def rush_into(t):
return 2*smooth(t/2.0)
return 2 * smooth(t / 2.0)
def rush_from(t):
return 2*smooth(t/2.0+0.5) - 1
return 2 * smooth(t / 2.0 + 0.5) - 1
def slow_into(t):
return np.sqrt(1-(1-t)*(1-t))
return np.sqrt(1 - (1 - t) * (1 - t))
def double_smooth(t):
if t < 0.5:
return 0.5*smooth(2*t)
return 0.5 * smooth(2 * t)
else:
return 0.5*(1 + smooth(2*t - 1))
return 0.5 * (1 + smooth(2 * t - 1))
def there_and_back(t, inflection = 10.0):
new_t = 2*t if t < 0.5 else 2*(1 - t)
def there_and_back(t, inflection=10.0):
new_t = 2 * t if t < 0.5 else 2 * (1 - t)
return smooth(new_t, inflection)
def there_and_back_with_pause(t):
if t < 1./3:
return smooth(3*t)
elif t < 2./3:
if t < 1. / 3:
return smooth(3 * t)
elif t < 2. / 3:
return 1
else:
return smooth(3 - 3*t)
return smooth(3 - 3 * t)
def running_start(t, pull_factor = -0.5):
def running_start(t, pull_factor=-0.5):
return bezier([0, 0, pull_factor, pull_factor, 1, 1, 1])(t)
def not_quite_there(func = smooth, proportion = 0.7):
def not_quite_there(func=smooth, proportion=0.7):
def result(t):
return proportion*func(t)
return proportion * func(t)
return result
def wiggle(t, wiggles = 2):
return there_and_back(t) * np.sin(wiggles*np.pi*t)
def squish_rate_func(func, a = 0.4, b = 0.6):
def wiggle(t, wiggles=2):
return there_and_back(t) * np.sin(wiggles * np.pi * t)
def squish_rate_func(func, a=0.4, b=0.6):
def result(t):
if a == b:
return a
@@ -55,15 +66,15 @@ def squish_rate_func(func, a = 0.4, b = 0.6):
elif t > b:
return func(1)
else:
return func((t-a)/(b-a))
return func((t - a) / (b - a))
return result
# Stylistically, should this take parameters (with default values)?
# Ultimately, the functionality is entirely subsumed by squish_rate_func,
# but it may be useful to have a nice name for with nice default params for
# but it may be useful to have a nice name for with nice default params for
# "lingering", different from squish_rate_func's default params
def lingering(t):
return squish_rate_func(lambda t: t, 0, 0.8)(t)

View File

@@ -1,27 +1,33 @@
import numpy as np
import operator as op
def sigmoid(x):
return 1.0/(1 + np.exp(-x))
return 1.0 / (1 + np.exp(-x))
def choose(n, r):
if n < r: return 0
if r == 0: return 1
denom = reduce(op.mul, xrange(1, r+1), 1)
numer = reduce(op.mul, xrange(n, n-r, -1), 1)
return numer//denom
if n < r:
return 0
if r == 0:
return 1
denom = reduce(op.mul, xrange(1, r + 1), 1)
numer = reduce(op.mul, xrange(n, n - r, -1), 1)
return numer // denom
# Just to have a less heavyweight name for this extremely common operation
#
# We may wish to have more fine-grained control over division by zero behavior
# in the future (separate specifiable values for 0/0 and x/0 with x != 0),
# but for now, we just allow the option to handle indeterminate 0/0.
def fdiv(a, b, zero_over_zero_value = None):
if zero_over_zero_value != None:
def fdiv(a, b, zero_over_zero_value=None):
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)
where = np.logical_or (a != 0, b != 0)
where = np.logical_or(a != 0, b != 0)
else:
out = None
where = True
return np.true_divide(a, b, out = out, where = where)
return np.true_divide(a, b, out=out, where=where)

View File

@@ -1,5 +1,6 @@
import os
def play_chord(*nums):
commands = [
"play",
@@ -8,10 +9,10 @@ def play_chord(*nums):
"--no-show-progress",
"synth",
] + [
"sin %-"+str(num)
"sin %-" + str(num)
for num in nums
] + [
"fade h 0.5 1 0.5",
"fade h 0.5 1 0.5",
"> /dev/null"
]
try:
@@ -19,8 +20,10 @@ def play_chord(*nums):
except:
pass
def play_error_sound():
play_chord(11, 8, 6, 1)
def play_finish_sound():
play_chord(12, 9, 5, 2)
play_chord(12, 9, 5, 2)

View File

@@ -3,12 +3,14 @@ import numpy as np
from constants import OUT
from constants import RIGHT
#Matrix operations
# Matrix operations
def thick_diagonal(dim, thickness = 2):
def thick_diagonal(dim, thickness=2):
row_indices = np.arange(dim).repeat(dim).reshape((dim, dim))
col_indices = np.transpose(row_indices)
return (np.abs(row_indices - col_indices)<thickness).astype('uint8')
return (np.abs(row_indices - col_indices) < thickness).astype('uint8')
def rotation_matrix(angle, axis):
"""
@@ -19,16 +21,18 @@ def rotation_matrix(angle, axis):
axis_to_z = np.linalg.inv(z_to_axis)
return reduce(np.dot, [z_to_axis, about_z, axis_to_z])
def rotation_about_z(angle):
return [
[np.cos(angle), -np.sin(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[np.sin(angle), np.cos(angle), 0],
[0, 0, 1]
]
def z_to_vector(vector):
"""
Returns some matrix in SO(3) which takes the z-axis to the
Returns some matrix in SO(3) which takes the z-axis to the
(normalized) vector provided as an argument
"""
norm = np.linalg.norm(vector)
@@ -37,7 +41,7 @@ def z_to_vector(vector):
v = np.array(vector) / norm
phi = np.arccos(v[2])
if any(v[:2]):
#projection of vector to unit circle
# projection of vector to unit circle
axis_proj = v[:2] / np.linalg.norm(v[:2])
theta = np.arccos(axis_proj[0])
if axis_proj[1] < 0:
@@ -51,15 +55,18 @@ def z_to_vector(vector):
])
return np.dot(rotation_about_z(theta), phi_down)
def rotate_vector(vector, angle, axis = OUT):
def rotate_vector(vector, angle, axis=OUT):
return np.dot(rotation_matrix(angle, axis), vector)
def angle_between(v1, v2):
return np.arccos(np.dot(
v1 / np.linalg.norm(v1),
v1 / np.linalg.norm(v1),
v2 / np.linalg.norm(v2)
))
def angle_of_vector(vector):
"""
Returns polar coordinate theta when vector is project on xy plane
@@ -69,6 +76,7 @@ def angle_of_vector(vector):
return 0
return np.angle(complex(*vector[:2]))
def angle_between_vectors(v1, v2):
"""
Returns the angle between two 3D vectors.
@@ -76,7 +84,8 @@ def angle_between_vectors(v1, v2):
"""
l1 = np.linalg.norm(v1)
l2 = np.linalg.norm(v2)
return np.arccos(np.dot(v1,v2)/(l1*l2))
return np.arccos(np.dot(v1, v2) / (l1 * l2))
def project_along_vector(point, vector):
matrix = np.identity(3) - np.outer(vector, vector)
@@ -84,20 +93,23 @@ def project_along_vector(point, vector):
###
def compass_directions(n = 4, start_vect = RIGHT):
angle = 2*np.pi/n
def compass_directions(n=4, start_vect=RIGHT):
angle = 2 * np.pi / n
return np.array([
rotate_vector(start_vect, k*angle)
rotate_vector(start_vect, k * angle)
for k in range(n)
])
def complex_to_R3(complex_num):
return np.array((complex_num.real, complex_num.imag, 0))
def R3_to_complex(point):
return complex(*point[:2])
def center_of_mass(points):
points = [np.array(point).astype("float") for point in points]
return sum(points) / len(points)

View File

@@ -1,22 +1,26 @@
import re
import string
def to_camel_case(name):
return "".join([
filter(
lambda c : c not in string.punctuation + string.whitespace, part
lambda c: c not in string.punctuation + string.whitespace, part
).capitalize()
for part in name.split("_")
])
def initials(name, sep_values = [" ", "_"]):
def initials(name, sep_values=[" ", "_"]):
return "".join([
(s[0] if s else "")
(s[0] if s else "")
for s in re.split("|".join(sep_values), name)
])
def camel_case_initials(name):
return filter(lambda c : c.isupper(), name)
return filter(lambda c: c.isupper(), name)
def complex_string(complex_num):
return filter(lambda c : c not in "()", str(complex_num))
return filter(lambda c: c not in "()", str(complex_num))