Merge branch 'main' of https://github.com/ManimCommunity/manim into experimental

This commit is contained in:
JasonGrace2282 2024-07-15 09:56:17 -04:00
commit d6ea8412d6
No known key found for this signature in database
GPG key ID: 8D61FE3F93FB15FA
38 changed files with 1587 additions and 470 deletions

View file

@ -36,9 +36,9 @@ RUN pip install -r docs/requirements.txt
ARG NB_USER=manimuser
ARG NB_UID=1000
ENV USER ${NB_USER}
ENV NB_UID ${NB_UID}
ENV HOME /manim
ENV USER=${NB_USER}
ENV NB_UID=${NB_UID}
ENV HOME=/manim
RUN adduser --disabled-password \
--gecos "Default user" \

View file

@ -759,7 +759,6 @@ its :class:`.FileWriter` to open an output container. The process
is started by a call to ``libav`` and opens a container to which rendered
raw frames can be written. As long as the output is open, the container
can be accessed via the ``output_container`` attribute of the file writer.
With the writing process in place, the renderer then asks the scene
to "begin" the animations.
@ -904,7 +903,6 @@ method is called.
would be slightly longer than 1 second. We decided against this at some point.
In the end, the time progression is closed (which completes the displayed progress bar)
in the terminal.
This pretty much concludes the walkthrough of a :class:`.Scene.play` call,
and actually there is not too much more to say for our toy example either: at

View file

@ -99,6 +99,11 @@ def make_logger(
logger = logging.getLogger("manim")
logger.addHandler(rich_handler)
logger.setLevel(verbosity)
logger.propagate = False
if not (libav_logger := logging.getLogger()).hasHandlers():
libav_logger.addHandler(rich_handler)
libav_logger.setLevel(verbosity)
if not (libav_logger := logging.getLogger()).hasHandlers():
libav_logger.addHandler(rich_handler)

View file

@ -40,6 +40,8 @@ if TYPE_CHECKING:
__all__ = ["config_file_paths", "make_config_parser", "ManimConfig", "ManimFrame"]
logger = logging.getLogger("manim")
def config_file_paths() -> list[Path]:
"""The paths where ``.cfg`` files will be searched for.
@ -812,7 +814,7 @@ class ManimConfig(MutableMapping):
try:
self.upto_animation_number = nflag[1]
except Exception:
logging.getLogger("manim").info(
logger.info(
f"No end scene number specified in -n option. Rendering from {nflag[0]} onwards...",
)
@ -1052,7 +1054,7 @@ class ManimConfig(MutableMapping):
val,
["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
logging.getLogger("manim").setLevel(val)
logger.setLevel(val)
@property
def format(self) -> str:
@ -1282,6 +1284,8 @@ class ManimConfig(MutableMapping):
@background_opacity.setter
def background_opacity(self, value: float) -> None:
self._set_between("background_opacity", value, 0, 1)
if self.background_opacity < 1:
self.resolve_movie_file_extension(is_transparent=True)
@property
def frame_size(self) -> tuple[int, int]:
@ -1316,8 +1320,8 @@ class ManimConfig(MutableMapping):
@property
def transparent(self) -> bool:
"""Whether the background opacity is 0.0 (-t)."""
return self._d["background_opacity"] == 0.0
"""Whether the background opacity is less than 1.0 (-t)."""
return self._d["background_opacity"] < 1.0
@transparent.setter
def transparent(self, value: bool) -> None:
@ -1435,6 +1439,7 @@ class ManimConfig(MutableMapping):
self._d.__setitem__("window_size", value)
def resolve_movie_file_extension(self, is_transparent: bool) -> None:
prev_file_extension = self.movie_file_extension
if is_transparent:
self.movie_file_extension = ".webm" if self.format == "webm" else ".mov"
elif self.format == "webm":
@ -1443,6 +1448,11 @@ class ManimConfig(MutableMapping):
self.movie_file_extension = ".mov"
else:
self.movie_file_extension = ".mp4"
if self.movie_file_extension != prev_file_extension:
logger.warning(
f"Output format changed to '{self.movie_file_extension}' "
"to support transparency",
)
@property
def enable_gui(self) -> bool:
@ -1788,7 +1798,7 @@ class ManimConfig(MutableMapping):
def tex_template_file(self, val: str) -> None:
if val:
if not os.access(val, os.R_OK):
logging.getLogger("manim").warning(
logger.warning(
f"Custom TeX template {val} not found or not readable.",
)
else:

View file

@ -20,7 +20,6 @@ if TYPE_CHECKING:
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVGroup
from manim.mobject.types.vectorized_mobject import VGroup
__all__ = ["AnimationGroup", "Succession", "LaggedStart", "LaggedStartMap"]
@ -81,15 +80,16 @@ class AnimationGroup(Animation):
return list(self.group)
def begin(self) -> None:
for anim in self.animations:
anim.begin()
self.process_subanimation_buffer(anim.buffer)
if not self.animations:
raise ValueError(
f"Trying to play {self} without animations, this is not supported. "
"Please add at least one subanimation."
)
for anim in self.animations:
anim.begin()
self.process_subanimation_buffer(anim.buffer)
self.anim_group_time = 0.0
if self.suspend_mobject_updating:
self.group.suspend_updating()

View file

@ -48,6 +48,7 @@ from manim.mobject.geometry.arc import Circle, Dot
from manim.mobject.geometry.line import Line
from manim.mobject.geometry.polygram import Rectangle
from manim.mobject.geometry.shape_matchers import SurroundingRectangle
from manim.scene.scene import Scene
from .. import config
from ..animation.animation import Animation

View file

@ -41,9 +41,7 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
A function without (required) input arguments that returns
a mobject.
Examples
--------
Examples --------
.. manim:: TangentAnimation
class TangentAnimation(Scene):

View file

@ -1,3 +1,5 @@
"""A camera that controls the FOV, orientation, and position of the scene."""
from __future__ import annotations
import math

View file

@ -30,6 +30,8 @@ from manim.utils.module_ops import scene_classes_from_file
__all__ = ["render"]
__all__ = ["render"]
@cloup.command(
context_settings=None,

View file

@ -1,14 +1,15 @@
from __future__ import annotations
import logging
import re
from cloup import Choice, option, option_group
from ... import logger
__all__ = ["global_options"]
logger = logging.getLogger("manim")
def validate_gui_location(ctx, param, value):
if value:
try:

View file

@ -1,12 +1,15 @@
from __future__ import annotations
import logging
import re
from cloup import Choice, option, option_group
from manim.constants import QUALITIES, RendererType
from ... import logger
__all__ = ["render_options"]
logger = logging.getLogger("manim")
__all__ = ["render_options"]

View file

@ -58,7 +58,6 @@ if TYPE_CHECKING:
NonTimeBasedUpdater: TypeAlias = Callable[["Mobject"], object]
Updater: TypeAlias = NonTimeBasedUpdater | TimeBasedUpdater
class Mobject:
"""Mathematical Object: base class for objects that can be displayed on screen.

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import copy
import inspect
import itertools as it
import logging
import numbers
import os
import pickle
@ -17,7 +18,7 @@ import moderngl
import numpy as np
from typing_extensions import TypeVar
from manim import config, logger
from manim import config
from manim.constants import *
from manim.event_handler import EVENT_DISPATCHER
from manim.event_handler.event_listener import EventListener
@ -72,6 +73,8 @@ if TYPE_CHECKING:
R = TypeVar("R", bound="RendererData")
T_co = TypeVar("T_co", covariant=True, bound="OpenGLMobject")
logger = logging.getLogger("manim")
UNIFORM_DTYPE = np.float64

View file

@ -41,11 +41,19 @@ from manim.utils.space_ops import (
)
if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from typing import Callable
from collections.abc import Callable, Iterable, Sequence
from typing_extensions import Self
__all__ = [
"OpenGLVMobject",
"OpenGLVGroup",
"OpenGLVectorizedPoint",
"OpenGLCurvesAsSubmobjects",
"OpenGLDashedVMobject",
]
DEFAULT_STROKE_COLOR = GREY_A
DEFAULT_FILL_COLOR = GREY_C
@ -117,6 +125,10 @@ class OpenGLVMobject(OpenGLMobject):
super().__init__(**kwargs)
# self.refresh_unit_normal()
def _assert_valid_submobjects(self, submobjects: Iterable[OpenGLVMobject]) -> Self:
return self._assert_valid_submobjects_internal(submobjects, OpenGLVMobject)
def get_group_class(self) -> type[OpenGLVGroup]: # type: ignore
return OpenGLVGroup
@ -982,6 +994,52 @@ class OpenGLVMobject(OpenGLMobject):
for n in range(num_curves):
yield self.get_nth_curve_function_with_length(n, **kwargs)
def point_from_proportion(self, alpha: float) -> np.ndarray:
"""Gets the point at a proportion along the path of the :class:`OpenGLVMobject`.
Parameters
----------
alpha
The proportion along the the path of the :class:`OpenGLVMobject`.
Returns
-------
:class:`numpy.ndarray`
The point on the :class:`OpenGLVMobject`.
Raises
------
:exc:`ValueError`
If ``alpha`` is not between 0 and 1.
:exc:`Exception`
If the :class:`OpenGLVMobject` has no points.
"""
if alpha < 0 or alpha > 1:
raise ValueError(f"Alpha {alpha} not between 0 and 1.")
self.throw_error_if_no_points()
if alpha == 1:
return self.points[-1]
curves_and_lengths = tuple(self.get_curve_functions_with_lengths())
target_length = alpha * np.sum(
np.fromiter((length for _, length in curves_and_lengths), dtype=np.float64)
)
current_length = 0
for curve, length in curves_and_lengths:
if current_length + length >= target_length:
if length != 0:
residue = (target_length - current_length) / length
else:
residue = 0
return curve(residue)
current_length += length
def proportion_from_point(
self,
point: Iterable[float | int],
@ -1038,7 +1096,7 @@ class OpenGLVMobject(OpenGLMobject):
return alpha
def get_anchors_and_handles(self):
def get_anchors_and_handles(self) -> Iterable[np.ndarray]:
"""
Returns anchors1, handles, anchors2,
where (anchors1[i], handles[i], anchors2[i])
@ -1070,12 +1128,12 @@ class OpenGLVMobject(OpenGLMobject):
nppc = self.n_points_per_curve
return self.points[nppc - 1 :: nppc]
def get_anchors(self) -> np.ndarray:
def get_anchors(self) -> Iterable[np.ndarray]:
"""Returns the anchors of the curves forming the OpenGLVMobject.
Returns
-------
np.ndarray
Iterable[np.ndarray]
The anchors.
"""
points = self.points
@ -1239,8 +1297,8 @@ class OpenGLVMobject(OpenGLMobject):
return path
for n in range(n_subpaths):
sp1 = get_nth_subpath(subpaths1, n)
sp2 = get_nth_subpath(subpaths2, n)
sp1 = np.asarray(get_nth_subpath(subpaths1, n))
sp2 = np.asarray(get_nth_subpath(subpaths2, n))
diff1 = max(0, (len(sp2) - len(sp1)) // nppc)
diff2 = max(0, (len(sp1) - len(sp2)) // nppc)
sp1 = self.insert_n_curves_to_point_list(diff1, sp1)
@ -1300,6 +1358,7 @@ class OpenGLVMobject(OpenGLMobject):
new_points = new_bezier_tuples.reshape(-1, 3)
return new_points
def interpolate_color(self, mobject1, mobject2, alpha):
attrs = [
"fill_color",
@ -1328,6 +1387,7 @@ class OpenGLVMobject(OpenGLMobject):
setattr(self, attr, getattr(mobject2, attr))
continue
attr1 = getattr(mobject1, attr)
attr2 = getattr(mobject2, attr)
if isinstance(attr1, list) or isinstance(attr2, list):
@ -1503,8 +1563,6 @@ class OpenGLVGroup(OpenGLVMobject):
"""
def __init__(self, *vmobjects, **kwargs):
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
raise Exception("All submobjects must be of type OpenGLVMobject")
super().__init__(**kwargs)
self.add(*vmobjects)
@ -1582,8 +1640,6 @@ class OpenGLVGroup(OpenGLVMobject):
(gr-circle_red).animate.shift(RIGHT)
)
"""
if not all(isinstance(m, OpenGLVMobject) for m in vmobjects):
raise TypeError("All submobjects must be of type OpenGLVMobject")
return super().add(*vmobjects)
def __add__(self, vmobject):
@ -1629,8 +1685,7 @@ class OpenGLVGroup(OpenGLVMobject):
>>> config.renderer = original_renderer
"""
if not all(isinstance(m, OpenGLVMobject) for m in value):
raise TypeError("All submobjects must be of type OpenGLVMobject")
self._assert_valid_submobjects(tuplify(value))
self.submobjects[key] = value # type: ignore

View file

@ -215,7 +215,7 @@ class Surface(VGroup):
def set_fill_by_value(
self,
axes: Mobject,
axes: OpenGLMobject,
colorscale: list[ParsableManimColor] | ParsableManimColor | None = None,
axis: int = 2,
**kwargs,
@ -962,8 +962,8 @@ class Line3D(Cylinder):
def pointify(
self,
mob_or_point: Mobject | Point3D,
direction: Vector3D = None,
mob_or_point: OpenGLMobject | Point3D,
direction: Vector3D | None = None,
) -> np.ndarray:
"""Gets a point representing the center of the :class:`Mobjects <.Mobject>`.
@ -979,7 +979,7 @@ class Line3D(Cylinder):
:class:`numpy.array`
Center of the :class:`Mobjects <.Mobject>` or point, or edge if direction is given.
"""
if isinstance(mob_or_point, (Mobject, OpenGLMobject)):
if isinstance(mob_or_point, OpenGLMobject):
mob = mob_or_point
if direction is None:
return mob.get_center()

File diff suppressed because it is too large Load diff

View file

@ -8,8 +8,8 @@ from .plugins_flags import get_plugins, list_plugins
__all__ = [
"plugins",
"Hooks",
"get_plugins",
"list_plugins",
"get_plugins",
]
requested_plugins: set[str] = set(config["plugins"])
@ -17,4 +17,4 @@ missing_plugins = requested_plugins - set(get_plugins().keys())
if missing_plugins:
logger.warning("Missing Plugins: %s", missing_plugins)
logger.warning(f"Missing Plugins: {missing_plugins}")

View file

@ -11,6 +11,8 @@ from manim.event_handler.window import WindowABC
__all__ = ["Window"]
__all__ = ["Window"]
class Window(PygletWindow, WindowABC):
name = "Manim Community"

View file

@ -1,6 +1,7 @@
from __future__ import annotations
import copy
import logging
import re
from functools import lru_cache
from pathlib import Path
@ -10,8 +11,6 @@ import numpy as np
from manim.utils.iterables import resize_array
from .. import logger
# Mobjects that should be rendered with
# the same shader will be organized and
# clumped together based on keeping track
@ -20,6 +19,8 @@ from .. import logger
__all__ = ["ShaderWrapper"]
logger = logging.getLogger("manim")
def get_shader_dir():
return Path(__file__).parent / "shaders"

View file

@ -0,0 +1,292 @@
from __future__ import annotations
import collections
import numpy as np
from ..utils import opengl
from ..utils.space_ops import cross2d, earclip_triangulation
from .shader import Shader
__all__ = [
"render_opengl_vectorized_mobject_fill",
"render_opengl_vectorized_mobject_stroke",
]
def build_matrix_lists(mob):
root_hierarchical_matrix = mob.hierarchical_model_matrix()
matrix_to_mobject_list = collections.defaultdict(list)
if mob.has_points():
matrix_to_mobject_list[tuple(root_hierarchical_matrix.ravel())].append(mob)
mobject_to_hierarchical_matrix = {mob: root_hierarchical_matrix}
dfs = [mob]
while dfs:
parent = dfs.pop()
for child in parent.submobjects:
child_hierarchical_matrix = (
mobject_to_hierarchical_matrix[parent] @ child.model_matrix
)
mobject_to_hierarchical_matrix[child] = child_hierarchical_matrix
if child.has_points():
matrix_to_mobject_list[tuple(child_hierarchical_matrix.ravel())].append(
child,
)
dfs.append(child)
return matrix_to_mobject_list
def render_opengl_vectorized_mobject_fill(renderer, mobject):
matrix_to_mobject_list = build_matrix_lists(mobject)
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
model_matrix = np.array(matrix_tuple).reshape((4, 4))
render_mobject_fills_with_matrix(renderer, model_matrix, mobject_list)
def render_mobject_fills_with_matrix(renderer, model_matrix, mobjects):
# Precompute the total number of vertices for which to reserve space.
# Note that triangulate_mobject() will cache its results.
total_size = 0
for submob in mobjects:
total_size += triangulate_mobject(submob).shape[0]
attributes = np.empty(
total_size,
dtype=[
("in_vert", np.float32, (3,)),
("in_color", np.float32, (4,)),
("texture_coords", np.float32, (2,)),
("texture_mode", np.int32),
],
)
write_offset = 0
for submob in mobjects:
if not submob.has_points():
continue
mobject_triangulation = triangulate_mobject(submob)
end_offset = write_offset + mobject_triangulation.shape[0]
attributes[write_offset:end_offset] = mobject_triangulation
attributes["in_color"][write_offset:end_offset] = np.repeat(
submob.fill_rgba,
mobject_triangulation.shape[0],
axis=0,
)
write_offset = end_offset
fill_shader = Shader(renderer.context, name="vectorized_mobject_fill")
fill_shader.set_uniform(
"u_model_view_matrix",
opengl.matrix_to_shader_input(
renderer.camera.unformatted_view_matrix @ model_matrix,
),
)
fill_shader.set_uniform(
"u_projection_matrix",
renderer.scene.camera.projection_matrix,
)
vbo = renderer.context.buffer(attributes.tobytes())
vao = renderer.context.simple_vertex_array(
fill_shader.shader_program,
vbo,
*attributes.dtype.names,
)
vao.render()
vao.release()
vbo.release()
def triangulate_mobject(mob):
if not mob.needs_new_triangulation:
return mob.triangulation
# 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
# normal_vector = mob.get_unit_normal()
points = mob.points
b0s = points[0::3]
b1s = points[1::3]
b2s = points[2::3]
v01s = b1s - b0s
v12s = b2s - b1s
crosses = cross2d(v01s, v12s)
convexities = np.sign(crosses)
if mob.orientation == 1:
concave_parts = convexities > 0
convex_parts = convexities <= 0
else:
concave_parts = convexities < 0
convex_parts = convexities >= 0
# These are the vertices to which we'll apply a polygon triangulation
atol = mob.tolerance_for_point_equality
end_of_loop = np.zeros(len(b0s), dtype=bool)
end_of_loop[:-1] = (np.abs(b2s[:-1] - b0s[1:]) > atol).any(1)
end_of_loop[-1] = True
indices = np.arange(len(points), dtype=int)
inner_vert_indices = np.hstack(
[
indices[0::3],
indices[1::3][concave_parts],
indices[2::3][end_of_loop],
],
)
inner_vert_indices.sort()
rings = np.arange(1, len(inner_vert_indices) + 1)[inner_vert_indices % 3 == 2]
# Triangulate
inner_verts = points[inner_vert_indices]
inner_tri_indices = inner_vert_indices[earclip_triangulation(inner_verts, rings)]
bezier_triangle_indices = np.reshape(indices, (-1, 3))
concave_triangle_indices = np.reshape(bezier_triangle_indices[concave_parts], (-1))
convex_triangle_indices = np.reshape(bezier_triangle_indices[convex_parts], (-1))
points = points[
np.hstack(
[
concave_triangle_indices,
convex_triangle_indices,
inner_tri_indices,
],
)
]
texture_coords = np.tile(
[
[0.0, 0.0],
[0.5, 0.0],
[1.0, 1.0],
],
(points.shape[0] // 3, 1),
)
texture_mode = np.hstack(
(
np.ones(concave_triangle_indices.shape[0]),
-1 * np.ones(convex_triangle_indices.shape[0]),
np.zeros(inner_tri_indices.shape[0]),
),
)
attributes = np.zeros(
points.shape[0],
dtype=[
("in_vert", np.float32, (3,)),
("in_color", np.float32, (4,)),
("texture_coords", np.float32, (2,)),
("texture_mode", np.int32),
],
)
attributes["in_vert"] = points
attributes["texture_coords"] = texture_coords
attributes["texture_mode"] = texture_mode
mob.triangulation = attributes
mob.needs_new_triangulation = False
return attributes
def render_opengl_vectorized_mobject_stroke(renderer, mobject):
matrix_to_mobject_list = build_matrix_lists(mobject)
for matrix_tuple, mobject_list in matrix_to_mobject_list.items():
model_matrix = np.array(matrix_tuple).reshape((4, 4))
render_mobject_strokes_with_matrix(renderer, model_matrix, mobject_list)
def render_mobject_strokes_with_matrix(renderer, model_matrix, mobjects):
# Precompute the total number of vertices for which to reserve space.
total_size = 0
for submob in mobjects:
total_size += submob.points.shape[0]
points = np.empty((total_size, 3))
colors = np.empty((total_size, 4))
widths = np.empty(total_size)
write_offset = 0
for submob in mobjects:
if not submob.has_points():
continue
end_offset = write_offset + submob.points.shape[0]
points[write_offset:end_offset] = submob.points
if submob.stroke_rgba.shape[0] == points[write_offset:end_offset].shape[0]:
colors[write_offset:end_offset] = submob.stroke_rgba
else:
colors[write_offset:end_offset] = np.repeat(
submob.stroke_rgba,
submob.points.shape[0],
axis=0,
)
widths[write_offset:end_offset] = np.repeat(
submob.stroke_width,
submob.points.shape[0],
)
write_offset = end_offset
stroke_data = np.zeros(
len(points),
dtype=[
# ("previous_curve", np.float32, (3, 3)),
("current_curve", np.float32, (3, 3)),
# ("next_curve", np.float32, (3, 3)),
("tile_coordinate", np.float32, (2,)),
("in_color", np.float32, (4,)),
("in_width", np.float32),
],
)
stroke_data["in_color"] = colors
stroke_data["in_width"] = widths
curves = np.reshape(points, (-1, 3, 3))
# stroke_data["previous_curve"] = np.repeat(np.roll(curves, 1, axis=0), 3, axis=0)
stroke_data["current_curve"] = np.repeat(curves, 3, axis=0)
# stroke_data["next_curve"] = np.repeat(np.roll(curves, -1, axis=0), 3, axis=0)
# Repeat each vertex in order to make a tile.
stroke_data = np.tile(stroke_data, 2)
stroke_data["tile_coordinate"] = np.vstack(
(
np.tile(
[
[0.0, 0.0],
[0.0, 1.0],
[1.0, 1.0],
],
(len(points) // 3, 1),
),
np.tile(
[
[0.0, 0.0],
[1.0, 0.0],
[1.0, 1.0],
],
(len(points) // 3, 1),
),
),
)
shader = Shader(renderer.context, "vectorized_mobject_stroke")
shader.set_uniform(
"u_model_view_matrix",
opengl.matrix_to_shader_input(
renderer.camera.unformatted_view_matrix @ model_matrix,
),
)
shader.set_uniform("u_projection_matrix", renderer.scene.camera.projection_matrix)
shader.set_uniform("manim_unit_normal", tuple(-mobjects[0].unit_normal[0]))
vbo = renderer.context.buffer(stroke_data.tobytes())
vao = renderer.context.simple_vertex_array(
shader.shader_program, vbo, *stroke_data.dtype.names
)
renderer.frame_buffer_object.use()
vao.render()
vao.release()
vbo.release()

View file

@ -0,0 +1,551 @@
"""A scene suitable for rendering three-dimensional objects and animations."""
from __future__ import annotations
__all__ = ["ThreeDScene", "SpecialThreeDScene"]
import warnings
from collections.abc import Iterable, Sequence
import numpy as np
from manim.mobject.geometry.line import Line
from manim.mobject.graphing.coordinate_systems import ThreeDAxes
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.mobject.three_d.three_dimensions import Sphere
from manim.mobject.value_tracker import ValueTracker
from .. import config
from ..animation.animation import Animation
from ..animation.transform import Transform
from ..camera.three_d_camera import ThreeDCamera
from ..constants import DEGREES, RendererType
from ..mobject.mobject import Mobject
from ..mobject.types.vectorized_mobject import VectorizedPoint, VGroup
from ..renderer.opengl_renderer import OpenGLCamera
from ..scene.scene import Scene
from ..utils.config_ops import merge_dicts_recursively
class ThreeDScene(Scene):
"""
This is a Scene, with special configurations and properties that
make it suitable for Three Dimensional Scenes.
"""
def __init__(
self,
camera_class=ThreeDCamera,
ambient_camera_rotation=None,
default_angled_camera_orientation_kwargs=None,
**kwargs,
):
self.ambient_camera_rotation = ambient_camera_rotation
if default_angled_camera_orientation_kwargs is None:
default_angled_camera_orientation_kwargs = {
"phi": 70 * DEGREES,
"theta": -135 * DEGREES,
}
self.default_angled_camera_orientation_kwargs = (
default_angled_camera_orientation_kwargs
)
super().__init__(camera_class=camera_class, **kwargs)
def set_camera_orientation(
self,
phi: float | None = None,
theta: float | None = None,
gamma: float | None = None,
zoom: float | None = None,
focal_distance: float | None = None,
frame_center: Mobject | Sequence[float] | None = None,
**kwargs,
):
"""
This method sets the orientation of the camera in the scene.
Parameters
----------
phi
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
theta
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
focal_distance
The focal_distance of the Camera.
gamma
The rotation of the camera about the vector from the ORIGIN to the Camera.
zoom
The zoom factor of the scene.
frame_center
The new center of the camera frame in cartesian coordinates.
"""
if phi is not None:
self.renderer.camera.set_phi(phi)
if theta is not None:
self.renderer.camera.set_theta(theta)
if focal_distance is not None:
self.renderer.camera.set_focal_distance(focal_distance)
if gamma is not None:
self.renderer.camera.set_gamma(gamma)
if zoom is not None:
self.renderer.camera.set_zoom(zoom)
if frame_center is not None:
self.renderer.camera._frame_center.move_to(frame_center)
def begin_ambient_camera_rotation(self, rate: float = 0.02, about: str = "theta"):
"""
This method begins an ambient rotation of the camera about the Z_AXIS,
in the anticlockwise direction
Parameters
----------
rate
The rate at which the camera should rotate about the Z_AXIS.
Negative rate means clockwise rotation.
about
one of 3 options: ["theta", "phi", "gamma"]. defaults to theta.
"""
# TODO, use a ValueTracker for rate, so that it
# can begin and end smoothly
about: str = about.lower()
try:
if config.renderer == RendererType.CAIRO:
trackers = {
"theta": self.camera.theta_tracker,
"phi": self.camera.phi_tracker,
"gamma": self.camera.gamma_tracker,
}
x: ValueTracker = trackers[about]
x.add_updater(lambda m, dt: x.increment_value(rate * dt))
self.add(x)
elif config.renderer == RendererType.OPENGL:
cam: OpenGLCamera = self.camera
methods = {
"theta": cam.increment_theta,
"phi": cam.increment_phi,
"gamma": cam.increment_gamma,
}
cam.add_updater(lambda m, dt: methods[about](rate * dt))
self.add(self.camera)
except Exception:
raise ValueError("Invalid ambient rotation angle.")
def stop_ambient_camera_rotation(self, about="theta"):
"""
This method stops all ambient camera rotation.
"""
about: str = about.lower()
try:
if config.renderer == RendererType.CAIRO:
trackers = {
"theta": self.camera.theta_tracker,
"phi": self.camera.phi_tracker,
"gamma": self.camera.gamma_tracker,
}
x: ValueTracker = trackers[about]
x.clear_updaters()
self.remove(x)
elif config.renderer == RendererType.OPENGL:
self.camera.clear_updaters()
except Exception:
raise ValueError("Invalid ambient rotation angle.")
def begin_3dillusion_camera_rotation(
self,
rate: float = 1,
origin_phi: float | None = None,
origin_theta: float | None = None,
):
"""
This method creates a 3D camera rotation illusion around
the current camera orientation.
Parameters
----------
rate
The rate at which the camera rotation illusion should operate.
origin_phi
The polar angle the camera should move around. Defaults
to the current phi angle.
origin_theta
The azimutal angle the camera should move around. Defaults
to the current theta angle.
"""
if origin_theta is None:
origin_theta = self.renderer.camera.theta_tracker.get_value()
if origin_phi is None:
origin_phi = self.renderer.camera.phi_tracker.get_value()
val_tracker_theta = ValueTracker(0)
def update_theta(m, dt):
val_tracker_theta.increment_value(dt * rate)
val_for_left_right = 0.2 * np.sin(val_tracker_theta.get_value())
return m.set_value(origin_theta + val_for_left_right)
self.renderer.camera.theta_tracker.add_updater(update_theta)
self.add(self.renderer.camera.theta_tracker)
val_tracker_phi = ValueTracker(0)
def update_phi(m, dt):
val_tracker_phi.increment_value(dt * rate)
val_for_up_down = 0.1 * np.cos(val_tracker_phi.get_value()) - 0.1
return m.set_value(origin_phi + val_for_up_down)
self.renderer.camera.phi_tracker.add_updater(update_phi)
self.add(self.renderer.camera.phi_tracker)
def stop_3dillusion_camera_rotation(self):
"""
This method stops all illusion camera rotations.
"""
self.renderer.camera.theta_tracker.clear_updaters()
self.remove(self.renderer.camera.theta_tracker)
self.renderer.camera.phi_tracker.clear_updaters()
self.remove(self.renderer.camera.phi_tracker)
def move_camera(
self,
phi: float | None = None,
theta: float | None = None,
gamma: float | None = None,
zoom: float | None = None,
focal_distance: float | None = None,
frame_center: Mobject | Sequence[float] | None = None,
added_anims: Iterable[Animation] = [],
**kwargs,
):
"""
This method animates the movement of the camera
to the given spherical coordinates.
Parameters
----------
phi
The polar angle i.e the angle between Z_AXIS and Camera through ORIGIN in radians.
theta
The azimuthal angle i.e the angle that spins the camera around the Z_AXIS.
focal_distance
The radial focal_distance between ORIGIN and Camera.
gamma
The rotation of the camera about the vector from the ORIGIN to the Camera.
zoom
The zoom factor of the camera.
frame_center
The new center of the camera frame in cartesian coordinates.
added_anims
Any other animations to be played at the same time.
"""
anims = []
if config.renderer == RendererType.CAIRO:
self.camera: ThreeDCamera
value_tracker_pairs = [
(phi, self.camera.phi_tracker),
(theta, self.camera.theta_tracker),
(focal_distance, self.camera.focal_distance_tracker),
(gamma, self.camera.gamma_tracker),
(zoom, self.camera.zoom_tracker),
]
for value, tracker in value_tracker_pairs:
if value is not None:
anims.append(tracker.animate.set_value(value))
if frame_center is not None:
anims.append(self.camera._frame_center.animate.move_to(frame_center))
elif config.renderer == RendererType.OPENGL:
cam: OpenGLCamera = self.camera
cam2 = cam.copy()
methods = {
"theta": cam2.set_theta,
"phi": cam2.set_phi,
"gamma": cam2.set_gamma,
"zoom": cam2.scale,
"frame_center": cam2.move_to,
}
if frame_center is not None:
if isinstance(frame_center, OpenGLMobject):
frame_center = frame_center.get_center()
frame_center = list(frame_center)
zoom_value = None
if zoom is not None:
zoom_value = config.frame_height / (zoom * cam.height)
for value, method in [
[theta, "theta"],
[phi, "phi"],
[gamma, "gamma"],
[zoom_value, "zoom"],
[frame_center, "frame_center"],
]:
if value is not None:
methods[method](value)
if focal_distance is not None:
warnings.warn(
"focal distance of OpenGLCamera can not be adjusted.",
stacklevel=2,
)
anims += [Transform(cam, cam2)]
self.play(*anims + added_anims, **kwargs)
# These lines are added to improve performance. If manim thinks that frame_center is moving,
# it is required to redraw every object. These lines remove frame_center from the Scene once
# its animation is done, ensuring that manim does not think that it is moving. Since the
# frame_center is never actually drawn, this shouldn't break anything.
if frame_center is not None and config.renderer == RendererType.CAIRO:
self.remove(self.camera._frame_center)
def get_moving_mobjects(self, *animations: Animation):
"""
This method returns a list of all of the Mobjects in the Scene that
are moving, that are also in the animations passed.
Parameters
----------
*animations
The animations whose mobjects will be checked.
"""
moving_mobjects = super().get_moving_mobjects(*animations)
camera_mobjects = self.renderer.camera.get_value_trackers() + [
self.renderer.camera._frame_center,
]
if any(cm in moving_mobjects for cm in camera_mobjects):
return self.mobjects
return moving_mobjects
def add_fixed_orientation_mobjects(self, *mobjects: Mobject, **kwargs):
"""
This method is used to prevent the rotation and tilting
of mobjects as the camera moves around. The mobject can
still move in the x,y,z directions, but will always be
at the angle (relative to the camera) that it was at
when it was passed through this method.)
Parameters
----------
*mobjects
The Mobject(s) whose orientation must be fixed.
**kwargs
Some valid kwargs are
use_static_center_func : bool
center_func : function
"""
if config.renderer == RendererType.CAIRO:
self.add(*mobjects)
self.renderer.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs)
elif config.renderer == RendererType.OPENGL:
for mob in mobjects:
mob: OpenGLMobject
mob.fix_orientation()
self.add(mob)
def add_fixed_in_frame_mobjects(self, *mobjects: Mobject):
"""
This method is used to prevent the rotation and movement
of mobjects as the camera moves around. The mobject is
essentially overlaid, and is not impacted by the camera's
movement in any way.
Parameters
----------
*mobjects
The Mobjects whose orientation must be fixed.
"""
if config.renderer == RendererType.CAIRO:
self.add(*mobjects)
self.camera: ThreeDCamera
self.camera.add_fixed_in_frame_mobjects(*mobjects)
elif config.renderer == RendererType.OPENGL:
for mob in mobjects:
mob: OpenGLMobject
mob.fix_in_frame()
self.add(mob)
def remove_fixed_orientation_mobjects(self, *mobjects: Mobject):
"""
This method "unfixes" the orientation of the mobjects
passed, meaning they will no longer be at the same angle
relative to the camera. This only makes sense if the
mobject was passed through add_fixed_orientation_mobjects first.
Parameters
----------
*mobjects
The Mobjects whose orientation must be unfixed.
"""
if config.renderer == RendererType.CAIRO:
self.renderer.camera.remove_fixed_orientation_mobjects(*mobjects)
elif config.renderer == RendererType.OPENGL:
for mob in mobjects:
mob: OpenGLMobject
mob.unfix_orientation()
self.remove(mob)
def remove_fixed_in_frame_mobjects(self, *mobjects: Mobject):
"""
This method undoes what add_fixed_in_frame_mobjects does.
It allows the mobject to be affected by the movement of
the camera.
Parameters
----------
*mobjects
The Mobjects whose position and orientation must be unfixed.
"""
if config.renderer == RendererType.CAIRO:
self.renderer.camera.remove_fixed_in_frame_mobjects(*mobjects)
elif config.renderer == RendererType.OPENGL:
for mob in mobjects:
mob: OpenGLMobject
mob.unfix_from_frame()
self.remove(mob)
##
def set_to_default_angled_camera_orientation(self, **kwargs):
"""
This method sets the default_angled_camera_orientation to the
keyword arguments passed, and sets the camera to that orientation.
Parameters
----------
**kwargs
Some recognised kwargs are phi, theta, focal_distance, gamma,
which have the same meaning as the parameters in set_camera_orientation.
"""
config = dict(
self.default_camera_orientation_kwargs,
) # Where doe this come from?
config.update(kwargs)
self.set_camera_orientation(**config)
class SpecialThreeDScene(ThreeDScene):
"""An extension of :class:`ThreeDScene` with more settings.
It has some extra configuration for axes, spheres,
and an override for low quality rendering. Further key differences
are:
* The camera shades applicable 3DMobjects by default,
except if rendering in low quality.
* Some default params for Spheres and Axes have been added.
"""
def __init__(
self,
cut_axes_at_radius=True,
camera_config={"should_apply_shading": True, "exponential_projection": True},
three_d_axes_config={
"num_axis_pieces": 1,
"axis_config": {
"unit_size": 2,
"tick_frequency": 1,
"numbers_with_elongated_ticks": [0, 1, 2],
"stroke_width": 2,
},
},
sphere_config={"radius": 2, "resolution": (24, 48)},
default_angled_camera_position={
"phi": 70 * DEGREES,
"theta": -110 * DEGREES,
},
# When scene is extracted with -l flag, this
# configuration will override the above configuration.
low_quality_config={
"camera_config": {"should_apply_shading": False},
"three_d_axes_config": {"num_axis_pieces": 1},
"sphere_config": {"resolution": (12, 24)},
},
**kwargs,
):
self.cut_axes_at_radius = cut_axes_at_radius
self.camera_config = camera_config
self.three_d_axes_config = three_d_axes_config
self.sphere_config = sphere_config
self.default_angled_camera_position = default_angled_camera_position
self.low_quality_config = low_quality_config
if self.renderer.camera_config["pixel_width"] == config["pixel_width"]:
_config = {}
else:
_config = self.low_quality_config
_config = merge_dicts_recursively(_config, kwargs)
super().__init__(**_config)
def get_axes(self):
"""Return a set of 3D axes.
Returns
-------
:class:`.ThreeDAxes`
A set of 3D axes.
"""
axes = ThreeDAxes(**self.three_d_axes_config)
for axis in axes:
if self.cut_axes_at_radius:
p0 = axis.get_start()
p1 = axis.number_to_point(-1)
p2 = axis.number_to_point(1)
p3 = axis.get_end()
new_pieces = VGroup(Line(p0, p1), Line(p1, p2), Line(p2, p3))
for piece in new_pieces:
piece.shade_in_3d = True
new_pieces.match_style(axis.pieces)
axis.pieces.submobjects = new_pieces.submobjects
for tick in axis.tick_marks:
tick.add(VectorizedPoint(1.5 * tick.get_center()))
return axes
def get_sphere(self, **kwargs):
"""
Returns a sphere with the passed keyword arguments as properties.
Parameters
----------
**kwargs
Any valid parameter of :class:`~.Sphere` or :class:`~.Surface`.
Returns
-------
:class:`~.Sphere`
The sphere object.
"""
config = merge_dicts_recursively(self.sphere_config, kwargs)
return Sphere(**config)
def get_default_camera_position(self):
"""
Returns the default_angled_camera position.
Returns
-------
dict
Dictionary of phi, theta, focal_distance, and gamma.
"""
return self.default_angled_camera_position
def set_camera_to_default_position(self):
"""
Sets the camera to its default position.
"""
self.set_camera_orientation(**self.default_angled_camera_position)

View file

@ -1148,6 +1148,7 @@ def get_smooth_quadratic_bezier_handle_points(points: Point3D_Array) -> Point3D_
return handles
# Figuring out which Bézier curves most smoothly connect a sequence of points
def get_smooth_cubic_bezier_handle_points(
anchors: Point3D_Array,
) -> tuple[Point3D_Array, Point3D_Array]:

View file

@ -6,13 +6,14 @@ __all__ = ["deprecated", "deprecated_params"]
import inspect
import logging
import re
from collections.abc import Iterable
from typing import Any, Callable
from decorator import decorate, decorator
from .. import logger
logger = logging.getLogger("manim")
def _get_callable_info(callable: Callable) -> tuple[str, str]:

View file

@ -82,7 +82,6 @@ from __future__ import annotations
import csv
import itertools as it
import os
import re
import shutil
import sys

View file

@ -17,16 +17,11 @@ import numpy as np
from .. import config, logger
if typing.TYPE_CHECKING:
from typing_extensions import TypeVar
from manim.animation.protocol import AnimationProtocol
from manim.camera.camera import Camera
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
from manim.scene.scene import Scene
T = TypeVar("T")
S = TypeVar("S", default=str)
__all__ = ["KEYS_TO_FILTER_OUT", "get_hash_from_play_call", "get_json"]
# Sometimes there are elements that are not suitable for hashing (too long or
@ -37,8 +32,6 @@ KEYS_TO_FILTER_OUT = {
"pixel_array",
"pixel_array_to_cairo_context",
}
class _Memoizer:
"""Implements the memoization logic to optimize the hashing procedure and prevent
the circular references within iterable processed.

View file

@ -15,6 +15,8 @@ from manim.renderer.shader import shader_program_cache
__all__ = ["ManimMagic"]
__all__ = ["ManimMagic"]
try:
from IPython import get_ipython
from IPython.core.interactiveshell import InteractiveShell

View file

@ -19,6 +19,8 @@ if TYPE_CHECKING:
from manim.scene.scene import Scene
__all__ = ["scene_classes_from_file"]
__all__ = ["scene_classes_from_file"]

View file

@ -114,17 +114,6 @@ def clip(a, min_a, max_a):
return a
def fdiv(a: float, b: float, zero_over_zero_value: float | None = None) -> float:
if zero_over_zero_value is not None:
out = np.full_like(a, zero_over_zero_value)
where = np.logical_or(a != 0, b != 0)
else:
out = None
where = True
return np.true_divide(a, b, out=out, where=where)
def sigmoid(x: float) -> float:
r"""Returns the output of the logistic function.

View file

@ -1,12 +1,12 @@
from __future__ import annotations
import contextlib
import logging
import warnings
from pathlib import Path
import numpy as np
from manim import logger
from manim.typing import PixelArray
from ._show_diff import show_diff_helper
@ -16,6 +16,8 @@ __all__ = ["_FramesTester", "_ControlDataWriter"]
FRAME_ABSOLUTE_TOLERANCE = 1.01
FRAME_MISMATCH_RATIO_TOLERANCE = 1e-5
logger = logging.getLogger("manim")
class _FramesTester:
def __init__(self, file_path: Path, show_diff=False) -> None:

View file

@ -23,6 +23,7 @@ __all__ = ["frames_comparison"]
SCENE_PARAMETER_NAME = "scene"
_tests_root_dir_path = Path(__file__).absolute().parents[2]
PATH_CONTROL_DATA = _tests_root_dir_path / Path("control_data", "graphical_units_data")
MIN_CAIRO_VERSION = 11800
def frames_comparison(

58
poetry.lock generated
View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
[[package]]
name = "alabaster"
@ -978,13 +978,13 @@ files = [
[[package]]
name = "exceptiongroup"
version = "1.2.1"
version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
files = [
{file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
{file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
]
[package.extras]
@ -1825,13 +1825,13 @@ jupyter-server = ">=1.1.2"
[[package]]
name = "jupyter-server"
version = "2.14.1"
version = "2.14.2"
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
optional = true
python-versions = ">=3.8"
files = [
{file = "jupyter_server-2.14.1-py3-none-any.whl", hash = "sha256:16f7177c3a4ea8fe37784e2d31271981a812f0b2874af17339031dc3510cc2a5"},
{file = "jupyter_server-2.14.1.tar.gz", hash = "sha256:12558d158ec7a0653bf96cc272bc7ad79e0127d503b982ed144399346694f726"},
{file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"},
{file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"},
]
[package.dependencies]
@ -3281,13 +3281,13 @@ urllib3 = ">=1.26.0"
[[package]]
name = "pyglet"
version = "2.0.15"
version = "2.0.16"
description = "pyglet is a cross-platform games and multimedia package."
optional = false
python-versions = ">=3.8"
files = [
{file = "pyglet-2.0.15-py3-none-any.whl", hash = "sha256:9e4cc16efc308106fd3a9ff8f04e7a6f4f6a807c6ac8a331375efbbac8be85af"},
{file = "pyglet-2.0.15.tar.gz", hash = "sha256:42085567cece0c7f1c14e36eef799938cbf528cfbb0150c484b984f3ff1aa771"},
{file = "pyglet-2.0.16-py3-none-any.whl", hash = "sha256:332593c8c14fa2c545a0da3da2f99b8c75c6e822a90cd4ea8e239fad658ff5a1"},
{file = "pyglet-2.0.16.tar.gz", hash = "sha256:af007b22ff5f302edeb2a06d749cfef53f52e67f1e52f0b2babd840d37193482"},
]
[[package]]
@ -3928,29 +3928,29 @@ files = [
[[package]]
name = "ruff"
version = "0.5.1"
version = "0.5.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.5.1-py3-none-linux_armv6l.whl", hash = "sha256:6ecf968fcf94d942d42b700af18ede94b07521bd188aaf2cd7bc898dd8cb63b6"},
{file = "ruff-0.5.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:204fb0a472f00f2e6280a7c8c7c066e11e20e23a37557d63045bf27a616ba61c"},
{file = "ruff-0.5.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d235968460e8758d1e1297e1de59a38d94102f60cafb4d5382033c324404ee9d"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38beace10b8d5f9b6bdc91619310af6d63dd2019f3fb2d17a2da26360d7962fa"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e478d2f09cf06add143cf8c4540ef77b6599191e0c50ed976582f06e588c994"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0368d765eec8247b8550251c49ebb20554cc4e812f383ff9f5bf0d5d94190b0"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3a9a9a1b582e37669b0138b7c1d9d60b9edac880b80eb2baba6d0e566bdeca4d"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bdd9f723e16003623423affabcc0a807a66552ee6a29f90eddad87a40c750b78"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be9fd62c1e99539da05fcdc1e90d20f74aec1b7a1613463ed77870057cd6bd96"},
{file = "ruff-0.5.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e216fc75a80ea1fbd96af94a6233d90190d5b65cc3d5dfacf2bd48c3e067d3e1"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c4c2112e9883a40967827d5c24803525145e7dab315497fae149764979ac7929"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dfaf11c8a116394da3b65cd4b36de30d8552fa45b8119b9ef5ca6638ab964fa3"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d7ceb9b2fe700ee09a0c6b192c5ef03c56eb82a0514218d8ff700f6ade004108"},
{file = "ruff-0.5.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bac6288e82f6296f82ed5285f597713acb2a6ae26618ffc6b429c597b392535c"},
{file = "ruff-0.5.1-py3-none-win32.whl", hash = "sha256:5c441d9c24ec09e1cb190a04535c5379b36b73c4bc20aa180c54812c27d1cca4"},
{file = "ruff-0.5.1-py3-none-win_amd64.whl", hash = "sha256:b1789bf2cd3d1b5a7d38397cac1398ddf3ad7f73f4de01b1e913e2abc7dfc51d"},
{file = "ruff-0.5.1-py3-none-win_arm64.whl", hash = "sha256:2875b7596a740cbbd492f32d24be73e545a4ce0a3daf51e4f4e609962bfd3cd2"},
{file = "ruff-0.5.1.tar.gz", hash = "sha256:3164488aebd89b1745b47fd00604fb4358d774465f20d1fcd907f9c0fc1b0655"},
{file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"},
{file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"},
{file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"},
{file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"},
{file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"},
{file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"},
{file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"},
]
[[package]]

View file

@ -1,5 +1,6 @@
from __future__ import annotations
import logging
import sys
from pathlib import Path
@ -45,6 +46,15 @@ def pytest_collection_modifyitems(config, items):
item.add_marker(slow_skip)
@pytest.fixture
def manim_caplog(caplog):
logger = logging.getLogger("manim")
logger.propagate = True
caplog.set_level(logging.INFO, logger="manim")
yield caplog
logger.propagate = False
@pytest.fixture
def config():
saved = manim.config.copy()

View file

@ -2,6 +2,7 @@
from __future__ import annotations
import logging
import tempfile
from pathlib import Path
@ -10,6 +11,7 @@ import numpy as np
from manim import Manager, logger
from manim.scene.scene import Scene
logger = logging.getLogger("manim")
def set_test_scene(scene_object: type[Scene], module_name: str, config):
"""Function used to set up the test data for a new feature. This will basically set up a pre-rendered frame for a scene. This is meant to be used only

View file

@ -16,22 +16,22 @@ def test_animation_forbidden_run_time(run_time):
test_scene.play(FadeIn(None, run_time=run_time))
def test_animation_run_time_shorter_than_frame_rate(caplog, config):
def test_animation_run_time_shorter_than_frame_rate(manim_caplog, config):
manager = Manager(Scene)
test_scene = manager.scene
test_scene.play(FadeIn(None, run_time=1 / (config.frame_rate + 1)))
assert (
"Original run time of FadeIn(Mobject) is shorter than current frame rate"
in caplog.text
in manim_caplog.text
)
@pytest.mark.parametrize("frozen_frame", [False, True])
def test_wait_run_time_shorter_than_frame_rate(caplog, frozen_frame):
def test_wait_run_time_shorter_than_frame_rate(manim_caplog, frozen_frame):
manager = Manager(Scene)
test_scene = manager.scene
test_scene.wait(1e-9, frozen_frame=frozen_frame)
assert (
"Original run time of Wait(Mobject) is shorter than current frame rate"
in caplog.text
in manim_caplog.text
)

View file

@ -15,8 +15,7 @@ def test_get_arc_center():
)
def test_BackgroundRectangle(caplog):
caplog.set_level(logging.INFO)
def test_BackgroundRectangle(manim_caplog):
c = Circle()
bg = BackgroundRectangle(c)
bg.set_style(fill_opacity=0.42)
@ -24,5 +23,5 @@ def test_BackgroundRectangle(caplog):
bg.set_style(fill_opacity=1, hello="world")
assert (
"Argument {'hello': 'world'} is ignored in BackgroundRectangle.set_style."
in caplog.text
in manim_caplog.text
)

View file

@ -4,7 +4,11 @@ import datetime
import pytest
<<<<<<< HEAD
from manim import Circle, FadeIn, Group, Manager, Mobject, Scene, Square
=======
from manim import Circle, FadeIn, Group, Mobject, Scene, Square
>>>>>>> f1ce5122253882a20b6714adf728eccfa41162e9
from manim.animation.animation import Wait
@ -26,30 +30,14 @@ def test_scene_add_remove(dry_run):
# Check that Scene.add() returns the Scene (for chained calls)
assert scene.add(Mobject()) is scene
to_remove = Mobject()
manager = Manager(Scene)
scene = manager.scene
scene.add(to_remove)
scene.add(*(Mobject() for _ in range(10)))
assert len(scene.mobjects) == 11
scene.remove(to_remove)
assert len(scene.mobjects) == 10
scene.remove(to_remove)
assert len(scene.mobjects) == 10
# Check that Scene.remove() returns the instance (for chained calls)
assert scene.add(Mobject()) is scene
def test_scene_time(dry_run):
manager = Manager(Scene)
scene = manager.scene
assert scene.renderer.time == 0
assert scene.time == 0
scene.wait(2)
assert scene.renderer.time == 2
assert scene.time == 2
scene.play(FadeIn(Circle()), run_time=0.5)
assert pytest.approx(scene.renderer.time) == 2.5
scene.renderer._original_skipping_status = True
assert pytest.approx(scene.time) == 2.5
scene._original_skipping_status = True
scene.play(FadeIn(Square()), run_time=5) # this animation gets skipped.
assert pytest.approx(scene.renderer.time) == 7.5

View file

@ -1,23 +1,13 @@
from __future__ import annotations
import logging
import pytest
from manim.utils.deprecation import deprecated, deprecated_params
def _get_caplog_record_msg(warn_caplog_manim):
logger_name, level, message = warn_caplog_manim.record_tuples[0]
def _get_caplog_record_msg(manim_caplog):
logger_name, level, message = manim_caplog.record_tuples[0]
return message
@pytest.fixture()
def warn_caplog_manim(caplog):
caplog.set_level(logging.WARNING, logger="manim")
yield caplog
@deprecated
class Foo:
def __init__(self):
@ -77,11 +67,11 @@ class QuuzAll:
doc_admonition = "\n\n.. attention:: Deprecated\n "
def test_deprecate_class_no_args(warn_caplog_manim):
def test_deprecate_class_no_args(manim_caplog):
"""Test the deprecation of a class (decorator with no arguments)."""
f = Foo()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Foo has been deprecated and may be removed in a later version."
@ -89,11 +79,11 @@ def test_deprecate_class_no_args(warn_caplog_manim):
assert f.__doc__ == f"{doc_admonition}{msg}"
def test_deprecate_class_since(warn_caplog_manim):
def test_deprecate_class_since(manim_caplog):
"""Test the deprecation of a class (decorator with since argument)."""
b = Bar()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Bar has been deprecated since v0.6.0 and may be removed in a later version."
@ -101,11 +91,11 @@ def test_deprecate_class_since(warn_caplog_manim):
assert b.__doc__ == f"The Bar class.{doc_admonition}{msg}"
def test_deprecate_class_until(warn_caplog_manim):
def test_deprecate_class_until(manim_caplog):
"""Test the deprecation of a class (decorator with until argument)."""
bz = Baz()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Baz has been deprecated and is expected to be removed after 06/01/2021."
@ -113,11 +103,11 @@ def test_deprecate_class_until(warn_caplog_manim):
assert bz.__doc__ == f"The Baz class.{doc_admonition}{msg}"
def test_deprecate_class_since_and_until(warn_caplog_manim):
def test_deprecate_class_since_and_until(manim_caplog):
"""Test the deprecation of a class (decorator with since and until arguments)."""
qx = Qux()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Qux has been deprecated since 0.7.0 and is expected to be removed after 0.9.0-rc2."
@ -125,11 +115,11 @@ def test_deprecate_class_since_and_until(warn_caplog_manim):
assert qx.__doc__ == f"{doc_admonition}{msg}"
def test_deprecate_class_msg(warn_caplog_manim):
def test_deprecate_class_msg(manim_caplog):
"""Test the deprecation of a class (decorator with msg argument)."""
qu = Quux()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Quux has been deprecated and may be removed in a later version. Use something else."
@ -137,11 +127,11 @@ def test_deprecate_class_msg(warn_caplog_manim):
assert qu.__doc__ == f"{doc_admonition}{msg}"
def test_deprecate_class_replacement(warn_caplog_manim):
def test_deprecate_class_replacement(manim_caplog):
"""Test the deprecation of a class (decorator with replacement argument)."""
qz = Quuz()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Quuz has been deprecated and may be removed in a later version. Use ReplaceQuuz instead."
@ -150,11 +140,11 @@ def test_deprecate_class_replacement(warn_caplog_manim):
assert qz.__doc__ == f"{doc_admonition}{doc_msg}"
def test_deprecate_class_all(warn_caplog_manim):
def test_deprecate_class_all(manim_caplog):
"""Test the deprecation of a class (decorator with all arguments)."""
qza = QuuzAll()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class QuuzAll has been deprecated since 0.7.0 and is expected to be removed after 1.2.1. Use ReplaceQuuz instead. Don't use this please."
@ -243,11 +233,11 @@ class Top:
return kwargs
def test_deprecate_func_no_args(warn_caplog_manim):
def test_deprecate_func_no_args(manim_caplog):
"""Test the deprecation of a method (decorator with no arguments)."""
useless()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The function useless has been deprecated and may be removed in a later version."
@ -255,12 +245,12 @@ def test_deprecate_func_no_args(warn_caplog_manim):
assert useless.__doc__ == f"{doc_admonition}{msg}"
def test_deprecate_func_in_class_since_and_message(warn_caplog_manim):
def test_deprecate_func_in_class_since_and_message(manim_caplog):
"""Test the deprecation of a method within a class (decorator with since and message arguments)."""
t = Top()
t.mid_func()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.mid_func has been deprecated since 0.8.0 and may be removed in a later version. This method is useless."
@ -268,11 +258,11 @@ def test_deprecate_func_in_class_since_and_message(warn_caplog_manim):
assert t.mid_func.__doc__ == f"Middle function in Top.{doc_admonition}{msg}"
def test_deprecate_nested_class_until_and_replacement(warn_caplog_manim):
def test_deprecate_nested_class_until_and_replacement(manim_caplog):
"""Test the deprecation of a nested class (decorator with until and replacement arguments)."""
n = Top().Nested()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The class Top.Nested has been deprecated and is expected to be removed after 1.4.0. Use Top.NewNested instead."
@ -281,12 +271,12 @@ def test_deprecate_nested_class_until_and_replacement(warn_caplog_manim):
assert n.__doc__ == f"{doc_admonition}{doc_msg}"
def test_deprecate_nested_class_func_since_and_until(warn_caplog_manim):
def test_deprecate_nested_class_func_since_and_until(manim_caplog):
"""Test the deprecation of a method within a nested class (decorator with since and until arguments)."""
n = Top().NewNested()
n.nested_func()
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.NewNested.nested_func has been deprecated since 1.0.0 and is expected to be removed after 12/25/2025."
@ -297,13 +287,13 @@ def test_deprecate_nested_class_func_since_and_until(warn_caplog_manim):
)
def test_deprecate_nested_func(warn_caplog_manim):
def test_deprecate_nested_func(manim_caplog):
"""Test the deprecation of a nested method (decorator with no arguments)."""
b = Top().Bottom()
answer = b.normal_func()
answer(1)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The method Top.Bottom.normal_func.<locals>.nested_func has been deprecated and may be removed in a later version."
@ -311,36 +301,36 @@ def test_deprecate_nested_func(warn_caplog_manim):
assert answer.__doc__ == f"{doc_admonition}{msg}"
def test_deprecate_func_params(warn_caplog_manim):
def test_deprecate_func_params(manim_caplog):
"""Test the deprecation of method parameters (decorator with params argument)."""
t = Top()
t.foo(a=2, b=3, z=4)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameters a and b of method Top.foo have been deprecated and may be removed in a later version. Use something else."
)
def test_deprecate_func_single_param_since_and_until(warn_caplog_manim):
def test_deprecate_func_single_param_since_and_until(manim_caplog):
"""Test the deprecation of a single method parameter (decorator with since and until arguments)."""
t = Top()
t.bar(a=1, b=2)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter a of method Top.bar has been deprecated since v0.2 and is expected to be removed after v0.4."
)
def test_deprecate_func_param_redirect_tuple(warn_caplog_manim):
def test_deprecate_func_param_redirect_tuple(manim_caplog):
"""Test the deprecation of a method parameter and redirecting it to a new one using tuple."""
t = Top()
obj = t.baz(x=1, old_param=2)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter old_param of method Top.baz has been deprecated and may be removed in a later version."
@ -348,12 +338,12 @@ def test_deprecate_func_param_redirect_tuple(warn_caplog_manim):
assert obj == {"x": 1, "new_param": 2}
def test_deprecate_func_param_redirect_lambda(warn_caplog_manim):
def test_deprecate_func_param_redirect_lambda(manim_caplog):
"""Test the deprecation of a method parameter and redirecting it to a new one using lambda function."""
t = Top()
obj = t.qux(runtime_in_ms=500)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter runtime_in_ms of method Top.qux has been deprecated and may be removed in a later version."
@ -361,12 +351,12 @@ def test_deprecate_func_param_redirect_lambda(warn_caplog_manim):
assert obj == {"run_time": 0.5}
def test_deprecate_func_param_redirect_many_to_one(warn_caplog_manim):
def test_deprecate_func_param_redirect_many_to_one(manim_caplog):
"""Test the deprecation of multiple method parameters and redirecting them to one."""
t = Top()
obj = t.quux(point2D_x=3, point2D_y=5)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameters point2D_x and point2D_y of method Top.quux have been deprecated and may be removed in a later version."
@ -374,23 +364,23 @@ def test_deprecate_func_param_redirect_many_to_one(warn_caplog_manim):
assert obj == {"point2D": (3, 5)}
def test_deprecate_func_param_redirect_one_to_many(warn_caplog_manim):
def test_deprecate_func_param_redirect_one_to_many(manim_caplog):
"""Test the deprecation of one method parameter and redirecting it to many."""
t = Top()
obj1 = t.quuz(point2D=0)
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."
)
assert obj1 == {"x": 0, "y": 0}
warn_caplog_manim.clear()
manim_caplog.clear()
obj2 = t.quuz(point2D=(2, 3))
assert len(warn_caplog_manim.record_tuples) == 1
msg = _get_caplog_record_msg(warn_caplog_manim)
assert len(manim_caplog.record_tuples) == 1
msg = _get_caplog_record_msg(manim_caplog)
assert (
msg
== "The parameter point2D of method Top.quuz has been deprecated and may be removed in a later version."

View file

@ -75,6 +75,18 @@ def test_transparent(config):
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 0])
def test_transparent_by_background_opacity(config, dry_run):
config.background_opacity = 0.5
assert config.transparent is True
manager = Manager(MyScene)
manager.render()
frame = manager.renderer.get_pixels()
np.testing.assert_allclose(frame[0, 0], [0, 0, 0, 127])
assert config.movie_file_extension == ".mov"
assert config.transparent is True
def test_background_color(config):
"""Test the 'background_color' config option."""
@ -221,7 +233,6 @@ def test_dry_run_with_png_format_skipped_animations(config, dry_run):
manager = Manager(MyScene)
manager.render()
def test_tex_template_file(tmp_path):
"""Test that a custom tex template file can be set from a config file."""
tex_file = Path(tmp_path / "my_template.tex")