mirror of
https://github.com/3b1b/manim.git
synced 2025-04-13 09:47:07 +00:00
Merge branch 'video-work' into render-groups
This commit is contained in:
commit
6eafdc63cc
51 changed files with 847 additions and 834 deletions
|
@ -31,7 +31,7 @@ class OpeningManimExample(Scene):
|
|||
)
|
||||
linear_transform_words.arrange(RIGHT)
|
||||
linear_transform_words.to_edge(UP)
|
||||
linear_transform_words.set_stroke(BLACK, 10, background=True)
|
||||
linear_transform_words.set_backstroke(width=5)
|
||||
|
||||
self.play(
|
||||
ShowCreation(grid),
|
||||
|
@ -52,7 +52,7 @@ class OpeningManimExample(Scene):
|
|||
this is the map $z \\rightarrow z^2$
|
||||
""")
|
||||
complex_map_words.to_corner(UR)
|
||||
complex_map_words.set_stroke(BLACK, 5, background=True)
|
||||
complex_map_words.set_backstroke(width=5)
|
||||
|
||||
self.play(
|
||||
FadeOut(grid),
|
||||
|
@ -268,16 +268,8 @@ class UpdatersExample(Scene):
|
|||
# that of the newly constructed object
|
||||
brace = always_redraw(Brace, square, UP)
|
||||
|
||||
text, number = label = VGroup(
|
||||
Text("Width = "),
|
||||
DecimalNumber(
|
||||
0,
|
||||
show_ellipsis=True,
|
||||
num_decimal_places=2,
|
||||
include_sign=True,
|
||||
)
|
||||
)
|
||||
label.arrange(RIGHT)
|
||||
label = TexText("Width = 0.00")
|
||||
number = label.make_number_changable("0.00")
|
||||
|
||||
# This ensures that the method deicmal.next_to(square)
|
||||
# is called on every frame
|
||||
|
@ -554,9 +546,7 @@ class TexAndNumbersExample(Scene):
|
|||
)
|
||||
|
||||
|
||||
class SurfaceExample(Scene):
|
||||
samples = 4
|
||||
|
||||
class SurfaceExample(ThreeDScene):
|
||||
def construct(self):
|
||||
surface_text = Text("For 3d scenes, try using surfaces")
|
||||
surface_text.fix_in_frame()
|
||||
|
@ -588,13 +578,6 @@ class SurfaceExample(Scene):
|
|||
mob.mesh = SurfaceMesh(mob)
|
||||
mob.mesh.set_stroke(BLUE, 1, opacity=0.5)
|
||||
|
||||
# Set perspective
|
||||
frame = self.camera.frame
|
||||
frame.set_euler_angles(
|
||||
theta=-30 * DEGREES,
|
||||
phi=70 * DEGREES,
|
||||
)
|
||||
|
||||
surface = surfaces[0]
|
||||
|
||||
self.play(
|
||||
|
@ -616,12 +599,12 @@ class SurfaceExample(Scene):
|
|||
self.play(
|
||||
Transform(surface, surfaces[2]),
|
||||
# Move camera frame during the transition
|
||||
frame.animate.increment_phi(-10 * DEGREES),
|
||||
frame.animate.increment_theta(-20 * DEGREES),
|
||||
self.frame.animate.increment_phi(-10 * DEGREES),
|
||||
self.frame.animate.increment_theta(-20 * DEGREES),
|
||||
run_time=3
|
||||
)
|
||||
# Add ambient rotation
|
||||
frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
|
||||
self.frame.add_updater(lambda m, dt: m.increment_theta(-0.1 * dt))
|
||||
|
||||
# Play around with where the light is
|
||||
light_text = Text("You can move around the light source")
|
||||
|
@ -690,6 +673,8 @@ class InteractiveDevelopment(Scene):
|
|||
|
||||
|
||||
class ControlsExample(Scene):
|
||||
drag_to_pan = False
|
||||
|
||||
def setup(self):
|
||||
self.textbox = Textbox()
|
||||
self.checkbox = Checkbox()
|
||||
|
|
|
@ -98,11 +98,7 @@ class DrawBorderThenFill(Animation):
|
|||
self.mobject = vmobject
|
||||
|
||||
def begin(self) -> None:
|
||||
# Trigger triangulation calculation
|
||||
for submob in self.mobject.get_family():
|
||||
if not submob._use_winding_fill:
|
||||
submob.get_triangulation()
|
||||
|
||||
self.mobject.set_animating_status(True)
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
self.mobject.match_style(self.outline)
|
||||
|
@ -136,7 +132,6 @@ class DrawBorderThenFill(Animation):
|
|||
if index == 1 and self.sm_to_index[hash(submob)] == 0:
|
||||
# First time crossing over
|
||||
submob.set_data(outline.data)
|
||||
submob.needs_new_triangulation = False
|
||||
self.sm_to_index[hash(submob)] = 1
|
||||
|
||||
if index == 0:
|
||||
|
|
|
@ -70,7 +70,7 @@ class Transform(Animation):
|
|||
def finish(self) -> None:
|
||||
super().finish()
|
||||
self.mobject.unlock_data()
|
||||
if self.target_mobject is not None:
|
||||
if self.target_mobject is not None and self.rate_func(1) == 1:
|
||||
self.mobject.become(self.target_mobject)
|
||||
|
||||
def create_target(self) -> Mobject:
|
||||
|
|
|
@ -131,9 +131,10 @@ class TransformMatchingStrings(TransformMatchingParts):
|
|||
target: StringMobject,
|
||||
matched_keys: Iterable[str] = [],
|
||||
key_map: dict[str, str] = dict(),
|
||||
matched_pairs: Iterable[tuple[Mobject, Mobject]] = [],
|
||||
**kwargs,
|
||||
):
|
||||
matched_pairs = [
|
||||
matched_pairs = list(matched_pairs) + [
|
||||
*[(source[key], target[key]) for key in matched_keys],
|
||||
*[(source[key1], target[key2]) for key1, key2 in key_map.items()],
|
||||
*[
|
||||
|
|
|
@ -72,9 +72,9 @@ class Camera(object):
|
|||
|
||||
def init_context(self) -> None:
|
||||
if self.window is None:
|
||||
self.ctx = moderngl.create_standalone_context()
|
||||
self.ctx: moderngl.Context = moderngl.create_standalone_context()
|
||||
else:
|
||||
self.ctx = self.window.ctx
|
||||
self.ctx: moderngl.Context = self.window.ctx
|
||||
|
||||
self.ctx.enable(moderngl.PROGRAM_POINT_SIZE)
|
||||
self.ctx.enable(moderngl.BLEND)
|
||||
|
@ -125,16 +125,20 @@ class Camera(object):
|
|||
def clear(self) -> None:
|
||||
self.fbo.clear(*self.background_rgba)
|
||||
|
||||
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
|
||||
# Copy blocks from fbo into draw_fbo using Blit
|
||||
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, self.fbo.glo)
|
||||
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, self.draw_fbo.glo)
|
||||
src_viewport = self.fbo.viewport
|
||||
def blit(self, src_fbo, dst_fbo):
|
||||
"""
|
||||
Copy blocks between fbo's using Blit
|
||||
"""
|
||||
gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo)
|
||||
gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo)
|
||||
gl.glBlitFramebuffer(
|
||||
*src_viewport,
|
||||
*self.draw_fbo.viewport,
|
||||
*src_fbo.viewport,
|
||||
*dst_fbo.viewport,
|
||||
gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR
|
||||
)
|
||||
|
||||
def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes:
|
||||
self.blit(self.fbo, self.draw_fbo)
|
||||
return self.draw_fbo.read(
|
||||
viewport=self.draw_fbo.viewport,
|
||||
components=self.n_channels,
|
||||
|
@ -169,10 +173,10 @@ class Camera(object):
|
|||
|
||||
# Getting camera attributes
|
||||
def get_pixel_size(self) -> float:
|
||||
return self.frame.get_shape()[0] / self.get_pixel_shape()[0]
|
||||
return self.frame.get_width() / self.get_pixel_shape()[0]
|
||||
|
||||
def get_pixel_shape(self) -> tuple[int, int]:
|
||||
return self.draw_fbo.size
|
||||
return self.fbo.size
|
||||
|
||||
def get_pixel_width(self) -> int:
|
||||
return self.get_pixel_shape()[0]
|
||||
|
@ -223,6 +227,8 @@ class Camera(object):
|
|||
self.fbo.use()
|
||||
for mobject in mobjects:
|
||||
mobject.render(self.ctx, self.uniforms)
|
||||
if self.window is not None and self.fbo is not self.window_fbo:
|
||||
self.blit(self.fbo, self.window_fbo)
|
||||
|
||||
def refresh_uniforms(self) -> None:
|
||||
frame = self.frame
|
||||
|
@ -231,12 +237,12 @@ class Camera(object):
|
|||
cam_pos = self.frame.get_implied_camera_location()
|
||||
|
||||
self.uniforms.update(
|
||||
frame_shape=frame.get_shape(),
|
||||
pixel_size=self.get_pixel_size(),
|
||||
view=tuple(view_matrix.T.flatten()),
|
||||
focal_distance=frame.get_focal_distance() / frame.get_scale(),
|
||||
frame_scale=frame.get_scale(),
|
||||
pixel_size=self.get_pixel_size(),
|
||||
camera_position=tuple(cam_pos),
|
||||
light_position=tuple(light_pos),
|
||||
focal_distance=frame.get_focal_distance(),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -4,9 +4,10 @@ import math
|
|||
|
||||
import numpy as np
|
||||
from scipy.spatial.transform import Rotation
|
||||
from pyrr import Matrix44
|
||||
|
||||
from manimlib.constants import DEGREES, RADIANS
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.space_ops import normalize
|
||||
|
@ -20,28 +21,30 @@ if TYPE_CHECKING:
|
|||
class CameraFrame(Mobject):
|
||||
def __init__(
|
||||
self,
|
||||
frame_shape: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT),
|
||||
frame_shape: tuple[float, float] = FRAME_SHAPE,
|
||||
center_point: Vect3 = ORIGIN,
|
||||
focal_dist_to_height: float = 2.0,
|
||||
# Field of view in the y direction
|
||||
fovy: float = 45 * DEGREES,
|
||||
**kwargs,
|
||||
):
|
||||
self.frame_shape = frame_shape
|
||||
self.center_point = center_point
|
||||
self.focal_dist_to_height = focal_dist_to_height
|
||||
self.view_matrix = np.identity(4)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_uniforms(self) -> None:
|
||||
super().init_uniforms()
|
||||
# As a quaternion
|
||||
self.uniforms["orientation"] = Rotation.identity().as_quat()
|
||||
self.uniforms["focal_dist_to_height"] = self.focal_dist_to_height
|
||||
self.uniforms["fovy"] = fovy
|
||||
|
||||
def init_points(self) -> None:
|
||||
self.set_points([ORIGIN, LEFT, RIGHT, DOWN, UP])
|
||||
self.set_width(self.frame_shape[0], stretch=True)
|
||||
self.set_height(self.frame_shape[1], stretch=True)
|
||||
self.move_to(self.center_point)
|
||||
self.default_orientation = Rotation.identity()
|
||||
self.view_matrix = np.identity(4)
|
||||
self.camera_location = OUT # This will be updated by set_points
|
||||
|
||||
self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP]))
|
||||
self.set_width(frame_shape[0], stretch=True)
|
||||
self.set_height(frame_shape[1], stretch=True)
|
||||
self.move_to(center_point)
|
||||
|
||||
def note_changed_data(self, recurse_up: bool = True):
|
||||
super().note_changed_data(recurse_up)
|
||||
self.get_view_matrix(refresh=True)
|
||||
self.get_implied_camera_location(refresh=True)
|
||||
|
||||
def set_orientation(self, rotation: Rotation):
|
||||
self.uniforms["orientation"][:] = rotation.as_quat()
|
||||
|
@ -50,15 +53,21 @@ class CameraFrame(Mobject):
|
|||
def get_orientation(self):
|
||||
return Rotation.from_quat(self.uniforms["orientation"])
|
||||
|
||||
def to_default_state(self):
|
||||
self.center()
|
||||
self.set_height(FRAME_HEIGHT)
|
||||
self.set_width(FRAME_WIDTH)
|
||||
self.set_orientation(Rotation.identity())
|
||||
def make_orientation_default(self):
|
||||
self.default_orientation = self.get_orientation()
|
||||
return self
|
||||
|
||||
def get_euler_angles(self):
|
||||
return self.get_orientation().as_euler("zxz")[::-1]
|
||||
def to_default_state(self):
|
||||
self.set_shape(*FRAME_SHAPE)
|
||||
self.center()
|
||||
self.set_orientation(self.default_orientation)
|
||||
return self
|
||||
|
||||
def get_euler_angles(self) -> np.ndarray:
|
||||
orientation = self.get_orientation()
|
||||
if all(orientation.as_quat() == [0, 0, 0, 1]):
|
||||
return np.zeros(3)
|
||||
return orientation.as_euler("zxz")[::-1]
|
||||
|
||||
def get_theta(self):
|
||||
return self.get_euler_angles()[0]
|
||||
|
@ -69,22 +78,40 @@ class CameraFrame(Mobject):
|
|||
def get_gamma(self):
|
||||
return self.get_euler_angles()[2]
|
||||
|
||||
def get_scale(self):
|
||||
return self.get_height() / FRAME_SHAPE[1]
|
||||
|
||||
def get_inverse_camera_rotation_matrix(self):
|
||||
return self.get_orientation().as_matrix().T
|
||||
|
||||
def get_view_matrix(self):
|
||||
def get_view_matrix(self, refresh=False):
|
||||
"""
|
||||
Returns a 4x4 for the affine transformation mapping a point
|
||||
into the camera's internal coordinate system
|
||||
"""
|
||||
result = self.view_matrix
|
||||
result[:] = np.identity(4)
|
||||
result[:3, 3] = -self.get_center()
|
||||
rotation = np.identity(4)
|
||||
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
|
||||
result[:] = np.dot(rotation, result)
|
||||
return result
|
||||
if refresh:
|
||||
shift = np.identity(4)
|
||||
rotation = np.identity(4)
|
||||
scale_mat = np.identity(4)
|
||||
|
||||
shift[:3, 3] = -self.get_center()
|
||||
rotation[:3, :3] = self.get_inverse_camera_rotation_matrix()
|
||||
scale = self.get_scale()
|
||||
if scale > 0:
|
||||
scale_mat[:3, :3] /= self.get_scale()
|
||||
|
||||
self.view_matrix = np.dot(scale_mat, np.dot(rotation, shift))
|
||||
|
||||
return self.view_matrix
|
||||
|
||||
def get_inv_view_matrix(self):
|
||||
return np.linalg.inv(self.get_view_matrix())
|
||||
|
||||
@Mobject.affects_data
|
||||
def interpolate(self, *args, **kwargs):
|
||||
super().interpolate(*args, **kwargs)
|
||||
|
||||
@Mobject.affects_data
|
||||
def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs):
|
||||
rot = Rotation.from_rotvec(angle * normalize(axis))
|
||||
self.set_orientation(rot * self.get_orientation())
|
||||
|
@ -101,7 +128,11 @@ class CameraFrame(Mobject):
|
|||
for i, var in enumerate([theta, phi, gamma]):
|
||||
if var is not None:
|
||||
eulers[i] = var * units
|
||||
self.set_orientation(Rotation.from_euler("zxz", eulers[::-1]))
|
||||
if all(eulers == 0):
|
||||
rot = Rotation.identity()
|
||||
else:
|
||||
rot = Rotation.from_euler("zxz", eulers[::-1])
|
||||
self.set_orientation(rot)
|
||||
return self
|
||||
|
||||
def reorient(
|
||||
|
@ -139,16 +170,20 @@ class CameraFrame(Mobject):
|
|||
return self
|
||||
|
||||
def set_focal_distance(self, focal_distance: float):
|
||||
self.uniforms["focal_dist_to_height"] = focal_distance / self.get_height()
|
||||
self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance)
|
||||
return self
|
||||
|
||||
def set_field_of_view(self, field_of_view: float):
|
||||
self.uniforms["focal_dist_to_height"] = 2 * math.tan(field_of_view / 2)
|
||||
self.uniforms["fovy"] = field_of_view
|
||||
return self
|
||||
|
||||
def get_shape(self):
|
||||
return (self.get_width(), self.get_height())
|
||||
|
||||
def get_aspect_ratio(self):
|
||||
width, height = self.get_shape()
|
||||
return width / height
|
||||
|
||||
def get_center(self) -> np.ndarray:
|
||||
# Assumes first point is at the center
|
||||
return self.get_points()[0]
|
||||
|
@ -162,12 +197,24 @@ class CameraFrame(Mobject):
|
|||
return points[4, 1] - points[3, 1]
|
||||
|
||||
def get_focal_distance(self) -> float:
|
||||
return self.uniforms["focal_dist_to_height"] * self.get_height()
|
||||
return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"])
|
||||
|
||||
def get_field_of_view(self) -> float:
|
||||
return 2 * math.atan(self.uniforms["focal_dist_to_height"] / 2)
|
||||
return self.uniforms["fovy"]
|
||||
|
||||
def get_implied_camera_location(self) -> np.ndarray:
|
||||
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
||||
dist = self.get_focal_distance()
|
||||
return self.get_center() + dist * to_camera
|
||||
def get_implied_camera_location(self, refresh=False) -> np.ndarray:
|
||||
if refresh:
|
||||
to_camera = self.get_inverse_camera_rotation_matrix()[2]
|
||||
dist = self.get_focal_distance()
|
||||
self.camera_location = self.get_center() + dist * to_camera
|
||||
return self.camera_location
|
||||
|
||||
def to_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
||||
view = self.get_view_matrix()
|
||||
point4d = [*point, 0 if relative else 1]
|
||||
return np.dot(point4d, view.T)[:3]
|
||||
|
||||
def from_fixed_frame_point(self, point: Vect3, relative: bool = False):
|
||||
inv_view = self.get_inv_view_matrix()
|
||||
point4d = [*point, 0 if relative else 1]
|
||||
return np.dot(point4d, inv_view.T)[:3]
|
||||
|
|
|
@ -11,6 +11,7 @@ if TYPE_CHECKING:
|
|||
ASPECT_RATIO: float = 16.0 / 9.0
|
||||
FRAME_HEIGHT: float = 8.0
|
||||
FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO
|
||||
FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT)
|
||||
FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2
|
||||
FRAME_X_RADIUS: float = FRAME_WIDTH / 2
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ from __future__ import annotations
|
|||
|
||||
import numpy as np
|
||||
|
||||
from manimlib.event_handler.event_listner import EventListner
|
||||
from manimlib.event_handler.event_listner import EventListener
|
||||
from manimlib.event_handler.event_type import EventType
|
||||
|
||||
|
||||
class EventDispatcher(object):
|
||||
def __init__(self):
|
||||
self.event_listners: dict[
|
||||
EventType, list[EventListner]
|
||||
EventType, list[EventListener]
|
||||
] = {
|
||||
event_type: []
|
||||
for event_type in EventType
|
||||
|
@ -17,15 +17,15 @@ class EventDispatcher(object):
|
|||
self.mouse_point = np.array((0., 0., 0.))
|
||||
self.mouse_drag_point = np.array((0., 0., 0.))
|
||||
self.pressed_keys: set[int] = set()
|
||||
self.draggable_object_listners: list[EventListner] = []
|
||||
self.draggable_object_listners: list[EventListener] = []
|
||||
|
||||
def add_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
def add_listner(self, event_listner: EventListener):
|
||||
assert(isinstance(event_listner, EventListener))
|
||||
self.event_listners[event_listner.event_type].append(event_listner)
|
||||
return self
|
||||
|
||||
def remove_listner(self, event_listner: EventListner):
|
||||
assert(isinstance(event_listner, EventListner))
|
||||
def remove_listner(self, event_listner: EventListener):
|
||||
assert(isinstance(event_listner, EventListener))
|
||||
try:
|
||||
while event_listner in self.event_listners[event_listner.event_type]:
|
||||
self.event_listners[event_listner.event_type].remove(event_listner)
|
||||
|
@ -56,7 +56,7 @@ class EventDispatcher(object):
|
|||
|
||||
if event_type == EventType.MouseDragEvent:
|
||||
for listner in self.draggable_object_listners:
|
||||
assert(isinstance(listner, EventListner))
|
||||
assert(isinstance(listner, EventListener))
|
||||
propagate_event = listner.callback(listner.mobject, event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return propagate_event
|
||||
|
|
|
@ -9,7 +9,7 @@ if TYPE_CHECKING:
|
|||
from manimlib.mobject.mobject import Mobject
|
||||
|
||||
|
||||
class EventListner(object):
|
||||
class EventListener(object):
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
|
|
|
@ -11,7 +11,7 @@ from manimlib.utils.rate_functions import smooth
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, List, Iterable
|
||||
from typing import Callable, List, Iterable, Self
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
|
@ -49,7 +49,7 @@ class AnimatedBoundary(VGroup):
|
|||
lambda m, dt: self.update_boundary_copies(dt)
|
||||
)
|
||||
|
||||
def update_boundary_copies(self, dt: float) -> None:
|
||||
def update_boundary_copies(self, dt: float) -> Self:
|
||||
# Not actual time, but something which passes at
|
||||
# an altered rate to make the implementation below
|
||||
# cleaner
|
||||
|
@ -79,6 +79,7 @@ class AnimatedBoundary(VGroup):
|
|||
)
|
||||
|
||||
self.total_time += dt
|
||||
return self
|
||||
|
||||
def full_family_become_partial(
|
||||
self,
|
||||
|
@ -86,7 +87,7 @@ class AnimatedBoundary(VGroup):
|
|||
mob2: VMobject,
|
||||
a: float,
|
||||
b: float
|
||||
):
|
||||
) -> Self:
|
||||
family1 = mob1.family_members_with_points()
|
||||
family2 = mob2.family_members_with_points()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
|
@ -118,7 +119,7 @@ class TracedPath(VMobject):
|
|||
self.traced_points: list[np.ndarray] = []
|
||||
self.add_updater(lambda m, dt: m.update_path(dt))
|
||||
|
||||
def update_path(self, dt: float):
|
||||
def update_path(self, dt: float) -> Self:
|
||||
if dt == 0:
|
||||
return self
|
||||
point = self.traced_point_func().copy()
|
||||
|
|
|
@ -9,7 +9,6 @@ import itertools as it
|
|||
from manimlib.constants import BLACK, BLUE, BLUE_D, BLUE_E, GREEN, GREY_A, WHITE, RED
|
||||
from manimlib.constants import DEGREES, PI
|
||||
from manimlib.constants import DL, UL, DOWN, DR, LEFT, ORIGIN, OUT, RIGHT, UP
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import FRAME_X_RADIUS, FRAME_Y_RADIUS
|
||||
from manimlib.constants import MED_SMALL_BUFF, SMALL_BUFF
|
||||
from manimlib.mobject.functions import ParametricCurve
|
||||
|
@ -22,6 +21,7 @@ from manimlib.mobject.svg.tex_mobject import Tex
|
|||
from manimlib.mobject.types.dot_cloud import DotCloud
|
||||
from manimlib.mobject.types.surface import ParametricSurface
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.simple_functions import binary_search
|
||||
from manimlib.utils.space_ops import angle_of_vector
|
||||
|
@ -32,7 +32,7 @@ from manimlib.utils.space_ops import normalize
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, Type, TypeVar
|
||||
from typing import Callable, Iterable, Sequence, Type, TypeVar, Optional, Self
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, VectN, RangeSpecifier
|
||||
|
||||
|
@ -236,7 +236,13 @@ class CoordinateSystem(ABC):
|
|||
"""
|
||||
return self.input_to_graph_point(x, graph)
|
||||
|
||||
def bind_graph_to_func(self, graph, func, jagged=False, get_discontinuities=None):
|
||||
def bind_graph_to_func(
|
||||
self,
|
||||
graph: VMobject,
|
||||
func: Callable[[Vect3], Vect3],
|
||||
jagged: bool = False,
|
||||
get_discontinuities: Optional[Callable[[], Vect3]] = None
|
||||
) -> VMobject:
|
||||
"""
|
||||
Use for graphing functions which might change over time, or change with
|
||||
conditions
|
||||
|
@ -637,6 +643,8 @@ class NumberPlane(Axes):
|
|||
lines2 = VGroup()
|
||||
inputs = np.arange(axis2.x_min, axis2.x_max + step, step)
|
||||
for i, x in enumerate(inputs):
|
||||
if abs(x) < 1e-8:
|
||||
continue
|
||||
new_line = line.copy()
|
||||
new_line.shift(axis2.n2p(x) - axis2.n2p(0))
|
||||
if i % (1 + ratio) == 0:
|
||||
|
@ -658,7 +666,7 @@ class NumberPlane(Axes):
|
|||
kwargs["buff"] = 0
|
||||
return Arrow(self.c2p(0, 0), self.c2p(*coords), **kwargs)
|
||||
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50):
|
||||
def prepare_for_nonlinear_transform(self, num_inserted_curves: int = 50) -> Self:
|
||||
for mob in self.family_members_with_points():
|
||||
num_curves = mob.get_num_curves()
|
||||
if num_inserted_curves > num_curves:
|
||||
|
@ -697,7 +705,7 @@ class ComplexPlane(NumberPlane):
|
|||
skip_first: bool = True,
|
||||
font_size: int = 36,
|
||||
**kwargs
|
||||
):
|
||||
) -> Self:
|
||||
if numbers is None:
|
||||
numbers = self.get_default_coordinate_values(skip_first)
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ from manimlib.utils.space_ops import rotation_matrix_transpose
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Iterable
|
||||
from typing import Iterable, Self, Optional
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array
|
||||
|
||||
|
||||
|
@ -67,7 +67,7 @@ class TipableVMobject(VMobject):
|
|||
)
|
||||
|
||||
# Adding, Creating, Modifying tips
|
||||
def add_tip(self, at_start: bool = False, **kwargs):
|
||||
def add_tip(self, at_start: bool = False, **kwargs) -> Self:
|
||||
"""
|
||||
Adds a tip to the TipableVMobject instance, recognising
|
||||
that the endpoints might need to be switched if it's
|
||||
|
@ -112,7 +112,7 @@ class TipableVMobject(VMobject):
|
|||
tip.shift(anchor - tip.get_tip_point())
|
||||
return tip
|
||||
|
||||
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool):
|
||||
def reset_endpoints_based_on_tip(self, tip: ArrowTip, at_start: bool) -> Self:
|
||||
if self.get_length() == 0:
|
||||
# Zero length, put_start_and_end_on wouldn't
|
||||
# work
|
||||
|
@ -127,7 +127,7 @@ class TipableVMobject(VMobject):
|
|||
self.put_start_and_end_on(start, end)
|
||||
return self
|
||||
|
||||
def asign_tip_attr(self, tip: ArrowTip, at_start: bool):
|
||||
def asign_tip_attr(self, tip: ArrowTip, at_start: bool) -> Self:
|
||||
if at_start:
|
||||
self.start_tip = tip
|
||||
else:
|
||||
|
@ -258,7 +258,7 @@ class Arc(TipableVMobject):
|
|||
angle = angle_of_vector(self.get_end() - self.get_arc_center())
|
||||
return angle % TAU
|
||||
|
||||
def move_arc_center_to(self, point: Vect3):
|
||||
def move_arc_center_to(self, point: Vect3) -> Self:
|
||||
self.shift(point - self.get_arc_center())
|
||||
return self
|
||||
|
||||
|
@ -318,7 +318,7 @@ class Circle(Arc):
|
|||
dim_to_match: int = 0,
|
||||
stretch: bool = False,
|
||||
buff: float = MED_SMALL_BUFF
|
||||
):
|
||||
) -> Self:
|
||||
self.replace(mobject, dim_to_match, stretch)
|
||||
self.stretch((self.get_width() + 2 * buff) / self.get_width(), 0)
|
||||
self.stretch((self.get_height() + 2 * buff) / self.get_height(), 1)
|
||||
|
@ -448,12 +448,8 @@ class Annulus(VMobject):
|
|||
)
|
||||
|
||||
self.radius = outer_radius
|
||||
# Make sure to add enough components that triangulation doesn't fail
|
||||
kw = dict(
|
||||
n_components=int(max(8, np.ceil(TAU / math.acos(inner_radius / outer_radius))))
|
||||
)
|
||||
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0, **kw)
|
||||
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0, **kw)
|
||||
outer_path = outer_radius * Arc.create_quadratic_bezier_points(TAU, 0)
|
||||
inner_path = inner_radius * Arc.create_quadratic_bezier_points(-TAU, 0)
|
||||
self.add_subpath(outer_path)
|
||||
self.add_subpath(inner_path)
|
||||
self.shift(center)
|
||||
|
@ -479,7 +475,7 @@ class Line(TipableVMobject):
|
|||
end: Vect3,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
) -> Self:
|
||||
vect = end - start
|
||||
dist = get_norm(vect)
|
||||
if np.isclose(dist, 0):
|
||||
|
@ -508,9 +504,10 @@ class Line(TipableVMobject):
|
|||
self.set_points_as_corners([start, end])
|
||||
return self
|
||||
|
||||
def set_path_arc(self, new_value: float) -> None:
|
||||
def set_path_arc(self, new_value: float) -> Self:
|
||||
self.path_arc = new_value
|
||||
self.init_points()
|
||||
return self
|
||||
|
||||
def set_start_and_end_attrs(self, start: Vect3 | Mobject, end: Vect3 | Mobject):
|
||||
# If either start or end are Mobjects, this
|
||||
|
@ -545,7 +542,7 @@ class Line(TipableVMobject):
|
|||
result[:len(point)] = point
|
||||
return result
|
||||
|
||||
def put_start_and_end_on(self, start: Vect3, end: Vect3):
|
||||
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
|
||||
curr_start, curr_end = self.get_start_and_end()
|
||||
if np.isclose(curr_start, curr_end).all():
|
||||
# Handle null lines more gracefully
|
||||
|
@ -573,7 +570,7 @@ class Line(TipableVMobject):
|
|||
def get_slope(self) -> float:
|
||||
return np.tan(self.get_angle())
|
||||
|
||||
def set_angle(self, angle: float, about_point: Vect3 | None = None):
|
||||
def set_angle(self, angle: float, about_point: Optional[Vect3] = None) -> Self:
|
||||
if about_point is None:
|
||||
about_point = self.get_start()
|
||||
self.rotate(
|
||||
|
@ -699,13 +696,13 @@ class Arrow(Line):
|
|||
end: Vect3,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
):
|
||||
) -> Self:
|
||||
super().set_points_by_ends(start, end, buff, path_arc)
|
||||
self.insert_tip_anchor()
|
||||
self.create_tip_with_stroke_width()
|
||||
return self
|
||||
|
||||
def insert_tip_anchor(self):
|
||||
def insert_tip_anchor(self) -> Self:
|
||||
prev_end = self.get_end()
|
||||
arc_len = self.get_arc_length()
|
||||
tip_len = self.get_stroke_width() * self.width_to_tip_len * self.tip_width_ratio
|
||||
|
@ -720,7 +717,7 @@ class Arrow(Line):
|
|||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def create_tip_with_stroke_width(self):
|
||||
def create_tip_with_stroke_width(self) -> Self:
|
||||
if self.get_num_points() < 3:
|
||||
return self
|
||||
tip_width = self.tip_width_ratio * min(
|
||||
|
@ -731,7 +728,7 @@ class Arrow(Line):
|
|||
self.data['stroke_width'][-3:, 0] = tip_width * np.linspace(1, 0, 3)
|
||||
return self
|
||||
|
||||
def reset_tip(self):
|
||||
def reset_tip(self) -> Self:
|
||||
self.set_points_by_ends(
|
||||
self.get_start(), self.get_end(),
|
||||
path_arc=self.path_arc
|
||||
|
@ -743,13 +740,13 @@ class Arrow(Line):
|
|||
color: ManimColor | Iterable[ManimColor] | None = None,
|
||||
width: float | Iterable[float] | None = None,
|
||||
*args, **kwargs
|
||||
):
|
||||
) -> Self:
|
||||
super().set_stroke(color=color, width=width, *args, **kwargs)
|
||||
if self.has_points():
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float):
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
if scale_factor != 1.0:
|
||||
self.reset_tip()
|
||||
return self
|
||||
|
@ -791,7 +788,7 @@ class FillArrow(Line):
|
|||
end: Vect3,
|
||||
buff: float = 0,
|
||||
path_arc: float = 0
|
||||
) -> None:
|
||||
) -> Self:
|
||||
# Find the right tip length and thickness
|
||||
vect = end - start
|
||||
length = max(get_norm(vect), 1e-8)
|
||||
|
@ -852,8 +849,9 @@ class FillArrow(Line):
|
|||
axis=rotate_vector(self.get_unit_vector(), -PI / 2),
|
||||
)
|
||||
self.shift(start - self.get_start())
|
||||
return self
|
||||
|
||||
def reset_points_around_ends(self):
|
||||
def reset_points_around_ends(self) -> Self:
|
||||
self.set_points_by_ends(
|
||||
self.get_start().copy(),
|
||||
self.get_end().copy(),
|
||||
|
@ -868,21 +866,21 @@ class FillArrow(Line):
|
|||
def get_end(self) -> Vect3:
|
||||
return self.get_points()[self.tip_index]
|
||||
|
||||
def put_start_and_end_on(self, start: Vect3, end: Vect3):
|
||||
def put_start_and_end_on(self, start: Vect3, end: Vect3) -> Self:
|
||||
self.set_points_by_ends(start, end, buff=0, path_arc=self.path_arc)
|
||||
return self
|
||||
|
||||
def scale(self, *args, **kwargs):
|
||||
def scale(self, *args, **kwargs) -> Self:
|
||||
super().scale(*args, **kwargs)
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_thickness(self, thickness: float):
|
||||
def set_thickness(self, thickness: float) -> Self:
|
||||
self.thickness = thickness
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
||||
def set_path_arc(self, path_arc: float):
|
||||
def set_path_arc(self, path_arc: float) -> Self:
|
||||
self.path_arc = path_arc
|
||||
self.reset_points_around_ends()
|
||||
return self
|
||||
|
@ -925,7 +923,7 @@ class Polygon(VMobject):
|
|||
def get_vertices(self) -> Vect3Array:
|
||||
return self.get_start_anchors()
|
||||
|
||||
def round_corners(self, radius: float | None = None):
|
||||
def round_corners(self, radius: Optional[float] = None) -> Self:
|
||||
if radius is None:
|
||||
verts = self.get_vertices()
|
||||
min_edge_length = min(
|
||||
|
|
|
@ -18,7 +18,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
from typing import Sequence, Self
|
||||
import numpy.typing as npt
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
@ -76,7 +76,7 @@ class Matrix(VMobject):
|
|||
self,
|
||||
matrix: Sequence[Sequence[str | float | VMobject]],
|
||||
v_buff: float = 0.8,
|
||||
h_buff: float = 1.3,
|
||||
h_buff: float = 1.0,
|
||||
bracket_h_buff: float = 0.2,
|
||||
bracket_v_buff: float = 0.25,
|
||||
add_background_rectangles_to_entries: bool = False,
|
||||
|
@ -129,7 +129,7 @@ class Matrix(VMobject):
|
|||
v_buff: float,
|
||||
h_buff: float,
|
||||
aligned_corner: Vect3,
|
||||
):
|
||||
) -> Self:
|
||||
for i, row in enumerate(matrix):
|
||||
for j, elem in enumerate(row):
|
||||
mob = matrix[i][j]
|
||||
|
@ -139,7 +139,7 @@ class Matrix(VMobject):
|
|||
)
|
||||
return self
|
||||
|
||||
def add_brackets(self, v_buff: float, h_buff: float):
|
||||
def add_brackets(self, v_buff: float, h_buff: float) -> Self:
|
||||
height = len(self.mob_matrix)
|
||||
brackets = Tex("".join((
|
||||
R"\left[\begin{array}{c}",
|
||||
|
@ -168,13 +168,13 @@ class Matrix(VMobject):
|
|||
for row in self.mob_matrix
|
||||
])
|
||||
|
||||
def set_column_colors(self, *colors: ManimColor):
|
||||
def set_column_colors(self, *colors: ManimColor) -> Self:
|
||||
columns = self.get_columns()
|
||||
for color, column in zip(colors, columns):
|
||||
column.set_color(color)
|
||||
return self
|
||||
|
||||
def add_background_to_entries(self):
|
||||
def add_background_to_entries(self) -> Self:
|
||||
for mob in self.get_entries():
|
||||
mob.add_background_rectangle()
|
||||
return self
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,7 +11,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypeVar
|
||||
from typing import TypeVar, Self
|
||||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
T = TypeVar("T", bound=VMobject)
|
||||
|
@ -163,7 +163,7 @@ class DecimalNumber(VMobject):
|
|||
def get_tex(self):
|
||||
return self.num_string
|
||||
|
||||
def set_value(self, number: float | complex):
|
||||
def set_value(self, number: float | complex) -> Self:
|
||||
move_to_point = self.get_edge_center(self.edge_to_fix)
|
||||
style = self.family_members_with_points()[0].get_style()
|
||||
self.set_submobjects_from_number(number)
|
||||
|
@ -171,14 +171,16 @@ class DecimalNumber(VMobject):
|
|||
self.set_style(**style)
|
||||
return self
|
||||
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> None:
|
||||
def _handle_scale_side_effects(self, scale_factor: float) -> Self:
|
||||
self.uniforms["font_size"] = scale_factor * self.uniforms["font_size"]
|
||||
return self
|
||||
|
||||
def get_value(self) -> float | complex:
|
||||
return self.number
|
||||
|
||||
def increment_value(self, delta_t: float | complex = 1) -> None:
|
||||
def increment_value(self, delta_t: float | complex = 1) -> Self:
|
||||
self.set_value(self.get_value() + delta_t)
|
||||
return self
|
||||
|
||||
|
||||
class Integer(DecimalNumber):
|
||||
|
|
|
@ -14,7 +14,7 @@ from manimlib.utils.customization import get_customization
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Sequence
|
||||
from typing import Sequence, Self
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.typing import ManimColor
|
||||
|
||||
|
@ -60,20 +60,20 @@ class BackgroundRectangle(SurroundingRectangle):
|
|||
)
|
||||
self.original_fill_opacity = fill_opacity
|
||||
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float):
|
||||
def pointwise_become_partial(self, mobject: Mobject, a: float, b: float) -> Self:
|
||||
self.set_fill(opacity=b * self.original_fill_opacity)
|
||||
return self
|
||||
|
||||
def set_style_data(
|
||||
def set_style(
|
||||
self,
|
||||
stroke_color: ManimColor | None = None,
|
||||
stroke_width: float | None = None,
|
||||
fill_color: ManimColor | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
family: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
# Unchangeable style, except for fill_opacity
|
||||
VMobject.set_style_data(
|
||||
VMobject.set_style(
|
||||
self,
|
||||
stroke_color=BLACK,
|
||||
stroke_width=0,
|
||||
|
|
|
@ -251,8 +251,6 @@ class Laptop(VGroup):
|
|||
self.axis = axis
|
||||
|
||||
self.add(body, screen_plate, axis)
|
||||
self.rotate(5 * np.pi / 12, LEFT, about_point=ORIGIN)
|
||||
self.rotate(np.pi / 6, DOWN, about_point=ORIGIN)
|
||||
|
||||
|
||||
class VideoIcon(SVGMobject):
|
||||
|
@ -383,7 +381,6 @@ class Bubble(SVGMobject):
|
|||
self.flip()
|
||||
|
||||
self.content = Mobject()
|
||||
self.refresh_triangulation()
|
||||
|
||||
def get_tip(self):
|
||||
# TODO, find a better way
|
||||
|
|
|
@ -30,10 +30,7 @@ class SingleStringTex(SVGMobject):
|
|||
fill_opacity: float = 1.0,
|
||||
stroke_width: float = 0,
|
||||
svg_default: dict = dict(fill_color=WHITE),
|
||||
path_string_config: dict = dict(
|
||||
should_subdivide_sharp_curves=True,
|
||||
should_remove_null_curves=True,
|
||||
),
|
||||
path_string_config: dict = dict(),
|
||||
font_size: int = 48,
|
||||
alignment: str = R"\centering",
|
||||
math_mode: bool = True,
|
||||
|
|
|
@ -295,15 +295,11 @@ class VMobjectFromSVGPath(VMobject):
|
|||
def __init__(
|
||||
self,
|
||||
path_obj: se.Path,
|
||||
should_subdivide_sharp_curves: bool = False,
|
||||
should_remove_null_curves: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
# Get rid of arcs
|
||||
path_obj.approximate_arcs_with_quads()
|
||||
self.path_obj = path_obj
|
||||
self.should_subdivide_sharp_curves = should_subdivide_sharp_curves
|
||||
self.should_remove_null_curves = should_remove_null_curves
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def init_points(self) -> None:
|
||||
|
@ -313,14 +309,6 @@ class VMobjectFromSVGPath(VMobject):
|
|||
path_string = self.path_obj.d()
|
||||
if path_string not in PATH_TO_POINTS:
|
||||
self.handle_commands()
|
||||
if self.should_subdivide_sharp_curves:
|
||||
# For a healthy triangulation later
|
||||
self.subdivide_sharp_curves()
|
||||
if self.should_remove_null_curves:
|
||||
# Get rid of any null curves
|
||||
self.set_points(self.get_points_without_null_curves())
|
||||
# So triangulation doesn't get messed up
|
||||
self.subdivide_intersections()
|
||||
# Save for future use
|
||||
PATH_TO_POINTS[path_string] = self.get_points().copy()
|
||||
else:
|
||||
|
|
|
@ -65,7 +65,7 @@ class SurfaceMesh(VGroup):
|
|||
u_indices = np.linspace(0, full_nu - 1, part_nu)
|
||||
v_indices = np.linspace(0, full_nv - 1, part_nv)
|
||||
|
||||
points, du_points, dv_points = uv_surface.get_surface_points_and_nudged_points()
|
||||
points = uv_surface.get_points()
|
||||
normals = uv_surface.get_unit_normals()
|
||||
nudge = self.normal_nudge
|
||||
nudged_points = points + nudge * normals
|
||||
|
@ -96,7 +96,7 @@ class Sphere(Surface):
|
|||
def __init__(
|
||||
self,
|
||||
u_range: Tuple[float, float] = (0, TAU),
|
||||
v_range: Tuple[float, float] = (0, PI),
|
||||
v_range: Tuple[float, float] = (1e-5, PI - 1e-5),
|
||||
resolution: Tuple[int, int] = (101, 51),
|
||||
radius: float = 1.0,
|
||||
**kwargs,
|
||||
|
@ -166,7 +166,6 @@ class Cylinder(Surface):
|
|||
self.scale(self.radius)
|
||||
self.set_depth(self.height, stretch=True)
|
||||
self.apply_matrix(z_to_vector(self.axis))
|
||||
return self
|
||||
|
||||
def uv_func(self, u: float, v: float) -> np.ndarray:
|
||||
return np.array([np.cos(u), np.sin(u), v])
|
||||
|
@ -186,6 +185,7 @@ class Line3D(Cylinder):
|
|||
height=get_norm(axis),
|
||||
radius=width / 2,
|
||||
axis=axis,
|
||||
resolution=resolution,
|
||||
**kwargs
|
||||
)
|
||||
self.shift((start + end) / 2)
|
||||
|
@ -376,16 +376,6 @@ class Dodecahedron(VGroup3D):
|
|||
|
||||
super().__init__(*pentagons, **style)
|
||||
|
||||
# # Rotate those two pentagons by all the axis permuations to fill
|
||||
# # out the dodecahedron
|
||||
# Id = np.identity(3)
|
||||
# for i in range(3):
|
||||
# perm = [j % 3 for j in range(i, i + 3)]
|
||||
# for b in [1, -1]:
|
||||
# matrix = b * np.array([Id[0][perm], Id[1][perm], Id[2][perm]])
|
||||
# self.add(pentagon1.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
# self.add(pentagon2.copy().apply_matrix(matrix, about_point=ORIGIN))
|
||||
|
||||
|
||||
class Prismify(VGroup3D):
|
||||
def __init__(self, vmobject, depth=1.0, direction=IN, **kwargs):
|
||||
|
|
|
@ -13,7 +13,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
from typing import Sequence, Tuple
|
||||
from typing import Sequence, Tuple, Self
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array
|
||||
|
||||
|
||||
|
@ -70,7 +70,7 @@ class DotCloud(PMobject):
|
|||
v_buff_ratio: float = 1.0,
|
||||
d_buff_ratio: float = 1.0,
|
||||
height: float = DEFAULT_GRID_HEIGHT,
|
||||
):
|
||||
) -> Self:
|
||||
n_points = n_rows * n_cols * n_layers
|
||||
points = np.repeat(range(n_points), 3, axis=0).reshape((n_points, 3))
|
||||
points[:, 0] = points[:, 0] % n_cols
|
||||
|
@ -96,7 +96,7 @@ class DotCloud(PMobject):
|
|||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_radii(self, radii: npt.ArrayLike):
|
||||
def set_radii(self, radii: npt.ArrayLike) -> Self:
|
||||
n_points = self.get_num_points()
|
||||
radii = np.array(radii).reshape((len(radii), 1))
|
||||
self.data["radius"][:] = resize_with_interpolation(radii, n_points)
|
||||
|
@ -107,7 +107,7 @@ class DotCloud(PMobject):
|
|||
return self.data["radius"]
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_radius(self, radius: float):
|
||||
def set_radius(self, radius: float) -> Self:
|
||||
data = self.data if self.get_num_points() > 0 else self._data_defaults
|
||||
data["radius"][:] = radius
|
||||
self.refresh_bounding_box()
|
||||
|
@ -116,13 +116,14 @@ class DotCloud(PMobject):
|
|||
def get_radius(self) -> float:
|
||||
return self.get_radii().max()
|
||||
|
||||
def set_glow_factor(self, glow_factor: float) -> None:
|
||||
def set_glow_factor(self, glow_factor: float) -> Self:
|
||||
self.uniforms["glow_factor"] = glow_factor
|
||||
return self
|
||||
|
||||
def get_glow_factor(self) -> float:
|
||||
return self.uniforms["glow_factor"]
|
||||
|
||||
def compute_bounding_box(self) -> np.ndarray:
|
||||
def compute_bounding_box(self) -> Vect3Array:
|
||||
bb = super().compute_bounding_box()
|
||||
radius = self.get_radius()
|
||||
bb[0] += np.full((3,), -radius)
|
||||
|
@ -134,7 +135,7 @@ class DotCloud(PMobject):
|
|||
scale_factor: float | npt.ArrayLike,
|
||||
scale_radii: bool = True,
|
||||
**kwargs
|
||||
):
|
||||
) -> Self:
|
||||
super().scale(scale_factor, **kwargs)
|
||||
if scale_radii:
|
||||
self.set_radii(scale_factor * self.get_radii())
|
||||
|
@ -145,7 +146,7 @@ class DotCloud(PMobject):
|
|||
reflectiveness: float = 0.5,
|
||||
gloss: float = 0.1,
|
||||
shadow: float = 0.2
|
||||
):
|
||||
) -> Self:
|
||||
self.set_shading(reflectiveness, gloss, shadow)
|
||||
self.apply_depth_test()
|
||||
return self
|
||||
|
|
|
@ -10,7 +10,7 @@ from manimlib.utils.iterables import resize_with_interpolation
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from typing import Callable, Self
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array, Vect4Array
|
||||
|
||||
|
||||
|
@ -28,7 +28,7 @@ class PMobject(Mobject):
|
|||
rgbas: Vect4Array | None = None,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float | None = None
|
||||
):
|
||||
) -> Self:
|
||||
"""
|
||||
points must be a Nx3 numpy array, as must rgbas if it is not None
|
||||
"""
|
||||
|
@ -46,13 +46,13 @@ class PMobject(Mobject):
|
|||
self.data["rgba"][-len(rgbas):] = rgbas
|
||||
return self
|
||||
|
||||
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None):
|
||||
def add_point(self, point: Vect3, rgba=None, color=None, opacity=None) -> Self:
|
||||
rgbas = None if rgba is None else [rgba]
|
||||
self.add_points([point], rgbas, color, opacity)
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_color_by_gradient(self, *colors: ManimColor):
|
||||
def set_color_by_gradient(self, *colors: ManimColor) -> Self:
|
||||
self.data["rgba"][:] = np.array(list(map(
|
||||
color_to_rgba,
|
||||
color_gradient(colors, self.get_num_points())
|
||||
|
@ -60,20 +60,20 @@ class PMobject(Mobject):
|
|||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def match_colors(self, pmobject: PMobject):
|
||||
def match_colors(self, pmobject: PMobject) -> Self:
|
||||
self.data["rgba"][:] = resize_with_interpolation(
|
||||
pmobject.data["rgba"], self.get_num_points()
|
||||
)
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def filter_out(self, condition: Callable[[np.ndarray], bool]):
|
||||
def filter_out(self, condition: Callable[[np.ndarray], bool]) -> Self:
|
||||
for mob in self.family_members_with_points():
|
||||
mob.data = mob.data[~np.apply_along_axis(condition, 1, mob.get_points())]
|
||||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]):
|
||||
def sort_points(self, function: Callable[[Vect3], None] = lambda p: p[0]) -> Self:
|
||||
"""
|
||||
function is any map from R^3 to R
|
||||
"""
|
||||
|
@ -85,7 +85,7 @@ class PMobject(Mobject):
|
|||
return self
|
||||
|
||||
@Mobject.affects_data
|
||||
def ingest_submobjects(self):
|
||||
def ingest_submobjects(self) -> Self:
|
||||
self.data = np.vstack([
|
||||
sm.data for sm in self.get_family()
|
||||
])
|
||||
|
@ -96,7 +96,7 @@ class PMobject(Mobject):
|
|||
return self.get_points()[int(index)]
|
||||
|
||||
@Mobject.affects_data
|
||||
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float):
|
||||
def pointwise_become_partial(self, pmobject: PMobject, a: float, b: float) -> Self:
|
||||
lower_index = int(a * pmobject.get_num_points())
|
||||
upper_index = int(b * pmobject.get_num_points())
|
||||
self.data = pmobject.data[lower_index:upper_index].copy()
|
||||
|
|
|
@ -12,11 +12,12 @@ from manimlib.utils.images import get_full_raster_image_path
|
|||
from manimlib.utils.iterables import listify
|
||||
from manimlib.utils.iterables import resize_with_interpolation
|
||||
from manimlib.utils.space_ops import normalize_along_axis
|
||||
from manimlib.utils.space_ops import cross
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, Tuple
|
||||
from typing import Callable, Iterable, Sequence, Tuple, Self
|
||||
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.typing import ManimColor, Vect3, Vect3Array
|
||||
|
@ -27,11 +28,9 @@ class Surface(Mobject):
|
|||
shader_folder: str = "surface"
|
||||
shader_dtype: np.dtype = np.dtype([
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('normal', np.float32, (3,)),
|
||||
('rgba', np.float32, (4,)),
|
||||
])
|
||||
pointlike_data_keys = ['point', 'du_point', 'dv_point']
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -96,17 +95,24 @@ class Surface(Mobject):
|
|||
for grid in (uv_grid, uv_plus_du, uv_plus_dv)
|
||||
]
|
||||
self.set_points(points)
|
||||
self.data["du_point"][:] = du_points
|
||||
self.data["dv_point"][:] = dv_points
|
||||
self.data["normal"] = normalize_along_axis(cross(
|
||||
(du_points - points) / self.epsilon,
|
||||
(dv_points - points) / self.epsilon,
|
||||
), 1)
|
||||
|
||||
def compute_triangle_indices(self):
|
||||
def apply_points_function(self, *args, **kwargs) -> Self:
|
||||
super().apply_points_function(*args, **kwargs)
|
||||
self.get_unit_normals()
|
||||
return self
|
||||
|
||||
def compute_triangle_indices(self) -> np.ndarray:
|
||||
# TODO, if there is an event which changes
|
||||
# the resolution of the surface, make sure
|
||||
# this is called.
|
||||
nu, nv = self.resolution
|
||||
if nu == 0 or nv == 0:
|
||||
self.triangle_indices = np.zeros(0, dtype=int)
|
||||
return
|
||||
return self.triangle_indices
|
||||
index_grid = np.arange(nu * nv).reshape((nu, nv))
|
||||
indices = np.zeros(6 * (nu - 1) * (nv - 1), dtype=int)
|
||||
indices[0::6] = index_grid[:-1, :-1].flatten() # Top left
|
||||
|
@ -116,20 +122,32 @@ class Surface(Mobject):
|
|||
indices[4::6] = index_grid[+1:, :-1].flatten() # Bottom left
|
||||
indices[5::6] = index_grid[+1:, +1:].flatten() # Bottom right
|
||||
self.triangle_indices = indices
|
||||
return self.triangle_indices
|
||||
|
||||
def get_triangle_indices(self) -> np.ndarray:
|
||||
return self.triangle_indices
|
||||
|
||||
def get_surface_points_and_nudged_points(self) -> tuple[Vect3Array, Vect3Array, Vect3Array]:
|
||||
return (self.data['point'], self.data['du_point'], self.data['dv_point'])
|
||||
|
||||
def get_unit_normals(self) -> Vect3Array:
|
||||
s_points, du_points, dv_points = self.get_surface_points_and_nudged_points()
|
||||
normals = np.cross(
|
||||
(du_points - s_points) / self.epsilon,
|
||||
(dv_points - s_points) / self.epsilon,
|
||||
nu, nv = self.resolution
|
||||
indices = np.arange(nu * nv)
|
||||
|
||||
left = indices - 1
|
||||
right = indices + 1
|
||||
up = indices - nv
|
||||
down = indices + nv
|
||||
|
||||
left[0] = indices[0]
|
||||
right[-1] = indices[-1]
|
||||
up[:nv] = indices[:nv]
|
||||
down[-nv:] = indices[-nv:]
|
||||
|
||||
points = self.get_points()
|
||||
crosses = cross(
|
||||
points[right] - points[left],
|
||||
points[up] - points[down],
|
||||
)
|
||||
return normalize_along_axis(normals, 1)
|
||||
self.data["normal"] = normalize_along_axis(crosses, 1)
|
||||
return self.data["normal"]
|
||||
|
||||
@Mobject.affects_data
|
||||
def pointwise_become_partial(
|
||||
|
@ -138,7 +156,7 @@ class Surface(Mobject):
|
|||
a: float,
|
||||
b: float,
|
||||
axis: int | None = None
|
||||
):
|
||||
) -> Self:
|
||||
assert(isinstance(smobject, Surface))
|
||||
if axis is None:
|
||||
axis = self.prefered_creation_axis
|
||||
|
@ -147,12 +165,11 @@ class Surface(Mobject):
|
|||
return self
|
||||
|
||||
nu, nv = smobject.resolution
|
||||
for key in ['point', 'du_point', 'dv_point']:
|
||||
self.data[key][:] = self.get_partial_points_array(
|
||||
self.data[key], a, b,
|
||||
(nu, nv, 3),
|
||||
axis=axis
|
||||
)
|
||||
self.data['point'][:] = self.get_partial_points_array(
|
||||
self.data['point'], a, b,
|
||||
(nu, nv, 3),
|
||||
axis=axis
|
||||
)
|
||||
return self
|
||||
|
||||
def get_partial_points_array(
|
||||
|
@ -196,7 +213,7 @@ class Surface(Mobject):
|
|||
return points.reshape((nu * nv, *resolution[2:]))
|
||||
|
||||
@Mobject.affects_data
|
||||
def sort_faces_back_to_front(self, vect: Vect3 = OUT):
|
||||
def sort_faces_back_to_front(self, vect: Vect3 = OUT) -> Self:
|
||||
tri_is = self.triangle_indices
|
||||
points = self.get_points()
|
||||
|
||||
|
@ -206,24 +223,25 @@ class Surface(Mobject):
|
|||
tri_is[k::3] = tri_is[k::3][indices]
|
||||
return self
|
||||
|
||||
def always_sort_to_camera(self, camera: Camera):
|
||||
def always_sort_to_camera(self, camera: Camera) -> Self:
|
||||
def updater(surface: Surface):
|
||||
vect = camera.get_location() - surface.get_center()
|
||||
surface.sort_faces_back_to_front(vect)
|
||||
self.add_updater(updater)
|
||||
return self
|
||||
|
||||
def set_clip_plane(
|
||||
self,
|
||||
vect: Vect3 | None = None,
|
||||
threshold: float | None = None
|
||||
):
|
||||
) -> Self:
|
||||
if vect is not None:
|
||||
self.uniforms["clip_plane"][:3] = vect
|
||||
if threshold is not None:
|
||||
self.uniforms["clip_plane"][3] = threshold
|
||||
return self
|
||||
|
||||
def deactivate_clip_plane(self):
|
||||
def deactivate_clip_plane(self) -> Self:
|
||||
self.uniforms["clip_plane"][:] = 0
|
||||
return self
|
||||
|
||||
|
@ -263,8 +281,7 @@ class TexturedSurface(Surface):
|
|||
shader_folder: str = "textured_surface"
|
||||
shader_dtype: Sequence[Tuple[str, type, Tuple[int]]] = [
|
||||
('point', np.float32, (3,)),
|
||||
('du_point', np.float32, (3,)),
|
||||
('dv_point', np.float32, (3,)),
|
||||
('normal', np.float32, (3,)),
|
||||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
|
@ -306,8 +323,9 @@ class TexturedSurface(Surface):
|
|||
surf = self.uv_surface
|
||||
nu, nv = surf.resolution
|
||||
self.resize_points(surf.get_num_points())
|
||||
for key in ['point', 'du_point', 'dv_point']:
|
||||
self.data[key][:] = surf.data[key]
|
||||
self.resolution = surf.resolution
|
||||
self.data['point'][:] = surf.data['point']
|
||||
self.data['normal'][:] = surf.data['normal']
|
||||
self.data['opacity'][:, 0] = surf.data["rgba"][:, 3]
|
||||
self.data["im_coords"] = np.array([
|
||||
[u, v]
|
||||
|
@ -320,7 +338,7 @@ class TexturedSurface(Surface):
|
|||
self.uniforms["num_textures"] = self.num_textures
|
||||
|
||||
@Mobject.affects_data
|
||||
def set_opacity(self, opacity: float | Iterable[float]):
|
||||
def set_opacity(self, opacity: float | Iterable[float]) -> Self:
|
||||
op_arr = np.array(listify(opacity))
|
||||
self.data["opacity"][:, 0] = resize_with_interpolation(op_arr, len(self.data))
|
||||
return self
|
||||
|
@ -330,7 +348,7 @@ class TexturedSurface(Surface):
|
|||
color: ManimColor | Iterable[ManimColor] | None,
|
||||
opacity: float | Iterable[float] | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
if opacity is not None:
|
||||
self.set_opacity(opacity)
|
||||
return self
|
||||
|
@ -341,7 +359,7 @@ class TexturedSurface(Surface):
|
|||
a: float,
|
||||
b: float,
|
||||
axis: int = 1
|
||||
):
|
||||
) -> Self:
|
||||
super().pointwise_become_partial(tsmobject, a, b, axis)
|
||||
im_coords = self.data["im_coords"]
|
||||
im_coords[:] = tsmobject.data["im_coords"]
|
||||
|
|
|
@ -45,7 +45,7 @@ from manimlib.shader_wrapper import FillShaderWrapper
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Tuple
|
||||
from typing import Callable, Iterable, Tuple, Any, Self
|
||||
from manimlib.typing import ManimColor, Vect3, Vect4, Vect3Array, Vect4Array
|
||||
from moderngl.context import Context
|
||||
|
||||
|
@ -68,8 +68,8 @@ class VMobject(Mobject):
|
|||
fill_data_names = ['point', 'fill_rgba', 'base_point', 'unit_normal']
|
||||
stroke_data_names = ['point', 'stroke_rgba', 'stroke_width', 'joint_product']
|
||||
|
||||
fill_render_primitive: int = moderngl.TRIANGLE_STRIP
|
||||
stroke_render_primitive: int = moderngl.TRIANGLE_STRIP
|
||||
fill_render_primitive: int = moderngl.TRIANGLES
|
||||
stroke_render_primitive: int = moderngl.TRIANGLES
|
||||
|
||||
pre_function_handle_to_anchor_scale_factor: float = 0.01
|
||||
make_smooth_after_applying_functions: bool = False
|
||||
|
@ -128,39 +128,10 @@ class VMobject(Mobject):
|
|||
self.uniforms["joint_type"] = JOINT_TYPE_MAP[self.joint_type]
|
||||
self.uniforms["flat_stroke"] = float(self.flat_stroke)
|
||||
|
||||
# These are here just to make type checkers happy
|
||||
def get_family(self, recurse: bool = True) -> list[VMobject]:
|
||||
return super().get_family(recurse)
|
||||
|
||||
def family_members_with_points(self) -> list[VMobject]:
|
||||
return super().family_members_with_points()
|
||||
|
||||
def replicate(self, n: int) -> VGroup:
|
||||
return super().replicate(n)
|
||||
|
||||
def get_grid(self, *args, **kwargs) -> VGroup:
|
||||
return super().get_grid(*args, **kwargs)
|
||||
|
||||
def __getitem__(self, value: int | slice) -> VMobject:
|
||||
return super().__getitem__(value)
|
||||
|
||||
def add(self, *vmobjects: VMobject):
|
||||
def add(self, *vmobjects: VMobject) -> Self:
|
||||
if not all((isinstance(m, VMobject) for m in vmobjects)):
|
||||
raise Exception("All submobjects must be of type VMobject")
|
||||
super().add(*vmobjects)
|
||||
|
||||
def add_background_rectangle(
|
||||
self,
|
||||
color: ManimColor | None = None,
|
||||
opacity: float = 0.75,
|
||||
**kwargs
|
||||
):
|
||||
normal = self.family_members_with_points()[0].get_unit_normal()
|
||||
super().add_background_rectangle(color, opacity, **kwargs)
|
||||
rect = self.background_rectangle
|
||||
if np.dot(rect.get_unit_normal(), normal) < 0:
|
||||
rect.reverse_points()
|
||||
return self
|
||||
return super().add(*vmobjects)
|
||||
|
||||
# Colors
|
||||
def init_colors(self):
|
||||
|
@ -185,7 +156,7 @@ class VMobject(Mobject):
|
|||
rgba_array: Vect4Array,
|
||||
name: str | None = None,
|
||||
recurse: bool = False
|
||||
):
|
||||
) -> Self:
|
||||
if name is None:
|
||||
names = ["fill_rgba", "stroke_rgba"]
|
||||
else:
|
||||
|
@ -201,7 +172,7 @@ class VMobject(Mobject):
|
|||
opacity: float | Iterable[float] | None = None,
|
||||
border_width: float | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_rgba_array_by_color(color, opacity, 'fill_rgba', recurse)
|
||||
if border_width is not None:
|
||||
for mob in self.get_family(recurse):
|
||||
|
@ -215,7 +186,7 @@ class VMobject(Mobject):
|
|||
opacity: float | Iterable[float] | None = None,
|
||||
background: bool | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_rgba_array_by_color(color, opacity, 'stroke_rgba', recurse)
|
||||
|
||||
if width is not None:
|
||||
|
@ -238,7 +209,7 @@ class VMobject(Mobject):
|
|||
color: ManimColor | Iterable[ManimColor] = BLACK,
|
||||
width: float | Iterable[float] = 3,
|
||||
background: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_stroke(color, width, background=background)
|
||||
return self
|
||||
|
||||
|
@ -255,7 +226,7 @@ class VMobject(Mobject):
|
|||
stroke_background: bool = True,
|
||||
shading: Tuple[float, float, float] | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
if fill_rgba is not None:
|
||||
mob.data['fill_rgba'][:] = resize_with_interpolation(fill_rgba, len(mob.data['fill_rgba']))
|
||||
|
@ -286,7 +257,7 @@ class VMobject(Mobject):
|
|||
mob.set_shading(*shading, recurse=False)
|
||||
return self
|
||||
|
||||
def get_style(self):
|
||||
def get_style(self) -> dict[str, Any]:
|
||||
data = self.data if self.get_num_points() > 0 else self._data_defaults
|
||||
return {
|
||||
"fill_rgba": data['fill_rgba'].copy(),
|
||||
|
@ -296,7 +267,7 @@ class VMobject(Mobject):
|
|||
"shading": self.get_shading(),
|
||||
}
|
||||
|
||||
def match_style(self, vmobject: VMobject, recurse: bool = True):
|
||||
def match_style(self, vmobject: VMobject, recurse: bool = True) -> Self:
|
||||
self.set_style(**vmobject.get_style(), recurse=False)
|
||||
if recurse:
|
||||
# Does its best to match up submobject lists, and
|
||||
|
@ -315,7 +286,7 @@ class VMobject(Mobject):
|
|||
color: ManimColor | Iterable[ManimColor] | None,
|
||||
opacity: float | Iterable[float] | None = None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_fill(color, opacity=opacity, recurse=recurse)
|
||||
self.set_stroke(color, opacity=opacity, recurse=recurse)
|
||||
return self
|
||||
|
@ -324,12 +295,16 @@ class VMobject(Mobject):
|
|||
self,
|
||||
opacity: float | Iterable[float] | None,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_fill(opacity=opacity, recurse=recurse)
|
||||
self.set_stroke(opacity=opacity, recurse=recurse)
|
||||
return self
|
||||
|
||||
def fade(self, darkness: float = 0.5, recurse: bool = True):
|
||||
def set_anti_alias_width(self, anti_alias_width: float, recurse: bool = True) -> Self:
|
||||
self.set_uniform(recurse, anti_alias_width=anti_alias_width)
|
||||
return self
|
||||
|
||||
def fade(self, darkness: float = 0.5, recurse: bool = True) -> Self:
|
||||
mobs = self.get_family() if recurse else [self]
|
||||
for mob in mobs:
|
||||
factor = 1.0 - darkness
|
||||
|
@ -399,6 +374,9 @@ class VMobject(Mobject):
|
|||
return self.get_fill_color()
|
||||
return self.get_stroke_color()
|
||||
|
||||
def get_anti_alias_width(self):
|
||||
return self.uniforms["anti_alias_width"]
|
||||
|
||||
def has_stroke(self) -> bool:
|
||||
return any(self.data['stroke_width']) and any(self.data['stroke_rgba'][:, 3])
|
||||
|
||||
|
@ -410,7 +388,7 @@ class VMobject(Mobject):
|
|||
return self.get_fill_opacity()
|
||||
return self.get_stroke_opacity()
|
||||
|
||||
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True):
|
||||
def set_flat_stroke(self, flat_stroke: bool = True, recurse: bool = True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["flat_stroke"] = float(flat_stroke)
|
||||
return self
|
||||
|
@ -418,7 +396,7 @@ class VMobject(Mobject):
|
|||
def get_flat_stroke(self) -> bool:
|
||||
return self.uniforms["flat_stroke"] == 1.0
|
||||
|
||||
def set_joint_type(self, joint_type: str, recurse: bool = True):
|
||||
def set_joint_type(self, joint_type: str, recurse: bool = True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
mob.uniforms["joint_type"] = JOINT_TYPE_MAP[joint_type]
|
||||
return self
|
||||
|
@ -426,10 +404,34 @@ class VMobject(Mobject):
|
|||
def get_joint_type(self) -> float:
|
||||
return self.uniforms["joint_type"]
|
||||
|
||||
def apply_depth_test(
|
||||
self,
|
||||
anti_alias_width: float = 0,
|
||||
fill_border_width: float = 0,
|
||||
recurse: bool=True
|
||||
) -> Self:
|
||||
super().apply_depth_test(recurse)
|
||||
self.set_anti_alias_width(anti_alias_width)
|
||||
self.set_fill(border_width=fill_border_width)
|
||||
return self
|
||||
|
||||
def deactivate_depth_test(
|
||||
self,
|
||||
anti_alias_width: float = 1.0,
|
||||
fill_border_width: float = 0.5,
|
||||
recurse: bool=True
|
||||
) -> Self:
|
||||
super().apply_depth_test(recurse)
|
||||
self.set_anti_alias_width(anti_alias_width)
|
||||
self.set_fill(border_width=fill_border_width)
|
||||
return self
|
||||
|
||||
@Mobject.affects_family_data
|
||||
def use_winding_fill(self, value: bool = True, recurse: bool = True):
|
||||
def use_winding_fill(self, value: bool = True, recurse: bool = True) -> Self:
|
||||
for submob in self.get_family(recurse):
|
||||
submob._use_winding_fill = value
|
||||
if not value and submob.has_points():
|
||||
submob.subdivide_intersections()
|
||||
return self
|
||||
|
||||
# Points
|
||||
|
@ -437,7 +439,7 @@ class VMobject(Mobject):
|
|||
self,
|
||||
anchors: Vect3Array,
|
||||
handles: Vect3Array,
|
||||
):
|
||||
) -> Self:
|
||||
assert(len(anchors) == len(handles) + 1)
|
||||
points = resize_array(self.get_points(), 2 * len(anchors) - 1)
|
||||
points[0::2] = anchors
|
||||
|
@ -445,7 +447,7 @@ class VMobject(Mobject):
|
|||
self.set_points(points)
|
||||
return self
|
||||
|
||||
def start_new_path(self, point: Vect3):
|
||||
def start_new_path(self, point: Vect3) -> Self:
|
||||
# Path ends are signaled by a handle point sitting directly
|
||||
# on top of the previous anchor
|
||||
if self.has_points():
|
||||
|
@ -460,7 +462,7 @@ class VMobject(Mobject):
|
|||
handle1: Vect3,
|
||||
handle2: Vect3,
|
||||
anchor2: Vect3
|
||||
):
|
||||
) -> Self:
|
||||
self.start_new_path(anchor1)
|
||||
self.add_cubic_bezier_curve_to(handle1, handle2, anchor2)
|
||||
return self
|
||||
|
@ -470,7 +472,7 @@ class VMobject(Mobject):
|
|||
handle1: Vect3,
|
||||
handle2: Vect3,
|
||||
anchor: Vect3,
|
||||
):
|
||||
) -> Self:
|
||||
"""
|
||||
Add cubic bezier curve to the path.
|
||||
"""
|
||||
|
@ -492,7 +494,7 @@ class VMobject(Mobject):
|
|||
self.append_points(quad_approx[1:])
|
||||
return self
|
||||
|
||||
def add_quadratic_bezier_curve_to(self, handle: Vect3, anchor: Vect3):
|
||||
def add_quadratic_bezier_curve_to(self, handle: Vect3, anchor: Vect3) -> Self:
|
||||
self.throw_error_if_no_points()
|
||||
last_point = self.get_last_point()
|
||||
if self.consider_points_equal(handle, last_point):
|
||||
|
@ -501,14 +503,14 @@ class VMobject(Mobject):
|
|||
self.append_points([handle, anchor])
|
||||
return self
|
||||
|
||||
def add_line_to(self, point: Vect3):
|
||||
def add_line_to(self, point: Vect3) -> Self:
|
||||
self.throw_error_if_no_points()
|
||||
last_point = self.get_last_point()
|
||||
alphas = np.linspace(0, 1, 5 if self.long_lines else 3)
|
||||
self.append_points(outer_interpolate(last_point, point, alphas[1:]))
|
||||
return self
|
||||
|
||||
def add_smooth_curve_to(self, point: Vect3):
|
||||
def add_smooth_curve_to(self, point: Vect3) -> Self:
|
||||
if self.has_new_path_started():
|
||||
self.add_line_to(point)
|
||||
else:
|
||||
|
@ -517,7 +519,7 @@ class VMobject(Mobject):
|
|||
self.add_quadratic_bezier_curve_to(new_handle, point)
|
||||
return self
|
||||
|
||||
def add_smooth_cubic_curve_to(self, handle: Vect3, point: Vect3):
|
||||
def add_smooth_cubic_curve_to(self, handle: Vect3, point: Vect3) -> Self:
|
||||
self.throw_error_if_no_points()
|
||||
if self.get_num_points() == 1:
|
||||
new_handle = handle
|
||||
|
@ -538,7 +540,7 @@ class VMobject(Mobject):
|
|||
points = self.get_points()
|
||||
return 2 * points[-1] - points[-2]
|
||||
|
||||
def close_path(self, smooth: bool = False):
|
||||
def close_path(self, smooth: bool = False) -> Self:
|
||||
if self.is_closed():
|
||||
return self
|
||||
last_path_start = self.get_subpaths()[-1][0]
|
||||
|
@ -556,7 +558,7 @@ class VMobject(Mobject):
|
|||
self,
|
||||
tuple_to_subdivisions: Callable,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
for vmob in self.get_family(recurse):
|
||||
if not vmob.has_points():
|
||||
continue
|
||||
|
@ -578,7 +580,7 @@ class VMobject(Mobject):
|
|||
self,
|
||||
angle_threshold: float = 30 * DEGREES,
|
||||
recurse: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
def tuple_to_subdivisions(b0, b1, b2):
|
||||
angle = angle_between_vectors(b1 - b0, b2 - b1)
|
||||
return int(angle / angle_threshold)
|
||||
|
@ -586,7 +588,7 @@ class VMobject(Mobject):
|
|||
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
|
||||
return self
|
||||
|
||||
def subdivide_intersections(self, recurse: bool = True, n_subdivisions: int = 1):
|
||||
def subdivide_intersections(self, recurse: bool = True, n_subdivisions: int = 1) -> Self:
|
||||
path = self.get_anchors()
|
||||
def tuple_to_subdivisions(b0, b1, b2):
|
||||
if line_intersects_path(b0, b1, path):
|
||||
|
@ -596,12 +598,12 @@ class VMobject(Mobject):
|
|||
self.subdivide_curves_by_condition(tuple_to_subdivisions, recurse)
|
||||
return self
|
||||
|
||||
def add_points_as_corners(self, points: Iterable[Vect3]):
|
||||
def add_points_as_corners(self, points: Iterable[Vect3]) -> Self:
|
||||
for point in points:
|
||||
self.add_line_to(point)
|
||||
return points
|
||||
return self
|
||||
|
||||
def set_points_as_corners(self, points: Iterable[Vect3]):
|
||||
def set_points_as_corners(self, points: Iterable[Vect3]) -> Self:
|
||||
anchors = np.array(points)
|
||||
handles = 0.5 * (anchors[:-1] + anchors[1:])
|
||||
self.set_anchors_and_handles(anchors, handles)
|
||||
|
@ -611,7 +613,7 @@ class VMobject(Mobject):
|
|||
self,
|
||||
points: Iterable[Vect3],
|
||||
approx: bool = True
|
||||
):
|
||||
) -> Self:
|
||||
self.set_points_as_corners(points)
|
||||
self.make_smooth(approx=approx)
|
||||
return self
|
||||
|
@ -620,7 +622,7 @@ class VMobject(Mobject):
|
|||
dots = self.get_joint_products()[::2, 3]
|
||||
return bool((dots > 1 - 1e-3).all())
|
||||
|
||||
def change_anchor_mode(self, mode: str):
|
||||
def change_anchor_mode(self, mode: str) -> Self:
|
||||
assert(mode in ("jagged", "approx_smooth", "true_smooth"))
|
||||
subpaths = self.get_subpaths()
|
||||
self.clear_points()
|
||||
|
@ -643,7 +645,7 @@ class VMobject(Mobject):
|
|||
self.add_subpath(new_subpath)
|
||||
return self
|
||||
|
||||
def make_smooth(self, approx=False, recurse=True):
|
||||
def make_smooth(self, approx=False, recurse=True) -> Self:
|
||||
"""
|
||||
Edits the path so as to pass smoothly through all
|
||||
the current anchor points.
|
||||
|
@ -658,15 +660,16 @@ class VMobject(Mobject):
|
|||
submob.change_anchor_mode(mode)
|
||||
return self
|
||||
|
||||
def make_approximately_smooth(self, recurse=True):
|
||||
def make_approximately_smooth(self, recurse=True) -> Self:
|
||||
self.make_smooth(approx=True, recurse=recurse)
|
||||
return self
|
||||
|
||||
def make_jagged(self, recurse=True):
|
||||
def make_jagged(self, recurse=True) -> Self:
|
||||
for submob in self.get_family(recurse):
|
||||
submob.change_anchor_mode("jagged")
|
||||
return self
|
||||
|
||||
def add_subpath(self, points: Vect3Array):
|
||||
def add_subpath(self, points: Vect3Array) -> Self:
|
||||
assert(len(points) % 2 == 1 or len(points) == 0)
|
||||
if not self.has_points():
|
||||
self.set_points(points)
|
||||
|
@ -676,7 +679,7 @@ class VMobject(Mobject):
|
|||
self.append_points(points[1:])
|
||||
return self
|
||||
|
||||
def append_vectorized_mobject(self, vmobject: VMobject):
|
||||
def append_vectorized_mobject(self, vmobject: VMobject) -> Self:
|
||||
self.add_subpath(vmobject.get_points())
|
||||
n = vmobject.get_num_points()
|
||||
self.data[-n:] = vmobject.data
|
||||
|
@ -694,7 +697,7 @@ class VMobject(Mobject):
|
|||
def get_bezier_tuples(self) -> Iterable[Vect3Array]:
|
||||
return self.get_bezier_tuples_from_points(self.get_points())
|
||||
|
||||
def get_subpath_end_indices_from_points(self, points: Vect3Array):
|
||||
def get_subpath_end_indices_from_points(self, points: Vect3Array) -> np.ndarray:
|
||||
atol = self.tolerance_for_point_equality
|
||||
a0, h, a1 = points[0:-1:2], points[1::2], points[2::2]
|
||||
# An anchor point is considered the end of a path
|
||||
|
@ -710,7 +713,7 @@ class VMobject(Mobject):
|
|||
is_end[:-1] = is_end[:-1] & ~is_end[1:]
|
||||
return np.array([2 * n for n, end in enumerate(is_end) if end])
|
||||
|
||||
def get_subpath_end_indices(self):
|
||||
def get_subpath_end_indices(self) -> np.ndarray:
|
||||
return self.get_subpath_end_indices_from_points(self.get_points())
|
||||
|
||||
def get_subpaths_from_points(self, points: Vect3Array) -> list[Vect3Array]:
|
||||
|
@ -839,7 +842,7 @@ class VMobject(Mobject):
|
|||
self.data["unit_normal"][:] = normal
|
||||
return normal
|
||||
|
||||
def refresh_unit_normal(self):
|
||||
def refresh_unit_normal(self) -> Self:
|
||||
self.get_unit_normal()
|
||||
return self
|
||||
|
||||
|
@ -849,20 +852,20 @@ class VMobject(Mobject):
|
|||
axis: Vect3 = OUT,
|
||||
about_point: Vect3 | None = None,
|
||||
**kwargs
|
||||
):
|
||||
) -> Self:
|
||||
super().rotate(angle, axis, about_point, **kwargs)
|
||||
for mob in self.get_family():
|
||||
mob.refresh_unit_normal()
|
||||
return self
|
||||
|
||||
def ensure_positive_orientation(self, recurse=True):
|
||||
def ensure_positive_orientation(self, recurse=True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
if mob.get_unit_normal()[2] < 0:
|
||||
mob.reverse_points()
|
||||
return self
|
||||
|
||||
# Alignment
|
||||
def align_points(self, vmobject: VMobject):
|
||||
def align_points(self, vmobject: VMobject) -> Self:
|
||||
winding = self._use_winding_fill and vmobject._use_winding_fill
|
||||
self.use_winding_fill(winding)
|
||||
vmobject.use_winding_fill(winding)
|
||||
|
@ -870,8 +873,11 @@ class VMobject(Mobject):
|
|||
# If both have fill, and they have the same shape, just
|
||||
# give them the same triangulation so that it's not recalculated
|
||||
# needlessly throughout an animation
|
||||
if not self._use_winding_fill and self.has_fill() \
|
||||
and vmobject.has_fill() and self.has_same_shape_as(vmobject):
|
||||
match_tris = not self._use_winding_fill and \
|
||||
self.has_fill() and \
|
||||
vmobject.has_fill() and \
|
||||
self.has_same_shape_as(vmobject)
|
||||
if match_tris:
|
||||
vmobject.triangulation = self.triangulation
|
||||
return self
|
||||
|
||||
|
@ -884,6 +890,11 @@ class VMobject(Mobject):
|
|||
# Figure out what the subpaths are, and align
|
||||
subpaths1 = self.get_subpaths()
|
||||
subpaths2 = vmobject.get_subpaths()
|
||||
for subpaths in [subpaths1, subpaths2]:
|
||||
subpaths.sort(key=lambda sp: -sum(
|
||||
get_norm(p2 - p1)
|
||||
for p1, p2 in zip(sp, sp[1:])
|
||||
))
|
||||
n_subpaths = max(len(subpaths1), len(subpaths2))
|
||||
|
||||
# Start building new ones
|
||||
|
@ -892,8 +903,7 @@ class VMobject(Mobject):
|
|||
|
||||
def get_nth_subpath(path_list, n):
|
||||
if n >= len(path_list):
|
||||
# Create a null path at the very end
|
||||
return [path_list[-1][-1]] * 3
|
||||
return np.vstack([path_list[0][:-1], path_list[0][::-1]])
|
||||
return path_list[n]
|
||||
|
||||
for n in range(n_subpaths):
|
||||
|
@ -917,22 +927,14 @@ class VMobject(Mobject):
|
|||
mob.get_joint_products()
|
||||
return self
|
||||
|
||||
def invisible_copy(self):
|
||||
result = self.copy()
|
||||
if not result.has_fill() or result.get_num_points() == 0:
|
||||
return result
|
||||
result.append_vectorized_mobject(self.copy().reverse_points())
|
||||
result.set_opacity(0)
|
||||
return result
|
||||
|
||||
def insert_n_curves(self, n: int, recurse: bool = True):
|
||||
def insert_n_curves(self, n: int, recurse: bool = True) -> Self:
|
||||
for mob in self.get_family(recurse):
|
||||
if mob.get_num_curves() > 0:
|
||||
new_points = mob.insert_n_curves_to_point_list(n, mob.get_points())
|
||||
mob.set_points(new_points)
|
||||
return self
|
||||
|
||||
def insert_n_curves_to_point_list(self, n: int, points: Vect3Array):
|
||||
def insert_n_curves_to_point_list(self, n: int, points: Vect3Array) -> Vect3Array:
|
||||
if len(points) == 1:
|
||||
return np.repeat(points, 2 * n + 1, 0)
|
||||
|
||||
|
@ -965,7 +967,7 @@ class VMobject(Mobject):
|
|||
mobject2: VMobject,
|
||||
alpha: float,
|
||||
*args, **kwargs
|
||||
):
|
||||
) -> Self:
|
||||
super().interpolate(mobject1, mobject2, alpha, *args, **kwargs)
|
||||
if self.has_fill() and not self._use_winding_fill:
|
||||
tri1 = mobject1.get_triangulation()
|
||||
|
@ -974,7 +976,7 @@ class VMobject(Mobject):
|
|||
self.refresh_triangulation()
|
||||
return self
|
||||
|
||||
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float):
|
||||
def pointwise_become_partial(self, vmobject: VMobject, a: float, b: float) -> Self:
|
||||
assert(isinstance(vmobject, VMobject))
|
||||
vm_points = vmobject.get_points()
|
||||
self.data["joint_product"] = vmobject.data["joint_product"]
|
||||
|
@ -1017,12 +1019,12 @@ class VMobject(Mobject):
|
|||
self.set_points(new_points, refresh_joints=False)
|
||||
return self
|
||||
|
||||
def get_subcurve(self, a: float, b: float) -> VMobject:
|
||||
def get_subcurve(self, a: float, b: float) -> Self:
|
||||
vmob = self.copy()
|
||||
vmob.pointwise_become_partial(self, a, b)
|
||||
return vmob
|
||||
|
||||
def get_outer_vert_indices(self):
|
||||
def get_outer_vert_indices(self) -> np.ndarray:
|
||||
"""
|
||||
Returns the pattern (0, 1, 2, 2, 3, 4, 4, 5, 6, ...)
|
||||
"""
|
||||
|
@ -1033,12 +1035,12 @@ class VMobject(Mobject):
|
|||
|
||||
# Data for shaders that may need refreshing
|
||||
|
||||
def refresh_triangulation(self):
|
||||
def refresh_triangulation(self) -> Self:
|
||||
for mob in self.get_family():
|
||||
mob.needs_new_triangulation = True
|
||||
return self
|
||||
|
||||
def get_triangulation(self):
|
||||
def get_triangulation(self) -> np.ndarray:
|
||||
# Figure out how to triangulate the interior to know
|
||||
# how to send the points as to the vertex shader.
|
||||
# First triangles come directly from the points
|
||||
|
@ -1095,12 +1097,12 @@ class VMobject(Mobject):
|
|||
self.needs_new_triangulation = False
|
||||
return tri_indices
|
||||
|
||||
def refresh_joint_products(self):
|
||||
def refresh_joint_products(self) -> Self:
|
||||
for mob in self.get_family():
|
||||
mob.needs_new_joint_products = True
|
||||
return self
|
||||
|
||||
def get_joint_products(self, refresh: bool = False):
|
||||
def get_joint_products(self, refresh: bool = False) -> np.ndarray:
|
||||
"""
|
||||
The 'joint product' is a 4-vector holding the cross and dot
|
||||
product between tangent vectors at a joint
|
||||
|
@ -1151,10 +1153,11 @@ class VMobject(Mobject):
|
|||
self.data["joint_product"][:, 3] = (vect_to_vert * vect_from_vert).sum(1)
|
||||
return self.data["joint_product"]
|
||||
|
||||
def lock_matching_data(self, vmobject1: VMobject, vmobject2: VMobject):
|
||||
def lock_matching_data(self, vmobject1: VMobject, vmobject2: VMobject) -> Self:
|
||||
for mob in [self, vmobject1, vmobject2]:
|
||||
mob.get_joint_products()
|
||||
super().lock_matching_data(vmobject1, vmobject2)
|
||||
return self
|
||||
|
||||
def triggers_refreshed_triangulation(func: Callable):
|
||||
@wraps(func)
|
||||
|
@ -1166,7 +1169,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
return wrapper
|
||||
|
||||
def set_points(self, points: Vect3Array, refresh_joints: bool = True):
|
||||
def set_points(self, points: Vect3Array, refresh_joints: bool = True) -> Self:
|
||||
assert(len(points) == 0 or len(points) % 2 == 1)
|
||||
super().set_points(points)
|
||||
self.refresh_triangulation()
|
||||
|
@ -1176,13 +1179,13 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def append_points(self, points: Vect3Array):
|
||||
def append_points(self, points: Vect3Array) -> Self:
|
||||
assert(len(points) % 2 == 0)
|
||||
super().append_points(points)
|
||||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def reverse_points(self, recurse: bool = True):
|
||||
def reverse_points(self, recurse: bool = True) -> Self:
|
||||
# This will reset which anchors are
|
||||
# considered path ends
|
||||
for mob in self.get_family(recurse):
|
||||
|
@ -1195,7 +1198,7 @@ class VMobject(Mobject):
|
|||
return self
|
||||
|
||||
@triggers_refreshed_triangulation
|
||||
def set_data(self, data: np.ndarray):
|
||||
def set_data(self, data: np.ndarray) -> Self:
|
||||
super().set_data(data)
|
||||
return self
|
||||
|
||||
|
@ -1206,15 +1209,25 @@ class VMobject(Mobject):
|
|||
function: Callable[[Vect3], Vect3],
|
||||
make_smooth: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
) -> Self:
|
||||
super().apply_function(function, **kwargs)
|
||||
if self.make_smooth_after_applying_functions or make_smooth:
|
||||
self.make_smooth(approx=True)
|
||||
return self
|
||||
|
||||
def apply_points_function(self, *args, **kwargs):
|
||||
def apply_points_function(self, *args, **kwargs) -> Self:
|
||||
super().apply_points_function(*args, **kwargs)
|
||||
self.refresh_joint_products()
|
||||
return self
|
||||
|
||||
def set_animating_status(self, is_animating: bool, recurse: bool = True):
|
||||
super().set_animating_status(is_animating, recurse)
|
||||
if is_animating:
|
||||
for submob in self.get_family(recurse):
|
||||
submob.get_joint_products(refresh=True)
|
||||
if not submob._use_winding_fill:
|
||||
submob.get_triangulation()
|
||||
return self
|
||||
|
||||
# For shaders
|
||||
def init_shader_data(self, ctx: Context):
|
||||
|
@ -1249,7 +1262,7 @@ class VMobject(Mobject):
|
|||
self.stroke_shader_wrapper,
|
||||
]
|
||||
|
||||
def refresh_shader_wrapper_id(self):
|
||||
def refresh_shader_wrapper_id(self) -> Self:
|
||||
if not self._shaders_initialized:
|
||||
return self
|
||||
for wrapper in self.shader_wrappers:
|
||||
|
@ -1269,43 +1282,40 @@ class VMobject(Mobject):
|
|||
|
||||
# Build up data lists
|
||||
fill_datas = []
|
||||
fill_border_datas = []
|
||||
fill_indices = []
|
||||
fill_border_datas = []
|
||||
stroke_datas = []
|
||||
back_stroke_datas = []
|
||||
for submob in family:
|
||||
submob.get_joint_products()
|
||||
indices = submob.get_outer_vert_indices()
|
||||
has_fill = submob.has_fill()
|
||||
has_stroke = submob.has_stroke()
|
||||
if has_fill:
|
||||
back_stroke = has_stroke and submob.stroke_behind
|
||||
front_stroke = has_stroke and not submob.stroke_behind
|
||||
if back_stroke:
|
||||
back_stroke_datas.append(submob.data[stroke_names][indices])
|
||||
if front_stroke:
|
||||
stroke_datas.append(submob.data[stroke_names][indices])
|
||||
if has_fill and submob._use_winding_fill:
|
||||
data = submob.data[fill_names]
|
||||
data["base_point"][:] = data["point"][0]
|
||||
fill_datas.append(data)
|
||||
if self._use_winding_fill:
|
||||
# Add dummy
|
||||
fill_datas.append(data[-1:])
|
||||
else:
|
||||
fill_indices.append(submob.get_triangulation())
|
||||
fill_datas.append(data[indices])
|
||||
if has_fill and not submob._use_winding_fill:
|
||||
fill_datas.append(submob.data[fill_names])
|
||||
fill_indices.append(submob.get_triangulation())
|
||||
if has_fill and not front_stroke:
|
||||
# Add fill border
|
||||
if not has_stroke:
|
||||
names = list(stroke_names)
|
||||
names[names.index('stroke_rgba')] = 'fill_rgba'
|
||||
names[names.index('stroke_width')] = 'fill_border_width'
|
||||
border_stroke_data = submob.data[names]
|
||||
fill_border_datas.append(border_stroke_data)
|
||||
fill_border_datas.append(border_stroke_data[-1:])
|
||||
if has_stroke:
|
||||
lst = back_stroke_datas if submob.stroke_behind else stroke_datas
|
||||
lst.append(submob.data[stroke_names])
|
||||
# Set data array to be one longer than number of points,
|
||||
# with a dummy vertex added at the end. This is to ensure
|
||||
# it can be safely stacked onto other stroke data arrays.
|
||||
lst.append(submob.data[stroke_names][-1:])
|
||||
names = list(stroke_names)
|
||||
names[names.index('stroke_rgba')] = 'fill_rgba'
|
||||
names[names.index('stroke_width')] = 'fill_border_width'
|
||||
border_stroke_data = submob.data[names].astype(
|
||||
self.stroke_shader_wrapper.vert_data.dtype
|
||||
)
|
||||
fill_border_datas.append(border_stroke_data[indices])
|
||||
|
||||
shader_wrappers = [
|
||||
self.back_stroke_shader_wrapper.read_in(
|
||||
[*back_stroke_datas, *fill_border_datas]
|
||||
),
|
||||
self.back_stroke_shader_wrapper.read_in([*back_stroke_datas, *fill_border_datas]),
|
||||
self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None),
|
||||
self.stroke_shader_wrapper.read_in(stroke_datas),
|
||||
]
|
||||
|
@ -1319,7 +1329,7 @@ class VGroup(VMobject):
|
|||
super().__init__(**kwargs)
|
||||
self.add(*vmobjects)
|
||||
|
||||
def __add__(self, other: VMobject | VGroup):
|
||||
def __add__(self, other: VMobject) -> Self:
|
||||
assert(isinstance(other, VMobject))
|
||||
return self.add(other)
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
|
||||
from typing import Self
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import listify
|
||||
|
||||
|
@ -36,7 +36,7 @@ class ValueTracker(Mobject):
|
|||
return result[0]
|
||||
return result
|
||||
|
||||
def set_value(self, value: float | complex | np.ndarray):
|
||||
def set_value(self, value: float | complex | np.ndarray) -> Self:
|
||||
self.uniforms["value"][:] = value
|
||||
return self
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ from typing import TYPE_CHECKING
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable, Sequence, TypeVar, Tuple
|
||||
import numpy.typing as npt
|
||||
from manimlib.typing import ManimColor, Vect3, VectN, Vect3Array
|
||||
|
||||
from manimlib.mobject.coordinate_systems import CoordinateSystem
|
||||
|
|
|
@ -3,13 +3,15 @@ from __future__ import annotations
|
|||
import itertools as it
|
||||
import numpy as np
|
||||
import pyperclip
|
||||
from IPython.core.getipython import get_ipython
|
||||
|
||||
from manimlib.animation.fading import FadeIn
|
||||
from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL
|
||||
from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER
|
||||
from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR
|
||||
from manimlib.constants import FRAME_WIDTH, SMALL_BUFF
|
||||
from manimlib.constants import FRAME_WIDTH, FRAME_HEIGHT, SMALL_BUFF
|
||||
from manimlib.constants import PI
|
||||
from manimlib.constants import DEGREES
|
||||
from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C
|
||||
from manimlib.mobject.geometry import Line
|
||||
from manimlib.mobject.geometry import Rectangle
|
||||
|
@ -25,10 +27,16 @@ from manimlib.mobject.types.vectorized_mobject import VHighlight
|
|||
from manimlib.mobject.types.vectorized_mobject import VMobject
|
||||
from manimlib.scene.scene import Scene
|
||||
from manimlib.scene.scene import SceneState
|
||||
from manimlib.scene.scene import PAN_3D_KEY
|
||||
from manimlib.utils.family_ops import extract_mobject_family_members
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
from manimlib.utils.tex_file_writing import LatexError
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
|
||||
SELECT_KEY = 's'
|
||||
UNSELECT_KEY = 'u'
|
||||
|
@ -68,7 +76,7 @@ class InteractiveScene(Scene):
|
|||
"""
|
||||
corner_dot_config = dict(
|
||||
color=WHITE,
|
||||
radius=0.025,
|
||||
radius=0.05,
|
||||
glow_factor=2.0,
|
||||
)
|
||||
selection_rectangle_stroke_color = WHITE
|
||||
|
@ -126,7 +134,7 @@ class InteractiveScene(Scene):
|
|||
|
||||
def update_selection_rectangle(self, rect: Rectangle):
|
||||
p1 = rect.fixed_corner
|
||||
p2 = self.mouse_point.get_center()
|
||||
p2 = self.frame.to_fixed_frame_point(self.mouse_point.get_center())
|
||||
rect.set_points_as_corners([
|
||||
p1, np.array([p2[0], p1[1], 0]),
|
||||
p2, np.array([p1[0], p2[1], 0]),
|
||||
|
@ -228,9 +236,6 @@ class InteractiveScene(Scene):
|
|||
super().remove(*mobjects)
|
||||
self.regenerate_selection_search_set()
|
||||
|
||||
# def increment_time(self, dt: float) -> None:
|
||||
# super().increment_time(dt)
|
||||
|
||||
# Related to selection
|
||||
|
||||
def toggle_selection_mode(self):
|
||||
|
@ -273,7 +278,7 @@ class InteractiveScene(Scene):
|
|||
|
||||
def get_corner_dots(self, mobject: Mobject) -> Mobject:
|
||||
dots = DotCloud(**self.corner_dot_config)
|
||||
radius = self.corner_dot_config["radius"]
|
||||
radius = float(self.corner_dot_config["radius"])
|
||||
if mobject.get_depth() < 1e-2:
|
||||
vects = [DL, UL, UR, DR]
|
||||
else:
|
||||
|
@ -339,8 +344,17 @@ class InteractiveScene(Scene):
|
|||
# Functions for keyboard actions
|
||||
|
||||
def copy_selection(self):
|
||||
ids = map(id, self.selection)
|
||||
pyperclip.copy(",".join(map(str, ids)))
|
||||
names = []
|
||||
shell = get_ipython()
|
||||
for mob in self.selection:
|
||||
name = str(id(mob))
|
||||
if shell is None:
|
||||
continue
|
||||
for key, value in shell.user_ns.items():
|
||||
if mob is value:
|
||||
name = key
|
||||
names.append(name)
|
||||
pyperclip.copy(", ".join(names))
|
||||
|
||||
def paste_selection(self):
|
||||
clipboard_str = pyperclip.paste()
|
||||
|
@ -377,7 +391,9 @@ class InteractiveScene(Scene):
|
|||
def enable_selection(self):
|
||||
self.is_selecting = True
|
||||
self.add(self.selection_rectangle)
|
||||
self.selection_rectangle.fixed_corner = self.mouse_point.get_center().copy()
|
||||
self.selection_rectangle.fixed_corner = self.frame.to_fixed_frame_point(
|
||||
self.mouse_point.get_center()
|
||||
)
|
||||
|
||||
def gather_new_selection(self):
|
||||
self.is_selecting = False
|
||||
|
@ -387,7 +403,9 @@ class InteractiveScene(Scene):
|
|||
for mob in reversed(self.get_selection_search_set()):
|
||||
if self.selection_rectangle.is_touching(mob):
|
||||
additions.append(mob)
|
||||
self.add_to_selection(*additions)
|
||||
if self.selection_rectangle.get_arc_length() < 1e-2:
|
||||
break
|
||||
self.toggle_from_selection(*additions)
|
||||
|
||||
def prepare_grab(self):
|
||||
mp = self.mouse_point.get_center()
|
||||
|
@ -447,6 +465,7 @@ class InteractiveScene(Scene):
|
|||
else:
|
||||
self.save_mobject_to_file(self.selection)
|
||||
|
||||
# Key actions
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
super().on_key_press(symbol, modifiers)
|
||||
char = chr(symbol)
|
||||
|
@ -485,6 +504,8 @@ class InteractiveScene(Scene):
|
|||
self.toggle_selection_mode()
|
||||
elif char == "s" and modifiers == COMMAND_MODIFIER:
|
||||
self.save_selection_to_file()
|
||||
elif char == PAN_3D_KEY and modifiers == COMMAND_MODIFIER:
|
||||
self.copy_frame_anim_call()
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
self.nudge_selection(
|
||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
|
@ -507,7 +528,6 @@ class InteractiveScene(Scene):
|
|||
super().on_key_release(symbol, modifiers)
|
||||
if chr(symbol) == SELECT_KEY:
|
||||
self.gather_new_selection()
|
||||
# self.remove(self.crosshair)
|
||||
if chr(symbol) in GRAB_KEYS:
|
||||
self.is_grabbing = False
|
||||
elif chr(symbol) == INFORMATION_KEY:
|
||||
|
@ -516,7 +536,7 @@ class InteractiveScene(Scene):
|
|||
self.prepare_resizing(about_corner=False)
|
||||
|
||||
# Mouse actions
|
||||
def handle_grabbing(self, point: np.ndarray):
|
||||
def handle_grabbing(self, point: Vect3):
|
||||
diff = point - self.mouse_to_selection
|
||||
if self.window.is_key_pressed(ord(GRAB_KEY)):
|
||||
self.selection.move_to(diff)
|
||||
|
@ -525,7 +545,7 @@ class InteractiveScene(Scene):
|
|||
elif self.window.is_key_pressed(ord(Y_GRAB_KEY)):
|
||||
self.selection.set_y(diff[1])
|
||||
|
||||
def handle_resizing(self, point: np.ndarray):
|
||||
def handle_resizing(self, point: Vect3):
|
||||
if not hasattr(self, "scale_about_point"):
|
||||
return
|
||||
vect = point - self.scale_about_point
|
||||
|
@ -545,15 +565,16 @@ class InteractiveScene(Scene):
|
|||
about_point=self.scale_about_point
|
||||
)
|
||||
|
||||
def handle_sweeping_selection(self, point: np.ndarray):
|
||||
def handle_sweeping_selection(self, point: Vect3):
|
||||
mob = self.point_to_mobject(
|
||||
point, search_set=self.get_selection_search_set(),
|
||||
point,
|
||||
search_set=self.get_selection_search_set(),
|
||||
buff=SMALL_BUFF
|
||||
)
|
||||
if mob is not None:
|
||||
self.add_to_selection(mob)
|
||||
|
||||
def choose_color(self, point: np.ndarray):
|
||||
def choose_color(self, point: Vect3):
|
||||
# Search through all mobject on the screen, not just the palette
|
||||
to_search = [
|
||||
sm
|
||||
|
@ -566,9 +587,9 @@ class InteractiveScene(Scene):
|
|||
self.selection.set_color(mob.get_color())
|
||||
self.remove(self.color_palette)
|
||||
|
||||
def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None:
|
||||
def on_mouse_motion(self, point: Vect3, d_point: Vect3) -> None:
|
||||
super().on_mouse_motion(point, d_point)
|
||||
self.crosshair.move_to(point)
|
||||
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
|
||||
if self.is_grabbing:
|
||||
self.handle_grabbing(point)
|
||||
elif self.window.is_key_pressed(ord(RESIZE_KEY)):
|
||||
|
@ -576,17 +597,34 @@ class InteractiveScene(Scene):
|
|||
elif self.window.is_key_pressed(ord(SELECT_KEY)) and self.window.is_key_pressed(SHIFT_SYMBOL):
|
||||
self.handle_sweeping_selection(point)
|
||||
|
||||
def on_mouse_release(self, point: np.ndarray, button: int, mods: int) -> None:
|
||||
def on_mouse_drag(
|
||||
self,
|
||||
point: Vect3,
|
||||
d_point: Vect3,
|
||||
buttons: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
super().on_mouse_drag(point, d_point, buttons, modifiers)
|
||||
self.crosshair.move_to(self.frame.to_fixed_frame_point(point))
|
||||
|
||||
def on_mouse_release(self, point: Vect3, button: int, mods: int) -> None:
|
||||
super().on_mouse_release(point, button, mods)
|
||||
if self.color_palette in self.mobjects:
|
||||
self.choose_color(point)
|
||||
return
|
||||
mobject = self.point_to_mobject(
|
||||
point,
|
||||
search_set=self.get_selection_search_set(),
|
||||
buff=1e-4,
|
||||
)
|
||||
if mobject is not None:
|
||||
self.toggle_from_selection(mobject)
|
||||
else:
|
||||
self.clear_selection()
|
||||
|
||||
# Copying code to recreate state
|
||||
def copy_frame_anim_call(self):
|
||||
frame = self.frame
|
||||
center = frame.get_center()
|
||||
height = frame.get_height()
|
||||
angles = frame.get_euler_angles()
|
||||
|
||||
call = f"self.frame.animate.reorient"
|
||||
call += str(tuple((angles / DEGREES).astype(int)))
|
||||
if any(center != 0):
|
||||
call += f".move_to({list(np.round(center, 2))})"
|
||||
if height != FRAME_HEIGHT:
|
||||
call += ".set_height({:.2f})".format(height)
|
||||
pyperclip.copy(call)
|
||||
|
|
|
@ -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
|
|
@ -19,6 +19,7 @@ from tqdm.auto import tqdm as ProgressDisplay
|
|||
from manimlib.animation.animation import prepare_animation
|
||||
from manimlib.animation.fading import VFadeInThenOut
|
||||
from manimlib.camera.camera import Camera
|
||||
from manimlib.camera.camera_frame import CameraFrame
|
||||
from manimlib.config import get_module
|
||||
from manimlib.constants import ARROW_SYMBOLS
|
||||
from manimlib.constants import DEFAULT_WAIT_TIME
|
||||
|
@ -44,6 +45,7 @@ from typing import TYPE_CHECKING
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Callable, Iterable
|
||||
from manimlib.typing import Vect3
|
||||
|
||||
from PIL.Image import Image
|
||||
|
||||
|
@ -52,19 +54,22 @@ if TYPE_CHECKING:
|
|||
|
||||
PAN_3D_KEY = 'd'
|
||||
FRAME_SHIFT_KEY = 'f'
|
||||
ZOOM_KEY = 'z'
|
||||
RESET_FRAME_KEY = 'r'
|
||||
QUIT_KEY = 'q'
|
||||
|
||||
|
||||
class Scene(object):
|
||||
random_seed: int = 0
|
||||
pan_sensitivity: float = 3.0
|
||||
pan_sensitivity: float = 0.5
|
||||
scroll_sensitivity: float = 20
|
||||
drag_to_pan: bool = True
|
||||
max_num_saved_states: int = 50
|
||||
default_camera_config: dict = dict()
|
||||
default_window_config: dict = dict()
|
||||
default_file_writer_config: dict = dict()
|
||||
samples = 0
|
||||
# Euler angles, in degrees
|
||||
default_frame_orientation = (0, 0)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -110,6 +115,10 @@ class Scene(object):
|
|||
|
||||
# Core state of the scene
|
||||
self.camera: Camera = Camera(**self.camera_config)
|
||||
self.frame: CameraFrame = self.camera.frame
|
||||
self.frame.reorient(*self.default_frame_orientation)
|
||||
self.frame.make_orientation_default()
|
||||
|
||||
self.file_writer = SceneFileWriter(self, **self.file_writer_config)
|
||||
self.mobjects: list[Mobject] = [self.camera.frame]
|
||||
self.render_groups: list[Mobject] = []
|
||||
|
@ -828,9 +837,10 @@ class Scene(object):
|
|||
|
||||
def on_mouse_motion(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray
|
||||
point: Vect3,
|
||||
d_point: Vect3
|
||||
) -> None:
|
||||
assert(self.window is not None)
|
||||
self.mouse_point.move_to(point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point}
|
||||
|
@ -841,25 +851,24 @@ class Scene(object):
|
|||
frame = self.camera.frame
|
||||
# Handle perspective changes
|
||||
if self.window.is_key_pressed(ord(PAN_3D_KEY)):
|
||||
frame.increment_theta(-self.pan_sensitivity * d_point[0])
|
||||
frame.increment_phi(self.pan_sensitivity * d_point[1])
|
||||
ff_d_point = frame.to_fixed_frame_point(d_point, relative=True)
|
||||
ff_d_point *= self.pan_sensitivity
|
||||
frame.increment_theta(-ff_d_point[0])
|
||||
frame.increment_phi(ff_d_point[1])
|
||||
# Handle frame movements
|
||||
elif self.window.is_key_pressed(ord(FRAME_SHIFT_KEY)):
|
||||
shift = -d_point
|
||||
shift[0] *= frame.get_width() / 2
|
||||
shift[1] *= frame.get_height() / 2
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), shift)
|
||||
frame.shift(shift)
|
||||
frame.shift(-d_point)
|
||||
|
||||
def on_mouse_drag(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
d_point: np.ndarray,
|
||||
point: Vect3,
|
||||
d_point: Vect3,
|
||||
buttons: int,
|
||||
modifiers: int
|
||||
) -> None:
|
||||
self.mouse_drag_point.move_to(point)
|
||||
if self.drag_to_pan:
|
||||
self.frame.shift(-d_point)
|
||||
|
||||
event_data = {"point": point, "d_point": d_point, "buttons": buttons, "modifiers": modifiers}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseDragEvent, **event_data)
|
||||
|
@ -868,7 +877,7 @@ class Scene(object):
|
|||
|
||||
def on_mouse_press(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
point: Vect3,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
|
@ -880,7 +889,7 @@ class Scene(object):
|
|||
|
||||
def on_mouse_release(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
point: Vect3,
|
||||
button: int,
|
||||
mods: int
|
||||
) -> None:
|
||||
|
@ -891,22 +900,21 @@ class Scene(object):
|
|||
|
||||
def on_mouse_scroll(
|
||||
self,
|
||||
point: np.ndarray,
|
||||
offset: np.ndarray
|
||||
point: Vect3,
|
||||
offset: Vect3,
|
||||
x_pixel_offset: float,
|
||||
y_pixel_offset: float
|
||||
) -> None:
|
||||
event_data = {"point": point, "offset": offset}
|
||||
propagate_event = EVENT_DISPATCHER.dispatch(EventType.MouseScrollEvent, **event_data)
|
||||
if propagate_event is not None and propagate_event is False:
|
||||
return
|
||||
|
||||
frame = self.camera.frame
|
||||
if self.window.is_key_pressed(ord(ZOOM_KEY)):
|
||||
factor = 1 + np.arctan(10 * offset[1])
|
||||
frame.scale(1 / factor, about_point=point)
|
||||
else:
|
||||
transform = frame.get_inverse_camera_rotation_matrix()
|
||||
shift = np.dot(np.transpose(transform), offset)
|
||||
frame.shift(-20.0 * shift)
|
||||
rel_offset = y_pixel_offset / self.camera.get_pixel_height()
|
||||
self.frame.scale(
|
||||
1 - self.scroll_sensitivity * rel_offset,
|
||||
about_point=point
|
||||
)
|
||||
|
||||
def on_key_release(
|
||||
self,
|
||||
|
@ -1006,3 +1014,14 @@ class SceneState():
|
|||
|
||||
class EndScene(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ThreeDScene(Scene):
|
||||
samples = 4
|
||||
default_frame_orientation = (-30, 70)
|
||||
|
||||
def add(self, *mobjects, set_depth_test: bool = True):
|
||||
for mob in mobjects:
|
||||
if set_depth_test and not mob.is_fixed_in_frame():
|
||||
mob.apply_depth_test()
|
||||
super().add(*mobjects)
|
||||
|
|
|
@ -248,8 +248,8 @@ class ShaderWrapper(object):
|
|||
def generate_vao(self, refresh: bool = True):
|
||||
self.release()
|
||||
# Data buffer
|
||||
vbo = self.vbo = self.get_vertex_buffer_object(refresh)
|
||||
ibo = self.ibo = self.get_index_buffer_object(refresh)
|
||||
vbo = self.get_vertex_buffer_object(refresh)
|
||||
ibo = self.get_index_buffer_object(refresh)
|
||||
|
||||
# Vertex array object
|
||||
self.vao = self.ctx.vertex_array(
|
||||
|
@ -280,12 +280,10 @@ class FillShaderWrapper(ShaderWrapper):
|
|||
self.fill_canvas = get_fill_canvas(self.ctx)
|
||||
|
||||
def render(self):
|
||||
vao = self.vao
|
||||
assert(vao is not None)
|
||||
winding = (len(self.vert_indices) == 0)
|
||||
vao.program['winding'].value = winding
|
||||
self.program['winding'].value = winding
|
||||
if not winding:
|
||||
vao.render(moderngl.TRIANGLES)
|
||||
super().render()
|
||||
return
|
||||
|
||||
original_fbo = self.ctx.fbo
|
||||
|
@ -301,14 +299,13 @@ class FillShaderWrapper(ShaderWrapper):
|
|||
gl.GL_ONE, gl.GL_ONE,
|
||||
)
|
||||
gl.glBlendEquationSeparate(gl.GL_FUNC_ADD, gl.GL_MAX)
|
||||
self.ctx.blend_equation = moderngl.FUNC_ADD, moderngl.MAX
|
||||
|
||||
vao.render(moderngl.TRIANGLE_STRIP)
|
||||
super().render()
|
||||
|
||||
original_fbo.use()
|
||||
gl.glBlendFunc(gl.GL_ONE, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
gl.glBlendEquation(gl.GL_FUNC_ADD)
|
||||
|
||||
texture_vao.render(moderngl.TRIANGLE_STRIP)
|
||||
texture_vao.render()
|
||||
|
||||
gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
|
||||
|
|
|
@ -10,10 +10,10 @@ out vec2 v_im_coords;
|
|||
out float v_opacity;
|
||||
|
||||
// Analog of import for manim only
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
v_im_coords = im_coords;
|
||||
v_opacity = opacity;
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
}
|
23
manimlib/shaders/inserts/emit_gl_Position.glsl
Normal file
23
manimlib/shaders/inserts/emit_gl_Position.glsl
Normal 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;
|
||||
}
|
|
@ -24,14 +24,11 @@ vec4 add_light(vec4 color, vec3 point, vec3 unit_normal){
|
|||
vec3 to_camera = normalize(camera_position - point);
|
||||
vec3 to_light = normalize(light_position - point);
|
||||
|
||||
// Note, this effectively treats surfaces as two-sided
|
||||
// if(dot(to_camera, unit_normal) < 0) unit_normal *= -1;
|
||||
|
||||
float light_to_normal = dot(to_light, unit_normal);
|
||||
// When unit normal points towards light, brighten
|
||||
float bright_factor = max(light_to_normal, 0) * reflectiveness;
|
||||
// For glossy surface, add extra shine if light beam go towards camera
|
||||
vec3 light_reflection = -to_light + 2 * unit_normal * dot(to_light, unit_normal);
|
||||
vec3 light_reflection = reflect(-to_light, unit_normal);
|
||||
float light_to_cam = dot(light_reflection, to_camera);
|
||||
float shine = gloss * exp(-3 * pow(1 - light_to_cam, 2));
|
||||
bright_factor += shine;
|
||||
|
|
|
@ -1,23 +1,23 @@
|
|||
uniform float is_fixed_in_frame;
|
||||
uniform mat4 view;
|
||||
uniform vec2 frame_shape;
|
||||
uniform float focal_distance;
|
||||
|
||||
const vec2 DEFAULT_FRAME_SHAPE = vec2(8.0 * 16.0 / 9.0, 8.0);
|
||||
const float DEFAULT_FRAME_HEIGHT = 8.0;
|
||||
const float ASPECT_RATIO = 16.0 / 9.0;
|
||||
const float X_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT / ASPECT_RATIO;
|
||||
const float Y_SCALE = 2.0 / DEFAULT_FRAME_HEIGHT;
|
||||
|
||||
vec4 get_gl_Position(vec3 point){
|
||||
bool is_fixed = bool(is_fixed_in_frame);
|
||||
void emit_gl_Position(vec3 point){
|
||||
vec4 result = vec4(point, 1.0);
|
||||
if(!is_fixed){
|
||||
if(!bool(is_fixed_in_frame)){
|
||||
result = view * result;
|
||||
}
|
||||
|
||||
vec2 shape = is_fixed ? DEFAULT_FRAME_SHAPE : frame_shape;
|
||||
result.x *= 2.0 / shape.x;
|
||||
result.y *= 2.0 / shape.y;
|
||||
// Essentially a projection matrix
|
||||
result.x *= X_SCALE;
|
||||
result.y *= Y_SCALE;
|
||||
result.z /= focal_distance;
|
||||
result.w = 1.0 - result.z;
|
||||
// Flip and scale to prevent premature clipping
|
||||
result.z *= -0.1;
|
||||
return result;
|
||||
gl_Position = result;
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ uniform vec3 color6;
|
|||
uniform vec3 color7;
|
||||
uniform vec3 color8;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
|
|
@ -6,9 +6,9 @@ out vec3 xyz_coords;
|
|||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
}
|
|
@ -26,8 +26,6 @@ uniform float saturation_factor;
|
|||
uniform float black_for_cycles;
|
||||
uniform float is_parameter_space;
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
|
||||
in vec3 xyz_coords;
|
||||
|
||||
out vec4 frag_color;
|
||||
|
|
|
@ -6,9 +6,9 @@ out vec3 xyz_coords;
|
|||
uniform float scale_factor;
|
||||
uniform vec3 offset;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
xyz_coords = (point - offset) / scale_factor;
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
}
|
|
@ -27,7 +27,7 @@ const vec2 SIMPLE_QUADRATIC[3] = vec2[3](
|
|||
);
|
||||
|
||||
// Analog of import for manim only
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
|
||||
void emit_triangle(vec3 points[3], vec4 v_color[3]){
|
||||
|
@ -41,7 +41,7 @@ void emit_triangle(vec3 points[3], vec4 v_color[3]){
|
|||
uv_coords = SIMPLE_QUADRATIC[i];
|
||||
color = v_color[i];
|
||||
point = points[i];
|
||||
gl_Position = get_gl_Position(points[i]);
|
||||
emit_gl_Position(points[i]);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
|
@ -57,10 +57,6 @@ void emit_simple_triangle(){
|
|||
|
||||
|
||||
void main(){
|
||||
// We use the triangle strip primative, but
|
||||
// actually only need every other strip element
|
||||
if (winding && int(v_vert_index[0]) % 2 == 1) return;
|
||||
|
||||
// Curves are marked as ended when the handle after
|
||||
// the first anchor is set equal to that anchor
|
||||
if (verts[0] == verts[1]) return;
|
||||
|
|
|
@ -26,7 +26,7 @@ float dist_to_curve(){
|
|||
// Evaluate F(x, y) = y - x^2
|
||||
// divide by its gradient's magnitude
|
||||
float Fxy = y0 - x0 * x0;
|
||||
float approx_dist = abs(Fxy) / sqrt(1.0 + 4 * x0 * x0);
|
||||
float approx_dist = abs(Fxy) * inversesqrt(1.0 + 4 * x0 * x0);
|
||||
if(approx_dist < QUICK_DIST_WIDTH) return approx_dist;
|
||||
|
||||
// Otherwise, solve for the minimal distance.
|
||||
|
|
|
@ -13,7 +13,6 @@ in vec3 verts[3];
|
|||
in vec4 v_joint_product[3];
|
||||
in float v_stroke_width[3];
|
||||
in vec4 v_color[3];
|
||||
in float v_vert_index[3];
|
||||
|
||||
out vec4 color;
|
||||
out float uv_stroke_width;
|
||||
|
@ -36,7 +35,7 @@ const float COS_THRESHOLD = 0.99;
|
|||
|
||||
vec3 unit_normal = vec3(0.0, 0.0, 1.0);
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
#INSERT get_xyz_to_uv.glsl
|
||||
#INSERT finalize_color.glsl
|
||||
|
||||
|
@ -90,7 +89,7 @@ vec3 get_perp(int index, vec4 joint_product, vec3 point, vec3 tangent, float aaw
|
|||
*/
|
||||
float buff = 0.5 * v_stroke_width[index] + aaw;
|
||||
// Add correction for sharp angles to prevent weird bevel effects
|
||||
if(joint_product.w < -0.9) buff *= 10 * (joint_product.w + 1.0);
|
||||
if(joint_product.w < -0.75) buff *= 4 * (joint_product.w + 1.0);
|
||||
vec3 normal = get_joint_unit_normal(joint_product);
|
||||
// Set global unit normal
|
||||
unit_normal = normal;
|
||||
|
@ -154,10 +153,6 @@ void get_corners(
|
|||
}
|
||||
|
||||
void main() {
|
||||
// We use the triangle strip primative, but
|
||||
// actually only need every other strip element
|
||||
if (int(v_vert_index[0]) % 2 == 1) return;
|
||||
|
||||
// Curves are marked as ended when the handle after
|
||||
// the first anchor is set equal to that anchor
|
||||
if (verts[0] == verts[1]) return;
|
||||
|
@ -207,7 +202,7 @@ void main() {
|
|||
}
|
||||
|
||||
color = finalize_color(v_color[i / 2], corners[i], unit_normal);
|
||||
gl_Position = get_gl_Position(corners[i]);
|
||||
emit_gl_Position(corners[i]);
|
||||
EmitVertex();
|
||||
}
|
||||
EndPrimitive();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#version 330
|
||||
|
||||
uniform vec2 frame_shape;
|
||||
uniform float frame_scale;
|
||||
uniform float is_fixed_in_frame;
|
||||
|
||||
in vec3 point;
|
||||
in vec4 stroke_rgba;
|
||||
|
@ -14,14 +15,15 @@ out vec3 verts;
|
|||
out vec4 v_joint_product;
|
||||
out float v_stroke_width;
|
||||
out vec4 v_color;
|
||||
out float v_vert_index;
|
||||
|
||||
const float STROKE_WIDTH_CONVERSION = 0.01;
|
||||
|
||||
void main(){
|
||||
verts = point;
|
||||
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width * frame_shape[1] / 8.0;
|
||||
v_stroke_width = STROKE_WIDTH_CONVERSION * stroke_width;
|
||||
if(!bool(is_fixed_in_frame)){
|
||||
v_stroke_width *= frame_scale;
|
||||
}
|
||||
v_joint_product = joint_product;
|
||||
v_color = stroke_rgba;
|
||||
v_vert_index = gl_VertexID;
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
in vec3 point;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
}
|
|
@ -3,20 +3,18 @@
|
|||
uniform vec4 clip_plane;
|
||||
|
||||
in vec3 point;
|
||||
in vec3 du_point;
|
||||
in vec3 dv_point;
|
||||
in vec3 normal;
|
||||
in vec4 rgba;
|
||||
|
||||
out vec4 v_color;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
#INSERT finalize_color.glsl
|
||||
|
||||
void main(){
|
||||
gl_Position = get_gl_Position(point);
|
||||
vec3 normal = get_unit_normal(point, du_point, dv_point);
|
||||
v_color = finalize_color(rgba, point, normal);
|
||||
emit_gl_Position(point);
|
||||
v_color = finalize_color(rgba, point, normalize(normal));
|
||||
|
||||
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
|
||||
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
#version 330
|
||||
|
||||
in vec3 point;
|
||||
in vec3 du_point;
|
||||
in vec3 dv_point;
|
||||
in vec3 normal;
|
||||
in vec2 im_coords;
|
||||
in float opacity;
|
||||
|
||||
|
@ -11,13 +10,13 @@ out vec3 v_normal;
|
|||
out vec2 v_im_coords;
|
||||
out float v_opacity;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
#INSERT get_unit_normal.glsl
|
||||
|
||||
void main(){
|
||||
v_point = point;
|
||||
v_normal = get_unit_normal(point, du_point, dv_point);
|
||||
v_normal = normal;
|
||||
v_im_coords = im_coords;
|
||||
v_opacity = opacity;
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
}
|
|
@ -12,14 +12,14 @@ out float scaled_aaw;
|
|||
out vec3 v_point;
|
||||
out vec3 light_pos;
|
||||
|
||||
#INSERT get_gl_Position.glsl
|
||||
#INSERT emit_gl_Position.glsl
|
||||
|
||||
void main(){
|
||||
v_point = point;
|
||||
color = rgba;
|
||||
scaled_aaw = (anti_alias_width * pixel_size) / radius;
|
||||
|
||||
gl_Position = get_gl_Position(point);
|
||||
emit_gl_Position(point);
|
||||
float z = -10 * gl_Position.z;
|
||||
float scaled_radius = radius * 1.0 / (1.0 - z);
|
||||
gl_PointSize = 2 * ((scaled_radius / pixel_size) + anti_alias_width);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import itertools as it
|
||||
import numpy as np
|
||||
|
||||
|
||||
def merge_dicts_recursively(*dicts):
|
||||
|
@ -29,3 +30,19 @@ def soft_dict_update(d1, d2):
|
|||
for key, value in list(d2.items()):
|
||||
if key not in d1:
|
||||
d1[key] = value
|
||||
|
||||
|
||||
def dict_eq(d1, d2):
|
||||
if len(d1) != len(d2):
|
||||
return False
|
||||
for key in d1:
|
||||
value1 = d1[key]
|
||||
value2 = d2[key]
|
||||
if type(value1) != type(value2):
|
||||
return False
|
||||
if type(d1[key]) == np.ndarray:
|
||||
if any(d1[key] != d2[key]):
|
||||
return False
|
||||
elif d1[key] != d2[key]:
|
||||
return False
|
||||
return True
|
||||
|
|
|
@ -7,8 +7,8 @@ import moderngl
|
|||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
from manimlib.constants import DEFAULT_PIXEL_HEIGHT
|
||||
from manimlib.constants import DEFAULT_PIXEL_WIDTH
|
||||
from manimlib.config import parse_cli
|
||||
from manimlib.config import get_configuration
|
||||
from manimlib.utils.customization import get_customization
|
||||
from manimlib.utils.directories import get_shader_dir
|
||||
from manimlib.utils.file_ops import find_file
|
||||
|
@ -103,7 +103,7 @@ def get_colormap_code(rgb_list: Sequence[float]) -> str:
|
|||
|
||||
|
||||
@lru_cache()
|
||||
def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
|
||||
def get_fill_canvas(ctx: moderngl.Context) -> Tuple[Framebuffer, VertexArray, Tuple[float, float, float]]:
|
||||
"""
|
||||
Because VMobjects with fill are rendered in a funny way, using
|
||||
alpha blending to effectively compute the winding number around
|
||||
|
@ -114,15 +114,14 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
|
|||
which can display that texture as a simple quad onto a screen,
|
||||
along with the rgb value which is meant to be discarded.
|
||||
"""
|
||||
cam_config = get_customization()['camera_resolutions']
|
||||
res_name = cam_config['default_resolution']
|
||||
size = tuple(map(int, cam_config[res_name].split("x")))
|
||||
cam_config = get_configuration(parse_cli())['camera_config']
|
||||
size = (cam_config['pixel_width'], cam_config['pixel_height'])
|
||||
|
||||
# Important to make sure dtype is floating point (not fixed point)
|
||||
# so that alpha values can be negative and are not clipped
|
||||
texture = ctx.texture(size=size, components=4, dtype='f2')
|
||||
depth_buffer = ctx.depth_renderbuffer(size) # TODO, currently not used
|
||||
texture_fbo = ctx.framebuffer(texture, depth_buffer)
|
||||
depth_texture = ctx.depth_texture(size=size)
|
||||
texture_fbo = ctx.framebuffer(texture, depth_texture)
|
||||
|
||||
# We'll paint onto a canvas with initially negative rgbs, and
|
||||
# discard any pixels remaining close to this value. This is
|
||||
|
@ -150,6 +149,7 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
|
|||
#version 330
|
||||
|
||||
uniform sampler2D Texture;
|
||||
uniform sampler2D DepthTexture;
|
||||
uniform vec3 null_rgb;
|
||||
|
||||
in vec2 v_textcoord;
|
||||
|
@ -159,17 +159,21 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
|
|||
|
||||
void main() {
|
||||
color = texture(Texture, v_textcoord);
|
||||
if(color.a == 0) discard;
|
||||
if(distance(color.rgb, null_rgb) < MIN_DIST_TO_NULL) discard;
|
||||
|
||||
// Un-blend from the null value
|
||||
color.rgb -= (1 - color.a) * null_rgb;
|
||||
// Counteract scaling in fill frag
|
||||
color.a *= 1.01;
|
||||
|
||||
//TODO, set gl_FragDepth;
|
||||
gl_FragDepth = texture(DepthTexture, v_textcoord)[0];
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
simple_program['Texture'].value = get_texture_id(texture)
|
||||
simple_program['DepthTexture'].value = get_texture_id(depth_texture)
|
||||
simple_program['null_rgb'].value = null_rgb
|
||||
|
||||
verts = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
|
||||
|
@ -177,5 +181,6 @@ def get_fill_canvas(ctx) -> Tuple[Framebuffer, VertexArray, Tuple[float, float,
|
|||
simple_program,
|
||||
ctx.buffer(verts.astype('f4').tobytes()),
|
||||
'texcoord',
|
||||
mode=moderngl.TRIANGLE_STRIP
|
||||
)
|
||||
return (texture_fbo, fill_texture_vao, null_rgb)
|
||||
|
|
|
@ -16,7 +16,7 @@ def num_tex_symbols(tex: str) -> int:
|
|||
# \begin{array}{cc}, etc.
|
||||
pattern = "|".join(
|
||||
rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?"
|
||||
for s in ["begin", "end", "phantom"]
|
||||
for s in ["begin", "end", "phantom", "text"]
|
||||
)
|
||||
tex = re.sub(pattern, "", tex)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ from moderngl_window.context.pyglet.window import Window as PygletWindow
|
|||
from moderngl_window.timers.clock import Timer
|
||||
from screeninfo import get_monitors
|
||||
|
||||
from manimlib.constants import FRAME_SHAPE
|
||||
from manimlib.utils.customization import get_customization
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
@ -77,17 +78,18 @@ class Window(PygletWindow):
|
|||
py: int,
|
||||
relative: bool = False
|
||||
) -> np.ndarray:
|
||||
pw, ph = self.size
|
||||
fw, fh = self.scene.camera.get_frame_shape()
|
||||
fc = self.scene.camera.get_frame_center()
|
||||
if relative:
|
||||
return np.array([px / pw, py / ph, 0])
|
||||
else:
|
||||
return np.array([
|
||||
fc[0] + px * fw / pw - fw / 2,
|
||||
fc[1] + py * fh / ph - fh / 2,
|
||||
0
|
||||
])
|
||||
if not hasattr(self.scene, "frame"):
|
||||
return np.zeros(3)
|
||||
|
||||
pixel_shape = np.array(self.size)
|
||||
fixed_frame_shape = np.array(FRAME_SHAPE)
|
||||
frame = self.scene.frame
|
||||
|
||||
coords = np.zeros(3)
|
||||
coords[:2] = (fixed_frame_shape / pixel_shape) * np.array([px, py])
|
||||
if not relative:
|
||||
coords[:2] -= 0.5 * fixed_frame_shape
|
||||
return frame.from_fixed_frame_point(coords, relative)
|
||||
|
||||
def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None:
|
||||
super().on_mouse_motion(x, y, dx, dy)
|
||||
|
@ -115,7 +117,7 @@ class Window(PygletWindow):
|
|||
super().on_mouse_scroll(x, y, x_offset, y_offset)
|
||||
point = self.pixel_coords_to_space_coords(x, y)
|
||||
offset = self.pixel_coords_to_space_coords(x_offset, y_offset, relative=True)
|
||||
self.scene.on_mouse_scroll(point, offset)
|
||||
self.scene.on_mouse_scroll(point, offset, x_offset, y_offset)
|
||||
|
||||
def on_key_press(self, symbol: int, modifiers: int) -> None:
|
||||
self.pressed_keys.add(symbol) # Modifiers?
|
||||
|
|
|
@ -13,6 +13,7 @@ pydub
|
|||
pygments
|
||||
PyOpenGL
|
||||
pyperclip
|
||||
pyrr
|
||||
pyyaml
|
||||
rich
|
||||
scipy
|
||||
|
|
Loading…
Add table
Reference in a new issue