Merge pull request #172 from 3b1b/WindingNumber

Winding number
This commit is contained in:
Grant Sanderson
2018-03-22 15:02:44 -07:00
committed by GitHub
2 changed files with 290 additions and 137 deletions

View File

@ -452,66 +452,6 @@ def make_alpha_winder(func, start, end, num_checkpoints):
return resit_near(func(x), check_points[index])
return return_func
def split_interval((a, b)):
mid = (a + b)/2.0
return ((a, mid), (mid, b))
# I am surely reinventing some wheel here, but what's done is done...
class RectangleData():
def __init__(self, x_interval, y_interval):
self.rect = (x_interval, y_interval)
def get_top_left(self):
return np.array((self.rect[0][0], self.rect[1][1]))
def get_top_right(self):
return np.array((self.rect[0][1], self.rect[1][1]))
def get_bottom_right(self):
return np.array((self.rect[0][1], self.rect[1][0]))
def get_bottom_left(self):
return np.array((self.rect[0][0], self.rect[1][0]))
def get_top(self):
return (self.get_top_left(), self.get_top_right())
def get_right(self):
return (self.get_top_right(), self.get_bottom_right())
def get_bottom(self):
return (self.get_bottom_right(), self.get_bottom_left())
def get_left(self):
return (self.get_bottom_left(), self.get_top_left())
def splits_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
# TODO: Can refactor the following; will do later
if dim == 0:
return_data = [RectangleData(new_interval, y_interval) for new_interval in split_interval(x_interval)]
elif dim == 1:
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)[::-1]]
else:
print "RectangleData.splits_on_dim passed illegitimate dimension!"
return tuple(return_data)
def split_line_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
if dim == 0:
sides = (self.get_top(), self.get_bottom())
elif dim == 1:
sides = (self.get_left(), self.get_right())
else:
print "RectangleData.split_line_on_dim passed illegitimate dimension!"
return tuple([mid(x, y) for (x, y) in sides])
# The various inconsistent choices of what datatype to use where are a bit of a mess,
# but I'm more keen to rush this video out now than to sort this out.
@ -645,6 +585,7 @@ def walker_animation_with_display(
number_update_func = None,
show_arrows = True,
scale_arrows = False,
num_decimal_points = 1,
**kwargs
):
@ -659,7 +600,7 @@ def walker_animation_with_display(
if number_update_func != None:
display = DecimalNumber(0,
num_decimal_points = 1,
num_decimal_points = num_decimal_points,
fill_color = WHITE,
include_background_rectangle = True)
display.background_rectangle.fill_opacity = 0.5
@ -799,12 +740,14 @@ class PiWalker(ColorMappedByFuncScene):
"step_run_time" : 1,
"scale_arrows" : False,
"display_wind" : True,
"wind_reset_indices" : [],
"display_size" : False,
"display_odometer" : False,
"color_foreground_not_background" : False,
"show_num_plane" : False,
"draw_lines" : True,
"num_checkpoints" : 10,
"num_decimal_points" : 1
}
def construct(self):
@ -840,7 +783,7 @@ class PiWalker(ColorMappedByFuncScene):
clockwise_val_func = lambda p : -point_to_rev(self.func(p))
alpha_winder = make_alpha_winder(clockwise_val_func, start_coords, end_coords, self.num_checkpoints)
number_update_func = lambda alpha, alpha_winder = alpha_winder, start_wind = start_wind: alpha_winder(alpha) - alpha_winder(0) + start_wind
start_wind = number_update_func(1)
start_wind = 0 if i + 1 in self.wind_reset_indices else number_update_func(1)
elif self.display_size:
# We need to do this roundabout default argument thing to get the closure we want,
# so the next iteration changing val_alpha_func doesn't change this closure
@ -858,7 +801,8 @@ class PiWalker(ColorMappedByFuncScene):
scale_arrows = self.scale_arrows,
number_update_func = number_update_func,
run_time = self.step_run_time,
walker_stroke_color = WALKER_LIGHT_COLOR if self.color_foreground_not_background else BLACK
walker_stroke_color = WALKER_LIGHT_COLOR if self.color_foreground_not_background else BLACK,
num_decimal_points = self.num_decimal_points,
)
if self.display_odometer:
@ -904,6 +848,9 @@ class PiWalkerRect(PiWalker):
"walk_height" : 2,
"func" : plane_func_from_complex_func(lambda c: c**2),
"double_up" : False,
# New default for the scenes using this:
"display_wind" : True
}
def setup(self):
@ -929,6 +876,114 @@ class PiWalkerCircle(PiWalker):
self.walk_coords = [r * np.array((np.cos(i * TAU/N), np.sin(i * TAU/N))) for i in range(N)]
PiWalker.setup(self)
def split_interval((a, b)):
mid = (a + b)/2.0
return ((a, mid), (mid, b))
# I am surely reinventing some wheel here, but what's done is done...
class RectangleData():
def __init__(self, x_interval, y_interval):
self.rect = (x_interval, y_interval)
def get_top_left(self):
return np.array((self.rect[0][0], self.rect[1][1]))
def get_top_right(self):
return np.array((self.rect[0][1], self.rect[1][1]))
def get_bottom_right(self):
return np.array((self.rect[0][1], self.rect[1][0]))
def get_bottom_left(self):
return np.array((self.rect[0][0], self.rect[1][0]))
def get_top(self):
return (self.get_top_left(), self.get_top_right())
def get_right(self):
return (self.get_top_right(), self.get_bottom_right())
def get_bottom(self):
return (self.get_bottom_right(), self.get_bottom_left())
def get_left(self):
return (self.get_bottom_left(), self.get_top_left())
def get_center(self):
return interpolate(self.get_top_left(), self.get_bottom_right(), 0.5)
def get_width(self):
return self.rect[0][1] - self.rect[0][0]
def get_height(self):
return self.rect[1][1] - self.rect[1][0]
def splits_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
# TODO: Can refactor the following; will do later
if dim == 0:
return_data = [RectangleData(new_interval, y_interval) for new_interval in split_interval(x_interval)]
elif dim == 1:
return_data = [RectangleData(x_interval, new_interval) for new_interval in split_interval(y_interval)[::-1]]
else:
print "RectangleData.splits_on_dim passed illegitimate dimension!"
return tuple(return_data)
def split_line_on_dim(self, dim):
x_interval = self.rect[0]
y_interval = self.rect[1]
if dim == 0:
sides = (self.get_top(), self.get_bottom())
elif dim == 1:
sides = (self.get_left(), self.get_right())
else:
print "RectangleData.split_line_on_dim passed illegitimate dimension!"
return tuple([mid(x, y) for (x, y) in sides])
class EquationSolver2dNode(object):
def __init__(self, first_anim, children = []):
self.first_anim = first_anim
self.children = children
def depth(self):
if len(self.children) == 0:
return 0
return 1 + max(map(lambda n : n.depth(), self.children))
def nodes_at_depth(self, n):
if n == 0:
return [self]
# Not the efficient way to flatten lists, because Python + is linear in list size,
# but we have at most two children, so no big deal here
return sum(map(lambda c : c.nodes_at_depth(n - 1), self.children), [])
# This is definitely NOT the efficient way to do BFS, but I'm just trying to write something
# quick without thinking that gets the job done on small instances for now
def hacky_bfs(self):
depth = self.depth()
# Not the efficient way to flatten lists, because Python + is linear in list size,
# but this IS hacky_bfs...
return sum(map(lambda i : self.nodes_at_depth(i), range(depth + 1)), [])
def display_in_series(self):
return Succession(self.first_anim, *map(lambda n : n.display_in_series(), self.children))
def display_in_parallel(self):
return Succession(self.first_anim, AnimationGroup(*map(lambda n : n.display_in_parallel(), self.children)))
def display_in_bfs(self):
bfs_nodes = self.hacky_bfs()
return Succession(*map(lambda n : n.first_anim, bfs_nodes))
class EquationSolver2d(ColorMappedObjectsScene):
CONFIG = {
"camera_config" : {"use_z_coordinate_for_display_order": True},
@ -938,7 +993,11 @@ class EquationSolver2d(ColorMappedObjectsScene):
"initial_upper_y" : 3,
"num_iterations" : 1,
"num_checkpoints" : 10,
# Should really merge this into one enum-style variable
"display_in_parallel" : False,
"display_in_bfs" : False,
"use_fancy_lines" : True,
# TODO: Consider adding a "find_all_roots" flag, which could be turned off
# to only explore one of the two candidate subrectangles when both are viable
@ -955,7 +1014,11 @@ class EquationSolver2d(ColorMappedObjectsScene):
"show_winding_numbers" : True,
# Used for UhOhScene;
"manual_wind_override" : None
"manual_wind_override" : None,
"show_cursor" : False,
"linger_parameter" : 0.2,
}
def construct(self):
@ -976,11 +1039,14 @@ class EquationSolver2d(ColorMappedObjectsScene):
obj1.color_using_background_image(bg)
run_time_base = 1
run_time_with_lingering = run_time_base + 0.2
run_time_with_lingering = run_time_base + self.linger_parameter
base_rate = lambda t : t
linger_rate = squish_rate_func(lambda t : t, 0,
fdiv(run_time_base, run_time_with_lingering))
cursor_base = TextMobject("?")
cursor_base.scale(2)
# Helper functions for manual_wind_override
def head(m):
if m == None:
@ -998,7 +1064,7 @@ class EquationSolver2d(ColorMappedObjectsScene):
print "Solver at depth: " + str(cur_depth)
if cur_depth >= self.num_iterations:
return empty_animation
return EquationSolver2dNode(empty_animation)
def draw_line_return_wind(start, end, start_wind, should_linger = False, draw_line = True):
alpha_winder = make_alpha_winder(clockwise_val_func, start, end, self.num_checkpoints)
@ -1050,6 +1116,24 @@ class EquationSolver2d(ColorMappedObjectsScene):
draw_line = i in sides_to_draw)
anim = Succession(anim, next_anim)
if self.show_cursor:
cursor = cursor_base.copy()
center_x, center_y = rect.get_center()
width = rect.get_width()
height = rect.get_height()
cursor.move_to(num_plane.coords_to_point(center_x, center_y))
cursor.scale(min(width, height))
# Do a quick FadeIn, wait, and quick FadeOut on the cursor, matching rectangle-drawing time
cursor_anim = Succession(
FadeIn(cursor, run_time = 0.1),
Animation(cursor, run_time = 3.8),
FadeOut(cursor, run_time = 0.1)
)
anim = AnimationGroup(anim, cursor_anim)
total_wind = head(manual_wind_override) or round(wind_so_far)
if total_wind == 0:
@ -1064,14 +1148,14 @@ class EquationSolver2d(ColorMappedObjectsScene):
# their "Nothing here" status?
# Or draw a large X or something
fill_rect = polygonObject = Polygon(*points, fill_opacity = 0.8, color = DARK_GREY)
return Succession(anim, FadeIn(fill_rect))
return EquationSolver2dNode(Succession(anim, FadeIn(fill_rect)))
else:
(sub_rect1, sub_rect2) = rect.splits_on_dim(dim_to_split)
if dim_to_split == 0:
sub_rect_and_sides = [(sub_rect1, 1), (sub_rect2, 3)]
else:
sub_rect_and_sides = [(sub_rect1, 2), (sub_rect2, 0)]
sub_anims = [
children = [
Animate2dSolver(
cur_depth = cur_depth + 1,
rect = sub_rect,
@ -1084,14 +1168,8 @@ class EquationSolver2d(ColorMappedObjectsScene):
mid_line_coords = rect.split_line_on_dim(dim_to_split)
mid_line_points = [num_plane.coords_to_point(x, y) + 2 * IN for (x, y) in mid_line_coords]
mid_line = DashedLine(*mid_line_points)
if self.display_in_parallel:
recursive_anim = AnimationGroup(*sub_anims)
else:
recursive_anim = Succession(*sub_anims)
return Succession(anim,
ShowCreation(mid_line),
recursive_anim
)
return EquationSolver2dNode(Succession(anim, ShowCreation(mid_line)), children)
lower_x = self.initial_lower_x
upper_x = self.initial_upper_x
@ -1105,7 +1183,7 @@ class EquationSolver2d(ColorMappedObjectsScene):
print "Starting to compute anim"
anim = self.anim = Animate2dSolver(
node = Animate2dSolver(
cur_depth = 0,
rect = rect,
dim_to_split = 0,
@ -1115,6 +1193,13 @@ class EquationSolver2d(ColorMappedObjectsScene):
print "Done computing anim"
if self.display_in_parallel:
anim = node.display_in_parallel()
elif self.display_in_bfs:
anim = node.display_in_bfs()
else:
anim = node.display_in_series()
# Keep timing details here in sync with details above
rect_points = [
rect.get_top_left(),
@ -1146,6 +1231,11 @@ class EquationSolver2d(ColorMappedObjectsScene):
self.wait()
# In case we wish to reference these later, as had been done in the
# WindingNumber_G/SearchSpacePerimeterVsArea scene at one point
self.node = node
self.anim = anim
# TODO: Perhaps have option for bullets (pulses) to fade out and in at ends of line, instead of
# jarringly popping out and in?
#
@ -1763,10 +1853,12 @@ class DemonstrateColorMapping(ColorMappedObjectsScene):
self.play(ReplacementTransform(background, bright_background))
self.wait()
# Everything in this is manually kept in sync with WindingNumber_G/TransitionFromPathsToBoundaries
class LoopSplitScene(ColorMappedObjectsScene):
CONFIG = {
# TODO: Change this to something more colorful down the midline
"func" : plane_func_by_wind_spec((0.1/2, 1.1/2, 1), (-3.1/2, -1.3/2, 2), (1.8/2, -2.1/2, -1)),
"func" : plane_func_by_wind_spec(
(-2, 0, 2), (2, 0, 1)
),
"use_fancy_lines" : True,
}
@ -1776,7 +1868,7 @@ class LoopSplitScene(ColorMappedObjectsScene):
num_bullets = 4,
pulse_time = 1,
**kwargs):
line = Line(start, end, color = WHITE, **kwargs)
line = Line(start, end, color = WHITE, stroke_width = 4, **kwargs)
if self.use_fancy_lines:
line.color_using_background_image(self.background_image_file)
anim = LinePulser(
@ -1815,35 +1907,24 @@ class LoopSplitScene(ColorMappedObjectsScene):
def pl(a, b):
return self.PulsedLine(a, b, default_bullet)
faded = 0.3
def indicate_circle(x, double_horizontal_stretch = False):
circle = Circle(color = WHITE, radius = 2 * np.sqrt(2))
circle.move_to(x.get_center())
# Workaround for FadeOut/FadeIn not playing well with ContinualAnimations due to
# Transforms making copies no longer identified with the ContinualAnimation's tracked mobject
def fader_bullet(start, end, mob):
return UpdateFromAlphaFunc(mob, lambda m, a : m.set_fill(opacity = interpolate(start, end, a)))
def fader_blist(start, end, blist):
return map(lambda b : fader_bullet(start, end, b), blist)
def fader_widthmob(start, end, mob):
return UpdateFromAlphaFunc(mob, lambda m, a : m.set_stroke(width = interpolate(start, end, a) * stroke_width))
def indicate_circle(x):
circle = Circle(color = WHITE)
circle.surround(x)
# if x.get_slope == 0:
# circle.stretch(0, 0.3)
# else:
# circle.stretch(0.3, 0)
circle.stretch(0.3, 0.3)
if x.get_slope() == 0:
circle.stretch(0.2, 1)
if double_horizontal_stretch:
circle.stretch(2, 0)
else:
circle.stretch(0.2, 0)
return circle
tl_line_trip = pl(tl, tm)
left_mid_trip = pl(tm, bm)
midline_left_trip = pl(tm, bm)
bl_line_trip = pl(bm, bl)
left_line_trip = pl(bl, tl)
left_square_trips = [tl_line_trip, left_mid_trip, bl_line_trip, left_line_trip]
left_square_trips = [tl_line_trip, midline_left_trip, bl_line_trip, left_line_trip]
left_square_lines = [x[0] for x in left_square_trips]
left_square_lines_vmobject = VMobject(*left_square_lines)
left_square_bullets = [x[1] for x in left_square_trips]
@ -1852,54 +1933,93 @@ class LoopSplitScene(ColorMappedObjectsScene):
tr_line_trip = pl(tm, tr)
right_line_trip = pl(tr, br)
br_line_trip = pl(br, bm)
right_midline_trip = pl(bm, tm)
midline_right_trip = pl(bm, tm)
right_square_trips = [tr_line_trip, right_line_trip, br_line_trip, right_midline_trip]
right_square_trips = [tr_line_trip, right_line_trip, br_line_trip, midline_right_trip]
right_square_lines = [x[0] for x in right_square_trips]
right_square_lines_vmobject = VMobject(*right_square_lines)
right_square_bullets = [x[1] for x in right_square_trips]
right_square_anims = [x[2] for x in right_square_trips]
# Hm... still some slight bug with this.
midline_trips = [midline_left_trip, midline_right_trip]
midline_lines = [x[0] for x in midline_trips]
midline_lines_vmobject = VMobject(*midline_lines)
midline_bullets = [x[1] for x in midline_trips]
midline_anims = [x[1] for x in midline_trips]
left_line = left_line_trip[0]
right_line = right_line_trip[0]
for b in left_square_bullets + right_square_bullets:
b.set_fill(opacity = 0)
faded = 0.3
# Workaround for FadeOut/FadeIn not playing well with ContinualAnimations due to
# Transforms making copies no longer identified with the ContinualAnimation's tracked mobject
def bullet_fade(start, end, mob):
return UpdateFromAlphaFunc(mob, lambda m, a : m.set_fill(opacity = interpolate(start, end, a)))
def bullet_list_fade(start, end, bullet_list):
return map(lambda b : bullet_fade(start, end, b), bullet_list)
def line_fade(start, end, mob):
return UpdateFromAlphaFunc(mob, lambda m, a : m.set_stroke(width = interpolate(start, end, a) * stroke_width))
def play_combined_fade(start, end, lines_vmobject, bullets):
self.play(
line_fade(start, end, lines_vmobject),
*bullet_list_fade(start, end, bullets)
)
def play_fade_left(start, end):
play_combined_fade(start, end, left_square_lines_vmobject, left_square_bullets)
def play_fade_right(start, end):
play_combined_fade(start, end, right_square_lines_vmobject, right_square_bullets)
def play_fade_mid(start, end):
play_combined_fade(start, end, midline_lines_vmobject, midline_bullets)
def flash_circles(circles):
self.play(LaggedStart(FadeIn, VGroup(circles)))
self.play(*map(FadeOut, circles))
self.add(left_square_lines_vmobject, right_square_lines_vmobject)
self.remove(*midline_lines)
self.wait()
self.play(ShowCreation(midline_lines[0]))
self.add(midline_lines_vmobject)
self.wait()
self.add(*left_square_anims)
self.play(fader_widthmob(0, 1, left_square_lines_vmobject), *fader_blist(0, 1, left_square_bullets))
self.play(line_fade(1, faded, right_square_lines_vmobject), *bullet_list_fade(0, 1, left_square_bullets))
self.wait()
flash_circles([indicate_circle(l) for l in left_square_lines])
self.play(line_fade(faded, 1, right_square_lines_vmobject), *bullet_list_fade(1, 0, left_square_bullets))
self.add(*right_square_anims)
self.play(fader_widthmob(0, 1, right_square_lines_vmobject), *fader_blist(0, 1, right_square_bullets))
self.play(fader_widthmob(1, faded, right_square_lines_vmobject), *fader_blist(1, faded, right_square_bullets))
# left_circlers = [indicate_circle(l) for l in left_square_lines]
# self.play(*map(FadeIn, left_circlers))
# self.play(*map(FadeOut, left_circlers))
self.play(ShowCreation(indicate_circle(left_square_lines[1])))
self.play(line_fade(1, faded, left_square_lines_vmobject), *bullet_list_fade(0, 1, right_square_bullets))
self.wait()
return
highlight_circle = Circle(color = WHITE) # Perhaps make this a dashed circle?
highlight_circle.surround(mid_lines)
highlight_circle.stretch(0.3, 0)
self.play(Indicate(mid_lines), ShowCreation(highlight_circle, run_time = 0.5))
flash_circles([indicate_circle(l) for l in right_square_lines])
self.play(line_fade(faded, 1, left_square_lines_vmobject), *bullet_list_fade(1, 0, right_square_bullets))
self.wait()
self.play(*bullet_list_fade(0, 1, left_square_bullets + right_square_bullets))
self.play(FadeOut(highlight_circle), FadeOut(mid_lines))
# Because FadeOut didn't remove the continual pulsers, we remove them manually
self.remove(mid_line_left[1], mid_line_right[1])
outside_circlers = [
indicate_circle(left_line),
indicate_circle(right_line),
indicate_circle(top_line, double_horizontal_stretch = True),
indicate_circle(bottom_line, double_horizontal_stretch = True)
]
flash_circles(outside_circlers)
# Brings loop back together; keep in sync with motions which bring loop apart above
# self.play(
# ApplyMethod(left_open_loop.shift, 2 * RIGHT),
# ApplyMethod(right_open_loop.shift, 2 * LEFT)
# )
inner_circle = indicate_circle(midline_lines[0])
self.play(FadeIn(inner_circle))
self.play(FadeOut(inner_circle), line_fade(1, 0, midline_lines_vmobject), *bullet_list_fade(1, 0, midline_bullets))
self.wait()
self.wait(3)
# Is there a way to abstract this into a general process to derive a new mapped scene from an old scene?
class LoopSplitSceneMapped(LoopSplitScene):
@ -1928,14 +2048,16 @@ class SolveX5MinusXMinus1(EquationSolver2d):
"use_fancy_lines" : True,
}
class SolveX5MinusXMinus1Parallel(EquationSolver2d):
class SolveX5MinusXMinus1Parallel(SolveX5MinusXMinus1):
CONFIG = {
"func" : plane_func_from_complex_func(lambda c : c**5 - c - 1),
"num_iterations" : 5,
"use_fancy_lines" : True,
"display_in_parallel" : True
}
class SolveX5MinusXMinus1BFS(SolveX5MinusXMinus1):
CONFIG = {
"display_in_bfs" : True
}
class PreviewClip(EquationSolver2d):
CONFIG = {
"func" : example_plane_func,
@ -1944,6 +2066,23 @@ class PreviewClip(EquationSolver2d):
"use_fancy_lines" : True,
}
class QuickPreview(PreviewClip):
CONFIG = {
"num_iterations" : 3,
"display_in_parallel" : False,
"display_in_bfs" : True,
"show_cursor" : True
}
class LongEquationSolver(EquationSolver2d):
CONFIG = {
"func" : example_plane_func,
"num_iterations" : 10,
"display_in_bfs" : True,
"linger_parameter" : 0.4,
"show_cursor" : True,
}
# TODO: Borsuk-Ulam visuals
# Note: May want to do an ordinary square scene, then MappingCamera it into a circle
# class BorsukUlamScene(PiWalker):
@ -2468,6 +2607,12 @@ class OneFifthTwoFifthWinder(SpecifiedWinder):
"step_size" : 0.01,
"show_num_plane" : False,
"step_run_time" : 6,
"num_decimal_points" : 2,
}
class OneFifthOneFifthWinderWithReset(OneFifthTwoFifthWinder):
CONFIG = {
"wind_reset_indices" : [1]
}
class OneFifthTwoFifthWinderOdometer(OneFifthTwoFifthWinder):
@ -2500,6 +2645,8 @@ class CWColorWalk(PiWalkerRect):
"walk_width" : 2,
"walk_height" : 2,
"draw_lines" : False,
"display_wind" : False,
"step_run_time" : 2
}
class CWColorWalkOdometer(CWColorWalk):
@ -2521,7 +2668,8 @@ class CCWColorWalkOdometer(CCWColorWalk):
class ThreeTurnWalker(PiWalkerRect):
CONFIG = {
"func" : plane_func_from_complex_func(lambda c: c**3 * complex(1, 1)**3),
"double_up" : True
"double_up" : True,
"wind_reset_indices" : [4]
}
class ThreeTurnWalkerOdometer(ThreeTurnWalker):
@ -2561,6 +2709,7 @@ class ZeroTurnWalkerOdometer(ZeroTurnWalker):
class NegOneTurnWalker(PiWalkerRect):
CONFIG = {
"step_run_time" : 2,
"func" : plane_func_by_wind_spec((0, 0, -1))
}

View File

@ -159,7 +159,11 @@ class Circle(Arc):
def surround(self, mobject, dim_to_match = 0, stretch = False, buffer_factor = 1.2):
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
# Something goes wrong here when surrounding lines?
# TODO: Figure out and fix
self.replace(mobject, dim_to_match, stretch)
self.scale_to_fit_width(np.sqrt(mobject.get_width()**2 + mobject.get_height()**2))
self.scale(buffer_factor)