diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 67fa2471..03f618a5 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -206,11 +206,7 @@ class Camera(object): else: method = Mobject.get_family return remove_list_redundancies(list( - it.chain(*[ - method(m) - for m in mobjects - if not (isinstance(m, VMobject) and m.is_subpath) - ]) + it.chain(*[method(m) for m in mobjects]) )) def get_mobjects_to_display( @@ -324,32 +320,61 @@ class Camera(object): self.display_vectorized(vmobject, ctx) def display_vectorized(self, vmobject, ctx): - if vmobject.is_subpath: - # Subpath vectorized mobjects are taken care - # of by their parent - return self.set_cairo_context_path(ctx, vmobject) self.apply_stroke(ctx, vmobject, background=True) self.apply_fill(ctx, vmobject) self.apply_stroke(ctx, vmobject) return self + # def old_set_cairo_context_path(self, ctx, vmobject): + # ctx.new_path() + # for vmob in it.chain([vmobject], vmobject.get_subpath_mobjects()): + # points = self.transform_points_pre_display( + # vmob, vmob.points + # ) + # if np.any(np.isnan(points)) or np.any(points == np.inf): + # # TODO, print some kind of warning about + # # mobject having invalid points? + # points = np.zeros((1, 3)) + # ctx.new_sub_path() + # ctx.move_to(*points[0][:2]) + # for p0, p1, p2 in zip(points[1::3], points[2::3], points[3::3]): + # ctx.curve_to(*p0[:2], *p1[:2], *p2[:2]) + # if vmob.is_closed(): + # ctx.close_path() + # return self + def set_cairo_context_path(self, ctx, vmobject): + # self.old_set_cairo_context_path(ctx, vmobject) + # return + + points = vmobject.get_points() + if len(points) == 0: + return + elif np.any(np.isnan(points)) or np.any(points == np.inf): + # TODO, print some kind of warning about + # mobject having invalid points? + points = np.zeros((1, 3)) + + def should_start_new_path(last_p3, p0): + if last_p3 is None: + return True + else: + return not vmobject.consider_points_equals( + last_p3, p0 + ) + + last_p3 = None + quads = vmobject.get_all_cubic_bezier_point_tuples() ctx.new_path() - for vmob in it.chain([vmobject], vmobject.get_subpath_mobjects()): - points = self.transform_points_pre_display( - vmob, vmob.points - ) - if np.any(np.isnan(points)) or np.any(points == np.inf): - points = np.zeros((1, 3)) - ctx.new_sub_path() - ctx.move_to(*points[0][:2]) - for triplet in zip(points[1::3], points[2::3], points[3::3]): - ctx.curve_to(*it.chain(*[ - point[:2] for point in triplet - ])) - if vmob.is_closed(): - ctx.close_path() + for p0, p1, p2, p3 in quads: + if should_start_new_path(last_p3, p0): + ctx.new_sub_path() + ctx.move_to(*p0[:2]) + ctx.curve_to(*p1[:2], *p2[:2], *p3[:2]) + last_p3 = p3 + if vmobject.is_closed(): + ctx.close_path() return self def set_cairo_context_color(self, ctx, rgbas, vmobject): diff --git a/manimlib/camera/mapping_camera.py b/manimlib/camera/mapping_camera.py index d4a0ea15..c80c91cf 100644 --- a/manimlib/camera/mapping_camera.py +++ b/manimlib/camera/mapping_camera.py @@ -12,7 +12,7 @@ from manimlib.utils.config_ops import digest_config class MappingCamera(Camera): CONFIG = { "mapping_func": lambda p: p, - "min_anchor_points": 50, + "min_num_curves": 50, "allow_object_intrusion": False } @@ -27,8 +27,8 @@ class MappingCamera(Camera): mobject_copies = [mobject.copy() for mobject in mobjects] for mobject in mobject_copies: if isinstance(mobject, VMobject) and \ - 0 < mobject.get_num_anchor_points() < self.min_anchor_points: - mobject.insert_n_anchor_points(self.min_anchor_points) + 0 < mobject.get_num_curves() < self.min_num_curves: + mobject.insert_n_curves(self.min_num_curves) Camera.capture_mobjects( self, mobject_copies, include_submobjects=False, diff --git a/manimlib/mobject/coordinate_systems.py b/manimlib/mobject/coordinate_systems.py index 58c0798c..4b98c78b 100644 --- a/manimlib/mobject/coordinate_systems.py +++ b/manimlib/mobject/coordinate_systems.py @@ -324,12 +324,12 @@ class NumberPlane(VMobject): arrow = Arrow(ORIGIN, point, **kwargs) return arrow - def prepare_for_nonlinear_transform(self, num_inserted_anchor_points=50): + def prepare_for_nonlinear_transform(self, num_inserted_curves=50): for mob in self.family_members_with_points(): - num_anchors = mob.get_num_anchor_points() - if num_inserted_anchor_points > num_anchors: - mob.insert_n_anchor_points( - num_inserted_anchor_points - num_anchors) + num_curves = mob.get_num_curves() + if num_inserted_curves > num_curves: + mob.insert_n_curves( + num_inserted_curves - num_curves) mob.make_smooth() return self diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 08d895d3..c23db8df 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -43,12 +43,16 @@ class Arc(VMobject): # Appropriate tangent lines to the circle d_theta = self.angle / (self.num_anchors - 1.0) tangent_vectors = np.zeros(anchors.shape) + # Rotate all 90 degress, via (x, y) -> (-y, x) tangent_vectors[:, 1] = anchors[:, 0] tangent_vectors[:, 0] = -anchors[:, 1] + # Use tangent vectors to deduce anchors handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1] handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:] self.set_anchors_and_handles( - anchors, handles1, handles2 + anchors[:-1], + handles1, handles2, + anchors[1:], ) self.scale(self.radius, about_point=ORIGIN) self.shift(self.arc_center) @@ -250,9 +254,9 @@ class AnnularSector(VMobject): for alpha in np.linspace(0, 1, 4) ]) self.points = np.array(arc1.points) - self.add_control_points(a1_to_a2_points[1:]) - self.add_control_points(arc2.points[1:]) - self.add_control_points(a2_to_a1_points[1:]) + self.add_cubic_bezier_curve(*a1_to_a2_points[1:]) + self.add_cubic_bezier_curve(*arc2.points[1:]) + self.add_cubic_bezier_curve(*a2_to_a1_points[1:]) def get_arc_center(self): first_point = self.points[0] diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index ea38891d..974d988d 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -331,7 +331,7 @@ class VMobjectFromSVGPathstring(VMobject): re.split(pattern, self.path_string)[1:] )) # Which mobject should new points be added to - self.growing_path = self + self = self for command, coord_string in pairs: self.handle_command(command, coord_string) # people treat y-coordinate differently @@ -342,28 +342,23 @@ class VMobjectFromSVGPathstring(VMobject): 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 + points = self.points new_points = self.string_to_points(coord_string) - if command == "M": # moveto - if isLower and len(points) > 0: - new_points[0] += points[-1] - if len(points) > 0: - self.growing_path = self.add_subpath(new_points[:1]) - else: - self.growing_path.start_at(new_points[0]) - - if len(new_points) <= 1: - return - - points = self.growing_path.points - new_points = new_points[1:] - command = "L" - if isLower and len(points) > 0: new_points += points[-1] - if command in ["L", "H", "V"]: # lineto + if command == "M": # moveto + self.start_new_path(new_points[0]) + + if len(new_points) <= 1: + return + + # Huh? When does this come up? + points = self.points + new_points = new_points[1:] + command = "L" + elif command in ["L", "H", "V"]: # lineto if command == "H": new_points[0, 1] = points[-1, 1] elif command == "V": @@ -372,21 +367,27 @@ class VMobjectFromSVGPathstring(VMobject): new_points[0, 0] += points[-1, 1] new_points[0, 1] = new_points[0, 0] new_points[0, 0] = points[-1, 0] - new_points = new_points.repeat(3, axis=0) - elif command == "C": # curveto + self.add_line_to(new_points[0]) + return + + if command == "C": # curveto pass # Yay! No action required elif command in ["S", "T"]: # smooth curveto - handle1 = points[-1] + (points[-1] - points[-2]) - new_points = np.append([handle1], new_points, axis=0) - if command in ["Q", "T"]: # quadratic Bezier curve + self.add_smooth_curve_to(*new_points) + # handle1 = points[-1] + (points[-1] - points[-2]) + # new_points = np.append([handle1], new_points, axis=0) + return + elif command == "Q": # quadratic Bezier curve # TODO, this is a suboptimal approximation new_points = np.append([new_points[0]], new_points, axis=0) elif command == "A": # elliptical Arc raise Exception("Not implemented") elif command == "Z": # closepath - if not is_closed(points): - # Both handles and new anchor are the start - new_points = points[[0, 0, 0]] + if is_closed(points): + return + # Both handles and new anchor are the start + # TODO, is this needed? + new_points = points[[0, 0, 0]] # self.mark_paths_closed = True # Handle situations where there's multiple relative control points @@ -395,7 +396,7 @@ class VMobjectFromSVGPathstring(VMobject): new_points[i:i + 3] -= points[-1] new_points[i:i + 3] += new_points[i - 1] - self.growing_path.add_control_points(new_points) + self.add_cubic_bezier_curve_to(*new_points) def string_to_points(self, coord_string): numbers = string_to_numbers(coord_string) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index c0603356..4e0dee7d 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1,4 +1,5 @@ import itertools as it +import sys from colour import Color @@ -8,7 +9,7 @@ from manimlib.mobject.three_d_utils import get_3d_vmob_gradient_start_and_end_po from manimlib.utils.bezier import bezier from manimlib.utils.bezier import get_smooth_handle_points from manimlib.utils.bezier import interpolate -from manimlib.utils.bezier import is_closed +from manimlib.utils.bezier import integer_interpolate from manimlib.utils.bezier import partial_bezier_points from manimlib.utils.color import color_to_rgba from manimlib.utils.iterables import make_even @@ -17,6 +18,15 @@ from manimlib.utils.iterables import tuplify from manimlib.utils.simple_functions import clip_in_place +# TODO +# - Change cubic curve groups to have 4 points instead of 3 +# - Change sub_path idea accordingly +# - No more mark_paths_closed, instead have the camera test +# if last point in close to first point +# - Think about length of self.points. Always 0 or 1 mod 4? +# That's kind of weird. + + class VMobject(Mobject): CONFIG = { "fill_color": None, @@ -38,14 +48,16 @@ class VMobject(Mobject): "sheen_direction": UL, # Indicates that it will not be displayed, but # that it should count in parent mobject's path - "is_subpath": False, "close_new_points": False, - "mark_paths_closed": False, "propagate_style_to_family": False, "pre_function_handle_to_anchor_scale_factor": 0.01, "make_smooth_after_applying_functions": False, "background_image_file": None, "shade_in_3d": False, + # This is within a pixel + # TODO, what if you're rather zoomed in? + "tolerance_for_point_equality": 1e-6, + "n_points_per_cubic_curve": 4, } def get_group_class(self): @@ -364,63 +376,136 @@ class VMobject(Mobject): submob.z_index_group = self return self - # Drawing - def start_at(self, point): - if len(self.points) == 0: - self.points = np.zeros((1, 3)) - self.points[0] = point + # Points + def set_points(self, points): + self.points = np.array(points) return self - def add_control_points(self, control_points): - assert(len(control_points) % 3 == 0) - self.points = np.append( - self.points, - control_points, - axis=0 - ) - return self + def get_points(self): + return np.array(self.points) - def is_closed(self): - return is_closed(self.points) - - def set_anchors_and_handles(self, anchors, handles1, handles2): - assert(len(anchors) == len(handles1) + 1) - assert(len(anchors) == len(handles2) + 1) - total_len = 3 * (len(anchors) - 1) + 1 + def set_anchors_and_handles(self, anchors1, handles1, handles2, anchors2): + assert(len(anchors1) == len(handles1) == len(handles2) == len(anchors2)) + nppcc = self.n_points_per_cubic_curve # 4 + total_len = nppcc * len(anchors1) self.points = np.zeros((total_len, self.dim)) - self.points[0] = anchors[0] - arrays = [handles1, handles2, anchors[1:]] + arrays = [anchors1, handles1, handles2, anchors2] for index, array in enumerate(arrays): - self.points[index + 1::3] = array - return self.points + self.points[index::nppcc] = array + return self - def set_points_as_corners(self, points): - if len(points) <= 1: - return self - points = self.prepare_new_anchor_points(points) - self.set_anchors_and_handles(points, *[ - interpolate(points[:-1], points[1:], alpha) - for alpha in (1. / 3, 2. / 3) + def clear_points(self): + self.points = np.zeros((0, self.dim)) + + def append_points(self, new_points): + # TODO, check that number new points is a multiple of 4? + # or else that if len(self.points) % 4 == 1, then + # len(new_points) % 4 == 3? + self.points = np.append(self.points, new_points, axis=0) + return self + + def start_new_path(self, point): + # TODO, make sure that len(self.points) % 4 == 0? + self.append_points([point]) + return self + + def add_cubic_bezier_curve(self, anchor1, handle1, handle2, anchor2): + # TODO, check the len(self.points) % 4 == 0? + self.append_points([anchor1, handle1, handle2, anchor2]) + + def add_cubic_bezier_curve_to(self, handle1, handle2, anchor): + """ + Add cubic bezier curve to the path. + """ + self.throw_error_if_no_points() + new_points = [handle1, handle2, anchor] + if self.has_new_path_started(): + self.append_points(new_points) + else: + self.append_points([self.get_last_point()] + new_points) + + def add_line_to(self, point): + nppcc = self.n_points_per_cubic_curve + self.add_cubic_bezier_curve_to(*[ + interpolate(self.get_last_point(), point, a) + for a in np.linspace(0, 1, nppcc)[1:] ]) return self - def set_points_smoothly(self, points): - if len(points) <= 1: - return self - points = self.prepare_new_anchor_points(points) - h1, h2 = get_smooth_handle_points(points) - self.set_anchors_and_handles(points, h1, h2) + def add_smooth_curve_to(self, *points): + """ + If two points are passed in, the first is intepretted + as a handle, the second as an anchor + """ + if len(points) == 1: + handle2 = None + new_anchor = points[0] + elif len(points) == 2: + handle2, new_anchor = points + else: + name = sys._getframe(0).f_code.co_name + raise Exception("Only call {} with 1 or 2 points".format(name)) + + if self.has_new_path_started(): + self.add_line_to(new_anchor) + else: + self.throw_error_if_no_points() + last_h2, last_a2 = self.points[-2:] + last_tangent = (last_a2 - last_h2) + handle1 = last_a2 + last_tangent + if handle2 is None: + to_anchor_vect = new_anchor - last_a2 + new_tangent = rotate_vector( + last_tangent, PI, axis=to_anchor_vect + ) + handle2 = new_anchor - new_tangent + self.append_points([ + last_a2, handle1, handle2, new_anchor + ]) return self - def prepare_new_anchor_points(self, points): - if not isinstance(points, np.ndarray): - points = np.array(points) - if self.close_new_points and not is_closed(points): - points = np.append(points, [points[0]], axis=0) + # TODO, remove + # def add_control_points(self, control_points): + # assert(len(control_points) % 3 == 0) + # self.points = np.append( + # self.points, + # control_points, + # axis=0 + # ) + # return self + + def has_new_path_started(self): + nppcc = self.n_points_per_cubic_curve # 4 + return len(self.points) % nppcc == 1 + + def get_last_point(self): + return self.points[-1] + + def is_closed(self): + return self.consider_points_equals( + self.points[0], self.points[-1] + ) + + def has_no_points(self): + return len(self.points) == 0 + + def add_points_as_corners(self, points): + for point in points: + self.add_line_to(point) return points - def set_points(self, points): - self.points = np.array(points) + def set_points_as_corners(self, points): + self.clear_points() + self.start_new_path(points[0]) + self.add_points_as_corners(points[1:]) + return self + + def set_points_smoothly(self, points): + assert(len(points) > 1) + h1, h2 = get_smooth_handle_points(points) + self.set_anchors_and_handles( + points[:-1], h1, h2, points[1:] + ) return self def set_anchor_points(self, points, mode="smooth"): @@ -432,6 +517,7 @@ class VMobject(Mobject): raise Exception("Unknown mode") return self + # TODO, this will not work! def change_anchor_mode(self, mode): for submob in self.family_members_with_points(): anchors = submob.get_anchors() @@ -445,35 +531,18 @@ class VMobject(Mobject): return self.change_anchor_mode("corners") def add_subpath(self, points): - """ - A VMobject is meant to represent - a single "path", in the svg sense of the word. - However, one such path may really consist of separate - continuous components if there is a move_to command. - These other portions of the path will be treated as submobjects, - but will be tracked in a separate special list for when - it comes time to display. - """ - subpath_mobject = self.copy() # Really helps to be of the same class - subpath_mobject.submobjects = [] - subpath_mobject.is_subpath = True - subpath_mobject.set_points(points) - self.add(subpath_mobject) - return subpath_mobject + assert(len(points) % 4 == 0) + self.points = np.append(self.points, points, axis=0) + return self def append_vectorized_mobject(self, vectorized_mobject): new_points = list(vectorized_mobject.points) - if len(new_points) == 0: - return - if self.get_num_points() == 0: - self.start_at(new_points[0]) - self.add_control_points(new_points[1:]) - else: - self.add_control_points(2 * [new_points[0]] + new_points) - return self - def get_subpath_mobjects(self): - return [m for m in self.submobjects if hasattr(m, 'is_subpath') and m.is_subpath] + if self.has_new_path_started(): + # Remove last point, which is starting + # a new path + self.points = self.points[:-1] + self.append_points(new_points) def apply_function(self, function): factor = self.pre_function_handle_to_anchor_scale_factor @@ -495,95 +564,134 @@ class VMobject(Mobject): again. """ for submob in self.family_members_with_points(): - anchors, handles1, handles2 = submob.get_anchors_and_handles() - # print len(anchors), len(handles1), len(handles2) - a_to_h1 = handles1 - anchors[:-1] - a_to_h2 = handles2 - anchors[1:] - handles1 = anchors[:-1] + factor * a_to_h1 - handles2 = anchors[1:] + factor * a_to_h2 - submob.set_anchors_and_handles(anchors, handles1, handles2) + a1, h1, h2, a2 = submob.get_anchors_and_handles() + a1_to_h1 = h1 - a1 + a2_to_h2 = h2 - a2 + new_h1 = a1 + factor * a1_to_h1 + new_h2 = a2 + factor * a2_to_h2 + submob.set_anchors_and_handles(a1, new_h1, new_h2, a2) + return self + + # + def consider_points_equals(self, p0, p1): + return np.allclose( + p0, p1, + atol=self.tolerance_for_point_equality + ) # Information about line - def component_curves(self): - for n in range(self.get_num_anchor_points() - 1): - yield self.get_nth_curve(n) + def get_all_cubic_bezier_point_tuples(self): + points = self.get_points() + if self.has_new_path_started(): + points = points[:-1] + # TODO, Throw error if len(points) is > 1 and + # not divisible by 4 (i.e. n_points_per_cubic_curve) + nppcc = self.n_points_per_cubic_curve + return np.array([ + points[i:i + nppcc] + for i in range(0, len(points), nppcc) + ]) - def get_nth_curve(self, n): - return bezier(self.points[3 * n:3 * n + 4]) + def get_nth_curve_points(self, n): + assert(n < self.get_num_curves()) + nppcc = self.n_points_per_cubic_curve + return self.points[nppcc * n:nppcc * (n + 1)] - def get_num_anchor_points(self): - return (len(self.points) - 1) // 3 + 1 + def get_nth_curve_function(self, n): + return bezier(self.get_nth_curve_points(n)) + + def get_num_curves(self): + nppcc = self.n_points_per_cubic_curve + return len(self.points) // nppcc def point_from_proportion(self, alpha): - num_cubics = self.get_num_anchor_points() - 1 - interpoint_alpha = num_cubics * (alpha % (1. / num_cubics)) - index = min(3 * int(alpha * num_cubics), 3 * num_cubics) - cubic = bezier(self.points[index:index + 4]) - return cubic(interpoint_alpha) + num_cubics = self.get_num_curves() + n, residue = integer_interpolate(0, num_cubics, alpha) + curve = self.get_nth_curve_function(n) + return curve(residue) def get_anchors_and_handles(self): + """ + returns anchors1, handles1, handles2, anchors2, + where (anchors1[i], handles1[i], handles2[i], anchors2[i]) + will be four points defining a cubic bezier curve + for any i in range(0, len(anchors1)) + """ + nppcc = self.n_points_per_cubic_curve return [ - self.points[i::3] - for i in range(3) + self.points[i::nppcc] + for i in range(nppcc) ] + def get_start_anchors(self): + return self.points[0::self.n_points_per_cubic_curve] + + def get_end_anchors(self): + nppcc = self.n_points_per_cubic_curve + return self.points[nppcc - 1::nppcc] + def get_anchors(self): - return self.points[::3] + return np.array(list(it.chain(*zip( + self.get_start_anchors(), + self.get_end_anchors(), + )))) def get_points_defining_boundary(self): return np.array(list(it.chain(*[ - sm.get_anchors() for sm in self.get_family() + sm.get_anchors() + for sm in self.get_family() ]))) # Alignment def align_points(self, vmobject): + # This will call back to align_points_with_larger Mobject.align_points(self, vmobject) self.align_rgbas(vmobject) - is_subpath = self.is_subpath or vmobject.is_subpath - self.is_subpath = vmobject.is_subpath = is_subpath - mark_closed = self.mark_paths_closed and vmobject.mark_paths_closed - self.mark_paths_closed = vmobject.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() - ) + lnc = larger_mobject.get_num_curves() + snc = self.get_num_curves() + self.insert_n_curves(lnc - snc) return self - def insert_n_anchor_points(self, n): - curr = self.get_num_anchor_points() - if curr == 0: - self.points = np.zeros((1, 3)) - n = n - 1 - if curr == 1: - self.points = np.repeat(self.points, 3 * n + 1, axis=0) - return self - points = np.array([self.points[0]]) - num_curves = curr - 1 - # Curves in self are buckets, and we need to know - # how many new anchor points to put into each one. - # Each element of index_allocation is like a bucket, - # and its value tells you the appropriate index of - # the smaller curve. - index_allocation = ( - np.arange(curr + n - 1) * num_curves) // (curr + n - 1) - for index in range(num_curves): - curr_bezier_points = self.points[3 * index:3 * index + 4] - num_inter_curves = sum(index_allocation == index) - alphas = np.linspace(0, 1, num_inter_curves + 1) - # alphas = np.arange(0, num_inter_curves+1)/float(num_inter_curves) - for a, b in zip(alphas, alphas[1:]): - new_points = partial_bezier_points( - curr_bezier_points, a, b + def insert_n_curves(self, n): + new_path_point = None + if self.has_new_path_started(): + new_path_point = self.get_last_point() + curr_curve_points = self.get_all_cubic_bezier_point_tuples() + curr_num = len(curr_curve_points) + target_num = curr_num + n + # This is an array with values ranging from 0 + # up to curr_num, with repeats such that + # it's total length is target_num. For example, + # with curr_num = 10, target_num = 15, this would + # be [0, 0, 1, 2, 2, 3, 4, 4, 5, 6, 6, 7, 8, 8, 9] + repeat_indices = (np.arange(target_num) * curr_num) // target_num + + # If the nth term of this list is k, it means + # that the nth curve of our path should be split + # into k pieces. In the above example, this would + # be [2, 1, 2, 1, 2, 1, 2, 1, 2, 1] + split_factors = [ + sum(repeat_indices == i) + for i in range(curr_num) + ] + + self.clear_points() + for points, sf in zip(curr_curve_points, split_factors): + # What was once a single cubic curve defined + # by "points" will now be broken into sf + # smaller cubic curves + alphas = np.linspace(0, 1, sf + 1) + for a1, a2 in zip(alphas, alphas[1:]): + self.append_points( + partial_bezier_points(points, a1, a2) ) - points = np.append( - points, new_points[1:], axis=0 - ) - self.set_points(points) + if new_path_point is not None: + self.append_points([new_path_point]) return self def align_rgbas(self, vmobject): @@ -604,11 +712,6 @@ class VMobject(Mobject): center = self.get_center() return VectorizedPoint(center) - def repeat_submobject(self, submobject): - if submobject.is_subpath: - return VectorizedPoint(submobject.points[0]) - return submobject.copy() - def interpolate_color(self, mobject1, mobject2, alpha): attrs = [ "fill_rgbas", @@ -628,40 +731,40 @@ class VMobject(Mobject): if alpha == 1.0: setattr(self, attr, getattr(mobject2, attr)) - def pointwise_become_partial(self, mobject, a, b): - assert(isinstance(mobject, VMobject)) + def pointwise_become_partial(self, vmobject, a, b): + assert(isinstance(vmobject, VMobject)) # Partial curve includes three portions: # - A middle section, which matches the curve exactly # - A start, which is some ending portion of an inner cubic # - An end, which is the starting portion of a later inner cubic if a <= 0 and b >= 1: - self.set_points(mobject.points) - self.mark_paths_closed = mobject.mark_paths_closed + self.set_points(vmobject.points) return self - self.mark_paths_closed = False - num_cubics = mobject.get_num_anchor_points() - 1 - lower_index = int(a * num_cubics) - upper_index = int(b * num_cubics) - points = np.array( - mobject.points[3 * lower_index:3 * upper_index + 4] - ) - if len(points) > 1: - a_residue = (num_cubics * a) % 1 - b_residue = (num_cubics * b) % 1 - if b == 1: - b_residue = 1 - elif lower_index == upper_index: - b_residue = (b_residue - a_residue) / (1 - a_residue) + bezier_quads = vmobject.get_all_cubic_bezier_point_tuples() + num_cubics = len(bezier_quads) - points[:4] = partial_bezier_points( - points[:4], a_residue, 1 - ) - points[-4:] = partial_bezier_points( - points[-4:], 0, b_residue - ) - self.set_points(points) + lower_index, lower_residue = integer_interpolate(0, num_cubics, a) + upper_index, upper_residue = integer_interpolate(0, num_cubics, b) + + self.clear_points() + self.append_points(partial_bezier_points( + bezier_quads[lower_index], lower_residue, 1 + )) + for quad in bezier_quads[lower_index + 1:upper_index]: + self.append_points(quad) + self.append_points(partial_bezier_points( + bezier_quads[upper_index], 0, upper_residue + )) return self + # Errors + def throw_error_if_no_points(self): + if self.has_no_points(): + message = "Cannot call VMobject.{}" +\ + "for a VMobject with no points" + caller_name = sys._getframe(1).f_code.co_name + raise Exception(message.format(caller_name)) + class VGroup(VMobject): def __init__(self, *args, **kwargs): diff --git a/manimlib/once_useful_constructs/fractals.py b/manimlib/once_useful_constructs/fractals.py index 0f5da995..0c40d1e1 100644 --- a/manimlib/once_useful_constructs/fractals.py +++ b/manimlib/once_useful_constructs/fractals.py @@ -292,12 +292,13 @@ class CircularFractal(SelfSimilarFractal): class JaggedCurvePiece(VMobject): - def insert_n_anchor_points(self, n): - if self.get_num_anchor_points() == 0: - self.points = np.zeros((1, 3)) + def insert_n_curves(self, n): + if self.get_num_curves() == 0: + self.set_points(np.zeros((1, 3))) anchors = self.get_anchors() - indices = np.linspace(0, len(anchors) - 1, n + len(anchors)) \ - .astype('int') + indices = np.linspace( + 0, len(anchors) - 1, n + len(anchors) + ).astype('int') self.set_points_as_corners(anchors[indices]) diff --git a/manimlib/utils/bezier.py b/manimlib/utils/bezier.py index 197f5606..ea4ded67 100644 --- a/manimlib/utils/bezier.py +++ b/manimlib/utils/bezier.py @@ -1,8 +1,7 @@ from scipy import linalg import numpy as np -from manimlib.utils.simple_functions import choose_using_cache -from manimlib.utils.space_ops import get_norm +from manimlib.utils.simple_functions import choose CLOSED_THRESHOLD = 0.001 @@ -10,7 +9,7 @@ CLOSED_THRESHOLD = 0.001 def bezier(points): n = len(points) - 1 return lambda t: sum([ - ((1 - t)**(n - k)) * (t**k) * choose_using_cache(n, k) * point + ((1 - t)**(n - k)) * (t**k) * choose(n, k) * point for k, point in enumerate(points) ]) @@ -41,6 +40,25 @@ def interpolate(start, end, alpha): return (1 - alpha) * start + alpha * end +def integer_interpolate(start, end, alpha): + """ + alpha is a float between 0 and 1. This returns + an integer between start and end (inclusive) representing + appropriate interpolation between them, along with a + "residue" representing a new proportion between the + returned integer and the next one of the + list. + + For example, if start=0, end=10, alpha=0.46, This + would return (4, 0.6). + """ + if alpha >= 1: + return (end - 1, 1.0) + value = int(interpolate(start, end, alpha)) + residue = ((end - start) * alpha) % 1 + return (value, residue) + + def mid(start, end): return (start + end) / 2.0 @@ -134,4 +152,4 @@ def diag_to_matrix(l_and_u, diag): def is_closed(points): - return get_norm(points[0] - points[-1]) < CLOSED_THRESHOLD + return np.allclose(points[0], points[-1]) diff --git a/manimlib/utils/simple_functions.py b/manimlib/utils/simple_functions.py index f567cf9a..797a09cc 100644 --- a/manimlib/utils/simple_functions.py +++ b/manimlib/utils/simple_functions.py @@ -15,11 +15,13 @@ def choose_using_cache(n, r): if n not in CHOOSE_CACHE: CHOOSE_CACHE[n] = {} if r not in CHOOSE_CACHE[n]: - CHOOSE_CACHE[n][r] = choose(n, r) + CHOOSE_CACHE[n][r] = choose(n, r, use_cache=False) return CHOOSE_CACHE[n][r] -def choose(n, r): +def choose(n, r, use_cache=True): + if use_cache: + return choose_using_cache(n, r) if n < r: return 0 if r == 0: diff --git a/old_projects/WindingNumber.py b/old_projects/WindingNumber.py index a2d7b1bd..b53f827b 100644 --- a/old_projects/WindingNumber.py +++ b/old_projects/WindingNumber.py @@ -1763,7 +1763,7 @@ class Initial2dFuncSceneWithoutMorphing(Initial2dFuncSceneBase): # Alternative to the above, manually implementing split screen with a morphing animation class Initial2dFuncSceneMorphing(Initial2dFuncSceneBase): CONFIG = { - "num_needed_anchor_points" : 10, + "num_needed_anchor_curves" : 10, } def setup(self): @@ -1781,8 +1781,8 @@ class Initial2dFuncSceneMorphing(Initial2dFuncSceneBase): def obj_draw(self, input_object): output_object = input_object.copy() - if input_object.get_num_anchor_points() < self.num_needed_anchor_points: - input_object.insert_n_anchor_points(self.num_needed_anchor_points) + if input_object.get_num_curves() < self.num_needed_anchor_curves: + input_object.insert_n_curves(self.num_needed_anchor_curves) output_object.apply_function(self.func) self.squash_onto_left(input_object) self.squash_onto_right(output_object) diff --git a/old_projects/WindingNumber_G.py b/old_projects/WindingNumber_G.py index 2e9bff09..6047bba3 100644 --- a/old_projects/WindingNumber_G.py +++ b/old_projects/WindingNumber_G.py @@ -1606,7 +1606,7 @@ class DirectionOfA2DFunctionAlongABoundary(InputOutputScene): input_plane.coords_to_point(2.5, -1.5), ) rect.replace(line, stretch = True) - rect.insert_n_anchor_points(50) + rect.insert_n_curves(50) rect.match_background_image_file(colorings[0]) rect_image = rect.copy() @@ -1815,7 +1815,7 @@ class ForeverNarrowingLoop(InputOutputScene): # circle circle = Circle(color = WHITE, radius = self.circle_start_radius) circle.flip(axis = RIGHT) - circle.insert_n_anchor_points(50) + circle.insert_n_curves(50) if self.start_around_target: circle.move_to(input_plane.coords_to_point(*self.target_coords)) else: @@ -2788,7 +2788,7 @@ class WindingNumbersInInputOutputContext(PathContainingZero): in_loop = Circle() in_loop.flip(RIGHT) # in_loop = Square(side_length = 2) - in_loop.insert_n_anchor_points(100) + in_loop.insert_n_curves(100) in_loop.move_to(self.input_plane.coords_to_point( *self.in_loop_center_coords )) diff --git a/old_projects/alt_calc.py b/old_projects/alt_calc.py index 2f414e7a..b2612fe4 100644 --- a/old_projects/alt_calc.py +++ b/old_projects/alt_calc.py @@ -45,7 +45,7 @@ class NumberlineTransformationScene(ZoomedScene): "color": BLUE, }, "output_line_config": {}, - "num_inserted_number_line_anchors": 20, + "num_inserted_number_line_curves": 20, "default_delta_x": 0.1, "default_sample_dot_radius": 0.07, "default_sample_dot_colors": [RED, YELLOW], @@ -76,8 +76,8 @@ class NumberlineTransformationScene(ZoomedScene): full_config = dict(self.number_line_config) full_config.update(added_config) number_line = NumberLine(**full_config) - number_line.main_line.insert_n_anchor_points( - self.num_inserted_number_line_anchors + number_line.main_line.insert_n_curves( + self.num_inserted_number_line_curves ) number_line.shift(zero_point - number_line.number_to_point(0)) number_lines.add(number_line) @@ -179,8 +179,8 @@ class NumberlineTransformationScene(ZoomedScene): self.moving_input_line = input_line_copy input_line_copy.remove(input_line_copy.numbers) # input_line_copy.set_stroke(width=2) - input_line_copy.main_line.insert_n_anchor_points( - self.num_inserted_number_line_anchors + input_line_copy.main_line.insert_n_curves( + self.num_inserted_number_line_curves ) return AnimationGroup( self.get_mapping_animation( @@ -311,7 +311,7 @@ class NumberlineTransformationScene(ZoomedScene): # Add miniature number_line mini_line = self.mini_line = Line(frame.get_left(), frame.get_right()) mini_line.scale(self.mini_line_scale_factor) - mini_line.insert_n_anchor_points(self.num_inserted_number_line_anchors) + mini_line.insert_n_curves(self.num_inserted_number_line_curves) mini_line.match_style(self.input_line.main_line) mini_line_copy = mini_line.copy() zcbr_group.add(mini_line_copy, mini_line) diff --git a/old_projects/basel/basel2.py b/old_projects/basel/basel2.py index 1fb14602..59f2f84e 100644 --- a/old_projects/basel/basel2.py +++ b/old_projects/basel/basel2.py @@ -1523,7 +1523,7 @@ class ShowLightInThreeDimensions(IntroduceScreen, ThreeDScene): screens = VGroup( Square(), RegularPolygon(8), - Circle().insert_n_anchor_points(25), + Circle().insert_n_curves(25), ) for screen in screens: screen.set_height(self.screen_height) @@ -3883,7 +3883,7 @@ class ThinkBackToHowAmazingThisIs(ThreeDScene): self.number_line = number_line def show_giant_circle(self): - self.number_line.main_line.insert_n_anchor_points(10000) + self.number_line.main_line.insert_n_curves(10000) everything = VGroup(*self.mobjects) circle = everything.copy() circle.move_to(ORIGIN) diff --git a/old_projects/borsuk.py b/old_projects/borsuk.py index 17561751..143ba724 100644 --- a/old_projects/borsuk.py +++ b/old_projects/borsuk.py @@ -2635,7 +2635,7 @@ class Test(Scene): def construct(self): randy = Randolph() necklace = Necklace() - necklace.insert_n_anchor_points(20) + necklace.insert_n_curves(20) # necklace.apply_function( # lambda (x, y, z) : x*RIGHT + (y + 0.1*x**2)*UP # ) diff --git a/old_projects/div_curl.py b/old_projects/div_curl.py index 2a1ec4f6..3f1c5fdf 100644 --- a/old_projects/div_curl.py +++ b/old_projects/div_curl.py @@ -843,7 +843,7 @@ class CylinderModel(Scene): shift_val = 0.1 * LEFT + 0.2 * UP scale_factor = get_norm(RIGHT - shift_val) movers = VGroup(self.warped_grid, self.unit_circle) - self.unit_circle.insert_n_anchor_points(50) + self.unit_circle.insert_n_curves(50) stream_lines = self.get_stream_lines() stream_lines.scale(scale_factor) @@ -4493,7 +4493,7 @@ class BroughtToYouBy(PiCreatureScene): math.move_to(self.pi_creatures) spiral = Line(0.5 * RIGHT, 0.5 * RIGHT + 70 * UP) - spiral.insert_n_anchor_points(1000) + spiral.insert_n_curves(1000) from old_projects.zeta import zeta spiral.apply_complex_function(zeta) step = 0.1 diff --git a/old_projects/efvgt.py b/old_projects/efvgt.py index b0336a49..0ea307d3 100644 --- a/old_projects/efvgt.py +++ b/old_projects/efvgt.py @@ -2964,7 +2964,7 @@ class ComplexExponentiationAdderHalf( ) line.set_color(YELLOW) for submob in line: - submob.insert_n_anchor_points(10) + submob.insert_n_curves(10) submob.make_smooth() circle = VGroup( Circle(), @@ -3069,7 +3069,7 @@ class ComplexExponentiationMultiplierHalf( line.set_color(YELLOW) line.shift(FRAME_X_RADIUS*LEFT) for submob in line: - submob.insert_n_anchor_points(10) + submob.insert_n_curves(10) submob.make_smooth() circle = VGroup( Circle(), @@ -3110,7 +3110,7 @@ class ComplexExponentiationMultiplierHalf( arc_line = Line(RIGHT, RIGHT+angle*UP) brace = Brace(arc_line, RIGHT, buff = 0) for submob in brace.family_members_with_points(): - submob.insert_n_anchor_points(10) + submob.insert_n_curves(10) curved_brace = brace.copy() curved_brace.shift(LEFT) curved_brace.apply_complex_function( diff --git a/old_projects/eoc/chapter1.py b/old_projects/eoc/chapter1.py index 9254025a..7ba7eadb 100644 --- a/old_projects/eoc/chapter1.py +++ b/old_projects/eoc/chapter1.py @@ -162,7 +162,7 @@ class CircleScene(PiCreatureScene): def get_unwrapped(self, ring, to_edge = LEFT, **kwargs): R = ring.R R_plus_dr = ring.R + ring.dR - n_anchors = ring.get_num_anchor_points() + n_anchors = ring.get_num_curves() result = VMobject() result.set_points_as_corners([ interpolate(np.pi*R_plus_dr*LEFT, np.pi*R_plus_dr*RIGHT, a) @@ -1115,7 +1115,7 @@ class GraphRectangles(CircleScene, GraphScene): ring.rect = rect - n_anchors = ring.get_num_anchor_points() + n_anchors = ring.get_num_curves() target = VMobject() target.set_points_as_corners([ interpolate(ORIGIN, DOWN, a) diff --git a/old_projects/eoc/chapter3.py b/old_projects/eoc/chapter3.py index 27866768..cdcc1c51 100644 --- a/old_projects/eoc/chapter3.py +++ b/old_projects/eoc/chapter3.py @@ -2218,7 +2218,7 @@ class IntroduceUnitCircleWithSine(GraphScene): color = YELLOW, ) line.shift(FRAME_X_RADIUS*RIGHT/3).to_edge(UP) - line.insert_n_anchor_points(10) + line.insert_n_curves(10) line.make_smooth() arc = Arc( diff --git a/old_projects/eoc/chapter5.py b/old_projects/eoc/chapter5.py index 5c597e33..0adcf241 100644 --- a/old_projects/eoc/chapter5.py +++ b/old_projects/eoc/chapter5.py @@ -1114,7 +1114,7 @@ class WhyPi(PiCreatureScene): circum.set_color(circle.get_color()) circum.scale(np.pi) circum.next_to(circle, DOWN, LARGE_BUFF) - circum.insert_n_anchor_points(circle.get_num_anchor_points()-2) + circum.insert_n_curves(circle.get_num_curves()-2) circum.make_jagged() pi = TexMobject("\\pi") pi.next_to(circum, UP) diff --git a/old_projects/eoc/old_chapter1.py b/old_projects/eoc/old_chapter1.py index 72eb56d1..ec4bab66 100644 --- a/old_projects/eoc/old_chapter1.py +++ b/old_projects/eoc/old_chapter1.py @@ -156,7 +156,7 @@ class CircleScene(PiCreatureScene): def get_unwrapped(self, ring, to_edge = LEFT, **kwargs): R = ring.R R_plus_dr = ring.R + ring.dR - n_anchors = ring.get_num_anchor_points() + n_anchors = ring.get_num_curves() result = VMobject() result.set_points_as_corners([ interpolate(np.pi*R_plus_dr*LEFT, np.pi*R_plus_dr*RIGHT, a) diff --git a/old_projects/eola/chapter3.py b/old_projects/eola/chapter3.py index 11af086f..6bf42848 100644 --- a/old_projects/eola/chapter3.py +++ b/old_projects/eola/chapter3.py @@ -500,7 +500,7 @@ class SneakyNonlinearTransformationExplained(SneakyNonlinearTransformation): FRAME_Y_RADIUS*LEFT+FRAME_Y_RADIUS*DOWN, FRAME_Y_RADIUS*RIGHT + FRAME_Y_RADIUS*UP ) - diag.insert_n_anchor_points(20) + diag.insert_n_curves(20) diag.change_anchor_mode("smooth") diag.set_color(YELLOW) self.play(ShowCreation(diag)) diff --git a/old_projects/fourier.py b/old_projects/fourier.py index 60ef57f1..dbebf759 100644 --- a/old_projects/fourier.py +++ b/old_projects/fourier.py @@ -2794,7 +2794,7 @@ class WriteComplexExponentialExpression(DrawFrequencyPlot): for t in (get_t(), TAU) ] for mob in arc, circle: - mob.insert_n_anchor_points(20) + mob.insert_n_curves(20) mob.set_stroke(RED, 4) mob.apply_function( lambda p : plane.number_to_point( diff --git a/old_projects/fractal_dimension.py b/old_projects/fractal_dimension.py index f9b70efd..6aaf3187 100644 --- a/old_projects/fractal_dimension.py +++ b/old_projects/fractal_dimension.py @@ -1442,8 +1442,8 @@ class DimensionOfQuadraticKoch(DimensionOfKoch): for order in range(2, self.koch_curve_order+1): new_curve = self.get_curve(order) new_curve.move_to(curve) - n_anchors = len(curve.get_anchors()) - curve.insert_n_anchor_points(6*n_anchors) + n_curve_parts = curve.get_num_curves() + curve.insert_n_curves(6 * n_curve_parts) curve.make_jagged() self.play(Transform(curve, new_curve, run_time = 2)) self.wait() diff --git a/old_projects/pi_day.py b/old_projects/pi_day.py index ce230f4d..c19110d0 100644 --- a/old_projects/pi_day.py +++ b/old_projects/pi_day.py @@ -483,7 +483,7 @@ class EulerWrites628(Scene): fill_opacity = 0.3, fill_color = GREEN, ) - rect.insert_n_anchor_points(20) + rect.insert_n_curves(20) rect.apply_function(lambda p : np.array([p[0], p[1] - 0.005*p[0]**2, p[2]])) rect.rotate(0.012*TAU) rect.move_to(image) diff --git a/old_projects/sphere_area.py b/old_projects/sphere_area.py index 2f228a82..9dbb6558 100644 --- a/old_projects/sphere_area.py +++ b/old_projects/sphere_area.py @@ -3206,7 +3206,7 @@ class SecondProof(SpecialThreeDScene): ring.set_fill(color, opacity=1) ring.set_stroke(color, width=0.5, opacity=1) for piece in ring: - piece.insert_n_anchor_points(4) + piece.insert_n_curves(4) piece.on_sphere = True piece.points = np.array([ *piece.points[3:-1], diff --git a/old_projects/turbulence.py b/old_projects/turbulence.py index eea6c944..3cf10e08 100644 --- a/old_projects/turbulence.py +++ b/old_projects/turbulence.py @@ -134,7 +134,7 @@ class Chaos(Eddy): y * UP, y * UP + self.width * RIGHT, stroke_width=1 ) - line.insert_n_anchor_points(self.n_midpoints) + line.insert_n_curves(self.n_midpoints) line.total_time = random.random() delta_h = self.height / (self.n_lines - 1) @@ -876,7 +876,7 @@ class HighCurlFieldBreakingLayersLines(HighCurlFieldBreakingLayers): def get_line(self): line = Line(LEFT, RIGHT) - line.insert_n_anchor_points(500) + line.insert_n_curves(500) line.set_width(5) return line diff --git a/old_projects/uncertainty.py b/old_projects/uncertainty.py index c6a2b78f..354c0af4 100644 --- a/old_projects/uncertainty.py +++ b/old_projects/uncertainty.py @@ -2039,7 +2039,7 @@ class IntroduceDopplerRadar(Scene): pulse_graph.underlying_function(x), echo_graph.underlying_function(x), ]), - num_graph_points = echo_graph.get_num_anchor_points(), + num_graph_points = echo_graph.get_num_curves(), color = WHITE ) sum_graph.background_image_file = "blue_yellow_gradient" diff --git a/old_projects/wcat.py b/old_projects/wcat.py index a88ade66..0385e532 100644 --- a/old_projects/wcat.py +++ b/old_projects/wcat.py @@ -984,7 +984,7 @@ class DeformToInterval(ClosedLoopScene): interval.shift(2*DOWN) numbers = interval.get_number_mobjects(0, 1) line = Line(interval.get_left(), interval.get_right()) - line.insert_n_anchor_points(self.loop.get_num_anchor_points()) + line.insert_n_curves(self.loop.get_num_curves()) line.make_smooth() self.loop.scale(0.7) @@ -1058,7 +1058,7 @@ class RepresentPairInUnitSquare(ClosedLoopScene): interval.shift(LEFT) numbers = interval.get_number_mobjects(0, 1) line = Line(interval.get_left(), interval.get_right()) - line.insert_n_anchor_points(self.loop.get_num_anchor_points()) + line.insert_n_curves(self.loop.get_num_curves()) line.make_smooth() vert_interval = interval.copy() square = Square() @@ -1270,7 +1270,7 @@ class EndpointsGluedTogether(ClosedLoopScene): interval.shift(2*DOWN) numbers = interval.get_number_mobjects(0, 1) line = Line(interval.get_left(), interval.get_right()) - line.insert_n_anchor_points(self.loop.get_num_anchor_points()) + line.insert_n_curves(self.loop.get_num_curves()) line.make_smooth() self.loop.scale(0.7) diff --git a/old_projects/zeta.py b/old_projects/zeta.py index f36462a2..debaee77 100644 --- a/old_projects/zeta.py +++ b/old_projects/zeta.py @@ -37,7 +37,7 @@ class ZetaTransformationScene(ComplexTransformationScene): for line in mob.family_members_with_points(): #Find point of line cloest to 1 on C if not isinstance(line, Line): - line.insert_n_anchor_points(self.min_added_anchors) + line.insert_n_curves(self.min_added_anchors) continue p1 = line.get_start()+LEFT p2 = line.get_end()+LEFT @@ -47,14 +47,14 @@ class ZetaTransformationScene(ComplexTransformationScene): ) #See how big this line will become diameter = abs(zeta(complex(*closest_to_one[:2]))) - target_num_anchors = np.clip( + target_num_curves = np.clip( int(self.anchor_density*np.pi*diameter), self.min_added_anchors, self.max_added_anchors, ) - num_anchors = line.get_num_anchor_points() - if num_anchors < target_num_anchors: - line.insert_n_anchor_points(target_num_anchors-num_anchors) + num_curves = line.get_num_curves() + if num_curves < target_num_curves: + line.insert_n_curves(target_num_curves-num_curves) line.make_smooth() def add_extra_plane_lines_for_zeta(self, animate = False, **kwargs): @@ -2279,7 +2279,7 @@ class IntroduceAnglePreservation(VisualizingSSquared): color = YELLOW ) arc.shift(intersection_point) - arc.insert_n_anchor_points(10) + arc.insert_n_curves(10) arc.generate_target() input_z = complex(*arc.get_center()[:2]) scale_factor = abs(2*input_z)