mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
middle of massive restructure, everything still broken
This commit is contained in:
parent
096c5c1890
commit
2e074afb60
46 changed files with 791 additions and 3970 deletions
13
__init__.py
13
__init__.py
|
@ -1,10 +1,13 @@
|
|||
from animation import *
|
||||
from mobject import *
|
||||
from scene import *
|
||||
from scripts import *
|
||||
from topics import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
from displayer import *
|
||||
from extract_scene import *
|
||||
from helpers import *
|
||||
from image_mobject import *
|
||||
from mobject import *
|
||||
from old_proje import *
|
||||
from playground import *
|
||||
from region import *
|
||||
from script_wrapper import *
|
||||
from tex_utils import *
|
||||
from tex_utils import *
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
from animation import *
|
||||
from transform import *
|
||||
from simple_animations import *
|
||||
from meta_animations import *
|
||||
from simple_animations import *
|
||||
from transform import *
|
|
@ -7,11 +7,10 @@ import os
|
|||
import copy
|
||||
import progressbar
|
||||
import inspect
|
||||
from images2gif import writeGif
|
||||
|
||||
from helpers import *
|
||||
from constants import *
|
||||
from mobject import Mobject, Point
|
||||
from mobject import Mobject
|
||||
from topics.geometry import Point
|
||||
|
||||
class Animation(object):
|
||||
DEFAULT_CONFIG = {
|
||||
|
@ -119,4 +118,3 @@ class Animation(object):
|
|||
|
||||
|
||||
|
||||
|
||||
|
|
95
animation/meta_animations.py
Normal file
95
animation/meta_animations.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
from copy import deepcopy
|
||||
|
||||
from helpers import *
|
||||
from animation import Animation
|
||||
from transform import Transform
|
||||
|
||||
|
||||
class DelayByOrder(Animation):
|
||||
"""
|
||||
Modifier of animation.
|
||||
|
||||
Warning: This will not work on all animation types, but
|
||||
when it does, it will be pretty cool
|
||||
"""
|
||||
DEFAULT_CONFIG = {
|
||||
"max_power" : 5
|
||||
}
|
||||
def __init__(self, animation, **kwargs):
|
||||
digest_config(self, DelayByOrder, kwargs, locals())
|
||||
kwargs.update(dict([
|
||||
(attr, getattr(animation, attr))
|
||||
for attr in Animation.DEFAULT_CONFIG
|
||||
]))
|
||||
self.num_mobject_points = animation.mobject.get_num_points()
|
||||
Animation.__init__(self, animation.mobject, **kwargs)
|
||||
self.name = self.__class__.__name__ + str(self.animation)
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
dim = self.mobject.DIM
|
||||
alpha_array = np.array([
|
||||
[alpha**power]*dim
|
||||
for n in range(self.num_mobject_points)
|
||||
for prop in [(n+1.0)/self.num_mobject_points]
|
||||
for power in [1+prop*(self.max_power-1)]
|
||||
])
|
||||
self.animation.update_mobject(alpha_array)
|
||||
|
||||
|
||||
|
||||
class TransformAnimations(Transform):
|
||||
DEFAULT_CONFIG = {
|
||||
"alpha_func" : squish_alpha_func(smooth)
|
||||
}
|
||||
def __init__(self, start_anim, end_anim, **kwargs):
|
||||
digest_config(self, TransformAnimations, kwargs, locals())
|
||||
if "run_time" in kwargs:
|
||||
self.run_time = kwargs.pop("run_time")
|
||||
else:
|
||||
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():
|
||||
Mobject.align_data(start_anim.starting_mobject, end_anim.starting_mobject)
|
||||
for anim in start_anim, end_anim:
|
||||
if hasattr(anim, "ending_mobject"):
|
||||
Mobject.align_data(anim.starting_mobject, anim.ending_mobject)
|
||||
|
||||
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.ending_mobject
|
||||
|
||||
def update(self, alpha):
|
||||
self.start_anim.update(alpha)
|
||||
self.end_anim.update(alpha)
|
||||
Transform.update(self, alpha)
|
||||
|
||||
|
||||
class Succession(Animation):
|
||||
def __init__(self, *animations, **kwargs):
|
||||
if "run_time" in kwargs:
|
||||
run_time = kwargs.pop("run_time")
|
||||
else:
|
||||
run_time = sum([anim.run_time for anim in animations])
|
||||
self.num_anims = len(animations)
|
||||
self.anims = animations
|
||||
mobject = animations[0].mobject
|
||||
Animation.__init__(self, mobject, run_time = run_time, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + \
|
||||
"".join(map(str, self.anims))
|
||||
|
||||
def update(self, alpha):
|
||||
scaled_alpha = alpha*self.num_anims
|
||||
self.mobject = self.anims
|
||||
for index in range(len(self.anims)):
|
||||
self.anims[index].update(scaled_alpha - index)
|
||||
|
||||
|
||||
|
||||
|
|
@ -2,42 +2,12 @@ import numpy as np
|
|||
import itertools as it
|
||||
from copy import deepcopy
|
||||
|
||||
from animation import Animation
|
||||
from transform import Transform
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
from animation import Animation
|
||||
from meta_animations import DelayByOrder
|
||||
from transform import Transform
|
||||
|
||||
class DelayByOrder(Animation):
|
||||
"""
|
||||
Modifier of animation.
|
||||
|
||||
Warning: This will not work on all animation types, but
|
||||
when it does, it will be pretty cool
|
||||
"""
|
||||
DEFAULT_CONFIG = {
|
||||
"max_power" : 5
|
||||
}
|
||||
def __init__(self, animation, **kwargs):
|
||||
digest_config(self, DelayByOrder, kwargs, locals())
|
||||
kwargs.update(dict([
|
||||
(attr, getattr(animation, attr))
|
||||
for attr in Animation.DEFAULT_CONFIG
|
||||
]))
|
||||
self.num_mobject_points = animation.mobject.get_num_points()
|
||||
Animation.__init__(self, animation.mobject, **kwargs)
|
||||
self.name = self.__class__.__name__ + str(self.animation)
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
dim = self.mobject.DIM
|
||||
alpha_array = np.array([
|
||||
[alpha**power]*dim
|
||||
for n in range(self.num_mobject_points)
|
||||
for prop in [(n+1.0)/self.num_mobject_points]
|
||||
for power in [1+prop*(self.max_power-1)]
|
||||
])
|
||||
self.animation.update_mobject(alpha_array)
|
||||
|
||||
class Rotating(Animation):
|
||||
DEFAULT_CONFIG = {
|
||||
|
@ -55,17 +25,6 @@ class Rotating(Animation):
|
|||
for axis in self.axes:
|
||||
self.mobject.rotate(self.radians * alpha, axis)
|
||||
|
||||
class RotationAsTransform(Rotating):
|
||||
DEFAULT_CONFIG = {
|
||||
"axes" : [OUT],
|
||||
"radians" : np.pi / 2,
|
||||
"run_time" : DEFAULT_ANIMATION_RUN_TIME,
|
||||
"alpha_func" : smooth,
|
||||
}
|
||||
def __init__(self, mobject, **kwargs):
|
||||
digest_config(self, RotationAsTransform, kwargs, locals())
|
||||
Rotating.__init__(self, mobject, **kwargs)
|
||||
|
||||
class FadeOut(Animation):
|
||||
def update_mobject(self, alpha):
|
||||
self.mobject.rgbs = self.starting_mobject.rgbs * (1 - alpha)
|
||||
|
@ -136,96 +95,6 @@ class Homotopy(Animation):
|
|||
for x, y, z in self.starting_mobject.points
|
||||
])
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, **kwargs):
|
||||
"""
|
||||
Complex Hootopy a function (z, t) to z'
|
||||
"""
|
||||
def homotopy((x, y, z, t)):
|
||||
c = complex_homotopy((complex(x, y), t))
|
||||
return (c.real, c.imag, z)
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
mobject = args.pop(0)
|
||||
elif "mobject" in kwargs:
|
||||
mobject = kwargs["mobject"]
|
||||
else:
|
||||
mobject = Grid()
|
||||
Homotopy.__init__(self, homotopy, mobject, *args, **kwargs)
|
||||
self.name = "ComplexHomotopy" + \
|
||||
to_cammel_case(complex_homotopy.__name__)
|
||||
|
||||
class Succession(Animation):
|
||||
def __init__(self, *animations, **kwargs):
|
||||
if "run_time" in kwargs:
|
||||
run_time = kwargs.pop("run_time")
|
||||
else:
|
||||
run_time = sum([anim.run_time for anim in animations])
|
||||
self.num_anims = len(animations)
|
||||
self.anims = animations
|
||||
mobject = animations[0].mobject
|
||||
Animation.__init__(self, mobject, run_time = run_time, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.__class__.__name__ + \
|
||||
"".join(map(str, self.anims))
|
||||
|
||||
def update(self, alpha):
|
||||
scaled_alpha = alpha*self.num_anims
|
||||
self.mobject = self.anims
|
||||
for index in range(len(self.anims)):
|
||||
self.anims[index].update(scaled_alpha - index)
|
||||
|
||||
####### Pi Creature Stuff #############
|
||||
|
||||
class WalkPiCreature(Animation):
|
||||
def __init__(self, pi_creature, destination, *args, **kwargs):
|
||||
self.final = deepcopy(pi_creature).move_to(destination)
|
||||
self.middle = pi_creature.get_step_intermediate(self.final)
|
||||
Animation.__init__(self, pi_creature, *args, **kwargs)
|
||||
|
||||
def update_mobject(self, alpha):
|
||||
if alpha < 0.5:
|
||||
Mobject.interpolate(
|
||||
self.starting_mobject,
|
||||
self.middle,
|
||||
self.mobject,
|
||||
2*alpha
|
||||
)
|
||||
else:
|
||||
Mobject.interpolate(
|
||||
self.middle,
|
||||
self.final,
|
||||
self.mobject,
|
||||
2*alpha - 1
|
||||
)
|
||||
|
||||
|
||||
class BlinkPiCreature(Transform):
|
||||
def __init__(self, pi_creature, *args, **kwargs):
|
||||
blinked = deepcopy(pi_creature).blink()
|
||||
Transform.__init__(
|
||||
self, pi_creature, blinked,
|
||||
alpha_func = squish_alpha_func(there_and_back),
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class WaveArm(Transform):
|
||||
def __init__(self, pi_creature, *args, **kwargs):
|
||||
final = deepcopy(pi_creature)
|
||||
body_to_arm = pi_creature.arm.get_center()-pi_creature.get_center()
|
||||
if body_to_arm[0] < 0:
|
||||
wag_direction = LEFT
|
||||
else:
|
||||
wag_direction = RIGHT
|
||||
final.arm.wag(0.7*UP, wag_direction, 2.0)
|
||||
final.rewire_part_attributes(self_from_parts = True)
|
||||
Transform.__init__(
|
||||
self, pi_creature, final,
|
||||
alpha_func = there_and_back,
|
||||
*args, **kwargs
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,39 +4,12 @@ import inspect
|
|||
import copy
|
||||
import warnings
|
||||
|
||||
from animation import Animation
|
||||
from mobject import Mobject, Point, ComplexPlane
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
def straight_path(start_points, end_points, alpha):
|
||||
return (1-alpha)*start_points + alpha*end_points
|
||||
|
||||
def path_along_arc(arc_angle):
|
||||
"""
|
||||
If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
|
||||
perpendicualr to vect in the left direction.
|
||||
"""
|
||||
if arc_angle == 0:
|
||||
return straight_path
|
||||
def path(start_points, end_points, alpha):
|
||||
vects = end_points - start_points
|
||||
centers = start_points + 0.5*vects
|
||||
if arc_angle != np.pi:
|
||||
for i, b in [(0, -1), (1, 1)]:
|
||||
centers[:,i] += 0.5*b*vects[:,1-i]/np.tan(arc_angle/2)
|
||||
return centers + np.dot(
|
||||
start_points-centers,
|
||||
np.transpose(rotation_about_z(alpha*arc_angle))
|
||||
)
|
||||
return path
|
||||
|
||||
def clockwise_path():
|
||||
return path_along_arc(np.pi)
|
||||
|
||||
def counterclockwise_path():
|
||||
return path_along_arc(-np.pi)
|
||||
|
||||
from animation import Animation
|
||||
from mobject import Mobject
|
||||
from topics.geometry import Point
|
||||
from topics.complex_numbers import ComplexPlane
|
||||
|
||||
class Transform(Animation):
|
||||
DEFAULT_CONFIG = {
|
||||
|
@ -137,6 +110,12 @@ class ApplyMethod(Transform):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
class Rotate(ApplyMethod):
|
||||
def __init__(self, mobject, angle = np.pi, **kwargs):
|
||||
kwargs["interpolation_function"] = path_along_arc(angle)
|
||||
ApplyMethod.__init__(self, mobject.rotate, angle, **kwargs)
|
||||
|
||||
|
||||
class ApplyPointwiseFunction(ApplyMethod):
|
||||
DEFAULT_CONFIG = {
|
||||
"run_time" : DEFAULT_POINTWISE_FUNCTION_RUN_TIME
|
||||
|
@ -147,20 +126,6 @@ class ApplyPointwiseFunction(ApplyMethod):
|
|||
self, mobject.apply_function, function, **kwargs
|
||||
)
|
||||
|
||||
|
||||
class ComplexFunction(ApplyPointwiseFunction):
|
||||
def __init__(self, function, mobject = ComplexPlane, **kwargs):
|
||||
if "interpolation_function" not in kwargs:
|
||||
self.interpolation_function = path_along_arc(
|
||||
np.log(function(complex(1))).imag
|
||||
)
|
||||
ApplyPointwiseFunction.__init__(
|
||||
self,
|
||||
lambda (x, y, z) : complex_to_R3(function(complex(x, y))),
|
||||
instantiate(mobject),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
class FadeToColor(ApplyMethod):
|
||||
def __init__(self, mobject, color, **kwargs):
|
||||
ApplyMethod.__init__(self, mobject.highlight, color, **kwargs)
|
||||
|
@ -200,35 +165,6 @@ class ApplyMatrix(Animation):
|
|||
)
|
||||
|
||||
|
||||
class TransformAnimations(Transform):
|
||||
DEFAULT_CONFIG = {
|
||||
"alpha_func" : squish_alpha_func(smooth)
|
||||
}
|
||||
def __init__(self, start_anim, end_anim, **kwargs):
|
||||
digest_config(self, TransformAnimations, kwargs, locals())
|
||||
if "run_time" in kwargs:
|
||||
self.run_time = kwargs.pop("run_time")
|
||||
else:
|
||||
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():
|
||||
Mobject.align_data(start_anim.starting_mobject, end_anim.starting_mobject)
|
||||
for anim in start_anim, end_anim:
|
||||
if hasattr(anim, "ending_mobject"):
|
||||
Mobject.align_data(anim.starting_mobject, anim.ending_mobject)
|
||||
|
||||
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.ending_mobject
|
||||
|
||||
def update(self, alpha):
|
||||
self.start_anim.update(alpha)
|
||||
self.end_anim.update(alpha)
|
||||
Transform.update(self, alpha)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
16
displayer.py
16
displayer.py
|
@ -8,8 +8,7 @@ import cv2
|
|||
from colour import Color
|
||||
import progressbar
|
||||
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
FFMPEG_BIN = "ffmpeg"
|
||||
|
||||
|
@ -136,19 +135,6 @@ def get_file_path(name, extension):
|
|||
os.makedirs(directory)
|
||||
return file_path
|
||||
|
||||
def write_to_gif(scene, name):
|
||||
#TODO, find better means of compression
|
||||
if not name.endswith(".gif"):
|
||||
name += ".gif"
|
||||
file_path = os.path.join(GIF_DIR, name)
|
||||
temppath = os.path.join(GIF_DIR, "Temp.gif")
|
||||
print "Writing " + name + "..."
|
||||
images = [Image.fromarray(frame) for frame in scene.frames]
|
||||
writeGif(temppath, images, scene.frame_duration)
|
||||
print "Compressing..."
|
||||
os.system("gifsicle -O " + temppath + " > " + file_path)
|
||||
os.system("rm " + temppath)
|
||||
|
||||
def write_to_movie(scene, name):
|
||||
file_path = get_file_path(name, ".mp4")
|
||||
print "Writing to %s"%file_path
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import sys
|
||||
import getopt
|
||||
import imp
|
||||
import itertools as it
|
||||
import inspect
|
||||
import traceback
|
||||
from helpers import cammel_case_initials
|
||||
from scene import Scene
|
||||
import imp
|
||||
|
||||
from constants import *
|
||||
from helpers import *
|
||||
from scene import Scene
|
||||
|
||||
HELP_MESSAGE = """
|
||||
<script name> [<scene name or initials>] [<arg_string>]
|
||||
|
@ -35,6 +37,7 @@ def get_configuration(sys_argv):
|
|||
print str(err)
|
||||
sys.exit(2)
|
||||
config = {
|
||||
"module" : None,
|
||||
"scene_name" : "",
|
||||
"args_extension" : "",
|
||||
"display_config" : PRODUCTION_QUALITY_DISPLAY_CONFIG,
|
||||
|
@ -70,9 +73,11 @@ def get_configuration(sys_argv):
|
|||
config["write"] = True
|
||||
|
||||
if len(args) > 0:
|
||||
config["scene_name"] = args[0]
|
||||
config["module"] = args[0]
|
||||
if len(args) > 1:
|
||||
config["args_extension"] = " ".join(args[1:])
|
||||
config["scene_name"] = args[1]
|
||||
if len(args) > 2:
|
||||
config["args_extension"] = " ".join(args[2:])
|
||||
return config
|
||||
|
||||
def handle_scene(scene, **config):
|
||||
|
@ -120,7 +125,7 @@ def prompt_user_for_args(args_list, args_to_string):
|
|||
print INVALID_NUMBER_MESSAGE
|
||||
sys.exit()
|
||||
|
||||
def get_args(SceneClass, config):
|
||||
def get_scene_args(SceneClass, config):
|
||||
tuplify = lambda x : x if type(x) == tuple else (x,)
|
||||
args_list = map(tuplify, SceneClass.args_list)
|
||||
preset_extensions = [
|
||||
|
@ -146,11 +151,13 @@ def get_args(SceneClass, config):
|
|||
else:
|
||||
return [SceneClass.string_to_args(config["args_extension"])]
|
||||
|
||||
def command_line_create_scene(movie_prefix = ""):
|
||||
script = sys.modules["__main__"]
|
||||
scene_names_to_classes = dict(inspect.getmembers(script, is_scene))
|
||||
def main():
|
||||
config = get_configuration(sys.argv)
|
||||
config["movie_prefix"] = movie_prefix
|
||||
module = imp.load_source(config["module_name"], ".")
|
||||
scene_names_to_classes = dict(
|
||||
inspect.getmembers(module, is_scene)
|
||||
)
|
||||
config["movie_prefix"] = config["module_name"].split(".py")[0]
|
||||
if config["scene_name"] in scene_names_to_classes:
|
||||
scene_classes = [scene_names_to_classes[config["scene_name"]] ]
|
||||
elif config["scene_name"] == "" and config["write_all"]:
|
||||
|
@ -164,7 +171,7 @@ def command_line_create_scene(movie_prefix = ""):
|
|||
"announce_construction" : True
|
||||
}
|
||||
for SceneClass in scene_classes:
|
||||
for args in get_args(SceneClass, config):
|
||||
for args in get_scene_args(SceneClass, config):
|
||||
scene_kwargs["construct_args"] = args
|
||||
try:
|
||||
handle_scene(SceneClass(**scene_kwargs), **config)
|
||||
|
@ -174,7 +181,8 @@ def command_line_create_scene(movie_prefix = ""):
|
|||
print "\n\n"
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
|
32
helpers.py
32
helpers.py
|
@ -93,6 +93,38 @@ def random_color():
|
|||
color.set_rgb([1 - 0.5 * random() for x in range(3)])
|
||||
return color
|
||||
|
||||
|
||||
################################################
|
||||
|
||||
def straight_path(start_points, end_points, alpha):
|
||||
return (1-alpha)*start_points + alpha*end_points
|
||||
|
||||
def path_along_arc(arc_angle):
|
||||
"""
|
||||
If vect is vector from start to end, [vect[:,1], -vect[:,0]] is
|
||||
perpendicualr to vect in the left direction.
|
||||
"""
|
||||
if arc_angle == 0:
|
||||
return straight_path
|
||||
def path(start_points, end_points, alpha):
|
||||
vects = end_points - start_points
|
||||
centers = start_points + 0.5*vects
|
||||
if arc_angle != np.pi:
|
||||
for i, b in [(0, -1), (1, 1)]:
|
||||
centers[:,i] += 0.5*b*vects[:,1-i]/np.tan(arc_angle/2)
|
||||
return centers + np.dot(
|
||||
start_points-centers,
|
||||
np.transpose(rotation_about_z(alpha*arc_angle))
|
||||
)
|
||||
return path
|
||||
|
||||
def clockwise_path():
|
||||
return path_along_arc(np.pi)
|
||||
|
||||
def counterclockwise_path():
|
||||
return path_along_arc(-np.pi)
|
||||
|
||||
|
||||
################################################
|
||||
|
||||
def to_cammel_case(name):
|
||||
|
|
|
@ -4,8 +4,8 @@ import os
|
|||
from PIL import Image
|
||||
from random import random
|
||||
|
||||
from tex_utils import *
|
||||
from mobject import *
|
||||
from tex_utils import tex_to_image
|
||||
from mobject import Mobject
|
||||
|
||||
class ImageMobject(Mobject):
|
||||
"""
|
||||
|
@ -98,26 +98,6 @@ class ImageMobject(Mobject):
|
|||
points *= 2 * SPACE_WIDTH / width
|
||||
self.add_points(points, rgbs = rgbs)
|
||||
|
||||
class Face(ImageMobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"mode" : "simple",
|
||||
"scale_value" : 0.5
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Mode can be "simple", "talking", "straight"
|
||||
"""
|
||||
digest_config(self, Face, kwargs)
|
||||
ImageMobject.__init__(self, self.mode + "_face", **kwargs)
|
||||
|
||||
class VideoIcon(ImageMobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"scale_value" : 0.3
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, VideoIcon, kwargs)
|
||||
ImageMobject.__init__(self, "video_icon", **kwargs)
|
||||
|
||||
#TODO, Make both of these proper mobject classes
|
||||
def text_mobject(text, size = None):
|
||||
size = size or "\\Large" #TODO, auto-adjust?
|
845
images2gif.py
845
images2gif.py
|
@ -1,845 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2010, Almar Klein, Ant1, Marius van Voorden
|
||||
#
|
||||
# This code is subject to the (new) BSD license:
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# * Neither the name of the <organization> nor the
|
||||
# names of its contributors may be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
# ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
""" Module images2gif
|
||||
|
||||
Provides functionality for reading and writing animated GIF images.
|
||||
Use writeGif to write a series of numpy arrays or PIL images as an
|
||||
animated GIF. Use readGif to read an animated gif as a series of numpy
|
||||
arrays.
|
||||
|
||||
Acknowledgements
|
||||
----------------
|
||||
|
||||
Many thanks to Ant1 for:
|
||||
* noting the use of "palette=PIL.Image.ADAPTIVE", which significantly
|
||||
improves the results.
|
||||
* the modifications to save each image with its own palette, or optionally
|
||||
the global palette (if its the same).
|
||||
|
||||
Many thanks to Marius van Voorden for porting the NeuQuant quantization
|
||||
algorithm of Anthony Dekker to Python (See the NeuQuant class for its
|
||||
license).
|
||||
|
||||
This code is based on gifmaker (in the scripts folder of the source
|
||||
distribution of PIL)
|
||||
|
||||
Some implementation details are ased on gif file structure as provided
|
||||
by wikipedia.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import progressbar
|
||||
|
||||
try:
|
||||
import PIL
|
||||
from PIL import Image, ImageChops
|
||||
from PIL.GifImagePlugin import getheader, getdata
|
||||
except ImportError:
|
||||
PIL = None
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
np = None
|
||||
|
||||
try:
|
||||
from scipy.spatial import cKDTree
|
||||
except ImportError:
|
||||
cKDTree = None
|
||||
|
||||
# getheader gives a 87a header and a color palette (two elements in a list).
|
||||
# getdata()[0] gives the Image Descriptor up to (including) "LZW min code size".
|
||||
# getdatas()[1:] is the image data itself in chuncks of 256 bytes (well
|
||||
# technically the first byte says how many bytes follow, after which that
|
||||
# amount (max 255) follows).
|
||||
|
||||
def checkImages(images):
|
||||
""" checkImages(images)
|
||||
Check numpy images and correct intensity range etc.
|
||||
The same for all movie formats.
|
||||
"""
|
||||
# Init results
|
||||
images2 = []
|
||||
|
||||
for im in images:
|
||||
if PIL and isinstance(im, PIL.Image.Image):
|
||||
# We assume PIL images are allright
|
||||
images2.append(im)
|
||||
|
||||
elif np and isinstance(im, np.ndarray):
|
||||
# Check and convert dtype
|
||||
if im.dtype == np.uint8:
|
||||
images2.append(im) # Ok
|
||||
elif im.dtype in [np.float32, np.float64]:
|
||||
im = im.copy()
|
||||
im[im<0] = 0
|
||||
im[im>1] = 1
|
||||
im *= 255
|
||||
images2.append( im.astype(np.uint8) )
|
||||
else:
|
||||
im = im.astype(np.uint8)
|
||||
images2.append(im)
|
||||
# Check size
|
||||
if im.ndim == 2:
|
||||
pass # ok
|
||||
elif im.ndim == 3:
|
||||
if im.shape[2] not in [3,4]:
|
||||
raise ValueError('This array can not represent an image.')
|
||||
else:
|
||||
raise ValueError('This array can not represent an image.')
|
||||
else:
|
||||
raise ValueError('Invalid image type: ' + str(type(im)))
|
||||
|
||||
# Done
|
||||
return images2
|
||||
|
||||
|
||||
def intToBin(i):
|
||||
""" Integer to two bytes """
|
||||
# devide in two parts (bytes)
|
||||
i1 = i % 256
|
||||
i2 = int( i/256)
|
||||
# make string (little endian)
|
||||
return chr(i1) + chr(i2)
|
||||
|
||||
|
||||
def getheaderAnim(im):
|
||||
""" Animation header. To replace the getheader()[0] """
|
||||
bb = "GIF89a"
|
||||
bb += intToBin(im.size[0])
|
||||
bb += intToBin(im.size[1])
|
||||
bb += "\x87\x00\x00"
|
||||
return bb
|
||||
|
||||
|
||||
def getImageDescriptor(im):
|
||||
""" Used for the local color table properties per image.
|
||||
Otherwise global color table applies to all frames irrespective of
|
||||
wether additional colours comes in play that require a redefined palette
|
||||
Still a maximum of 256 color per frame, obviously.
|
||||
|
||||
Written by Ant1 on 2010-08-22
|
||||
"""
|
||||
bb = '\x2C' # Image separator,
|
||||
bb += intToBin( 0 ) # Left position
|
||||
bb += intToBin( 0 ) # Top position
|
||||
bb += intToBin( im.size[0] ) # image width
|
||||
bb += intToBin( im.size[1] ) # image height
|
||||
bb += '\x87' # packed field : local color table flag1, interlace0, sorted table0, reserved00, lct size111=7=2^(7+1)=256.
|
||||
# LZW minimum size code now comes later, begining of [image data] blocks
|
||||
return bb
|
||||
|
||||
|
||||
#def getAppExt(loops=float('inf')):
|
||||
#compile error commented by zcwang
|
||||
def getAppExt(loops=float(0)):
|
||||
""" Application extention. Part that specifies amount of loops.
|
||||
If loops is inf, it goes on infinitely.
|
||||
"""
|
||||
if loops == 0:
|
||||
loops = 2**16-1
|
||||
#bb = "" # application extension should not be used
|
||||
# (the extension interprets zero loops
|
||||
# to mean an infinite number of loops)
|
||||
# Mmm, does not seem to work
|
||||
if True:
|
||||
bb = "\x21\xFF\x0B" # application extension
|
||||
bb += "NETSCAPE2.0"
|
||||
bb += "\x03\x01"
|
||||
# if loops == float('inf'):
|
||||
if loops == float(0):
|
||||
loops = 2**16-1
|
||||
bb += intToBin(loops)
|
||||
bb += '\x00' # end
|
||||
return bb
|
||||
|
||||
|
||||
def getGraphicsControlExt(duration=0.1):
|
||||
""" Graphics Control Extension. A sort of header at the start of
|
||||
each image. Specifies transparancy and duration. """
|
||||
bb = '\x21\xF9\x04'
|
||||
bb += '\x08' # no transparancy
|
||||
bb += intToBin( int(duration*100) ) # in 100th of seconds
|
||||
bb += '\x00' # no transparant color
|
||||
bb += '\x00' # end
|
||||
return bb
|
||||
|
||||
|
||||
def _writeGifToFile(fp, images, durations, loops):
|
||||
""" Given a set of images writes the bytes to the specified stream.
|
||||
"""
|
||||
# Obtain palette for all images and count each occurance
|
||||
palettes, occur = [], []
|
||||
for im in images:
|
||||
palettes.append( getheader(im)[1] )
|
||||
for palette in palettes:
|
||||
occur.append( palettes.count( palette ) )
|
||||
|
||||
# Select most-used palette as the global one (or first in case no max)
|
||||
globalPalette = palettes[ occur.index(max(occur)) ]
|
||||
|
||||
# Init
|
||||
frames = 0
|
||||
firstFrame = True
|
||||
|
||||
|
||||
for im, palette in zip(images, palettes):
|
||||
if firstFrame:
|
||||
# Write header
|
||||
|
||||
# Gather info
|
||||
header = getheaderAnim(im)
|
||||
appext = getAppExt(loops)
|
||||
|
||||
# Write
|
||||
fp.write(header)
|
||||
fp.write(globalPalette)
|
||||
fp.write(appext)
|
||||
|
||||
# Next frame is not the first
|
||||
firstFrame = False
|
||||
|
||||
if True:
|
||||
# Write palette and image data
|
||||
|
||||
# Gather info
|
||||
data = getdata(im)
|
||||
imdes, data = data[0], data[1:]
|
||||
graphext = getGraphicsControlExt(durations[frames])
|
||||
# Make image descriptor suitable for using 256 local color palette
|
||||
lid = getImageDescriptor(im)
|
||||
|
||||
# Write local header
|
||||
if palette != globalPalette:
|
||||
# Use local color palette
|
||||
fp.write(graphext)
|
||||
fp.write(lid) # write suitable image descriptor
|
||||
fp.write(palette) # write local color table
|
||||
fp.write('\x08') # LZW minimum size code
|
||||
else:
|
||||
# Use global color palette
|
||||
fp.write(graphext)
|
||||
fp.write(imdes) # write suitable image descriptor
|
||||
|
||||
# Write image data
|
||||
for d in data:
|
||||
fp.write(d)
|
||||
|
||||
# Prepare for next round
|
||||
frames = frames + 1
|
||||
|
||||
fp.write(";") # end gif
|
||||
return frames
|
||||
|
||||
|
||||
## Exposed functions
|
||||
|
||||
def writeGif(filename, images, duration=0.1, repeat=True, dither=False, nq=0):
|
||||
""" writeGif(filename, images, duration=0.1, repeat=True, dither=False)
|
||||
|
||||
Write an animated gif from the specified images.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
filename : string
|
||||
The name of the file to write the image to.
|
||||
images : list
|
||||
Should be a list consisting of PIL images or numpy arrays.
|
||||
The latter should be between 0 and 255 for integer types, and
|
||||
between 0 and 1 for float types.
|
||||
duration : scalar or list of scalars
|
||||
The duration for all frames, or (if a list) for each frame.
|
||||
repeat : bool or integer
|
||||
The amount of loops. If True, loops infinitetely.
|
||||
dither : bool
|
||||
Whether to apply dithering
|
||||
nq : integer
|
||||
If nonzero, applies the NeuQuant quantization algorithm to create
|
||||
the color palette. This algorithm is superior, but slower than
|
||||
the standard PIL algorithm. The value of nq is the quality
|
||||
parameter. 1 represents the best quality. 10 is in general a
|
||||
good tradeoff between quality and speed.
|
||||
|
||||
"""
|
||||
progress_bar = progressbar.ProgressBar(maxval=len(images))
|
||||
progress_bar.start()
|
||||
|
||||
# Check PIL
|
||||
if PIL is None:
|
||||
raise RuntimeError("Need PIL to write animated gif files.")
|
||||
|
||||
# Check images
|
||||
images = checkImages(images)
|
||||
|
||||
# Check loops
|
||||
if repeat is False:
|
||||
loops = 1
|
||||
elif repeat is True:
|
||||
loops = 0 # zero means infinite
|
||||
else:
|
||||
loops = int(repeat)
|
||||
|
||||
# Convert to PIL images
|
||||
images2 = []
|
||||
for im in images:
|
||||
if isinstance(im, Image.Image):
|
||||
images2.append(im)
|
||||
elif np and isinstance(im, np.ndarray):
|
||||
if im.ndim==3 and im.shape[2]==3:
|
||||
im = Image.fromarray(im,'RGB')
|
||||
elif im.ndim==2:
|
||||
im = Image.fromarray(im,'L')
|
||||
images2.append(im)
|
||||
|
||||
# Convert to paletted PIL images
|
||||
images, images2 = images2, []
|
||||
if nq >= 1:
|
||||
# NeuQuant algorithm
|
||||
for im in images:
|
||||
im = im.convert("RGBA") # NQ assumes RGBA
|
||||
NQ = NeuQuant(im, int(nq)) # Learn colors from image
|
||||
if dither:
|
||||
im = im.convert("RGB").quantize(palette=NQ.paletteImage())
|
||||
else:
|
||||
im = NQ.quantize(im) # Use to quantize the image itself
|
||||
images2.append(im)
|
||||
else:
|
||||
# Adaptive PIL algorithm
|
||||
AD = Image.ADAPTIVE
|
||||
count = 0
|
||||
for im in images:
|
||||
progress_bar.update(count)
|
||||
count += 1
|
||||
im = im.convert('P', palette=AD, dither=dither)
|
||||
images2.append(im)
|
||||
|
||||
# Check duration
|
||||
if hasattr(duration, '__len__'):
|
||||
if len(duration) == len(images2):
|
||||
durations = [d for d in duration]
|
||||
else:
|
||||
raise ValueError("len(duration) doesn't match amount of images.")
|
||||
else:
|
||||
duration = [duration for im in images2]
|
||||
|
||||
# Open file
|
||||
fp = open(filename, 'wb')
|
||||
|
||||
# Write
|
||||
try:
|
||||
n = _writeGifToFile(fp, images2, duration, loops)
|
||||
finally:
|
||||
fp.close()
|
||||
progress_bar.finish()
|
||||
|
||||
|
||||
def readGif(filename, asNumpy=True):
|
||||
""" readGif(filename, asNumpy=True)
|
||||
|
||||
Read images from an animated GIF file. Returns a list of numpy
|
||||
arrays, or, if asNumpy is false, a list if PIL images.
|
||||
|
||||
"""
|
||||
|
||||
# Check PIL
|
||||
if PIL is None:
|
||||
raise RuntimeError("Need PIL to read animated gif files.")
|
||||
|
||||
# Check Numpy
|
||||
if np is None:
|
||||
raise RuntimeError("Need Numpy to read animated gif files.")
|
||||
|
||||
# Check whether it exists
|
||||
if not os.path.isfile(filename):
|
||||
raise IOError('File not found: '+str(filename))
|
||||
|
||||
# Load file using PIL
|
||||
pilIm = PIL.Image.open(filename)
|
||||
pilIm.seek(0)
|
||||
|
||||
# Read all images inside
|
||||
images = []
|
||||
try:
|
||||
while True:
|
||||
# Get image as numpy array
|
||||
tmp = pilIm.convert() # Make without palette
|
||||
a = np.asarray(tmp)
|
||||
if len(a.shape)==0:
|
||||
raise MemoryError("Too little memory to convert PIL image to array")
|
||||
# Store, and next
|
||||
images.append(a)
|
||||
pilIm.seek(pilIm.tell()+1)
|
||||
except EOFError:
|
||||
pass
|
||||
|
||||
# Convert to normal PIL images if needed
|
||||
if not asNumpy:
|
||||
images2 = images
|
||||
images = []
|
||||
for im in images2:
|
||||
images.append( PIL.Image.fromarray(im) )
|
||||
|
||||
# Done
|
||||
return images
|
||||
|
||||
|
||||
class NeuQuant:
|
||||
""" NeuQuant(image, samplefac=10, colors=256)
|
||||
|
||||
samplefac should be an integer number of 1 or higher, 1
|
||||
being the highest quality, but the slowest performance.
|
||||
With avalue of 10, one tenth of all pixels are used during
|
||||
training. This value seems a nice tradeof between speed
|
||||
and quality.
|
||||
|
||||
colors is the amount of colors to reduce the image to. This
|
||||
should best be a power of two.
|
||||
|
||||
See also:
|
||||
http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||
|
||||
License of the NeuQuant Neural-Net Quantization Algorithm
|
||||
---------------------------------------------------------
|
||||
|
||||
Copyright (c) 1994 Anthony Dekker
|
||||
Ported to python by Marius van Voorden in 2010
|
||||
|
||||
NEUQUANT Neural-Net quantization algorithm by Anthony Dekker, 1994.
|
||||
See "Kohonen neural networks for optimal colour quantization"
|
||||
in "network: Computation in Neural Systems" Vol. 5 (1994) pp 351-367.
|
||||
for a discussion of the algorithm.
|
||||
See also http://members.ozemail.com.au/~dekker/NEUQUANT.HTML
|
||||
|
||||
Any party obtaining a copy of these files from the author, directly or
|
||||
indirectly, is granted, free of charge, a full and unrestricted irrevocable,
|
||||
world-wide, paid up, royalty-free, nonexclusive right and license to deal
|
||||
in this software and documentation files (the "Software"), including without
|
||||
limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons who receive
|
||||
copies from any such party to do so, with the only requirement being
|
||||
that this copyright notice remain intact.
|
||||
|
||||
"""
|
||||
|
||||
NCYCLES = None # Number of learning cycles
|
||||
NETSIZE = None # Number of colours used
|
||||
SPECIALS = None # Number of reserved colours used
|
||||
BGCOLOR = None # Reserved background colour
|
||||
CUTNETSIZE = None
|
||||
MAXNETPOS = None
|
||||
|
||||
INITRAD = None # For 256 colours, radius starts at 32
|
||||
RADIUSBIASSHIFT = None
|
||||
RADIUSBIAS = None
|
||||
INITBIASRADIUS = None
|
||||
RADIUSDEC = None # Factor of 1/30 each cycle
|
||||
|
||||
ALPHABIASSHIFT = None
|
||||
INITALPHA = None # biased by 10 bits
|
||||
|
||||
GAMMA = None
|
||||
BETA = None
|
||||
BETAGAMMA = None
|
||||
|
||||
network = None # The network itself
|
||||
colormap = None # The network itself
|
||||
|
||||
netindex = None # For network lookup - really 256
|
||||
|
||||
bias = None # Bias and freq arrays for learning
|
||||
freq = None
|
||||
|
||||
pimage = None
|
||||
|
||||
# Four primes near 500 - assume no image has a length so large
|
||||
# that it is divisible by all four primes
|
||||
PRIME1 = 499
|
||||
PRIME2 = 491
|
||||
PRIME3 = 487
|
||||
PRIME4 = 503
|
||||
MAXPRIME = PRIME4
|
||||
|
||||
pixels = None
|
||||
samplefac = None
|
||||
|
||||
a_s = None
|
||||
|
||||
|
||||
def setconstants(self, samplefac, colors):
|
||||
self.NCYCLES = 100 # Number of learning cycles
|
||||
self.NETSIZE = colors # Number of colours used
|
||||
self.SPECIALS = 3 # Number of reserved colours used
|
||||
self.BGCOLOR = self.SPECIALS-1 # Reserved background colour
|
||||
self.CUTNETSIZE = self.NETSIZE - self.SPECIALS
|
||||
self.MAXNETPOS = self.NETSIZE - 1
|
||||
|
||||
self.INITRAD = self.NETSIZE/8 # For 256 colours, radius starts at 32
|
||||
self.RADIUSBIASSHIFT = 6
|
||||
self.RADIUSBIAS = 1 << self.RADIUSBIASSHIFT
|
||||
self.INITBIASRADIUS = self.INITRAD * self.RADIUSBIAS
|
||||
self.RADIUSDEC = 30 # Factor of 1/30 each cycle
|
||||
|
||||
self.ALPHABIASSHIFT = 10 # Alpha starts at 1
|
||||
self.INITALPHA = 1 << self.ALPHABIASSHIFT # biased by 10 bits
|
||||
|
||||
self.GAMMA = 1024.0
|
||||
self.BETA = 1.0/1024.0
|
||||
self.BETAGAMMA = self.BETA * self.GAMMA
|
||||
|
||||
self.network = np.empty((self.NETSIZE, 3), dtype='float64') # The network itself
|
||||
self.colormap = np.empty((self.NETSIZE, 4), dtype='int32') # The network itself
|
||||
|
||||
self.netindex = np.empty(256, dtype='int32') # For network lookup - really 256
|
||||
|
||||
self.bias = np.empty(self.NETSIZE, dtype='float64') # Bias and freq arrays for learning
|
||||
self.freq = np.empty(self.NETSIZE, dtype='float64')
|
||||
|
||||
self.pixels = None
|
||||
self.samplefac = samplefac
|
||||
|
||||
self.a_s = {}
|
||||
|
||||
def __init__(self, image, samplefac=10, colors=256):
|
||||
|
||||
# Check Numpy
|
||||
if np is None:
|
||||
raise RuntimeError("Need Numpy for the NeuQuant algorithm.")
|
||||
|
||||
# Check image
|
||||
if image.size[0] * image.size[1] < NeuQuant.MAXPRIME:
|
||||
raise IOError("Image is too small")
|
||||
assert image.mode == "RGBA"
|
||||
|
||||
# Initialize
|
||||
self.setconstants(samplefac, colors)
|
||||
self.pixels = np.fromstring(image.tostring(), np.uint32)
|
||||
self.setUpArrays()
|
||||
|
||||
self.learn()
|
||||
self.fix()
|
||||
self.inxbuild()
|
||||
|
||||
def writeColourMap(self, rgb, outstream):
|
||||
for i in range(self.NETSIZE):
|
||||
bb = self.colormap[i,0];
|
||||
gg = self.colormap[i,1];
|
||||
rr = self.colormap[i,2];
|
||||
out.write(rr if rgb else bb)
|
||||
out.write(gg)
|
||||
out.write(bb if rgb else rr)
|
||||
return self.NETSIZE
|
||||
|
||||
def setUpArrays(self):
|
||||
self.network[0,0] = 0.0 # Black
|
||||
self.network[0,1] = 0.0
|
||||
self.network[0,2] = 0.0
|
||||
|
||||
self.network[1,0] = 255.0 # White
|
||||
self.network[1,1] = 255.0
|
||||
self.network[1,2] = 255.0
|
||||
|
||||
# RESERVED self.BGCOLOR # Background
|
||||
|
||||
for i in range(self.SPECIALS):
|
||||
self.freq[i] = 1.0 / self.NETSIZE
|
||||
self.bias[i] = 0.0
|
||||
|
||||
for i in range(self.SPECIALS, self.NETSIZE):
|
||||
p = self.network[i]
|
||||
p[:] = (255.0 * (i-self.SPECIALS)) / self.CUTNETSIZE
|
||||
|
||||
self.freq[i] = 1.0 / self.NETSIZE
|
||||
self.bias[i] = 0.0
|
||||
|
||||
# Omitted: setPixels
|
||||
|
||||
def altersingle(self, alpha, i, b, g, r):
|
||||
"""Move neuron i towards biased (b,g,r) by factor alpha"""
|
||||
n = self.network[i] # Alter hit neuron
|
||||
n[0] -= (alpha*(n[0] - b))
|
||||
n[1] -= (alpha*(n[1] - g))
|
||||
n[2] -= (alpha*(n[2] - r))
|
||||
|
||||
def geta(self, alpha, rad):
|
||||
try:
|
||||
return self.a_s[(alpha, rad)]
|
||||
except KeyError:
|
||||
length = rad*2-1
|
||||
mid = length/2
|
||||
q = np.array(range(mid-1,-1,-1)+range(-1,mid))
|
||||
a = alpha*(rad*rad - q*q)/(rad*rad)
|
||||
a[mid] = 0
|
||||
self.a_s[(alpha, rad)] = a
|
||||
return a
|
||||
|
||||
def alterneigh(self, alpha, rad, i, b, g, r):
|
||||
if i-rad >= self.SPECIALS-1:
|
||||
lo = i-rad
|
||||
start = 0
|
||||
else:
|
||||
lo = self.SPECIALS-1
|
||||
start = (self.SPECIALS-1 - (i-rad))
|
||||
|
||||
if i+rad <= self.NETSIZE:
|
||||
hi = i+rad
|
||||
end = rad*2-1
|
||||
else:
|
||||
hi = self.NETSIZE
|
||||
end = (self.NETSIZE - (i+rad))
|
||||
|
||||
a = self.geta(alpha, rad)[start:end]
|
||||
|
||||
p = self.network[lo+1:hi]
|
||||
p -= np.transpose(np.transpose(p - np.array([b, g, r])) * a)
|
||||
|
||||
#def contest(self, b, g, r):
|
||||
# """ Search for biased BGR values
|
||||
# Finds closest neuron (min dist) and updates self.freq
|
||||
# finds best neuron (min dist-self.bias) and returns position
|
||||
# for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative
|
||||
# self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])"""
|
||||
#
|
||||
# i, j = self.SPECIALS, self.NETSIZE
|
||||
# dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1)
|
||||
# bestpos = i + np.argmin(dists)
|
||||
# biasdists = dists - self.bias[i:j]
|
||||
# bestbiaspos = i + np.argmin(biasdists)
|
||||
# self.freq[i:j] -= self.BETA * self.freq[i:j]
|
||||
# self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
|
||||
# self.freq[bestpos] += self.BETA
|
||||
# self.bias[bestpos] -= self.BETAGAMMA
|
||||
# return bestbiaspos
|
||||
def contest(self, b, g, r):
|
||||
""" Search for biased BGR values
|
||||
Finds closest neuron (min dist) and updates self.freq
|
||||
finds best neuron (min dist-self.bias) and returns position
|
||||
for frequently chosen neurons, self.freq[i] is high and self.bias[i] is negative
|
||||
self.bias[i] = self.GAMMA*((1/self.NETSIZE)-self.freq[i])"""
|
||||
i, j = self.SPECIALS, self.NETSIZE
|
||||
dists = abs(self.network[i:j] - np.array([b,g,r])).sum(1)
|
||||
bestpos = i + np.argmin(dists)
|
||||
biasdists = dists - self.bias[i:j]
|
||||
bestbiaspos = i + np.argmin(biasdists)
|
||||
self.freq[i:j] *= (1-self.BETA)
|
||||
self.bias[i:j] += self.BETAGAMMA * self.freq[i:j]
|
||||
self.freq[bestpos] += self.BETA
|
||||
self.bias[bestpos] -= self.BETAGAMMA
|
||||
return bestbiaspos
|
||||
|
||||
|
||||
|
||||
|
||||
def specialFind(self, b, g, r):
|
||||
for i in range(self.SPECIALS):
|
||||
n = self.network[i]
|
||||
if n[0] == b and n[1] == g and n[2] == r:
|
||||
return i
|
||||
return -1
|
||||
|
||||
def learn(self):
|
||||
biasRadius = self.INITBIASRADIUS
|
||||
alphadec = 30 + ((self.samplefac-1)/3)
|
||||
lengthcount = self.pixels.size
|
||||
samplepixels = lengthcount / self.samplefac
|
||||
delta = samplepixels / self.NCYCLES
|
||||
alpha = self.INITALPHA
|
||||
|
||||
i = 0;
|
||||
rad = biasRadius >> self.RADIUSBIASSHIFT
|
||||
if rad <= 1:
|
||||
rad = 0
|
||||
|
||||
print "Beginning 1D learning: samplepixels =",samplepixels," rad =", rad
|
||||
|
||||
step = 0
|
||||
pos = 0
|
||||
if lengthcount%NeuQuant.PRIME1 != 0:
|
||||
step = NeuQuant.PRIME1
|
||||
elif lengthcount%NeuQuant.PRIME2 != 0:
|
||||
step = NeuQuant.PRIME2
|
||||
elif lengthcount%NeuQuant.PRIME3 != 0:
|
||||
step = NeuQuant.PRIME3
|
||||
else:
|
||||
step = NeuQuant.PRIME4
|
||||
|
||||
i = 0
|
||||
printed_string = ''
|
||||
while i < samplepixels:
|
||||
if i%100 == 99:
|
||||
tmp = '\b'*len(printed_string)
|
||||
printed_string = str((i+1)*100/samplepixels)+"%\n"
|
||||
print tmp + printed_string,
|
||||
p = self.pixels[pos]
|
||||
r = (p >> 16) & 0xff
|
||||
g = (p >> 8) & 0xff
|
||||
b = (p ) & 0xff
|
||||
|
||||
if i == 0: # Remember background colour
|
||||
self.network[self.BGCOLOR] = [b, g, r]
|
||||
|
||||
j = self.specialFind(b, g, r)
|
||||
if j < 0:
|
||||
j = self.contest(b, g, r)
|
||||
|
||||
if j >= self.SPECIALS: # Don't learn for specials
|
||||
a = (1.0 * alpha) / self.INITALPHA
|
||||
self.altersingle(a, j, b, g, r)
|
||||
if rad > 0:
|
||||
self.alterneigh(a, rad, j, b, g, r)
|
||||
|
||||
pos = (pos+step)%lengthcount
|
||||
|
||||
i += 1
|
||||
if i%delta == 0:
|
||||
alpha -= alpha / alphadec
|
||||
biasRadius -= biasRadius / self.RADIUSDEC
|
||||
rad = biasRadius >> self.RADIUSBIASSHIFT
|
||||
if rad <= 1:
|
||||
rad = 0
|
||||
print "Finished 1D learning: final alpha =",(1.0*alpha)/self.INITALPHA,"!"
|
||||
|
||||
def fix(self):
|
||||
for i in range(self.NETSIZE):
|
||||
for j in range(3):
|
||||
x = int(0.5 + self.network[i,j])
|
||||
x = max(0, x)
|
||||
x = min(255, x)
|
||||
self.colormap[i,j] = x
|
||||
self.colormap[i,3] = i
|
||||
|
||||
def inxbuild(self):
|
||||
previouscol = 0
|
||||
startpos = 0
|
||||
for i in range(self.NETSIZE):
|
||||
p = self.colormap[i]
|
||||
q = None
|
||||
smallpos = i
|
||||
smallval = p[1] # Index on g
|
||||
# Find smallest in i..self.NETSIZE-1
|
||||
for j in range(i+1, self.NETSIZE):
|
||||
q = self.colormap[j]
|
||||
if q[1] < smallval: # Index on g
|
||||
smallpos = j
|
||||
smallval = q[1] # Index on g
|
||||
|
||||
q = self.colormap[smallpos]
|
||||
# Swap p (i) and q (smallpos) entries
|
||||
if i != smallpos:
|
||||
p[:],q[:] = q, p.copy()
|
||||
|
||||
# smallval entry is now in position i
|
||||
if smallval != previouscol:
|
||||
self.netindex[previouscol] = (startpos+i) >> 1
|
||||
for j in range(previouscol+1, smallval):
|
||||
self.netindex[j] = i
|
||||
previouscol = smallval
|
||||
startpos = i
|
||||
self.netindex[previouscol] = (startpos+self.MAXNETPOS) >> 1
|
||||
for j in range(previouscol+1, 256): # Really 256
|
||||
self.netindex[j] = self.MAXNETPOS
|
||||
|
||||
|
||||
def paletteImage(self):
|
||||
""" PIL weird interface for making a paletted image: create an image which
|
||||
already has the palette, and use that in Image.quantize. This function
|
||||
returns this palette image. """
|
||||
if self.pimage is None:
|
||||
palette = []
|
||||
for i in range(self.NETSIZE):
|
||||
palette.extend(self.colormap[i][:3])
|
||||
|
||||
palette.extend([0]*(256-self.NETSIZE)*3)
|
||||
|
||||
# a palette image to use for quant
|
||||
self.pimage = Image.new("P", (1, 1), 0)
|
||||
self.pimage.putpalette(palette)
|
||||
return self.pimage
|
||||
|
||||
|
||||
def quantize(self, image):
|
||||
""" Use a kdtree to quickly find the closest palette colors for the pixels """
|
||||
if cKDTree:
|
||||
return self.quantize_with_scipy(image)
|
||||
else:
|
||||
print 'Scipy not available, falling back to slower version.'
|
||||
return self.quantize_without_scipy(image)
|
||||
|
||||
|
||||
def quantize_with_scipy(self, image):
|
||||
w,h = image.size
|
||||
px = np.asarray(image).copy()
|
||||
px2 = px[:,:,:3].reshape((w*h,3))
|
||||
|
||||
kdtree = cKDTree(self.colormap[:,:3],leafsize=10)
|
||||
result = kdtree.query(px2)
|
||||
colorindex = result[1]
|
||||
print "Distance:", (result[0].sum()/(w*h))
|
||||
px2[:] = self.colormap[colorindex,:3]
|
||||
|
||||
return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
|
||||
|
||||
|
||||
def quantize_without_scipy(self, image):
|
||||
"""" This function can be used if no scipy is availabe.
|
||||
It's 7 times slower though.
|
||||
"""
|
||||
w,h = image.size
|
||||
px = np.asarray(image).copy()
|
||||
memo = {}
|
||||
for j in range(w):
|
||||
for i in range(h):
|
||||
key = (px[i,j,0],px[i,j,1],px[i,j,2])
|
||||
try:
|
||||
val = memo[key]
|
||||
except KeyError:
|
||||
val = self.convert(key)
|
||||
memo[key] = val
|
||||
px[i,j,0],px[i,j,1],px[i,j,2] = val
|
||||
return Image.fromarray(px).convert("RGB").quantize(palette=self.paletteImage())
|
||||
|
||||
def convert(self, (r, g, b)):
|
||||
i = self.inxsearch(r, g, b)
|
||||
return self.colormap[i,:3]
|
||||
|
||||
def inxsearch(self, r, g, b):
|
||||
"""Search for BGR values 0..255 and return colour index"""
|
||||
dists = (self.colormap[:,:3] - np.array([r,g,b]))
|
||||
a= np.argmin((dists*dists).sum(1))
|
||||
return a
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
im = np.zeros((200,200), dtype=np.uint8)
|
||||
im[10:30,:] = 100
|
||||
im[:,80:120] = 255
|
||||
im[-50:-40,:] = 50
|
||||
|
||||
images = [im*1.0, im*0.8, im*0.6, im*0.4, im*0]
|
||||
writeGif('lala3.gif',images, duration=0.5, dither=0)
|
|
@ -8,10 +8,8 @@ from copy import deepcopy
|
|||
from colour import Color
|
||||
import inspect
|
||||
|
||||
|
||||
from constants import *
|
||||
from helpers import *
|
||||
import displayer as disp
|
||||
from helpers import *
|
||||
|
||||
|
||||
class Mobject(object):
|
|
@ -1,6 +0,0 @@
|
|||
from mobject import *
|
||||
from image_mobject import *
|
||||
from simple_mobjects import *
|
||||
from three_dimensional_mobjects import *
|
||||
from function_graphs import *
|
||||
from creatures import *
|
|
@ -11,129 +11,10 @@ from mobject import *
|
|||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene
|
||||
from script_wrapper import command_line_create_scene
|
||||
|
||||
MOVIE_PREFIX = "complex_actions/"
|
||||
from topics.complex_numbers import *
|
||||
|
||||
DEFAULT_PLANE_CONFIG = {
|
||||
"point_thickness" : 2*DEFAULT_POINT_THICKNESS
|
||||
}
|
||||
|
||||
def complex_string(complex_num):
|
||||
return filter(lambda c : c not in "()", str(complex_num))
|
||||
|
||||
class ComplexMultiplication(Scene):
|
||||
args_list = [
|
||||
complex(np.sqrt(3), 1),
|
||||
complex(1,-1)/3,
|
||||
complex(-2, 0),
|
||||
(complex(np.sqrt(3), 1), True),
|
||||
(complex(1,-1)/3, True),
|
||||
(complex(-2, 0), True),
|
||||
]
|
||||
@staticmethod
|
||||
def args_to_string(multiplier, mark_one = False):
|
||||
num_str = complex_string(multiplier)
|
||||
arrow_str = "MarkOne" if mark_one else ""
|
||||
return num_str + arrow_str
|
||||
|
||||
@staticmethod
|
||||
def string_to_args(arg_string):
|
||||
parts = arg_string.split()
|
||||
multiplier = complex(parts[0])
|
||||
mark_one = len(parts) > 1 and parts[1] == "MarkOne"
|
||||
return (multiplier, mark_one)
|
||||
|
||||
def construct(self, multiplier, mark_one = False, **plane_config):
|
||||
norm = np.linalg.norm(multiplier)
|
||||
arg = np.log(multiplier).imag
|
||||
plane_config["faded_line_frequency"] = 0
|
||||
plane_config.update(DEFAULT_PLANE_CONFIG)
|
||||
if norm > 1 and "density" not in plane_config:
|
||||
plane_config["density"] = norm*DEFAULT_POINT_DENSITY_1D
|
||||
if "radius" not in plane_config:
|
||||
radius = SPACE_WIDTH
|
||||
if norm > 0 and norm < 1:
|
||||
radius /= norm
|
||||
else:
|
||||
radius = plane_config["radius"]
|
||||
plane_config["x_radius"] = plane_config["y_radius"] = radius
|
||||
plane = ComplexPlane(**plane_config)
|
||||
self.plane = plane
|
||||
self.add(plane)
|
||||
# plane.add_spider_web()
|
||||
self.anim_config = {
|
||||
"run_time" : 2.0,
|
||||
"interpolation_function" : path_along_arc(arg)
|
||||
}
|
||||
|
||||
plane_config["faded_line_frequency"] = 0.5
|
||||
background = ComplexPlane(color = "grey", **plane_config)
|
||||
# background.add_spider_web()
|
||||
labels = background.get_coordinate_labels()
|
||||
self.paint_into_background(background, *labels)
|
||||
self.mobjects_to_move_without_molding = []
|
||||
if mark_one:
|
||||
self.draw_dot("1", 1, True)
|
||||
self.draw_dot("z", multiplier)
|
||||
|
||||
|
||||
self.mobjects_to_multiply = [plane]
|
||||
|
||||
self.additional_animations = []
|
||||
self.multiplier = multiplier
|
||||
if self.__class__ == ComplexMultiplication:
|
||||
self.apply_multiplication()
|
||||
|
||||
def draw_dot(self, tex_string, value, move_dot = False):
|
||||
dot = Dot(
|
||||
self.plane.number_to_point(value),
|
||||
radius = 0.1*self.plane.unit_to_spatial_width,
|
||||
color = BLUE if value == 1 else YELLOW
|
||||
)
|
||||
label = tex_mobject(tex_string)
|
||||
label.shift(dot.get_center()+1.5*UP+RIGHT)
|
||||
arrow = Arrow(label, dot)
|
||||
self.add(label)
|
||||
self.play(ShowCreation(arrow))
|
||||
self.play(ShowCreation(dot))
|
||||
self.dither()
|
||||
|
||||
self.remove(label, arrow)
|
||||
if move_dot:
|
||||
self.mobjects_to_move_without_molding.append(dot)
|
||||
return dot
|
||||
|
||||
|
||||
def apply_multiplication(self):
|
||||
def func((x, y, z)):
|
||||
complex_num = self.multiplier*complex(x, y)
|
||||
return (complex_num.real, complex_num.imag, z)
|
||||
mobjects = self.mobjects_to_multiply
|
||||
mobjects += self.mobjects_to_move_without_molding
|
||||
mobjects += [anim.mobject for anim in self.additional_animations]
|
||||
|
||||
|
||||
self.add(*mobjects)
|
||||
full_multiplications = [
|
||||
ApplyMethod(mobject.apply_function, func, **self.anim_config)
|
||||
for mobject in self.mobjects_to_multiply
|
||||
]
|
||||
movements_with_plane = [
|
||||
ApplyMethod(
|
||||
mobject.shift,
|
||||
func(mobject.get_center())-mobject.get_center(),
|
||||
**self.anim_config
|
||||
)
|
||||
for mobject in self.mobjects_to_move_without_molding
|
||||
]
|
||||
self.dither()
|
||||
self.play(*reduce(op.add, [
|
||||
full_multiplications,
|
||||
movements_with_plane,
|
||||
self.additional_animations
|
||||
]))
|
||||
self.dither()
|
||||
|
||||
|
||||
class SuccessiveComplexMultiplications(ComplexMultiplication):
|
||||
|
@ -280,13 +161,6 @@ class DrawSolutionsToZToTheNEqualsW(Scene):
|
|||
self.add(*plane.get_coordinate_labels())
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class DrawComplexAngleAndMagnitude(Scene):
|
||||
args_list = [
|
||||
(
|
||||
|
@ -378,15 +252,3 @@ class DrawComplexAngleAndMagnitude(Scene):
|
|||
|
||||
self.add_local_mobjects()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene(MOVIE_PREFIX)
|
|
@ -11,7 +11,6 @@ from mobject import *
|
|||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene
|
||||
from script_wrapper import command_line_create_scene
|
||||
|
||||
class LogoGeneration(Scene):
|
||||
LOGO_RADIUS = 1.5
|
||||
|
@ -61,6 +60,3 @@ class LogoGeneration(Scene):
|
|||
print "Dragging pixels..."
|
||||
self.frames = drag_pixels(self.frames)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene()
|
|
@ -1697,10 +1697,5 @@ class IntersectionChoppingExamples(Scene):
|
|||
self.remove(*self.mobjects)
|
||||
|
||||
|
||||
##################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene(MOVIE_PREFIX)
|
||||
|
||||
|
||||
|
|
@ -4,9 +4,9 @@ from PIL import Image
|
|||
import cv2
|
||||
from copy import deepcopy
|
||||
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
import displayer as disp
|
||||
from helpers import *
|
||||
|
||||
class Region(object):
|
||||
def __init__(self,
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from copy import deepcopy
|
||||
import sys
|
||||
|
||||
|
||||
from animation import *
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene
|
||||
from script_wrapper import command_line_create_scene
|
||||
|
||||
|
||||
class SampleScene(Scene):
|
||||
def construct(self):
|
||||
words = text_mobject("Hi There")
|
||||
self.paint_into_background(words.shift(UP).highlight(RED_A))
|
||||
self.paint_into_background(words.shift(DOWN).highlight(RED_E))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene()
|
|
@ -1,7 +1,3 @@
|
|||
from scene import *
|
||||
from sub_scenes import *
|
||||
from arithmetic_scenes import *
|
||||
from counting_scene import *
|
||||
from pascals_triangle import *
|
||||
from scene_from_video import *
|
||||
from number_line import *
|
||||
from tk_scene import *
|
|
@ -1,85 +0,0 @@
|
|||
from scene import Scene
|
||||
|
||||
from mobject import *
|
||||
from animation import *
|
||||
from region import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
DEFAULT_COUNT_NUM_OFFSET = (SPACE_WIDTH - 1, SPACE_HEIGHT - 1, 0)
|
||||
DEFAULT_COUNT_RUN_TIME = 5.0
|
||||
|
||||
class CountingScene(Scene):
|
||||
def count(self, items, item_type = "mobject", *args, **kwargs):
|
||||
if item_type == "mobject":
|
||||
self.count_mobjects(items, *args, **kwargs)
|
||||
elif item_type == "region":
|
||||
self.count_regions(items, *args, **kwargs)
|
||||
else:
|
||||
raise Exception("Unknown item_type, should be mobject or region")
|
||||
return self
|
||||
|
||||
def count_mobjects(
|
||||
self, mobjects, mode = "highlight",
|
||||
color = "red",
|
||||
display_numbers = True,
|
||||
num_offset = DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time = DEFAULT_COUNT_RUN_TIME):
|
||||
"""
|
||||
Note, leaves final number mobject as "number" attribute
|
||||
|
||||
mode can be "highlight", "show_creation" or "show", otherwise
|
||||
a warning is given and nothing is animating during the count
|
||||
"""
|
||||
if len(mobjects) > 50: #TODO
|
||||
raise Exception("I don't know if you should be counting \
|
||||
too many mobjects...")
|
||||
if len(mobjects) == 0:
|
||||
raise Exception("Counting mobject list of length 0")
|
||||
if mode not in ["highlight", "show_creation", "show"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / len(mobjects)
|
||||
if mode == "highlight":
|
||||
self.add(*mobjects)
|
||||
for mob, num in zip(mobjects, it.count(1)):
|
||||
if display_numbers:
|
||||
num_mob = tex_mobject(str(num))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
if mode == "highlight":
|
||||
original_color = mob.color
|
||||
mob.highlight(color)
|
||||
self.dither(frame_time)
|
||||
mob.highlight(original_color)
|
||||
if mode == "show_creation":
|
||||
self.play(ShowCreation(mob, run_time = frame_time))
|
||||
if mode == "show":
|
||||
self.add(mob)
|
||||
self.dither(frame_time)
|
||||
if display_numbers:
|
||||
self.remove(num_mob)
|
||||
if display_numbers:
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
||||
|
||||
def count_regions(self, regions,
|
||||
mode = "one_at_a_time",
|
||||
num_offset = DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time = DEFAULT_COUNT_RUN_TIME,
|
||||
**unused_kwargsn):
|
||||
if mode not in ["one_at_a_time", "show_all"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / (len(regions))
|
||||
for region, count in zip(regions, it.count(1)):
|
||||
num_mob = tex_mobject(str(count))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
self.highlight_region(region)
|
||||
self.dither(frame_time)
|
||||
if mode == "one_at_a_time":
|
||||
self.reset_background()
|
||||
self.remove(num_mob)
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
|
@ -1,92 +0,0 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from scene import Scene
|
||||
|
||||
from mobject import *
|
||||
from animation import *
|
||||
from region import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
|
||||
class NumberLineScene(Scene):
|
||||
def construct(self, **number_line_config):
|
||||
self.number_line = NumberLine(**number_line_config)
|
||||
self.displayed_numbers = self.number_line.default_numbers_to_display()
|
||||
self.number_mobs = self.number_line.get_number_mobjects(*self.displayed_numbers)
|
||||
self.add(self.number_line, *self.number_mobs)
|
||||
|
||||
def zoom_in_on(self, number, zoom_factor, run_time = 2.0):
|
||||
unit_length_to_spatial_width = self.number_line.unit_length_to_spatial_width*zoom_factor
|
||||
radius = SPACE_WIDTH/unit_length_to_spatial_width
|
||||
tick_frequency = 10**(np.floor(np.log10(radius)))
|
||||
left_tick = tick_frequency*(np.ceil((number-radius)/tick_frequency))
|
||||
new_number_line = NumberLine(
|
||||
numerical_radius = radius,
|
||||
unit_length_to_spatial_width = unit_length_to_spatial_width,
|
||||
tick_frequency = tick_frequency,
|
||||
leftmost_tick = left_tick,
|
||||
number_at_center = number
|
||||
)
|
||||
new_displayed_numbers = new_number_line.default_numbers_to_display()
|
||||
new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers)
|
||||
|
||||
transforms = []
|
||||
additional_mobjects = []
|
||||
squished_new_line = deepcopy(new_number_line)
|
||||
squished_new_line.scale(1.0/zoom_factor)
|
||||
squished_new_line.shift(self.number_line.number_to_point(number))
|
||||
squished_new_line.points[:,1] = self.number_line.number_to_point(0)[1]
|
||||
transforms.append(Transform(squished_new_line, new_number_line))
|
||||
for mob, num in zip(new_number_mobs, new_displayed_numbers):
|
||||
point = Point(self.number_line.number_to_point(num))
|
||||
point.shift(new_number_line.get_vertical_number_offset())
|
||||
transforms.append(Transform(point, mob))
|
||||
for mob in self.mobjects:
|
||||
if mob == self.number_line:
|
||||
new_mob = deepcopy(mob)
|
||||
new_mob.shift(-self.number_line.number_to_point(number))
|
||||
new_mob.stretch(zoom_factor, 0)
|
||||
transforms.append(Transform(mob, new_mob))
|
||||
continue
|
||||
mob_center = mob.get_center()
|
||||
number_under_center = self.number_line.point_to_number(mob_center)
|
||||
new_point = new_number_line.number_to_point(number_under_center)
|
||||
new_point += mob_center[1]*UP
|
||||
if mob in self.number_mobs:
|
||||
transforms.append(Transform(mob, Point(new_point)))
|
||||
else:
|
||||
transforms.append(ApplyMethod(mob.shift, new_point - mob_center))
|
||||
additional_mobjects.append(mob)
|
||||
line_to_hide_pixelation = Line(
|
||||
self.number_line.get_left(),
|
||||
self.number_line.get_right(),
|
||||
color = self.number_line.get_color()
|
||||
)
|
||||
self.add(line_to_hide_pixelation)
|
||||
self.play(*transforms, run_time = run_time)
|
||||
self.clear()
|
||||
self.number_line = new_number_line
|
||||
self.displayed_numbers = new_displayed_numbers
|
||||
self.number_mobs = new_number_mobs
|
||||
self.add(self.number_line, *self.number_mobs)
|
||||
self.add(*additional_mobjects)
|
||||
|
||||
def show_multiplication(self, num, **kwargs):
|
||||
if "interpolation_function" not in kwargs:
|
||||
if num > 0:
|
||||
kwargs["interpolation_function"] = straight_path
|
||||
else:
|
||||
kwargs["interpolation_function"] = counterclockwise_path()
|
||||
self.play(*[
|
||||
ApplyMethod(self.number_line.stretch, num, 0, **kwargs)
|
||||
]+[
|
||||
ApplyMethod(mob.shift, (num-1)*mob.get_center()[0]*RIGHT, **kwargs)
|
||||
for mob in self.number_mobs
|
||||
])
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -10,8 +10,7 @@ import progressbar
|
|||
import inspect
|
||||
|
||||
from helpers import *
|
||||
from mobject import *
|
||||
from animation import *
|
||||
|
||||
import displayer as disp
|
||||
from tk_scene import TkSceneRoot
|
||||
|
||||
|
@ -199,11 +198,6 @@ class Scene(object):
|
|||
self.frames = self.frames*num
|
||||
return self
|
||||
|
||||
def write_to_gif(self, name = None,
|
||||
end_dither_time = DEFAULT_DITHER_TIME):
|
||||
self.dither(end_dither_time)
|
||||
disp.write_to_gif(self, name or str(self))
|
||||
|
||||
def write_to_movie(self, name = None):
|
||||
if len(self.frames) == 0:
|
||||
print "No frames, I'll just save an image instead"
|
||||
|
|
|
@ -2,7 +2,7 @@ import numpy as np
|
|||
import cv2
|
||||
import itertools as it
|
||||
|
||||
from scene import *
|
||||
from scene import Scene
|
||||
|
||||
|
||||
class SceneFromVideo(Scene):
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from scene import *
|
||||
import Tkinter
|
||||
from PIL import ImageTk, Image
|
||||
import itertools as it
|
||||
import time
|
||||
|
||||
|
||||
class TkSceneRoot(Tkinter.Tk):
|
||||
def __init__(self, scene):
|
||||
if scene.frames == []:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,8 @@
|
|||
import os
|
||||
import itertools as it
|
||||
from PIL import Image
|
||||
from constants import *
|
||||
|
||||
from helpers import *
|
||||
|
||||
#TODO, Cleanup and refactor this file.
|
||||
|
||||
|
|
11
topics/__init__.py
Normal file
11
topics/__init__.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from arithmetic import *
|
||||
from characters import *
|
||||
from combinatorics import *
|
||||
from complex_numbers import *
|
||||
from functions import *
|
||||
from geometry import *
|
||||
from graph_theory import *
|
||||
from matrix_as_transform_2d import *
|
||||
from number_line import *
|
||||
from pythagorean_proof import *
|
||||
from three_dimensions import *
|
|
@ -2,13 +2,7 @@ import numpy as np
|
|||
import itertools as it
|
||||
|
||||
from scene import Scene
|
||||
from graphs import *
|
||||
|
||||
from mobject import *
|
||||
from animation import *
|
||||
from region import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
from animation import Animation
|
||||
|
||||
class RearrangeEquation(Scene):
|
||||
def construct(
|
||||
|
@ -87,7 +81,27 @@ class RearrangeEquation(Scene):
|
|||
return all_mobs[:num_start_terms], all_mobs[num_start_terms:]
|
||||
|
||||
|
||||
|
||||
class FlipThroughSymbols(Animation):
|
||||
DEFAULT_CONFIG = {
|
||||
"start_center" : ORIGIN,
|
||||
"end_center" : ORIGIN,
|
||||
}
|
||||
def __init__(self, tex_list, **kwargs):
|
||||
digest_config(self, FlipThroughSymbols, kwargs, locals())
|
||||
self.curr_tex = self.tex_list[0]
|
||||
mobject = tex_mobject(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]
|
||||
|
||||
if new_tex != self.curr_tex:
|
||||
self.curr_tex = new_tex
|
||||
self.mobject = tex_mobject(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,11 +1,7 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
import os
|
||||
|
||||
from image_mobject import *
|
||||
from mobject import *
|
||||
from simple_mobjects import *
|
||||
from helpers import *
|
||||
|
||||
from mobject import Mobject, CompoundMobject
|
||||
from image_mobject import ImageMobject
|
||||
|
||||
class PiCreature(CompoundMobject):
|
||||
DEFAULT_COLOR = BLUE
|
||||
|
@ -169,11 +165,112 @@ class Mortimer(PiCreature):
|
|||
self.rotate(np.pi, UP)
|
||||
|
||||
|
||||
class Bubble(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"direction" : LEFT,
|
||||
"index_of_tip" : -1,
|
||||
"center_point" : ORIGIN,
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, Bubble, kwargs)
|
||||
Mobject.__init__(self, **kwargs)
|
||||
self.center_offset = self.center_point - Mobject.get_center(self)
|
||||
if self.direction[0] > 0:
|
||||
self.rotate(np.pi, UP)
|
||||
self.content = Mobject()
|
||||
|
||||
def get_tip(self):
|
||||
return self.points[self.index_of_tip]
|
||||
|
||||
def get_bubble_center(self):
|
||||
return self.get_center()+self.center_offset
|
||||
|
||||
def move_tip_to(self, point):
|
||||
self.shift(point - self.get_tip())
|
||||
return self
|
||||
|
||||
def flip(self):
|
||||
self.direction = -np.array(self.direction)
|
||||
self.rotate(np.pi, UP)
|
||||
return self
|
||||
|
||||
def pin_to(self, mobject):
|
||||
mob_center = mobject.get_center()
|
||||
if (mob_center[0] > 0) != (self.direction[0] > 0):
|
||||
self.flip()
|
||||
boundary_point = mobject.get_boundary_point(UP-self.direction)
|
||||
vector_from_center = 1.5*(boundary_point-mob_center)
|
||||
self.move_tip_to(mob_center+vector_from_center)
|
||||
return self
|
||||
|
||||
def add_content(self, mobject):
|
||||
scaled_width = 0.75*self.get_width()
|
||||
if mobject.get_width() > scaled_width:
|
||||
mobject.scale(scaled_width / mobject.get_width())
|
||||
mobject.shift(self.get_bubble_center())
|
||||
self.content = mobject
|
||||
return self
|
||||
|
||||
def write(self, text):
|
||||
self.add_content(text_mobject(text))
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
self.content = Mobject()
|
||||
return self
|
||||
|
||||
class SpeechBubble(Bubble):
|
||||
DEFAULT_CONFIG = {
|
||||
"initial_width" : 4,
|
||||
"initial_height" : 2,
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, SpeechBubble, kwargs)
|
||||
Bubble.__init__(self, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
complex_power = 0.9
|
||||
radius = self.initial_width/2
|
||||
circle = Circle(radius = radius)
|
||||
circle.scale(1.0/radius)
|
||||
circle.apply_complex_function(lambda z : z**complex_power)
|
||||
circle.scale(radius)
|
||||
boundary_point_as_complex = radius*complex(-1)**complex_power
|
||||
boundary_points = [
|
||||
[
|
||||
boundary_point_as_complex.real,
|
||||
unit*boundary_point_as_complex.imag,
|
||||
0
|
||||
]
|
||||
for unit in -1, 1
|
||||
]
|
||||
tip = radius*(1.5*LEFT+UP)
|
||||
self.add(
|
||||
circle,
|
||||
Line(boundary_points[0], tip),
|
||||
Line(boundary_points[1], tip)
|
||||
)
|
||||
self.highlight("white")
|
||||
self.rotate(np.pi/2)
|
||||
self.points[:,1] *= float(self.initial_height)/self.initial_width
|
||||
|
||||
class ThoughtBubble(Bubble):
|
||||
DEFAULT_CONFIG = {
|
||||
"num_bulges" : 7,
|
||||
"initial_inner_radius" : 1.8,
|
||||
"initial_width" : 6
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, ThoughtBubble, kwargs)
|
||||
Bubble.__init__(self, **kwargs)
|
||||
self.index_of_tip = np.argmin(self.points[:,1])
|
||||
|
||||
def generate_points(self):
|
||||
self.add(Circle().scale(0.15).shift(2.5*DOWN+2*LEFT))
|
||||
self.add(Circle().scale(0.3).shift(2*DOWN+1.5*LEFT))
|
||||
for n in range(self.num_bulges):
|
||||
theta = 2*np.pi*n/self.num_bulges
|
||||
self.add(Circle().shift((np.cos(theta), np.sin(theta), 0)))
|
||||
self.filter_out(lambda p : np.linalg.norm(p) < self.initial_inner_radius)
|
||||
self.stretch_to_fit_width(self.initial_width)
|
||||
self.highlight("white")
|
|
@ -1,17 +1,90 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
from helpers import *
|
||||
|
||||
from scene import Scene
|
||||
|
||||
from mobject import *
|
||||
from animation import *
|
||||
from region import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
DEFAULT_COUNT_NUM_OFFSET = (SPACE_WIDTH - 1, SPACE_HEIGHT - 1, 0)
|
||||
DEFAULT_COUNT_RUN_TIME = 5.0
|
||||
|
||||
class CountingScene(Scene):
|
||||
def count(self, items, item_type = "mobject", *args, **kwargs):
|
||||
if item_type == "mobject":
|
||||
self.count_mobjects(items, *args, **kwargs)
|
||||
elif item_type == "region":
|
||||
self.count_regions(items, *args, **kwargs)
|
||||
else:
|
||||
raise Exception("Unknown item_type, should be mobject or region")
|
||||
return self
|
||||
|
||||
def count_mobjects(
|
||||
self, mobjects, mode = "highlight",
|
||||
color = "red",
|
||||
display_numbers = True,
|
||||
num_offset = DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time = DEFAULT_COUNT_RUN_TIME):
|
||||
"""
|
||||
Note, leaves final number mobject as "number" attribute
|
||||
|
||||
mode can be "highlight", "show_creation" or "show", otherwise
|
||||
a warning is given and nothing is animating during the count
|
||||
"""
|
||||
if len(mobjects) > 50: #TODO
|
||||
raise Exception("I don't know if you should be counting \
|
||||
too many mobjects...")
|
||||
if len(mobjects) == 0:
|
||||
raise Exception("Counting mobject list of length 0")
|
||||
if mode not in ["highlight", "show_creation", "show"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / len(mobjects)
|
||||
if mode == "highlight":
|
||||
self.add(*mobjects)
|
||||
for mob, num in zip(mobjects, it.count(1)):
|
||||
if display_numbers:
|
||||
num_mob = tex_mobject(str(num))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
if mode == "highlight":
|
||||
original_color = mob.color
|
||||
mob.highlight(color)
|
||||
self.dither(frame_time)
|
||||
mob.highlight(original_color)
|
||||
if mode == "show_creation":
|
||||
self.play(ShowCreation(mob, run_time = frame_time))
|
||||
if mode == "show":
|
||||
self.add(mob)
|
||||
self.dither(frame_time)
|
||||
if display_numbers:
|
||||
self.remove(num_mob)
|
||||
if display_numbers:
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
||||
|
||||
def count_regions(self, regions,
|
||||
mode = "one_at_a_time",
|
||||
num_offset = DEFAULT_COUNT_NUM_OFFSET,
|
||||
run_time = DEFAULT_COUNT_RUN_TIME,
|
||||
**unused_kwargsn):
|
||||
if mode not in ["one_at_a_time", "show_all"]:
|
||||
raise Warning("Unknown mode")
|
||||
frame_time = run_time / (len(regions))
|
||||
for region, count in zip(regions, it.count(1)):
|
||||
num_mob = tex_mobject(str(count))
|
||||
num_mob.center().shift(num_offset)
|
||||
self.add(num_mob)
|
||||
self.highlight_region(region)
|
||||
self.dither(frame_time)
|
||||
if mode == "one_at_a_time":
|
||||
self.reset_background()
|
||||
self.remove(num_mob)
|
||||
self.add(num_mob)
|
||||
self.number = num_mob
|
||||
return self
|
||||
|
||||
|
||||
BIG_N_PASCAL_ROWS = 11
|
||||
N_PASCAL_ROWS = 7
|
||||
|
||||
class PascalsTriangleScene(Scene):
|
||||
args_list = [
|
||||
(N_PASCAL_ROWS,),
|
||||
|
@ -86,3 +159,13 @@ class PascalsTriangleScene(Scene):
|
|||
self.coords_to_mobs[n][k] = mob
|
||||
self.add(mob)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
212
topics/complex_numbers.py
Normal file
212
topics/complex_numbers.py
Normal file
|
@ -0,0 +1,212 @@
|
|||
from helpers import *
|
||||
|
||||
from number_line import NumberPlane
|
||||
from animation.transform import ApplyPointwiseFunction
|
||||
from animation.animation import Homotopy
|
||||
from scene import Scene
|
||||
|
||||
|
||||
def complex_string(complex_num):
|
||||
return filter(lambda c : c not in "()", str(complex_num))
|
||||
|
||||
class ComplexPlane(NumberPlane):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : GREEN,
|
||||
"unit_to_spatial_width" : 1,
|
||||
"line_frequency" : 1,
|
||||
"faded_line_frequency" : 0.5,
|
||||
"number_at_center" : complex(0),
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, ComplexPlane, kwargs)
|
||||
kwargs.update({
|
||||
"x_unit_to_spatial_width" : self.unit_to_spatial_width,
|
||||
"y_uint_to_spatial_height" : self.unit_to_spatial_width,
|
||||
"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,
|
||||
"num_pair_at_center" : (self.number_at_center.real, self.number_at_center.imag),
|
||||
})
|
||||
NumberPlane.__init__(self, **kwargs)
|
||||
|
||||
def number_to_point(self, number):
|
||||
number = complex(number)
|
||||
return self.num_pair_to_point((number.real, number.imag))
|
||||
|
||||
def get_coordinate_labels(self, *numbers):
|
||||
result = []
|
||||
nudge = 0.1*(DOWN+RIGHT)
|
||||
if len(numbers) == 0:
|
||||
numbers = range(-int(self.x_radius), int(self.x_radius))
|
||||
numbers += [
|
||||
complex(0, y)
|
||||
for y in range(-int(self.y_radius), int(self.y_radius))
|
||||
]
|
||||
for number in numbers:
|
||||
point = self.number_to_point(number)
|
||||
if number == 0:
|
||||
num_str = "0"
|
||||
else:
|
||||
num_str = str(number).replace("j", "i")
|
||||
num = tex_mobject(num_str)
|
||||
num.scale(self.number_scale_factor)
|
||||
num.shift(point-num.get_corner(UP+LEFT)+nudge)
|
||||
result.append(num)
|
||||
return result
|
||||
|
||||
def add_coordinates(self, *numbers):
|
||||
self.add(*self.get_coordinate_labels(*numbers))
|
||||
return self
|
||||
|
||||
def add_spider_web(self, circle_freq = 1, angle_freq = np.pi/6):
|
||||
self.fade(self.fade_factor)
|
||||
config = {
|
||||
"color" : self.color,
|
||||
"density" : self.density,
|
||||
}
|
||||
for radius in np.arange(circle_freq, SPACE_WIDTH, circle_freq):
|
||||
self.add(Circle(radius = radius, **config))
|
||||
for angle in np.arange(0, 2*np.pi, angle_freq):
|
||||
end_point = np.cos(angle)*RIGHT + np.sin(angle)*UP
|
||||
end_point *= SPACE_WIDTH
|
||||
self.add(Line(ORIGIN, end_point, **config))
|
||||
return self
|
||||
|
||||
|
||||
class ComplexFunction(ApplyPointwiseFunction):
|
||||
def __init__(self, function, mobject = ComplexPlane, **kwargs):
|
||||
if "interpolation_function" not in kwargs:
|
||||
self.interpolation_function = path_along_arc(
|
||||
np.log(function(complex(1))).imag
|
||||
)
|
||||
ApplyPointwiseFunction.__init__(
|
||||
self,
|
||||
lambda (x, y, z) : complex_to_R3(function(complex(x, y))),
|
||||
instantiate(mobject),
|
||||
**kwargs
|
||||
)
|
||||
|
||||
class ComplexHomotopy(Homotopy):
|
||||
def __init__(self, complex_homotopy, **kwargs):
|
||||
"""
|
||||
Complex Hootopy a function (z, t) to z'
|
||||
"""
|
||||
def homotopy((x, y, z, t)):
|
||||
c = complex_homotopy((complex(x, y), t))
|
||||
return (c.real, c.imag, z)
|
||||
if len(args) > 0:
|
||||
args = list(args)
|
||||
mobject = args.pop(0)
|
||||
elif "mobject" in kwargs:
|
||||
mobject = kwargs["mobject"]
|
||||
else:
|
||||
mobject = Grid()
|
||||
Homotopy.__init__(self, homotopy, mobject, *args, **kwargs)
|
||||
self.name = "ComplexHomotopy" + \
|
||||
to_cammel_case(complex_homotopy.__name__)
|
||||
|
||||
class ComplexMultiplication(Scene):
|
||||
@staticmethod
|
||||
def args_to_string(multiplier, mark_one = False):
|
||||
num_str = complex_string(multiplier)
|
||||
arrow_str = "MarkOne" if mark_one else ""
|
||||
return num_str + arrow_str
|
||||
|
||||
@staticmethod
|
||||
def string_to_args(arg_string):
|
||||
parts = arg_string.split()
|
||||
multiplier = complex(parts[0])
|
||||
mark_one = len(parts) > 1 and parts[1] == "MarkOne"
|
||||
return (multiplier, mark_one)
|
||||
|
||||
def construct(self, multiplier, mark_one = False, **plane_config):
|
||||
norm = np.linalg.norm(multiplier)
|
||||
arg = np.log(multiplier).imag
|
||||
plane_config["faded_line_frequency"] = 0
|
||||
plane_config.update(DEFAULT_PLANE_CONFIG)
|
||||
if norm > 1 and "density" not in plane_config:
|
||||
plane_config["density"] = norm*DEFAULT_POINT_DENSITY_1D
|
||||
if "radius" not in plane_config:
|
||||
radius = SPACE_WIDTH
|
||||
if norm > 0 and norm < 1:
|
||||
radius /= norm
|
||||
else:
|
||||
radius = plane_config["radius"]
|
||||
plane_config["x_radius"] = plane_config["y_radius"] = radius
|
||||
plane = ComplexPlane(**plane_config)
|
||||
self.plane = plane
|
||||
self.add(plane)
|
||||
# plane.add_spider_web()
|
||||
self.anim_config = {
|
||||
"run_time" : 2.0,
|
||||
"interpolation_function" : path_along_arc(arg)
|
||||
}
|
||||
|
||||
plane_config["faded_line_frequency"] = 0.5
|
||||
background = ComplexPlane(color = "grey", **plane_config)
|
||||
# background.add_spider_web()
|
||||
labels = background.get_coordinate_labels()
|
||||
self.paint_into_background(background, *labels)
|
||||
self.mobjects_to_move_without_molding = []
|
||||
if mark_one:
|
||||
self.draw_dot("1", 1, True)
|
||||
self.draw_dot("z", multiplier)
|
||||
|
||||
|
||||
self.mobjects_to_multiply = [plane]
|
||||
|
||||
self.additional_animations = []
|
||||
self.multiplier = multiplier
|
||||
if self.__class__ == ComplexMultiplication:
|
||||
self.apply_multiplication()
|
||||
|
||||
def draw_dot(self, tex_string, value, move_dot = False):
|
||||
dot = Dot(
|
||||
self.plane.number_to_point(value),
|
||||
radius = 0.1*self.plane.unit_to_spatial_width,
|
||||
color = BLUE if value == 1 else YELLOW
|
||||
)
|
||||
label = tex_mobject(tex_string)
|
||||
label.shift(dot.get_center()+1.5*UP+RIGHT)
|
||||
arrow = Arrow(label, dot)
|
||||
self.add(label)
|
||||
self.play(ShowCreation(arrow))
|
||||
self.play(ShowCreation(dot))
|
||||
self.dither()
|
||||
|
||||
self.remove(label, arrow)
|
||||
if move_dot:
|
||||
self.mobjects_to_move_without_molding.append(dot)
|
||||
return dot
|
||||
|
||||
|
||||
def apply_multiplication(self):
|
||||
def func((x, y, z)):
|
||||
complex_num = self.multiplier*complex(x, y)
|
||||
return (complex_num.real, complex_num.imag, z)
|
||||
mobjects = self.mobjects_to_multiply
|
||||
mobjects += self.mobjects_to_move_without_molding
|
||||
mobjects += [anim.mobject for anim in self.additional_animations]
|
||||
|
||||
|
||||
self.add(*mobjects)
|
||||
full_multiplications = [
|
||||
ApplyMethod(mobject.apply_function, func, **self.anim_config)
|
||||
for mobject in self.mobjects_to_multiply
|
||||
]
|
||||
movements_with_plane = [
|
||||
ApplyMethod(
|
||||
mobject.shift,
|
||||
func(mobject.get_center())-mobject.get_center(),
|
||||
**self.anim_config
|
||||
)
|
||||
for mobject in self.mobjects_to_move_without_molding
|
||||
]
|
||||
self.dither()
|
||||
self.play(*reduce(op.add, [
|
||||
full_multiplications,
|
||||
movements_with_plane,
|
||||
self.additional_animations
|
||||
]))
|
||||
self.dither()
|
65
topics/functions.py
Normal file
65
topics/functions.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from helpers import *
|
||||
|
||||
from helpers import *
|
||||
|
||||
from mobject import Mobject, Mobject1D, CompoundMobject
|
||||
|
||||
|
||||
class FunctionGraph(Mobject1D):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : BLUE,
|
||||
"x_min" : -10,
|
||||
"x_max" : 10,
|
||||
"spatial_radius" : SPACE_WIDTH,
|
||||
}
|
||||
def __init__(self, function, **kwargs):
|
||||
digest_config(self, FunctionGraph, kwargs, locals())
|
||||
Mobject1D.__init__(self, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
numerical_radius = (self.x_max - self.x_min)/2
|
||||
numerical_center = (self.x_max + self.x_min)/2
|
||||
ratio = numerical_radius / self.spatial_radius
|
||||
epsilon = self.epsilon * ratio
|
||||
self.add_points([
|
||||
np.array([(x-numerical_center)/ratio, self.function(x), 0])
|
||||
for x in np.arange(self.x_min, self.x_max, self.epsilon)
|
||||
])
|
||||
|
||||
|
||||
class ParametricFunction(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : WHITE,
|
||||
"dim" : 1,
|
||||
"expected_measure" : 10.0,
|
||||
"density" : None
|
||||
}
|
||||
def __init__(self, function, **kwargs):
|
||||
digest_config(self, ParametricFunction, kwargs, locals())
|
||||
if self.density:
|
||||
self.epsilon = 1.0 / self.density
|
||||
elif self.dim == 1:
|
||||
self.epsilon = 1.0 / self.expected_measure / DEFAULT_POINT_DENSITY_1D
|
||||
else:
|
||||
self.epsilon = 1.0 / np.sqrt(self.expected_measure) / DEFAULT_POINT_DENSITY_2D
|
||||
Mobject.__init__(self, *args, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
if self.dim == 1:
|
||||
self.add_points([
|
||||
self.function(t)
|
||||
for t in np.arange(-1, 1, self.epsilon)
|
||||
])
|
||||
if self.dim == 2:
|
||||
self.add_points([
|
||||
self.function(s, t)
|
||||
for t in np.arange(-1, 1, self.epsilon)
|
||||
for s in np.arange(-1, 1, self.epsilon)
|
||||
])
|
||||
|
||||
|
||||
class Axes(CompoundMobject):
|
||||
def __init__(self, **kwargs):
|
||||
x_axis = NumberLine(**kwargs)
|
||||
y_axis = NumberLine(**kwargs).rotate(np.pi/2, OUT)
|
||||
CompoundMobject.__init__(self, x_axis, y_axis)
|
|
@ -1,11 +1,7 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject
|
||||
from image_mobject import text_mobject
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
from mobject import Mobject, Mobject1D
|
||||
|
||||
|
||||
class Point(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
|
@ -52,6 +48,7 @@ class Cross(Mobject1D):
|
|||
])
|
||||
self.shift(self.center_point)
|
||||
|
||||
|
||||
class Line(Mobject1D):
|
||||
DEFAULT_CONFIG = {
|
||||
"min_density" : 0.1
|
||||
|
@ -230,129 +227,4 @@ class Square(Rectangle):
|
|||
digest_config(self, Square, kwargs)
|
||||
for arg in ["height", "width"]:
|
||||
kwargs[arg] = self.side_length
|
||||
Rectangle.__init__(self, **kwargs)
|
||||
|
||||
class Bubble(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"direction" : LEFT,
|
||||
"index_of_tip" : -1,
|
||||
"center_point" : ORIGIN,
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, Bubble, kwargs)
|
||||
Mobject.__init__(self, **kwargs)
|
||||
self.center_offset = self.center_point - Mobject.get_center(self)
|
||||
if self.direction[0] > 0:
|
||||
self.rotate(np.pi, UP)
|
||||
self.content = Mobject()
|
||||
|
||||
def get_tip(self):
|
||||
return self.points[self.index_of_tip]
|
||||
|
||||
def get_bubble_center(self):
|
||||
return self.get_center()+self.center_offset
|
||||
|
||||
def move_tip_to(self, point):
|
||||
self.shift(point - self.get_tip())
|
||||
return self
|
||||
|
||||
def flip(self):
|
||||
self.direction = -np.array(self.direction)
|
||||
self.rotate(np.pi, UP)
|
||||
return self
|
||||
|
||||
def pin_to(self, mobject):
|
||||
mob_center = mobject.get_center()
|
||||
if (mob_center[0] > 0) != (self.direction[0] > 0):
|
||||
self.flip()
|
||||
boundary_point = mobject.get_boundary_point(UP-self.direction)
|
||||
vector_from_center = 1.5*(boundary_point-mob_center)
|
||||
self.move_tip_to(mob_center+vector_from_center)
|
||||
return self
|
||||
|
||||
def add_content(self, mobject):
|
||||
scaled_width = 0.75*self.get_width()
|
||||
if mobject.get_width() > scaled_width:
|
||||
mobject.scale(scaled_width / mobject.get_width())
|
||||
mobject.shift(self.get_bubble_center())
|
||||
self.content = mobject
|
||||
return self
|
||||
|
||||
def write(self, text):
|
||||
self.add_content(text_mobject(text))
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
self.content = Mobject()
|
||||
return self
|
||||
|
||||
class SpeechBubble(Bubble):
|
||||
DEFAULT_CONFIG = {
|
||||
"initial_width" : 4,
|
||||
"initial_height" : 2,
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, SpeechBubble, kwargs)
|
||||
Bubble.__init__(self, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
complex_power = 0.9
|
||||
radius = self.initial_width/2
|
||||
circle = Circle(radius = radius)
|
||||
circle.scale(1.0/radius)
|
||||
circle.apply_complex_function(lambda z : z**complex_power)
|
||||
circle.scale(radius)
|
||||
boundary_point_as_complex = radius*complex(-1)**complex_power
|
||||
boundary_points = [
|
||||
[
|
||||
boundary_point_as_complex.real,
|
||||
unit*boundary_point_as_complex.imag,
|
||||
0
|
||||
]
|
||||
for unit in -1, 1
|
||||
]
|
||||
tip = radius*(1.5*LEFT+UP)
|
||||
self.add(
|
||||
circle,
|
||||
Line(boundary_points[0], tip),
|
||||
Line(boundary_points[1], tip)
|
||||
)
|
||||
self.highlight("white")
|
||||
self.rotate(np.pi/2)
|
||||
self.points[:,1] *= float(self.initial_height)/self.initial_width
|
||||
|
||||
class ThoughtBubble(Bubble):
|
||||
DEFAULT_CONFIG = {
|
||||
"num_bulges" : 7,
|
||||
"initial_inner_radius" : 1.8,
|
||||
"initial_width" : 6
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, ThoughtBubble, kwargs)
|
||||
Bubble.__init__(self, **kwargs)
|
||||
self.index_of_tip = np.argmin(self.points[:,1])
|
||||
|
||||
def generate_points(self):
|
||||
self.add(Circle().scale(0.15).shift(2.5*DOWN+2*LEFT))
|
||||
self.add(Circle().scale(0.3).shift(2*DOWN+1.5*LEFT))
|
||||
for n in range(self.num_bulges):
|
||||
theta = 2*np.pi*n/self.num_bulges
|
||||
self.add(Circle().shift((np.cos(theta), np.sin(theta), 0)))
|
||||
self.filter_out(lambda p : np.linalg.norm(p) < self.initial_inner_radius)
|
||||
self.stretch_to_fit_width(self.initial_width)
|
||||
self.highlight("white")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Rectangle.__init__(self, **kwargs)
|
|
@ -3,13 +3,10 @@ import numpy as np
|
|||
import operator as op
|
||||
from random import random
|
||||
|
||||
from helpers import *
|
||||
|
||||
from scene import Scene
|
||||
|
||||
from mobject import *
|
||||
from animation import *
|
||||
from region import *
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
class Graph():
|
||||
def __init__(self):
|
|
@ -5,13 +5,10 @@ import itertools as it
|
|||
from copy import deepcopy
|
||||
import sys
|
||||
|
||||
from helpers import *
|
||||
|
||||
from animation import *
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene, NumberLineScene
|
||||
from script_wrapper import command_line_create_scene
|
||||
from scene import Scene
|
||||
from number_line import NumberLineScene
|
||||
|
||||
MOVIE_PREFIX = "matrix_as_transform_2d/"
|
||||
|
|
@ -1,63 +1,7 @@
|
|||
import numpy as np
|
||||
import itertools as it
|
||||
|
||||
from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject
|
||||
from simple_mobjects import Arrow, Line, Circle
|
||||
from image_mobject import tex_mobject
|
||||
from constants import *
|
||||
from helpers import *
|
||||
|
||||
class FunctionGraph(Mobject1D):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : BLUE,
|
||||
"x_min" : -10,
|
||||
"x_max" : 10,
|
||||
"spatial_radius" : SPACE_WIDTH,
|
||||
}
|
||||
def __init__(self, function, **kwargs):
|
||||
digest_config(self, FunctionGraph, kwargs, locals())
|
||||
Mobject1D.__init__(self, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
numerical_radius = (self.x_max - self.x_min)/2
|
||||
numerical_center = (self.x_max + self.x_min)/2
|
||||
ratio = numerical_radius / self.spatial_radius
|
||||
epsilon = self.epsilon * ratio
|
||||
self.add_points([
|
||||
np.array([(x-numerical_center)/ratio, self.function(x), 0])
|
||||
for x in np.arange(self.x_min, self.x_max, self.epsilon)
|
||||
])
|
||||
|
||||
|
||||
class ParametricFunction(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : WHITE,
|
||||
"dim" : 1,
|
||||
"expected_measure" : 10.0,
|
||||
"density" : None
|
||||
}
|
||||
def __init__(self, function, **kwargs):
|
||||
digest_config(self, ParametricFunction, kwargs, locals())
|
||||
if self.density:
|
||||
self.epsilon = 1.0 / self.density
|
||||
elif self.dim == 1:
|
||||
self.epsilon = 1.0 / self.expected_measure / DEFAULT_POINT_DENSITY_1D
|
||||
else:
|
||||
self.epsilon = 1.0 / np.sqrt(self.expected_measure) / DEFAULT_POINT_DENSITY_2D
|
||||
Mobject.__init__(self, *args, **kwargs)
|
||||
|
||||
def generate_points(self):
|
||||
if self.dim == 1:
|
||||
self.add_points([
|
||||
self.function(t)
|
||||
for t in np.arange(-1, 1, self.epsilon)
|
||||
])
|
||||
if self.dim == 2:
|
||||
self.add_points([
|
||||
self.function(s, t)
|
||||
for t in np.arange(-1, 1, self.epsilon)
|
||||
for s in np.arange(-1, 1, self.epsilon)
|
||||
])
|
||||
from mobject import Mobject1D
|
||||
from scene import Scene
|
||||
|
||||
class NumberLine(Mobject1D):
|
||||
DEFAULT_CONFIG = {
|
||||
|
@ -67,7 +11,7 @@ class NumberLine(Mobject1D):
|
|||
"tick_size" : 0.1,
|
||||
"tick_frequency" : 0.5,
|
||||
"leftmost_tick" : None,
|
||||
"number_at_center" : 0,
|
||||
"number_at_center" : 0,
|
||||
"numbers_with_elongated_ticks" : [0],
|
||||
"longer_tick_multiple" : 2,
|
||||
}
|
||||
|
@ -168,13 +112,6 @@ class UnitInterval(NumberLine):
|
|||
NumberLine.__init__(self, **kwargs)
|
||||
|
||||
|
||||
class Axes(CompoundMobject):
|
||||
def __init__(self, **kwargs):
|
||||
x_axis = NumberLine(**kwargs)
|
||||
y_axis = NumberLine(**kwargs).rotate(np.pi/2, OUT)
|
||||
CompoundMobject.__init__(self, x_axis, y_axis)
|
||||
|
||||
|
||||
class NumberPlane(Mobject1D):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : BLUE,
|
||||
|
@ -275,69 +212,94 @@ class NumberPlane(Mobject1D):
|
|||
arrow.add_tip()
|
||||
return arrow
|
||||
|
||||
class ComplexPlane(NumberPlane):
|
||||
DEFAULT_CONFIG = {
|
||||
"color" : GREEN,
|
||||
"unit_to_spatial_width" : 1,
|
||||
"line_frequency" : 1,
|
||||
"faded_line_frequency" : 0.5,
|
||||
"number_at_center" : complex(0),
|
||||
}
|
||||
def __init__(self, **kwargs):
|
||||
digest_config(self, ComplexPlane, kwargs)
|
||||
kwargs.update({
|
||||
"x_unit_to_spatial_width" : self.unit_to_spatial_width,
|
||||
"y_uint_to_spatial_height" : self.unit_to_spatial_width,
|
||||
"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,
|
||||
"num_pair_at_center" : (self.number_at_center.real, self.number_at_center.imag),
|
||||
})
|
||||
NumberPlane.__init__(self, **kwargs)
|
||||
|
||||
def number_to_point(self, number):
|
||||
number = complex(number)
|
||||
return self.num_pair_to_point((number.real, number.imag))
|
||||
class NumberLineScene(Scene):
|
||||
def construct(self, **number_line_config):
|
||||
self.number_line = NumberLine(**number_line_config)
|
||||
self.displayed_numbers = self.number_line.default_numbers_to_display()
|
||||
self.number_mobs = self.number_line.get_number_mobjects(*self.displayed_numbers)
|
||||
self.add(self.number_line, *self.number_mobs)
|
||||
|
||||
def get_coordinate_labels(self, *numbers):
|
||||
result = []
|
||||
nudge = 0.1*(DOWN+RIGHT)
|
||||
if len(numbers) == 0:
|
||||
numbers = range(-int(self.x_radius), int(self.x_radius))
|
||||
numbers += [
|
||||
complex(0, y)
|
||||
for y in range(-int(self.y_radius), int(self.y_radius))
|
||||
]
|
||||
for number in numbers:
|
||||
point = self.number_to_point(number)
|
||||
if number == 0:
|
||||
num_str = "0"
|
||||
def zoom_in_on(self, number, zoom_factor, run_time = 2.0):
|
||||
unit_length_to_spatial_width = self.number_line.unit_length_to_spatial_width*zoom_factor
|
||||
radius = SPACE_WIDTH/unit_length_to_spatial_width
|
||||
tick_frequency = 10**(np.floor(np.log10(radius)))
|
||||
left_tick = tick_frequency*(np.ceil((number-radius)/tick_frequency))
|
||||
new_number_line = NumberLine(
|
||||
numerical_radius = radius,
|
||||
unit_length_to_spatial_width = unit_length_to_spatial_width,
|
||||
tick_frequency = tick_frequency,
|
||||
leftmost_tick = left_tick,
|
||||
number_at_center = number
|
||||
)
|
||||
new_displayed_numbers = new_number_line.default_numbers_to_display()
|
||||
new_number_mobs = new_number_line.get_number_mobjects(*new_displayed_numbers)
|
||||
|
||||
transforms = []
|
||||
additional_mobjects = []
|
||||
squished_new_line = deepcopy(new_number_line)
|
||||
squished_new_line.scale(1.0/zoom_factor)
|
||||
squished_new_line.shift(self.number_line.number_to_point(number))
|
||||
squished_new_line.points[:,1] = self.number_line.number_to_point(0)[1]
|
||||
transforms.append(Transform(squished_new_line, new_number_line))
|
||||
for mob, num in zip(new_number_mobs, new_displayed_numbers):
|
||||
point = Point(self.number_line.number_to_point(num))
|
||||
point.shift(new_number_line.get_vertical_number_offset())
|
||||
transforms.append(Transform(point, mob))
|
||||
for mob in self.mobjects:
|
||||
if mob == self.number_line:
|
||||
new_mob = deepcopy(mob)
|
||||
new_mob.shift(-self.number_line.number_to_point(number))
|
||||
new_mob.stretch(zoom_factor, 0)
|
||||
transforms.append(Transform(mob, new_mob))
|
||||
continue
|
||||
mob_center = mob.get_center()
|
||||
number_under_center = self.number_line.point_to_number(mob_center)
|
||||
new_point = new_number_line.number_to_point(number_under_center)
|
||||
new_point += mob_center[1]*UP
|
||||
if mob in self.number_mobs:
|
||||
transforms.append(Transform(mob, Point(new_point)))
|
||||
else:
|
||||
num_str = str(number).replace("j", "i")
|
||||
num = tex_mobject(num_str)
|
||||
num.scale(self.number_scale_factor)
|
||||
num.shift(point-num.get_corner(UP+LEFT)+nudge)
|
||||
result.append(num)
|
||||
return result
|
||||
transforms.append(ApplyMethod(mob.shift, new_point - mob_center))
|
||||
additional_mobjects.append(mob)
|
||||
line_to_hide_pixelation = Line(
|
||||
self.number_line.get_left(),
|
||||
self.number_line.get_right(),
|
||||
color = self.number_line.get_color()
|
||||
)
|
||||
self.add(line_to_hide_pixelation)
|
||||
self.play(*transforms, run_time = run_time)
|
||||
self.clear()
|
||||
self.number_line = new_number_line
|
||||
self.displayed_numbers = new_displayed_numbers
|
||||
self.number_mobs = new_number_mobs
|
||||
self.add(self.number_line, *self.number_mobs)
|
||||
self.add(*additional_mobjects)
|
||||
|
||||
def show_multiplication(self, num, **kwargs):
|
||||
if "interpolation_function" not in kwargs:
|
||||
if num > 0:
|
||||
kwargs["interpolation_function"] = straight_path
|
||||
else:
|
||||
kwargs["interpolation_function"] = counterclockwise_path()
|
||||
self.play(*[
|
||||
ApplyMethod(self.number_line.stretch, num, 0, **kwargs)
|
||||
]+[
|
||||
ApplyMethod(mob.shift, (num-1)*mob.get_center()[0]*RIGHT, **kwargs)
|
||||
for mob in self.number_mobs
|
||||
])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def add_coordinates(self, *numbers):
|
||||
self.add(*self.get_coordinate_labels(*numbers))
|
||||
return self
|
||||
|
||||
def add_spider_web(self, circle_freq = 1, angle_freq = np.pi/6):
|
||||
self.fade(self.fade_factor)
|
||||
config = {
|
||||
"color" : self.color,
|
||||
"density" : self.density,
|
||||
}
|
||||
for radius in np.arange(circle_freq, SPACE_WIDTH, circle_freq):
|
||||
self.add(Circle(radius = radius, **config))
|
||||
for angle in np.arange(0, 2*np.pi, angle_freq):
|
||||
end_point = np.cos(angle)*RIGHT + np.sin(angle)*UP
|
||||
end_point *= SPACE_WIDTH
|
||||
self.add(Line(ORIGIN, end_point, **config))
|
||||
return self
|
||||
|
||||
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
import numpy as np
|
||||
import itertools as it
|
||||
from copy import deepcopy
|
||||
import sys
|
||||
|
||||
from helpers import *
|
||||
|
||||
from animation import *
|
||||
from mobject import *
|
||||
from constants import *
|
||||
from region import *
|
||||
from scene import Scene
|
||||
from script_wrapper import command_line_create_scene
|
||||
from geometry import Polygon
|
||||
from region import region_from_polygon_vertices, region_from_line_boundary
|
||||
|
||||
MOVIE_PREFIX = "pythagorean_proof"
|
||||
|
||||
|
@ -513,14 +509,3 @@ class ShowRearrangementInBigSquareWithRegions(ShowRearrangementInBigSquare):
|
|||
self.highlight_region(region_from_polygon_vertices(
|
||||
RIGHT+DOWN, RIGHT+2*DOWN, 2*RIGHT+2*DOWN, 2*RIGHT+DOWN
|
||||
), color = A_COLOR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
command_line_create_scene(MOVIE_PREFIX)
|
|
@ -2,10 +2,10 @@ import numpy as np
|
|||
import itertools as it
|
||||
|
||||
from mobject import Mobject, Mobject1D, Mobject2D, CompoundMobject
|
||||
from simple_mobjects import Line
|
||||
from constants import *
|
||||
from geometry import Line
|
||||
from helpers import *
|
||||
|
||||
|
||||
class Stars(Mobject):
|
||||
DEFAULT_CONFIG = {
|
||||
"point_thickness" : 1,
|
Loading…
Add table
Reference in a new issue