Characters are vectorized

This commit is contained in:
Grant Sanderson 2016-04-17 19:29:27 -07:00
parent f5e4c3b334
commit e4c73306db
9 changed files with 231 additions and 290 deletions

View file

@ -12,18 +12,28 @@ from mobject import Mobject, Point
class Transform(Animation): class Transform(Animation):
CONFIG = { CONFIG = {
"path_func" : straight_path "path_arc" : 0,
"path_func" : None,
} }
def __init__(self, mobject, ending_mobject, **kwargs): def __init__(self, mobject, ending_mobject, **kwargs):
#Copy ending_mobject so as to not mess with caller #Copy ending_mobject so as to not mess with caller
ending_mobject = ending_mobject.copy() ending_mobject = ending_mobject.copy()
digest_config(self, kwargs, locals()) digest_config(self, kwargs, locals())
mobject.align_data(ending_mobject) mobject.align_data(ending_mobject)
self.init_path_func()
Animation.__init__(self, mobject, **kwargs) Animation.__init__(self, mobject, **kwargs)
self.name += "To" + str(ending_mobject) self.name += "To" + str(ending_mobject)
self.mobject.stroke_width = ending_mobject.stroke_width self.mobject.stroke_width = ending_mobject.stroke_width
def init_path_func(self):
if self.path_func is not None:
return
if self.path_arc == 0:
self.path_func = straight_path
else:
self.path_func = path_along_arc(self.path_arc)
def update_mobject(self, alpha): def update_mobject(self, alpha):
families = map( families = map(
@ -36,12 +46,12 @@ class Transform(Animation):
class ClockwiseTransform(Transform): class ClockwiseTransform(Transform):
CONFIG = { CONFIG = {
"path_func" : clockwise_path() "path_arc" : -np.pi
} }
class CounterclockwiseTransform(Transform): class CounterclockwiseTransform(Transform):
CONFIG = { CONFIG = {
"path_func" : counterclockwise_path() "path_arc" : np.pi
} }
class GrowFromCenter(Transform): class GrowFromCenter(Transform):

View file

@ -30,8 +30,8 @@ class Mobject(object):
if self.name is None: if self.name is None:
self.name = self.__class__.__name__ self.name = self.__class__.__name__
self.init_points() self.init_points()
self.init_colors()
self.generate_points() self.generate_points()
self.init_colors()
def __str__(self): def __str__(self):
return self.name return self.name
@ -172,6 +172,9 @@ class Mobject(object):
self.do_in_place(self.rotate, angle, axis, axes) self.do_in_place(self.rotate, angle, axis, axes)
return self return self
def flip(self, axis = UP):
self.rotate_in_place(np.pi, axis)
def scale_in_place(self, scale_factor): def scale_in_place(self, scale_factor):
self.do_in_place(self.scale, scale_factor) self.do_in_place(self.scale, scale_factor)
return self return self

View file

@ -2,14 +2,14 @@ from .mobject import Mobject
from helpers import * from helpers import *
class PMobject(Mobject): class PMobject(Mobject):
def init_colors(self): def init_points(self):
self.rgbs = np.zeros((0, 3)) self.rgbs = np.zeros((0, 3))
self.points = np.zeros((0, 3))
return self return self
def get_array_attrs(self): def get_array_attrs(self):
return Mobject.get_array_attrs(self) + ["rgbs"] return Mobject.get_array_attrs(self) + ["rgbs"]
def add_points(self, points, rgbs = None, color = None): def add_points(self, points, rgbs = None, color = None):
""" """
points must be a Nx3 numpy array, as must rgbs if it is not None points must be a Nx3 numpy array, as must rgbs if it is not None

View file

@ -5,59 +5,77 @@ from vectorized_mobject import VMobject
from topics.geometry import Rectangle, Circle from topics.geometry import Rectangle, Circle
from helpers import * from helpers import *
SVG_SCALE_VALUE = 0.05
class SVGMobject(VMobject): class SVGMobject(VMobject):
CONFIG = {
"stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE, #TODO...
}
def __init__(self, svg_file, **kwargs): def __init__(self, svg_file, **kwargs):
digest_config(self, kwargs, locals()) digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs) VMobject.__init__(self, **kwargs)
self.move_into_position()
def generate_points(self): def generate_points(self):
doc = minidom.parse(self.svg_file) doc = minidom.parse(self.svg_file)
defs = doc.getElementsByTagName("defs")[0] self.ref_to_element = {}
g = doc.getElementsByTagName("g")[0] for svg in doc.getElementsByTagName("svg"):
ref_to_mob = self.get_ref_to_mobject_map(defs) self.add(*self.get_mobjects_from(svg))
for element in g.childNodes:
if not isinstance(element, minidom.Element):
continue
mob = None
if element.tagName == 'use':
mob = self.use_to_mobject(element, ref_to_mob)
elif element.tagName == 'rect':
mob = self.rect_to_mobject(element)
elif element.tagName == 'circle':
mob = self.circle_to_mobject(element)
else:
warnings.warn("Unknown element type: " + element.tagName)
if mob is not None:
self.add(mob)
doc.unlink() doc.unlink()
self.move_into_position()
self.organize_submobjects()
def use_to_mobject(self, use_element, ref_to_mob): def get_mobjects_from(self, element):
result = []
if not isinstance(element, minidom.Element):
return result
if element.tagName == 'defs':
self.update_ref_to_element(element)
elif element.tagName == 'style':
pass #TODO, handle style
elif element.tagName in ['g', 'svg']:
result += it.chain(*[
self.get_mobjects_from(child)
for child in element.childNodes
])
elif element.tagName == 'path':
result.append(self.path_to_mobject(element))
elif element.tagName == 'use':
result += self.use_to_mobjects(element)
elif element.tagName == 'rect':
result.append(self.rect_to_mobject(element))
elif element.tagName == 'circle':
result.append(self.circle_to_mobject(element))
else:
warnings.warn("Unknown element type: " + element.tagName)
result = filter(lambda m : m is not None, result)
self.handle_transforms(element, VMobject(*result))
return result
def g_to_mobjects(self, g_element):
mob = VMobject(*self.get_mobjects_from(g_element))
self.handle_transforms(g_element, mob)
return mob.submobjects
def path_to_mobject(self, path_element):
return VMobjectFromSVGPathstring(
path_element.getAttribute('d')
)
def use_to_mobjects(self, use_element):
#Remove initial "#" character #Remove initial "#" character
ref = use_element.getAttribute("xlink:href")[1:] ref = use_element.getAttribute("xlink:href")[1:]
try: try:
mob = ref_to_mob[ref] return self.get_mobjects_from(
self.ref_to_element[ref]
)
except: except:
warnings.warn("%s not recognized"%ref) warnings.warn("%s not recognized"%ref)
return return
if mob in self.submobjects:
mob = VMobjectFromSVGPathstring( # <circle class="st1" cx="143.8" cy="268" r="22.6"/>
mob.get_original_path_string()
)
self.handle_transform(use_element, mob)
self.handle_shift(use_element, mob)
return mob
def circle_to_mobject(self, circle_element): def circle_to_mobject(self, circle_element):
pass x, y, r = [
float(circle_element.getAttribute(key))
if circle_element.hasAttribute(key)
else 0.0
for key in "cx", "cy", "r"
]
return Circle(radius = r).shift(x*RIGHT+y*DOWN)
def rect_to_mobject(self, rect_element): def rect_to_mobject(self, rect_element):
if rect_element.hasAttribute("fill"): if rect_element.hasAttribute("fill"):
@ -70,45 +88,30 @@ class SVGMobject(VMobject):
fill_color = WHITE, fill_color = WHITE,
fill_opacity = 1.0 fill_opacity = 1.0
) )
self.handle_shift(rect_element, mob)
mob.shift(mob.get_center()-mob.get_corner(DOWN+LEFT)) mob.shift(mob.get_center()-mob.get_corner(DOWN+LEFT))
return mob return mob
def handle_shift(self, element, mobject): def handle_transforms(self, element, mobject):
x, y = 0, 0 x, y = 0, 0
if element.hasAttribute('x'): try:
x = float(element.getAttribute('x')) x = float(element.getAttribute('x'))
if element.hasAttribute('y'):
#Flip y #Flip y
y = -float(element.getAttribute('y')) y = -float(element.getAttribute('y'))
except:
pass
mobject.shift(x*RIGHT+y*UP) mobject.shift(x*RIGHT+y*UP)
#TODO, transforms
def handle_transform(self, element, mobject): def update_ref_to_element(self, defs):
pass new_refs = dict([
(element.getAttribute('id'), element)
for element in defs.childNodes
if isinstance(element, minidom.Element) and element.hasAttribute('id')
])
self.ref_to_element.update(new_refs)
def move_into_position(self): def move_into_position(self):
self.center() pass #subclasses should tweak as needed
self.scale(SVG_SCALE_VALUE)
self.init_colors()
def organize_submobjects(self):
self.submobjects.sort(
lambda m1, m2 : int((m1.get_left()-m2.get_left())[0])
)
def get_ref_to_mobject_map(self, defs):
ref_to_mob = {}
for element in defs.childNodes:
if not isinstance(element, minidom.Element):
continue
ref = element.getAttribute('id')
if element.tagName == "path":
path_string = element.getAttribute('d')
mob = VMobjectFromSVGPathstring(path_string)
ref_to_mob[ref] = mob
if element.tagName == "use":
ref_to_mob[ref] = self.use_to_mobject(element, ref_to_mob)
return ref_to_mob
class VMobjectFromSVGPathstring(VMobject): class VMobjectFromSVGPathstring(VMobject):
@ -117,7 +120,7 @@ class VMobjectFromSVGPathstring(VMobject):
VMobject.__init__(self, **kwargs) VMobject.__init__(self, **kwargs)
def get_path_commands(self): def get_path_commands(self):
return [ result = [
"M", #moveto "M", #moveto
"L", #lineto "L", #lineto
"H", #horizontal lineto "H", #horizontal lineto
@ -129,6 +132,8 @@ class VMobjectFromSVGPathstring(VMobject):
"A", #elliptical Arc "A", #elliptical Arc
"Z", #closepath "Z", #closepath
] ]
result += map(lambda s : s.lower(), result)
return result
def generate_points(self): def generate_points(self):
pattern = "[%s]"%("".join(self.get_path_commands())) pattern = "[%s]"%("".join(self.get_path_commands()))
@ -144,10 +149,14 @@ class VMobjectFromSVGPathstring(VMobject):
self.rotate(np.pi, RIGHT) self.rotate(np.pi, RIGHT)
def handle_command(self, command, coord_string): def handle_command(self, command, coord_string):
isLower = command.islower()
command = command.upper()
#new_points are the points that will be added to the curr_points #new_points are the points that will be added to the curr_points
#list. This variable may get modified in the conditionals below. #list. This variable may get modified in the conditionals below.
points = self.growing_path.points points = self.growing_path.points
new_points = self.string_to_points(coord_string) new_points = self.string_to_points(coord_string)
if isLower:
new_points += points[-1]
if command == "M": #moveto if command == "M": #moveto
if len(points) > 0: if len(points) > 0:
self.growing_path = self.add_subpath(new_points) self.growing_path = self.add_subpath(new_points)
@ -178,9 +187,10 @@ class VMobjectFromSVGPathstring(VMobject):
self.growing_path.add_control_points(new_points) self.growing_path.add_control_points(new_points)
def string_to_points(self, coord_string): def string_to_points(self, coord_string):
coord_string = coord_string.replace("-",",-")
numbers = [ numbers = [
float(s) float(s)
for s in coord_string.split(" ") for s in re.split("[ ,]", coord_string)
if s != "" if s != ""
] ]
if len(numbers)%2 == 1: if len(numbers)%2 == 1:

View file

@ -2,11 +2,14 @@ from vectorized_mobject import VMobject
from svg_mobject import SVGMobject from svg_mobject import SVGMobject
from helpers import * from helpers import *
TEX_MOB_SCALE_VAL = 0.05
class TexMobject(SVGMobject): class TexMobject(SVGMobject):
CONFIG = { CONFIG = {
"template_tex_file" : TEMPLATE_TEX_FILE, "template_tex_file" : TEMPLATE_TEX_FILE,
"color" : WHITE,
"stroke_width" : 0, "stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE,
"should_center" : True, "should_center" : True,
"next_to_direction" : RIGHT, "next_to_direction" : RIGHT,
"next_to_buff" : 0.2, "next_to_buff" : 0.2,
@ -14,8 +17,8 @@ class TexMobject(SVGMobject):
def __init__(self, expression, **kwargs): def __init__(self, expression, **kwargs):
digest_config(self, kwargs, locals()) digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs) VMobject.__init__(self, **kwargs)
if self.should_center: self.move_into_position()
self.center() self.organize_submobjects()
def generate_points(self): def generate_points(self):
if isinstance(self.expression, list): if isinstance(self.expression, list):
@ -26,7 +29,6 @@ class TexMobject(SVGMobject):
self.template_tex_file self.template_tex_file
) )
SVGMobject.generate_points(self) SVGMobject.generate_points(self)
self.init_colors()
def handle_list_expression(self): def handle_list_expression(self):
@ -44,6 +46,15 @@ class TexMobject(SVGMobject):
self.submobjects = subs self.submobjects = subs
return self return self
def organize_submobjects(self):
self.submobjects.sort(
lambda m1, m2 : int((m1.get_left()-m2.get_left())[0])
)
def move_into_position(self):
self.center()
self.scale(TEX_MOB_SCALE_VAL)
self.init_colors()
class TextMobject(TexMobject): class TextMobject(TexMobject):
@ -130,45 +141,6 @@ def dvi_to_svg(dvi_file, regen_if_exists = False):
return result return result
# directory, filename = os.path.split(dvi_file)
# name = filename.replace(".dvi", "")
# images_dir = os.path.join(TEX_IMAGE_DIR, name)
# if not os.path.exists(images_dir):
# os.mkdir(images_dir)
# if os.listdir(images_dir) == [] or regen_if_exists:
# commands = [
# "convert",
# "-density",
# str(PDF_DENSITY),
# dvi_file,
# "-size",
# str(DEFAULT_WIDTH) + "x" + str(DEFAULT_HEIGHT),
# os.path.join(images_dir, name + ".png")
# ]
# os.system(" ".join(commands))
# return get_sorted_image_list(images_dir)
def get_sorted_image_list(images_dir):
return sorted([
os.path.join(images_dir, name)
for name in os.listdir(images_dir)
if name.endswith(".png")
], cmp_enumerated_files)
def cmp_enumerated_files(name1, name2):
name1, name2 = [
os.path.split(name)[1].replace(".png", "")
for name in name1, name2
]
num1, num2 = [
int(name.split("-")[-1])
for name in (name1, name2)
]
return num1 - num2

View file

@ -124,16 +124,16 @@ class VMobject(Mobject):
raise Exception("Unknown mode") raise Exception("Unknown mode")
return self return self
def change_mode(self, mode): def change_anchor_mode(self, mode):
anchors, h1, h2 = self.get_anchors_and_handles() anchors, h1, h2 = self.get_anchors_and_handles()
self.set_anchor_points(anchors, mode = mode) self.set_anchor_points(anchors, mode = mode)
return self return self
def make_smooth(self): def make_smooth(self):
return self.change_mode("smooth") return self.change_anchor_mode("smooth")
def make_jagged(self): def make_jagged(self):
return self.change_mode("corners") return self.change_anchor_mode("corners")
def add_subpath(self, points): def add_subpath(self, points):
""" """
@ -186,16 +186,20 @@ class VMobject(Mobject):
## Alignment ## Alignment
def align_points(self, mobject):
Mobject.align_points(self, mobject)
is_subpath = self.is_subpath or mobject.is_subpath
self.is_subpath = mobject.is_subpath = is_subpath
mark_closed = self.mark_paths_closed and mobject.mark_paths_closed
self.mark_paths_closed = mobject.mark_paths_closed = mark_closed
return self
def align_points_with_larger(self, larger_mobject): def align_points_with_larger(self, larger_mobject):
assert(isinstance(larger_mobject, VMobject)) assert(isinstance(larger_mobject, VMobject))
self.insert_n_anchor_points( self.insert_n_anchor_points(
larger_mobject.get_num_anchor_points()-\ larger_mobject.get_num_anchor_points()-\
self.get_num_anchor_points() self.get_num_anchor_points()
) )
is_subpath = self.is_subpath or larger_mobject.is_subpath
self.is_subpath = larger_mobject.is_subpath = is_subpath
mark_closed = self.mark_paths_closed and larger_mobject.mark_paths_closed
self.mark_paths_closed = larger_mobject.mark_paths_closed = mark_closed
return self return self
def insert_n_anchor_points(self, n): def insert_n_anchor_points(self, n):

View file

@ -4,7 +4,7 @@ import itertools as it
from helpers import * from helpers import *
from scene import Scene from scene import Scene
from animation import Animation from animation import Animation
from mobject import TexMobject from mobject.tex_mobject import TexMobject
class RearrangeEquation(Scene): class RearrangeEquation(Scene):
def construct( def construct(
@ -12,8 +12,7 @@ class RearrangeEquation(Scene):
start_terms, start_terms,
end_terms, end_terms,
index_map, index_map,
size = None, path_arc = np.pi,
path = counterclockwise_path(),
start_transform = None, start_transform = None,
end_transform = None, end_transform = None,
leave_start_terms = False, leave_start_terms = False,
@ -21,7 +20,7 @@ class RearrangeEquation(Scene):
): ):
transform_kwargs["path_func"] = path transform_kwargs["path_func"] = path
start_mobs, end_mobs = self.get_mobs_from_terms( start_mobs, end_mobs = self.get_mobs_from_terms(
start_terms, end_terms, size start_terms, end_terms
) )
if start_transform: if start_transform:
start_mobs = start_transform(Mobject(*start_mobs)).split() start_mobs = start_transform(Mobject(*start_mobs)).split()
@ -59,7 +58,7 @@ class RearrangeEquation(Scene):
self.dither() self.dither()
def get_mobs_from_terms(self, start_terms, end_terms, size): def get_mobs_from_terms(self, start_terms, end_terms):
""" """
Need to ensure that all image mobjects for a tex expression Need to ensure that all image mobjects for a tex expression
stemming from the same string are point-for-point copies of one stemming from the same string are point-for-point copies of one
@ -68,8 +67,8 @@ class RearrangeEquation(Scene):
""" """
num_start_terms = len(start_terms) num_start_terms = len(start_terms)
all_mobs = np.array( all_mobs = np.array(
TexMobject(start_terms, size = size).split() + \ TexMobject(start_terms).split() + \
TexMobject(end_terms, size = size).split() TexMobject(end_terms).split()
) )
all_terms = np.array(start_terms+end_terms) all_terms = np.array(start_terms+end_terms)
for term in set(all_terms): for term in set(all_terms):

View file

@ -1,136 +1,113 @@
from helpers import * from helpers import *
from mobject import Mobject from mobject import Mobject
from mobject.image_mobject import ImageMobject from mobject.svg_mobject import SVGMobject
from mobject.tex_mobject import TexMobject, TextMobject from mobject.vectorized_mobject import VMobject
from topics.geometry import Circle, Line from mobject.tex_mobject import TextMobject
PI_CREATURE_DIR = os.path.join(IMAGE_DIR, "PiCreature") PI_CREATURE_DIR = os.path.join(IMAGE_DIR, "PiCreature")
PI_CREATURE_SCALE_VAL = 0.5 PI_CREATURE_SCALE_VAL = 0.5
PI_CREATURE_MOUTH_TO_EYES_DISTANCE = 0.25
def part_name_to_directory(name): MOUTH_INDEX = 5
return os.path.join(PI_CREATURE_DIR, "pi_creature_"+name) + ".png" BODY_INDEX = 4
RIGHT_PUPIL_INDEX = 3
LEFT_PUPIL_INDEX = 2
RIGHT_EYE_INDEX = 1
LEFT_EYE_INDEX = 0
class PiCreature(Mobject):
class PiCreature(SVGMobject):
CONFIG = { CONFIG = {
"color" : BLUE_E "color" : BLUE_E,
"stroke_width" : 0,
"fill_opacity" : 1.0,
} }
PART_NAMES = [ def __init__(self, mode = "plain", **kwargs):
'arm', self.parts_named = False
'body', svg_file = os.path.join(
'left_eye', PI_CREATURE_DIR,
'right_eye', "PiCreatures_%s.svg"%mode
'left_leg', )
'right_leg', digest_config(self, kwargs, locals())
'mouth', SVGMobject.__init__(self, svg_file, **kwargs)
] self.init_colors()
WHITE_PART_NAMES = ['left_eye', 'right_eye', 'mouth']
def __init__(self, **kwargs):
Mobject.__init__(self, **kwargs)
for part_name in self.PART_NAMES:
mob = ImageMobject(
part_name_to_directory(part_name),
should_center = False
)
if part_name not in self.WHITE_PART_NAMES:
mob.highlight(self.color)
setattr(self, part_name, mob)
self.add(mob)
self.eyes = Mobject(self.left_eye, self.right_eye)
self.legs = Mobject(self.left_leg, self.right_leg)
self.mouth.center().shift(self.get_mouth_center())
self.add(self.mouth)
self.scale(PI_CREATURE_SCALE_VAL)
def get_parts(self): def move_into_position(self):
return [getattr(self, pn) for pn in self.PART_NAMES] self.scale_to_fit_height(4)
self.center()
def get_white_parts(self): def name_parts(self):
return [ self.mouth = self.submobjects[MOUTH_INDEX]
getattr(self, pn) self.body = self.submobjects[BODY_INDEX]
for pn in self.WHITE_PART_NAMES self.pupils = VMobject(*[
] self.submobjects[LEFT_PUPIL_INDEX],
self.submobjects[RIGHT_PUPIL_INDEX]
])
self.eyes = VMobject(*[
self.submobjects[LEFT_EYE_INDEX],
self.submobjects[RIGHT_EYE_INDEX]
])
self.submobjects = []
self.add(self.body, self.mouth, self.eyes, self.pupils)
self.parts_named = True
def get_mouth_center(self): def init_colors(self):
result = self.body.get_center() VMobject.init_colors(self)
result[0] = self.eyes.get_center()[0] if not self.parts_named:
return result self.name_parts()
self.mouth.set_fill(BLACK)
self.body.set_fill(self.color)
self.pupils.set_fill(BLACK)
self.eyes.set_fill(WHITE)
def highlight(self, color, condition = None):
for part in set(self.get_parts()).difference(self.get_white_parts()): def highlight(self, color):
part.highlight(color, condition) self.body.set_fill(color)
return self return self
def move_to(self, destination): def move_to(self, destination):
self.shift(destination-self.get_bottom()) self.shift(destination-self.get_bottom())
return self return self
def get_eye_center(self): def change_mode(self, mode):
return self.eyes.get_center() curr_center = self.get_center()
curr_height = self.get_height()
def make_mean(self): flip = self.is_flipped()
eye_x, eye_y = self.get_eye_center()[:2] self.__class__.__init__(self, mode)
def should_delete((x, y, z)): self.scale_to_fit_height(curr_height)
return y - eye_y > 0.3*abs(x - eye_x) self.shift(curr_center)
self.eyes.highlight("black", should_delete) if flip:
self.give_straight_face() self.flip()
return self return self
def make_sad(self): def look_left(self):
eye_x, eye_y = self.get_eye_center()[:2] self.change_mode(self.mode + "_looking_left")
eye_y += 0.15
def should_delete((x, y, z)):
return y - eye_y > -0.3*abs(x - eye_x)
self.eyey.highlight("black", should_delete)
self.give_frown()
return self return self
def get_step_intermediate(self, pi_creature): def is_flipped(self):
vect = pi_creature.get_center() - self.get_center() return self.eyes.submobjects[0].get_center()[0] > \
result = self.copy().shift(vect / 2.0) self.eyes.submobjects[1].get_center()[0]
left_forward = vect[0] > 0
if self.right_leg.get_center()[0] < self.left_leg.get_center()[0]:
#For Mortimer's case
left_forward = not left_forward
if left_forward:
result.left_leg.wag(vect/2.0, DOWN)
result.right_leg.wag(-vect/2.0, DOWN)
else:
result.right_leg.wag(vect/2.0, DOWN)
result.left_leg.wag(-vect/2.0, DOWN)
return result
def blink(self): def blink(self):
bottom = self.eyes.get_bottom() eye_bottom_y = self.eyes.get_bottom()[1]
self.eyes.apply_function( for mob in self.eyes, self.pupils:
lambda (x, y, z) : (x, bottom[1], z) mob.apply_function(
) lambda p : [p[0], eye_bottom_y, p[2]]
)
return self return self
def shift_eyes(self):
for eye in self.left_eye, self.right_eye:
eye.rotate_in_place(np.pi, UP)
return self
def to_symbol(self):
Mobject.__init__(
self,
*list(set(self.get_parts()).difference(self.get_white_parts()))
)
class Randolph(PiCreature): class Randolph(PiCreature):
pass #Nothing more than an alternative name pass #Nothing more than an alternative name
class Mortimer(PiCreature): class Mortimer(PiCreature):
CONFIG = { CONFIG = {
"color" : MAROON_E "color" : "#be2612"
} }
def __init__(self, **kwargs): def __init__(self, *args, **kwargs):
PiCreature.__init__(self, **kwargs) PiCreature.__init__(self, *args, **kwargs)
self.rotate(np.pi, UP) self.flip()
class Mathematician(PiCreature): class Mathematician(PiCreature):
@ -138,31 +115,43 @@ class Mathematician(PiCreature):
"color" : GREY, "color" : GREY,
} }
class Bubble(Mobject): class Bubble(SVGMobject):
CONFIG = { CONFIG = {
"direction" : LEFT, "direction" : LEFT,
"center_point" : ORIGIN, "center_point" : ORIGIN,
"content_scale_factor" : 0.75,
"height" : 4,
"width" : 6,
"file_name" : None,
} }
def __init__(self, **kwargs): def __init__(self, **kwargs):
Mobject.__init__(self, **kwargs) digest_config(self, kwargs, locals())
self.center_offset = self.center_point - Mobject.get_center(self) if self.file_name is None:
raise Exception("Must invoke Bubble subclass")
svg_file = os.path.join(
IMAGE_DIR, self.file_name
)
SVGMobject.__init__(self, svg_file, **kwargs)
self.center()
self.stretch_to_fit_height(self.height)
self.stretch_to_fit_width(self.width)
if self.direction[0] > 0: if self.direction[0] > 0:
self.rotate(np.pi, UP) Mobject.flip(self)
self.content = Mobject() self.content = Mobject()
def get_tip(self): def get_tip(self):
raise Exception("Not implemented") return self.get_corner(DOWN+self.direction)
def get_bubble_center(self): def get_bubble_center(self):
return self.get_center()+self.center_offset return self.get_center() + self.get_height()*UP/8.0
def move_tip_to(self, point): def move_tip_to(self, point):
self.shift(point - self.get_tip()) self.shift(point - self.get_tip())
return self return self
def flip(self): def flip(self):
Mobject.flip(self)
self.direction = -np.array(self.direction) self.direction = -np.array(self.direction)
self.rotate(np.pi, UP)
return self return self
def pin_to(self, mobject): def pin_to(self, mobject):
@ -175,11 +164,14 @@ class Bubble(Mobject):
return self return self
def add_content(self, mobject): def add_content(self, mobject):
scaled_width = 0.75*self.get_width() if self.content in self.submobjects:
self.submobjects.remove(self.content)
scaled_width = self.content_scale_factor*self.get_width()
if mobject.get_width() > scaled_width: if mobject.get_width() > scaled_width:
mobject.scale(scaled_width / mobject.get_width()) mobject.scale(scaled_width / mobject.get_width())
mobject.shift(self.get_bubble_center()) mobject.shift(self.get_bubble_center())
self.content = mobject self.content = mobject
self.add(self.content)
return self return self
def write(self, text): def write(self, text):
@ -187,69 +179,16 @@ class Bubble(Mobject):
return self return self
def clear(self): def clear(self):
self.content = Mobject() self.add_content(Mobject())
return self return self
class SpeechBubble(Bubble): class SpeechBubble(Bubble):
CONFIG = { CONFIG = {
"initial_width" : 6, "file_name" : "Bubbles_speech.svg",
"initial_height" : 4,
} }
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.little_line = Line(boundary_points[0], tip)
self.circle = circle
self.add(
circle,
self.little_line,
Line(boundary_points[1], tip)
)
self.highlight("white")
self.rotate(np.pi/2)
self.stretch_to_fit_height(self.initial_height)
def get_tip(self):
return self.little_line.points[-1]
def get_bubble_center(self):
return self.circle.get_center()
class ThoughtBubble(Bubble): class ThoughtBubble(Bubble):
CONFIG = { CONFIG = {
"num_bulges" : 7, "file_name" : "Bubbles_thought.svg",
"initial_inner_radius" : 1.8,
"initial_width" : 6,
} }
def __init__(self, **kwargs):
Bubble.__init__(self, **kwargs)
def get_tip(self):
return self.small_circle.get_bottom()
def generate_points(self):
self.small_circle = Circle().scale(0.15)
self.small_circle.shift(2.5*DOWN+2*LEFT)
self.add(self.small_circle)
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")

View file

@ -50,6 +50,10 @@ class Dot(Circle): #Use 1D density, even though 2D
"fill_color" : WHITE, "fill_color" : WHITE,
"fill_opacity" : 1.0 "fill_opacity" : 1.0
} }
def __init__(self, point = ORIGIN, **kwargs):
Circle.__init__(self, **kwargs)
self.shift(point)
self.init_colors()
class Line(VMobject): class Line(VMobject):