From cd29ff6e0d8e46930da9074e0e2c62799349e16b Mon Sep 17 00:00:00 2001 From: Jean Whitmore Date: Sun, 24 Feb 2019 11:57:02 -0600 Subject: [PATCH 1/3] Reverted Cleaned Up Arrow Implementation (commit d489b5df5c77...) --- manimlib/mobject/geometry.py | 277 ++++++++++++++--------------------- 1 file changed, 111 insertions(+), 166 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 14d67226..8a6b49ef 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -12,6 +12,7 @@ from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import angle_between_vectors +from manimlib.utils.space_ops import center_of_mass from manimlib.utils.space_ops import compass_directions from manimlib.utils.space_ops import line_intersection from manimlib.utils.space_ops import get_norm @@ -20,20 +21,14 @@ from manimlib.utils.space_ops import rotate_vector DEFAULT_DOT_RADIUS = 0.08 -DEFAULT_SMALL_DOT_RADIUS = 0.04 DEFAULT_DASH_LENGTH = 0.05 -DEFAULT_ARROW_TIP_LENGTH = 0.35 class TipableVMobject(VMobject): CONFIG = { - "tip_length": DEFAULT_ARROW_TIP_LENGTH, + "tip_length": 0.3, # TODO "normal_vector": OUT, - "tip_style": { - "fill_opacity": 1, - "stroke_width": 0, - } } """ Meant simply for shard functionality between @@ -41,120 +36,61 @@ class TipableVMobject(VMobject): """ def add_tip(self, tip_length=None, at_start=False): - tip = self.create_tip(tip_length, at_start) - self.reset_endpoints_based_on_tip(tip, at_start) - self.asign_tip_attr(tip, at_start) - self.add(tip) - return self - - def create_tip(self, tip_length=None, at_start=False): tip = self.get_unpositioned_tip(tip_length) - self.position_tip(tip, at_start) - return tip - - def get_unpositioned_tip(self, tip_length=None): - if tip_length is None: - tip_length = self.get_default_tip_length() - color = self.get_color() - style = { - "fill_color": color, - "stroke_color": color - } - style.update(self.tip_style) - tip = ArrowTip(length=tip_length, **style) - return tip - - def position_tip(self, tip, at_start=False): # Last two control points, defining both # the end, and the tangency direction if at_start: anchor = self.get_start() handle = self.get_first_handle() + self.start_tip = tip else: handle = self.get_last_handle() anchor = self.get_end() - tip.rotate( - angle_of_vector(handle - anchor) - - PI - tip.get_angle() - ) - tip.shift(anchor - tip.get_tip_point()) + self.tip = tip + tip.rotate(angle_of_vector(handle - anchor)) + tip.shift(anchor - tip.get_start()) + + self.reset_endpoints_based_on_tip(tip, at_start) + self.add(tip) + return self + + def get_unpositioned_tip(self, tip_length=None): + if tip_length is None: + tip_length = self.tip_length + tip = Triangle(start_angle=PI) + tip.match_style(self) + tip.set_fill(self.get_stroke_color(), opacity=1) + tip.set_height(tip_length) + tip.set_width(tip_length, stretch=True) return tip def reset_endpoints_based_on_tip(self, tip, at_start): - if self.get_length() == 0: - # Zero length, put_start_and_end_on wouldn't - # work - return self + tip_base = tip.point_from_proportion(0.5) if at_start: self.put_start_and_end_on( - tip.get_base(), self.get_end() + tip_base, self.get_end() ) else: self.put_start_and_end_on( - self.get_start(), tip.get_base(), + self.get_start(), tip_base, ) return self - def asign_tip_attr(self, tip, at_start): - if at_start: - self.start_tip = tip - else: - self.tip = tip - return self - - def get_tips(self): - result = VGroup() - if hasattr(self, "tip"): - result.add(self.tip) - if hasattr(self, "start_tip"): - result.add(self.start_tip) - return result - - def get_tip(self): - tips = self.get_tips() - if len(tips) == 0: - raise Exception("tip not found") - else: - return tips[0] - - def get_default_tip_length(self): - return self.tip_length - def get_first_handle(self): return self.points[1] def get_last_handle(self): return self.points[-2] - def get_end(self): - if self.has_tip(): - return self.tip.get_start() - else: - return VMobject.get_end(self) + # def get_end(self): + # if hasattr(self, "tip"): + # return self.tip[0].get_anchors()[0] + # else: + # return Line.get_end(self) - def get_start(self): - if self.has_start_tip(): - return self.start_tip.get_start() - else: - return VMobject.get_start(self) - - def has_tip(self): - return hasattr(self, "tip") and self.tip in self - - def has_start_tip(self): - return hasattr(self, "start_tip") and self.start_tip in self - - def pop_tips(self): - start, end = self.get_start_and_end() - result = VGroup() - if self.has_tip(): - result.add(self.tip) - self.remove(self.tip) - if self.has_start_tip(): - result.add(self.start_tip) - self.remove(self.start_tip) - self.put_start_and_end_on(start, end) - return result + # def get_start(self): + # if hasattr(self, "tip"): + # pass class Arc(TipableVMobject): @@ -302,12 +238,6 @@ class Dot(Circle): Circle.__init__(self, arc_center=point, **kwargs) -class SmallDot(Dot): - CONFIG = { - "radius": DEFAULT_SMALL_DOT_RADIUS, - } - - class Ellipse(Circle): CONFIG = { "width": 2, @@ -383,7 +313,7 @@ class Line(TipableVMobject): def __init__(self, start, end, **kwargs): digest_config(self, kwargs) - self.set_start_and_end_attrs(start, end) + self.set_start_and_end(start, end) VMobject.__init__(self, **kwargs) def generate_points(self): @@ -418,7 +348,7 @@ class Line(TipableVMobject): ) return self - def set_start_and_end_attrs(self, start, end): + def set_start_and_end(self, start, end): # If either start or end are Mobjects, this # gives their centers rough_start = self.pointify(start) @@ -542,61 +472,105 @@ class Arrow(Line): "stroke_width": 6, "buff": MED_SMALL_BUFF, "tip_width_to_length_ratio": 1, - "max_tip_length_to_length_ratio": 0.2, - "max_stroke_width_to_length_ratio": 6, + "max_tip_length_to_length_ratio": 0.35, + "max_stem_width_to_tip_width_ratio": 0.3, "preserve_tip_size_when_scaling": True, "rectangular_stem_width": 0.05, } def __init__(self, *args, **kwargs): Line.__init__(self, *args, **kwargs) - # TODO, should this be affected when - # Arrow.set_stroke is called? - self.initial_stroke_width = self.stroke_width + self.add_tip(tip_length=self.tip_length) + # self.init_colors() + + def init_tip(self): self.add_tip() - self.set_stroke_width_from_length() - def scale(self, factor, **kwargs): - has_tip = self.has_tip() - has_start_tip = self.has_start_tip() - if has_tip or has_start_tip: - self.pop_tips() + # def add_tip(self, add_at_end=True): + # tip = VMobject( + # close_new_points=True, + # mark_paths_closed=True, + # fill_color=self.color, + # fill_opacity=1, + # stroke_color=self.color, + # stroke_width=0, + # ) + # tip.add_at_end = add_at_end + # self.set_tip_points(tip, add_at_end, preserve_normal=False) + # self.add(tip) + # if not hasattr(self, 'tip'): + # self.tip = VGroup() + # self.tip.match_style(tip) + # self.tip.add(tip) + # return tip - VMobject.scale(self, factor, **kwargs) - self.set_stroke_width_from_length() + def set_tip_points( + self, tip, + add_at_end=True, + tip_length=None, + preserve_normal=True, + ): + if tip_length is None: + tip_length = self.tip_length + if preserve_normal: + normal_vector = self.get_normal_vector() + else: + normal_vector = self.normal_vector + line_length = get_norm(self.points[-1] - self.points[0]) + tip_length = min( + tip_length, self.max_tip_length_to_length_ratio * line_length + ) + + indices = (-2, -1) if add_at_end else (1, 0) + pre_end_point, end_point = [ + self.get_anchors()[index] + for index in indices + ] + vect = end_point - pre_end_point + perp_vect = np.cross(vect, normal_vector) + for v in vect, perp_vect: + if get_norm(v) == 0: + v[0] = 1 + v *= tip_length / get_norm(v) + ratio = self.tip_width_to_length_ratio + tip.set_points_as_corners([ + end_point, + end_point - vect + perp_vect * ratio / 2, + end_point - vect - perp_vect * ratio / 2, + ]) - if has_tip: - self.add_tip() - if has_start_tip: - self.add_tip(at_start=True) return self def get_normal_vector(self): - p0, p1, p2 = self.tip.get_start_anchors()[:3] - return normalize(np.cross(p2 - p1, p1 - p0)) + p0, p1, p2 = self.tip[0].get_anchors()[:3] + result = np.cross(p2 - p1, p1 - p0) + norm = get_norm(result) + if norm == 0: + return self.normal_vector + else: + return result / norm def reset_normal_vector(self): self.normal_vector = self.get_normal_vector() return self - def get_default_tip_length(self): - max_ratio = self.max_tip_length_to_length_ratio - return min( - self.tip_length, - max_ratio * self.get_length(), - ) + def get_tip(self): + return self.tip - def set_stroke_width_from_length(self): - max_ratio = self.max_stroke_width_to_length_ratio - self.set_stroke(width=min( - self.initial_stroke_width, - max_ratio * self.get_length(), - )) - return self + # def put_start_and_end_on(self, *args, **kwargs): + # Line.put_start_and_end_on(self, *args, **kwargs) + # self.set_tip_points(self.tip[0], preserve_normal=False) + # return self - # TODO, should this be the default for everything? - def copy(self): - return self.deepcopy() + # def scale(self, scale_factor, **kwargs): + # Line.scale(self, scale_factor, **kwargs) + # if self.preserve_tip_size_when_scaling: + # for t in self.tip: + # self.set_tip_points(t, add_at_end=t.add_at_end) + # return self + + # def copy(self): + # return self.deepcopy() class Vector(Arrow): @@ -696,35 +670,6 @@ class Triangle(RegularPolygon): RegularPolygon.__init__(self, n=3, **kwargs) -class ArrowTip(Triangle): - CONFIG = { - "fill_opacity": 1, - "stroke_width": 0, - "length": DEFAULT_ARROW_TIP_LENGTH, - "start_angle": PI, - } - - def __init__(self, **kwargs): - Triangle.__init__(self, **kwargs) - self.set_width(self.length) - self.set_height(self.length, stretch=True) - - def get_base(self): - return self.point_from_proportion(0.5) - - def get_tip_point(self): - return self.points[0] - - def get_vector(self): - return self.get_tip_point() - self.get_base() - - def get_angle(self): - return angle_of_vector(self.get_vector()) - - def get_length(self): - return get_norm(self.get_vector()) - - class Rectangle(Polygon): CONFIG = { "color": WHITE, From 61dbad5edd9041c9a1b4fad15eaa0e76942f17cf Mon Sep 17 00:00:00 2001 From: Jean Whitmore Date: Sun, 3 Mar 2019 07:42:00 -0600 Subject: [PATCH 2/3] Revert "Reverted Cleaned Up Arrow Implementation (commit d489b5df5c77...)" Undoing my revert of another commit because I found a fix. This reverts commit cd29ff6e0d8e46930da9074e0e2c62799349e16b. --- manimlib/mobject/geometry.py | 277 +++++++++++++++++++++-------------- 1 file changed, 166 insertions(+), 111 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 8a6b49ef..14d67226 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -12,7 +12,6 @@ from manimlib.utils.iterables import adjacent_pairs from manimlib.utils.simple_functions import fdiv from manimlib.utils.space_ops import angle_of_vector from manimlib.utils.space_ops import angle_between_vectors -from manimlib.utils.space_ops import center_of_mass from manimlib.utils.space_ops import compass_directions from manimlib.utils.space_ops import line_intersection from manimlib.utils.space_ops import get_norm @@ -21,14 +20,20 @@ from manimlib.utils.space_ops import rotate_vector DEFAULT_DOT_RADIUS = 0.08 +DEFAULT_SMALL_DOT_RADIUS = 0.04 DEFAULT_DASH_LENGTH = 0.05 +DEFAULT_ARROW_TIP_LENGTH = 0.35 class TipableVMobject(VMobject): CONFIG = { - "tip_length": 0.3, + "tip_length": DEFAULT_ARROW_TIP_LENGTH, # TODO "normal_vector": OUT, + "tip_style": { + "fill_opacity": 1, + "stroke_width": 0, + } } """ Meant simply for shard functionality between @@ -36,61 +41,120 @@ class TipableVMobject(VMobject): """ def add_tip(self, tip_length=None, at_start=False): + tip = self.create_tip(tip_length, at_start) + self.reset_endpoints_based_on_tip(tip, at_start) + self.asign_tip_attr(tip, at_start) + self.add(tip) + return self + + def create_tip(self, tip_length=None, at_start=False): tip = self.get_unpositioned_tip(tip_length) + self.position_tip(tip, at_start) + return tip + + def get_unpositioned_tip(self, tip_length=None): + if tip_length is None: + tip_length = self.get_default_tip_length() + color = self.get_color() + style = { + "fill_color": color, + "stroke_color": color + } + style.update(self.tip_style) + tip = ArrowTip(length=tip_length, **style) + return tip + + def position_tip(self, tip, at_start=False): # Last two control points, defining both # the end, and the tangency direction if at_start: anchor = self.get_start() handle = self.get_first_handle() - self.start_tip = tip else: handle = self.get_last_handle() anchor = self.get_end() - self.tip = tip - tip.rotate(angle_of_vector(handle - anchor)) - tip.shift(anchor - tip.get_start()) - - self.reset_endpoints_based_on_tip(tip, at_start) - self.add(tip) - return self - - def get_unpositioned_tip(self, tip_length=None): - if tip_length is None: - tip_length = self.tip_length - tip = Triangle(start_angle=PI) - tip.match_style(self) - tip.set_fill(self.get_stroke_color(), opacity=1) - tip.set_height(tip_length) - tip.set_width(tip_length, stretch=True) + tip.rotate( + angle_of_vector(handle - anchor) - + PI - tip.get_angle() + ) + tip.shift(anchor - tip.get_tip_point()) return tip def reset_endpoints_based_on_tip(self, tip, at_start): - tip_base = tip.point_from_proportion(0.5) + if self.get_length() == 0: + # Zero length, put_start_and_end_on wouldn't + # work + return self if at_start: self.put_start_and_end_on( - tip_base, self.get_end() + tip.get_base(), self.get_end() ) else: self.put_start_and_end_on( - self.get_start(), tip_base, + self.get_start(), tip.get_base(), ) return self + def asign_tip_attr(self, tip, at_start): + if at_start: + self.start_tip = tip + else: + self.tip = tip + return self + + def get_tips(self): + result = VGroup() + if hasattr(self, "tip"): + result.add(self.tip) + if hasattr(self, "start_tip"): + result.add(self.start_tip) + return result + + def get_tip(self): + tips = self.get_tips() + if len(tips) == 0: + raise Exception("tip not found") + else: + return tips[0] + + def get_default_tip_length(self): + return self.tip_length + def get_first_handle(self): return self.points[1] def get_last_handle(self): return self.points[-2] - # def get_end(self): - # if hasattr(self, "tip"): - # return self.tip[0].get_anchors()[0] - # else: - # return Line.get_end(self) + def get_end(self): + if self.has_tip(): + return self.tip.get_start() + else: + return VMobject.get_end(self) - # def get_start(self): - # if hasattr(self, "tip"): - # pass + def get_start(self): + if self.has_start_tip(): + return self.start_tip.get_start() + else: + return VMobject.get_start(self) + + def has_tip(self): + return hasattr(self, "tip") and self.tip in self + + def has_start_tip(self): + return hasattr(self, "start_tip") and self.start_tip in self + + def pop_tips(self): + start, end = self.get_start_and_end() + result = VGroup() + if self.has_tip(): + result.add(self.tip) + self.remove(self.tip) + if self.has_start_tip(): + result.add(self.start_tip) + self.remove(self.start_tip) + self.put_start_and_end_on(start, end) + return result class Arc(TipableVMobject): @@ -238,6 +302,12 @@ class Dot(Circle): Circle.__init__(self, arc_center=point, **kwargs) +class SmallDot(Dot): + CONFIG = { + "radius": DEFAULT_SMALL_DOT_RADIUS, + } + + class Ellipse(Circle): CONFIG = { "width": 2, @@ -313,7 +383,7 @@ class Line(TipableVMobject): def __init__(self, start, end, **kwargs): digest_config(self, kwargs) - self.set_start_and_end(start, end) + self.set_start_and_end_attrs(start, end) VMobject.__init__(self, **kwargs) def generate_points(self): @@ -348,7 +418,7 @@ class Line(TipableVMobject): ) return self - def set_start_and_end(self, start, end): + def set_start_and_end_attrs(self, start, end): # If either start or end are Mobjects, this # gives their centers rough_start = self.pointify(start) @@ -472,105 +542,61 @@ class Arrow(Line): "stroke_width": 6, "buff": MED_SMALL_BUFF, "tip_width_to_length_ratio": 1, - "max_tip_length_to_length_ratio": 0.35, - "max_stem_width_to_tip_width_ratio": 0.3, + "max_tip_length_to_length_ratio": 0.2, + "max_stroke_width_to_length_ratio": 6, "preserve_tip_size_when_scaling": True, "rectangular_stem_width": 0.05, } def __init__(self, *args, **kwargs): Line.__init__(self, *args, **kwargs) - self.add_tip(tip_length=self.tip_length) - # self.init_colors() - - def init_tip(self): + # TODO, should this be affected when + # Arrow.set_stroke is called? + self.initial_stroke_width = self.stroke_width self.add_tip() + self.set_stroke_width_from_length() - # def add_tip(self, add_at_end=True): - # tip = VMobject( - # close_new_points=True, - # mark_paths_closed=True, - # fill_color=self.color, - # fill_opacity=1, - # stroke_color=self.color, - # stroke_width=0, - # ) - # tip.add_at_end = add_at_end - # self.set_tip_points(tip, add_at_end, preserve_normal=False) - # self.add(tip) - # if not hasattr(self, 'tip'): - # self.tip = VGroup() - # self.tip.match_style(tip) - # self.tip.add(tip) - # return tip + def scale(self, factor, **kwargs): + has_tip = self.has_tip() + has_start_tip = self.has_start_tip() + if has_tip or has_start_tip: + self.pop_tips() - def set_tip_points( - self, tip, - add_at_end=True, - tip_length=None, - preserve_normal=True, - ): - if tip_length is None: - tip_length = self.tip_length - if preserve_normal: - normal_vector = self.get_normal_vector() - else: - normal_vector = self.normal_vector - line_length = get_norm(self.points[-1] - self.points[0]) - tip_length = min( - tip_length, self.max_tip_length_to_length_ratio * line_length - ) - - indices = (-2, -1) if add_at_end else (1, 0) - pre_end_point, end_point = [ - self.get_anchors()[index] - for index in indices - ] - vect = end_point - pre_end_point - perp_vect = np.cross(vect, normal_vector) - for v in vect, perp_vect: - if get_norm(v) == 0: - v[0] = 1 - v *= tip_length / get_norm(v) - ratio = self.tip_width_to_length_ratio - tip.set_points_as_corners([ - end_point, - end_point - vect + perp_vect * ratio / 2, - end_point - vect - perp_vect * ratio / 2, - ]) + VMobject.scale(self, factor, **kwargs) + self.set_stroke_width_from_length() + if has_tip: + self.add_tip() + if has_start_tip: + self.add_tip(at_start=True) return self def get_normal_vector(self): - p0, p1, p2 = self.tip[0].get_anchors()[:3] - result = np.cross(p2 - p1, p1 - p0) - norm = get_norm(result) - if norm == 0: - return self.normal_vector - else: - return result / norm + p0, p1, p2 = self.tip.get_start_anchors()[:3] + return normalize(np.cross(p2 - p1, p1 - p0)) def reset_normal_vector(self): self.normal_vector = self.get_normal_vector() return self - def get_tip(self): - return self.tip + def get_default_tip_length(self): + max_ratio = self.max_tip_length_to_length_ratio + return min( + self.tip_length, + max_ratio * self.get_length(), + ) - # def put_start_and_end_on(self, *args, **kwargs): - # Line.put_start_and_end_on(self, *args, **kwargs) - # self.set_tip_points(self.tip[0], preserve_normal=False) - # return self + def set_stroke_width_from_length(self): + max_ratio = self.max_stroke_width_to_length_ratio + self.set_stroke(width=min( + self.initial_stroke_width, + max_ratio * self.get_length(), + )) + return self - # def scale(self, scale_factor, **kwargs): - # Line.scale(self, scale_factor, **kwargs) - # if self.preserve_tip_size_when_scaling: - # for t in self.tip: - # self.set_tip_points(t, add_at_end=t.add_at_end) - # return self - - # def copy(self): - # return self.deepcopy() + # TODO, should this be the default for everything? + def copy(self): + return self.deepcopy() class Vector(Arrow): @@ -670,6 +696,35 @@ class Triangle(RegularPolygon): RegularPolygon.__init__(self, n=3, **kwargs) +class ArrowTip(Triangle): + CONFIG = { + "fill_opacity": 1, + "stroke_width": 0, + "length": DEFAULT_ARROW_TIP_LENGTH, + "start_angle": PI, + } + + def __init__(self, **kwargs): + Triangle.__init__(self, **kwargs) + self.set_width(self.length) + self.set_height(self.length, stretch=True) + + def get_base(self): + return self.point_from_proportion(0.5) + + def get_tip_point(self): + return self.points[0] + + def get_vector(self): + return self.get_tip_point() - self.get_base() + + def get_angle(self): + return angle_of_vector(self.get_vector()) + + def get_length(self): + return get_norm(self.get_vector()) + + class Rectangle(Polygon): CONFIG = { "color": WHITE, From 73a6c2400be756fc4e9c25374c6c3495facc7b3b Mon Sep 17 00:00:00 2001 From: Jean Whitmore Date: Sun, 3 Mar 2019 07:53:15 -0600 Subject: [PATCH 3/3] Fixed CurvedArrow constructor by moving get_length() to base class. I moved get_length() from the Line class to the shared base class, TipableVMobject. TESTED python3 -m manim manim_tutorial_P37.py MoreShapes -pl --- manimlib/mobject/geometry.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 14d67226..e7aa0d6a 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -138,6 +138,10 @@ class TipableVMobject(VMobject): else: return VMobject.get_start(self) + def get_length(self): + start, end = self.get_start_and_end() + return get_norm(start - end) + def has_tip(self): return hasattr(self, "tip") and self.tip in self @@ -439,10 +443,6 @@ class Line(TipableVMobject): return mob.get_boundary_point(direction) return np.array(mob_or_point) - def get_length(self): - start, end = self.get_start_and_end() - return get_norm(start - end) - def get_vector(self): return self.get_end() - self.get_start()