mirror of
https://github.com/3b1b/manim.git
synced 2025-08-05 16:49:03 +00:00
commit
15e5d8a07a
15 changed files with 234 additions and 63 deletions
|
@ -4,6 +4,7 @@ from copy import deepcopy
|
|||
|
||||
from manimlib.mobject.mobject import _AnimationBuilder
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
from manimlib.utils.iterables import remove_list_redundancies
|
||||
from manimlib.utils.rate_functions import smooth
|
||||
from manimlib.utils.rate_functions import squish_rate_func
|
||||
from manimlib.utils.simple_functions import clip
|
||||
|
@ -37,7 +38,10 @@ class Animation(object):
|
|||
remover: bool = False,
|
||||
# What to enter into the update function upon completion
|
||||
final_alpha_value: float = 1.0,
|
||||
suspend_mobject_updating: bool = True,
|
||||
# If set to True, the mobject itself will have its internal updaters called,
|
||||
# but the start or target mobjects would not be suspended. To completely suspend
|
||||
# updating, call mobject.suspend_updating() before the animation
|
||||
suspend_mobject_updating: bool = False,
|
||||
):
|
||||
self.mobject = mobject
|
||||
self.run_time = run_time
|
||||
|
@ -65,12 +69,6 @@ class Animation(object):
|
|||
self.mobject.set_animating_status(True)
|
||||
self.starting_mobject = self.create_starting_mobject()
|
||||
if self.suspend_mobject_updating:
|
||||
# All calls to self.mobject's internal updaters
|
||||
# during the animation, either from this Animation
|
||||
# or from the surrounding scene, should do nothing.
|
||||
# It is, however, okay and desirable to call
|
||||
# the internal updaters of self.starting_mobject,
|
||||
# or any others among self.get_all_mobjects()
|
||||
self.mobject_was_updating = not self.mobject.updating_suspended
|
||||
self.mobject.suspend_updating()
|
||||
self.families = list(self.get_all_families_zipped())
|
||||
|
@ -105,23 +103,19 @@ class Animation(object):
|
|||
def update_mobjects(self, dt: float) -> None:
|
||||
"""
|
||||
Updates things like starting_mobject, and (for
|
||||
Transforms) target_mobject. Note, since typically
|
||||
(always?) self.mobject will have its updating
|
||||
suspended during the animation, this will do
|
||||
nothing to self.mobject.
|
||||
Transforms) target_mobject.
|
||||
"""
|
||||
for mob in self.get_all_mobjects_to_update():
|
||||
mob.update(dt)
|
||||
|
||||
def get_all_mobjects_to_update(self) -> list[Mobject]:
|
||||
# The surrounding scene typically handles
|
||||
# updating of self.mobject. Besides, in
|
||||
# most cases its updating is suspended anyway
|
||||
# updating of self.mobject.
|
||||
items = list(filter(
|
||||
lambda m: m is not self.mobject,
|
||||
self.get_all_mobjects()
|
||||
))
|
||||
items = list(set(items))
|
||||
items = remove_list_redundancies(items)
|
||||
return items
|
||||
|
||||
def copy(self):
|
||||
|
|
|
@ -142,6 +142,7 @@ class Mobject(object):
|
|||
self.uniforms: UniformDict = {
|
||||
"is_fixed_in_frame": 0.0,
|
||||
"shading": np.array(self.shading, dtype=float),
|
||||
"clip_plane": np.zeros(4),
|
||||
}
|
||||
|
||||
def init_colors(self):
|
||||
|
@ -1946,6 +1947,21 @@ class Mobject(object):
|
|||
mob.depth_test = False
|
||||
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) -> Self:
|
||||
self.uniforms["clip_plane"][:] = 0
|
||||
return self
|
||||
|
||||
# Shader code manipulation
|
||||
|
||||
@affects_data
|
||||
|
|
|
@ -15,7 +15,6 @@ if TYPE_CHECKING:
|
|||
from manimlib.typing import ManimColor, Vect3
|
||||
|
||||
|
||||
|
||||
class BulletedList(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import moderngl
|
||||
from PIL import Image
|
||||
from moderngl import TRIANGLES
|
||||
|
||||
from manimlib.constants import DL, DR, UL, UR
|
||||
from manimlib.mobject.mobject import Mobject
|
||||
|
@ -25,7 +25,7 @@ class ImageMobject(Mobject):
|
|||
('im_coords', np.float32, (2,)),
|
||||
('opacity', np.float32, (1,)),
|
||||
]
|
||||
render_primitive: int = TRIANGLES
|
||||
render_primitive: int = moderngl.TRIANGLES
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
|
|
@ -65,10 +65,6 @@ class Surface(Mobject):
|
|||
)
|
||||
self.compute_triangle_indices()
|
||||
|
||||
def init_uniforms(self):
|
||||
super().init_uniforms()
|
||||
self.uniforms["clip_plane"] = np.zeros(4)
|
||||
|
||||
def uv_func(self, u: float, v: float) -> tuple[float, float, float]:
|
||||
# To be implemented in subclasses
|
||||
return (u, v, 0.0)
|
||||
|
@ -216,21 +212,6 @@ class Surface(Mobject):
|
|||
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) -> Self:
|
||||
self.uniforms["clip_plane"][:] = 0
|
||||
return self
|
||||
|
||||
def get_shader_vert_indices(self) -> np.ndarray:
|
||||
return self.get_triangle_indices()
|
||||
|
||||
|
|
|
@ -767,6 +767,8 @@ class VMobject(Mobject):
|
|||
def quick_point_from_proportion(self, alpha: float) -> Vect3:
|
||||
# Assumes all curves have the same length, so is inaccurate
|
||||
num_curves = self.get_num_curves()
|
||||
if num_curves == 0:
|
||||
return self.get_center()
|
||||
n, residue = integer_interpolate(0, num_curves, alpha)
|
||||
curve_func = self.get_nth_curve_function(n)
|
||||
return curve_func(residue)
|
||||
|
|
|
@ -5,7 +5,8 @@ import itertools as it
|
|||
import numpy as np
|
||||
|
||||
from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH
|
||||
from manimlib.constants import WHITE
|
||||
from manimlib.constants import BLUE, WHITE
|
||||
from manimlib.constants import ORIGIN
|
||||
from manimlib.animation.indication import VShowPassingFlash
|
||||
from manimlib.mobject.geometry import Arrow
|
||||
from manimlib.mobject.types.vectorized_mobject import VGroup
|
||||
|
@ -15,6 +16,7 @@ from manimlib.utils.bezier import inverse_interpolate
|
|||
from manimlib.utils.color import get_colormap_list
|
||||
from manimlib.utils.color import rgb_to_color
|
||||
from manimlib.utils.dict_ops import merge_dicts_recursively
|
||||
from manimlib.utils.iterables import cartesian_product
|
||||
from manimlib.utils.rate_functions import linear
|
||||
from manimlib.utils.simple_functions import sigmoid
|
||||
from manimlib.utils.space_ops import get_norm
|
||||
|
@ -118,7 +120,184 @@ def get_sample_points_from_coordinate_system(
|
|||
|
||||
# Mobjects
|
||||
|
||||
class VectorField(VGroup):
|
||||
|
||||
class VectorField(VMobject):
|
||||
def __init__(
|
||||
self,
|
||||
func,
|
||||
stroke_color: ManimColor = BLUE,
|
||||
stroke_opacity: float = 1.0,
|
||||
center: Vect3 = ORIGIN,
|
||||
sample_points: Optional[Vect3Array] = None,
|
||||
x_density: float = 2.0,
|
||||
y_density: float = 2.0,
|
||||
z_density: float = 2.0,
|
||||
width: float = 14.0,
|
||||
height: float = 8.0,
|
||||
depth: float = 0.0,
|
||||
stroke_width: float = 2,
|
||||
tip_width_ratio: float = 4,
|
||||
tip_len_to_width: float = 0.01,
|
||||
max_vect_len: float | None = None,
|
||||
min_drawn_norm: float = 1e-2,
|
||||
flat_stroke: bool = False,
|
||||
norm_to_opacity_func=None,
|
||||
norm_to_rgb_func=None,
|
||||
**kwargs
|
||||
):
|
||||
self.func = func
|
||||
self.stroke_width = stroke_width
|
||||
self.tip_width_ratio = tip_width_ratio
|
||||
self.tip_len_to_width = tip_len_to_width
|
||||
self.min_drawn_norm = min_drawn_norm
|
||||
self.norm_to_opacity_func = norm_to_opacity_func
|
||||
self.norm_to_rgb_func = norm_to_rgb_func
|
||||
|
||||
if max_vect_len is not None:
|
||||
self.max_vect_len = max_vect_len
|
||||
else:
|
||||
densities = np.array([x_density, y_density, z_density])
|
||||
dims = np.array([width, height, depth])
|
||||
self.max_vect_len = 1.0 / densities[dims > 0].mean()
|
||||
|
||||
if sample_points is None:
|
||||
self.sample_points = self.get_sample_points(
|
||||
center, width, height, depth,
|
||||
x_density, y_density, z_density
|
||||
)
|
||||
else:
|
||||
self.sample_points = sample_points
|
||||
|
||||
self.init_base_stroke_width_array(len(self.sample_points))
|
||||
|
||||
super().__init__(
|
||||
stroke_color=stroke_color,
|
||||
stroke_opacity=stroke_opacity,
|
||||
flat_stroke=flat_stroke,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
n_samples = len(self.sample_points)
|
||||
self.set_points(np.zeros((8 * n_samples - 1, 3)))
|
||||
self.set_stroke(width=stroke_width)
|
||||
self.set_joint_type('no_joint')
|
||||
self.update_vectors()
|
||||
|
||||
def get_sample_points(
|
||||
self,
|
||||
center: np.ndarray,
|
||||
width: float,
|
||||
height: float,
|
||||
depth: float,
|
||||
x_density: float,
|
||||
y_density: float,
|
||||
z_density: float
|
||||
) -> np.ndarray:
|
||||
to_corner = np.array([width / 2, height / 2, depth / 2])
|
||||
spacings = 1.0 / np.array([x_density, y_density, z_density])
|
||||
to_corner = spacings * (to_corner / spacings).astype(int)
|
||||
lower_corner = center - to_corner
|
||||
upper_corner = center + to_corner + spacings
|
||||
return cartesian_product(*(
|
||||
np.arange(low, high, space)
|
||||
for low, high, space in zip(lower_corner, upper_corner, spacings)
|
||||
))
|
||||
|
||||
def init_base_stroke_width_array(self, n_sample_points):
|
||||
arr = np.ones(8 * n_sample_points - 1)
|
||||
arr[4::8] = self.tip_width_ratio
|
||||
arr[5::8] = self.tip_width_ratio * 0.5
|
||||
arr[6::8] = 0
|
||||
arr[7::8] = 0
|
||||
self.base_stroke_width_array = arr
|
||||
|
||||
def set_sample_points(self, sample_points: Vect3Array):
|
||||
self.sample_points = sample_points
|
||||
return self
|
||||
|
||||
def set_stroke(self, color=None, width=None, opacity=None, behind=None, flat=None, recurse=True):
|
||||
super().set_stroke(color, None, opacity, behind, flat, recurse)
|
||||
if width is not None:
|
||||
self.set_stroke_width(float(width))
|
||||
return self
|
||||
|
||||
def set_stroke_width(self, width: float):
|
||||
if self.get_num_points() > 0:
|
||||
self.get_stroke_widths()[:] = width * self.base_stroke_width_array
|
||||
self.stroke_width = width
|
||||
return self
|
||||
|
||||
def update_vectors(self):
|
||||
tip_width = self.tip_width_ratio * self.stroke_width
|
||||
tip_len = self.tip_len_to_width * tip_width
|
||||
samples = self.sample_points
|
||||
|
||||
# Get raw outputs and lengths
|
||||
outputs = self.func(samples)
|
||||
norms = np.linalg.norm(outputs, axis=1)[:, np.newaxis]
|
||||
|
||||
# How long should the arrows be drawn?
|
||||
max_len = self.max_vect_len
|
||||
if max_len < np.inf:
|
||||
drawn_norms = max_len * np.tanh(norms / max_len)
|
||||
else:
|
||||
drawn_norms = norms
|
||||
|
||||
# What's the distance from the base of an arrow to
|
||||
# the base of its head?
|
||||
dist_to_head_base = np.clip(drawn_norms - tip_len, 0, np.inf)
|
||||
|
||||
# Set all points
|
||||
unit_outputs = np.zeros_like(outputs)
|
||||
np.true_divide(outputs, norms, out=unit_outputs, where=(norms > self.min_drawn_norm))
|
||||
|
||||
points = self.get_points()
|
||||
points[0::8] = samples
|
||||
points[2::8] = samples + dist_to_head_base * unit_outputs
|
||||
points[4::8] = points[2::8]
|
||||
points[6::8] = samples + drawn_norms * unit_outputs
|
||||
for i in (1, 3, 5):
|
||||
points[i::8] = 0.5 * (points[i - 1::8] + points[i + 1::8])
|
||||
points[7::8] = points[6:-1:8]
|
||||
|
||||
# Adjust stroke widths
|
||||
width_arr = self.stroke_width * self.base_stroke_width_array
|
||||
width_scalars = np.clip(drawn_norms / tip_len, 0, 1)
|
||||
width_scalars = np.repeat(width_scalars, 8)[:-1]
|
||||
self.get_stroke_widths()[:] = width_scalars * width_arr
|
||||
|
||||
# Potentially adjust opacity and color
|
||||
if self.norm_to_opacity_func is not None:
|
||||
self.get_stroke_opacities()[:] = self.norm_to_opacity_func(
|
||||
np.repeat(norms, 8)[:-1]
|
||||
)
|
||||
if self.norm_to_rgb_func is not None:
|
||||
self.get_stroke_colors()
|
||||
self.data['stroke_rgba'][:, :3] = self.norm_to_rgb_func(
|
||||
np.repeat(norms, 8)[:-1]
|
||||
)
|
||||
|
||||
self.note_changed_data()
|
||||
return self
|
||||
|
||||
|
||||
class TimeVaryingVectorField(VectorField):
|
||||
def __init__(
|
||||
self,
|
||||
# Takes in an array of points and a float for time
|
||||
time_func,
|
||||
**kwargs
|
||||
):
|
||||
self.time = 0
|
||||
super().__init__(func=lambda p: time_func(p, self.time), **kwargs)
|
||||
self.add_updater(lambda m, dt: m.increment_time(dt))
|
||||
always(self.update_vectors)
|
||||
|
||||
def increment_time(self, dt):
|
||||
self.time += dt
|
||||
|
||||
|
||||
class OldVectorField(VGroup):
|
||||
def __init__(
|
||||
self,
|
||||
func: Callable[[float, float], Sequence[float]],
|
||||
|
|
|
@ -507,6 +507,8 @@ class InteractiveScene(Scene):
|
|||
self.save_selection_to_file()
|
||||
elif char == "d" and modifiers == SHIFT_MODIFIER:
|
||||
self.copy_frame_positioning()
|
||||
elif char == "c" and modifiers == SHIFT_MODIFIER:
|
||||
self.copy_cursor_position()
|
||||
elif symbol in ARROW_SYMBOLS:
|
||||
self.nudge_selection(
|
||||
vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)],
|
||||
|
@ -631,3 +633,6 @@ class InteractiveScene(Scene):
|
|||
call += ", {:.2f}".format(height)
|
||||
call += ")"
|
||||
pyperclip.copy(call)
|
||||
|
||||
def copy_cursor_position(self):
|
||||
pyperclip.copy(str(tuple(self.mouse_point.get_center().round(2))))
|
||||
|
|
|
@ -255,17 +255,6 @@ class Scene(object):
|
|||
pt_inputhooks.register("manim", inputhook)
|
||||
shell.enable_gui("manim")
|
||||
|
||||
# This is hacky, but there's an issue with ipython which is that
|
||||
# when you define lambda's or list comprehensions during a shell session,
|
||||
# they are not aware of local variables in the surrounding scope. Because
|
||||
# That comes up a fair bit during scene construction, to get around this,
|
||||
# we (admittedly sketchily) update the global namespace to match the local
|
||||
# namespace, since this is just a shell session anyway.
|
||||
shell.events.register(
|
||||
"pre_run_cell",
|
||||
lambda *args, **kwargs: shell.user_global_ns.update(shell.user_ns)
|
||||
)
|
||||
|
||||
# Operation to run after each ipython command
|
||||
def post_cell_func(*args, **kwargs):
|
||||
if not self.is_window_closing():
|
||||
|
|
|
@ -9,6 +9,7 @@ import sys
|
|||
import numpy as np
|
||||
from pydub import AudioSegment
|
||||
from tqdm.auto import tqdm as ProgressDisplay
|
||||
from pathlib import Path
|
||||
|
||||
from manimlib.constants import FFMPEG_BIN
|
||||
from manimlib.logger import log
|
||||
|
@ -299,15 +300,21 @@ class SceneFileWriter(object):
|
|||
self.video_codec = "libx264rgb"
|
||||
self.pixel_format = "rgb32"
|
||||
|
||||
def get_insert_file_path(self, index: int) -> Path:
|
||||
movie_path = Path(self.get_movie_file_path())
|
||||
scene_name = movie_path.stem
|
||||
insert_dir = Path(movie_path.parent, "inserts")
|
||||
guarantee_existence(str(insert_dir))
|
||||
return Path(insert_dir, f"{scene_name}_{index}{movie_path.suffix}")
|
||||
|
||||
def begin_insert(self):
|
||||
# Begin writing process
|
||||
self.write_to_movie = True
|
||||
self.init_output_directories()
|
||||
movie_path = self.get_movie_file_path()
|
||||
count = 0
|
||||
while os.path.exists(name := movie_path.replace(".", f"_insert_{count}.")):
|
||||
count += 1
|
||||
self.inserted_file_path = name
|
||||
index = 0
|
||||
while (insert_path := self.get_insert_file_path(index)).exists():
|
||||
index += 1
|
||||
self.inserted_file_path = str(insert_path)
|
||||
self.open_movie_pipe(self.inserted_file_path)
|
||||
|
||||
def end_insert(self):
|
||||
|
|
|
@ -2,6 +2,7 @@ uniform float is_fixed_in_frame;
|
|||
uniform mat4 view;
|
||||
uniform float focal_distance;
|
||||
uniform vec3 frame_rescale_factors;
|
||||
uniform vec4 clip_plane;
|
||||
|
||||
void emit_gl_Position(vec3 point){
|
||||
vec4 result = vec4(point, 1.0);
|
||||
|
@ -13,4 +14,8 @@ void emit_gl_Position(vec3 point){
|
|||
// Flip and scale to prevent premature clipping
|
||||
result.z *= -0.1;
|
||||
gl_Position = result;
|
||||
|
||||
if(clip_plane.xyz != vec3(0.0, 0.0, 0.0)){
|
||||
gl_ClipDistance[0] = dot(vec4(point, 1.0), clip_plane);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#version 330
|
||||
|
||||
uniform vec4 clip_plane;
|
||||
|
||||
in vec3 point;
|
||||
in vec3 du_point;
|
||||
in vec3 dv_point;
|
||||
|
@ -17,8 +15,4 @@ void main(){
|
|||
emit_gl_Position(point);
|
||||
vec3 normal = cross(normalize(du_point - point), normalize(dv_point - 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);
|
||||
}
|
||||
}
|
|
@ -27,6 +27,6 @@ def index_labels(
|
|||
label = Integer(n)
|
||||
label.set_height(label_height)
|
||||
label.move_to(submob)
|
||||
label.set_stroke(BLACK, 5, background=True)
|
||||
label.set_backstroke(BLACK, 5)
|
||||
labels.add(label)
|
||||
return labels
|
||||
|
|
|
@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
|||
|
||||
def remove_list_redundancies(lst: Sequence[T]) -> list[T]:
|
||||
"""
|
||||
Used instead of list(set(l)) to maintain order
|
||||
Remove duplicate elements while preserving order.
|
||||
Keeps the last occurrence of each element
|
||||
"""
|
||||
return list(reversed(dict.fromkeys(reversed(lst))))
|
||||
|
|
|
@ -67,7 +67,7 @@ def set_program_uniform(
|
|||
uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid]
|
||||
|
||||
if type(value) is np.ndarray and value.ndim > 0:
|
||||
value = tuple(value)
|
||||
value = tuple(value.flatten())
|
||||
if uniform_mirror.get(name, None) == value:
|
||||
return False
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue