Port geometry.py up to DashedLine

This commit is contained in:
Devin Neal 2021-02-21 00:42:38 -08:00
commit 4246483978
9 changed files with 619 additions and 499 deletions

View file

@ -18,6 +18,26 @@ if config["use_opengl_renderer"]:
# for a 1920x1080 video)
class Testing(Scene):
def construct(self):
c = OpenGLLine(LEFT, RIGHT)
c2 = OpenGLLine(LEFT, RIGHT)
print(c.data["points"])
c2.shift(RIGHT)
self.play(Transform(c, c2))
# c = OpenGLDashedLine(LEFT, RIGHT)
# c2 = OpenGLDashedLine(LEFT, RIGHT)
# print(c.data["points"])
# c2.shift(RIGHT)
# self.play(Transform(c, c2))
# mob = ArcBetweenPoints(LEFT, RIGHT)
# mob2 = ArcBetweenPoints(LEFT, RIGHT)
# mob2.shift(RIGHT)
# self.play(Transform(mob, mob2), run_time=0.3)
class OpeningManim(Scene):
def construct(self):
title = Tex(r"This is some \LaTeX")
@ -75,20 +95,15 @@ class OpeningManim(Scene):
class SquareToCircle(Scene):
def construct(self):
# circle = Circle()
# square = Square()
# square.flip(RIGHT)
# square.rotate(-3 * TAU / 8)
# circle.set_fill(PINK, opacity=0.5)
circle = Circle()
square = Square()
square.flip(RIGHT)
square.rotate(-3 * TAU / 8)
circle.set_fill(PINK, opacity=0.5)
# self.play(ShowCreation(square))
# self.play(Transform(square, circle))
# self.play(FadeOut(square))
mob = ArcBetweenPoints(LEFT, RIGHT)
mob2 = ArcBetweenPoints(LEFT, RIGHT)
mob2.shift(RIGHT)
self.play(Transform(mob, mob2))
self.play(ShowCreation(square))
self.play(Transform(square, circle))
self.play(FadeOut(square))
class WarpSquare(Scene):

View file

@ -320,244 +320,277 @@ class OpenGLCircle(OpenGLArc):
return self.point_from_proportion((angle - start_angle) / TAU)
# class Dot(Circle):
# CONFIG = {
# "radius": DEFAULT_DOT_RADIUS,
# "stroke_width": 0,
# "fill_opacity": 1.0,
# "color": WHITE,
# }
#
# def __init__(self, point=ORIGIN, **kwargs):
# super().__init__(arc_center=point, **kwargs)
#
#
# class SmallDot(Dot):
# CONFIG = {
# "radius": DEFAULT_SMALL_DOT_RADIUS,
# }
#
#
# class Ellipse(Circle):
# CONFIG = {"width": 2, "height": 1}
#
# def __init__(self, **kwargs):
# super().__init__(**kwargs)
# self.set_width(self.width, stretch=True)
# self.set_height(self.height, stretch=True)
#
#
# class AnnularSector(Arc):
# CONFIG = {
# "inner_radius": 1,
# "outer_radius": 2,
# "angle": TAU / 4,
# "start_angle": 0,
# "fill_opacity": 1,
# "stroke_width": 0,
# "color": WHITE,
# }
#
# def init_points(self):
# 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.get_points())
# self.add_line_to(outer_arc.get_points()[0])
# self.append_points(outer_arc.get_points())
# self.add_line_to(inner_arc.get_points()[0])
#
#
# class Sector(AnnularSector):
# CONFIG = {"outer_radius": 1, "inner_radius": 0}
#
#
# class Annulus(Circle):
# CONFIG = {
# "inner_radius": 1,
# "outer_radius": 2,
# "fill_opacity": 1,
# "stroke_width": 0,
# "color": WHITE,
# "mark_paths_closed": False,
# }
#
# def init_points(self):
# self.radius = self.outer_radius
# outer_circle = Circle(radius=self.outer_radius)
# inner_circle = Circle(radius=self.inner_radius)
# inner_circle.reverse_points()
# self.append_points(outer_circle.get_points())
# self.append_points(inner_circle.get_points())
# self.shift(self.arc_center)
#
#
# class Line(TipableVMobject):
# CONFIG = {
# "buff": 0,
# # Angle of arc specified here
# "path_arc": 0,
# }
#
# def __init__(self, start=LEFT, end=RIGHT, **kwargs):
# digest_config(self, kwargs)
# self.set_start_and_end_attrs(start, end)
# super().__init__(**kwargs)
#
# def init_points(self):
# self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
#
# def set_points_by_ends(self, start, end, buff=0, path_arc=0):
# if path_arc:
# self.set_points(Arc.create_quadratic_bezier_points(path_arc))
# self.put_start_and_end_on(start, end)
# else:
# self.set_points_as_corners([start, end])
# self.account_for_buff(self.buff)
#
# def set_path_arc(self, new_value):
# self.path_arc = new_value
# self.init_points()
#
# def account_for_buff(self, buff):
# if buff == 0:
# return
# #
# if self.path_arc == 0:
# length = self.get_length()
# else:
# length = self.get_arc_length()
# #
# if length < 2 * buff:
# return
# buff_prop = buff / length
# self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
# return self
#
# 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)
# 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
# # start and end, if they're mobjects
# self.start = self.pointify(start, vect) + self.buff * vect
# self.end = self.pointify(end, -vect) - self.buff * vect
#
# def pointify(self, mob_or_point, direction=None):
# """
# Take an argument passed into Line (or subclass) and turn
# it into a 3d point.
# """
# if isinstance(mob_or_point, Mobject):
# mob = mob_or_point
# if direction is None:
# return mob.get_center()
# else:
# return mob.get_continuous_bounding_box_point(direction)
# else:
# point = mob_or_point
# result = np.zeros(self.dim)
# result[: len(point)] = point
# return result
#
# def put_start_and_end_on(self, start, end):
# curr_start, curr_end = self.get_start_and_end()
# if (curr_start == curr_end).all():
# self.set_points_by_ends(start, end, self.path_arc)
# return super().put_start_and_end_on(start, end)
#
# def get_vector(self):
# return self.get_end() - self.get_start()
#
# def get_unit_vector(self):
# return normalize(self.get_vector())
#
# def get_angle(self):
# return angle_of_vector(self.get_vector())
#
# def get_projection(self, point):
# """
# Return projection of a point onto the line
# """
# unit_vect = self.get_unit_vector()
# start = self.get_start()
# return start + np.dot(point - start, unit_vect) * unit_vect
#
# def get_slope(self):
# return np.tan(self.get_angle())
#
# def set_angle(self, angle, about_point=None):
# if about_point is None:
# about_point = self.get_start()
# self.rotate(
# angle - self.get_angle(),
# about_point=about_point,
# )
# return self
#
# def set_length(self, length):
# self.scale(length / self.get_length())
#
#
# class DashedLine(Line):
# CONFIG = {
# "dash_length": DEFAULT_DASH_LENGTH,
# "dash_spacing": None,
# "positive_space_ratio": 0.5,
# }
#
# def __init__(self, *args, **kwargs):
# super().__init__(*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)
#
# 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,
# )
#
# def get_start(self):
# if len(self.submobjects) > 0:
# return self.submobjects[0].get_start()
# else:
# return Line.get_start(self)
#
# def get_end(self):
# if len(self.submobjects) > 0:
# return self.submobjects[-1].get_end()
# else:
# return Line.get_end(self)
#
# def get_first_handle(self):
# return self.submobjects[0].get_points()[1]
#
# def get_last_handle(self):
# return self.submobjects[-1].get_points()[-2]
#
#
class OpenGLDot(OpenGLCircle):
def __init__(
self,
point=ORIGIN,
radius=DEFAULT_DOT_RADIUS,
stroke_width=0,
fill_opacity=1.0,
color=WHITE,
**kwargs
):
super().__init__(
arc_center=point,
radius=radius,
stroke_width=stroke_width,
fill_opacity=fill_opacity,
color=color,
**kwargs
)
class OpenGLSmallDot(OpenGLDot):
def __init__(self, radius=DEFAULT_SMALL_DOT_RADIUS, **kwargs):
super().__init__(radius=radius, **kwargs)
class OpenGLEllipse(OpenGLCircle):
def __init__(self, width=2, height=1, **kwargs):
super().__init__(**kwargs)
self.set_width(width, stretch=True)
self.set_height(height, stretch=True)
class OpenGLAnnularSector(OpenGLArc):
def __init__(
self,
inner_radius=1,
outer_radius=2,
angle=TAU / 4,
start_angle=0,
fill_opacity=1,
stroke_width=0,
color=WHITE,
**kwargs
):
self.inner_radius = inner_radius
self.outer_radius = outer_radius
OpenGLArc.__init__(
self,
start_angle=start_angle,
angle=angle,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
color=color,
**kwargs
)
def init_points(self):
inner_arc, outer_arc = [
OpenGLArc(
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.get_points())
self.add_line_to(outer_arc.get_points()[0])
self.append_points(outer_arc.get_points())
self.add_line_to(inner_arc.get_points()[0])
class OpenGLSector(OpenGLAnnularSector):
def __init__(self, outer_radius=1, inner_radius=0, **kwargs):
OpenGLAnnularSector.__init__(
self, inner_radius=inner_radius, outer_radius=outer_radius, **kwargs
)
class OpenGLAnnulus(OpenGLCircle):
def __init__(
self,
inner_radius=1,
outer_radius=2,
fill_opacity=1,
stroke_width=0,
color=WHITE,
mark_paths_closed=False,
**kwargs
):
self.mark_paths_closed = mark_paths_closed # is this even used?
self.inner_radius = inner_radius
self.outer_radius = outer_radius
OpenGLCircle.__init__(
self,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
color=color,
**kwargs
)
def init_points(self):
self.radius = self.outer_radius
outer_circle = OpenGLCircle(radius=self.outer_radius)
inner_circle = OpenGLCircle(radius=self.inner_radius)
inner_circle.reverse_points()
self.append_points(outer_circle.get_points())
self.append_points(inner_circle.get_points())
self.shift(self.arc_center)
class OpenGLLine(OpenGLTipableVMobject):
def __init__(self, start=LEFT, end=RIGHT, buff=0, path_arc=0, **kwargs):
self.dim = 3
self.buff = buff
self.path_arc = path_arc
self.set_start_and_end_attrs(start, end)
super().__init__(**kwargs)
def init_points(self):
self.set_points_by_ends(self.start, self.end, self.buff, self.path_arc)
def set_points_by_ends(self, start, end, buff=0, path_arc=0):
if path_arc:
self.set_points(OpenGLArc.create_quadratic_bezier_points(path_arc))
self.put_start_and_end_on(start, end)
else:
self.set_points_as_corners([start, end])
self.account_for_buff(self.buff)
def set_path_arc(self, new_value):
self.path_arc = new_value
self.init_points()
def account_for_buff(self, buff):
if buff == 0:
return
#
if self.path_arc == 0:
length = self.get_length()
else:
length = self.get_arc_length()
#
if length < 2 * buff:
return
buff_prop = buff / length
self.pointwise_become_partial(self, buff_prop, 1 - buff_prop)
return self
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)
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
# start and end, if they're mobjects
self.start = self.pointify(start, vect) + self.buff * vect
self.end = self.pointify(end, -vect) - self.buff * vect
def pointify(self, mob_or_point, direction=None):
"""
Take an argument passed into Line (or subclass) and turn
it into a 3d point.
"""
if isinstance(mob_or_point, Mobject):
mob = mob_or_point
if direction is None:
return mob.get_center()
else:
return mob.get_continuous_bounding_box_point(direction)
else:
point = mob_or_point
result = np.zeros(self.dim)
result[: len(point)] = point
return result
def put_start_and_end_on(self, start, end):
curr_start, curr_end = self.get_start_and_end()
if (curr_start == curr_end).all():
self.set_points_by_ends(start, end, self.path_arc)
return super().put_start_and_end_on(start, end)
def get_vector(self):
return self.get_end() - self.get_start()
def get_unit_vector(self):
return normalize(self.get_vector())
def get_angle(self):
return angle_of_vector(self.get_vector())
def get_projection(self, point):
"""
Return projection of a point onto the line
"""
unit_vect = self.get_unit_vector()
start = self.get_start()
return start + np.dot(point - start, unit_vect) * unit_vect
def get_slope(self):
return np.tan(self.get_angle())
def set_angle(self, angle, about_point=None):
if about_point is None:
about_point = self.get_start()
self.rotate(
angle - self.get_angle(),
about_point=about_point,
)
return self
def set_length(self, length):
self.scale(length / self.get_length())
class OpenGLDashedLine(OpenGLLine):
def __init__(
self,
*args,
dash_length=DEFAULT_DASH_LENGTH,
dash_spacing=None,
positive_space_ratio=0.5,
**kwargs
):
self.dash_length = dash_length
self.dash_spacing = (dash_spacing,)
self.positive_space_ratio = positive_space_ratio
super().__init__(*args, **kwargs)
ps_ratio = self.positive_space_ratio
num_dashes = self.calculate_num_dashes(ps_ratio)
dashes = OpenGLDashedVMobject(
self, num_dashes=num_dashes, positive_space_ratio=ps_ratio
)
self.clear_points()
self.add(*dashes)
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,
)
def get_start(self):
if len(self.submobjects) > 0:
return self.submobjects[0].get_start()
else:
return OpenGLLine.get_start(self)
def get_end(self):
if len(self.submobjects) > 0:
return self.submobjects[-1].get_end()
else:
return OpenGLLine.get_end(self)
def get_first_handle(self):
return self.submobjects[0].get_points()[1]
def get_last_handle(self):
return self.submobjects[-1].get_points()[-2]
# class TangentLine(Line):
# CONFIG = {"length": 1, "d_alpha": 1e-6}
#
@ -716,253 +749,137 @@ class OpenGLCircle(OpenGLArc):
# self.add_cubic_bezier_curve(a0, h0, h1, a1)
#
#
# class Polygon(VMobject):
# 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):
# return self.get_start_anchors()
#
# 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)
# # Negative radius gives concave curves
# angle *= np.sign(radius)
# # Distance between vertex and start of the arc
# cut_off_length = radius * np.tan(angle / 2)
# # Determines counterclockwise vs. clockwise
# sign = np.sign(np.cross(vect1, vect2)[2])
# arc = ArcBetweenPoints(
# v2 - unit_vect1 * cut_off_length,
# v2 + unit_vect2 * cut_off_length,
# angle=sign * angle,
# n_components=2,
# )
# arcs.append(arc)
#
# self.clear_points()
# # 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.get_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())
# return self
#
#
# class RegularPolygon(Polygon):
# CONFIG = {
# "start_angle": None,
# }
#
# 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)
# vertices = compass_directions(n, start_vect)
# super().__init__(*vertices, **kwargs)
#
#
# class Triangle(RegularPolygon):
# def __init__(self, **kwargs):
# super().__init__(n=3, **kwargs)
#
#
# class ArrowTip(Triangle):
# CONFIG = {
# "fill_opacity": 1,
# "fill_color": WHITE,
# "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.get_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,
# "width": 4.0,
# "height": 2.0,
# "mark_paths_closed": True,
# "close_new_points": True,
# }
#
# def __init__(self, width=None, height=None, **kwargs):
# Polygon.__init__(self, UR, UL, DL, DR, **kwargs)
#
# if width is None:
# width = self.width
# if height is None:
# height = self.height
#
# self.set_width(width, stretch=True)
# self.set_height(height, stretch=True)
#
#
# class Square(Rectangle):
# CONFIG = {
# "side_length": 2.0,
# }
#
# def __init__(self, side_length=None, **kwargs):
# digest_config(self, kwargs)
#
# if side_length is None:
# side_length = self.side_length
#
# super().__init__(side_length, side_length, **kwargs)
#
#
# class RoundedRectangle(Rectangle):
# CONFIG = {
# "corner_radius": 0.5,
# }
#
# def __init__(self, **kwargs):
# Rectangle.__init__(self, **kwargs)
# self.round_corners(self.corner_radius)
class OpenGLPolygon(OpenGLVMobject):
def __init__(self, *vertices, **kwargs):
self.vertices = vertices
super().__init__(**kwargs)
from ..utils.space_ops import angle_of_vector
from ..utils.space_ops import angle_between_vectors
from ..utils.space_ops import compass_directions
from ..utils.space_ops import find_intersection
from ..utils.space_ops import get_norm
from ..utils.space_ops import normalize
from ..utils.space_ops import rotate_vector
from ..utils.space_ops import rotation_matrix_transpose
from ..utils.iterables import adjacent_n_tuples
from ..constants import *
from ..mobject.geometry import TipableVMobject
from ..mobject.types.vectorized_mobject import VMobject
def init_points(self):
verts = self.vertices
self.set_points_as_corners([*verts, verts[0]])
def get_vertices(self):
return self.get_start_anchors()
class TestOpenGLArc(TipableVMobject):
"""A circular arc."""
opengl_compatible = True
def __init__(
self,
start_angle=0,
angle=TAU / 4,
radius=1.0,
num_components=8,
anchors_span_full_range=True,
arc_center=ORIGIN,
**kwargs
):
if radius is None: # apparently None is passed by ArcBetweenPoints
radius = 1.0
self.radius = radius
self.num_components = num_components
self.anchors_span_full_range = anchors_span_full_range
self.arc_center = arc_center
self.start_angle = start_angle
self.angle = angle
self._failed_to_get_center = False
TipableVMobject.__init__(self, **kwargs)
def init_gl_points(self):
self.set_points(
TestOpenGLArc.create_quadratic_bezier_points(
angle=self.angle,
start_angle=self.start_angle,
n_components=self.num_components,
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)
# Negative radius gives concave curves
angle *= np.sign(radius)
# Distance between vertex and start of the arc
cut_off_length = radius * np.tan(angle / 2)
# Determines counterclockwise vs. clockwise
sign = np.sign(np.cross(vect1, vect2)[2])
arc = OpenGLArcBetweenPoints(
v2 - unit_vect1 * cut_off_length,
v2 + unit_vect2 * cut_off_length,
angle=sign * angle,
n_components=2,
)
)
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
arcs.append(arc)
@staticmethod
def create_quadratic_bezier_points(angle, start_angle=0, n_components=8):
samples = np.array(
[
[np.cos(a), np.sin(a), 0]
for a in np.linspace(
start_angle,
start_angle + angle,
2 * n_components + 1,
)
]
)
theta = angle / n_components
samples[1::2] /= np.cos(theta / 2)
points = np.zeros((3 * n_components, 3))
points[0::3] = samples[0:-1:2]
points[1::3] = samples[1::2]
points[2::3] = samples[2::2]
return points
def get_arc_center(self):
"""
Looks at the normals to the first two
anchors, and finds their intersection points
"""
# First two anchors and handles
a1, h, a2 = self.get_points()[:3]
# Tangent vectors
t1 = h - a1
t2 = h - a2
# 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):
self.shift(point - self.get_arc_center())
self.clear_points()
# 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.get_points())
line = OpenGLLine(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())
return self
class TestOpenGLArcBetweenPoints(TestOpenGLArc):
def __init__(self, start, end, angle=TAU / 4, **kwargs):
super().__init__(angle=angle, **kwargs)
if angle == 0:
self.set_points_as_corners([LEFT, RIGHT])
self.put_start_and_end_on(start, end)
class OpenGLRegularPolygon(OpenGLPolygon):
def __init__(self, n=6, start_angle=None, **kwargs):
self.start_angle = start_angle
if self.start_angle is None:
if n % 2 == 0:
self.start_angle = 0
else:
self.start_angle = 90 * DEGREES
start_vect = rotate_vector(RIGHT, self.start_angle)
vertices = compass_directions(n, start_vect)
super().__init__(*vertices, **kwargs)
class OpenGLTriangle(OpenGLRegularPolygon):
def __init__(self, **kwargs):
super().__init__(n=3, **kwargs)
class OpenGLArrowTip(OpenGLTriangle):
def __init__(
self,
fill_opacity=1,
fill_color=WHITE,
stroke_width=0,
width=DEFAULT_ARROW_TIP_WIDTH,
length=DEFAULT_ARROW_TIP_LENGTH,
angle=0,
**kwargs
):
OpenGLTriangle.__init__(
self,
start_angle=0,
fill_opacity=fill_opacity,
fill_color=fill_color,
stroke_width=stroke_width,
**kwargs
)
self.set_width(width, stretch=True)
self.set_height(length, stretch=True)
def get_base(self):
return self.point_from_proportion(0.5)
def get_tip_point(self):
return self.get_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 OpenGLRectangle(OpenGLPolygon):
def __init__(
self,
color=WHITE,
width=4.0,
height=2.0,
mark_paths_closed=True,
close_new_points=True,
**kwargs
):
self.mark_paths_closed = mark_paths_closed
self.close_new_points = close_new_points
OpenGLPolygon.__init__(self, UR, UL, DL, DR, color=color, **kwargs)
self.set_width(width, stretch=True)
self.set_height(height, stretch=True)
class OpenGLSquare(OpenGLRectangle):
def __init__(self, side_length=2.0, **kwargs):
self.side_length = side_length
super().__init__(height=side_length, width=side_length, **kwargs)
class OpenGLRoundedRectangle(OpenGLRectangle):
def __init__(self, corner_radius=0.5, **kwargs):
self.corner_radius = corner_radius
OpenGLRectangle.__init__(self, **kwargs)
self.round_corners(self.corner_radius)

View file

@ -11,13 +11,13 @@ from ...utils.color import *
# from manimlib.constants import *
# from manimlib.mobject.mobject import Mobject
# from manimlib.mobject.mobject import Point
# from manimlib.utils.bezier import bezier
from ...utils.bezier import bezier
# from manimlib.utils.bezier import get_smooth_quadratic_bezier_handle_points
# from manimlib.utils.bezier import get_smooth_cubic_bezier_handle_points
# from manimlib.utils.bezier import get_quadratic_approximation_of_cubic
# from manimlib.utils.bezier import interpolate
# from manimlib.utils.bezier import integer_interpolate
# from manimlib.utils.bezier import partial_quadratic_bezier_points
from ...utils.bezier import interpolate
from ...utils.bezier import integer_interpolate
from ...utils.bezier import partial_quadratic_bezier_points
# from manimlib.utils.color import rgb_to_hex
from ...utils.iterables import make_even
@ -26,12 +26,12 @@ from ...utils.iterables import resize_with_interpolation
from ...utils.iterables import listify
# from manimlib.utils.space_ops import angle_between_vectors
# from manimlib.utils.space_ops import cross2d
# from manimlib.utils.space_ops import earclip_triangulation
from ...utils.space_ops import cross2d
from ...utils.space_ops import earclip_triangulation
from ...utils.space_ops import get_norm
from ...utils.space_ops import get_unit_normal
# from manimlib.utils.space_ops import z_to_vector
from ...utils.space_ops import z_to_vector
# from manimlib.shader_wrapper import ShaderWrapper
@ -741,7 +741,7 @@ class OpenGLVMobject(OpenGLMobject):
return self
def pointwise_become_partial(self, vmobject, a, b):
assert isinstance(vmobject, VMobject)
assert isinstance(vmobject, OpenGLVMobject)
if a <= 0 and b >= 1:
self.become(vmobject)
return self

View file

@ -1,6 +1,3 @@
from ..mobject.opengl_mobject import *
from ..mobject.types.opengl_vectorized_mobject import *
from ..mobject.opengl_geometry import *
Arc = TestOpenGLArc
ArcBetweenPoints = TestOpenGLArcBetweenPoints

View file

@ -6,6 +6,10 @@ from ..mobject.types.vectorized_mobject import VMobject
import itertools as it
import time
from .. import logger
from ..constants import *
from ..utils.space_ops import cross2d
from ..utils.space_ops import earclip_triangulation
from ..utils.space_ops import z_to_vector
from ..mobject import opengl_geometry
@ -231,7 +235,7 @@ class OpenGLRenderer:
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
atol = tolerance_for_point_equality
atol = mob.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
end_of_loop[-1] = True

View file

@ -3,6 +3,7 @@
__all__ = [
"bezier",
"partial_bezier_points",
"partial_quadratic_bezier_points",
"interpolate",
"integer_interpolate",
"mid",
@ -76,6 +77,28 @@ def partial_bezier_points(points: np.ndarray, a: float, b: float) -> np.ndarray:
return np.array([bezier(a_to_1[: i + 1])(end_prop) for i in range(len(points))])
# Shortened version of partial_bezier_points just for quadratics,
# since this is called a fair amount
def partial_quadratic_bezier_points(points, a, b):
if a == 1:
return 3 * [points[-1]]
def curve(t):
return (
points[0] * (1 - t) * (1 - t)
+ 2 * points[1] * t * (1 - t)
+ points[2] * t * t
)
# bezier(points)
h0 = curve(a) if a > 0 else points[0]
h2 = curve(b) if b < 1 else points[2]
h1_prime = (1 - a) * points[1] + a * points[2]
end_prop = (b - a) / (1.0 - a)
h1 = (1 - end_prop) * h0 + end_prop * h1_prime
return [h0, h1, h2]
# Linear interpolation variants

View file

@ -27,6 +27,8 @@ __all__ = [
"find_intersection",
"line_intersection",
"get_winding_number",
"cross2d",
"earclip_triangulation",
]
@ -34,6 +36,7 @@ from functools import reduce
import numpy as np
import math
from mapbox_earcut import triangulate_float32 as earcut
from ..constants import OUT
from ..constants import PI
@ -41,12 +44,18 @@ from ..constants import RIGHT
from ..constants import TAU
from ..utils.iterables import adjacent_pairs
from ..utils.simple_functions import fdiv
import itertools as it
from .. import config
def get_norm(vect):
return sum([x ** 2 for x in vect]) ** 0.5
def norm_squared(v):
return v[0] * v[0] + v[1] * v[1] + v[2] * v[2]
# Quaternions
# TODO, implement quaternion type
@ -209,8 +218,22 @@ def cross(v1, v2):
)
def get_unit_normal(v1, v2):
return normalize(cross(v1, v2))
def get_unit_normal(v1, v2, tol=1e-6):
if config["use_opengl_renderer"]:
v1 = normalize(v1)
v2 = normalize(v2)
cp = cross(v1, v2)
cp_norm = get_norm(cp)
if cp_norm < tol:
# Vectors align, so find a normal to them in the plane shared with the z-axis
new_cp = cross(cross(v1, OUT), v1)
new_cp_norm = get_norm(new_cp)
if new_cp_norm < tol:
return DOWN
return new_cp / new_cp_norm
return cp / cp_norm
else:
return normalize(cross(v1, v2))
###
@ -327,3 +350,90 @@ def shoelace_direction(x_y):
"""
area = shoelace(x_y)
return "CW" if area > 0 else "CCW"
def cross2d(a, b):
if len(a.shape) == 2:
return a[:, 0] * b[:, 1] - a[:, 1] * b[:, 0]
else:
return a[0] * b[1] - b[0] * a[1]
def earclip_triangulation(verts, ring_ends):
"""
Returns a list of indices giving a triangulation
of a polygon, potentially with holes
- verts is a numpy array of points
- ring_ends is a list of indices indicating where
the ends of new paths are
"""
# First, connect all the rings so that the polygon
# with holes is instead treated as a (very convex)
# polygon with one edge. Do this by drawing connections
# between rings close to each other
rings = [list(range(e0, e1)) for e0, e1 in zip([0, *ring_ends], ring_ends)]
attached_rings = rings[:1]
detached_rings = rings[1:]
loop_connections = dict()
while detached_rings:
i_range, j_range = [
list(
filter(
# Ignore indices that are already being
# used to draw some connection
lambda i: i not in loop_connections,
it.chain(*ring_group),
)
)
for ring_group in (attached_rings, detached_rings)
]
# Closet point on the atttached rings to an estimated midpoint
# of the detached rings
tmp_j_vert = midpoint(verts[j_range[0]], verts[j_range[len(j_range) // 2]])
i = min(i_range, key=lambda i: norm_squared(verts[i] - tmp_j_vert))
# Closet point of the detached rings to the aforementioned
# point of the attached rings
j = min(j_range, key=lambda j: norm_squared(verts[i] - verts[j]))
# Recalculate i based on new j
i = min(i_range, key=lambda i: norm_squared(verts[i] - verts[j]))
# Remember to connect the polygon at these points
loop_connections[i] = j
loop_connections[j] = i
# Move the ring which j belongs to from the
# attached list to the detached list
new_ring = next(filter(lambda ring: ring[0] <= j < ring[-1], detached_rings))
detached_rings.remove(new_ring)
attached_rings.append(new_ring)
# Setup linked list
after = []
end0 = 0
for end1 in ring_ends:
after.extend(range(end0 + 1, end1))
after.append(end0)
end0 = end1
# Find an ordering of indices walking around the polygon
indices = []
i = 0
for x in range(len(verts) + len(ring_ends) - 1):
# starting = False
if i in loop_connections:
j = loop_connections[i]
indices.extend([i, j])
i = after[j]
else:
indices.append(i)
i = after[i]
if i == 0:
break
meta_indices = earcut(verts[indices, :2], [len(indices)])
return [indices[mi] for mi in meta_indices]

55
poetry.lock generated
View file

@ -628,6 +628,17 @@ category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "mapbox-earcut"
version = "0.12.10"
description = "Python bindings for the mapbox earcut C++ polygon triangulation library."
category = "main"
optional = false
python-versions = "*"
[package.dependencies]
numpy = "*"
[[package]]
name = "markupsafe"
version = "1.1.1"
@ -1493,7 +1504,7 @@ webgl_renderer = ["grpcio", "grpcio-tools", "watchdog"]
[metadata]
lock-version = "1.1"
python-versions = "^3.6.2"
content-hash = "0396636548f0b80f59e84ff57b7b9e33dfc4330726c4a5e818760ead768bf58d"
content-hash = "965cf2774981ec78ba20c7e33026afd4afdce0d6face6145b034f6efa0317cfe"
[metadata.files]
alabaster = [
@ -1943,6 +1954,48 @@ manimpango = [
{file = "ManimPango-0.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:515649c1dd574121efa2cb3654a3fef8491bd2c4fafdd4176edda41d0720ea49"},
{file = "ManimPango-0.2.3.tar.gz", hash = "sha256:6fe4fe0a8623b52de96393e9b2275cce7734ff92e674391e6a10baac84abf4de"},
]
mapbox-earcut = [
{file = "mapbox_earcut-0.12.10-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2aaf7bdb002d36d38d1412088329a9176de3fcadc24a53951b76e49a27e34d78"},
{file = "mapbox_earcut-0.12.10-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddda755c63cf5f9bff338cd375e4e937035898827fef960102e2f4c8984cb068"},
{file = "mapbox_earcut-0.12.10-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:3475382dd573986d0dfe3f6b6fd3b1f576e74af7fd7d95066d2506ce8f4b38e5"},
{file = "mapbox_earcut-0.12.10-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f2b9079df53258766c3108a1c46a7942963c26f216f9bc2d216f93eeed1da76c"},
{file = "mapbox_earcut-0.12.10-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:124cc27486d0e9c9b185a6d80e551e736e2bc81d28e077e297ac39980c14e588"},
{file = "mapbox_earcut-0.12.10-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:04228ff704d5edc49d71ba3fd07c4a8621a4f416d2ad5c2504a80d0cabd51c18"},
{file = "mapbox_earcut-0.12.10-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:9a15cb52f3f6d5980e41b6ca31976cc2048acf290f2e5159d7527e16e5c5d748"},
{file = "mapbox_earcut-0.12.10-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:6b083a065afd40961cc311b425c54275f531d91a27bdd34a2049993a68d8a651"},
{file = "mapbox_earcut-0.12.10-cp35-cp35m-win32.whl", hash = "sha256:fa3355248d3beaab67438373315e55a37ecc2694a4a4b1965e9d8f582a210479"},
{file = "mapbox_earcut-0.12.10-cp35-cp35m-win_amd64.whl", hash = "sha256:90b2977a9eda2a1873e44d7821ecf3b18062aa0878e957cb306e6018aba105c9"},
{file = "mapbox_earcut-0.12.10-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:553688de45e73fa7d192fd6c29b87a4c5ea60a728b3c7b4c5f1715a89e6a8e54"},
{file = "mapbox_earcut-0.12.10-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:498b7f35c6a7c5c25829bcf1a79015ae64e94e8e36b84bd06e93131352b409f0"},
{file = "mapbox_earcut-0.12.10-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:8b9c6147249edf5049ccf7a7bca5bb29c9d512b3a5b62510659cfc51f1b8f806"},
{file = "mapbox_earcut-0.12.10-cp36-cp36m-win32.whl", hash = "sha256:4eaf2a9e86bbf808bf045eb587ec93a393e9d75ab87e7c1c9c8a5d9e122e30ac"},
{file = "mapbox_earcut-0.12.10-cp36-cp36m-win_amd64.whl", hash = "sha256:cb82e4ea9a67a9cdd46c9fc39bcb24e11f49569010368f9c5d376489bb2d0317"},
{file = "mapbox_earcut-0.12.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:619a31a4d9748a30fbea82fd45beec6ab9e7f8a7ae7818be46f09d03ea0d9185"},
{file = "mapbox_earcut-0.12.10-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:054cd56cf8adf179d0aa0e8a061774e5afa767aaf6507851f4d2731b58f03d43"},
{file = "mapbox_earcut-0.12.10-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:ebeef32d504bf7e1ba986f86d87a8d3c96a54b6e03b0535ccdf0ce8f2f56e563"},
{file = "mapbox_earcut-0.12.10-cp37-cp37m-win32.whl", hash = "sha256:62a8787e30f7bbc6e1ebe8b11591119ed85aa5044b2cf3f6afb058e6fffeb8a3"},
{file = "mapbox_earcut-0.12.10-cp37-cp37m-win_amd64.whl", hash = "sha256:5f854a887f854bdea62c4b58785b82cf70056d53dc3bf23146ce68c125ceed96"},
{file = "mapbox_earcut-0.12.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:19f16be13a40d20419362f8ce27cf2dca8f0b8f4c7caa24caaff8351bcdf853e"},
{file = "mapbox_earcut-0.12.10-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:cac7c8c332d1e802f478b5b79166ab38005b6d1bd908fb96857a8941e702f70d"},
{file = "mapbox_earcut-0.12.10-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:24cc595987932b5d47b93ddb733c6bd084d91ff0af66ff2205aa717f0a5a13b8"},
{file = "mapbox_earcut-0.12.10-cp38-cp38-win32.whl", hash = "sha256:fe4b4e1c53fe0fde8e5fee86d868344f47d4d9fd89d9599548ea4b98349f10d6"},
{file = "mapbox_earcut-0.12.10-cp38-cp38-win_amd64.whl", hash = "sha256:6f4601b949ab43cf21dca86ffb6b4e2b30974ab3112f089d16b5f365e8b446f4"},
{file = "mapbox_earcut-0.12.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ca00bdcb162b6a543f3061f49e2dcde42d6b780a545b5054da437e4ff5e33905"},
{file = "mapbox_earcut-0.12.10-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:031132cdfa4d20e640f24d6817b65a91e3292c34ed527b4309617c9992284133"},
{file = "mapbox_earcut-0.12.10-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:f284c0fc7f3c84664e9dddabae4eae729e6458e96ec5f30c9d7000c9b0dadca7"},
{file = "mapbox_earcut-0.12.10-cp39-cp39-win32.whl", hash = "sha256:c94a263767a841a5f7e8f03c725bdf41c87b8936c1a5fe18a5a77f3b9344d68f"},
{file = "mapbox_earcut-0.12.10-cp39-cp39-win_amd64.whl", hash = "sha256:742bd1f94113f44f1adcb5f3e2dca6e288695645b7eeab5d94f589dacda092af"},
{file = "mapbox_earcut-0.12.10-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:ad76defd6188e71023e43e5aed32ab5d540c0452a74918fc5de9bcccdee6c053"},
{file = "mapbox_earcut-0.12.10-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:a3a9a81f696627f3c331f16c8d628b494949fa68067a42d2f1a6a56ee115eb0d"},
{file = "mapbox_earcut-0.12.10-pp27-pypy_73-win32.whl", hash = "sha256:044f70ed230bdb94f08356ec84f4bd30e4ea493d63700c3cc6fa86c5cf57623e"},
{file = "mapbox_earcut-0.12.10-pp36-pypy36_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e352482e6022cc113dd008886012f0966bd2511933f888805fa87f070423d5b1"},
{file = "mapbox_earcut-0.12.10-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:41a282c485e96135ff15c01e0e1eb334cf1a05235edde8154b7b6e30187e54f4"},
{file = "mapbox_earcut-0.12.10-pp36-pypy36_pp73-win32.whl", hash = "sha256:5e1e3cf5a881eafeba7a7e5c4b1b9e7376710c48513513894e2a082ebf870326"},
{file = "mapbox_earcut-0.12.10-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7b6c2bfc28dc82a27071079885ffbe0836c0ad5dbd4ab47a5edaa24a86218daf"},
{file = "mapbox_earcut-0.12.10-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:6e6614f7b437b4b0a69ab642cf35569079408ac25c193013d5ad26e82c71fc01"},
{file = "mapbox_earcut-0.12.10-pp37-pypy37_pp73-win32.whl", hash = "sha256:d016823e863309b9cc542bb1d3bcdc274ec65fb23f2566de298188dcd4a75c0e"},
{file = "mapbox_earcut-0.12.10.tar.gz", hash = "sha256:50d995673ac9ce8cb9abb7ab64b709d611c1d27557907e00b631b5272345c453"},
]
markupsafe = [
{file = "MarkupSafe-1.1.1-cp27-cp27m-macosx_10_6_intel.whl", hash = "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161"},
{file = "MarkupSafe-1.1.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"},

View file

@ -44,6 +44,7 @@ watchdog = { version = "*", optional = true }
jupyterlab = { version = "^3.0", optional = true }
moderngl = "^5.6.3"
moderngl-window = "^2.3.0"
mapbox-earcut = "^0.12.10"
[tool.poetry.extras]
webgl_renderer = ["grpcio","grpcio-tools","watchdog"]