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

View file

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

View file

@ -70,7 +70,7 @@ class Transform(Animation):
def finish(self) -> None: def finish(self) -> None:
super().finish() super().finish()
self.mobject.unlock_data() 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) self.mobject.become(self.target_mobject)
def create_target(self) -> Mobject: def create_target(self) -> Mobject:

View file

@ -131,9 +131,10 @@ class TransformMatchingStrings(TransformMatchingParts):
target: StringMobject, target: StringMobject,
matched_keys: Iterable[str] = [], matched_keys: Iterable[str] = [],
key_map: dict[str, str] = dict(), key_map: dict[str, str] = dict(),
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
**kwargs, **kwargs,
): ):
matched_pairs = [ matched_pairs = list(matched_pairs) + [
*[(source[key], target[key]) for key in matched_keys], *[(source[key], target[key]) for key in matched_keys],
*[(source[key1], target[key2]) for key1, key2 in key_map.items()], *[(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: def init_context(self) -> None:
if self.window is None: if self.window is None:
self.ctx = moderngl.create_standalone_context() self.ctx: moderngl.Context = moderngl.create_standalone_context()
else: else:
self.ctx = self.window.ctx self.ctx: moderngl.Context = self.window.ctx
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
self.ctx.enable(moderngl.BLEND) self.ctx.enable(moderngl.BLEND)
@ -125,16 +125,20 @@ class Camera(object):
def clear(self) -> None: def clear(self) -> None:
self.fbo.clear(*self.background_rgba) self.fbo.clear(*self.background_rgba)
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes: def blit(self, src_fbo, dst_fbo):
# Copy blocks from fbo into draw_fbo using Blit """
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo) Copy blocks between fbo's using Blit
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo) """
src_viewport = self.fbo.viewport gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo)
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
gl.glBlitFramebuffer( gl.glBlitFramebuffer(
*src_viewport, *src_fbo.viewport,
*self.draw_fbo.viewport, *dst_fbo.viewport,
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR 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( return self.draw_fbo.read(
viewport=self.draw_fbo.viewport, viewport=self.draw_fbo.viewport,
components=self.n_channels, components=self.n_channels,
@ -169,10 +173,10 @@ class Camera(object):
# Getting camera attributes # Getting camera attributes
def get_pixel_size(self) -> float: 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]: def get_pixel_shape(self) -> tuple[int, int]:
return self.draw_fbo.size return self.fbo.size
def get_pixel_width(self) -> int: def get_pixel_width(self) -> int:
return self.get_pixel_shape()[0] return self.get_pixel_shape()[0]
@ -223,6 +227,8 @@ class Camera(object):
self.fbo.use() self.fbo.use()
for mobject in mobjects: for mobject in mobjects:
mobject.render(self.ctx, self.uniforms) 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: def refresh_uniforms(self) -> None:
frame = self.frame frame = self.frame
@ -231,12 +237,12 @@ class Camera(object):
cam_pos = self.frame.get_implied_camera_location() cam_pos = self.frame.get_implied_camera_location()
self.uniforms.update( self.uniforms.update(
frame_shape=frame.get_shape(),
pixel_size=self.get_pixel_size(),
view=tuple(view_matrix.T.flatten()), 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), camera_position=tuple(cam_pos),
light_position=tuple(light_pos), light_position=tuple(light_pos),
focal_distance=frame.get_focal_distance(),
) )

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ from manimlib.utils.rate_functions import smooth
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, List, Iterable from typing import Callable, List, Iterable, Self
from manimlib.typing import ManimColor, Vect3 from manimlib.typing import ManimColor, Vect3
@ -49,7 +49,7 @@ class AnimatedBoundary(VGroup):
lambda m, dt: self.update_boundary_copies(dt) 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 # Not actual time, but something which passes at
# an altered rate to make the implementation below # an altered rate to make the implementation below
# cleaner # cleaner
@ -79,6 +79,7 @@ class AnimatedBoundary(VGroup):
) )
self.total_time += dt self.total_time += dt
return self
def full_family_become_partial( def full_family_become_partial(
self, self,
@ -86,7 +87,7 @@ class AnimatedBoundary(VGroup):
mob2: VMobject, mob2: VMobject,
a: float, a: float,
b: float b: float
): ) -> Self:
family1 = mob1.family_members_with_points() family1 = mob1.family_members_with_points()
family2 = mob2.family_members_with_points() family2 = mob2.family_members_with_points()
for sm1, sm2 in zip(family1, family2): for sm1, sm2 in zip(family1, family2):
@ -118,7 +119,7 @@ class TracedPath(VMobject):
self.traced_points: list[np.ndarray] = [] self.traced_points: list[np.ndarray] = []
self.add_updater(lambda m, dt: m.update_path(dt)) 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: if dt == 0:
return self return self
point = self.traced_point_func().copy() 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 BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
from manimlib.constants import DEGREES, PI from manimlib.constants import DEGREES, PI
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP 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 FRAME_X_RADIUS, FRAME_Y_RADIUS
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
from manimlib.mobject.functions import ParametricCurve 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.dot_cloud import DotCloud
from manimlib.mobject.types.surface import ParametricSurface from manimlib.mobject.types.surface import ParametricSurface
from manimlib.mobject.types.vectorized_mobject import VGroup 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.dict_ops import merge_dicts_recursively
from manimlib.utils.simple_functions import binary_search from manimlib.utils.simple_functions import binary_search
from manimlib.utils.space_ops import angle_of_vector 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 from typing import TYPE_CHECKING
if 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.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
@ -236,7 +236,13 @@ class CoordinateSystem(ABC):
""" """
return self.input_to_graph_point(x, graph) 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 Use for graphing functions which might change over time, or change with
conditions conditions
@ -637,6 +643,8 @@ class NumberPlane(Axes):
lines2 = VGroup() lines2 = VGroup()
inputs = np.arange(axis2.x_min, axis2.x_max + step, step) inputs = np.arange(axis2.x_min, axis2.x_max + step, step)
for i, x in enumerate(inputs): for i, x in enumerate(inputs):
if abs(x) < 1e-8:
continue
new_line = line.copy() new_line = line.copy()
new_line.shift(axis2.n2p(x) - axis2.n2p(0)) new_line.shift(axis2.n2p(x) - axis2.n2p(0))
if i % (1 + ratio) == 0: if i % (1 + ratio) == 0:
@ -658,7 +666,7 @@ class NumberPlane(Axes):
kwargs["buff"] = 0 kwargs["buff"] = 0
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs) 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(): for mob in self.family_members_with_points():
num_curves = mob.get_num_curves() num_curves = mob.get_num_curves()
if num_inserted_curves > num_curves: if num_inserted_curves > num_curves:
@ -697,7 +705,7 @@ class ComplexPlane(NumberPlane):
skip_first: bool = True, skip_first: bool = True,
font_size: int = 36, font_size: int = 36,
**kwargs **kwargs
): ) -> Self:
if numbers is None: if numbers is None:
numbers = self.get_default_coordinate_values(skip_first) 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 from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Iterable from typing import Iterable, Self, Optional
from manimlib.typing import ManimColor, Vect3, Vect3Array from manimlib.typing import ManimColor, Vect3, Vect3Array
@ -67,7 +67,7 @@ class TipableVMobject(VMobject):
) )
# Adding, Creating, Modifying tips # 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 Adds a tip to the TipableVMobject instance, recognising
that the endpoints might need to be switched if it's 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()) tip.shift(anchor - tip.get_tip_point())
return tip 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: if self.get_length() == 0:
# Zero length, put_start_and_end_on wouldn't # Zero length, put_start_and_end_on wouldn't
# work # work
@ -127,7 +127,7 @@ class TipableVMobject(VMobject):
self.put_start_and_end_on(start, end) self.put_start_and_end_on(start, end)
return self 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: if at_start:
self.start_tip = tip self.start_tip = tip
else: else:
@ -258,7 +258,7 @@ class Arc(TipableVMobject):
angle = angle_of_vector(self.get_end() - self.get_arc_center()) angle = angle_of_vector(self.get_end() - self.get_arc_center())
return angle % TAU 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()) self.shift(point - self.get_arc_center())
return self return self
@ -318,7 +318,7 @@ class Circle(Arc):
dim_to_match: int = 0, dim_to_match: int = 0,
stretch: bool = False, stretch: bool = False,
buff: float = MED_SMALL_BUFF buff: float = MED_SMALL_BUFF
): ) -> Self:
self.replace(mobject, dim_to_match, stretch) self.replace(mobject, dim_to_match, stretch)
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0) self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1) self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
@ -448,12 +448,8 @@ class Annulus(VMobject):
) )
self.radius = outer_radius self.radius = outer_radius
# Make sure to add enough components that triangulation doesn't fail outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0)
kw = dict( inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0)
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)
self.add_subpath(outer_path) self.add_subpath(outer_path)
self.add_subpath(inner_path) self.add_subpath(inner_path)
self.shift(center) self.shift(center)
@ -479,7 +475,7 @@ class Line(TipableVMobject):
end: Vect3, end: Vect3,
buff: float = 0, buff: float = 0,
path_arc: float = 0 path_arc: float = 0
): ) -> Self:
vect = end - start vect = end - start
dist = get_norm(vect) dist = get_norm(vect)
if np.isclose(dist, 0): if np.isclose(dist, 0):
@ -508,9 +504,10 @@ class Line(TipableVMobject):
self.set_points_as_corners([start, end]) self.set_points_as_corners([start, end])
return self 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.path_arc = new_value
self.init_points() self.init_points()
return self
def set_start_and_end_attrs(self, start: Vect3 | Mobject, end: Vect3 | Mobject): def set_start_and_end_attrs(self, start: Vect3 | Mobject, end: Vect3 | Mobject):
# If either start or end are Mobjects, this # If either start or end are Mobjects, this
@ -545,7 +542,7 @@ class Line(TipableVMobject):
result[:len(point)] = point result[:len(point)] = point
return result 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() curr_start, curr_end = self.get_start_and_end()
if np.isclose(curr_start, curr_end).all(): if np.isclose(curr_start, curr_end).all():
# Handle null lines more gracefully # Handle null lines more gracefully
@ -573,7 +570,7 @@ class Line(TipableVMobject):
def get_slope(self) -> float: def get_slope(self) -> float:
return np.tan(self.get_angle()) 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: if about_point is None:
about_point = self.get_start() about_point = self.get_start()
self.rotate( self.rotate(
@ -699,13 +696,13 @@ class Arrow(Line):
end: Vect3, end: Vect3,
buff: float = 0, buff: float = 0,
path_arc: float = 0 path_arc: float = 0
): ) -> Self:
super().set_points_by_ends(start, end, buff, path_arc) super().set_points_by_ends(start, end, buff, path_arc)
self.insert_tip_anchor() self.insert_tip_anchor()
self.create_tip_with_stroke_width() self.create_tip_with_stroke_width()
return self return self
def insert_tip_anchor(self): def insert_tip_anchor(self) -> Self:
prev_end = self.get_end() prev_end = self.get_end()
arc_len = self.get_arc_length() arc_len = self.get_arc_length()
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
@ -720,7 +717,7 @@ class Arrow(Line):
return self return self
@Mobject.affects_data @Mobject.affects_data
def create_tip_with_stroke_width(self): def create_tip_with_stroke_width(self) -> Self:
if self.get_num_points() < 3: if self.get_num_points() < 3:
return self return self
tip_width = self.tip_width_ratio * min( 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) self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3)
return self return self
def reset_tip(self): def reset_tip(self) -> Self:
self.set_points_by_ends( self.set_points_by_ends(
self.get_start(), self.get_end(), self.get_start(), self.get_end(),
path_arc=self.path_arc path_arc=self.path_arc
@ -743,13 +740,13 @@ class Arrow(Line):
color: ManimColor | Iterable[ManimColor] | None = None, color: ManimColor | Iterable[ManimColor] | None = None,
width: float | Iterable[float] | None = None, width: float | Iterable[float] | None = None,
*args, **kwargs *args, **kwargs
): ) -> Self:
super().set_stroke(color=color, width=width, *args, **kwargs) super().set_stroke(color=color, width=width, *args, **kwargs)
if self.has_points(): if self.has_points():
self.reset_tip() self.reset_tip()
return self 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: if scale_factor != 1.0:
self.reset_tip() self.reset_tip()
return self return self
@ -791,7 +788,7 @@ class FillArrow(Line):
end: Vect3, end: Vect3,
buff: float = 0, buff: float = 0,
path_arc: float = 0 path_arc: float = 0
) -> None: ) -> Self:
# Find the right tip length and thickness # Find the right tip length and thickness
vect = end - start vect = end - start
length = max(get_norm(vect), 1e-8) length = max(get_norm(vect), 1e-8)
@ -852,8 +849,9 @@ class FillArrow(Line):
axis=rotate_vector(self.get_unit_vector(), -PI / 2), axis=rotate_vector(self.get_unit_vector(), -PI / 2),
) )
self.shift(start - self.get_start()) 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.set_points_by_ends(
self.get_start().copy(), self.get_start().copy(),
self.get_end().copy(), self.get_end().copy(),
@ -868,21 +866,21 @@ class FillArrow(Line):
def get_end(self) -> Vect3: def get_end(self) -> Vect3:
return self.get_points()[self.tip_index] 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) self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
return self return self
def scale(self, *args, **kwargs): def scale(self, *args, **kwargs) -> Self:
super().scale(*args, **kwargs) super().scale(*args, **kwargs)
self.reset_points_around_ends() self.reset_points_around_ends()
return self return self
def set_thickness(self, thickness: float): def set_thickness(self, thickness: float) -> Self:
self.thickness = thickness self.thickness = thickness
self.reset_points_around_ends() self.reset_points_around_ends()
return self 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.path_arc = path_arc
self.reset_points_around_ends() self.reset_points_around_ends()
return self return self
@ -925,7 +923,7 @@ class Polygon(VMobject):
def get_vertices(self) -> Vect3Array: def get_vertices(self) -> Vect3Array:
return self.get_start_anchors() 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: if radius is None:
verts = self.get_vertices() verts = self.get_vertices()
min_edge_length = min( min_edge_length = min(

View file

@ -18,7 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Sequence from typing import Sequence, Self
import numpy.typing as npt import numpy.typing as npt
from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Mobject
from manimlib.typing import ManimColor, Vect3 from manimlib.typing import ManimColor, Vect3
@ -76,7 +76,7 @@ class Matrix(VMobject):
self, self,
matrix: Sequence[Sequence[str | float | VMobject]], matrix: Sequence[Sequence[str | float | VMobject]],
v_buff: float = 0.8, v_buff: float = 0.8,
h_buff: float = 1.3, h_buff: float = 1.0,
bracket_h_buff: float = 0.2, bracket_h_buff: float = 0.2,
bracket_v_buff: float = 0.25, bracket_v_buff: float = 0.25,
add_background_rectangles_to_entries: bool = False, add_background_rectangles_to_entries: bool = False,
@ -129,7 +129,7 @@ class Matrix(VMobject):
v_buff: float, v_buff: float,
h_buff: float, h_buff: float,
aligned_corner: Vect3, aligned_corner: Vect3,
): ) -> Self:
for i, row in enumerate(matrix): for i, row in enumerate(matrix):
for j, elem in enumerate(row): for j, elem in enumerate(row):
mob = matrix[i][j] mob = matrix[i][j]
@ -139,7 +139,7 @@ class Matrix(VMobject):
) )
return self 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) height = len(self.mob_matrix)
brackets = Tex("".join(( brackets = Tex("".join((
R"\left[\begin{array}{c}", R"\left[\begin{array}{c}",
@ -168,13 +168,13 @@ class Matrix(VMobject):
for row in self.mob_matrix 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() columns = self.get_columns()
for color, column in zip(colors, columns): for color, column in zip(colors, columns):
column.set_color(color) column.set_color(color)
return self return self
def add_background_to_entries(self): def add_background_to_entries(self) -> Self:
for mob in self.get_entries(): for mob in self.get_entries():
mob.add_background_rectangle() mob.add_background_rectangle()
return self 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 from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import TypeVar from typing import TypeVar, Self
from manimlib.typing import ManimColor, Vect3 from manimlib.typing import ManimColor, Vect3
T = TypeVar("T", bound=VMobject) T = TypeVar("T", bound=VMobject)
@ -163,7 +163,7 @@ class DecimalNumber(VMobject):
def get_tex(self): def get_tex(self):
return self.num_string 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) move_to_point = self.get_edge_center(self.edge_to_fix)
style = self.family_members_with_points()[0].get_style() style = self.family_members_with_points()[0].get_style()
self.set_submobjects_from_number(number) self.set_submobjects_from_number(number)
@ -171,14 +171,16 @@ class DecimalNumber(VMobject):
self.set_style(**style) self.set_style(**style)
return self 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"] self.uniforms["font_size"] = scale_factor * self.uniforms["font_size"]
return self
def get_value(self) -> float | complex: def get_value(self) -> float | complex:
return self.number 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) self.set_value(self.get_value() + delta_t)
return self
class Integer(DecimalNumber): class Integer(DecimalNumber):

View file

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

View file

@ -251,8 +251,6 @@ class Laptop(VGroup):
self.axis = axis self.axis = axis
self.add(body, screen_plate, 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): class VideoIcon(SVGMobject):
@ -383,7 +381,6 @@ class Bubble(SVGMobject):
self.flip() self.flip()
self.content = Mobject() self.content = Mobject()
self.refresh_triangulation()
def get_tip(self): def get_tip(self):
# TODO, find a better way # TODO, find a better way

View file

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

View file

@ -295,15 +295,11 @@ class VMobjectFromSVGPath(VMobject):
def __init__( def __init__(
self, self,
path_obj: se.Path, path_obj: se.Path,
should_subdivide_sharp_curves: bool = False,
should_remove_null_curves: bool = True,
**kwargs **kwargs
): ):
# Get rid of arcs # Get rid of arcs
path_obj.approximate_arcs_with_quads() path_obj.approximate_arcs_with_quads()
self.path_obj = path_obj 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) super().__init__(**kwargs)
def init_points(self) -> None: def init_points(self) -> None:
@ -313,14 +309,6 @@ class VMobjectFromSVGPath(VMobject):
path_string = self.path_obj.d() path_string = self.path_obj.d()
if path_string not in PATH_TO_POINTS: if path_string not in PATH_TO_POINTS:
self.handle_commands() 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 # Save for future use
PATH_TO_POINTS[path_string] = self.get_points().copy() PATH_TO_POINTS[path_string] = self.get_points().copy()
else: else:

View file

@ -65,7 +65,7 @@ class SurfaceMesh(VGroup):
u_indices = np.linspace(0, full_nu - 1, part_nu) u_indices = np.linspace(0, full_nu - 1, part_nu)
v_indices = np.linspace(0, full_nv - 1, part_nv) 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() normals = uv_surface.get_unit_normals()
nudge = self.normal_nudge nudge = self.normal_nudge
nudged_points = points + nudge * normals nudged_points = points + nudge * normals
@ -96,7 +96,7 @@ class Sphere(Surface):
def __init__( def __init__(
self, self,
u_range: Tuple[float, float] = (0, TAU), 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), resolution: Tuple[int, int] = (101, 51),
radius: float = 1.0, radius: float = 1.0,
**kwargs, **kwargs,
@ -166,7 +166,6 @@ class Cylinder(Surface):
self.scale(self.radius) self.scale(self.radius)
self.set_depth(self.height, stretch=True) self.set_depth(self.height, stretch=True)
self.apply_matrix(z_to_vector(self.axis)) self.apply_matrix(z_to_vector(self.axis))
return self
def uv_func(self, u: float, v: float) -> np.ndarray: def uv_func(self, u: float, v: float) -> np.ndarray:
return np.array([np.cos(u), np.sin(u), v]) return np.array([np.cos(u), np.sin(u), v])
@ -186,6 +185,7 @@ class Line3D(Cylinder):
height=get_norm(axis), height=get_norm(axis),
radius=width / 2, radius=width / 2,
axis=axis, axis=axis,
resolution=resolution,
**kwargs **kwargs
) )
self.shift((start + end) / 2) self.shift((start + end) / 2)
@ -376,16 +376,6 @@ class Dodecahedron(VGroup3D):
super().__init__(*pentagons, **style) 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): class Prismify(VGroup3D):
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs): def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):

View file

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

View file

@ -10,7 +10,7 @@ from manimlib.utils.iterables import resize_with_interpolation
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable from typing import Callable, Self
from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array
@ -28,7 +28,7 @@ class PMobject(Mobject):
rgbas: Vect4Array | None = None, rgbas: Vect4Array | None = None,
color: ManimColor | None = None, color: ManimColor | None = None,
opacity: float | None = None opacity: float | None = None
): ) -> Self:
""" """
points must be a Nx3 numpy array, as must rgbas if it is not None 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 self.data["rgba"][-len(rgbas):] = rgbas
return self 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] rgbas = None if rgba is None else [rgba]
self.add_points([point], rgbas, color, opacity) self.add_points([point], rgbas, color, opacity)
return self return self
@Mobject.affects_data @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( self.data["rgba"][:] = np.array(list(map(
color_to_rgba, color_to_rgba,
color_gradient(colors, self.get_num_points()) color_gradient(colors, self.get_num_points())
@ -60,20 +60,20 @@ class PMobject(Mobject):
return self return self
@Mobject.affects_data @Mobject.affects_data
def match_colors(self, pmobject: PMobject): def match_colors(self, pmobject: PMobject) -> Self:
self.data["rgba"][:] = resize_with_interpolation( self.data["rgba"][:] = resize_with_interpolation(
pmobject.data["rgba"], self.get_num_points() pmobject.data["rgba"], self.get_num_points()
) )
return self return self
@Mobject.affects_data @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(): for mob in self.family_members_with_points():
mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())] mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())]
return self return self
@Mobject.affects_data @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 function is any map from R^3 to R
""" """
@ -85,7 +85,7 @@ class PMobject(Mobject):
return self return self
@Mobject.affects_data @Mobject.affects_data
def ingest_submobjects(self): def ingest_submobjects(self) -> Self:
self.data = np.vstack([ self.data = np.vstack([
sm.data for sm in self.get_family() sm.data for sm in self.get_family()
]) ])
@ -96,7 +96,7 @@ class PMobject(Mobject):
return self.get_points()[int(index)] return self.get_points()[int(index)]
@Mobject.affects_data @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()) lower_index = int(a * pmobject.get_num_points())
upper_index = int(b * pmobject.get_num_points()) upper_index = int(b * pmobject.get_num_points())
self.data = pmobject.data[lower_index:upper_index].copy() 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 listify
from manimlib.utils.iterables import resize_with_interpolation from manimlib.utils.iterables import resize_with_interpolation
from manimlib.utils.space_ops import normalize_along_axis from manimlib.utils.space_ops import normalize_along_axis
from manimlib.utils.space_ops import cross
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
if 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.camera.camera import Camera
from manimlib.typing import ManimColor, Vect3, Vect3Array from manimlib.typing import ManimColor, Vect3, Vect3Array
@ -27,11 +28,9 @@ class Surface(Mobject):
shader_folder: str = "surface" shader_folder: str = "surface"
shader_dtype: np.dtype = np.dtype([ shader_dtype: np.dtype = np.dtype([
('point', np.float32, (3,)), ('point', np.float32, (3,)),
('du_point', np.float32, (3,)), ('normal', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('rgba', np.float32, (4,)), ('rgba', np.float32, (4,)),
]) ])
pointlike_data_keys = ['point', 'du_point', 'dv_point']
def __init__( def __init__(
self, self,
@ -96,17 +95,24 @@ class Surface(Mobject):
for grid in (uv_grid, uv_plus_du, uv_plus_dv) for grid in (uv_grid, uv_plus_du, uv_plus_dv)
] ]
self.set_points(points) self.set_points(points)
self.data["du_point"][:] = du_points self.data["normal"] = normalize_along_axis(cross(
self.data["dv_point"][:] = dv_points (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 # TODO, if there is an event which changes
# the resolution of the surface, make sure # the resolution of the surface, make sure
# this is called. # this is called.
nu, nv = self.resolution nu, nv = self.resolution
if nu == 0 or nv == 0: if nu == 0 or nv == 0:
self.triangle_indices = np.zeros(0, dtype=int) self.triangle_indices = np.zeros(0, dtype=int)
return return self.triangle_indices
index_grid = np.arange(nu * nv).reshape((nu, nv)) index_grid = np.arange(nu * nv).reshape((nu, nv))
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int) indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left 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[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
self.triangle_indices = indices self.triangle_indices = indices
return self.triangle_indices
def get_triangle_indices(self) -> np.ndarray: def get_triangle_indices(self) -> np.ndarray:
return self.triangle_indices 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: def get_unit_normals(self) -> Vect3Array:
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points() nu, nv = self.resolution
normals = np.cross( indices = np.arange(nu * nv)
(du_points - s_points) / self.epsilon,
(dv_points - s_points) / self.epsilon, 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 @Mobject.affects_data
def pointwise_become_partial( def pointwise_become_partial(
@ -138,7 +156,7 @@ class Surface(Mobject):
a: float, a: float,
b: float, b: float,
axis: int | None = None axis: int | None = None
): ) -> Self:
assert(isinstance(smobject, Surface)) assert(isinstance(smobject, Surface))
if axis is None: if axis is None:
axis = self.prefered_creation_axis axis = self.prefered_creation_axis
@ -147,12 +165,11 @@ class Surface(Mobject):
return self return self
nu, nv = smobject.resolution nu, nv = smobject.resolution
for key in ['point', 'du_point', 'dv_point']: self.data['point'][:] = self.get_partial_points_array(
self.data[key][:] = self.get_partial_points_array( self.data['point'], a, b,
self.data[key], a, b, (nu, nv, 3),
(nu, nv, 3), axis=axis
axis=axis )
)
return self return self
def get_partial_points_array( def get_partial_points_array(
@ -196,7 +213,7 @@ class Surface(Mobject):
return points.reshape((nu * nv, *resolution[2:])) return points.reshape((nu * nv, *resolution[2:]))
@Mobject.affects_data @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 tri_is = self.triangle_indices
points = self.get_points() points = self.get_points()
@ -206,24 +223,25 @@ class Surface(Mobject):
tri_is[k::3] = tri_is[k::3][indices] tri_is[k::3] = tri_is[k::3][indices]
return self return self
def always_sort_to_camera(self, camera: Camera): def always_sort_to_camera(self, camera: Camera) -> Self:
def updater(surface: Surface): def updater(surface: Surface):
vect = camera.get_location() - surface.get_center() vect = camera.get_location() - surface.get_center()
surface.sort_faces_back_to_front(vect) surface.sort_faces_back_to_front(vect)
self.add_updater(updater) self.add_updater(updater)
return self
def set_clip_plane( def set_clip_plane(
self, self,
vect: Vect3 | None = None, vect: Vect3 | None = None,
threshold: float | None = None threshold: float | None = None
): ) -> Self:
if vect is not None: if vect is not None:
self.uniforms["clip_plane"][:3] = vect self.uniforms["clip_plane"][:3] = vect
if threshold is not None: if threshold is not None:
self.uniforms["clip_plane"][3] = threshold self.uniforms["clip_plane"][3] = threshold
return self return self
def deactivate_clip_plane(self): def deactivate_clip_plane(self) -> Self:
self.uniforms["clip_plane"][:] = 0 self.uniforms["clip_plane"][:] = 0
return self return self
@ -263,8 +281,7 @@ class TexturedSurface(Surface):
shader_folder: str = "textured_surface" shader_folder: str = "textured_surface"
shader_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [ shader_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
('point', np.float32, (3,)), ('point', np.float32, (3,)),
('du_point', np.float32, (3,)), ('normal', np.float32, (3,)),
('dv_point', np.float32, (3,)),
('im_coords', np.float32, (2,)), ('im_coords', np.float32, (2,)),
('opacity', np.float32, (1,)), ('opacity', np.float32, (1,)),
] ]
@ -306,8 +323,9 @@ class TexturedSurface(Surface):
surf = self.uv_surface surf = self.uv_surface
nu, nv = surf.resolution nu, nv = surf.resolution
self.resize_points(surf.get_num_points()) self.resize_points(surf.get_num_points())
for key in ['point', 'du_point', 'dv_point']: self.resolution = surf.resolution
self.data[key][:] = surf.data[key] self.data['point'][:] = surf.data['point']
self.data['normal'][:] = surf.data['normal']
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3] self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
self.data["im_coords"] = np.array([ self.data["im_coords"] = np.array([
[u, v] [u, v]
@ -320,7 +338,7 @@ class TexturedSurface(Surface):
self.uniforms["num_textures"] = self.num_textures self.uniforms["num_textures"] = self.num_textures
@Mobject.affects_data @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)) op_arr = np.array(listify(opacity))
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data)) self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
return self return self
@ -330,7 +348,7 @@ class TexturedSurface(Surface):
color: ManimColor | Iterable[ManimColor] | None, color: ManimColor | Iterable[ManimColor] | None,
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
recurse: bool = True recurse: bool = True
): ) -> Self:
if opacity is not None: if opacity is not None:
self.set_opacity(opacity) self.set_opacity(opacity)
return self return self
@ -341,7 +359,7 @@ class TexturedSurface(Surface):
a: float, a: float,
b: float, b: float,
axis: int = 1 axis: int = 1
): ) -> Self:
super().pointwise_become_partial(tsmobject, a, b, axis) super().pointwise_become_partial(tsmobject, a, b, axis)
im_coords = self.data["im_coords"] im_coords = self.data["im_coords"]
im_coords[:] = tsmobject.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 from typing import TYPE_CHECKING
if 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 manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array
from moderngl.context import Context from moderngl.context import Context
@ -68,8 +68,8 @@ class VMobject(Mobject):
fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal'] fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal']
stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product'] stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
fill_render_primitive: int = moderngl.TRIANGLE_STRIP fill_render_primitive: int = moderngl.TRIANGLES
stroke_render_primitive: int = moderngl.TRIANGLE_STRIP stroke_render_primitive: int = moderngl.TRIANGLES
pre_function_handle_to_anchor_scale_factor: float = 0.01 pre_function_handle_to_anchor_scale_factor: float = 0.01
make_smooth_after_applying_functions: bool = False 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["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
self.uniforms["flat_stroke"] = float(self.flat_stroke) self.uniforms["flat_stroke"] = float(self.flat_stroke)
# These are here just to make type checkers happy def add(self, *vmobjects: VMobject) -> Self:
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):
if not all((isinstance(m, VMobject) for m in vmobjects)): if not all((isinstance(m, VMobject) for m in vmobjects)):
raise Exception("All submobjects must be of type VMobject") raise Exception("All submobjects must be of type VMobject")
super().add(*vmobjects) return 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
# Colors # Colors
def init_colors(self): def init_colors(self):
@ -185,7 +156,7 @@ class VMobject(Mobject):
rgba_array: Vect4Array, rgba_array: Vect4Array,
name: str | None = None, name: str | None = None,
recurse: bool = False recurse: bool = False
): ) -> Self:
if name is None: if name is None:
names = ["fill_rgba", "stroke_rgba"] names = ["fill_rgba", "stroke_rgba"]
else: else:
@ -201,7 +172,7 @@ class VMobject(Mobject):
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
border_width: float | None = None, border_width: float | None = None,
recurse: bool = True recurse: bool = True
): ) -> Self:
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse) self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
if border_width is not None: if border_width is not None:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
@ -215,7 +186,7 @@ class VMobject(Mobject):
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
background: bool | None = None, background: bool | None = None,
recurse: bool = True recurse: bool = True
): ) -> Self:
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse) self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
if width is not None: if width is not None:
@ -238,7 +209,7 @@ class VMobject(Mobject):
color: ManimColor | Iterable[ManimColor] = BLACK, color: ManimColor | Iterable[ManimColor] = BLACK,
width: float | Iterable[float] = 3, width: float | Iterable[float] = 3,
background: bool = True background: bool = True
): ) -> Self:
self.set_stroke(color, width, background=background) self.set_stroke(color, width, background=background)
return self return self
@ -255,7 +226,7 @@ class VMobject(Mobject):
stroke_background: bool = True, stroke_background: bool = True,
shading: Tuple[float, float, float] | None = None, shading: Tuple[float, float, float] | None = None,
recurse: bool = True recurse: bool = True
): ) -> Self:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
if fill_rgba is not None: if fill_rgba is not None:
mob.data['fill_rgba'][:] = resize_with_interpolation(fill_rgba, len(mob.data['fill_rgba'])) 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) mob.set_shading(*shading, recurse=False)
return self 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 data = self.data if self.get_num_points() > 0 else self._data_defaults
return { return {
"fill_rgba": data['fill_rgba'].copy(), "fill_rgba": data['fill_rgba'].copy(),
@ -296,7 +267,7 @@ class VMobject(Mobject):
"shading": self.get_shading(), "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) self.set_style(**vmobject.get_style(), recurse=False)
if recurse: if recurse:
# Does its best to match up submobject lists, and # Does its best to match up submobject lists, and
@ -315,7 +286,7 @@ class VMobject(Mobject):
color: ManimColor | Iterable[ManimColor] | None, color: ManimColor | Iterable[ManimColor] | None,
opacity: float | Iterable[float] | None = None, opacity: float | Iterable[float] | None = None,
recurse: bool = True recurse: bool = True
): ) -> Self:
self.set_fill(color, opacity=opacity, recurse=recurse) self.set_fill(color, opacity=opacity, recurse=recurse)
self.set_stroke(color, opacity=opacity, recurse=recurse) self.set_stroke(color, opacity=opacity, recurse=recurse)
return self return self
@ -324,12 +295,16 @@ class VMobject(Mobject):
self, self,
opacity: float | Iterable[float] | None, opacity: float | Iterable[float] | None,
recurse: bool = True recurse: bool = True
): ) -> Self:
self.set_fill(opacity=opacity, recurse=recurse) self.set_fill(opacity=opacity, recurse=recurse)
self.set_stroke(opacity=opacity, recurse=recurse) self.set_stroke(opacity=opacity, recurse=recurse)
return self 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] mobs = self.get_family() if recurse else [self]
for mob in mobs: for mob in mobs:
factor = 1.0 - darkness factor = 1.0 - darkness
@ -399,6 +374,9 @@ class VMobject(Mobject):
return self.get_fill_color() return self.get_fill_color()
return self.get_stroke_color() return self.get_stroke_color()
def get_anti_alias_width(self):
return self.uniforms["anti_alias_width"]
def has_stroke(self) -> bool: def has_stroke(self) -> bool:
return any(self.data['stroke_width']) and any(self.data['stroke_rgba'][:, 3]) 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_fill_opacity()
return self.get_stroke_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): for mob in self.get_family(recurse):
mob.uniforms["flat_stroke"] = float(flat_stroke) mob.uniforms["flat_stroke"] = float(flat_stroke)
return self return self
@ -418,7 +396,7 @@ class VMobject(Mobject):
def get_flat_stroke(self) -> bool: def get_flat_stroke(self) -> bool:
return self.uniforms["flat_stroke"] == 1.0 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): for mob in self.get_family(recurse):
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type] mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
return self return self
@ -426,10 +404,34 @@ class VMobject(Mobject):
def get_joint_type(self) -> float: def get_joint_type(self) -> float:
return self.uniforms["joint_type"] 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 @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): for submob in self.get_family(recurse):
submob._use_winding_fill = value submob._use_winding_fill = value
if not value and submob.has_points():
submob.subdivide_intersections()
return self return self
# Points # Points
@ -437,7 +439,7 @@ class VMobject(Mobject):
self, self,
anchors: Vect3Array, anchors: Vect3Array,
handles: Vect3Array, handles: Vect3Array,
): ) -> Self:
assert(len(anchors) == len(handles) + 1) assert(len(anchors) == len(handles) + 1)
points = resize_array(self.get_points(), 2 * len(anchors) - 1) points = resize_array(self.get_points(), 2 * len(anchors) - 1)
points[0::2] = anchors points[0::2] = anchors
@ -445,7 +447,7 @@ class VMobject(Mobject):
self.set_points(points) self.set_points(points)
return self 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 # Path ends are signaled by a handle point sitting directly
# on top of the previous anchor # on top of the previous anchor
if self.has_points(): if self.has_points():
@ -460,7 +462,7 @@ class VMobject(Mobject):
handle1: Vect3, handle1: Vect3,
handle2: Vect3, handle2: Vect3,
anchor2: Vect3 anchor2: Vect3
): ) -> Self:
self.start_new_path(anchor1) self.start_new_path(anchor1)
self.add_cubic_bezier_curve_to(handle1, handle2, anchor2) self.add_cubic_bezier_curve_to(handle1, handle2, anchor2)
return self return self
@ -470,7 +472,7 @@ class VMobject(Mobject):
handle1: Vect3, handle1: Vect3,
handle2: Vect3, handle2: Vect3,
anchor: Vect3, anchor: Vect3,
): ) -> Self:
""" """
Add cubic bezier curve to the path. Add cubic bezier curve to the path.
""" """
@ -492,7 +494,7 @@ class VMobject(Mobject):
self.append_points(quad_approx[1:]) self.append_points(quad_approx[1:])
return self 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() self.throw_error_if_no_points()
last_point = self.get_last_point() last_point = self.get_last_point()
if self.consider_points_equal(handle, last_point): if self.consider_points_equal(handle, last_point):
@ -501,14 +503,14 @@ class VMobject(Mobject):
self.append_points([handle, anchor]) self.append_points([handle, anchor])
return self return self
def add_line_to(self, point: Vect3): def add_line_to(self, point: Vect3) -> Self:
self.throw_error_if_no_points() self.throw_error_if_no_points()
last_point = self.get_last_point() last_point = self.get_last_point()
alphas = np.linspace(0, 1, 5 if self.long_lines else 3) alphas = np.linspace(0, 1, 5 if self.long_lines else 3)
self.append_points(outer_interpolate(last_point, point, alphas[1:])) self.append_points(outer_interpolate(last_point, point, alphas[1:]))
return self 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(): if self.has_new_path_started():
self.add_line_to(point) self.add_line_to(point)
else: else:
@ -517,7 +519,7 @@ class VMobject(Mobject):
self.add_quadratic_bezier_curve_to(new_handle, point) self.add_quadratic_bezier_curve_to(new_handle, point)
return self 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() self.throw_error_if_no_points()
if self.get_num_points() == 1: if self.get_num_points() == 1:
new_handle = handle new_handle = handle
@ -538,7 +540,7 @@ class VMobject(Mobject):
points = self.get_points() points = self.get_points()
return 2 * points[-1] - points[-2] 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(): if self.is_closed():
return self return self
last_path_start = self.get_subpaths()[-1][0] last_path_start = self.get_subpaths()[-1][0]
@ -556,7 +558,7 @@ class VMobject(Mobject):
self, self,
tuple_to_subdivisions: Callable, tuple_to_subdivisions: Callable,
recurse: bool = True recurse: bool = True
): ) -> Self:
for vmob in self.get_family(recurse): for vmob in self.get_family(recurse):
if not vmob.has_points(): if not vmob.has_points():
continue continue
@ -578,7 +580,7 @@ class VMobject(Mobject):
self, self,
angle_threshold: float = 30 * DEGREES, angle_threshold: float = 30 * DEGREES,
recurse: bool = True recurse: bool = True
): ) -> Self:
def tuple_to_subdivisions(b0, b1, b2): def tuple_to_subdivisions(b0, b1, b2):
angle = angle_between_vectors(b1 - b0, b2 - b1) angle = angle_between_vectors(b1 - b0, b2 - b1)
return int(angle / angle_threshold) return int(angle / angle_threshold)
@ -586,7 +588,7 @@ class VMobject(Mobject):
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse) self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self 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() path = self.get_anchors()
def tuple_to_subdivisions(b0, b1, b2): def tuple_to_subdivisions(b0, b1, b2):
if line_intersects_path(b0, b1, path): if line_intersects_path(b0, b1, path):
@ -596,12 +598,12 @@ class VMobject(Mobject):
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse) self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
return self 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: for point in points:
self.add_line_to(point) 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) anchors = np.array(points)
handles = 0.5 * (anchors[:-1] + anchors[1:]) handles = 0.5 * (anchors[:-1] + anchors[1:])
self.set_anchors_and_handles(anchors, handles) self.set_anchors_and_handles(anchors, handles)
@ -611,7 +613,7 @@ class VMobject(Mobject):
self, self,
points: Iterable[Vect3], points: Iterable[Vect3],
approx: bool = True approx: bool = True
): ) -> Self:
self.set_points_as_corners(points) self.set_points_as_corners(points)
self.make_smooth(approx=approx) self.make_smooth(approx=approx)
return self return self
@ -620,7 +622,7 @@ class VMobject(Mobject):
dots = self.get_joint_products()[::2, 3] dots = self.get_joint_products()[::2, 3]
return bool((dots > 1 - 1e-3).all()) 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")) assert(mode in ("jagged", "approx_smooth", "true_smooth"))
subpaths = self.get_subpaths() subpaths = self.get_subpaths()
self.clear_points() self.clear_points()
@ -643,7 +645,7 @@ class VMobject(Mobject):
self.add_subpath(new_subpath) self.add_subpath(new_subpath)
return self 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 Edits the path so as to pass smoothly through all
the current anchor points. the current anchor points.
@ -658,15 +660,16 @@ class VMobject(Mobject):
submob.change_anchor_mode(mode) submob.change_anchor_mode(mode)
return self return self
def make_approximately_smooth(self, recurse=True): def make_approximately_smooth(self, recurse=True) -> Self:
self.make_smooth(approx=True, recurse=recurse) 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): for submob in self.get_family(recurse):
submob.change_anchor_mode("jagged") submob.change_anchor_mode("jagged")
return self return self
def add_subpath(self, points: Vect3Array): def add_subpath(self, points: Vect3Array) -> Self:
assert(len(points) % 2 == 1 or len(points) == 0) assert(len(points) % 2 == 1 or len(points) == 0)
if not self.has_points(): if not self.has_points():
self.set_points(points) self.set_points(points)
@ -676,7 +679,7 @@ class VMobject(Mobject):
self.append_points(points[1:]) self.append_points(points[1:])
return self return self
def append_vectorized_mobject(self, vmobject: VMobject): def append_vectorized_mobject(self, vmobject: VMobject) -> Self:
self.add_subpath(vmobject.get_points()) self.add_subpath(vmobject.get_points())
n = vmobject.get_num_points() n = vmobject.get_num_points()
self.data[-n:] = vmobject.data self.data[-n:] = vmobject.data
@ -694,7 +697,7 @@ class VMobject(Mobject):
def get_bezier_tuples(self) -> Iterable[Vect3Array]: def get_bezier_tuples(self) -> Iterable[Vect3Array]:
return self.get_bezier_tuples_from_points(self.get_points()) 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 atol = self.tolerance_for_point_equality
a0, h, a1 = points[0:-1:2], points[1::2], points[2::2] a0, h, a1 = points[0:-1:2], points[1::2], points[2::2]
# An anchor point is considered the end of a path # 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:] is_end[:-1] = is_end[:-1] & ~is_end[1:]
return np.array([2 * n for n, end in enumerate(is_end) if end]) 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()) return self.get_subpath_end_indices_from_points(self.get_points())
def get_subpaths_from_points(self, points: Vect3Array) -> list[Vect3Array]: def get_subpaths_from_points(self, points: Vect3Array) -> list[Vect3Array]:
@ -839,7 +842,7 @@ class VMobject(Mobject):
self.data["unit_normal"][:] = normal self.data["unit_normal"][:] = normal
return normal return normal
def refresh_unit_normal(self): def refresh_unit_normal(self) -> Self:
self.get_unit_normal() self.get_unit_normal()
return self return self
@ -849,20 +852,20 @@ class VMobject(Mobject):
axis: Vect3 = OUT, axis: Vect3 = OUT,
about_point: Vect3 | None = None, about_point: Vect3 | None = None,
**kwargs **kwargs
): ) -> Self:
super().rotate(angle, axis, about_point, **kwargs) super().rotate(angle, axis, about_point, **kwargs)
for mob in self.get_family(): for mob in self.get_family():
mob.refresh_unit_normal() mob.refresh_unit_normal()
return self return self
def ensure_positive_orientation(self, recurse=True): def ensure_positive_orientation(self, recurse=True) -> Self:
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
if mob.get_unit_normal()[2] < 0: if mob.get_unit_normal()[2] < 0:
mob.reverse_points() mob.reverse_points()
return self return self
# Alignment # Alignment
def align_points(self, vmobject: VMobject): def align_points(self, vmobject: VMobject) -> Self:
winding = self._use_winding_fill and vmobject._use_winding_fill winding = self._use_winding_fill and vmobject._use_winding_fill
self.use_winding_fill(winding) self.use_winding_fill(winding)
vmobject.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 # If both have fill, and they have the same shape, just
# give them the same triangulation so that it's not recalculated # give them the same triangulation so that it's not recalculated
# needlessly throughout an animation # needlessly throughout an animation
if not self._use_winding_fill and self.has_fill() \ match_tris = not self._use_winding_fill and \
and vmobject.has_fill() and self.has_same_shape_as(vmobject): self.has_fill() and \
vmobject.has_fill() and \
self.has_same_shape_as(vmobject)
if match_tris:
vmobject.triangulation = self.triangulation vmobject.triangulation = self.triangulation
return self return self
@ -884,6 +890,11 @@ class VMobject(Mobject):
# Figure out what the subpaths are, and align # Figure out what the subpaths are, and align
subpaths1 = self.get_subpaths() subpaths1 = self.get_subpaths()
subpaths2 = vmobject.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)) n_subpaths = max(len(subpaths1), len(subpaths2))
# Start building new ones # Start building new ones
@ -892,8 +903,7 @@ class VMobject(Mobject):
def get_nth_subpath(path_list, n): def get_nth_subpath(path_list, n):
if n >= len(path_list): if n >= len(path_list):
# Create a null path at the very end return np.vstack([path_list[0][:-1], path_list[0][::-1]])
return [path_list[-1][-1]] * 3
return path_list[n] return path_list[n]
for n in range(n_subpaths): for n in range(n_subpaths):
@ -917,22 +927,14 @@ class VMobject(Mobject):
mob.get_joint_products() mob.get_joint_products()
return self return self
def invisible_copy(self): def insert_n_curves(self, n: int, recurse: bool = True) -> 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):
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
if mob.get_num_curves() > 0: if mob.get_num_curves() > 0:
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points()) new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
mob.set_points(new_points) mob.set_points(new_points)
return self 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: if len(points) == 1:
return np.repeat(points, 2 * n + 1, 0) return np.repeat(points, 2 * n + 1, 0)
@ -965,7 +967,7 @@ class VMobject(Mobject):
mobject2: VMobject, mobject2: VMobject,
alpha: float, alpha: float,
*args, **kwargs *args, **kwargs
): ) -> Self:
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs) super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
if self.has_fill() and not self._use_winding_fill: if self.has_fill() and not self._use_winding_fill:
tri1 = mobject1.get_triangulation() tri1 = mobject1.get_triangulation()
@ -974,7 +976,7 @@ class VMobject(Mobject):
self.refresh_triangulation() self.refresh_triangulation()
return self 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)) assert(isinstance(vmobject, VMobject))
vm_points = vmobject.get_points() vm_points = vmobject.get_points()
self.data["joint_product"] = vmobject.data["joint_product"] self.data["joint_product"] = vmobject.data["joint_product"]
@ -1017,12 +1019,12 @@ class VMobject(Mobject):
self.set_points(new_points, refresh_joints=False) self.set_points(new_points, refresh_joints=False)
return self return self
def get_subcurve(self, a: float, b: float) -> VMobject: def get_subcurve(self, a: float, b: float) -> Self:
vmob = self.copy() vmob = self.copy()
vmob.pointwise_become_partial(self, a, b) vmob.pointwise_become_partial(self, a, b)
return vmob 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, ...) 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 # Data for shaders that may need refreshing
def refresh_triangulation(self): def refresh_triangulation(self) -> Self:
for mob in self.get_family(): for mob in self.get_family():
mob.needs_new_triangulation = True mob.needs_new_triangulation = True
return self return self
def get_triangulation(self): def get_triangulation(self) -> np.ndarray:
# Figure out how to triangulate the interior to know # Figure out how to triangulate the interior to know
# how to send the points as to the vertex shader. # how to send the points as to the vertex shader.
# First triangles come directly from the points # First triangles come directly from the points
@ -1095,12 +1097,12 @@ class VMobject(Mobject):
self.needs_new_triangulation = False self.needs_new_triangulation = False
return tri_indices return tri_indices
def refresh_joint_products(self): def refresh_joint_products(self) -> Self:
for mob in self.get_family(): for mob in self.get_family():
mob.needs_new_joint_products = True mob.needs_new_joint_products = True
return self 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 The 'joint product' is a 4-vector holding the cross and dot
product between tangent vectors at a joint 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) self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1)
return self.data["joint_product"] 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]: for mob in [self, vmobject1, vmobject2]:
mob.get_joint_products() mob.get_joint_products()
super().lock_matching_data(vmobject1, vmobject2) super().lock_matching_data(vmobject1, vmobject2)
return self
def triggers_refreshed_triangulation(func: Callable): def triggers_refreshed_triangulation(func: Callable):
@wraps(func) @wraps(func)
@ -1166,7 +1169,7 @@ class VMobject(Mobject):
return self return self
return wrapper 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) assert(len(points) == 0 or len(points) % 2 == 1)
super().set_points(points) super().set_points(points)
self.refresh_triangulation() self.refresh_triangulation()
@ -1176,13 +1179,13 @@ class VMobject(Mobject):
return self return self
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
def append_points(self, points: Vect3Array): def append_points(self, points: Vect3Array) -> Self:
assert(len(points) % 2 == 0) assert(len(points) % 2 == 0)
super().append_points(points) super().append_points(points)
return self return self
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
def reverse_points(self, recurse: bool = True): def reverse_points(self, recurse: bool = True) -> Self:
# This will reset which anchors are # This will reset which anchors are
# considered path ends # considered path ends
for mob in self.get_family(recurse): for mob in self.get_family(recurse):
@ -1195,7 +1198,7 @@ class VMobject(Mobject):
return self return self
@triggers_refreshed_triangulation @triggers_refreshed_triangulation
def set_data(self, data: np.ndarray): def set_data(self, data: np.ndarray) -> Self:
super().set_data(data) super().set_data(data)
return self return self
@ -1206,15 +1209,25 @@ class VMobject(Mobject):
function: Callable[[Vect3], Vect3], function: Callable[[Vect3], Vect3],
make_smooth: bool = False, make_smooth: bool = False,
**kwargs **kwargs
): ) -> Self:
super().apply_function(function, **kwargs) super().apply_function(function, **kwargs)
if self.make_smooth_after_applying_functions or make_smooth: if self.make_smooth_after_applying_functions or make_smooth:
self.make_smooth(approx=True) self.make_smooth(approx=True)
return self return self
def apply_points_function(self, *args, **kwargs): def apply_points_function(self, *args, **kwargs) -> Self:
super().apply_points_function(*args, **kwargs) super().apply_points_function(*args, **kwargs)
self.refresh_joint_products() 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 # For shaders
def init_shader_data(self, ctx: Context): def init_shader_data(self, ctx: Context):
@ -1249,7 +1262,7 @@ class VMobject(Mobject):
self.stroke_shader_wrapper, self.stroke_shader_wrapper,
] ]
def refresh_shader_wrapper_id(self): def refresh_shader_wrapper_id(self) -> Self:
if not self._shaders_initialized: if not self._shaders_initialized:
return self return self
for wrapper in self.shader_wrappers: for wrapper in self.shader_wrappers:
@ -1269,43 +1282,40 @@ class VMobject(Mobject):
# Build up data lists # Build up data lists
fill_datas = [] fill_datas = []
fill_border_datas = []
fill_indices = [] fill_indices = []
fill_border_datas = []
stroke_datas = [] stroke_datas = []
back_stroke_datas = [] back_stroke_datas = []
for submob in family: for submob in family:
submob.get_joint_products() submob.get_joint_products()
indices = submob.get_outer_vert_indices()
has_fill = submob.has_fill() has_fill = submob.has_fill()
has_stroke = submob.has_stroke() 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 = submob.data[fill_names]
data["base_point"][:] = data["point"][0] data["base_point"][:] = data["point"][0]
fill_datas.append(data) fill_datas.append(data[indices])
if self._use_winding_fill: if has_fill and not submob._use_winding_fill:
# Add dummy fill_datas.append(submob.data[fill_names])
fill_datas.append(data[-1:]) fill_indices.append(submob.get_triangulation())
else: if has_fill and not front_stroke:
fill_indices.append(submob.get_triangulation())
# Add fill border # Add fill border
if not has_stroke: names = list(stroke_names)
names = list(stroke_names) names[names.index('stroke_rgba')] = 'fill_rgba'
names[names.index('stroke_rgba')] = 'fill_rgba' names[names.index('stroke_width')] = 'fill_border_width'
names[names.index('stroke_width')] = 'fill_border_width' border_stroke_data = submob.data[names].astype(
border_stroke_data = submob.data[names] self.stroke_shader_wrapper.vert_data.dtype
fill_border_datas.append(border_stroke_data) )
fill_border_datas.append(border_stroke_data[-1:]) fill_border_datas.append(border_stroke_data[indices])
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:])
shader_wrappers = [ shader_wrappers = [
self.back_stroke_shader_wrapper.read_in( self.back_stroke_shader_wrapper.read_in([*back_stroke_datas, *fill_border_datas]),
[*back_stroke_datas, *fill_border_datas]
),
self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None),
self.stroke_shader_wrapper.read_in(stroke_datas), self.stroke_shader_wrapper.read_in(stroke_datas),
] ]
@ -1319,7 +1329,7 @@ class VGroup(VMobject):
super().__init__(**kwargs) super().__init__(**kwargs)
self.add(*vmobjects) self.add(*vmobjects)
def __add__(self, other: VMobject | VGroup): def __add__(self, other: VMobject) -> Self:
assert(isinstance(other, VMobject)) assert(isinstance(other, VMobject))
return self.add(other) return self.add(other)

View file

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

View file

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

View file

@ -3,13 +3,15 @@ from __future__ import annotations
import itertools as it import itertools as it
import numpy as np import numpy as np
import pyperclip import pyperclip
from IPython.core.getipython import get_ipython
from manimlib.animation.fading import FadeIn from manimlib.animation.fading import FadeIn
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER 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 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 PI
from manimlib.constants import DEGREES
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Line
from manimlib.mobject.geometry import Rectangle 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.mobject.types.vectorized_mobject import VMobject
from manimlib.scene.scene import Scene from manimlib.scene.scene import Scene
from manimlib.scene.scene import SceneState 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.family_ops import extract_mobject_family_members
from manimlib.utils.space_ops import get_norm from manimlib.utils.space_ops import get_norm
from manimlib.utils.tex_file_writing import LatexError from manimlib.utils.tex_file_writing import LatexError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from manimlib.typing import Vect3
SELECT_KEY = 's' SELECT_KEY = 's'
UNSELECT_KEY = 'u' UNSELECT_KEY = 'u'
@ -68,7 +76,7 @@ class InteractiveScene(Scene):
""" """
corner_dot_config = dict( corner_dot_config = dict(
color=WHITE, color=WHITE,
radius=0.025, radius=0.05,
glow_factor=2.0, glow_factor=2.0,
) )
selection_rectangle_stroke_color = WHITE selection_rectangle_stroke_color = WHITE
@ -126,7 +134,7 @@ class InteractiveScene(Scene):
def update_selection_rectangle(self, rect: Rectangle): def update_selection_rectangle(self, rect: Rectangle):
p1 = rect.fixed_corner 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([ rect.set_points_as_corners([
p1, np.array([p2[0], p1[1], 0]), p1, np.array([p2[0], p1[1], 0]),
p2, np.array([p1[0], p2[1], 0]), p2, np.array([p1[0], p2[1], 0]),
@ -228,9 +236,6 @@ class InteractiveScene(Scene):
super().remove(*mobjects) super().remove(*mobjects)
self.regenerate_selection_search_set() self.regenerate_selection_search_set()
# def increment_time(self, dt: float) -> None:
# super().increment_time(dt)
# Related to selection # Related to selection
def toggle_selection_mode(self): def toggle_selection_mode(self):
@ -273,7 +278,7 @@ class InteractiveScene(Scene):
def get_corner_dots(self, mobject: Mobject) -> Mobject: def get_corner_dots(self, mobject: Mobject) -> Mobject:
dots = DotCloud(**self.corner_dot_config) 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: if mobject.get_depth() < 1e-2:
vects = [DL, UL, UR, DR] vects = [DL, UL, UR, DR]
else: else:
@ -339,8 +344,17 @@ class InteractiveScene(Scene):
# Functions for keyboard actions # Functions for keyboard actions
def copy_selection(self): def copy_selection(self):
ids = map(id, self.selection) names = []
pyperclip.copy(",".join(map(str, ids))) 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): def paste_selection(self):
clipboard_str = pyperclip.paste() clipboard_str = pyperclip.paste()
@ -377,7 +391,9 @@ class InteractiveScene(Scene):
def enable_selection(self): def enable_selection(self):
self.is_selecting = True self.is_selecting = True
self.add(self.selection_rectangle) 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): def gather_new_selection(self):
self.is_selecting = False self.is_selecting = False
@ -387,7 +403,9 @@ class InteractiveScene(Scene):
for mob in reversed(self.get_selection_search_set()): for mob in reversed(self.get_selection_search_set()):
if self.selection_rectangle.is_touching(mob): if self.selection_rectangle.is_touching(mob):
additions.append(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): def prepare_grab(self):
mp = self.mouse_point.get_center() mp = self.mouse_point.get_center()
@ -447,6 +465,7 @@ class InteractiveScene(Scene):
else: else:
self.save_mobject_to_file(self.selection) self.save_mobject_to_file(self.selection)
# Key actions
def on_key_press(self, symbol: int, modifiers: int) -> None: def on_key_press(self, symbol: int, modifiers: int) -> None:
super().on_key_press(symbol, modifiers) super().on_key_press(symbol, modifiers)
char = chr(symbol) char = chr(symbol)
@ -485,6 +504,8 @@ class InteractiveScene(Scene):
self.toggle_selection_mode() self.toggle_selection_mode()
elif char == "s" and modifiers == COMMAND_MODIFIER: elif char == "s" and modifiers == COMMAND_MODIFIER:
self.save_selection_to_file() self.save_selection_to_file()
elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER:
self.copy_frame_anim_call()
elif symbol in ARROW_SYMBOLS: elif symbol in ARROW_SYMBOLS:
self.nudge_selection( self.nudge_selection(
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
@ -507,7 +528,6 @@ class InteractiveScene(Scene):
super().on_key_release(symbol, modifiers) super().on_key_release(symbol, modifiers)
if chr(symbol) == SELECT_KEY: if chr(symbol) == SELECT_KEY:
self.gather_new_selection() self.gather_new_selection()
# self.remove(self.crosshair)
if chr(symbol) in GRAB_KEYS: if chr(symbol) in GRAB_KEYS:
self.is_grabbing = False self.is_grabbing = False
elif chr(symbol) == INFORMATION_KEY: elif chr(symbol) == INFORMATION_KEY:
@ -516,7 +536,7 @@ class InteractiveScene(Scene):
self.prepare_resizing(about_corner=False) self.prepare_resizing(about_corner=False)
# Mouse actions # Mouse actions
def handle_grabbing(self, point: np.ndarray): def handle_grabbing(self, point: Vect3):
diff = point - self.mouse_to_selection diff = point - self.mouse_to_selection
if self.window.is_key_pressed(ord(GRAB_KEY)): if self.window.is_key_pressed(ord(GRAB_KEY)):
self.selection.move_to(diff) self.selection.move_to(diff)
@ -525,7 +545,7 @@ class InteractiveScene(Scene):
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)): elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
self.selection.set_y(diff[1]) 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"): if not hasattr(self, "scale_about_point"):
return return
vect = point - self.scale_about_point vect = point - self.scale_about_point
@ -545,15 +565,16 @@ class InteractiveScene(Scene):
about_point=self.scale_about_point 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( mob = self.point_to_mobject(
point, search_set=self.get_selection_search_set(), point,
search_set=self.get_selection_search_set(),
buff=SMALL_BUFF buff=SMALL_BUFF
) )
if mob is not None: if mob is not None:
self.add_to_selection(mob) 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 # Search through all mobject on the screen, not just the palette
to_search = [ to_search = [
sm sm
@ -566,9 +587,9 @@ class InteractiveScene(Scene):
self.selection.set_color(mob.get_color()) self.selection.set_color(mob.get_color())
self.remove(self.color_palette) 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) 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: if self.is_grabbing:
self.handle_grabbing(point) self.handle_grabbing(point)
elif self.window.is_key_pressed(ord(RESIZE_KEY)): 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): elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
self.handle_sweeping_selection(point) 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) super().on_mouse_release(point, button, mods)
if self.color_palette in self.mobjects: if self.color_palette in self.mobjects:
self.choose_color(point) 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: else:
self.clear_selection() 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.animation import prepare_animation
from manimlib.animation.fading import VFadeInThenOut from manimlib.animation.fading import VFadeInThenOut
from manimlib.camera.camera import Camera from manimlib.camera.camera import Camera
from manimlib.camera.camera_frame import CameraFrame
from manimlib.config import get_module from manimlib.config import get_module
from manimlib.constants import ARROW_SYMBOLS from manimlib.constants import ARROW_SYMBOLS
from manimlib.constants import DEFAULT_WAIT_TIME from manimlib.constants import DEFAULT_WAIT_TIME
@ -44,6 +45,7 @@ from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Callable, Iterable from typing import Callable, Iterable
from manimlib.typing import Vect3
from PIL.Image import Image from PIL.Image import Image
@ -52,19 +54,22 @@ if TYPE_CHECKING:
PAN_3D_KEY = 'd' PAN_3D_KEY = 'd'
FRAME_SHIFT_KEY = 'f' FRAME_SHIFT_KEY = 'f'
ZOOM_KEY = 'z'
RESET_FRAME_KEY = 'r' RESET_FRAME_KEY = 'r'
QUIT_KEY = 'q' QUIT_KEY = 'q'
class Scene(object): class Scene(object):
random_seed: int = 0 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 max_num_saved_states: int = 50
default_camera_config: dict = dict() default_camera_config: dict = dict()
default_window_config: dict = dict() default_window_config: dict = dict()
default_file_writer_config: dict = dict() default_file_writer_config: dict = dict()
samples = 0 samples = 0
# Euler angles, in degrees
default_frame_orientation = (0, 0)
def __init__( def __init__(
self, self,
@ -110,6 +115,10 @@ class Scene(object):
# Core state of the scene # Core state of the scene
self.camera: Camera = Camera(**self.camera_config) 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.file_writer = SceneFileWriter(self, **self.file_writer_config)
self.mobjects: list[Mobject] = [self.camera.frame] self.mobjects: list[Mobject] = [self.camera.frame]
self.render_groups: list[Mobject] = [] self.render_groups: list[Mobject] = []
@ -828,9 +837,10 @@ class Scene(object):
def on_mouse_motion( def on_mouse_motion(
self, self,
point: np.ndarray, point: Vect3,
d_point: np.ndarray d_point: Vect3
) -> None: ) -> None:
assert(self.window is not None)
self.mouse_point.move_to(point) self.mouse_point.move_to(point)
event_data = {"point": point, "d_point": d_point} event_data = {"point": point, "d_point": d_point}
@ -841,25 +851,24 @@ class Scene(object):
frame = self.camera.frame frame = self.camera.frame
# Handle perspective changes # Handle perspective changes
if self.window.is_key_pressed(ord(PAN_3D_KEY)): if self.window.is_key_pressed(ord(PAN_3D_KEY)):
frame.increment_theta(-self.pan_sensitivity * d_point[0]) ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
frame.increment_phi(self.pan_sensitivity * d_point[1]) ff_d_point *= self.pan_sensitivity
frame.increment_theta(-ff_d_point[0])
frame.increment_phi(ff_d_point[1])
# Handle frame movements # Handle frame movements
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)): elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
shift = -d_point frame.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)
def on_mouse_drag( def on_mouse_drag(
self, self,
point: np.ndarray, point: Vect3,
d_point: np.ndarray, d_point: Vect3,
buttons: int, buttons: int,
modifiers: int modifiers: int
) -> None: ) -> None:
self.mouse_drag_point.move_to(point) 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} event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data) propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
@ -868,7 +877,7 @@ class Scene(object):
def on_mouse_press( def on_mouse_press(
self, self,
point: np.ndarray, point: Vect3,
button: int, button: int,
mods: int mods: int
) -> None: ) -> None:
@ -880,7 +889,7 @@ class Scene(object):
def on_mouse_release( def on_mouse_release(
self, self,
point: np.ndarray, point: Vect3,
button: int, button: int,
mods: int mods: int
) -> None: ) -> None:
@ -891,22 +900,21 @@ class Scene(object):
def on_mouse_scroll( def on_mouse_scroll(
self, self,
point: np.ndarray, point: Vect3,
offset: np.ndarray offset: Vect3,
x_pixel_offset: float,
y_pixel_offset: float
) -> None: ) -> None:
event_data = {"point": point, "offset": offset} event_data = {"point": point, "offset": offset}
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data) propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
if propagate_event is not None and propagate_event is False: if propagate_event is not None and propagate_event is False:
return return
frame = self.camera.frame rel_offset = y_pixel_offset / self.camera.get_pixel_height()
if self.window.is_key_pressed(ord(ZOOM_KEY)): self.frame.scale(
factor = 1 + np.arctan(10 * offset[1]) 1 - self.scroll_sensitivity * rel_offset,
frame.scale(1 / factor, about_point=point) about_point=point
else: )
transform = frame.get_inverse_camera_rotation_matrix()
shift = np.dot(np.transpose(transform), offset)
frame.shift(-20.0 * shift)
def on_key_release( def on_key_release(
self, self,
@ -1006,3 +1014,14 @@ class SceneState():
class EndScene(Exception): class EndScene(Exception):
pass 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): def generate_vao(self, refresh: bool = True):
self.release() self.release()
# Data buffer # Data buffer
vbo = self.vbo = self.get_vertex_buffer_object(refresh) vbo = self.get_vertex_buffer_object(refresh)
ibo = self.ibo = self.get_index_buffer_object(refresh) ibo = self.get_index_buffer_object(refresh)
# Vertex array object # Vertex array object
self.vao = self.ctx.vertex_array( self.vao = self.ctx.vertex_array(
@ -280,12 +280,10 @@ class FillShaderWrapper(ShaderWrapper):
self.fill_canvas = get_fill_canvas(self.ctx) self.fill_canvas = get_fill_canvas(self.ctx)
def render(self): def render(self):
vao = self.vao
assert(vao is not None)
winding = (len(self.vert_indices) == 0) winding = (len(self.vert_indices) == 0)
vao.program['winding'].value = winding self.program['winding'].value = winding
if not winding: if not winding:
vao.render(moderngl.TRIANGLES) super().render()
return return
original_fbo = self.ctx.fbo original_fbo = self.ctx.fbo
@ -301,14 +299,13 @@ class FillShaderWrapper(ShaderWrapper):
gl.GL_ONE, gl.GL_ONE, gl.GL_ONE, gl.GL_ONE,
) )
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX) 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() original_fbo.use()
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
gl.glBlendEquation(gl.GL_FUNC_ADD) 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) 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; out float v_opacity;
// Analog of import for manim only // Analog of import for manim only
#INSERT get_gl_Position.glsl #INSERT emit_gl_Position.glsl
void main(){ void main(){
v_im_coords = im_coords; v_im_coords = im_coords;
v_opacity = opacity; 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_camera = normalize(camera_position - point);
vec3 to_light = normalize(light_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); float light_to_normal = dot(to_light, unit_normal);
// When unit normal points towards light, brighten // When unit normal points towards light, brighten
float bright_factor = max(light_to_normal, 0) * reflectiveness; float bright_factor = max(light_to_normal, 0) * reflectiveness;
// For glossy surface, add extra shine if light beam go towards camera // 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 light_to_cam = dot(light_reflection, to_camera);
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2)); float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
bright_factor += shine; bright_factor += shine;

View file

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

View file

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

View file

@ -6,9 +6,9 @@ out vec3 xyz_coords;
uniform float scale_factor; uniform float scale_factor;
uniform vec3 offset; uniform vec3 offset;
#INSERT get_gl_Position.glsl #INSERT emit_gl_Position.glsl
void main(){ void main(){
xyz_coords = (point - offset) / scale_factor; 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 black_for_cycles;
uniform float is_parameter_space; uniform float is_parameter_space;
uniform vec2 frame_shape;
in vec3 xyz_coords; in vec3 xyz_coords;
out vec4 frag_color; out vec4 frag_color;

View file

@ -6,9 +6,9 @@ out vec3 xyz_coords;
uniform float scale_factor; uniform float scale_factor;
uniform vec3 offset; uniform vec3 offset;
#INSERT get_gl_Position.glsl #INSERT emit_gl_Position.glsl
void main(){ void main(){
xyz_coords = (point - offset) / scale_factor; 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 // 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]){ 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]; uv_coords = SIMPLE_QUADRATIC[i];
color = v_color[i]; color = v_color[i];
point = points[i]; point = points[i];
gl_Position = get_gl_Position(points[i]); emit_gl_Position(points[i]);
EmitVertex(); EmitVertex();
} }
EndPrimitive(); EndPrimitive();
@ -57,10 +57,6 @@ void emit_simple_triangle(){
void main(){ 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 // Curves are marked as ended when the handle after
// the first anchor is set equal to that anchor // the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return; if (verts[0] == verts[1]) return;

View file

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

View file

@ -13,7 +13,6 @@ in vec3 verts[3];
in vec4 v_joint_product[3]; in vec4 v_joint_product[3];
in float v_stroke_width[3]; in float v_stroke_width[3];
in vec4 v_color[3]; in vec4 v_color[3];
in float v_vert_index[3];
out vec4 color; out vec4 color;
out float uv_stroke_width; 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); 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 get_xyz_to_uv.glsl
#INSERT finalize_color.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; float buff = 0.5 * v_stroke_width[index] + aaw;
// Add correction for sharp angles to prevent weird bevel effects // 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); vec3 normal = get_joint_unit_normal(joint_product);
// Set global unit normal // Set global unit normal
unit_normal = normal; unit_normal = normal;
@ -154,10 +153,6 @@ void get_corners(
} }
void main() { 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 // Curves are marked as ended when the handle after
// the first anchor is set equal to that anchor // the first anchor is set equal to that anchor
if (verts[0] == verts[1]) return; if (verts[0] == verts[1]) return;
@ -207,7 +202,7 @@ void main() {
} }
color = finalize_color(v_color[i / 2], corners[i], unit_normal); color = finalize_color(v_color[i / 2], corners[i], unit_normal);
gl_Position = get_gl_Position(corners[i]); emit_gl_Position(corners[i]);
EmitVertex(); EmitVertex();
} }
EndPrimitive(); EndPrimitive();

View file

@ -1,6 +1,7 @@
#version 330 #version 330
uniform vec2 frame_shape; uniform float frame_scale;
uniform float is_fixed_in_frame;
in vec3 point; in vec3 point;
in vec4 stroke_rgba; in vec4 stroke_rgba;
@ -14,14 +15,15 @@ out vec3 verts;
out vec4 v_joint_product; out vec4 v_joint_product;
out float v_stroke_width; out float v_stroke_width;
out vec4 v_color; out vec4 v_color;
out float v_vert_index;
const float STROKE_WIDTH_CONVERSION = 0.01; const float STROKE_WIDTH_CONVERSION = 0.01;
void main(){ void main(){
verts = point; 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_joint_product = joint_product;
v_color = stroke_rgba; v_color = stroke_rgba;
v_vert_index = gl_VertexID;
} }

View file

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

View file

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

View file

@ -1,8 +1,7 @@
#version 330 #version 330
in vec3 point; in vec3 point;
in vec3 du_point; in vec3 normal;
in vec3 dv_point;
in vec2 im_coords; in vec2 im_coords;
in float opacity; in float opacity;
@ -11,13 +10,13 @@ out vec3 v_normal;
out vec2 v_im_coords; out vec2 v_im_coords;
out float v_opacity; out float v_opacity;
#INSERT get_gl_Position.glsl #INSERT emit_gl_Position.glsl
#INSERT get_unit_normal.glsl #INSERT get_unit_normal.glsl
void main(){ void main(){
v_point = point; v_point = point;
v_normal = get_unit_normal(point, du_point, dv_point); v_normal = normal;
v_im_coords = im_coords; v_im_coords = im_coords;
v_opacity = opacity; 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 v_point;
out vec3 light_pos; out vec3 light_pos;
#INSERT get_gl_Position.glsl #INSERT emit_gl_Position.glsl
void main(){ void main(){
v_point = point; v_point = point;
color = rgba; color = rgba;
scaled_aaw = (anti_alias_width * pixel_size) / radius; 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 z = -10 * gl_Position.z;
float scaled_radius = radius * 1.0 / (1.0 - z); float scaled_radius = radius * 1.0 / (1.0 - z);
gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width); gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width);

View file

@ -1,4 +1,5 @@
import itertools as it import itertools as it
import numpy as np
def merge_dicts_recursively(*dicts): def merge_dicts_recursively(*dicts):
@ -29,3 +30,19 @@ def soft_dict_update(d1, d2):
for key, value in list(d2.items()): for key, value in list(d2.items()):
if key not in d1: if key not in d1:
d1[key] = value 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 from PIL import Image
import numpy as np import numpy as np
from manimlib.constants import DEFAULT_PIXEL_HEIGHT from manimlib.config import parse_cli
from manimlib.constants import DEFAULT_PIXEL_WIDTH from manimlib.config import get_configuration
from manimlib.utils.customization import get_customization from manimlib.utils.customization import get_customization
from manimlib.utils.directories import get_shader_dir from manimlib.utils.directories import get_shader_dir
from manimlib.utils.file_ops import find_file from manimlib.utils.file_ops import find_file
@ -103,7 +103,7 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
@lru_cache() @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 Because VMobjects with fill are rendered in a funny way, using
alpha blending to effectively compute the winding number around 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, which can display that texture as a simple quad onto a screen,
along with the rgb value which is meant to be discarded. along with the rgb value which is meant to be discarded.
""" """
cam_config = get_customization()['camera_resolutions'] cam_config = get_configuration(parse_cli())['camera_config']
res_name = cam_config['default_resolution'] size = (cam_config['pixel_width'], cam_config['pixel_height'])
size = tuple(map(int, cam_config[res_name].split("x")))
# Important to make sure dtype is floating point (not fixed point) # Important to make sure dtype is floating point (not fixed point)
# so that alpha values can be negative and are not clipped # so that alpha values can be negative and are not clipped
texture = ctx.texture(size=size, components=4, dtype='f2') texture = ctx.texture(size=size, components=4, dtype='f2')
depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used depth_texture = ctx.depth_texture(size=size)
texture_fbo = ctx.framebuffer(texture, depth_buffer) texture_fbo = ctx.framebuffer(texture, depth_texture)
# We'll paint onto a canvas with initially negative rgbs, and # We'll paint onto a canvas with initially negative rgbs, and
# discard any pixels remaining close to this value. This is # 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 #version 330
uniform sampler2D Texture; uniform sampler2D Texture;
uniform sampler2D DepthTexture;
uniform vec3 null_rgb; uniform vec3 null_rgb;
in vec2 v_textcoord; in vec2 v_textcoord;
@ -159,17 +159,21 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
void main() { void main() {
color = texture(Texture, v_textcoord); color = texture(Texture, v_textcoord);
if(color.a == 0) discard;
if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard; if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
// Un-blend from the null value // Un-blend from the null value
color.rgb -= (1 - color.a) * null_rgb; 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['Texture'].value = get_texture_id(texture)
simple_program['DepthTexture'].value = get_texture_id(depth_texture)
simple_program['null_rgb'].value = null_rgb simple_program['null_rgb'].value = null_rgb
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]]) 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, simple_program,
ctx.buffer(verts.astype('f4').tobytes()), ctx.buffer(verts.astype('f4').tobytes()),
'texcoord', 'texcoord',
mode=moderngl.TRIANGLE_STRIP
) )
return (texture_fbo, fill_texture_vao, null_rgb) 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. # \begin{array}{cc}, etc.
pattern = "|".join( pattern = "|".join(
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
for s in ["begin", "end", "phantom"] for s in ["begin", "end", "phantom", "text"]
) )
tex = re.sub(pattern, "", tex) 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 moderngl_window.timers.clock import Timer
from screeninfo import get_monitors from screeninfo import get_monitors
from manimlib.constants import FRAME_SHAPE
from manimlib.utils.customization import get_customization from manimlib.utils.customization import get_customization
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -77,17 +78,18 @@ class Window(PygletWindow):
py: int, py: int,
relative: bool = False relative: bool = False
) -> np.ndarray: ) -> np.ndarray:
pw, ph = self.size if not hasattr(self.scene, "frame"):
fw, fh = self.scene.camera.get_frame_shape() return np.zeros(3)
fc = self.scene.camera.get_frame_center()
if relative: pixel_shape = np.array(self.size)
return np.array([px / pw, py / ph, 0]) fixed_frame_shape = np.array(FRAME_SHAPE)
else: frame = self.scene.frame
return np.array([
fc[0] + px * fw / pw - fw / 2, coords = np.zeros(3)
fc[1] + py * fh / ph - fh / 2, coords[:2] = (fixed_frame_shape / pixel_shape) * np.array([px, py])
0 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: def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
super().on_mouse_motion(x, y, dx, dy) 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) super().on_mouse_scroll(x, y, x_offset, y_offset)
point = self.pixel_coords_to_space_coords(x, y) point = self.pixel_coords_to_space_coords(x, y)
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True) 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: def on_key_press(self, symbol: int, modifiers: int) -> None:
self.pressed_keys.add(symbol) # Modifiers? self.pressed_keys.add(symbol) # Modifiers?

View file

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