Merge branch 'video-work' into render-groups

This commit is contained in:
Grant Sanderson 2023-02-01 20:17:01 -08:00
commit 6eafdc63cc
51 changed files with 847 additions and 834 deletions

View file

@ -31,7 +31,7 @@ class OpeningManimExample(Scene):
)
linear_transform_words.arrange(RIGHT)
linear_transform_words.to_edge(UP)
linear_transform_words.set_stroke(BLACK, 10, background=True)
linear_transform_words.set_backstroke(width=5)
self.play(
ShowCreation(grid),
@ -52,7 +52,7 @@ class OpeningManimExample(Scene):
this is the map $z \\rightarrow z^2$
""")
complex_map_words.to_corner(UR)
complex_map_words.set_stroke(BLACK, 5, background=True)
complex_map_words.set_backstroke(width=5)
self.play(
FadeOut(grid),
@ -268,16 +268,8 @@ class UpdatersExample(Scene):
# that of the newly constructed object
brace = always_redraw(Brace, square, UP)
text, number = label = VGroup(
Text("Width = "),
DecimalNumber(
0,
show_ellipsis=True,
num_decimal_places=2,
include_sign=True,
)
)
label.arrange(RIGHT)
label = TexText("Width = 0.00")
number = label.make_number_changable("0.00")
# This ensures that the method deicmal.next_to(square)
# is called on every frame
@ -554,9 +546,7 @@ class TexAndNumbersExample(Scene):
)
class SurfaceExample(Scene):
samples = 4
class SurfaceExample(ThreeDScene):
def construct(self):
surface_text = Text("For 3d scenes, try using surfaces")
surface_text.fix_in_frame()
@ -588,13 +578,6 @@ class SurfaceExample(Scene):
mob.mesh = SurfaceMesh(mob)
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
# Set perspective
frame = self.camera.frame
frame.set_euler_angles(
theta=-30 * DEGREES,
phi=70 * DEGREES,
)
surface = surfaces[0]
self.play(
@ -616,12 +599,12 @@ class SurfaceExample(Scene):
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.animate.increment_phi(-10 * DEGREES),
frame.animate.increment_theta(-20 * DEGREES),
self.frame.animate.increment_phi(-10 * DEGREES),
self.frame.animate.increment_theta(-20 * DEGREES),
run_time=3
)
# Add ambient rotation
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
# Play around with where the light is
light_text = Text("You can move around the light source")
@ -690,6 +673,8 @@ class InteractiveDevelopment(Scene):
class ControlsExample(Scene):
drag_to_pan = False
def setup(self):
self.textbox = Textbox()
self.checkbox = Checkbox()

View file

@ -98,11 +98,7 @@ class DrawBorderThenFill(Animation):
self.mobject = vmobject
def begin(self) -> None:
# Trigger triangulation calculation
for submob in self.mobject.get_family():
if not submob._use_winding_fill:
submob.get_triangulation()
self.mobject.set_animating_status(True)
self.outline = self.get_outline()
super().begin()
self.mobject.match_style(self.outline)
@ -136,7 +132,6 @@ class DrawBorderThenFill(Animation):
if index == 1 and self.sm_to_index[hash(submob)] == 0:
# First time crossing over
submob.set_data(outline.data)
submob.needs_new_triangulation = False
self.sm_to_index[hash(submob)] = 1
if index == 0:

View file

@ -70,7 +70,7 @@ class Transform(Animation):
def finish(self) -> None:
super().finish()
self.mobject.unlock_data()
if self.target_mobject is not None:
if self.target_mobject is not None and self.rate_func(1) == 1:
self.mobject.become(self.target_mobject)
def create_target(self) -> Mobject:

View file

@ -131,9 +131,10 @@ class TransformMatchingStrings(TransformMatchingParts):
target: StringMobject,
matched_keys: Iterable[str] = [],
key_map: dict[str, str] = dict(),
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
**kwargs,
):
matched_pairs = [
matched_pairs = list(matched_pairs) + [
*[(source[key], target[key]) for key in matched_keys],
*[(source[key1], target[key2]) for key1, key2 in key_map.items()],
*[

View file

@ -72,9 +72,9 @@ class Camera(object):
def init_context(self) -> None:
if self.window is None:
self.ctx = moderngl.create_standalone_context()
self.ctx: moderngl.Context = moderngl.create_standalone_context()
else:
self.ctx = self.window.ctx
self.ctx: moderngl.Context = self.window.ctx
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND)
@ -125,16 +125,20 @@ class Camera(object):
def clear(self) -> None:
self.fbo.clear(*self.background_rgba)
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
# Copy blocks from fbo into draw_fbo using Blit
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo)
src_viewport = self.fbo.viewport
def blit(self, src_fbo, dst_fbo):
"""
Copy blocks between fbo's using Blit
"""
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
gl.glBlitFramebuffer(
*src_viewport,
*self.draw_fbo.viewport,
*src_fbo.viewport,
*dst_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
)
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
self.blit(self.fbo, self.draw_fbo)
return self.draw_fbo.read(
viewport=self.draw_fbo.viewport,
components=self.n_channels,
@ -169,10 +173,10 @@ class Camera(object):
# Getting camera attributes
def get_pixel_size(self) -> float:
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]
return self.frame.get_width() / self.get_pixel_shape()[0]
def get_pixel_shape(self) -> tuple[int, int]:
return self.draw_fbo.size
return self.fbo.size
def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0]
@ -223,6 +227,8 @@ class Camera(object):
self.fbo.use()
for mobject in mobjects:
mobject.render(self.ctx, self.uniforms)
if self.window is not None and self.fbo is not self.window_fbo:
self.blit(self.fbo, self.window_fbo)
def refresh_uniforms(self) -> None:
frame = self.frame
@ -231,12 +237,12 @@ class Camera(object):
cam_pos = self.frame.get_implied_camera_location()
self.uniforms.update(
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
view=tuple(view_matrix.T.flatten()),
focal_distance=frame.get_focal_distance() / frame.get_scale(),
frame_scale=frame.get_scale(),
pixel_size=self.get_pixel_size(),
camera_position=tuple(cam_pos),
light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),
)

View file

@ -4,9 +4,10 @@ import math
import numpy as np
from scipy.spatial.transform import Rotation
from pyrr import Matrix44
from manimlib.constants import DEGREES, RADIANS
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import FRAME_SHAPE
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.mobject.mobject import Mobject
from manimlib.utils.space_ops import normalize
@ -20,28 +21,30 @@ if TYPE_CHECKING:
class CameraFrame(Mobject):
def __init__(
self,
frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT),
frame_shape: tuple[float, float] = FRAME_SHAPE,
center_point: Vect3 = ORIGIN,
focal_dist_to_height: float = 2.0,
# Field of view in the y direction
fovy: float = 45 * DEGREES,
**kwargs,
):
self.frame_shape = frame_shape
self.center_point = center_point
self.focal_dist_to_height = focal_dist_to_height
self.view_matrix = np.identity(4)
super().__init__(**kwargs)
def init_uniforms(self) -> None:
super().init_uniforms()
# As a quaternion
self.uniforms["orientation"] = Rotation.identity().as_quat()
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
self.uniforms["fovy"] = fovy
def init_points(self) -> None:
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
self.set_width(self.frame_shape[0], stretch=True)
self.set_height(self.frame_shape[1], stretch=True)
self.move_to(self.center_point)
self.default_orientation = Rotation.identity()
self.view_matrix = np.identity(4)
self.camera_location = OUT # This will be updated by set_points
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
self.set_width(frame_shape[0], stretch=True)
self.set_height(frame_shape[1], stretch=True)
self.move_to(center_point)
def note_changed_data(self, recurse_up: bool = True):
super().note_changed_data(recurse_up)
self.get_view_matrix(refresh=True)
self.get_implied_camera_location(refresh=True)
def set_orientation(self, rotation: Rotation):
self.uniforms["orientation"][:] = rotation.as_quat()
@ -50,15 +53,21 @@ class CameraFrame(Mobject):
def get_orientation(self):
return Rotation.from_quat(self.uniforms["orientation"])
def to_default_state(self):
self.center()
self.set_height(FRAME_HEIGHT)
self.set_width(FRAME_WIDTH)
self.set_orientation(Rotation.identity())
def make_orientation_default(self):
self.default_orientation = self.get_orientation()
return self
def get_euler_angles(self):
return self.get_orientation().as_euler("zxz")[::-1]
def to_default_state(self):
self.set_shape(*FRAME_SHAPE)
self.center()
self.set_orientation(self.default_orientation)
return self
def get_euler_angles(self) -> np.ndarray:
orientation = self.get_orientation()
if all(orientation.as_quat() == [0, 0, 0, 1]):
return np.zeros(3)
return orientation.as_euler("zxz")[::-1]
def get_theta(self):
return self.get_euler_angles()[0]
@ -69,22 +78,40 @@ class CameraFrame(Mobject):
def get_gamma(self):
return self.get_euler_angles()[2]
def get_scale(self):
return self.get_height() / FRAME_SHAPE[1]
def get_inverse_camera_rotation_matrix(self):
return self.get_orientation().as_matrix().T
def get_view_matrix(self):
def get_view_matrix(self, refresh=False):
"""
Returns a 4x4 for the affine transformation mapping a point
into the camera's internal coordinate system
"""
result = self.view_matrix
result[:] = np.identity(4)
result[:3, 3] = -self.get_center()
rotation = np.identity(4)
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
result[:] = np.dot(rotation, result)
return result
if refresh:
shift = np.identity(4)
rotation = np.identity(4)
scale_mat = np.identity(4)
shift[:3, 3] = -self.get_center()
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
scale = self.get_scale()
if scale > 0:
scale_mat[:3, :3] /= self.get_scale()
self.view_matrix = np.dot(scale_mat, np.dot(rotation, shift))
return self.view_matrix
def get_inv_view_matrix(self):
return np.linalg.inv(self.get_view_matrix())
@Mobject.affects_data
def interpolate(self, *args, **kwargs):
super().interpolate(*args, **kwargs)
@Mobject.affects_data
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
rot = Rotation.from_rotvec(angle * normalize(axis))
self.set_orientation(rot * self.get_orientation())
@ -101,7 +128,11 @@ class CameraFrame(Mobject):
for i, var in enumerate([theta, phi, gamma]):
if var is not None:
eulers[i] = var * units
self.set_orientation(Rotation.from_euler("zxz", eulers[::-1]))
if all(eulers == 0):
rot = Rotation.identity()
else:
rot = Rotation.from_euler("zxz", eulers[::-1])
self.set_orientation(rot)
return self
def reorient(
@ -139,16 +170,20 @@ class CameraFrame(Mobject):
return self
def set_focal_distance(self, focal_distance: float):
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
return self
def set_field_of_view(self, field_of_view: float):
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
self.uniforms["fovy"] = field_of_view
return self
def get_shape(self):
return (self.get_width(), self.get_height())
def get_aspect_ratio(self):
width, height = self.get_shape()
return width / height
def get_center(self) -> np.ndarray:
# Assumes first point is at the center
return self.get_points()[0]
@ -162,12 +197,24 @@ class CameraFrame(Mobject):
return points[4, 1] - points[3, 1]
def get_focal_distance(self) -> float:
return self.uniforms["focal_dist_to_height"] * self.get_height()
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
def get_field_of_view(self) -> float:
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
return self.uniforms["fovy"]
def get_implied_camera_location(self) -> np.ndarray:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
return self.get_center() + dist * to_camera
def get_implied_camera_location(self, refresh=False) -> np.ndarray:
if refresh:
to_camera = self.get_inverse_camera_rotation_matrix()[2]
dist = self.get_focal_distance()
self.camera_location = self.get_center() + dist * to_camera
return self.camera_location
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
view = self.get_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, view.T)[:3]
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
inv_view = self.get_inv_view_matrix()
point4d = [*point, 0 if relative else 1]
return np.dot(point4d, inv_view.T)[:3]

View file

@ -11,6 +11,7 @@ if TYPE_CHECKING:
ASPECT_RATIO: float = 16.0 / 9.0
FRAME_HEIGHT: float = 8.0
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
FRAME_X_RADIUS: float = FRAME_WIDTH / 2

View file

@ -2,14 +2,14 @@ from __future__ import annotations
import numpy as np
from manimlib.event_handler.event_listner import EventListner
from manimlib.event_handler.event_listner import EventListener
from manimlib.event_handler.event_type import EventType
class EventDispatcher(object):
def __init__(self):
self.event_listners: dict[
EventType, list[EventListner]
EventType, list[EventListener]
] = {
event_type: []
for event_type in EventType
@ -17,15 +17,15 @@ class EventDispatcher(object):
self.mouse_point = np.array((0., 0., 0.))
self.mouse_drag_point = np.array((0., 0., 0.))
self.pressed_keys: set[int] = set()
self.draggable_object_listners: list[EventListner] = []
self.draggable_object_listners: list[EventListener] = []
def add_listner(self, event_listner: EventListner):
assert(isinstance(event_listner, EventListner))
def add_listner(self, event_listner: EventListener):
assert(isinstance(event_listner, EventListener))
self.event_listners[event_listner.event_type].append(event_listner)
return self
def remove_listner(self, event_listner: EventListner):
assert(isinstance(event_listner, EventListner))
def remove_listner(self, event_listner: EventListener):
assert(isinstance(event_listner, EventListener))
try:
while event_listner in self.event_listners[event_listner.event_type]:
self.event_listners[event_listner.event_type].remove(event_listner)
@ -56,7 +56,7 @@ class EventDispatcher(object):
if event_type == EventType.MouseDragEvent:
for listner in self.draggable_object_listners:
assert(isinstance(listner, EventListner))
assert(isinstance(listner, EventListener))
propagate_event = listner.callback(listner.mobject, event_data)
if propagate_event is not None and propagate_event is False:
return propagate_event

View file

@ -9,7 +9,7 @@ if TYPE_CHECKING:
from manimlib.mobject.mobject import Mobject
class EventListner(object):
class EventListener(object):
def __init__(
self,
mobject: Mobject,

View file

@ -11,7 +11,7 @@ from manimlib.utils.rate_functions import smooth
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, List, Iterable
from typing import Callable, List, Iterable, Self
from manimlib.typing import ManimColor, Vect3
@ -49,7 +49,7 @@ class AnimatedBoundary(VGroup):
lambda m, dt: self.update_boundary_copies(dt)
)
def update_boundary_copies(self, dt: float) -> None:
def update_boundary_copies(self, dt: float) -> Self:
# Not actual time, but something which passes at
# an altered rate to make the implementation below
# cleaner
@ -79,6 +79,7 @@ class AnimatedBoundary(VGroup):
)
self.total_time += dt
return self
def full_family_become_partial(
self,
@ -86,7 +87,7 @@ class AnimatedBoundary(VGroup):
mob2: VMobject,
a: float,
b: float
):
) -> Self:
family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2):
@ -118,7 +119,7 @@ class TracedPath(VMobject):
self.traced_points: list[np.ndarray] = []
self.add_updater(lambda m, dt: m.update_path(dt))
def update_path(self, dt: float):
def update_path(self, dt: float) -> Self:
if dt == 0:
return self
point = self.traced_point_func().copy()

View file

@ -9,7 +9,6 @@ import itertools as it
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
from manimlib.constants import DEGREES, PI
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.mobject.functions import ParametricCurve
@ -22,6 +21,7 @@ from manimlib.mobject.svg.tex_mobject import Tex
from manimlib.mobject.types.dot_cloud import DotCloud
from manimlib.mobject.types.surface import ParametricSurface
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.utils.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import binary_search
from manimlib.utils.space_ops import angle_of_vector
@ -32,7 +32,7 @@ from manimlib.utils.space_ops import normalize
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, Type, TypeVar
from typing import Callable, Iterable, Sequence, Type, TypeVar, Optional, Self
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
@ -236,7 +236,13 @@ class CoordinateSystem(ABC):
"""
return self.input_to_graph_point(x, graph)
def bind_graph_to_func(self, graph, func, jagged=False, get_discontinuities=None):
def bind_graph_to_func(
self,
graph: VMobject,
func: Callable[[Vect3], Vect3],
jagged: bool = False,
get_discontinuities: Optional[Callable[[], Vect3]] = None
) -> VMobject:
"""
Use for graphing functions which might change over time, or change with
conditions
@ -637,6 +643,8 @@ class NumberPlane(Axes):
lines2 = VGroup()
inputs = np.arange(axis2.x_min, axis2.x_max + step, step)
for i, x in enumerate(inputs):
if abs(x) < 1e-8:
continue
new_line = line.copy()
new_line.shift(axis2.n2p(x) - axis2.n2p(0))
if i % (1 + ratio) == 0:
@ -658,7 +666,7 @@ class NumberPlane(Axes):
kwargs["buff"] = 0
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs)
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50):
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50) -> Self:
for mob in self.family_members_with_points():
num_curves = mob.get_num_curves()
if num_inserted_curves > num_curves:
@ -697,7 +705,7 @@ class ComplexPlane(NumberPlane):
skip_first: bool = True,
font_size: int = 36,
**kwargs
):
) -> Self:
if numbers is None:
numbers = self.get_default_coordinate_values(skip_first)

View file

@ -30,7 +30,7 @@ from manimlib.utils.space_ops import rotation_matrix_transpose
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Iterable
from typing import Iterable, Self, Optional
from manimlib.typing import ManimColor, Vect3, Vect3Array
@ -67,7 +67,7 @@ class TipableVMobject(VMobject):
)
# Adding, Creating, Modifying tips
def add_tip(self, at_start: bool = False, **kwargs):
def add_tip(self, at_start: bool = False, **kwargs) -> Self:
"""
Adds a tip to the TipableVMobject instance, recognising
that the endpoints might need to be switched if it's
@ -112,7 +112,7 @@ class TipableVMobject(VMobject):
tip.shift(anchor - tip.get_tip_point())
return tip
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool):
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool) -> Self:
if self.get_length() == 0:
# Zero length, put_start_and_end_on wouldn't
# work
@ -127,7 +127,7 @@ class TipableVMobject(VMobject):
self.put_start_and_end_on(start, end)
return self
def asign_tip_attr(self, tip: ArrowTip, at_start: bool):
def asign_tip_attr(self, tip: ArrowTip, at_start: bool) -> Self:
if at_start:
self.start_tip = tip
else:
@ -258,7 +258,7 @@ class Arc(TipableVMobject):
angle = angle_of_vector(self.get_end() - self.get_arc_center())
return angle % TAU
def move_arc_center_to(self, point: Vect3):
def move_arc_center_to(self, point: Vect3) -> Self:
self.shift(point - self.get_arc_center())
return self
@ -318,7 +318,7 @@ class Circle(Arc):
dim_to_match: int = 0,
stretch: bool = False,
buff: float = MED_SMALL_BUFF
):
) -> Self:
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)
@ -448,12 +448,8 @@ class Annulus(VMobject):
)
self.radius = outer_radius
# Make sure to add enough components that triangulation doesn't fail
kw = dict(
n_components=int(max(8, np.ceil(TAU / math.acos(inner_radius / outer_radius))))
)
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0, **kw)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0, **kw)
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0)
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0)
self.add_subpath(outer_path)
self.add_subpath(inner_path)
self.shift(center)
@ -479,7 +475,7 @@ class Line(TipableVMobject):
end: Vect3,
buff: float = 0,
path_arc: float = 0
):
) -> Self:
vect = end - start
dist = get_norm(vect)
if np.isclose(dist, 0):
@ -508,9 +504,10 @@ class Line(TipableVMobject):
self.set_points_as_corners([start, end])
return self
def set_path_arc(self, new_value: float) -> None:
def set_path_arc(self, new_value: float) -> Self:
self.path_arc = new_value
self.init_points()
return self
def set_start_and_end_attrs(self, start: Vect3 | Mobject, end: Vect3 | Mobject):
# If either start or end are Mobjects, this
@ -545,7 +542,7 @@ class Line(TipableVMobject):
result[:len(point)] = point
return result
def put_start_and_end_on(self, start: Vect3, end: Vect3):
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
curr_start, curr_end = self.get_start_and_end()
if np.isclose(curr_start, curr_end).all():
# Handle null lines more gracefully
@ -573,7 +570,7 @@ class Line(TipableVMobject):
def get_slope(self) -> float:
return np.tan(self.get_angle())
def set_angle(self, angle: float, about_point: Vect3 | None = None):
def set_angle(self, angle: float, about_point: Optional[Vect3] = None) -> Self:
if about_point is None:
about_point = self.get_start()
self.rotate(
@ -699,13 +696,13 @@ class Arrow(Line):
end: Vect3,
buff: float = 0,
path_arc: float = 0
):
) -> Self:
super().set_points_by_ends(start, end, buff, path_arc)
self.insert_tip_anchor()
self.create_tip_with_stroke_width()
return self
def insert_tip_anchor(self):
def insert_tip_anchor(self) -> Self:
prev_end = self.get_end()
arc_len = self.get_arc_length()
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
@ -720,7 +717,7 @@ class Arrow(Line):
return self
@Mobject.affects_data
def create_tip_with_stroke_width(self):
def create_tip_with_stroke_width(self) -> Self:
if self.get_num_points() < 3:
return self
tip_width = self.tip_width_ratio * min(
@ -731,7 +728,7 @@ class Arrow(Line):
self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3)
return self
def reset_tip(self):
def reset_tip(self) -> Self:
self.set_points_by_ends(
self.get_start(), self.get_end(),
path_arc=self.path_arc
@ -743,13 +740,13 @@ class Arrow(Line):
color: ManimColor | Iterable[ManimColor] | None = None,
width: float | Iterable[float] | None = None,
*args, **kwargs
):
) -> Self:
super().set_stroke(color=color, width=width, *args, **kwargs)
if self.has_points():
self.reset_tip()
return self
def _handle_scale_side_effects(self, scale_factor: float):
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
if scale_factor != 1.0:
self.reset_tip()
return self
@ -791,7 +788,7 @@ class FillArrow(Line):
end: Vect3,
buff: float = 0,
path_arc: float = 0
) -> None:
) -> Self:
# Find the right tip length and thickness
vect = end - start
length = max(get_norm(vect), 1e-8)
@ -852,8 +849,9 @@ class FillArrow(Line):
axis=rotate_vector(self.get_unit_vector(), -PI / 2),
)
self.shift(start - self.get_start())
return self
def reset_points_around_ends(self):
def reset_points_around_ends(self) -> Self:
self.set_points_by_ends(
self.get_start().copy(),
self.get_end().copy(),
@ -868,21 +866,21 @@ class FillArrow(Line):
def get_end(self) -> Vect3:
return self.get_points()[self.tip_index]
def put_start_and_end_on(self, start: Vect3, end: Vect3):
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
return self
def scale(self, *args, **kwargs):
def scale(self, *args, **kwargs) -> Self:
super().scale(*args, **kwargs)
self.reset_points_around_ends()
return self
def set_thickness(self, thickness: float):
def set_thickness(self, thickness: float) -> Self:
self.thickness = thickness
self.reset_points_around_ends()
return self
def set_path_arc(self, path_arc: float):
def set_path_arc(self, path_arc: float) -> Self:
self.path_arc = path_arc
self.reset_points_around_ends()
return self
@ -925,7 +923,7 @@ class Polygon(VMobject):
def get_vertices(self) -> Vect3Array:
return self.get_start_anchors()
def round_corners(self, radius: float | None = None):
def round_corners(self, radius: Optional[float] = None) -> Self:
if radius is None:
verts = self.get_vertices()
min_edge_length = min(

View file

@ -18,7 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence
from typing import Sequence, Self
import numpy.typing as npt
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3
@ -76,7 +76,7 @@ class Matrix(VMobject):
self,
matrix: Sequence[Sequence[str | float | VMobject]],
v_buff: float = 0.8,
h_buff: float = 1.3,
h_buff: float = 1.0,
bracket_h_buff: float = 0.2,
bracket_v_buff: float = 0.25,
add_background_rectangles_to_entries: bool = False,
@ -129,7 +129,7 @@ class Matrix(VMobject):
v_buff: float,
h_buff: float,
aligned_corner: Vect3,
):
) -> Self:
for i, row in enumerate(matrix):
for j, elem in enumerate(row):
mob = matrix[i][j]
@ -139,7 +139,7 @@ class Matrix(VMobject):
)
return self
def add_brackets(self, v_buff: float, h_buff: float):
def add_brackets(self, v_buff: float, h_buff: float) -> Self:
height = len(self.mob_matrix)
brackets = Tex("".join((
R"\left[\begin{array}{c}",
@ -168,13 +168,13 @@ class Matrix(VMobject):
for row in self.mob_matrix
])
def set_column_colors(self, *colors: ManimColor):
def set_column_colors(self, *colors: ManimColor) -> Self:
columns = self.get_columns()
for color, column in zip(colors, columns):
column.set_color(color)
return self
def add_background_to_entries(self):
def add_background_to_entries(self) -> Self:
for mob in self.get_entries():
mob.add_background_rectangle()
return self

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import TypeVar
from typing import TypeVar, Self
from manimlib.typing import ManimColor, Vect3
T = TypeVar("T", bound=VMobject)
@ -163,7 +163,7 @@ class DecimalNumber(VMobject):
def get_tex(self):
return self.num_string
def set_value(self, number: float | complex):
def set_value(self, number: float | complex) -> Self:
move_to_point = self.get_edge_center(self.edge_to_fix)
style = self.family_members_with_points()[0].get_style()
self.set_submobjects_from_number(number)
@ -171,14 +171,16 @@ class DecimalNumber(VMobject):
self.set_style(**style)
return self
def _handle_scale_side_effects(self, scale_factor: float) -> None:
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
self.uniforms["font_size"] = scale_factor * self.uniforms["font_size"]
return self
def get_value(self) -> float | complex:
return self.number
def increment_value(self, delta_t: float | complex = 1) -> None:
def increment_value(self, delta_t: float | complex = 1) -> Self:
self.set_value(self.get_value() + delta_t)
return self
class Integer(DecimalNumber):

View file

@ -14,7 +14,7 @@ from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Sequence
from typing import Sequence, Self
from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor
@ -60,20 +60,20 @@ class BackgroundRectangle(SurroundingRectangle):
)
self.original_fill_opacity = fill_opacity
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float):
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> Self:
self.set_fill(opacity=b * self.original_fill_opacity)
return self
def set_style_data(
def set_style(
self,
stroke_color: ManimColor | None = None,
stroke_width: float | None = None,
fill_color: ManimColor | None = None,
fill_opacity: float | None = None,
family: bool = True
):
) -> Self:
# Unchangeable style, except for fill_opacity
VMobject.set_style_data(
VMobject.set_style(
self,
stroke_color=BLACK,
stroke_width=0,

View file

@ -251,8 +251,6 @@ class Laptop(VGroup):
self.axis = axis
self.add(body, screen_plate, axis)
self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN)
self.rotate(np.pi / 6, DOWN, about_point=ORIGIN)
class VideoIcon(SVGMobject):
@ -383,7 +381,6 @@ class Bubble(SVGMobject):
self.flip()
self.content = Mobject()
self.refresh_triangulation()
def get_tip(self):
# TODO, find a better way

View file

@ -30,10 +30,7 @@ class SingleStringTex(SVGMobject):
fill_opacity: float = 1.0,
stroke_width: float = 0,
svg_default: dict = dict(fill_color=WHITE),
path_string_config: dict = dict(
should_subdivide_sharp_curves=True,
should_remove_null_curves=True,
),
path_string_config: dict = dict(),
font_size: int = 48,
alignment: str = R"\centering",
math_mode: bool = True,

View file

@ -295,15 +295,11 @@ class VMobjectFromSVGPath(VMobject):
def __init__(
self,
path_obj: se.Path,
should_subdivide_sharp_curves: bool = False,
should_remove_null_curves: bool = True,
**kwargs
):
# Get rid of arcs
path_obj.approximate_arcs_with_quads()
self.path_obj = path_obj
self.should_subdivide_sharp_curves = should_subdivide_sharp_curves
self.should_remove_null_curves = should_remove_null_curves
super().__init__(**kwargs)
def init_points(self) -> None:
@ -313,14 +309,6 @@ class VMobjectFromSVGPath(VMobject):
path_string = self.path_obj.d()
if path_string not in PATH_TO_POINTS:
self.handle_commands()
if self.should_subdivide_sharp_curves:
# For a healthy triangulation later
self.subdivide_sharp_curves()
if self.should_remove_null_curves:
# Get rid of any null curves
self.set_points(self.get_points_without_null_curves())
# So triangulation doesn't get messed up
self.subdivide_intersections()
# Save for future use
PATH_TO_POINTS[path_string] = self.get_points().copy()
else:

View file

@ -65,7 +65,7 @@ class SurfaceMesh(VGroup):
u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv)
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
points = uv_surface.get_points()
normals = uv_surface.get_unit_normals()
nudge = self.normal_nudge
nudged_points = points + nudge * normals
@ -96,7 +96,7 @@ class Sphere(Surface):
def __init__(
self,
u_range: Tuple[float, float] = (0, TAU),
v_range: Tuple[float, float] = (0, PI),
v_range: Tuple[float, float] = (1e-5, PI - 1e-5),
resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0,
**kwargs,
@ -166,7 +166,6 @@ class Cylinder(Surface):
self.scale(self.radius)
self.set_depth(self.height, stretch=True)
self.apply_matrix(z_to_vector(self.axis))
return self
def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([np.cos(u), np.sin(u), v])
@ -186,6 +185,7 @@ class Line3D(Cylinder):
height=get_norm(axis),
radius=width / 2,
axis=axis,
resolution=resolution,
**kwargs
)
self.shift((start + end) / 2)
@ -376,16 +376,6 @@ class Dodecahedron(VGroup3D):
super().__init__(*pentagons, **style)
# # Rotate those two pentagons by all the axis permuations to fill
# # out the dodecahedron
# Id = np.identity(3)
# for i in range(3):
# perm = [j % 3 for j in range(i, i + 3)]
# for b in [1, -1]:
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
class Prismify(VGroup3D):
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):

View file

@ -13,7 +13,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
import numpy.typing as npt
from typing import Sequence, Tuple
from typing import Sequence, Tuple, Self
from manimlib.typing import ManimColor, Vect3, Vect3Array
@ -70,7 +70,7 @@ class DotCloud(PMobject):
v_buff_ratio: float = 1.0,
d_buff_ratio: float = 1.0,
height: float = DEFAULT_GRID_HEIGHT,
):
) -> Self:
n_points = n_rows * n_cols * n_layers
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
points[:, 0] = points[:, 0] % n_cols
@ -96,7 +96,7 @@ class DotCloud(PMobject):
return self
@Mobject.affects_data
def set_radii(self, radii: npt.ArrayLike):
def set_radii(self, radii: npt.ArrayLike) -> Self:
n_points = self.get_num_points()
radii = np.array(radii).reshape((len(radii), 1))
self.data["radius"][:] = resize_with_interpolation(radii, n_points)
@ -107,7 +107,7 @@ class DotCloud(PMobject):
return self.data["radius"]
@Mobject.affects_data
def set_radius(self, radius: float):
def set_radius(self, radius: float) -> Self:
data = self.data if self.get_num_points() > 0 else self._data_defaults
data["radius"][:] = radius
self.refresh_bounding_box()
@ -116,13 +116,14 @@ class DotCloud(PMobject):
def get_radius(self) -> float:
return self.get_radii().max()
def set_glow_factor(self, glow_factor: float) -> None:
def set_glow_factor(self, glow_factor: float) -> Self:
self.uniforms["glow_factor"] = glow_factor
return self
def get_glow_factor(self) -> float:
return self.uniforms["glow_factor"]
def compute_bounding_box(self) -> np.ndarray:
def compute_bounding_box(self) -> Vect3Array:
bb = super().compute_bounding_box()
radius = self.get_radius()
bb[0] += np.full((3,), -radius)
@ -134,7 +135,7 @@ class DotCloud(PMobject):
scale_factor: float | npt.ArrayLike,
scale_radii: bool = True,
**kwargs
):
) -> Self:
super().scale(scale_factor, **kwargs)
if scale_radii:
self.set_radii(scale_factor * self.get_radii())
@ -145,7 +146,7 @@ class DotCloud(PMobject):
reflectiveness: float = 0.5,
gloss: float = 0.1,
shadow: float = 0.2
):
) -> Self:
self.set_shading(reflectiveness, gloss, shadow)
self.apply_depth_test()
return self

View file

@ -10,7 +10,7 @@ from manimlib.utils.iterables import resize_with_interpolation
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable
from typing import Callable, Self
from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array
@ -28,7 +28,7 @@ class PMobject(Mobject):
rgbas: Vect4Array | None = None,
color: ManimColor | None = None,
opacity: float | None = None
):
) -> Self:
"""
points must be a Nx3 numpy array, as must rgbas if it is not None
"""
@ -46,13 +46,13 @@ class PMobject(Mobject):
self.data["rgba"][-len(rgbas):] = rgbas
return self
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None):
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None) -> Self:
rgbas = None if rgba is None else [rgba]
self.add_points([point], rgbas, color, opacity)
return self
@Mobject.affects_data
def set_color_by_gradient(self, *colors: ManimColor):
def set_color_by_gradient(self, *colors: ManimColor) -> Self:
self.data["rgba"][:] = np.array(list(map(
color_to_rgba,
color_gradient(colors, self.get_num_points())
@ -60,20 +60,20 @@ class PMobject(Mobject):
return self
@Mobject.affects_data
def match_colors(self, pmobject: PMobject):
def match_colors(self, pmobject: PMobject) -> Self:
self.data["rgba"][:] = resize_with_interpolation(
pmobject.data["rgba"], self.get_num_points()
)
return self
@Mobject.affects_data
def filter_out(self, condition: Callable[[np.ndarray], bool]):
def filter_out(self, condition: Callable[[np.ndarray], bool]) -> Self:
for mob in self.family_members_with_points():
mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())]
return self
@Mobject.affects_data
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]):
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]) -> Self:
"""
function is any map from R^3 to R
"""
@ -85,7 +85,7 @@ class PMobject(Mobject):
return self
@Mobject.affects_data
def ingest_submobjects(self):
def ingest_submobjects(self) -> Self:
self.data = np.vstack([
sm.data for sm in self.get_family()
])
@ -96,7 +96,7 @@ class PMobject(Mobject):
return self.get_points()[int(index)]
@Mobject.affects_data
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float):
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float) -> Self:
lower_index = int(a * pmobject.get_num_points())
upper_index = int(b * pmobject.get_num_points())
self.data = pmobject.data[lower_index:upper_index].copy()

View file

@ -12,11 +12,12 @@ from manimlib.utils.images import get_full_raster_image_path
from manimlib.utils.iterables import listify
from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import cross
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, Tuple
from typing import Callable, Iterable, Sequence, Tuple, Self
from manimlib.camera.camera import Camera
from manimlib.typing import ManimColor, Vect3, Vect3Array
@ -27,11 +28,9 @@ class Surface(Mobject):
shader_folder: str = "surface"
shader_dtype: np.dtype = np.dtype([
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('normal', np.float32, (3,)),
('rgba', np.float32, (4,)),
])
pointlike_data_keys = ['point', 'du_point', 'dv_point']
def __init__(
self,
@ -96,17 +95,24 @@ class Surface(Mobject):
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
]
self.set_points(points)
self.data["du_point"][:] = du_points
self.data["dv_point"][:] = dv_points
self.data["normal"] = normalize_along_axis(cross(
(du_points - points) / self.epsilon,
(dv_points - points) / self.epsilon,
), 1)
def compute_triangle_indices(self):
def apply_points_function(self, *args, **kwargs) -> Self:
super().apply_points_function(*args, **kwargs)
self.get_unit_normals()
return self
def compute_triangle_indices(self) -> np.ndarray:
# TODO, if there is an event which changes
# the resolution of the surface, make sure
# this is called.
nu, nv = self.resolution
if nu == 0 or nv == 0:
self.triangle_indices = np.zeros(0, dtype=int)
return
return self.triangle_indices
index_grid = np.arange(nu * nv).reshape((nu, nv))
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left
@ -116,20 +122,32 @@ class Surface(Mobject):
indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
self.triangle_indices = indices
return self.triangle_indices
def get_triangle_indices(self) -> np.ndarray:
return self.triangle_indices
def get_surface_points_and_nudged_points(self) -> tuple[Vect3Array, Vect3Array, Vect3Array]:
return (self.data['point'], self.data['du_point'], self.data['dv_point'])
def get_unit_normals(self) -> Vect3Array:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
normals = np.cross(
(du_points - s_points) / self.epsilon,
(dv_points - s_points) / self.epsilon,
nu, nv = self.resolution
indices = np.arange(nu * nv)
left = indices - 1
right = indices + 1
up = indices - nv
down = indices + nv
left[0] = indices[0]
right[-1] = indices[-1]
up[:nv] = indices[:nv]
down[-nv:] = indices[-nv:]
points = self.get_points()
crosses = cross(
points[right] - points[left],
points[up] - points[down],
)
return normalize_along_axis(normals, 1)
self.data["normal"] = normalize_along_axis(crosses, 1)
return self.data["normal"]
@Mobject.affects_data
def pointwise_become_partial(
@ -138,7 +156,7 @@ class Surface(Mobject):
a: float,
b: float,
axis: int | None = None
):
) -> Self:
assert(isinstance(smobject, Surface))
if axis is None:
axis = self.prefered_creation_axis
@ -147,12 +165,11 @@ class Surface(Mobject):
return self
nu, nv = smobject.resolution
for key in ['point', 'du_point', 'dv_point']:
self.data[key][:] = self.get_partial_points_array(
self.data[key], a, b,
(nu, nv, 3),
axis=axis
)
self.data['point'][:] = self.get_partial_points_array(
self.data['point'], a, b,
(nu, nv, 3),
axis=axis
)
return self
def get_partial_points_array(
@ -196,7 +213,7 @@ class Surface(Mobject):
return points.reshape((nu * nv, *resolution[2:]))
@Mobject.affects_data
def sort_faces_back_to_front(self, vect: Vect3 = OUT):
def sort_faces_back_to_front(self, vect: Vect3 = OUT) -> Self:
tri_is = self.triangle_indices
points = self.get_points()
@ -206,24 +223,25 @@ class Surface(Mobject):
tri_is[k::3] = tri_is[k::3][indices]
return self
def always_sort_to_camera(self, camera: Camera):
def always_sort_to_camera(self, camera: Camera) -> Self:
def updater(surface: Surface):
vect = camera.get_location() - surface.get_center()
surface.sort_faces_back_to_front(vect)
self.add_updater(updater)
return self
def set_clip_plane(
self,
vect: Vect3 | None = None,
threshold: float | None = None
):
) -> Self:
if vect is not None:
self.uniforms["clip_plane"][:3] = vect
if threshold is not None:
self.uniforms["clip_plane"][3] = threshold
return self
def deactivate_clip_plane(self):
def deactivate_clip_plane(self) -> Self:
self.uniforms["clip_plane"][:] = 0
return self
@ -263,8 +281,7 @@ class TexturedSurface(Surface):
shader_folder: str = "textured_surface"
shader_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)),
('du_point', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('normal', np.float32, (3,)),
('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)),
]
@ -306,8 +323,9 @@ class TexturedSurface(Surface):
surf = self.uv_surface
nu, nv = surf.resolution
self.resize_points(surf.get_num_points())
for key in ['point', 'du_point', 'dv_point']:
self.data[key][:] = surf.data[key]
self.resolution = surf.resolution
self.data['point'][:] = surf.data['point']
self.data['normal'][:] = surf.data['normal']
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
self.data["im_coords"] = np.array([
[u, v]
@ -320,7 +338,7 @@ class TexturedSurface(Surface):
self.uniforms["num_textures"] = self.num_textures
@Mobject.affects_data
def set_opacity(self, opacity: float | Iterable[float]):
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
op_arr = np.array(listify(opacity))
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
return self
@ -330,7 +348,7 @@ class TexturedSurface(Surface):
color: ManimColor | Iterable[ManimColor] | None,
opacity: float | Iterable[float] | None = None,
recurse: bool = True
):
) -> Self:
if opacity is not None:
self.set_opacity(opacity)
return self
@ -341,7 +359,7 @@ class TexturedSurface(Surface):
a: float,
b: float,
axis: int = 1
):
) -> Self:
super().pointwise_become_partial(tsmobject, a, b, axis)
im_coords = self.data["im_coords"]
im_coords[:] = tsmobject.data["im_coords"]

View file

@ -45,7 +45,7 @@ from manimlib.shader_wrapper import FillShaderWrapper
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Tuple
from typing import Callable, Iterable, Tuple, Any, Self
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array
from moderngl.context import Context
@ -68,8 +68,8 @@ class VMobject(Mobject):
fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal']
stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
fill_render_primitive: int = moderngl.TRIANGLE_STRIP
stroke_render_primitive: int = moderngl.TRIANGLE_STRIP
fill_render_primitive: int = moderngl.TRIANGLES
stroke_render_primitive: int = moderngl.TRIANGLES
pre_function_handle_to_anchor_scale_factor: float = 0.01
make_smooth_after_applying_functions: bool = False
@ -128,39 +128,10 @@ class VMobject(Mobject):
self.uniforms["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
self.uniforms["flat_stroke"] = float(self.flat_stroke)
# These are here just to make type checkers happy
def get_family(self, recurse: bool = True) -> list[VMobject]:
return super().get_family(recurse)
def family_members_with_points(self) -> list[VMobject]:
return super().family_members_with_points()
def replicate(self, n: int) -> VGroup:
return super().replicate(n)
def get_grid(self, *args, **kwargs) -> VGroup:
return super().get_grid(*args, **kwargs)
def __getitem__(self, value: int | slice) -> VMobject:
return super().__getitem__(value)
def add(self, *vmobjects: VMobject):
def add(self, *vmobjects: VMobject) -> Self:
if not all((isinstance(m, VMobject) for m in vmobjects)):
raise Exception("All submobjects must be of type VMobject")
super().add(*vmobjects)
def add_background_rectangle(
self,
color: ManimColor | None = None,
opacity: float = 0.75,
**kwargs
):
normal = self.family_members_with_points()[0].get_unit_normal()
super().add_background_rectangle(color, opacity, **kwargs)
rect = self.background_rectangle
if np.dot(rect.get_unit_normal(), normal) < 0:
rect.reverse_points()
return self
return super().add(*vmobjects)
# Colors
def init_colors(self):
@ -185,7 +156,7 @@ class VMobject(Mobject):
rgba_array: Vect4Array,
name: str | None = None,
recurse: bool = False
):
) -> Self:
if name is None:
names = ["fill_rgba", "stroke_rgba"]
else:
@ -201,7 +172,7 @@ class VMobject(Mobject):
opacity: float | Iterable[float] | None = None,
border_width: float | None = None,
recurse: bool = True
):
) -> Self:
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
if border_width is not None:
for mob in self.get_family(recurse):
@ -215,7 +186,7 @@ class VMobject(Mobject):
opacity: float | Iterable[float] | None = None,
background: bool | None = None,
recurse: bool = True
):
) -> Self:
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
if width is not None:
@ -238,7 +209,7 @@ class VMobject(Mobject):
color: ManimColor | Iterable[ManimColor] = BLACK,
width: float | Iterable[float] = 3,
background: bool = True
):
) -> Self:
self.set_stroke(color, width, background=background)
return self
@ -255,7 +226,7 @@ class VMobject(Mobject):
stroke_background: bool = True,
shading: Tuple[float, float, float] | None = None,
recurse: bool = True
):
) -> Self:
for mob in self.get_family(recurse):
if fill_rgba is not None:
mob.data['fill_rgba'][:] = resize_with_interpolation(fill_rgba, len(mob.data['fill_rgba']))
@ -286,7 +257,7 @@ class VMobject(Mobject):
mob.set_shading(*shading, recurse=False)
return self
def get_style(self):
def get_style(self) -> dict[str, Any]:
data = self.data if self.get_num_points() > 0 else self._data_defaults
return {
"fill_rgba": data['fill_rgba'].copy(),
@ -296,7 +267,7 @@ class VMobject(Mobject):
"shading": self.get_shading(),
}
def match_style(self, vmobject: VMobject, recurse: bool = True):
def match_style(self, vmobject: VMobject, recurse: bool = True) -> Self:
self.set_style(**vmobject.get_style(), recurse=False)
if recurse:
# Does its best to match up submobject lists, and
@ -315,7 +286,7 @@ class VMobject(Mobject):
color: ManimColor | Iterable[ManimColor] | None,
opacity: float | Iterable[float] | None = None,
recurse: bool = True
):
) -> Self:
self.set_fill(color, opacity=opacity, recurse=recurse)
self.set_stroke(color, opacity=opacity, recurse=recurse)
return self
@ -324,12 +295,16 @@ class VMobject(Mobject):
self,
opacity: float | Iterable[float] | None,
recurse: bool = True
):
) -> Self:
self.set_fill(opacity=opacity, recurse=recurse)
self.set_stroke(opacity=opacity, recurse=recurse)
return self
def fade(self, darkness: float = 0.5, recurse: bool = True):
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True) -> Self:
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
return self
def fade(self, darkness: float = 0.5, recurse: bool = True) -> Self:
mobs = self.get_family() if recurse else [self]
for mob in mobs:
factor = 1.0 - darkness
@ -399,6 +374,9 @@ class VMobject(Mobject):
return self.get_fill_color()
return self.get_stroke_color()
def get_anti_alias_width(self):
return self.uniforms["anti_alias_width"]
def has_stroke(self) -> bool:
return any(self.data['stroke_width']) and any(self.data['stroke_rgba'][:, 3])
@ -410,7 +388,7 @@ class VMobject(Mobject):
return self.get_fill_opacity()
return self.get_stroke_opacity()
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True):
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True) -> Self:
for mob in self.get_family(recurse):
mob.uniforms["flat_stroke"] = float(flat_stroke)
return self
@ -418,7 +396,7 @@ class VMobject(Mobject):
def get_flat_stroke(self) -> bool:
return self.uniforms["flat_stroke"] == 1.0
def set_joint_type(self, joint_type: str, recurse: bool = True):
def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self:
for mob in self.get_family(recurse):
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
return self
@ -426,10 +404,34 @@ class VMobject(Mobject):
def get_joint_type(self) -> float:
return self.uniforms["joint_type"]
def apply_depth_test(
self,
anti_alias_width: float = 0,
fill_border_width: float = 0,
recurse: bool=True
) -> Self:
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
def deactivate_depth_test(
self,
anti_alias_width: float = 1.0,
fill_border_width: float = 0.5,
recurse: bool=True
) -> Self:
super().apply_depth_test(recurse)
self.set_anti_alias_width(anti_alias_width)
self.set_fill(border_width=fill_border_width)
return self
@Mobject.affects_family_data
def use_winding_fill(self, value: bool = True, recurse: bool = True):
def use_winding_fill(self, value: bool = True, recurse: bool = True) -> Self:
for submob in self.get_family(recurse):
submob._use_winding_fill = value
if not value and submob.has_points():
submob.subdivide_intersections()
return self
# Points
@ -437,7 +439,7 @@ class VMobject(Mobject):
self,
anchors: Vect3Array,
handles: Vect3Array,
):
) -> Self:
assert(len(anchors) == len(handles) + 1)
points = resize_array(self.get_points(), 2 * len(anchors) - 1)
points[0::2] = anchors
@ -445,7 +447,7 @@ class VMobject(Mobject):
self.set_points(points)
return self
def start_new_path(self, point: Vect3):
def start_new_path(self, point: Vect3) -> Self:
# Path ends are signaled by a handle point sitting directly
# on top of the previous anchor
if self.has_points():
@ -460,7 +462,7 @@ class VMobject(Mobject):
handle1: Vect3,
handle2: Vect3,
anchor2: Vect3
):
) -> Self:
self.start_new_path(anchor1)
self.add_cubic_bezier_curve_to(handle1, handle2, anchor2)
return self
@ -470,7 +472,7 @@ class VMobject(Mobject):
handle1: Vect3,
handle2: Vect3,
anchor: Vect3,
):
) -> Self:
"""
Add cubic bezier curve to the path.
"""
@ -492,7 +494,7 @@ class VMobject(Mobject):
self.append_points(quad_approx[1:])
return self
def add_quadratic_bezier_curve_to(self, handle: Vect3, anchor: Vect3):
def add_quadratic_bezier_curve_to(self, handle: Vect3, anchor: Vect3) -> Self:
self.throw_error_if_no_points()
last_point = self.get_last_point()
if self.consider_points_equal(handle, last_point):
@ -501,14 +503,14 @@ class VMobject(Mobject):
self.append_points([handle, anchor])
return self
def add_line_to(self, point: Vect3):
def add_line_to(self, point: Vect3) -> Self:
self.throw_error_if_no_points()
last_point = self.get_last_point()
alphas = np.linspace(0, 1, 5 if self.long_lines else 3)
self.append_points(outer_interpolate(last_point, point, alphas[1:]))
return self
def add_smooth_curve_to(self, point: Vect3):
def add_smooth_curve_to(self, point: Vect3) -> Self:
if self.has_new_path_started():
self.add_line_to(point)
else:
@ -517,7 +519,7 @@ class VMobject(Mobject):
self.add_quadratic_bezier_curve_to(new_handle, point)
return self
def add_smooth_cubic_curve_to(self, handle: Vect3, point: Vect3):
def add_smooth_cubic_curve_to(self, handle: Vect3, point: Vect3) -> Self:
self.throw_error_if_no_points()
if self.get_num_points() == 1:
new_handle = handle
@ -538,7 +540,7 @@ class VMobject(Mobject):
points = self.get_points()
return 2 * points[-1] - points[-2]
def close_path(self, smooth: bool = False):
def close_path(self, smooth: bool = False) -> Self:
if self.is_closed():
return self
last_path_start = self.get_subpaths()[-1][0]
@ -556,7 +558,7 @@ class VMobject(Mobject):
self,
tuple_to_subdivisions: Callable,
recurse: bool = True
):
) -> Self:
for vmob in self.get_family(recurse):
if not vmob.has_points():
continue
@ -578,7 +580,7 @@ class VMobject(Mobject):
self,
angle_threshold: float = 30 * DEGREES,
recurse: bool = True
):
) -> Self:
def tuple_to_subdivisions(b0, b1, b2):
angle = angle_between_vectors(b1 - b0, b2 - b1)
return int(angle / angle_threshold)
@ -586,7 +588,7 @@ class VMobject(Mobject):
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self
def subdivide_intersections(self, recurse: bool = True, n_subdivisions: int = 1):
def subdivide_intersections(self, recurse: bool = True, n_subdivisions: int = 1) -> Self:
path = self.get_anchors()
def tuple_to_subdivisions(b0, b1, b2):
if line_intersects_path(b0, b1, path):
@ -596,12 +598,12 @@ class VMobject(Mobject):
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self
def add_points_as_corners(self, points: Iterable[Vect3]):
def add_points_as_corners(self, points: Iterable[Vect3]) -> Self:
for point in points:
self.add_line_to(point)
return points
return self
def set_points_as_corners(self, points: Iterable[Vect3]):
def set_points_as_corners(self, points: Iterable[Vect3]) -> Self:
anchors = np.array(points)
handles = 0.5 * (anchors[:-1] + anchors[1:])
self.set_anchors_and_handles(anchors, handles)
@ -611,7 +613,7 @@ class VMobject(Mobject):
self,
points: Iterable[Vect3],
approx: bool = True
):
) -> Self:
self.set_points_as_corners(points)
self.make_smooth(approx=approx)
return self
@ -620,7 +622,7 @@ class VMobject(Mobject):
dots = self.get_joint_products()[::2, 3]
return bool((dots > 1 - 1e-3).all())
def change_anchor_mode(self, mode: str):
def change_anchor_mode(self, mode: str) -> Self:
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
subpaths = self.get_subpaths()
self.clear_points()
@ -643,7 +645,7 @@ class VMobject(Mobject):
self.add_subpath(new_subpath)
return self
def make_smooth(self, approx=False, recurse=True):
def make_smooth(self, approx=False, recurse=True) -> Self:
"""
Edits the path so as to pass smoothly through all
the current anchor points.
@ -658,15 +660,16 @@ class VMobject(Mobject):
submob.change_anchor_mode(mode)
return self
def make_approximately_smooth(self, recurse=True):
def make_approximately_smooth(self, recurse=True) -> Self:
self.make_smooth(approx=True, recurse=recurse)
return self
def make_jagged(self, recurse=True):
def make_jagged(self, recurse=True) -> Self:
for submob in self.get_family(recurse):
submob.change_anchor_mode("jagged")
return self
def add_subpath(self, points: Vect3Array):
def add_subpath(self, points: Vect3Array) -> Self:
assert(len(points) % 2 == 1 or len(points) == 0)
if not self.has_points():
self.set_points(points)
@ -676,7 +679,7 @@ class VMobject(Mobject):
self.append_points(points[1:])
return self
def append_vectorized_mobject(self, vmobject: VMobject):
def append_vectorized_mobject(self, vmobject: VMobject) -> Self:
self.add_subpath(vmobject.get_points())
n = vmobject.get_num_points()
self.data[-n:] = vmobject.data
@ -694,7 +697,7 @@ class VMobject(Mobject):
def get_bezier_tuples(self) -> Iterable[Vect3Array]:
return self.get_bezier_tuples_from_points(self.get_points())
def get_subpath_end_indices_from_points(self, points: Vect3Array):
def get_subpath_end_indices_from_points(self, points: Vect3Array) -> np.ndarray:
atol = self.tolerance_for_point_equality
a0, h, a1 = points[0:-1:2], points[1::2], points[2::2]
# An anchor point is considered the end of a path
@ -710,7 +713,7 @@ class VMobject(Mobject):
is_end[:-1] = is_end[:-1] & ~is_end[1:]
return np.array([2 * n for n, end in enumerate(is_end) if end])
def get_subpath_end_indices(self):
def get_subpath_end_indices(self) -> np.ndarray:
return self.get_subpath_end_indices_from_points(self.get_points())
def get_subpaths_from_points(self, points: Vect3Array) -> list[Vect3Array]:
@ -839,7 +842,7 @@ class VMobject(Mobject):
self.data["unit_normal"][:] = normal
return normal
def refresh_unit_normal(self):
def refresh_unit_normal(self) -> Self:
self.get_unit_normal()
return self
@ -849,20 +852,20 @@ class VMobject(Mobject):
axis: Vect3 = OUT,
about_point: Vect3 | None = None,
**kwargs
):
) -> Self:
super().rotate(angle, axis, about_point, **kwargs)
for mob in self.get_family():
mob.refresh_unit_normal()
return self
def ensure_positive_orientation(self, recurse=True):
def ensure_positive_orientation(self, recurse=True) -> Self:
for mob in self.get_family(recurse):
if mob.get_unit_normal()[2] < 0:
mob.reverse_points()
return self
# Alignment
def align_points(self, vmobject: VMobject):
def align_points(self, vmobject: VMobject) -> Self:
winding = self._use_winding_fill and vmobject._use_winding_fill
self.use_winding_fill(winding)
vmobject.use_winding_fill(winding)
@ -870,8 +873,11 @@ class VMobject(Mobject):
# If both have fill, and they have the same shape, just
# give them the same triangulation so that it's not recalculated
# needlessly throughout an animation
if not self._use_winding_fill and self.has_fill() \
and vmobject.has_fill() and self.has_same_shape_as(vmobject):
match_tris = not self._use_winding_fill and \
self.has_fill() and \
vmobject.has_fill() and \
self.has_same_shape_as(vmobject)
if match_tris:
vmobject.triangulation = self.triangulation
return self
@ -884,6 +890,11 @@ class VMobject(Mobject):
# Figure out what the subpaths are, and align
subpaths1 = self.get_subpaths()
subpaths2 = vmobject.get_subpaths()
for subpaths in [subpaths1, subpaths2]:
subpaths.sort(key=lambda sp: -sum(
get_norm(p2 - p1)
for p1, p2 in zip(sp, sp[1:])
))
n_subpaths = max(len(subpaths1), len(subpaths2))
# Start building new ones
@ -892,8 +903,7 @@ class VMobject(Mobject):
def get_nth_subpath(path_list, n):
if n >= len(path_list):
# Create a null path at the very end
return [path_list[-1][-1]] * 3
return np.vstack([path_list[0][:-1], path_list[0][::-1]])
return path_list[n]
for n in range(n_subpaths):
@ -917,22 +927,14 @@ class VMobject(Mobject):
mob.get_joint_products()
return self
def invisible_copy(self):
result = self.copy()
if not result.has_fill() or result.get_num_points() == 0:
return result
result.append_vectorized_mobject(self.copy().reverse_points())
result.set_opacity(0)
return result
def insert_n_curves(self, n: int, recurse: bool = True):
def insert_n_curves(self, n: int, recurse: bool = True) -> Self:
for mob in self.get_family(recurse):
if mob.get_num_curves() > 0:
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
mob.set_points(new_points)
return self
def insert_n_curves_to_point_list(self, n: int, points: Vect3Array):
def insert_n_curves_to_point_list(self, n: int, points: Vect3Array) -> Vect3Array:
if len(points) == 1:
return np.repeat(points, 2 * n + 1, 0)
@ -965,7 +967,7 @@ class VMobject(Mobject):
mobject2: VMobject,
alpha: float,
*args, **kwargs
):
) -> Self:
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
if self.has_fill() and not self._use_winding_fill:
tri1 = mobject1.get_triangulation()
@ -974,7 +976,7 @@ class VMobject(Mobject):
self.refresh_triangulation()
return self
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float):
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float) -> Self:
assert(isinstance(vmobject, VMobject))
vm_points = vmobject.get_points()
self.data["joint_product"] = vmobject.data["joint_product"]
@ -1017,12 +1019,12 @@ class VMobject(Mobject):
self.set_points(new_points, refresh_joints=False)
return self
def get_subcurve(self, a: float, b: float) -> VMobject:
def get_subcurve(self, a: float, b: float) -> Self:
vmob = self.copy()
vmob.pointwise_become_partial(self, a, b)
return vmob
def get_outer_vert_indices(self):
def get_outer_vert_indices(self) -> np.ndarray:
"""
Returns the pattern (0, 1, 2, 2, 3, 4, 4, 5, 6, ...)
"""
@ -1033,12 +1035,12 @@ class VMobject(Mobject):
# Data for shaders that may need refreshing
def refresh_triangulation(self):
def refresh_triangulation(self) -> Self:
for mob in self.get_family():
mob.needs_new_triangulation = True
return self
def get_triangulation(self):
def get_triangulation(self) -> np.ndarray:
# Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader.
# First triangles come directly from the points
@ -1095,12 +1097,12 @@ class VMobject(Mobject):
self.needs_new_triangulation = False
return tri_indices
def refresh_joint_products(self):
def refresh_joint_products(self) -> Self:
for mob in self.get_family():
mob.needs_new_joint_products = True
return self
def get_joint_products(self, refresh: bool = False):
def get_joint_products(self, refresh: bool = False) -> np.ndarray:
"""
The 'joint product' is a 4-vector holding the cross and dot
product between tangent vectors at a joint
@ -1151,10 +1153,11 @@ class VMobject(Mobject):
self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1)
return self.data["joint_product"]
def lock_matching_data(self, vmobject1: VMobject, vmobject2: VMobject):
def lock_matching_data(self, vmobject1: VMobject, vmobject2: VMobject) -> Self:
for mob in [self, vmobject1, vmobject2]:
mob.get_joint_products()
super().lock_matching_data(vmobject1, vmobject2)
return self
def triggers_refreshed_triangulation(func: Callable):
@wraps(func)
@ -1166,7 +1169,7 @@ class VMobject(Mobject):
return self
return wrapper
def set_points(self, points: Vect3Array, refresh_joints: bool = True):
def set_points(self, points: Vect3Array, refresh_joints: bool = True) -> Self:
assert(len(points) == 0 or len(points) % 2 == 1)
super().set_points(points)
self.refresh_triangulation()
@ -1176,13 +1179,13 @@ class VMobject(Mobject):
return self
@triggers_refreshed_triangulation
def append_points(self, points: Vect3Array):
def append_points(self, points: Vect3Array) -> Self:
assert(len(points) % 2 == 0)
super().append_points(points)
return self
@triggers_refreshed_triangulation
def reverse_points(self, recurse: bool = True):
def reverse_points(self, recurse: bool = True) -> Self:
# This will reset which anchors are
# considered path ends
for mob in self.get_family(recurse):
@ -1195,7 +1198,7 @@ class VMobject(Mobject):
return self
@triggers_refreshed_triangulation
def set_data(self, data: np.ndarray):
def set_data(self, data: np.ndarray) -> Self:
super().set_data(data)
return self
@ -1206,15 +1209,25 @@ class VMobject(Mobject):
function: Callable[[Vect3], Vect3],
make_smooth: bool = False,
**kwargs
):
) -> Self:
super().apply_function(function, **kwargs)
if self.make_smooth_after_applying_functions or make_smooth:
self.make_smooth(approx=True)
return self
def apply_points_function(self, *args, **kwargs):
def apply_points_function(self, *args, **kwargs) -> Self:
super().apply_points_function(*args, **kwargs)
self.refresh_joint_products()
return self
def set_animating_status(self, is_animating: bool, recurse: bool = True):
super().set_animating_status(is_animating, recurse)
if is_animating:
for submob in self.get_family(recurse):
submob.get_joint_products(refresh=True)
if not submob._use_winding_fill:
submob.get_triangulation()
return self
# For shaders
def init_shader_data(self, ctx: Context):
@ -1249,7 +1262,7 @@ class VMobject(Mobject):
self.stroke_shader_wrapper,
]
def refresh_shader_wrapper_id(self):
def refresh_shader_wrapper_id(self) -> Self:
if not self._shaders_initialized:
return self
for wrapper in self.shader_wrappers:
@ -1269,43 +1282,40 @@ class VMobject(Mobject):
# Build up data lists
fill_datas = []
fill_border_datas = []
fill_indices = []
fill_border_datas = []
stroke_datas = []
back_stroke_datas = []
for submob in family:
submob.get_joint_products()
indices = submob.get_outer_vert_indices()
has_fill = submob.has_fill()
has_stroke = submob.has_stroke()
if has_fill:
back_stroke = has_stroke and submob.stroke_behind
front_stroke = has_stroke and not submob.stroke_behind
if back_stroke:
back_stroke_datas.append(submob.data[stroke_names][indices])
if front_stroke:
stroke_datas.append(submob.data[stroke_names][indices])
if has_fill and submob._use_winding_fill:
data = submob.data[fill_names]
data["base_point"][:] = data["point"][0]
fill_datas.append(data)
if self._use_winding_fill:
# Add dummy
fill_datas.append(data[-1:])
else:
fill_indices.append(submob.get_triangulation())
fill_datas.append(data[indices])
if has_fill and not submob._use_winding_fill:
fill_datas.append(submob.data[fill_names])
fill_indices.append(submob.get_triangulation())
if has_fill and not front_stroke:
# Add fill border
if not has_stroke:
names = list(stroke_names)
names[names.index('stroke_rgba')] = 'fill_rgba'
names[names.index('stroke_width')] = 'fill_border_width'
border_stroke_data = submob.data[names]
fill_border_datas.append(border_stroke_data)
fill_border_datas.append(border_stroke_data[-1:])
if has_stroke:
lst = back_stroke_datas if submob.stroke_behind else stroke_datas
lst.append(submob.data[stroke_names])
# Set data array to be one longer than number of points,
# with a dummy vertex added at the end. This is to ensure
# it can be safely stacked onto other stroke data arrays.
lst.append(submob.data[stroke_names][-1:])
names = list(stroke_names)
names[names.index('stroke_rgba')] = 'fill_rgba'
names[names.index('stroke_width')] = 'fill_border_width'
border_stroke_data = submob.data[names].astype(
self.stroke_shader_wrapper.vert_data.dtype
)
fill_border_datas.append(border_stroke_data[indices])
shader_wrappers = [
self.back_stroke_shader_wrapper.read_in(
[*back_stroke_datas, *fill_border_datas]
),
self.back_stroke_shader_wrapper.read_in([*back_stroke_datas, *fill_border_datas]),
self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None),
self.stroke_shader_wrapper.read_in(stroke_datas),
]
@ -1319,7 +1329,7 @@ class VGroup(VMobject):
super().__init__(**kwargs)
self.add(*vmobjects)
def __add__(self, other: VMobject | VGroup):
def __add__(self, other: VMobject) -> Self:
assert(isinstance(other, VMobject))
return self.add(other)

View file

@ -1,7 +1,7 @@
from __future__ import annotations
import numpy as np
from typing import Self
from manimlib.mobject.mobject import Mobject
from manimlib.utils.iterables import listify
@ -36,7 +36,7 @@ class ValueTracker(Mobject):
return result[0]
return result
def set_value(self, value: float | complex | np.ndarray):
def set_value(self, value: float | complex | np.ndarray) -> Self:
self.uniforms["value"][:] = value
return self

View file

@ -22,7 +22,6 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable, Sequence, TypeVar, Tuple
import numpy.typing as npt
from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array
from manimlib.mobject.coordinate_systems import CoordinateSystem

View file

@ -3,13 +3,15 @@ from __future__ import annotations
import itertools as it
import numpy as np
import pyperclip
from IPython.core.getipython import get_ipython
from manimlib.animation.fading import FadeIn
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
from manimlib.constants import PI
from manimlib.constants import DEGREES
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle
@ -25,10 +27,16 @@ from manimlib.mobject.types.vectorized_mobject import VHighlight
from manimlib.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene import Scene
from manimlib.scene.scene import SceneState
from manimlib.scene.scene import PAN_3D_KEY
from manimlib.utils.family_ops import extract_mobject_family_members
from manimlib.utils.space_ops import get_norm
from manimlib.utils.tex_file_writing import LatexError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import Vect3
SELECT_KEY = 's'
UNSELECT_KEY = 'u'
@ -68,7 +76,7 @@ class InteractiveScene(Scene):
"""
corner_dot_config = dict(
color=WHITE,
radius=0.025,
radius=0.05,
glow_factor=2.0,
)
selection_rectangle_stroke_color = WHITE
@ -126,7 +134,7 @@ class InteractiveScene(Scene):
def update_selection_rectangle(self, rect: Rectangle):
p1 = rect.fixed_corner
p2 = self.mouse_point.get_center()
p2 = self.frame.to_fixed_frame_point(self.mouse_point.get_center())
rect.set_points_as_corners([
p1, np.array([p2[0], p1[1], 0]),
p2, np.array([p1[0], p2[1], 0]),
@ -228,9 +236,6 @@ class InteractiveScene(Scene):
super().remove(*mobjects)
self.regenerate_selection_search_set()
# def increment_time(self, dt: float) -> None:
# super().increment_time(dt)
# Related to selection
def toggle_selection_mode(self):
@ -273,7 +278,7 @@ class InteractiveScene(Scene):
def get_corner_dots(self, mobject: Mobject) -> Mobject:
dots = DotCloud(**self.corner_dot_config)
radius = self.corner_dot_config["radius"]
radius = float(self.corner_dot_config["radius"])
if mobject.get_depth() < 1e-2:
vects = [DL, UL, UR, DR]
else:
@ -339,8 +344,17 @@ class InteractiveScene(Scene):
# Functions for keyboard actions
def copy_selection(self):
ids = map(id, self.selection)
pyperclip.copy(",".join(map(str, ids)))
names = []
shell = get_ipython()
for mob in self.selection:
name = str(id(mob))
if shell is None:
continue
for key, value in shell.user_ns.items():
if mob is value:
name = key
names.append(name)
pyperclip.copy(", ".join(names))
def paste_selection(self):
clipboard_str = pyperclip.paste()
@ -377,7 +391,9 @@ class InteractiveScene(Scene):
def enable_selection(self):
self.is_selecting = True
self.add(self.selection_rectangle)
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
self.selection_rectangle.fixed_corner = self.frame.to_fixed_frame_point(
self.mouse_point.get_center()
)
def gather_new_selection(self):
self.is_selecting = False
@ -387,7 +403,9 @@ class InteractiveScene(Scene):
for mob in reversed(self.get_selection_search_set()):
if self.selection_rectangle.is_touching(mob):
additions.append(mob)
self.add_to_selection(*additions)
if self.selection_rectangle.get_arc_length() < 1e-2:
break
self.toggle_from_selection(*additions)
def prepare_grab(self):
mp = self.mouse_point.get_center()
@ -447,6 +465,7 @@ class InteractiveScene(Scene):
else:
self.save_mobject_to_file(self.selection)
# Key actions
def on_key_press(self, symbol: int, modifiers: int) -> None:
super().on_key_press(symbol, modifiers)
char = chr(symbol)
@ -485,6 +504,8 @@ class InteractiveScene(Scene):
self.toggle_selection_mode()
elif char == "s" and modifiers == COMMAND_MODIFIER:
self.save_selection_to_file()
elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER:
self.copy_frame_anim_call()
elif symbol in ARROW_SYMBOLS:
self.nudge_selection(
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
@ -507,7 +528,6 @@ class InteractiveScene(Scene):
super().on_key_release(symbol, modifiers)
if chr(symbol) == SELECT_KEY:
self.gather_new_selection()
# self.remove(self.crosshair)
if chr(symbol) in GRAB_KEYS:
self.is_grabbing = False
elif chr(symbol) == INFORMATION_KEY:
@ -516,7 +536,7 @@ class InteractiveScene(Scene):
self.prepare_resizing(about_corner=False)
# Mouse actions
def handle_grabbing(self, point: np.ndarray):
def handle_grabbing(self, point: Vect3):
diff = point - self.mouse_to_selection
if self.window.is_key_pressed(ord(GRAB_KEY)):
self.selection.move_to(diff)
@ -525,7 +545,7 @@ class InteractiveScene(Scene):
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
self.selection.set_y(diff[1])
def handle_resizing(self, point: np.ndarray):
def handle_resizing(self, point: Vect3):
if not hasattr(self, "scale_about_point"):
return
vect = point - self.scale_about_point
@ -545,15 +565,16 @@ class InteractiveScene(Scene):
about_point=self.scale_about_point
)
def handle_sweeping_selection(self, point: np.ndarray):
def handle_sweeping_selection(self, point: Vect3):
mob = self.point_to_mobject(
point, search_set=self.get_selection_search_set(),
point,
search_set=self.get_selection_search_set(),
buff=SMALL_BUFF
)
if mob is not None:
self.add_to_selection(mob)
def choose_color(self, point: np.ndarray):
def choose_color(self, point: Vect3):
# Search through all mobject on the screen, not just the palette
to_search = [
sm
@ -566,9 +587,9 @@ class InteractiveScene(Scene):
self.selection.set_color(mob.get_color())
self.remove(self.color_palette)
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
def on_mouse_motion(self, point: Vect3, d_point: Vect3) -> None:
super().on_mouse_motion(point, d_point)
self.crosshair.move_to(point)
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
if self.is_grabbing:
self.handle_grabbing(point)
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
@ -576,17 +597,34 @@ class InteractiveScene(Scene):
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
self.handle_sweeping_selection(point)
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
def on_mouse_drag(
self,
point: Vect3,
d_point: Vect3,
buttons: int,
modifiers: int
) -> None:
super().on_mouse_drag(point, d_point, buttons, modifiers)
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
def on_mouse_release(self, point: Vect3, button: int, mods: int) -> None:
super().on_mouse_release(point, button, mods)
if self.color_palette in self.mobjects:
self.choose_color(point)
return
mobject = self.point_to_mobject(
point,
search_set=self.get_selection_search_set(),
buff=1e-4,
)
if mobject is not None:
self.toggle_from_selection(mobject)
else:
self.clear_selection()
# Copying code to recreate state
def copy_frame_anim_call(self):
frame = self.frame
center = frame.get_center()
height = frame.get_height()
angles = frame.get_euler_angles()
call = f"self.frame.animate.reorient"
call += str(tuple((angles / DEGREES).astype(int)))
if any(center != 0):
call += f".move_to({list(np.round(center, 2))})"
if height != FRAME_HEIGHT:
call += ".set_height({:.2f})".format(height)
pyperclip.copy(call)

View file

@ -1,144 +0,0 @@
from manimlib.animation.animation import Animation
from manimlib.animation.transform import MoveToTarget
from manimlib.animation.transform import Transform
from manimlib.animation.update import UpdateFromFunc
from manimlib.constants import DOWN, RIGHT
from manimlib.constants import MED_LARGE_BUFF, SMALL_BUFF
from manimlib.mobject.probability import SampleSpace
from manimlib.mobject.types.vectorized_mobject import VGroup
from manimlib.scene.scene import Scene
class SampleSpaceScene(Scene):
def get_sample_space(self, **config):
self.sample_space = SampleSpace(**config)
return self.sample_space
def add_sample_space(self, **config):
self.add(self.get_sample_space(**config))
def get_division_change_animations(
self, sample_space, parts, p_list,
dimension=1,
new_label_kwargs=None,
**kwargs
):
if new_label_kwargs is None:
new_label_kwargs = {}
anims = []
p_list = sample_space.complete_p_list(p_list)
space_copy = sample_space.copy()
vect = DOWN if dimension == 1 else RIGHT
parts.generate_target()
for part, p in zip(parts.target, p_list):
part.replace(space_copy, stretch=True)
part.stretch(p, dimension)
parts.target.arrange(vect, buff=0)
parts.target.move_to(space_copy)
anims.append(MoveToTarget(parts))
if hasattr(parts, "labels") and parts.labels is not None:
label_kwargs = parts.label_kwargs
label_kwargs.update(new_label_kwargs)
new_braces, new_labels = sample_space.get_subdivision_braces_and_labels(
parts.target, **label_kwargs
)
anims += [
Transform(parts.braces, new_braces),
Transform(parts.labels, new_labels),
]
return anims
def get_horizontal_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "horizontal_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.horizontal_parts, p_list,
dimension=1,
**kwargs
)
def get_vertical_division_change_animations(self, p_list, **kwargs):
assert(hasattr(self.sample_space, "vertical_parts"))
return self.get_division_change_animations(
self.sample_space, self.sample_space.vertical_parts, p_list,
dimension=0,
**kwargs
)
def get_conditional_change_anims(
self, sub_sample_space_index, value, post_rects=None,
**kwargs
):
parts = self.sample_space.horizontal_parts
sub_sample_space = parts[sub_sample_space_index]
anims = self.get_division_change_animations(
sub_sample_space, sub_sample_space.vertical_parts, value,
dimension=0,
**kwargs
)
if post_rects is not None:
anims += self.get_posterior_rectangle_change_anims(post_rects)
return anims
def get_top_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(0, *args, **kwargs)
def get_bottom_conditional_change_anims(self, *args, **kwargs):
return self.get_conditional_change_anims(1, *args, **kwargs)
def get_prior_rectangles(self):
return VGroup(*[
self.sample_space.horizontal_parts[i].vertical_parts[0]
for i in range(2)
])
def get_posterior_rectangles(self, buff=MED_LARGE_BUFF):
prior_rects = self.get_prior_rectangles()
areas = [
rect.get_width() * rect.get_height()
for rect in prior_rects
]
total_area = sum(areas)
total_height = prior_rects.get_height()
post_rects = prior_rects.copy()
for rect, area in zip(post_rects, areas):
rect.stretch_to_fit_height(total_height * area / total_area)
rect.stretch_to_fit_width(
area / rect.get_height()
)
post_rects.arrange(DOWN, buff=0)
post_rects.next_to(
self.sample_space, RIGHT, buff
)
return post_rects
def get_posterior_rectangle_braces_and_labels(
self, post_rects, labels, direction=RIGHT, **kwargs
):
return self.sample_space.get_subdivision_braces_and_labels(
post_rects, labels, direction, **kwargs
)
def update_posterior_braces(self, post_rects):
braces = post_rects.braces
labels = post_rects.labels
for rect, brace, label in zip(post_rects, braces, labels):
brace.stretch_to_fit_height(rect.get_height())
brace.next_to(rect, RIGHT, SMALL_BUFF)
label.next_to(brace, RIGHT, SMALL_BUFF)
def get_posterior_rectangle_change_anims(self, post_rects):
def update_rects(rects):
new_rects = self.get_posterior_rectangles()
Transform(rects, new_rects).update(1)
if hasattr(rects, "braces"):
self.update_posterior_braces(rects)
return rects
anims = [UpdateFromFunc(post_rects, update_rects)]
if hasattr(post_rects, "braces"):
anims += list(map(Animation, [
post_rects.labels, post_rects.braces
]))
return anims

View file

@ -19,6 +19,7 @@ from tqdm.auto import tqdm as ProgressDisplay
from manimlib.animation.animation import prepare_animation
from manimlib.animation.fading import VFadeInThenOut
from manimlib.camera.camera import Camera
from manimlib.camera.camera_frame import CameraFrame
from manimlib.config import get_module
from manimlib.constants import ARROW_SYMBOLS
from manimlib.constants import DEFAULT_WAIT_TIME
@ -44,6 +45,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Callable, Iterable
from manimlib.typing import Vect3
from PIL.Image import Image
@ -52,19 +54,22 @@ if TYPE_CHECKING:
PAN_3D_KEY = 'd'
FRAME_SHIFT_KEY = 'f'
ZOOM_KEY = 'z'
RESET_FRAME_KEY = 'r'
QUIT_KEY = 'q'
class Scene(object):
random_seed: int = 0
pan_sensitivity: float = 3.0
pan_sensitivity: float = 0.5
scroll_sensitivity: float = 20
drag_to_pan: bool = True
max_num_saved_states: int = 50
default_camera_config: dict = dict()
default_window_config: dict = dict()
default_file_writer_config: dict = dict()
samples = 0
# Euler angles, in degrees
default_frame_orientation = (0, 0)
def __init__(
self,
@ -110,6 +115,10 @@ class Scene(object):
# Core state of the scene
self.camera: Camera = Camera(**self.camera_config)
self.frame: CameraFrame = self.camera.frame
self.frame.reorient(*self.default_frame_orientation)
self.frame.make_orientation_default()
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
self.mobjects: list[Mobject] = [self.camera.frame]
self.render_groups: list[Mobject] = []
@ -828,9 +837,10 @@ class Scene(object):
def on_mouse_motion(
self,
point: np.ndarray,
d_point: np.ndarray
point: Vect3,
d_point: Vect3
) -> None:
assert(self.window is not None)
self.mouse_point.move_to(point)
event_data = {"point": point, "d_point": d_point}
@ -841,25 +851,24 @@ class Scene(object):
frame = self.camera.frame
# Handle perspective changes
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
frame.increment_theta(-self.pan_sensitivity * d_point[0])
frame.increment_phi(self.pan_sensitivity * d_point[1])
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
ff_d_point *= self.pan_sensitivity
frame.increment_theta(-ff_d_point[0])
frame.increment_phi(ff_d_point[1])
# Handle frame movements
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
shift = -d_point
shift[0] *= frame.get_width() / 2
shift[1] *= frame.get_height() / 2
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), shift)
frame.shift(shift)
frame.shift(-d_point)
def on_mouse_drag(
self,
point: np.ndarray,
d_point: np.ndarray,
point: Vect3,
d_point: Vect3,
buttons: int,
modifiers: int
) -> None:
self.mouse_drag_point.move_to(point)
if self.drag_to_pan:
self.frame.shift(-d_point)
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
@ -868,7 +877,7 @@ class Scene(object):
def on_mouse_press(
self,
point: np.ndarray,
point: Vect3,
button: int,
mods: int
) -> None:
@ -880,7 +889,7 @@ class Scene(object):
def on_mouse_release(
self,
point: np.ndarray,
point: Vect3,
button: int,
mods: int
) -> None:
@ -891,22 +900,21 @@ class Scene(object):
def on_mouse_scroll(
self,
point: np.ndarray,
offset: np.ndarray
point: Vect3,
offset: Vect3,
x_pixel_offset: float,
y_pixel_offset: float
) -> None:
event_data = {"point": point, "offset": offset}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
if propagate_event is not None and propagate_event is False:
return
frame = self.camera.frame
if self.window.is_key_pressed(ord(ZOOM_KEY)):
factor = 1 + np.arctan(10 * offset[1])
frame.scale(1 / factor, about_point=point)
else:
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), offset)
frame.shift(-20.0 * shift)
rel_offset = y_pixel_offset / self.camera.get_pixel_height()
self.frame.scale(
1 - self.scroll_sensitivity * rel_offset,
about_point=point
)
def on_key_release(
self,
@ -1006,3 +1014,14 @@ class SceneState():
class EndScene(Exception):
pass
class ThreeDScene(Scene):
samples = 4
default_frame_orientation = (-30, 70)
def add(self, *mobjects, set_depth_test: bool = True):
for mob in mobjects:
if set_depth_test and not mob.is_fixed_in_frame():
mob.apply_depth_test()
super().add(*mobjects)

View file

@ -248,8 +248,8 @@ class ShaderWrapper(object):
def generate_vao(self, refresh: bool = True):
self.release()
# Data buffer
vbo = self.vbo = self.get_vertex_buffer_object(refresh)
ibo = self.ibo = self.get_index_buffer_object(refresh)
vbo = self.get_vertex_buffer_object(refresh)
ibo = self.get_index_buffer_object(refresh)
# Vertex array object
self.vao = self.ctx.vertex_array(
@ -280,12 +280,10 @@ class FillShaderWrapper(ShaderWrapper):
self.fill_canvas = get_fill_canvas(self.ctx)
def render(self):
vao = self.vao
assert(vao is not None)
winding = (len(self.vert_indices) == 0)
vao.program['winding'].value = winding
self.program['winding'].value = winding
if not winding:
vao.render(moderngl.TRIANGLES)
super().render()
return
original_fbo = self.ctx.fbo
@ -301,14 +299,13 @@ class FillShaderWrapper(ShaderWrapper):
gl.GL_ONE, gl.GL_ONE,
)
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX)
self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX
vao.render(moderngl.TRIANGLE_STRIP)
super().render()
original_fbo.use()
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glBlendEquation(gl.GL_FUNC_ADD)
texture_vao.render(moderngl.TRIANGLE_STRIP)
texture_vao.render()
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)

View file

@ -10,10 +10,10 @@ out vec2 v_im_coords;
out float v_opacity;
// Analog of import for manim only
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void main(){
v_im_coords = im_coords;
v_opacity = opacity;
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
}

View file

@ -0,0 +1,23 @@
uniform float is_fixed_in_frame;
uniform mat4 view;
uniform float focal_distance;
const float DEFAULT_FRAME_HEIGHT = 8.0;
const float ASPECT_RATIO = 16.0 / 9.0;
const float X_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT / ASPECT_RATIO;
const float Y_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT;
void emit_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
if(!bool(is_fixed_in_frame)){
result = view * result;
}
// Essentially a projection matrix
result.x *= X_SCALE;
result.y *= Y_SCALE;
result.z /= focal_distance;
result.w = 1.0 - result.z;
// Flip and scale to prevent premature clipping
result.z *= -0.1;
gl_Position = result;
}

View file

@ -24,14 +24,11 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
vec3 to_camera = normalize(camera_position - point);
vec3 to_light = normalize(light_position - point);
// Note, this effectively treats surfaces as two-sided
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
vec3 light_reflection = reflect(-to_light, unit_normal);
float light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
bright_factor += shine;

View file

@ -1,23 +1,23 @@
uniform float is_fixed_in_frame;
uniform mat4 view;
uniform vec2 frame_shape;
uniform float focal_distance;
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
const float DEFAULT_FRAME_HEIGHT = 8.0;
const float ASPECT_RATIO = 16.0 / 9.0;
const float X_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT / ASPECT_RATIO;
const float Y_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT;
vec4 get_gl_Position(vec3 point){
bool is_fixed = bool(is_fixed_in_frame);
void emit_gl_Position(vec3 point){
vec4 result = vec4(point, 1.0);
if(!is_fixed){
if(!bool(is_fixed_in_frame)){
result = view * result;
}
vec2 shape = is_fixed ? DEFAULT_FRAME_SHAPE : frame_shape;
result.x *= 2.0 / shape.x;
result.y *= 2.0 / shape.y;
// Essentially a projection matrix
result.x *= X_SCALE;
result.y *= Y_SCALE;
result.z /= focal_distance;
result.w = 1.0 - result.z;
// Flip and scale to prevent premature clipping
result.z *= -0.1;
return result;
gl_Position = result;
}

View file

@ -15,8 +15,6 @@ uniform vec3 color6;
uniform vec3 color7;
uniform vec3 color8;
uniform vec2 frame_shape;
in vec3 xyz_coords;
out vec4 frag_color;

View file

@ -6,9 +6,9 @@ out vec3 xyz_coords;
uniform float scale_factor;
uniform vec3 offset;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void main(){
xyz_coords = (point - offset) / scale_factor;
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
}

View file

@ -26,8 +26,6 @@ uniform float saturation_factor;
uniform float black_for_cycles;
uniform float is_parameter_space;
uniform vec2 frame_shape;
in vec3 xyz_coords;
out vec4 frag_color;

View file

@ -6,9 +6,9 @@ out vec3 xyz_coords;
uniform float scale_factor;
uniform vec3 offset;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void main(){
xyz_coords = (point - offset) / scale_factor;
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
}

View file

@ -27,7 +27,7 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3](
);
// Analog of import for manim only
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void emit_triangle(vec3 points[3], vec4 v_color[3]){
@ -41,7 +41,7 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){
uv_coords = SIMPLE_QUADRATIC[i];
color = v_color[i];
point = points[i];
gl_Position = get_gl_Position(points[i]);
emit_gl_Position(points[i]);
EmitVertex();
}
EndPrimitive();
@ -57,10 +57,6 @@ void emit_simple_triangle(){
void main(){
// We use the triangle strip primative, but
// actually only need every other strip element
if (winding && int(v_vert_index[0]) % 2 == 1) return;
// Curves are marked as ended when the handle after
// the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return;

View file

@ -26,7 +26,7 @@ float dist_to_curve(){
// Evaluate F(x, y) = y - x^2
// divide by its gradient's magnitude
float Fxy = y0 - x0 * x0;
float approx_dist = abs(Fxy) / sqrt(1.0 + 4 * x0 * x0);
float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0);
if(approx_dist < QUICK_DIST_WIDTH) return approx_dist;
// Otherwise, solve for the minimal distance.

View file

@ -13,7 +13,6 @@ in vec3 verts[3];
in vec4 v_joint_product[3];
in float v_stroke_width[3];
in vec4 v_color[3];
in float v_vert_index[3];
out vec4 color;
out float uv_stroke_width;
@ -36,7 +35,7 @@ const float COS_THRESHOLD = 0.99;
vec3 unit_normal = vec3(0.0, 0.0, 1.0);
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
#INSERT get_xyz_to_uv.glsl
#INSERT finalize_color.glsl
@ -90,7 +89,7 @@ vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw
*/
float buff = 0.5 * v_stroke_width[index] + aaw;
// Add correction for sharp angles to prevent weird bevel effects
if(joint_product.w < -0.9) buff *= 10 * (joint_product.w + 1.0);
if(joint_product.w < -0.75) buff *= 4 * (joint_product.w + 1.0);
vec3 normal = get_joint_unit_normal(joint_product);
// Set global unit normal
unit_normal = normal;
@ -154,10 +153,6 @@ void get_corners(
}
void main() {
// We use the triangle strip primative, but
// actually only need every other strip element
if (int(v_vert_index[0]) % 2 == 1) return;
// Curves are marked as ended when the handle after
// the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return;
@ -207,7 +202,7 @@ void main() {
}
color = finalize_color(v_color[i / 2], corners[i], unit_normal);
gl_Position = get_gl_Position(corners[i]);
emit_gl_Position(corners[i]);
EmitVertex();
}
EndPrimitive();

View file

@ -1,6 +1,7 @@
#version 330
uniform vec2 frame_shape;
uniform float frame_scale;
uniform float is_fixed_in_frame;
in vec3 point;
in vec4 stroke_rgba;
@ -14,14 +15,15 @@ out vec3 verts;
out vec4 v_joint_product;
out float v_stroke_width;
out vec4 v_color;
out float v_vert_index;
const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){
verts = point;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0;
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width;
if(!bool(is_fixed_in_frame)){
v_stroke_width *= frame_scale;
}
v_joint_product = joint_product;
v_color = stroke_rgba;
v_vert_index = gl_VertexID;
}

View file

@ -2,8 +2,8 @@
in vec3 point;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void main(){
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
}

View file

@ -3,20 +3,18 @@
uniform vec4 clip_plane;
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
in vec3 normal;
in vec4 rgba;
out vec4 v_color;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
#INSERT get_unit_normal.glsl
#INSERT finalize_color.glsl
void main(){
gl_Position = get_gl_Position(point);
vec3 normal = get_unit_normal(point, du_point, dv_point);
v_color = finalize_color(rgba, point, normal);
emit_gl_Position(point);
v_color = finalize_color(rgba, point, normalize(normal));
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);

View file

@ -1,8 +1,7 @@
#version 330
in vec3 point;
in vec3 du_point;
in vec3 dv_point;
in vec3 normal;
in vec2 im_coords;
in float opacity;
@ -11,13 +10,13 @@ out vec3 v_normal;
out vec2 v_im_coords;
out float v_opacity;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
#INSERT get_unit_normal.glsl
void main(){
v_point = point;
v_normal = get_unit_normal(point, du_point, dv_point);
v_normal = normal;
v_im_coords = im_coords;
v_opacity = opacity;
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
}

View file

@ -12,14 +12,14 @@ out float scaled_aaw;
out vec3 v_point;
out vec3 light_pos;
#INSERT get_gl_Position.glsl
#INSERT emit_gl_Position.glsl
void main(){
v_point = point;
color = rgba;
scaled_aaw = (anti_alias_width * pixel_size) / radius;
gl_Position = get_gl_Position(point);
emit_gl_Position(point);
float z = -10 * gl_Position.z;
float scaled_radius = radius * 1.0 / (1.0 - z);
gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width);

View file

@ -1,4 +1,5 @@
import itertools as it
import numpy as np
def merge_dicts_recursively(*dicts):
@ -29,3 +30,19 @@ def soft_dict_update(d1, d2):
for key, value in list(d2.items()):
if key not in d1:
d1[key] = value
def dict_eq(d1, d2):
if len(d1) != len(d2):
return False
for key in d1:
value1 = d1[key]
value2 = d2[key]
if type(value1) != type(value2):
return False
if type(d1[key]) == np.ndarray:
if any(d1[key] != d2[key]):
return False
elif d1[key] != d2[key]:
return False
return True

View file

@ -7,8 +7,8 @@ import moderngl
from PIL import Image
import numpy as np
from manimlib.constants import DEFAULT_PIXEL_HEIGHT
from manimlib.constants import DEFAULT_PIXEL_WIDTH
from manimlib.config import parse_cli
from manimlib.config import get_configuration
from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_shader_dir
from manimlib.utils.file_ops import find_file
@ -103,7 +103,7 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
@lru_cache()
def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
"""
Because VMobjects with fill are rendered in a funny way, using
alpha blending to effectively compute the winding number around
@ -114,15 +114,14 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded.
"""
cam_config = get_customization()['camera_resolutions']
res_name = cam_config['default_resolution']
size = tuple(map(int, cam_config[res_name].split("x")))
cam_config = get_configuration(parse_cli())['camera_config']
size = (cam_config['pixel_width'], cam_config['pixel_height'])
# Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped
texture = ctx.texture(size=size, components=4, dtype='f2')
depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used
texture_fbo = ctx.framebuffer(texture, depth_buffer)
depth_texture = ctx.depth_texture(size=size)
texture_fbo = ctx.framebuffer(texture, depth_texture)
# We'll paint onto a canvas with initially negative rgbs, and
# discard any pixels remaining close to this value. This is
@ -150,6 +149,7 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
#version 330
uniform sampler2D Texture;
uniform sampler2D DepthTexture;
uniform vec3 null_rgb;
in vec2 v_textcoord;
@ -159,17 +159,21 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
void main() {
color = texture(Texture, v_textcoord);
if(color.a == 0) discard;
if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
// Un-blend from the null value
color.rgb -= (1 - color.a) * null_rgb;
// Counteract scaling in fill frag
color.a *= 1.01;
//TODO, set gl_FragDepth;
gl_FragDepth = texture(DepthTexture, v_textcoord)[0];
}
''',
)
simple_program['Texture'].value = get_texture_id(texture)
simple_program['DepthTexture'].value = get_texture_id(depth_texture)
simple_program['null_rgb'].value = null_rgb
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
@ -177,5 +181,6 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
simple_program,
ctx.buffer(verts.astype('f4').tobytes()),
'texcoord',
mode=moderngl.TRIANGLE_STRIP
)
return (texture_fbo, fill_texture_vao, null_rgb)

View file

@ -16,7 +16,7 @@ def num_tex_symbols(tex: str) -> int:
# \begin{array}{cc}, etc.
pattern = "|".join(
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
for s in ["begin", "end", "phantom"]
for s in ["begin", "end", "phantom", "text"]
)
tex = re.sub(pattern, "", tex)

View file

@ -7,6 +7,7 @@ from moderngl_window.context.pyglet.window import Window as PygletWindow
from moderngl_window.timers.clock import Timer
from screeninfo import get_monitors
from manimlib.constants import FRAME_SHAPE
from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING
@ -77,17 +78,18 @@ class Window(PygletWindow):
py: int,
relative: bool = False
) -> np.ndarray:
pw, ph = self.size
fw, fh = self.scene.camera.get_frame_shape()
fc = self.scene.camera.get_frame_center()
if relative:
return np.array([px / pw, py / ph, 0])
else:
return np.array([
fc[0] + px * fw / pw - fw / 2,
fc[1] + py * fh / ph - fh / 2,
0
])
if not hasattr(self.scene, "frame"):
return np.zeros(3)
pixel_shape = np.array(self.size)
fixed_frame_shape = np.array(FRAME_SHAPE)
frame = self.scene.frame
coords = np.zeros(3)
coords[:2] = (fixed_frame_shape / pixel_shape) * np.array([px, py])
if not relative:
coords[:2] -= 0.5 * fixed_frame_shape
return frame.from_fixed_frame_point(coords, relative)
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
super().on_mouse_motion(x, y, dx, dy)
@ -115,7 +117,7 @@ class Window(PygletWindow):
super().on_mouse_scroll(x, y, x_offset, y_offset)
point = self.pixel_coords_to_space_coords(x, y)
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
self.scene.on_mouse_scroll(point, offset)
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
def on_key_press(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.add(symbol) # Modifiers?

View file

@ -13,6 +13,7 @@ pydub
pygments
PyOpenGL
pyperclip
pyrr
pyyaml
rich
scipy