From e4c73306db702de68bd2f2e7c96463b05b1d8372 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 17 Apr 2016 19:29:27 -0700 Subject: [PATCH] Characters are vectorized --- animation/transform.py | 16 +- mobject/mobject.py | 5 +- mobject/point_cloud_mobject.py | 4 +- mobject/svg_mobject.py | 142 +++++++++--------- mobject/tex_mobject.py | 58 ++------ mobject/vectorized_mobject.py | 18 ++- topics/arithmetic.py | 13 +- topics/characters.py | 261 +++++++++++++-------------------- topics/geometry.py | 4 + 9 files changed, 231 insertions(+), 290 deletions(-) diff --git a/animation/transform.py b/animation/transform.py index 0af4edb7..8c87f4c5 100644 --- a/animation/transform.py +++ b/animation/transform.py @@ -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): diff --git a/mobject/mobject.py b/mobject/mobject.py index 21907be2..a443e612 100644 --- a/mobject/mobject.py +++ b/mobject/mobject.py @@ -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 diff --git a/mobject/point_cloud_mobject.py b/mobject/point_cloud_mobject.py index 6784bdd1..51d70326 100644 --- a/mobject/point_cloud_mobject.py +++ b/mobject/point_cloud_mobject.py @@ -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 diff --git a/mobject/svg_mobject.py b/mobject/svg_mobject.py index 4c8dc95c..33878830 100644 --- a/mobject/svg_mobject.py +++ b/mobject/svg_mobject.py @@ -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 + + # 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: diff --git a/mobject/tex_mobject.py b/mobject/tex_mobject.py index 74eb889c..39e7e3ef 100644 --- a/mobject/tex_mobject.py +++ b/mobject/tex_mobject.py @@ -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 - - - diff --git a/mobject/vectorized_mobject.py b/mobject/vectorized_mobject.py index e1d1d11f..eeb72083 100644 --- a/mobject/vectorized_mobject.py +++ b/mobject/vectorized_mobject.py @@ -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): diff --git a/topics/arithmetic.py b/topics/arithmetic.py index 25df0449..39eb062d 100644 --- a/topics/arithmetic.py +++ b/topics/arithmetic.py @@ -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): diff --git a/topics/characters.py b/topics/characters.py index 62bcdf34..76521664 100644 --- a/topics/characters.py +++ b/topics/characters.py @@ -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") diff --git a/topics/geometry.py b/topics/geometry.py index 607bca50..314de5ee 100644 --- a/topics/geometry.py +++ b/topics/geometry.py @@ -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):