Port opengl_geometry.Circle stroke

This commit is contained in:
Devin Neal 2021-02-20 14:17:35 -08:00
commit 53b2d160c2
8 changed files with 969 additions and 49 deletions

View file

@ -16,6 +16,7 @@ if typing.TYPE_CHECKING:
from .. import logger
from ..mobject.mobject import Mobject, _AnimationBuilder
from ..utils.rate_functions import smooth
from ..mobject.opengl_mobject import OpenGLMobject
DEFAULT_ANIMATION_RUN_TIME: float = 1.0
DEFAULT_ANIMATION_LAG_RATIO: float = 0.0
@ -61,7 +62,9 @@ class Animation:
def _typecheck_input(self, mobject: Mobject) -> None:
if mobject is None:
logger.debug("creating dummy animation")
elif not isinstance(mobject, Mobject):
elif not isinstance(mobject, Mobject) and not isinstance(
mobject, OpenGLMobject
):
raise TypeError("Animation only works on Mobjects")
def __str__(self) -> str:

View file

@ -1,3 +1,16 @@
import numpy as np
from ..constants import *
from ..utils.color import *
from ..mobject.mobject import Mobject
from ..mobject.types.opengl_vectorized_mobject import OpenGLVGroup
from ..mobject.types.opengl_vectorized_mobject import OpenGLVMobject
from ..mobject.types.opengl_vectorized_mobject import OpenGLDashedVMobject
from ..utils.iterables import adjacent_n_tuples
from ..utils.iterables import adjacent_pairs
from ..utils.simple_functions import fdiv
# from ..utils.simple_functions import clip
from ..utils.space_ops import angle_of_vector
from ..utils.space_ops import angle_between_vectors
from ..utils.space_ops import compass_directions
@ -6,44 +19,206 @@ 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
class OpenGLArc(TipableVMobject):
"""A circular arc."""
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
opengl_compatible = True
class OpenGLTipableVMobject(OpenGLVMobject):
"""
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
"""
# Adding, Creating, Modifying tips
def __init__(
self,
tip_length=DEFAULT_ARROW_TIP_LENGTH,
normal_vector=OUT,
tip_config={},
**kwargs
):
self.tip_length = tip_length
self.normal_vector = normal_vector
self.tip_config = tip_config
OpenGLVMobject.__init__(self, **kwargs)
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 = dict()
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 = OpenGLVGroup()
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 = OpenGLVGroup()
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.get_points()[1]
def get_last_handle(self):
return self.get_points()[-2]
def get_end(self):
if self.has_tip():
return self.tip.get_start()
else:
return OpenGLVMobject.get_end(self)
def get_start(self):
if self.has_start_tip():
return self.start_tip.get_start()
else:
return OpenGLVMobject.get_start(self)
def get_length(self):
start, end = self.get_start_and_end()
return get_norm(start - end)
class OpenGLArc(OpenGLTipableVMobject):
def __init__(
self,
start_angle=0,
angle=TAU / 4,
radius=1.0,
num_components=8,
n_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)
self.radius = radius
self.n_components = n_components
self.anchors_span_full_range = anchors_span_full_range
self.arc_center = arc_center
OpenGLVMobject.__init__(self, **kwargs)
def init_gl_points(self):
def init_points(self):
self.set_points(
OpenGLArc.create_quadratic_bezier_points(
angle=self.angle,
start_angle=self.start_angle,
n_components=self.num_components,
n_components=self.n_components,
)
)
self.scale(self.radius, about_point=ORIGIN)
@ -104,3 +279,690 @@ class OpenGLArcBetweenPoints(OpenGLArc):
if angle == 0:
self.set_points_as_corners([LEFT, RIGHT])
self.put_start_and_end_on(start, end)
class OpenGLCurvedArrow(OpenGLArcBetweenPoints):
def __init__(self, start_point, end_point, **kwargs):
OpenGLArcBetweenPoints.__init__(self, start_point, end_point, **kwargs)
self.add_tip()
class OpenGLCurvedDoubleArrow(OpenGLCurvedArrow):
def __init__(self, start_point, end_point, **kwargs):
OpenGLCurvedArrow.__init__(self, start_point, end_point, **kwargs)
self.add_tip(at_start=True)
class OpenGLCircle(OpenGLArc):
def __init__(
self, color=RED, close_new_points=True, anchors_span_full_range=False, **kwargs
):
OpenGLArc.__init__(
self,
0,
TAU,
color=color,
close_new_points=close_new_points,
anchors_span_full_range=anchors_span_full_range,
**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
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)
def point_at_angle(self, angle):
start_angle = self.get_start_angle()
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 TangentLine(Line):
# CONFIG = {"length": 1, "d_alpha": 1e-6}
#
# def __init__(self, vmob, alpha, **kwargs):
# digest_config(self, kwargs)
# da = self.d_alpha
# a1 = clip(alpha - da, 0, 1)
# a2 = clip(alpha + da, 0, 1)
# super().__init__(vmob.pfp(a1), vmob.pfp(a2), **kwargs)
# self.scale(self.length / self.get_length())
#
#
# class Elbow(VMobject):
# CONFIG = {
# "width": 0.2,
# "angle": 0,
# }
#
# def __init__(self, **kwargs):
# super().__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):
# CONFIG = {
# "fill_color": GREY_A,
# "fill_opacity": 1,
# "stroke_width": 0,
# "buff": MED_SMALL_BUFF,
# "thickness": 0.05,
# "tip_width_ratio": 5,
# "tip_angle": PI / 3,
# "max_tip_length_to_length_ratio": 0.5,
# "max_width_to_length_ratio": 0.1,
# }
#
# def set_points_by_ends(self, start, end, buff=0, path_arc=0):
# # Find the right tip length and thickness
# vect = end - start
# length = max(get_norm(vect), 1e-8)
# thickness = self.thickness
# w_ratio = fdiv(self.max_width_to_length_ratio, fdiv(thickness, length))
# if w_ratio < 1:
# thickness *= w_ratio
#
# tip_width = self.tip_width_ratio * thickness
# tip_length = tip_width / (2 * np.tan(self.tip_angle / 2))
# t_ratio = fdiv(self.max_tip_length_to_length_ratio, fdiv(tip_length, length))
# if t_ratio < 1:
# tip_length *= t_ratio
# tip_width *= t_ratio
#
# # Find points for the stem
# if path_arc == 0:
# points1 = (length - tip_length) * np.array([RIGHT, 0.5 * RIGHT, ORIGIN])
# points1 += thickness * UP / 2
# points2 = points1[::-1] + thickness * DOWN
# else:
# # Solve for radius so that the tip-to-tail length matches |end - start|
# a = 2 * (1 - np.cos(path_arc))
# b = -2 * tip_length * np.sin(path_arc)
# c = tip_length ** 2 - length ** 2
# R = (-b + np.sqrt(b ** 2 - 4 * a * c)) / (2 * a)
#
# # Find arc points
# points1 = Arc.create_quadratic_bezier_points(path_arc)
# points2 = np.array(points1[::-1])
# points1 *= R + thickness / 2
# points2 *= R - thickness / 2
# if path_arc < 0:
# tip_length *= -1
# rot_T = rotation_matrix_transpose(PI / 2 - path_arc, OUT)
# for points in points1, points2:
# points[:] = np.dot(points, rot_T)
# points += R * DOWN
#
# self.set_points(points1)
# # Tip
# self.add_line_to(tip_width * UP / 2)
# self.add_line_to(tip_length * LEFT)
# self.tip_index = len(self.get_points()) - 1
# self.add_line_to(tip_width * DOWN / 2)
# self.add_line_to(points2[0])
# # Close it out
# self.append_points(points2)
# self.add_line_to(points1[0])
#
# if length > 0:
# # Final correction
# super().scale(length / self.get_length())
#
# self.rotate(angle_of_vector(vect) - self.get_angle())
# self.rotate(
# PI / 2 - np.arccos(normalize(vect)[2]),
# axis=rotate_vector(self.get_unit_vector(), -PI / 2),
# )
# self.shift(start - self.get_start())
# self.refresh_triangulation()
#
# def reset_points_around_ends(self):
# self.set_points_by_ends(
# self.get_start(), self.get_end(), path_arc=self.path_arc
# )
# return self
#
# def get_start(self):
# nppc = self.n_points_per_curve
# points = self.get_points()
# return (points[0] + points[-nppc]) / 2
#
# def get_end(self):
# return self.get_points()[self.tip_index]
#
# def put_start_and_end_on(self, start, end):
# self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
# return self
#
# def scale(self, *args, **kwargs):
# super().scale(*args, **kwargs)
# self.reset_points_around_ends()
# return self
#
# def set_thickness(self, thickness):
# self.thickness = thickness
# self.reset_points_around_ends()
# return self
#
# def set_path_arc(self, path_arc):
# self.path_arc = path_arc
# self.reset_points_around_ends()
# return self
#
#
# class Vector(Arrow):
# CONFIG = {
# "buff": 0,
# }
#
# def __init__(self, direction=RIGHT, **kwargs):
# if len(direction) == 2:
# direction = np.hstack([direction, 0])
# super().__init__(ORIGIN, direction, **kwargs)
#
#
# class DoubleArrow(Arrow):
# def __init__(self, *args, **kwargs):
# Arrow.__init__(self, *args, **kwargs)
# self.add_tip(at_start=True)
#
#
# class CubicBezier(VMobject):
# def __init__(self, a0, h0, h1, a1, **kwargs):
# VMobject.__init__(self, **kwargs)
# 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)
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
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,
)
)
self.scale(self.radius, about_point=ORIGIN)
self.shift(self.arc_center)
@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())
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)

View file

@ -448,12 +448,12 @@ class OpenGLMobject:
# Make sure any mobject or numpy array attributes are copied
family = self.get_family()
for attr, value in list(self.__dict__.items()):
if isinstance(value, Mobject) and value in family and value is not self:
if isinstance(value, OpenGLMobject) and value in family and value is not self:
setattr(copy_mobject, attr, value.copy())
if isinstance(value, np.ndarray):
setattr(copy_mobject, attr, value.copy())
if isinstance(value, ShaderWrapper):
setattr(copy_mobject, attr, value.copy())
# if isinstance(value, ShaderWrapper):
# setattr(copy_mobject, attr, value.copy())
return copy_mobject
def deepcopy(self):
@ -1157,7 +1157,7 @@ class OpenGLMobject:
def align_data(self, mobject):
# In case any data arrays get resized when aligned to shader data
self.refresh_shader_data()
# self.refresh_shader_data()
for mob1, mob2 in zip(self.get_family(), mobject.get_family()):
# Separate out how points are treated so that subclasses
# can handle that case differently if they choose

View file

@ -28,7 +28,7 @@ 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 manimlib.utils.space_ops import get_norm
from ...utils.space_ops import get_norm
from ...utils.space_ops import get_unit_normal
# from manimlib.utils.space_ops import z_to_vector
@ -1021,13 +1021,14 @@ class OpenGLVectorizedPoint(OpenGLPoint, OpenGLVMobject):
artificial_height=0.01,
**kwargs
):
self.color = color
self.fill_opacity = fill_opacity
self.stroke_width = stroke_width
self.artificial_width = artificial_width
self.artificial_height = artificial_height
super().__init__(**kwargs)
super().__init__(
color=color,
fill_opacity=fill_opacity,
stroke_width=stroke_width,
**kwargs)
self.set_points(np.array([location]))
@ -1041,14 +1042,13 @@ class OpenGLCurvesAsSubmobjects(OpenGLVGroup):
self.add(part)
class DashedVMobject(OpenGLVMobject):
class OpenGLDashedVMobject(OpenGLVMobject):
def __init__(
self, vmobject, num_dashes=15, positive_space_ratio=0.5, color=WHITE, **kwargs
):
self.num_dashes = num_dashes
self.positive_space_ratio = positive_space_ratio
self.color = color
super().__init__(**kwargs)
super().__init__(color=color, **kwargs)
num_dashes = self.num_dashes
ps_ratio = self.positive_space_ratio
if num_dashes > 0:

View file

@ -2,5 +2,5 @@ from ..mobject.opengl_mobject import *
from ..mobject.types.opengl_vectorized_mobject import *
from ..mobject.opengl_geometry import *
Arc = OpenGLArc
ArcBetweenPoints = OpenGLArcBetweenPoints
Arc = TestOpenGLArc
ArcBetweenPoints = TestOpenGLArcBetweenPoints

View file

@ -5,6 +5,7 @@ import numpy as np
from ..mobject.types.vectorized_mobject import VMobject
import itertools as it
import time
from .. import logger
from ..mobject import opengl_geometry
@ -298,6 +299,11 @@ class OpenGLRenderer:
self.file_writer = None
def play(self, scene, *args, **kwargs):
if len(args) == 0:
logger.warning("Called Scene.play with no animations")
return
# TODO: Handle data locking / unlocking.
if scene.compile_animation_data(*args, **kwargs):
self.animation_start_time = time.time()
self.animation_elapsed_time = 0

View file

@ -26,6 +26,7 @@ from ..utils.iterables import list_update, list_difference_update
from ..utils.family import extract_mobject_family_members
from ..renderer.cairo_renderer import CairoRenderer
from ..utils.exceptions import EndSceneEarlyException
from ..utils.family_ops import restructure_list_to_exclude_certain_family_members
class Scene(Container):
@ -320,15 +321,20 @@ class Scene(Container):
The same scene after adding the Mobjects in.
"""
mobjects = [*mobjects, *self.foreground_mobjects]
self.restructure_mobjects(to_remove=mobjects)
self.mobjects += mobjects
if self.moving_mobjects:
self.restructure_mobjects(
to_remove=mobjects, mobject_list_name="moving_mobjects"
)
self.moving_mobjects += mobjects
return self
if config["use_opengl_renderer"]:
new_mobjects = mobjects
self.remove(*new_mobjects)
self.mobjects += new_mobjects
else:
mobjects = [*mobjects, *self.foreground_mobjects]
self.restructure_mobjects(to_remove=mobjects)
self.mobjects += mobjects
if self.moving_mobjects:
self.restructure_mobjects(
to_remove=mobjects, mobject_list_name="moving_mobjects"
)
self.moving_mobjects += mobjects
return self
def add_mobjects_from_animations(self, animations):
@ -352,9 +358,16 @@ class Scene(Container):
*mobjects : Mobject
The mobjects to remove.
"""
for list_name in "mobjects", "foreground_mobjects":
self.restructure_mobjects(mobjects, list_name, False)
return self
if config["use_opengl_renderer"]:
mobjects_to_remove = mobjects
self.mobjects = restructure_list_to_exclude_certain_family_members(
self.mobjects, mobjects_to_remove
)
return self
else:
for list_name in "mobjects", "foreground_mobjects":
self.restructure_mobjects(mobjects, list_name, False)
return self
def restructure_mobjects(
self, to_remove, mobject_list_name="mobjects", extract_families=True
@ -793,26 +806,27 @@ class Scene(Container):
self.stop_condition = None
self.moving_mobjects = None
self.static_mobjects = None
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
# TODO, be smart about setting a static image
# the same way Scene.play does
self.renderer.static_image = None
self.stop_condition = self.animations[0].stop_condition
if not config["use_opengl_renderer"]:
if len(self.animations) == 1 and isinstance(self.animations[0], Wait):
self.update_mobjects(dt=0) # Any problems with this?
if self.should_update_mobjects():
# TODO, be smart about setting a static image
# the same way Scene.play does
self.renderer.static_image = None
self.stop_condition = self.animations[0].stop_condition
else:
self.duration = self.animations[0].duration
if not skip_rendering:
self.add_static_frames(self.animations[0].duration)
return None
else:
self.duration = self.animations[0].duration
if not skip_rendering:
self.add_static_frames(self.animations[0].duration)
return None
else:
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
(
self.moving_mobjects,
self.static_mobjects,
) = self.get_moving_and_static_mobjects(self.animations)
self.renderer.save_static_frame_data(self, self.static_mobjects)
# Paint all non-moving objects onto the screen, so they don't
# have to be rendered every frame
(
self.moving_mobjects,
self.static_mobjects,
) = self.get_moving_and_static_mobjects(self.animations)
self.renderer.save_static_frame_data(self, self.static_mobjects)
self.duration = self.get_run_time(self.animations)
self.time_progression = self._get_animation_time_progression(

35
manim/utils/family_ops.py Normal file
View file

@ -0,0 +1,35 @@
import itertools as it
def extract_mobject_family_members(mobject_list, only_those_with_points=False):
result = list(it.chain(*[mob.get_family() for mob in mobject_list]))
if only_those_with_points:
result = [mob for mob in result if mob.has_points()]
return result
def restructure_list_to_exclude_certain_family_members(mobject_list, to_remove):
"""
Removes anything in to_remove from mobject_list, but in the event that one of
the items to be removed is a member of the family of an item in mobject_list,
the other family members are added back into the list.
This is useful in cases where a scene contains a group, e.g. Group(m1, m2, m3),
but one of its submobjects is removed, e.g. scene.remove(m1), it's useful
for the list of mobject_list to be edited to contain other submobjects, but not m1.
"""
new_list = []
to_remove = extract_mobject_family_members(to_remove)
def add_safe_mobjects_from_list(list_to_examine, set_to_remove):
for mob in list_to_examine:
if mob in set_to_remove:
continue
intersect = set_to_remove.intersection(mob.get_family())
if intersect:
add_safe_mobjects_from_list(mob.submobjects, intersect)
else:
new_list.append(mob)
add_safe_mobjects_from_list(mobject_list, set(to_remove))
return new_list