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):
CONFIG = {
"path_func" : straight_path
"path_arc" : 0,
"path_func" : None,
}
def __init__(self, mobject, ending_mobject, **kwargs):
#Copy ending_mobject so as to not mess with caller
ending_mobject = ending_mobject.copy()
digest_config(self, kwargs, locals())
mobject.align_data(ending_mobject)
self.init_path_func()
Animation.__init__(self, mobject, **kwargs)
self.name += "To" + str(ending_mobject)
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):
families = map(
@ -36,12 +46,12 @@ class Transform(Animation):
class ClockwiseTransform(Transform):
CONFIG = {
"path_func" : clockwise_path()
"path_arc" : -np.pi
}
class CounterclockwiseTransform(Transform):
CONFIG = {
"path_func" : counterclockwise_path()
"path_arc" : np.pi
}
class GrowFromCenter(Transform):

View file

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

View file

@ -2,14 +2,14 @@ from .mobject import Mobject
from helpers import *
class PMobject(Mobject):
def init_colors(self):
def init_points(self):
self.rgbs = np.zeros((0, 3))
self.points = np.zeros((0, 3))
return self
def get_array_attrs(self):
return Mobject.get_array_attrs(self) + ["rgbs"]
def add_points(self, points, rgbs = None, color = 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 helpers import *
SVG_SCALE_VALUE = 0.05
class SVGMobject(VMobject):
CONFIG = {
"stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE, #TODO...
}
def __init__(self, svg_file, **kwargs):
digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs)
self.move_into_position()
def generate_points(self):
doc = minidom.parse(self.svg_file)
defs = doc.getElementsByTagName("defs")[0]
g = doc.getElementsByTagName("g")[0]
ref_to_mob = self.get_ref_to_mobject_map(defs)
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)
self.ref_to_element = {}
for svg in doc.getElementsByTagName("svg"):
self.add(*self.get_mobjects_from(svg))
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
ref = use_element.getAttribute("xlink:href")[1:]
try:
mob = ref_to_mob[ref]
return self.get_mobjects_from(
self.ref_to_element[ref]
)
except:
warnings.warn("%s not recognized"%ref)
return
if mob in self.submobjects:
mob = VMobjectFromSVGPathstring(
mob.get_original_path_string()
)
self.handle_transform(use_element, mob)
self.handle_shift(use_element, mob)
return mob
# <circle class="st1" cx="143.8" cy="268" r="22.6"/>
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):
if rect_element.hasAttribute("fill"):
@ -70,45 +88,30 @@ class SVGMobject(VMobject):
fill_color = WHITE,
fill_opacity = 1.0
)
self.handle_shift(rect_element, mob)
mob.shift(mob.get_center()-mob.get_corner(DOWN+LEFT))
return mob
def handle_shift(self, element, mobject):
def handle_transforms(self, element, mobject):
x, y = 0, 0
if element.hasAttribute('x'):
try:
x = float(element.getAttribute('x'))
if element.hasAttribute('y'):
#Flip y
y = -float(element.getAttribute('y'))
except:
pass
mobject.shift(x*RIGHT+y*UP)
#TODO, transforms
def handle_transform(self, element, mobject):
pass
def update_ref_to_element(self, defs):
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):
self.center()
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
pass #subclasses should tweak as needed
class VMobjectFromSVGPathstring(VMobject):
@ -117,7 +120,7 @@ class VMobjectFromSVGPathstring(VMobject):
VMobject.__init__(self, **kwargs)
def get_path_commands(self):
return [
result = [
"M", #moveto
"L", #lineto
"H", #horizontal lineto
@ -129,6 +132,8 @@ class VMobjectFromSVGPathstring(VMobject):
"A", #elliptical Arc
"Z", #closepath
]
result += map(lambda s : s.lower(), result)
return result
def generate_points(self):
pattern = "[%s]"%("".join(self.get_path_commands()))
@ -144,10 +149,14 @@ class VMobjectFromSVGPathstring(VMobject):
self.rotate(np.pi, RIGHT)
def handle_command(self, command, coord_string):
isLower = command.islower()
command = command.upper()
#new_points are the points that will be added to the curr_points
#list. This variable may get modified in the conditionals below.
points = self.growing_path.points
new_points = self.string_to_points(coord_string)
if isLower:
new_points += points[-1]
if command == "M": #moveto
if len(points) > 0:
self.growing_path = self.add_subpath(new_points)
@ -178,9 +187,10 @@ class VMobjectFromSVGPathstring(VMobject):
self.growing_path.add_control_points(new_points)
def string_to_points(self, coord_string):
coord_string = coord_string.replace("-",",-")
numbers = [
float(s)
for s in coord_string.split(" ")
for s in re.split("[ ,]", coord_string)
if s != ""
]
if len(numbers)%2 == 1:

View file

@ -2,11 +2,14 @@ from vectorized_mobject import VMobject
from svg_mobject import SVGMobject
from helpers import *
TEX_MOB_SCALE_VAL = 0.05
class TexMobject(SVGMobject):
CONFIG = {
"template_tex_file" : TEMPLATE_TEX_FILE,
"color" : WHITE,
"stroke_width" : 0,
"fill_opacity" : 1.0,
"fill_color" : WHITE,
"should_center" : True,
"next_to_direction" : RIGHT,
"next_to_buff" : 0.2,
@ -14,8 +17,8 @@ class TexMobject(SVGMobject):
def __init__(self, expression, **kwargs):
digest_config(self, kwargs, locals())
VMobject.__init__(self, **kwargs)
if self.should_center:
self.center()
self.move_into_position()
self.organize_submobjects()
def generate_points(self):
if isinstance(self.expression, list):
@ -26,7 +29,6 @@ class TexMobject(SVGMobject):
self.template_tex_file
)
SVGMobject.generate_points(self)
self.init_colors()
def handle_list_expression(self):
@ -44,6 +46,15 @@ class TexMobject(SVGMobject):
self.submobjects = subs
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):
@ -130,45 +141,6 @@ def dvi_to_svg(dvi_file, regen_if_exists = False):
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")
return self
def change_mode(self, mode):
def change_anchor_mode(self, mode):
anchors, h1, h2 = self.get_anchors_and_handles()
self.set_anchor_points(anchors, mode = mode)
return self
def make_smooth(self):
return self.change_mode("smooth")
return self.change_anchor_mode("smooth")
def make_jagged(self):
return self.change_mode("corners")
return self.change_anchor_mode("corners")
def add_subpath(self, points):
"""
@ -186,16 +186,20 @@ class VMobject(Mobject):
## 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):
assert(isinstance(larger_mobject, VMobject))
self.insert_n_anchor_points(
larger_mobject.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
def insert_n_anchor_points(self, n):

View file

@ -4,7 +4,7 @@ import itertools as it
from helpers import *
from scene import Scene
from animation import Animation
from mobject import TexMobject
from mobject.tex_mobject import TexMobject
class RearrangeEquation(Scene):
def construct(
@ -12,8 +12,7 @@ class RearrangeEquation(Scene):
start_terms,
end_terms,
index_map,
size = None,
path = counterclockwise_path(),
path_arc = np.pi,
start_transform = None,
end_transform = None,
leave_start_terms = False,
@ -21,7 +20,7 @@ class RearrangeEquation(Scene):
):
transform_kwargs["path_func"] = path
start_mobs, end_mobs = self.get_mobs_from_terms(
start_terms, end_terms, size
start_terms, end_terms
)
if start_transform:
start_mobs = start_transform(Mobject(*start_mobs)).split()
@ -59,7 +58,7 @@ class RearrangeEquation(Scene):
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
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)
all_mobs = np.array(
TexMobject(start_terms, size = size).split() + \
TexMobject(end_terms, size = size).split()
TexMobject(start_terms).split() + \
TexMobject(end_terms).split()
)
all_terms = np.array(start_terms+end_terms)
for term in set(all_terms):

View file

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