3b1b-manim/manimlib/mobject/geometry.py

798 lines
23 KiB
Python
Raw Normal View History

import numpy as np
from manimlib.constants import *
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.mobject.types.vectorized_mobject import DashedVMobject
from manimlib.utils.config_ops import digest_config
2019-02-07 21:57:40 -08:00
from manimlib.utils.iterables import adjacent_n_tuples
from manimlib.utils.iterables import adjacent_pairs
from manimlib.utils.simple_functions import fdiv
2020-02-18 22:32:57 -08:00
from manimlib.utils.simple_functions import clip
from manimlib.utils.space_ops import angle_of_vector
2019-02-07 21:57:40 -08:00
from manimlib.utils.space_ops import angle_between_vectors
from manimlib.utils.space_ops import compass_directions
from manimlib.utils.space_ops import find_intersection
from manimlib.utils.space_ops import get_norm
2019-02-07 10:59:04 -08:00
from manimlib.utils.space_ops import normalize
from manimlib.utils.space_ops import rotate_vector
2019-02-07 10:12:42 -08:00
DEFAULT_DOT_RADIUS = 0.08
DEFAULT_SMALL_DOT_RADIUS = 0.04
DEFAULT_DASH_LENGTH = 0.05
DEFAULT_ARROW_TIP_LENGTH = 0.35
DEFAULT_ARROW_TIP_WIDTH = 0.35
2019-02-07 10:12:42 -08:00
class TipableVMobject(VMobject):
"""
Meant for shared functionality between Arc and Line.
Functionality can be classified broadly into these groups:
* Adding, Creating, Modifying tips
- add_tip calls create_tip, before pushing the new tip
into the TipableVMobject's list of submobjects
- stylistic and positional configuration
* Checking for tips
- Boolean checks for whether the TipableVMobject has a tip
and a starting tip
* Getters
- Straightforward accessors, returning information pertaining
to the TipableVMobject instance's tip(s), its length etc
"""
CONFIG = {
"tip_config": {
"fill_opacity": 1,
"stroke_width": 0,
},
"normal_vector": OUT,
}
# Adding, Creating, Modifying tips
def add_tip(self, at_start=False, **kwargs):
"""
Adds a tip to the TipableVMobject instance, recognising
that the endpoints might need to be switched if it's
a 'starting tip' or not.
"""
tip = self.create_tip(at_start, **kwargs)
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, at_start=False, **kwargs):
"""
Stylises the tip, positions it spacially, and returns
the newly instantiated tip to the caller.
"""
tip = self.get_unpositioned_tip(**kwargs)
self.position_tip(tip, at_start)
return tip
def get_unpositioned_tip(self, **kwargs):
"""
Returns a tip that has been stylistically configured,
but has not yet been given a position in space.
"""
config = self.get_style()
config.update(self.tip_config)
config.update(kwargs)
return ArrowTip(**config)
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()
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())
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
if at_start:
start = tip.get_base()
end = self.get_end()
else:
start = self.get_start()
end = tip.get_base()
self.put_start_and_end_on(start, end)
return self
def asign_tip_attr(self, tip, at_start):
if at_start:
self.start_tip = tip
else:
self.tip = tip
return self
# Checking for tips
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
# Getters
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_tips(self):
"""
Returns a VGroup (collection of VMobjects) containing
the TipableVMObject instance's tips.
"""
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):
"""Returns the TipableVMobject instance's (first) tip,
otherwise throws an exception."""
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_start(self):
if self.has_start_tip():
return self.start_tip.get_start()
else:
return VMobject.get_start(self)
def get_length(self):
start, end = self.get_start_and_end()
return get_norm(start - end)
class Arc(TipableVMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
"radius": 1.0,
"n_components": 8,
"anchors_span_full_range": True,
"arc_center": ORIGIN,
}
2019-02-07 09:26:18 -08:00
def __init__(self, start_angle=0, angle=TAU / 4, **kwargs):
self.start_angle = start_angle
2018-01-19 17:30:39 -08:00
self.angle = angle
2016-04-17 00:31:38 -07:00
VMobject.__init__(self, **kwargs)
2015-06-10 22:00:35 -07:00
2020-02-11 19:55:00 -08:00
def init_points(self):
2019-02-07 09:26:18 -08:00
self.set_pre_positioned_points()
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
def set_pre_positioned_points(self):
samples = np.array([
np.cos(a) * RIGHT + np.sin(a) * UP
for a in np.linspace(
self.start_angle,
self.start_angle + self.angle,
2 * self.n_components + 1,
2016-04-17 00:31:38 -07:00
)
2018-01-17 15:12:39 -08:00
])
theta = self.angle / self.n_components
samples[1::2] /= np.cos(theta / 2)
self.points = np.zeros((3 * self.n_components, self.dim))
self.points[0::3] = samples[0:-1:2]
self.points[1::3] = samples[1::2]
self.points[2::3] = samples[2::2]
2018-01-22 16:30:07 -08:00
def get_arc_center(self):
2019-02-07 09:26:18 -08:00
"""
Looks at the normals to the first two
anchors, and finds their intersection points
"""
# First two anchors and handles
a1, h, a2 = self.points[:3]
2019-02-07 09:26:18 -08:00
# Tangent vectors
t1 = h - a1
t2 = h - a2
2019-02-07 09:26:18 -08:00
# Normals
n1 = rotate_vector(t1, TAU / 4)
n2 = rotate_vector(t2, TAU / 4)
return find_intersection(a1, n1, a2, n2)
def get_start_angle(self):
angle = angle_of_vector(self.get_start() - self.get_arc_center())
return angle % TAU
def get_stop_angle(self):
angle = angle_of_vector(self.get_end() - self.get_arc_center())
return angle % TAU
def move_arc_center_to(self, point):
2019-02-07 09:26:18 -08:00
self.shift(point - self.get_arc_center())
return self
class ArcBetweenPoints(Arc):
2019-02-07 21:57:40 -08:00
def __init__(self, start, end, angle=TAU / 4, **kwargs):
Arc.__init__(self, angle=angle, **kwargs)
2019-02-07 10:59:04 -08:00
if angle == 0:
self.set_points_as_corners([LEFT, RIGHT])
2019-02-07 21:57:40 -08:00
self.put_start_and_end_on(start, end)
class CurvedArrow(ArcBetweenPoints):
2019-02-07 09:26:18 -08:00
def __init__(self, start_point, end_point, **kwargs):
ArcBetweenPoints.__init__(self, start_point, end_point, **kwargs)
self.add_tip()
2019-02-07 09:26:18 -08:00
class CurvedDoubleArrow(CurvedArrow):
def __init__(self, start_point, end_point, **kwargs):
CurvedArrow.__init__(self, start_point, end_point, **kwargs)
2019-02-07 09:26:18 -08:00
self.add_tip(at_start=True)
2016-04-17 00:31:38 -07:00
class Circle(Arc):
2016-02-27 16:32:53 -08:00
CONFIG = {
"color": RED,
"close_new_points": True,
"anchors_span_full_range": False
}
2016-04-17 00:31:38 -07:00
def __init__(self, **kwargs):
2019-02-07 09:26:18 -08:00
Arc.__init__(self, 0, TAU, **kwargs)
def surround(self, mobject, dim_to_match=0, stretch=False, buff=MED_SMALL_BUFF):
# Ignores dim_to_match and stretch; result will always be a circle
# TODO: Perhaps create an ellipse class to handle singele-dimension stretching
2018-03-22 11:54:08 -07:00
self.replace(mobject, dim_to_match, stretch)
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
2019-02-07 09:26:18 -08:00
def point_at_angle(self, angle):
start_angle = self.get_start_angle()
return self.point_from_proportion(
(angle - start_angle) / TAU
)
class Dot(Circle):
2016-04-17 00:31:38 -07:00
CONFIG = {
2019-02-07 10:12:42 -08:00
"radius": DEFAULT_DOT_RADIUS,
"stroke_width": 0,
"fill_opacity": 1.0,
"color": WHITE
2016-04-17 00:31:38 -07:00
}
def __init__(self, point=ORIGIN, **kwargs):
2019-02-07 10:12:42 -08:00
Circle.__init__(self, arc_center=point, **kwargs)
self.lock_triangulation()
2015-06-10 22:00:35 -07:00
class SmallDot(Dot):
CONFIG = {
"radius": DEFAULT_SMALL_DOT_RADIUS,
}
2019-02-07 10:12:42 -08:00
class Ellipse(Circle):
2018-01-27 19:09:59 +01:00
CONFIG = {
"width": 2,
"height": 1
2018-01-27 19:09:59 +01:00
}
2019-02-07 10:12:42 -08:00
def __init__(self, **kwargs):
Circle.__init__(self, **kwargs)
self.set_width(self.width, stretch=True)
self.set_height(self.height, stretch=True)
2018-01-27 19:09:59 +01:00
2019-02-07 10:12:42 -08:00
class AnnularSector(Arc):
2018-01-17 15:12:39 -08:00
CONFIG = {
"inner_radius": 1,
"outer_radius": 2,
"angle": TAU / 4,
"start_angle": 0,
"fill_opacity": 1,
"stroke_width": 0,
"color": WHITE,
2018-01-17 15:12:39 -08:00
}
2020-02-11 19:55:00 -08:00
def init_points(self):
2019-02-07 10:12:42 -08:00
inner_arc, outer_arc = [
Arc(
start_angle=self.start_angle,
angle=self.angle,
radius=radius,
arc_center=self.arc_center,
)
for radius in (self.inner_radius, self.outer_radius)
]
outer_arc.reverse_points()
self.append_points(inner_arc.points)
self.add_line_to(outer_arc.points[0])
self.append_points(outer_arc.points)
self.add_line_to(inner_arc.points[0])
2018-01-19 17:30:39 -08:00
2018-01-18 15:06:38 -08:00
class Sector(AnnularSector):
2018-01-19 17:30:39 -08:00
CONFIG = {
"outer_radius": 1,
"inner_radius": 0
2018-01-19 17:30:39 -08:00
}
2018-01-18 15:06:38 -08:00
2018-01-18 15:06:38 -08:00
class Annulus(Circle):
CONFIG = {
"inner_radius": 1,
"outer_radius": 2,
"fill_opacity": 1,
"stroke_width": 0,
"color": WHITE,
"mark_paths_closed": False,
2018-01-18 15:06:38 -08:00
}
2020-02-11 19:55:00 -08:00
def init_points(self):
2018-01-18 15:06:38 -08:00
self.radius = self.outer_radius
outer_circle = Circle(radius=self.outer_radius)
2018-01-18 15:06:38 -08:00
inner_circle = Circle(radius=self.inner_radius)
2019-02-07 10:12:42 -08:00
inner_circle.reverse_points()
self.append_points(outer_circle.points)
self.append_points(inner_circle.points)
self.shift(self.arc_center)
2018-01-18 15:06:38 -08:00
class Line(TipableVMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
"buff": 0,
# Angle of arc specified here
"path_arc": None,
}
2019-04-06 11:52:23 -07:00
def __init__(self, start=LEFT, end=RIGHT, **kwargs):
digest_config(self, kwargs)
self.set_start_and_end_attrs(start, end)
2016-04-17 00:31:38 -07:00
VMobject.__init__(self, **kwargs)
2015-06-10 22:00:35 -07:00
2020-02-11 19:55:00 -08:00
def init_points(self):
2019-02-07 10:59:04 -08:00
if self.path_arc:
arc = ArcBetweenPoints(
self.start, self.end,
angle=self.path_arc
)
self.set_points(arc.points)
2017-04-26 16:21:38 -07:00
else:
2019-02-07 10:59:04 -08:00
self.set_points_as_corners([self.start, self.end])
self.account_for_buff()
def set_path_arc(self, new_value):
self.path_arc = new_value
2020-02-11 19:55:00 -08:00
self.init_points()
def account_for_buff(self):
2019-02-07 10:59:04 -08:00
if self.buff == 0:
return
#
if self.path_arc == 0:
length = self.get_length()
else:
length = self.get_arc_length()
#
2019-02-07 10:59:04 -08:00
if length < 2 * self.buff:
return
buff_proportion = self.buff / length
self.pointwise_become_partial(
self, buff_proportion, 1 - buff_proportion
)
return self
2017-04-26 16:21:38 -07:00
def set_start_and_end_attrs(self, start, end):
2019-02-07 10:59:04 -08:00
# If either start or end are Mobjects, this
# gives their centers
rough_start = self.pointify(start)
rough_end = self.pointify(end)
vect = normalize(rough_end - rough_start)
# Now that we know the direction between them,
# we can find the appropriate boundary point from
2019-02-07 10:59:04 -08:00
# start and end, if they're mobjects
self.start = self.pointify(start, vect)
self.end = self.pointify(end, -vect)
def pointify(self, mob_or_point, direction=None):
2016-04-17 00:31:38 -07:00
if isinstance(mob_or_point, Mobject):
2019-02-07 10:59:04 -08:00
mob = mob_or_point
if direction is None:
return mob.get_center()
else:
return mob.get_boundary_point(direction)
return mob_or_point
2016-04-17 00:31:38 -07:00
2019-04-06 11:52:23 -07:00
def put_start_and_end_on(self, start, end):
curr_start, curr_end = self.get_start_and_end()
if np.all(curr_start == curr_end):
# TODO, any problems with resetting
# these attrs?
self.start = start
self.end = end
2020-02-11 19:55:00 -08:00
self.init_points()
return super().put_start_and_end_on(start, end)
2019-04-06 11:52:23 -07:00
2017-08-30 13:16:08 -07:00
def get_vector(self):
return self.get_end() - self.get_start()
2018-07-17 12:44:18 -07:00
def get_unit_vector(self):
return normalize(self.get_vector())
def get_angle(self):
return angle_of_vector(self.get_vector())
2016-03-21 19:30:09 -07:00
2015-06-13 19:00:23 -07:00
def get_slope(self):
return np.tan(self.get_angle())
2015-06-13 19:00:23 -07:00
def set_angle(self, angle):
self.rotate(
angle - self.get_angle(),
about_point=self.get_start(),
)
2019-06-19 16:07:22 -07:00
def set_length(self, length):
self.scale(length / self.get_length())
2016-08-18 12:54:04 -07:00
class DashedLine(Line):
CONFIG = {
"dash_length": DEFAULT_DASH_LENGTH,
"dash_spacing": None,
"positive_space_ratio": 0.5,
2016-08-18 12:54:04 -07:00
}
2016-08-18 12:54:04 -07:00
def __init__(self, *args, **kwargs):
Line.__init__(self, *args, **kwargs)
ps_ratio = self.positive_space_ratio
num_dashes = self.calculate_num_dashes(ps_ratio)
dashes = DashedVMobject(
self,
num_dashes=num_dashes,
positive_space_ratio=ps_ratio
)
self.clear_points()
self.add(*dashes)
2016-08-18 12:54:04 -07:00
def calculate_num_dashes(self, positive_space_ratio):
try:
full_length = self.dash_length / positive_space_ratio
return int(np.ceil(self.get_length() / full_length))
except ZeroDivisionError:
return 1
def calculate_positive_space_ratio(self):
return fdiv(
self.dash_length,
self.dash_length + self.dash_spacing,
)
2016-08-18 12:54:04 -07:00
def get_start(self):
if len(self.submobjects) > 0:
return self.submobjects[0].get_start()
2017-02-27 15:56:22 -08:00
else:
return Line.get_start(self)
2016-08-18 12:54:04 -07:00
def get_end(self):
if len(self.submobjects) > 0:
return self.submobjects[-1].get_end()
2017-02-27 15:56:22 -08:00
else:
return Line.get_end(self)
2016-08-18 12:54:04 -07:00
def get_first_handle(self):
return self.submobjects[0].points[1]
def get_last_handle(self):
return self.submobjects[-1].points[-2]
2019-06-15 12:21:29 -07:00
class TangentLine(Line):
CONFIG = {
"length": 1,
"d_alpha": 1e-6
}
def __init__(self, vmob, alpha, **kwargs):
digest_config(self, kwargs)
da = self.d_alpha
2020-02-18 22:32:57 -08:00
a1 = clip(alpha - da, 0, 1)
a2 = clip(alpha + da, 0, 1)
2019-06-15 12:21:29 -07:00
super().__init__(
vmob.point_from_proportion(a1),
vmob.point_from_proportion(a2),
**kwargs
)
self.scale(self.length / self.get_length())
2018-11-29 17:29:31 -08:00
class Elbow(VMobject):
CONFIG = {
"width": 0.2,
"angle": 0,
}
def __init__(self, **kwargs):
VMobject.__init__(self, **kwargs)
self.set_points_as_corners([UP, UP + RIGHT, RIGHT])
self.set_width(self.width, about_point=ORIGIN)
self.rotate(self.angle, about_point=ORIGIN)
class Arrow(Line):
2016-02-27 16:32:53 -08:00
CONFIG = {
"stroke_width": 6,
"buff": MED_SMALL_BUFF,
# TODO, the interface is terrible
2019-03-20 21:27:45 -07:00
"max_tip_length_to_length_ratio": 0.25,
"max_stroke_width_to_length_ratio": 5,
"preserve_tip_size_when_scaling": True,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# TODO, should this be affected when
# Arrow.set_stroke is called?
self.initial_stroke_width = self.stroke_width
2018-03-31 16:32:34 -07:00
self.add_tip()
self.set_stroke_width_from_length()
def scale(self, factor, **kwargs):
if self.get_length() == 0:
return self
has_tip = self.has_tip()
has_start_tip = self.has_start_tip()
if has_tip or has_start_tip:
2019-03-20 21:27:45 -07:00
old_tips = self.pop_tips()
2017-08-24 19:06:34 -07:00
VMobject.scale(self, factor, **kwargs)
self.set_stroke_width_from_length()
2017-08-24 19:06:34 -07:00
# So horribly confusing, must redo
if has_tip:
self.add_tip()
old_tips[0].points[:, :] = self.tip.points
self.remove(self.tip)
self.tip = old_tips[0]
self.add(self.tip)
if has_start_tip:
self.add_tip(at_start=True)
old_tips[1].points[:, :] = self.start_tip.points
self.remove(self.start_tip)
self.start_tip = old_tips[1]
self.add(self.start_tip)
2017-05-05 11:19:10 -07:00
return self
def get_normal_vector(self):
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_default_tip_length(self):
max_ratio = self.max_tip_length_to_length_ratio
return min(
self.tip_length,
max_ratio * self.get_length(),
)
def set_stroke_width_from_length(self):
mr = self.max_stroke_width_to_length_ratio
width = min(self.initial_stroke_width, mr * self.get_length())
self.set_stroke(width=width)
return self
2016-03-17 23:54:28 -07:00
class Vector(Arrow):
CONFIG = {
"buff": 0,
2016-03-17 23:54:28 -07:00
}
2019-04-06 11:52:23 -07:00
def __init__(self, direction=RIGHT, **kwargs):
if len(direction) == 2:
2020-02-13 12:03:54 -08:00
direction = np.hstack([direction, 0])
Arrow.__init__(self, ORIGIN, direction, **kwargs)
2016-03-17 23:54:28 -07:00
2016-04-23 23:36:05 -07:00
class DoubleArrow(Arrow):
2019-02-07 21:24:00 -08:00
def __init__(self, *args, **kwargs):
Arrow.__init__(self, *args, **kwargs)
self.add_tip(at_start=True)
2016-07-12 10:34:35 -07:00
2016-04-17 00:31:38 -07:00
class CubicBezier(VMobject):
def __init__(self, a0, h0, h1, a1, **kwargs):
2016-04-17 00:31:38 -07:00
VMobject.__init__(self, **kwargs)
self.add_cubic_bezier_curve(a0, h0, h1, a1)
2015-10-20 21:55:46 -07:00
2016-04-17 00:31:38 -07:00
class Polygon(VMobject):
2016-02-27 16:32:53 -08:00
CONFIG = {
2019-02-07 21:24:00 -08:00
"color": BLUE,
2015-10-08 11:50:49 -07:00
}
2016-01-09 20:10:06 -08:00
def __init__(self, *vertices, **kwargs):
self.vertices = vertices
super().__init__(**kwargs)
def init_points(self):
verts = self.vertices
self.set_points_as_corners([*verts, verts[0]])
def get_vertices(self):
2019-02-05 15:24:51 -08:00
return self.get_start_anchors()
2019-02-07 21:57:40 -08:00
def round_corners(self, radius=0.5):
vertices = self.get_vertices()
arcs = []
for v1, v2, v3 in adjacent_n_tuples(vertices, 3):
vect1 = v2 - v1
vect2 = v3 - v2
unit_vect1 = normalize(vect1)
unit_vect2 = normalize(vect2)
angle = angle_between_vectors(vect1, vect2)
2019-02-07 22:25:09 -08:00
# Negative radius gives concave curves
angle *= np.sign(radius)
2019-02-07 21:57:40 -08:00
# Distance between vertex and start of the arc
cut_off_length = radius * np.tan(angle / 2)
2019-02-07 22:25:09 -08:00
# Determines counterclockwise vs. clockwise
sign = np.sign(np.cross(vect1, vect2)[2])
arc = ArcBetweenPoints(
2019-02-07 21:57:40 -08:00
v2 - unit_vect1 * cut_off_length,
v2 + unit_vect2 * cut_off_length,
angle=sign * angle,
n_components=2,
)
arcs.append(arc)
2019-02-07 22:25:09 -08:00
self.clear_points()
2019-02-07 21:57:40 -08:00
# To ensure that we loop through starting with last
arcs = [arcs[-1], *arcs[:-1]]
for arc1, arc2 in adjacent_pairs(arcs):
self.append_points(arc1.points)
line = Line(arc1.get_end(), arc2.get_start())
# Make sure anchors are evenly distributed
len_ratio = line.get_length() / arc1.get_arc_length()
line.insert_n_curves(
int(arc1.get_num_curves() * len_ratio)
)
self.append_points(line.get_points())
2019-02-07 21:57:40 -08:00
return self
2017-01-16 11:43:59 -08:00
class RegularPolygon(Polygon):
CONFIG = {
"start_angle": None,
}
2019-02-07 09:26:18 -08:00
def __init__(self, n=6, **kwargs):
digest_config(self, kwargs, locals())
if self.start_angle is None:
# 0 for odd, 90 for even
self.start_angle = (n % 2) * 90 * DEGREES
start_vect = rotate_vector(RIGHT, self.start_angle)
2017-01-16 11:43:59 -08:00
vertices = compass_directions(n, start_vect)
super().__init__(*vertices, **kwargs)
2019-02-07 09:26:18 -08:00
class Triangle(RegularPolygon):
def __init__(self, **kwargs):
super().__init__(n=3, **kwargs)
2019-02-07 09:26:18 -08:00
class ArrowTip(Triangle):
CONFIG = {
"fill_opacity": 1,
"stroke_width": 0,
"width": DEFAULT_ARROW_TIP_WIDTH,
"length": DEFAULT_ARROW_TIP_LENGTH,
"angle": 0,
}
def __init__(self, **kwargs):
Triangle.__init__(self, start_angle=0, **kwargs)
self.set_height(self.width)
self.set_width(self.length, stretch=True)
self.rotate(self.angle)
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())
2019-02-05 15:24:51 -08:00
class Rectangle(Polygon):
2016-02-27 16:32:53 -08:00
CONFIG = {
"color": WHITE,
"width": 4.0,
"height": 2.0,
"mark_paths_closed": True,
"close_new_points": True,
}
def __init__(self, width=None, height=None, **kwargs):
2020-06-05 13:23:15 -07:00
Polygon.__init__(self, UR, UL, DL, DR, **kwargs)
self.width = width or self.width
self.height = height or self.height
2019-02-07 21:24:00 -08:00
self.set_width(self.width, stretch=True)
self.set_height(self.height, stretch=True)
class Square(Rectangle):
2016-02-27 16:32:53 -08:00
CONFIG = {
"side_length": 2.0,
}
def __init__(self, **kwargs):
2016-03-19 17:19:02 -07:00
digest_config(self, kwargs)
2016-03-07 19:07:00 -08:00
Rectangle.__init__(
self,
height=self.side_length,
width=self.side_length,
2016-03-07 19:07:00 -08:00
**kwargs
)
class RoundedRectangle(Rectangle):
CONFIG = {
2018-09-27 17:37:25 -07:00
"corner_radius": 0.5,
}
2019-02-07 21:57:40 -08:00
def __init__(self, **kwargs):
Rectangle.__init__(self, **kwargs)
self.round_corners(self.corner_radius)