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,