mirror of
https://github.com/ManimCommunity/manim.git
synced 2026-06-22 10:01:47 +00:00
Implement sections for scenes (#3883)
Reimplement the ManimCE sections API, as well as generic fixes for `Tex`.
This commit is contained in:
parent
d09b03bb45
commit
7844c848f0
82 changed files with 1512 additions and 770 deletions
|
|
@ -18,7 +18,7 @@ repos:
|
|||
- id: python-check-blanket-noqa
|
||||
name: Precision flake ignores
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.2
|
||||
rev: v0.5.5
|
||||
hooks:
|
||||
- id: ruff
|
||||
name: ruff lint
|
||||
|
|
@ -32,16 +32,13 @@ repos:
|
|||
- id: flake8
|
||||
additional_dependencies:
|
||||
[
|
||||
flake8-bugbear==21.4.3,
|
||||
flake8-builtins==1.5.3,
|
||||
flake8-comprehensions>=3.6.1,
|
||||
flake8-docstrings==1.6.0,
|
||||
flake8-pytest-style==1.5.0,
|
||||
flake8-rst-docstrings==0.2.3,
|
||||
flake8-simplify==0.14.1,
|
||||
]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.10.1
|
||||
rev: v1.11.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies:
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ sys.path.insert(0, os.path.abspath("."))
|
|||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = "Manim"
|
||||
copyright = f"2020-{datetime.now().year}, The Manim Community Dev Team"
|
||||
copyright = f"2020-{datetime.now().year}, The Manim Community Dev Team" # noqa: A001
|
||||
author = "The Manim Community Dev Team"
|
||||
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ autodoc_type_aliases = {
|
|||
alias_name: f"~manim.{module}.{alias_name}"
|
||||
for module, module_dict in ALIAS_DOCS_DICT.items()
|
||||
for category_dict in module_dict.values()
|
||||
for alias_name in category_dict.keys()
|
||||
for alias_name in category_dict
|
||||
}
|
||||
autoclass_content = "both"
|
||||
|
||||
|
|
|
|||
|
|
@ -357,10 +357,10 @@ A list of all config options
|
|||
'log_dir', 'log_to_file', 'max_files_cached', 'media_dir', 'media_width',
|
||||
'movie_file_extension', 'notify_outdated_version', 'output_file', 'partial_movie_dir',
|
||||
'pixel_height', 'pixel_width', 'plugins', 'preview',
|
||||
'progress_bar', 'quality', 'right_side', 'save_as_gif', 'save_last_frame',
|
||||
'save_pngs', 'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'progress_bar', 'quality', 'right_side', 'save_last_frame',
|
||||
'scene_names', 'show_in_file_browser', 'sound', 'tex_dir',
|
||||
'tex_template', 'tex_template_file', 'text_dir', 'top', 'transparent',
|
||||
'upto_animation_number', 'use_opengl_renderer', 'verbosity', 'video_dir',
|
||||
'upto_animation_number', 'verbosity', 'video_dir',
|
||||
'window_position', 'window_monitor', 'window_size', 'write_all', 'write_to_movie',
|
||||
'enable_wireframe', 'force_window']
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ in order for Manim to work properly, some additional system
|
|||
dependencies need to be installed first. The following pages have
|
||||
operating system specific instructions for you to follow.
|
||||
|
||||
Manim requires Python version ``3.9`` or above to run.
|
||||
Manim requires Python version ``3.10`` or above to run.
|
||||
|
||||
.. hint::
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ The installation instructions depend on your particular operating
|
|||
system and package manager. If you happen to know exactly what you are doing,
|
||||
you can also simply ensure that your system has:
|
||||
|
||||
- a reasonably recent version of Python 3 (3.9 or above),
|
||||
- a reasonably recent version of Python 3 (3.10 or above),
|
||||
- with working Cairo bindings in the form of
|
||||
`pycairo <https://cairographics.org/pycairo/>`__,
|
||||
- and `Pango <https://pango.gnome.org>`__ headers.
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ to make one of them available on your system.
|
|||
Required Dependencies
|
||||
---------------------
|
||||
|
||||
Manim requires a recent version of Python (3.9 or above)
|
||||
Manim requires a recent version of Python (3.10 or above)
|
||||
in order to work.
|
||||
|
||||
Chocolatey
|
||||
|
|
@ -35,7 +35,7 @@ Pip
|
|||
***
|
||||
|
||||
As mentioned above, Manim needs a reasonably recent version of
|
||||
Python 3 (3.9 or above).
|
||||
Python 3 (3.10 or above).
|
||||
|
||||
**Python:** Head over to https://www.python.org, download an installer
|
||||
for a recent version of Python, and follow its instructions to get Python
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ Cameras
|
|||
*******
|
||||
|
||||
.. inheritance-diagram::
|
||||
manim.camera.cairo_camera
|
||||
manim.camera.camera
|
||||
:parts: 1
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.mobject.Mobject
|
||||
:top-classes: manim.camera.camera.Camera, manim.mobject.opengl.opengl_mobject.OpenGLMobject
|
||||
|
||||
Mobjects
|
||||
********
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ At this point, you have just executed the following command.
|
|||
|
||||
Let's dissect what just happened step by step. First, this command executes
|
||||
manim on the file ``scene.py``, which contains our animation code. Further,
|
||||
this command tells manim exactly which ``Scene`` is to be rendered, in this case,
|
||||
this command tells manim exactly which :class:`.Scene` is to be rendered, in this case,
|
||||
it is ``SquareToCircle``. This is necessary because a single scene file may
|
||||
contain more than one scene. Next, the flag `-p` tells manim to play the scene
|
||||
once it's rendered, and the `-ql` flag tells manim to render the scene in low
|
||||
|
|
@ -140,19 +140,30 @@ resolutions, e.g. ``-s -ql``, ``-s -qh``
|
|||
Sections
|
||||
********
|
||||
|
||||
In addition to the movie output file one can use sections. Each section produces
|
||||
its own output video. The cuts between two sections can be set like this:
|
||||
In addition to the movie output file one can use sections. If :attr:`ManimConfig.save_sections` is ``True``,
|
||||
each section produces its own output video. In order to use sections, set :attr:`~Scene.sections_api` to ``True``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def construct(self):
|
||||
# play the first animations...
|
||||
# you don't need a section in the very beginning as it gets created automatically
|
||||
self.next_section()
|
||||
# play more animations...
|
||||
self.next_section("this is an optional name that doesn't have to be unique")
|
||||
# play even more animations...
|
||||
self.next_section("this is a section without any animations, it will be removed")
|
||||
class MyScene(Scene):
|
||||
sections_api = True
|
||||
|
||||
@section
|
||||
def introduction(self):
|
||||
# play the first animations...
|
||||
# the default name of this section is the name of the method
|
||||
...
|
||||
|
||||
@section(name="this is an optional name that doesn't have to be unique")
|
||||
def second_section(self):
|
||||
# play more animations...
|
||||
...
|
||||
|
||||
@section(skip=True)
|
||||
def finale(self):
|
||||
# play even more animations...
|
||||
# however, they won't be included in the final output video
|
||||
...
|
||||
|
||||
All the animations between two of these cuts get concatenated into a single output
|
||||
video file.
|
||||
|
|
@ -160,24 +171,42 @@ Be aware that you need at least one animation in each section. For example this
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def construct(self):
|
||||
self.next_section()
|
||||
# this section doesn't have any animations and will be removed
|
||||
# but no error will be thrown
|
||||
# feel free to tend your flock of empty sections if you so desire
|
||||
self.add(Circle())
|
||||
self.next_section()
|
||||
class SectionsExampleWithNoAnimations(Scene):
|
||||
sections_api = True
|
||||
|
||||
@section
|
||||
def first(self):
|
||||
self.next_section()
|
||||
# this section doesn't have any animations and will be removed
|
||||
# but no error will be thrown
|
||||
# feel free to tend your flock of empty sections if you so desire
|
||||
self.add(Circle())
|
||||
|
||||
@section
|
||||
def next(self):
|
||||
# play some animations
|
||||
...
|
||||
|
||||
One way of fixing this is to wait a little:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def construct(self):
|
||||
self.next_section()
|
||||
self.add(Circle())
|
||||
# now we wait 1sec and have an animation to satisfy the section
|
||||
self.wait()
|
||||
self.next_section()
|
||||
class SectionsExampleWithNoAnimations(Scene):
|
||||
sections_api = True
|
||||
|
||||
@section
|
||||
def first(self):
|
||||
self.next_section()
|
||||
# this section doesn't have any animations and will be removed
|
||||
# but no error will be thrown
|
||||
# feel free to tend your flock of empty sections if you so desire
|
||||
self.add(Circle())
|
||||
self.wait()
|
||||
|
||||
@section
|
||||
def next(self):
|
||||
# play some animations
|
||||
...
|
||||
|
||||
For videos to be created for each section you have to add the ``--save_sections`` flag to the Manim call like this:
|
||||
|
||||
|
|
@ -258,13 +287,20 @@ You can also skip rendering all animations belonging to a section like this:
|
|||
|
||||
.. code-block:: python
|
||||
|
||||
def construct(self):
|
||||
self.next_section(skip_animations=True)
|
||||
# play some animations that shall be skipped...
|
||||
self.next_section()
|
||||
# play some animations that won't get skipped...
|
||||
class SkippingSections(Scene):
|
||||
sections_api = True
|
||||
|
||||
@section(skip=True)
|
||||
def first(self):
|
||||
# play some animations
|
||||
# things here will execute, but they
|
||||
# won't be written to the output file
|
||||
...
|
||||
|
||||
@section
|
||||
def next(self):
|
||||
# play some animations
|
||||
...
|
||||
|
||||
|
||||
Some command line flags
|
||||
|
|
@ -277,9 +313,9 @@ When executing the command
|
|||
manim -pql scene.py SquareToCircle
|
||||
|
||||
it specifies the scene to render. This is not necessary now. When a single
|
||||
file contains only one ``Scene`` class, it will just render the ``Scene``
|
||||
class. When a single file contains more than one ``Scene`` class, manim will
|
||||
let you choose a ``Scene`` class. If your file contains multiple ``Scene``
|
||||
file contains only one :class:`.Scene` class, it will just render the :class:`.Scene`
|
||||
class. When a single file contains more than one :class:`.Scene` class, manim will
|
||||
let you choose a :class:`.Scene` class. If your file contains multiple :class:`.Scene`
|
||||
classes, and you want to render them all, you can use the ``-a`` flag.
|
||||
|
||||
As discussed previously, the ``-ql`` specifies low render quality (854x480
|
||||
|
|
@ -294,7 +330,7 @@ the file browser at the location of the animation instead of playing it, you
|
|||
can use the ``-f`` flag. You can also omit these two flags.
|
||||
|
||||
Finally, by default manim will output .mp4 files. If you want your animations
|
||||
in .gif format instead, use the ``--format gif`` flag. The output files will
|
||||
in .gif format instead, use the ``--format=gif`` flag. The output files will
|
||||
be in the same folder as the .mp4 files, and with the same name, but a
|
||||
different file extension.
|
||||
|
||||
|
|
|
|||
|
|
@ -2,20 +2,26 @@ from manim import *
|
|||
|
||||
|
||||
class Test(Scene):
|
||||
def construct(self) -> None:
|
||||
self.play(
|
||||
Create(
|
||||
t := Tex(
|
||||
"Hello, world!", stroke_color=RED, fill_color=BLUE, stroke_width=2
|
||||
)
|
||||
)
|
||||
)
|
||||
self.play(FadeOut(t))
|
||||
sections_api = True
|
||||
|
||||
@section(name="spinning_math")
|
||||
def first_section(self) -> None:
|
||||
line = Line()
|
||||
line.add_updater(lambda m, dt: m.rotate(PI * dt))
|
||||
t = Tex(r"Math! $\sum e^{i\theta}$").add_updater(lambda m: m.next_to(line, UP))
|
||||
line.to_edge(LEFT)
|
||||
self.add(line, t)
|
||||
s = Square()
|
||||
self.add(s)
|
||||
self.play(Rotate(s, PI / 2))
|
||||
self.wait(7)
|
||||
t = Tex(
|
||||
"Hello, world!", stroke_color=RED, fill_color=BLUE, stroke_width=2
|
||||
).to_edge(RIGHT)
|
||||
self.add(t)
|
||||
self.play(Create(t), Rotate(s, PI / 2))
|
||||
self.wait(1)
|
||||
self.play(FadeOut(s))
|
||||
|
||||
@section
|
||||
def three_mobjects(self) -> None:
|
||||
sq = RegularPolygon(6)
|
||||
c = Circle()
|
||||
st = Star()
|
||||
|
|
@ -27,7 +33,11 @@ class Test(Scene):
|
|||
Create(st),
|
||||
)
|
||||
)
|
||||
self.play(FadeOut(VGroup(*self.mobjects)))
|
||||
self.play(FadeOut(VGroup(sq, c, st)))
|
||||
|
||||
@section(skip=True)
|
||||
def never_run(self) -> None:
|
||||
self.play(Write(Text("This should never be run")))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ from manim.mobject.types.vectorized_mobject import *
|
|||
from manim.mobject.value_tracker import *
|
||||
from manim.mobject.vector_field import *
|
||||
from manim.scene.scene import *
|
||||
from manim.scene.sections import *
|
||||
from manim.scene.vector_space_scene import *
|
||||
from manim.utils import color, rate_functions, unit
|
||||
from manim.utils.bezier import *
|
||||
|
|
|
|||
|
|
@ -29,15 +29,9 @@ save_last_frame = False
|
|||
# -a, --write_all
|
||||
write_all = False
|
||||
|
||||
# -g, --save_pngs
|
||||
save_pngs = False
|
||||
|
||||
# -0, --zero_pad
|
||||
zero_pad = 4
|
||||
|
||||
# -i, --save_as_gif
|
||||
save_as_gif = False
|
||||
|
||||
# --save_sections
|
||||
save_sections = False
|
||||
|
||||
|
|
@ -121,12 +115,6 @@ window_monitor = 0
|
|||
# --force_window
|
||||
force_window = False
|
||||
|
||||
# --use_projection_fill_shaders
|
||||
use_projection_fill_shaders = False
|
||||
|
||||
# --use_projection_stroke_shaders
|
||||
use_projection_stroke_shaders = False
|
||||
|
||||
movie_file_extension = .mp4
|
||||
|
||||
# These now override the --quality option.
|
||||
|
|
|
|||
|
|
@ -291,10 +291,8 @@ class ManimConfig(MutableMapping):
|
|||
"preview",
|
||||
"progress_bar",
|
||||
"quality",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"scene_names",
|
||||
"show_in_file_browser",
|
||||
"tex_dir",
|
||||
|
|
@ -305,8 +303,6 @@ class ManimConfig(MutableMapping):
|
|||
"renderer",
|
||||
"enable_gui",
|
||||
"gui_location",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"verbosity",
|
||||
"video_dir",
|
||||
"sections_dir",
|
||||
|
|
@ -337,6 +333,10 @@ class ManimConfig(MutableMapping):
|
|||
logger.warning(
|
||||
"preview and write_to_movie disabled, this is a dry run. Try passing -p or -w."
|
||||
)
|
||||
elif self.preview and self.write_to_movie:
|
||||
logger.warning(
|
||||
"Both preview and write_to_movie enabled, this can be slower than just previewing."
|
||||
)
|
||||
|
||||
# behave like a dict
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
|
|
@ -588,8 +588,6 @@ class ManimConfig(MutableMapping):
|
|||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"write_all",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"preview",
|
||||
"show_in_file_browser",
|
||||
|
|
@ -600,8 +598,6 @@ class ManimConfig(MutableMapping):
|
|||
"custom_folders",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
"no_latex_cleanup",
|
||||
|
|
@ -656,21 +652,18 @@ class ManimConfig(MutableMapping):
|
|||
gui_location = tuple(
|
||||
map(int, re.split(r"[;,\-]", parser["CLI"]["gui_location"])),
|
||||
)
|
||||
setattr(self, "gui_location", gui_location)
|
||||
self.gui_location = gui_location
|
||||
|
||||
window_size = parser["CLI"][
|
||||
"window_size"
|
||||
] # if not "default", get a tuple of the position
|
||||
if window_size != "default":
|
||||
window_size = tuple(map(int, re.split(r"[;,\-]", window_size)))
|
||||
setattr(self, "window_size", window_size)
|
||||
self.window_size = window_size
|
||||
|
||||
# plugins
|
||||
plugins = parser["CLI"].get("plugins", fallback="", raw=True)
|
||||
if plugins == "":
|
||||
plugins = []
|
||||
else:
|
||||
plugins = plugins.split(",")
|
||||
plugins = [] if plugins == "" else plugins.split(",")
|
||||
self.plugins = plugins
|
||||
# the next two must be set AFTER digesting pixel_width and pixel_height
|
||||
self["frame_height"] = parser["CLI"].getfloat("frame_height", 8.0)
|
||||
|
|
@ -687,7 +680,7 @@ class ManimConfig(MutableMapping):
|
|||
|
||||
val = parser["CLI"].get("progress_bar")
|
||||
if val:
|
||||
setattr(self, "progress_bar", val)
|
||||
self.progress_bar = val
|
||||
|
||||
val = parser["ffmpeg"].get("loglevel")
|
||||
if val:
|
||||
|
|
@ -697,11 +690,11 @@ class ManimConfig(MutableMapping):
|
|||
val = parser["jupyter"].getboolean("media_embed")
|
||||
except ValueError:
|
||||
val = None
|
||||
setattr(self, "media_embed", val)
|
||||
self.media_embed = val
|
||||
|
||||
val = parser["jupyter"].get("media_width")
|
||||
if val:
|
||||
setattr(self, "media_width", val)
|
||||
self.media_width = val
|
||||
|
||||
val = parser["CLI"].get("quality", fallback="", raw=True)
|
||||
if val:
|
||||
|
|
@ -761,8 +754,6 @@ class ManimConfig(MutableMapping):
|
|||
"show_in_file_browser",
|
||||
"write_to_movie",
|
||||
"save_last_frame",
|
||||
"save_pngs",
|
||||
"save_as_gif",
|
||||
"save_sections",
|
||||
"write_all",
|
||||
"disable_caching",
|
||||
|
|
@ -776,8 +767,6 @@ class ManimConfig(MutableMapping):
|
|||
"background_color",
|
||||
"enable_gui",
|
||||
"fullscreen",
|
||||
"use_projection_fill_shaders",
|
||||
"use_projection_stroke_shaders",
|
||||
"zero_pad",
|
||||
"enable_wireframe",
|
||||
"force_window",
|
||||
|
|
@ -852,15 +841,12 @@ class ManimConfig(MutableMapping):
|
|||
if args.tex_template:
|
||||
self.tex_template = TexTemplate.from_file(args.tex_template)
|
||||
|
||||
if (
|
||||
self.renderer == RendererType.OPENGL
|
||||
and getattr(args, "write_to_movie") is None
|
||||
):
|
||||
if self.renderer == RendererType.OPENGL and args.write_to_movie is None:
|
||||
# --write_to_movie was not passed on the command line, so don't generate video.
|
||||
self["write_to_movie"] = False
|
||||
|
||||
# Handle --gui_location flag.
|
||||
if getattr(args, "gui_location") is not None:
|
||||
if args.gui_location is not None:
|
||||
self.gui_location = args.gui_location
|
||||
|
||||
return self
|
||||
|
|
@ -980,24 +966,6 @@ class ManimConfig(MutableMapping):
|
|||
def write_all(self, value: bool) -> None:
|
||||
self._set_boolean("write_all", value)
|
||||
|
||||
@property
|
||||
def save_pngs(self) -> bool:
|
||||
"""Whether to save all frames in the scene as images files (-g)."""
|
||||
return self._d["save_pngs"]
|
||||
|
||||
@save_pngs.setter
|
||||
def save_pngs(self, value: bool) -> None:
|
||||
self._set_boolean("save_pngs", value)
|
||||
|
||||
@property
|
||||
def save_as_gif(self) -> bool:
|
||||
"""Whether to save the rendered scene in .gif format (-i)."""
|
||||
return self._d["save_as_gif"]
|
||||
|
||||
@save_as_gif.setter
|
||||
def save_as_gif(self, value: bool) -> None:
|
||||
self._set_boolean("save_as_gif", value)
|
||||
|
||||
@property
|
||||
def save_sections(self) -> bool:
|
||||
"""Whether to save single videos for each section in addition to the movie file."""
|
||||
|
|
@ -1481,24 +1449,6 @@ class ManimConfig(MutableMapping):
|
|||
def fullscreen(self, value: bool) -> None:
|
||||
self._set_boolean("fullscreen", value)
|
||||
|
||||
@property
|
||||
def use_projection_fill_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject fill which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_fill_shaders"]
|
||||
|
||||
@use_projection_fill_shaders.setter
|
||||
def use_projection_fill_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_fill_shaders", value)
|
||||
|
||||
@property
|
||||
def use_projection_stroke_shaders(self) -> bool:
|
||||
"""Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices."""
|
||||
return self._d["use_projection_stroke_shaders"]
|
||||
|
||||
@use_projection_stroke_shaders.setter
|
||||
def use_projection_stroke_shaders(self, value: bool) -> None:
|
||||
self._set_boolean("use_projection_stroke_shaders", value)
|
||||
|
||||
@property
|
||||
def zero_pad(self) -> int:
|
||||
"""PNG zero padding. A number between 0 (no zero padding) and 9 (9 columns minimum)."""
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ __all__ = ["Animation", "Wait", "override_animation"]
|
|||
|
||||
from collections.abc import Iterable, Sequence
|
||||
from copy import deepcopy
|
||||
from functools import partialmethod
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
from typing_extensions import Self, TypeVar
|
||||
|
|
@ -445,6 +446,52 @@ class Animation(AnimationProtocol):
|
|||
self.name = name
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls, **kwargs) -> None:
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
cls._original__init__ = cls.__init__
|
||||
|
||||
@classmethod
|
||||
def set_default(cls, **kwargs) -> None:
|
||||
"""Sets the default values of keyword arguments.
|
||||
|
||||
If this method is called without any additional keyword
|
||||
arguments, the original default values of the initialization
|
||||
method of this class are restored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
kwargs
|
||||
Passing any keyword argument will update the default
|
||||
values of the keyword arguments of the initialization
|
||||
function of this class.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. manim:: ChangeDefaultAnimation
|
||||
|
||||
class ChangeDefaultAnimation(Scene):
|
||||
def construct(self):
|
||||
Rotate.set_default(run_time=2, rate_func=rate_functions.linear)
|
||||
Indicate.set_default(color=None)
|
||||
|
||||
S = Square(color=BLUE, fill_color=BLUE, fill_opacity=0.25)
|
||||
self.add(S)
|
||||
self.play(Rotate(S, PI))
|
||||
self.play(Indicate(S))
|
||||
|
||||
Rotate.set_default()
|
||||
Indicate.set_default()
|
||||
|
||||
"""
|
||||
if kwargs:
|
||||
cls.__init__ = partialmethod(cls.__init__, **kwargs)
|
||||
else:
|
||||
cls.__init__ = cls._original__init__
|
||||
|
||||
|
||||
def prepare_animation(
|
||||
anim: AnimationProtocol
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ class AnimationGroup(Animation):
|
|||
def finish(self) -> None:
|
||||
self.interpolate(1)
|
||||
self.anims_begun[:] = True
|
||||
self.anims_begun[:] = True
|
||||
self.anims_finished[:] = True
|
||||
for anim in self.animations:
|
||||
self.process_subanimation_buffer(anim.buffer)
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ from manim.utils.color import ManimColor
|
|||
from .. import config
|
||||
from ..animation.animation import Animation
|
||||
from ..animation.composition import Succession
|
||||
from ..mobject.mobject import Group, Mobject
|
||||
from ..mobject.mobject import Group
|
||||
from ..mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from ..utils.bezier import integer_interpolate
|
||||
from ..utils.rate_functions import double_smooth, linear
|
||||
|
||||
|
|
@ -127,8 +128,8 @@ class ShowPartial(Animation):
|
|||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
submobject: OpenGLMobject,
|
||||
starting_submobject: OpenGLMobject,
|
||||
alpha: float,
|
||||
) -> Self:
|
||||
submobject.pointwise_become_partial(
|
||||
|
|
@ -231,7 +232,7 @@ class DrawBorderThenFill(Animation):
|
|||
run_time: float = 2,
|
||||
rate_func: Callable[[float], float] = double_smooth,
|
||||
stroke_width: float = 2,
|
||||
stroke_color: str = None,
|
||||
stroke_color: ManimColor | None = None,
|
||||
draw_border_animation_config: dict = {}, # what does this dict accept?
|
||||
fill_animation_config: dict = {},
|
||||
introducer: bool = True,
|
||||
|
|
@ -251,15 +252,19 @@ class DrawBorderThenFill(Animation):
|
|||
self.fill_animation_config = fill_animation_config
|
||||
self.outline = self.get_outline()
|
||||
|
||||
def _typecheck_input(self, vmobject: VMobject | OpenGLVMobject) -> None:
|
||||
if not isinstance(vmobject, (VMobject, OpenGLVMobject)):
|
||||
raise TypeError("DrawBorderThenFill only works for vectorized Mobjects")
|
||||
def _typecheck_input(self, vmobject: OpenGLVMobject) -> None:
|
||||
if not isinstance(vmobject, OpenGLVMobject):
|
||||
raise TypeError(
|
||||
f"{self.__class__.__name__} only works for vectorized Mobjects"
|
||||
)
|
||||
|
||||
def begin(self) -> None:
|
||||
# this self.get_outline() has to be called
|
||||
# before super().begin(), for whatever reason
|
||||
self.outline = self.get_outline()
|
||||
super().begin()
|
||||
|
||||
def get_outline(self) -> Mobject:
|
||||
def get_outline(self) -> OpenGLMobject:
|
||||
outline = self.mobject.copy()
|
||||
outline.set_fill(opacity=0)
|
||||
for sm in outline.family_members_with_points():
|
||||
|
|
@ -273,16 +278,16 @@ class DrawBorderThenFill(Animation):
|
|||
return vmobject.get_stroke_color()
|
||||
return vmobject.get_color()
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[Mobject]:
|
||||
def get_all_mobjects(self) -> Sequence[OpenGLMobject]:
|
||||
return [*super().get_all_mobjects(), self.outline]
|
||||
|
||||
def interpolate_submobject(
|
||||
self,
|
||||
submobject: Mobject,
|
||||
starting_submobject: Mobject,
|
||||
outline,
|
||||
submobject: OpenGLMobject,
|
||||
starting_submobject: OpenGLMobject,
|
||||
outline: OpenGLMobject,
|
||||
alpha: float,
|
||||
) -> None: # Fixme: not matching the parent class? What is outline doing here?
|
||||
) -> None:
|
||||
index: int
|
||||
subalpha: int
|
||||
index, subalpha = integer_interpolate(0, 2, alpha)
|
||||
|
|
@ -354,10 +359,7 @@ class Write(DrawBorderThenFill):
|
|||
) -> tuple[float, float]:
|
||||
length = len(vmobject.family_members_with_points())
|
||||
if run_time is None:
|
||||
if length < 15:
|
||||
run_time = 1
|
||||
else:
|
||||
run_time = 2
|
||||
run_time = 1 if length < 15 else 2
|
||||
if lag_ratio is None:
|
||||
lag_ratio = min(4.0 / max(1.0, length), 0.2)
|
||||
return run_time, lag_ratio
|
||||
|
|
@ -455,7 +457,7 @@ class SpiralIn(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
shapes: Mobject,
|
||||
shapes: OpenGLMobject,
|
||||
scale_factor: float = 8,
|
||||
fade_in_fraction=0.3,
|
||||
**kwargs,
|
||||
|
|
@ -512,7 +514,7 @@ class ShowIncreasingSubsets(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
group: Mobject,
|
||||
group: OpenGLMobject,
|
||||
suspend_mobject_updating: bool = False,
|
||||
int_func: Callable[[np.ndarray], np.ndarray] = np.floor,
|
||||
reverse_rate_function=False,
|
||||
|
|
@ -640,7 +642,7 @@ class ShowSubmobjectsOneByOne(ShowIncreasingSubsets):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
group: Iterable[Mobject],
|
||||
group: Iterable[OpenGLMobject],
|
||||
int_func: Callable[[np.ndarray], np.ndarray] = np.ceil,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
|
|
@ -725,7 +727,7 @@ class TypeWithCursor(AddTextLetterByLetter):
|
|||
def __init__(
|
||||
self,
|
||||
text: Text,
|
||||
cursor: Mobject,
|
||||
cursor: OpenGLMobject,
|
||||
buff: float = 0.1,
|
||||
keep_cursor_y: bool = True,
|
||||
leave_cursor_on: bool = True,
|
||||
|
|
|
|||
|
|
@ -61,10 +61,7 @@ class _Fade(Transform):
|
|||
) -> None:
|
||||
if not mobjects:
|
||||
raise ValueError("At least one mobject must be passed.")
|
||||
if len(mobjects) == 1:
|
||||
mobject = mobjects[0]
|
||||
else:
|
||||
mobject = Group(*mobjects)
|
||||
mobject = mobjects[0] if len(mobjects) == 1 else Group(*mobjects)
|
||||
|
||||
self.point_target = False
|
||||
if shift is None:
|
||||
|
|
|
|||
|
|
@ -6,15 +6,13 @@ __all__ = ["Rotating", "Rotate"]
|
|||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.animation.animation import Animation
|
||||
from manim.constants import ORIGIN, OUT, PI, TAU
|
||||
from manim.utils.rate_functions import linear
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.typing import RateFunc
|
||||
from manim.typing import Point3D, RateFunc, Vector3D
|
||||
|
||||
|
||||
class Rotating(Animation):
|
||||
|
|
@ -22,17 +20,15 @@ class Rotating(Animation):
|
|||
self,
|
||||
mobject: OpenGLMobject,
|
||||
angle: float = TAU,
|
||||
axis: np.ndarray = OUT,
|
||||
about_point: np.ndarray | None = None,
|
||||
about_edge: np.ndarray | None = None,
|
||||
run_time: float = 5.0,
|
||||
axis: Vector3D = OUT,
|
||||
about_point: Point3D | None = None,
|
||||
about_edge: Vector3D | None = None,
|
||||
rate_func: RateFunc = linear,
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
mobject,
|
||||
run_time=run_time,
|
||||
rate_func=rate_func,
|
||||
suspend_mobject_updating=suspend_mobject_updating,
|
||||
**kwargs,
|
||||
|
|
@ -79,6 +75,7 @@ class Rotate(Rotating):
|
|||
Examples
|
||||
--------
|
||||
.. manim:: UsingRotate
|
||||
|
||||
class UsingRotate(Scene):
|
||||
def construct(self):
|
||||
self.play(
|
||||
|
|
@ -89,16 +86,16 @@ class Rotate(Rotating):
|
|||
rate_func=linear,
|
||||
),
|
||||
Rotate(Square(side_length=0.5), angle=2*PI, rate_func=linear),
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: OpenGLMobject,
|
||||
angle: float = PI,
|
||||
axis: np.ndarray = OUT,
|
||||
axis: Vector3D = OUT,
|
||||
run_time: float = 1,
|
||||
about_edge: np.ndarray = ORIGIN,
|
||||
about_edge: Vector3D = ORIGIN,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
|
|
|
|||
|
|
@ -70,10 +70,7 @@ class Broadcast(LaggedStart):
|
|||
anims = []
|
||||
|
||||
# Works by saving the mob that is passed into the animation, scaling it to 0 (or the initial_width) and then restoring the original mob.
|
||||
if mobject.fill_opacity:
|
||||
fill_o = True
|
||||
else:
|
||||
fill_o = False
|
||||
fill_o = bool(mobject.fill_opacity)
|
||||
|
||||
for _ in range(self.n_mobs):
|
||||
mob = mobject.copy()
|
||||
|
|
|
|||
|
|
@ -206,10 +206,7 @@ class Transform(Animation):
|
|||
def finish(self) -> None:
|
||||
super().finish()
|
||||
if self.replace_mobject_with_target_in_scene:
|
||||
# Ideally this should stay at the same z-level as
|
||||
# the original mobject, but this is difficult to implement
|
||||
self.buffer.remove(self.mobject)
|
||||
self.buffer.add(self.target_mobject)
|
||||
self.buffer.replace(self.mobject, self.target_mobject)
|
||||
|
||||
def get_all_mobjects(self) -> Sequence[OpenGLMobject]:
|
||||
return [
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@
|
|||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"assert_is_mobject_method",
|
||||
"always",
|
||||
"f_always",
|
||||
"always_redraw",
|
||||
"turn_animation_into_updater",
|
||||
"cycle_animation",
|
||||
|
|
@ -11,24 +12,66 @@ __all__ = [
|
|||
|
||||
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
|
||||
|
||||
import numpy as np
|
||||
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
|
||||
from typing_extensions import Concatenate, ParamSpec, TypeIs
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
|
||||
|
||||
def assert_is_mobject_method(method: Callable) -> None:
|
||||
assert inspect.ismethod(method)
|
||||
mobject = method.__self__
|
||||
assert isinstance(mobject, (Mobject, OpenGLMobject))
|
||||
P = ParamSpec("P")
|
||||
|
||||
|
||||
def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
||||
M = TypeVar("M", bound=OpenGLMobject)
|
||||
|
||||
|
||||
# TODO: figure out how to typehint as MethodType[OpenGLMobject] to avoid the cast
|
||||
# madness in always/f_always
|
||||
def is_mobject_method(method: Callable[..., Any]) -> TypeIs[types.MethodType]:
|
||||
return inspect.ismethod(method) and isinstance(method.__self__, OpenGLMobject)
|
||||
|
||||
|
||||
def always(
|
||||
method: Callable[Concatenate[M, P], object], *args: P.args, **kwargs: P.kwargs
|
||||
) -> M:
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
func = method.__func__
|
||||
mobject.add_updater(lambda m: func(m, *args, **kwargs))
|
||||
return mobject
|
||||
|
||||
|
||||
def f_always(
|
||||
method: Callable[Concatenate[M, ...], None],
|
||||
*arg_generators: Callable[[], object],
|
||||
**kwargs,
|
||||
) -> M:
|
||||
"""
|
||||
More functional version of always, where instead
|
||||
of taking in args, it takes in functions which output
|
||||
the relevant arguments.
|
||||
"""
|
||||
if not is_mobject_method(method):
|
||||
raise ValueError("f_always must take a method of a Mobject")
|
||||
mobject = cast(M, method.__self__)
|
||||
func = method.__func__
|
||||
|
||||
def updater(mob):
|
||||
args = [arg_generator() for arg_generator in arg_generators]
|
||||
func(mob, *args, **kwargs)
|
||||
|
||||
mobject.add_updater(updater)
|
||||
return mobject
|
||||
|
||||
|
||||
def always_redraw(func: Callable[[], M]) -> M:
|
||||
"""Redraw the mobject constructed by a function every frame.
|
||||
|
||||
This function returns a mobject with an attached updater that
|
||||
|
|
@ -41,7 +84,8 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
A function without (required) input arguments that returns
|
||||
a mobject.
|
||||
|
||||
Examples --------
|
||||
Examples
|
||||
--------
|
||||
.. manim:: TangentAnimation
|
||||
|
||||
class TangentAnimation(Scene):
|
||||
|
|
@ -71,9 +115,10 @@ def always_redraw(func: Callable[[], Mobject]) -> Mobject:
|
|||
return mob
|
||||
|
||||
|
||||
# TODO: create a new Protocol for AnimationWithMobject
|
||||
def turn_animation_into_updater(
|
||||
animation: AnimationProtocol, cycle: bool = False, **kwargs
|
||||
) -> Mobject:
|
||||
animation: AnimationProtocol, cycle: bool = False
|
||||
) -> OpenGLMobject:
|
||||
"""
|
||||
Add an updater to the animation's mobject which applies
|
||||
the interpolation and update functions of the animation
|
||||
|
|
@ -97,14 +142,15 @@ def turn_animation_into_updater(
|
|||
self.wait(0.5)
|
||||
self.play(banner.expand(), run_time=0.5)
|
||||
"""
|
||||
mobject = animation.mobject
|
||||
mobject = cast(OpenGLMobject, animation.mobject)
|
||||
animation.suspend_mobject_updating = False
|
||||
animation.begin()
|
||||
animation.total_time = 0
|
||||
total_time = 0
|
||||
|
||||
def update(m: Mobject, dt: float):
|
||||
def update(m: OpenGLMobject, dt: float):
|
||||
nonlocal total_time
|
||||
run_time = animation.get_run_time()
|
||||
time_ratio = animation.total_time / run_time
|
||||
time_ratio = total_time / run_time
|
||||
if cycle:
|
||||
alpha = time_ratio % 1
|
||||
else:
|
||||
|
|
@ -115,11 +161,11 @@ def turn_animation_into_updater(
|
|||
return
|
||||
animation.interpolate(alpha)
|
||||
animation.update_mobjects(dt)
|
||||
animation.total_time += dt
|
||||
total_time += dt
|
||||
|
||||
mobject.add_updater(update)
|
||||
return mobject
|
||||
|
||||
|
||||
def cycle_animation(animation: AnimationProtocol, **kwargs) -> Mobject:
|
||||
def cycle_animation(animation: AnimationProtocol, **kwargs) -> OpenGLMobject:
|
||||
return turn_animation_into_updater(animation, cycle=True, **kwargs)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import typing
|
|||
from manim.animation.animation import Animation
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from manim.mobject.mobject import Mobject
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
|
||||
|
||||
class UpdateFromFunc(Animation):
|
||||
|
|
@ -23,15 +23,15 @@ class UpdateFromFunc(Animation):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
mobject: Mobject,
|
||||
update_function: typing.Callable[[Mobject], typing.Any],
|
||||
mobject: OpenGLMobject,
|
||||
update_function: typing.Callable[[OpenGLMobject], object],
|
||||
suspend_mobject_updating: bool = False,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.update_function = update_function
|
||||
super().__init__(
|
||||
mobject, suspend_mobject_updating=suspend_mobject_updating, **kwargs
|
||||
)
|
||||
self.update_function = update_function
|
||||
|
||||
def interpolate(self, alpha: float) -> None:
|
||||
self.update_function(self.mobject)
|
||||
|
|
@ -43,7 +43,9 @@ class UpdateFromAlphaFunc(UpdateFromFunc):
|
|||
|
||||
|
||||
class MaintainPositionRelativeTo(Animation):
|
||||
def __init__(self, mobject: Mobject, tracked_mobject: Mobject, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, mobject: OpenGLMobject, tracked_mobject: OpenGLMobject, **kwargs
|
||||
) -> None:
|
||||
self.tracked_mobject = tracked_mobject
|
||||
self.diff = op.sub(
|
||||
mobject.get_center(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ group.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from ast import literal_eval
|
||||
from pathlib import Path
|
||||
|
||||
|
|
@ -51,10 +52,8 @@ def value_from_string(value: str) -> str | int | bool:
|
|||
Union[:class:`str`, :class:`int`, :class:`bool`]
|
||||
Returns the literal of appropriate datatype.
|
||||
"""
|
||||
try:
|
||||
with contextlib.suppress(SyntaxError, ValueError):
|
||||
value = literal_eval(value)
|
||||
except (SyntaxError, ValueError):
|
||||
pass
|
||||
return value
|
||||
|
||||
|
||||
|
|
@ -197,7 +196,7 @@ To save your config please save that file and place it in your current working d
|
|||
"""Not enough values in input.
|
||||
You may have added a new entry to default.cfg, in which case you will have to
|
||||
modify write_cfg_subcmd_input to account for it.""",
|
||||
)
|
||||
) from None
|
||||
if temp:
|
||||
while temp and not _is_expected_datatype(
|
||||
temp,
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ class DefaultGroup(cloup.Group):
|
|||
warnings.warn(
|
||||
"Use default param of DefaultGroup or set_default_command() instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=1,
|
||||
)
|
||||
|
||||
def _decorator(f):
|
||||
|
|
|
|||
|
|
@ -54,14 +54,6 @@ def render(
|
|||
SCENES is an optional list of scenes in the file.
|
||||
"""
|
||||
|
||||
if args["save_as_gif"]:
|
||||
logger.warning("--save_as_gif is deprecated, please use --format=gif instead!")
|
||||
args["format"] = "gif"
|
||||
|
||||
if args["save_pngs"]:
|
||||
logger.warning("--save_pngs is deprecated, please use --format=png instead!")
|
||||
args["format"] = "png"
|
||||
|
||||
if args["show_in_file_browser"]:
|
||||
logger.warning(
|
||||
"The short form of show_in_file_browser is deprecated and will be moved to support --format.",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import re
|
|||
|
||||
from cloup import Choice, option, option_group
|
||||
|
||||
from manim.constants import QUALITIES, RendererType
|
||||
from manim.constants import QUALITIES
|
||||
|
||||
__all__ = ["render_options"]
|
||||
|
||||
|
|
@ -103,29 +103,6 @@ render_options = option_group(
|
|||
default=None,
|
||||
help="Render at this frame rate.",
|
||||
),
|
||||
option(
|
||||
"--renderer",
|
||||
type=Choice(
|
||||
[renderer_type.value for renderer_type in RendererType],
|
||||
case_sensitive=False,
|
||||
),
|
||||
help="Select a renderer for your Scene.",
|
||||
default="opengl",
|
||||
),
|
||||
option(
|
||||
"-g",
|
||||
"--save_pngs",
|
||||
is_flag=True,
|
||||
default=None,
|
||||
help="Save each frame as png (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"-i",
|
||||
"--save_as_gif",
|
||||
default=None,
|
||||
is_flag=True,
|
||||
help="Save as a gif (Deprecated).",
|
||||
),
|
||||
option(
|
||||
"--save_sections",
|
||||
default=None,
|
||||
|
|
@ -138,16 +115,4 @@ render_options = option_group(
|
|||
is_flag=True,
|
||||
help="Render scenes with alpha channel.",
|
||||
),
|
||||
option(
|
||||
"--use_projection_fill_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject fill which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
option(
|
||||
"--use_projection_stroke_shaders",
|
||||
is_flag=True,
|
||||
help="Use shaders for OpenGLVMobject stroke which are compatible with transformation matrices.",
|
||||
default=None,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -23,12 +24,10 @@ class EventListener:
|
|||
def __eq__(self, other: Any) -> bool:
|
||||
return_val = False
|
||||
if isinstance(other, EventListener):
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
return_val = (
|
||||
self.callback == other.callback
|
||||
and self.mobject == other.mobject
|
||||
and self.event_type == other.event_type
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return return_val
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
class WindowABC(ABC):
|
||||
is_closing: bool
|
||||
class WindowProtocol(Protocol):
|
||||
@property
|
||||
def is_closing(self) -> bool: ...
|
||||
|
||||
@abstractmethod
|
||||
def swap_buffers(self) -> None: ...
|
||||
def swap_buffers(self) -> object: ...
|
||||
|
||||
@abstractmethod
|
||||
def close(self) -> None: ...
|
||||
def close(self) -> object: ...
|
||||
|
||||
@abstractmethod
|
||||
def clear(self) -> None: ...
|
||||
def clear(self) -> object: ...
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class FileWriter(FileWriterProtocol):
|
|||
# first section gets automatically created for convenience
|
||||
# if you need the first section to be skipped, add a first section by hand, it will replace this one
|
||||
self.next_section(
|
||||
name="autocreated", type=DefaultSectionType.NORMAL, skip_animations=False
|
||||
name="autocreated", type_=DefaultSectionType.NORMAL, skip_animations=False
|
||||
)
|
||||
|
||||
def init_output_directories(self, scene_name: str) -> None:
|
||||
|
|
@ -93,10 +93,7 @@ class FileWriter(FileWriterProtocol):
|
|||
if config["dry_run"]: # in dry-run mode there is no output
|
||||
return
|
||||
|
||||
if config["input_file"]:
|
||||
module_name = config.get_dir("input_file").stem
|
||||
else:
|
||||
module_name = ""
|
||||
module_name = config.get_dir("input_file").stem if config["input_file"] else ""
|
||||
|
||||
if self.force_output_as_scene_name:
|
||||
self.output_name = Path(scene_name)
|
||||
|
|
@ -165,7 +162,7 @@ class FileWriter(FileWriterProtocol):
|
|||
if len(self.sections) and self.sections[-1].is_empty():
|
||||
self.sections.pop()
|
||||
|
||||
def next_section(self, name: str, type: str, skip_animations: bool) -> None:
|
||||
def next_section(self, name: str, type_: str, skip_animations: bool) -> None:
|
||||
"""Create segmentation cut here."""
|
||||
self.finish_last_section()
|
||||
|
||||
|
|
@ -183,7 +180,7 @@ class FileWriter(FileWriterProtocol):
|
|||
|
||||
self.sections.append(
|
||||
Section(
|
||||
type,
|
||||
type_,
|
||||
section_video,
|
||||
name,
|
||||
skip_animations,
|
||||
|
|
|
|||
|
|
@ -16,16 +16,18 @@ class FileWriterProtocol(Protocol):
|
|||
|
||||
def __init__(self, scene_name: str) -> None: ...
|
||||
|
||||
def begin_animation(self, allow_write: bool = False) -> None: ...
|
||||
def begin_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def end_animation(self, allow_write: bool = False) -> None: ...
|
||||
def end_animation(self, allow_write: bool = False) -> object: ...
|
||||
|
||||
def is_already_cached(self, hash_invocation: str) -> bool: ...
|
||||
|
||||
def add_partial_movie_file(self, hash_animation: str) -> None: ...
|
||||
def add_partial_movie_file(self, hash_animation: str) -> object: ...
|
||||
|
||||
def write_frame(self, frame: PixelArray) -> None: ...
|
||||
def write_frame(self, frame: PixelArray) -> object: ...
|
||||
|
||||
def next_section(self, name: str, type_: str, skip_animations: bool) -> object: ...
|
||||
|
||||
def finish(self) -> None: ...
|
||||
|
||||
def save_image(self, image: PixelArray) -> None: ...
|
||||
def save_image(self, image: PixelArray) -> object: ...
|
||||
|
|
|
|||
|
|
@ -34,23 +34,23 @@ class DefaultSectionType(str, Enum):
|
|||
|
||||
|
||||
class Section:
|
||||
"""A :class:`.Scene` can be segmented into multiple Sections.
|
||||
r"""A :class:`.Scene` can be segmented into multiple Sections.
|
||||
Refer to :doc:`the documentation</tutorials/output_and_config>` for more info.
|
||||
It consists of multiple animations.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
type
|
||||
Can be used by a third party applications to classify different types of sections.
|
||||
video
|
||||
Path to video file with animations belonging to section relative to sections directory.
|
||||
If ``None``, then the section will not be saved.
|
||||
name
|
||||
Human readable, non-unique name for this section.
|
||||
skip_animations
|
||||
Skip rendering the animations in this section when ``True``.
|
||||
partial_movie_files
|
||||
Animations belonging to this section.
|
||||
type\_
|
||||
Can be used by a third party applications to classify different types of sections.
|
||||
video
|
||||
Path to video file with animations belonging to section relative to sections directory.
|
||||
If ``None``, then the section will not be saved.
|
||||
name
|
||||
Human readable, non-unique name for this section.
|
||||
skip_animations
|
||||
Skip rendering the animations in this section when ``True``.
|
||||
partial_movie_files
|
||||
Animations belonging to this section.
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
|
@ -59,8 +59,8 @@ class Section:
|
|||
:meth:`.OpenGLRenderer.update_skipping_status`
|
||||
"""
|
||||
|
||||
def __init__(self, type: str, video: str | None, name: str, skip_animations: bool):
|
||||
self.type = type
|
||||
def __init__(self, type_: str, video: str | None, name: str, skip_animations: bool):
|
||||
self.type_ = type_
|
||||
# None when not to be saved -> still keeps section alive
|
||||
self.video: str | None = video
|
||||
self.name = name
|
||||
|
|
@ -94,7 +94,7 @@ class Section:
|
|||
return dict(
|
||||
{
|
||||
"name": self.name,
|
||||
"type": self.type,
|
||||
"type": self.type_,
|
||||
"video": self.video,
|
||||
},
|
||||
**video_metadata,
|
||||
|
|
|
|||
118
manim/manager.py
118
manim/manager.py
|
|
@ -5,19 +5,26 @@ __all__ = ["Manager"]
|
|||
import contextlib
|
||||
import platform
|
||||
import time
|
||||
import warnings
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import TYPE_CHECKING, Callable, Generic, TypeVar
|
||||
|
||||
import numpy as np
|
||||
from tqdm import tqdm
|
||||
|
||||
from manim import config, logger
|
||||
from manim.event_handler.window import WindowABC
|
||||
from manim.event_handler.window import WindowProtocol
|
||||
from manim.file_writer import FileWriter
|
||||
from manim.plugins import Hooks, plugins
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.renderer.opengl_renderer_window import Window
|
||||
from manim.scene.scene import Scene, SceneState
|
||||
from manim.utils.exceptions import EndSceneEarlyException
|
||||
from manim.utils.hashing import get_hash_from_play_call
|
||||
from manim.utils.progressbar import (
|
||||
ExperimentalProgressBarWarning,
|
||||
NullProgressBar,
|
||||
ProgressBar,
|
||||
ProgressBarProtocol,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy.typing as npt
|
||||
|
|
@ -53,6 +60,10 @@ class Manager(Generic[Scene_co]):
|
|||
Manager(Manimation).render()
|
||||
"""
|
||||
|
||||
window_class: type[WindowProtocol] = Window # type: ignore[type-abstract]
|
||||
file_writer_class: type[FileWriterProtocol] = FileWriter
|
||||
renderer_class: type[RendererProtocol] = OpenGLRenderer
|
||||
|
||||
def __init__(self, scene_cls: type[Scene_co]) -> None:
|
||||
# scene
|
||||
self.scene: Scene_co = scene_cls(manager=self)
|
||||
|
|
@ -69,10 +80,12 @@ class Manager(Generic[Scene_co]):
|
|||
self.renderer = self.create_renderer()
|
||||
self.renderer.use_window()
|
||||
|
||||
# file writer
|
||||
self.file_writer: FileWriterProtocol = self.create_file_writer()
|
||||
self._write_files = config.write_to_movie
|
||||
|
||||
# internal state
|
||||
self._skipping = False
|
||||
|
||||
# keep these as instance methods so subclasses
|
||||
# have access to everything
|
||||
def create_renderer(self) -> RendererProtocol:
|
||||
|
|
@ -85,12 +98,12 @@ class Manager(Generic[Scene_co]):
|
|||
-------
|
||||
An instance of a renderer
|
||||
"""
|
||||
renderer = plugins.renderer()
|
||||
renderer = self.renderer_class()
|
||||
if config.preview:
|
||||
renderer.use_window()
|
||||
return renderer
|
||||
|
||||
def create_window(self) -> WindowABC | None:
|
||||
def create_window(self) -> WindowProtocol | None:
|
||||
"""Create and return a window instance.
|
||||
|
||||
This can be overridden in subclasses (plugins), if more
|
||||
|
|
@ -100,7 +113,7 @@ class Manager(Generic[Scene_co]):
|
|||
-------
|
||||
A window if previewing, else None
|
||||
"""
|
||||
return plugins.window() if config.preview else None
|
||||
return self.window_class() if config.preview else None
|
||||
|
||||
def create_file_writer(self) -> FileWriterProtocol:
|
||||
"""Create and returna file writer instance.
|
||||
|
|
@ -112,7 +125,7 @@ class Manager(Generic[Scene_co]):
|
|||
-------
|
||||
A file writer satisfying :class:`.FileWriterProtocol`
|
||||
"""
|
||||
return FileWriter(scene_name=self.scene.get_default_scene_name())
|
||||
return self.file_writer_class(scene_name=self.scene.get_default_scene_name())
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up processes and manager"""
|
||||
|
|
@ -155,12 +168,27 @@ class Manager(Generic[Scene_co]):
|
|||
self.setup()
|
||||
|
||||
with contextlib.suppress(EndSceneEarlyException):
|
||||
self.scene.construct()
|
||||
self.construct()
|
||||
self.post_contruct()
|
||||
self._interact()
|
||||
|
||||
self.tear_down()
|
||||
|
||||
def construct(self) -> None:
|
||||
if not self.scene.sections_api:
|
||||
self.scene.construct()
|
||||
return
|
||||
for section in self.scene.find_sections():
|
||||
self.file_writer.next_section(
|
||||
section.name,
|
||||
section.type_,
|
||||
section.skip,
|
||||
)
|
||||
if section.skip:
|
||||
self._skipping = True
|
||||
section()
|
||||
self._skipping = False
|
||||
|
||||
def _render_second_pass(self) -> None:
|
||||
"""
|
||||
In the future, this method could be used
|
||||
|
|
@ -170,9 +198,6 @@ class Manager(Generic[Scene_co]):
|
|||
|
||||
def post_contruct(self) -> None:
|
||||
"""Run post-construct hooks, and clean up the file writer."""
|
||||
for hook in plugins.hooks[Hooks.POST_CONSTRUCT]:
|
||||
hook(self)
|
||||
|
||||
if self.file_writer.num_plays:
|
||||
self.file_writer.finish()
|
||||
# otherwise no animations were played
|
||||
|
|
@ -181,7 +206,6 @@ class Manager(Generic[Scene_co]):
|
|||
# FIXME: for some reason the OpenGLRenderer does not give out the
|
||||
# correct frame values here
|
||||
frame = self.renderer.get_pixels()
|
||||
# NOTE: add hooks for post-processing (e.g. gaussian blur)?
|
||||
self.file_writer.save_image(frame)
|
||||
|
||||
self._write_files = False
|
||||
|
|
@ -229,15 +253,16 @@ class Manager(Generic[Scene_co]):
|
|||
self.scene.time = self.time
|
||||
|
||||
if self.window is not None:
|
||||
self.window.clear()
|
||||
if not self._skipping:
|
||||
self.window.clear()
|
||||
|
||||
# if it's closing, then any subsequent methods will
|
||||
# raise an error because the internal C window pointer is nullptr.
|
||||
if self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
|
||||
self.render_state(write_frame=write_frame)
|
||||
|
||||
if not self._skipping:
|
||||
self.render_state(write_frame=write_frame)
|
||||
self._wait_for_animation_time()
|
||||
|
||||
def _wait_for_animation_time(self) -> None:
|
||||
|
|
@ -253,6 +278,9 @@ class Manager(Generic[Scene_co]):
|
|||
|
||||
self.window.swap_buffers()
|
||||
|
||||
if self._skipping:
|
||||
return
|
||||
|
||||
vt = self.time - self.virtual_animation_start_time
|
||||
rt = time.perf_counter() - self.real_animation_start_time
|
||||
# we can't sleep because we still need to poll for events,
|
||||
|
|
@ -267,6 +295,8 @@ class Manager(Generic[Scene_co]):
|
|||
def _play(self, *animations: AnimationProtocol) -> None:
|
||||
"""Play a bunch of animations"""
|
||||
|
||||
self.scene.pre_play()
|
||||
|
||||
if self.window is not None:
|
||||
self.real_animation_start_time = time.perf_counter()
|
||||
self.virtual_animation_start_time = self.time
|
||||
|
|
@ -287,7 +317,7 @@ class Manager(Generic[Scene_co]):
|
|||
Essentially, a series of methods that need to be called to successfully
|
||||
render a frame.
|
||||
"""
|
||||
if not config.write_to_movie:
|
||||
if not config.write_to_movie or self._skipping:
|
||||
return
|
||||
|
||||
if config.disable_caching:
|
||||
|
|
@ -315,13 +345,18 @@ class Manager(Generic[Scene_co]):
|
|||
|
||||
def _create_progressbar(
|
||||
self, total: float, description: str, **kwargs: Any
|
||||
) -> tqdm | contextlib.nullcontext[NullProgressBar]:
|
||||
) -> contextlib.AbstractContextManager[ProgressBarProtocol]:
|
||||
"""Create a progressbar"""
|
||||
|
||||
if not config.progress_bar:
|
||||
return contextlib.nullcontext(NullProgressBar())
|
||||
else:
|
||||
return tqdm(
|
||||
|
||||
with warnings.catch_warnings():
|
||||
if config.verbosity != "DEBUG":
|
||||
# Note: update when rich/notebook tqdm is no longer experimental
|
||||
warnings.simplefilter("ignore", category=ExperimentalProgressBarWarning)
|
||||
|
||||
return ProgressBar(
|
||||
total=total,
|
||||
unit="frames",
|
||||
desc=description % {"num": self.file_writer.num_plays},
|
||||
|
|
@ -349,7 +384,7 @@ class Manager(Generic[Scene_co]):
|
|||
update_mobjects = self.scene.should_update_mobjects()
|
||||
condition = stop_condition or (lambda: False)
|
||||
|
||||
progression = self._calc_time_progression(duration)
|
||||
progression = _calc_time_progression(duration)
|
||||
|
||||
state = self.scene.get_state()
|
||||
|
||||
|
|
@ -365,9 +400,7 @@ class Manager(Generic[Scene_co]):
|
|||
break
|
||||
else:
|
||||
self.time += dt
|
||||
# this fixes it, but at that point we might as well
|
||||
# just not cache
|
||||
self.renderer.render(self.scene.camera, state.mobjects)
|
||||
self.renderer.render(state)
|
||||
if self.window is not None and self.window.is_closing:
|
||||
raise EndSceneEarlyException()
|
||||
self._wait_for_animation_time()
|
||||
|
|
@ -380,8 +413,8 @@ class Manager(Generic[Scene_co]):
|
|||
self, animations: Sequence[AnimationProtocol]
|
||||
) -> None:
|
||||
last_t = 0.0
|
||||
run_time = self._calc_runtime(animations)
|
||||
progression = self._calc_time_progression(run_time)
|
||||
run_time = _calc_runtime(animations)
|
||||
progression = _calc_time_progression(run_time)
|
||||
with self._create_progressbar(
|
||||
progression.shape[0],
|
||||
f"Animation %(num)d: {animations[0]}{', etc.' if len(animations) > 1 else ''}",
|
||||
|
|
@ -392,20 +425,6 @@ class Manager(Generic[Scene_co]):
|
|||
self._update_frame(dt)
|
||||
progress.update(1)
|
||||
|
||||
def _calc_time_progression(self, run_time: float) -> npt.NDArray[np.float64]:
|
||||
"""Compute the time values at which to evaluate the animation"""
|
||||
|
||||
return np.arange(0, run_time, 1 / config.frame_rate)
|
||||
|
||||
def _calc_runtime(self, animations: Iterable[AnimationProtocol]) -> float:
|
||||
"""Calculate the runtime of an iterable of animations.
|
||||
|
||||
.. warning::
|
||||
|
||||
If animations is a generator, this will consume the generator.
|
||||
"""
|
||||
return max(animation.get_run_time() for animation in animations)
|
||||
|
||||
# -------------------------#
|
||||
# Rendering #
|
||||
# -------------------------#
|
||||
|
|
@ -432,8 +451,7 @@ class Manager(Generic[Scene_co]):
|
|||
use this to write a single frame!
|
||||
"""
|
||||
|
||||
# TODO: change self.scene.camera to state.camera
|
||||
self.renderer.render(self.scene.camera, state.mobjects)
|
||||
self.renderer.render(state)
|
||||
|
||||
should_write = write_frame if write_frame is not None else self._write_files
|
||||
if should_write:
|
||||
|
|
@ -446,7 +464,17 @@ class Manager(Generic[Scene_co]):
|
|||
self.file_writer.write_frame(frame)
|
||||
|
||||
|
||||
class NullProgressBar:
|
||||
"""Fake progressbar."""
|
||||
def _calc_time_progression(run_time: float) -> npt.NDArray[np.float64]:
|
||||
"""Compute the time values at which to evaluate the animation"""
|
||||
|
||||
def update(self, _: Any) -> None: ...
|
||||
return np.arange(0, run_time, 1 / config.frame_rate)
|
||||
|
||||
|
||||
def _calc_runtime(animations: Iterable[AnimationProtocol]) -> float:
|
||||
"""Calculate the runtime of an iterable of animations.
|
||||
|
||||
.. warning::
|
||||
|
||||
If animations is a generator, this will consume the generator.
|
||||
"""
|
||||
return max(animation.get_run_time() for animation in animations)
|
||||
|
|
|
|||
|
|
@ -401,7 +401,9 @@ class Arc(TipableVMobject):
|
|||
return line_intersection(line1=(a1, a1 + n1), line2=(a2, a2 + n2))
|
||||
except Exception:
|
||||
if warning:
|
||||
warnings.warn("Can't find Arc center, using ORIGIN instead")
|
||||
warnings.warn(
|
||||
"Can't find Arc center, using ORIGIN instead", stacklevel=1
|
||||
)
|
||||
self._failed_to_get_center = True
|
||||
return np.array(ORIGIN)
|
||||
|
||||
|
|
|
|||
|
|
@ -87,11 +87,10 @@ class _BooleanOps(VMobject, metaclass=ConvertToOpenGL):
|
|||
quads = vmobject.get_bezier_tuples_from_points(subpath)
|
||||
start = subpath[0]
|
||||
path.moveTo(*start[:2])
|
||||
for p0, p1, p2 in quads:
|
||||
for _p0, p1, p2 in quads:
|
||||
path.quadTo(*p1[:2], *p2[:2])
|
||||
if vmobject.consider_points_equals(subpath[0], subpath[-1]):
|
||||
path.close()
|
||||
|
||||
return path
|
||||
|
||||
def _convert_skia_path_to_vmobject(self, path: SkiaPath) -> VMobject:
|
||||
|
|
|
|||
|
|
@ -99,10 +99,7 @@ class Line(TipableVMobject):
|
|||
if buff == 0:
|
||||
return
|
||||
#
|
||||
if self.path_arc == 0:
|
||||
length = self.get_length()
|
||||
else:
|
||||
length = self.get_arc_length()
|
||||
length = self.get_length() if self.path_arc == 0 else self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -757,9 +757,6 @@ class Cutout(OpenGLVMobject):
|
|||
def __init__(self, main_shape: VMobject, *mobjects: VMobject, **kwargs) -> None:
|
||||
super().__init__(**kwargs)
|
||||
self.append_points(main_shape.points)
|
||||
if main_shape.get_direction() == "CW":
|
||||
sub_direction = "CCW"
|
||||
else:
|
||||
sub_direction = "CW"
|
||||
sub_direction = "CCW" if main_shape.get_direction() == "CW" else "CW"
|
||||
for mobject in mobjects:
|
||||
self.append_points(mobject.force_direction(sub_direction).points)
|
||||
|
|
|
|||
|
|
@ -338,10 +338,7 @@ def _tree_layout(
|
|||
parent = {u: root_vertex for u in children[root_vertex]}
|
||||
pos = {}
|
||||
obstruction = [0.0] * len(T)
|
||||
if orientation == "down":
|
||||
o = -1
|
||||
else:
|
||||
o = 1
|
||||
o = -1 if orientation == "down" else 1
|
||||
|
||||
def slide(v, dx):
|
||||
"""
|
||||
|
|
@ -404,15 +401,9 @@ def _tree_layout(
|
|||
if isinstance(scale, (float, int)) and (width > 0 or height > 0):
|
||||
sf = 2 * scale / max(width, height)
|
||||
elif isinstance(scale, tuple):
|
||||
if scale[0] is not None and width > 0:
|
||||
sw = 2 * scale[0] / width
|
||||
else:
|
||||
sw = 1
|
||||
sw = 2 * scale[0] / width if scale[0] is not None and width > 0 else 1
|
||||
|
||||
if scale[1] is not None and height > 0:
|
||||
sh = 2 * scale[1] / height
|
||||
else:
|
||||
sh = 1
|
||||
sh = 2 * scale[1] / height if scale[1] is not None and height > 0 else 1
|
||||
|
||||
sf = np.array([sw, sh, 0])
|
||||
else:
|
||||
|
|
@ -478,11 +469,11 @@ def _determine_graph_layout(
|
|||
return cast(LayoutFunction, layout)(
|
||||
nx_graph, scale=layout_scale, **layout_config
|
||||
)
|
||||
except TypeError:
|
||||
except TypeError as e:
|
||||
raise ValueError(
|
||||
f"The layout '{layout}' is neither a recognized layout, a layout function,"
|
||||
"nor a vertex placement dictionary.",
|
||||
)
|
||||
) from e
|
||||
|
||||
|
||||
class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
||||
|
|
@ -851,7 +842,7 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
label_fill_color=label_fill_color,
|
||||
vertex_type=vertex_type,
|
||||
vertex_config=vertex_config[v],
|
||||
vertex_mobject=vertex_mobjects[v] if v in vertex_mobjects else None,
|
||||
vertex_mobject=vertex_mobjects.get(v),
|
||||
)
|
||||
for v in vertices
|
||||
]
|
||||
|
|
@ -977,7 +968,8 @@ class GenericGraph(VMobject, metaclass=ConvertToOpenGL):
|
|||
::
|
||||
|
||||
>>> G = Graph([1, 2, 3], [(1, 2), (2, 3)])
|
||||
>>> removed = G.remove_vertices(2, 3); removed
|
||||
>>> removed = G.remove_vertices(2, 3)
|
||||
>>> removed
|
||||
VGroup(Line, Line, Dot, Dot)
|
||||
>>> G
|
||||
Undirected graph on 1 vertices and 0 edges
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ from manim.utils.color import (
|
|||
ManimColor,
|
||||
ParsableManimColor,
|
||||
color_gradient,
|
||||
interpolate_color,
|
||||
invert_color,
|
||||
)
|
||||
from manim.utils.config_ops import merge_dicts_recursively, update_dict_recursively
|
||||
|
|
@ -628,6 +629,8 @@ class CoordinateSystem:
|
|||
function: Callable[[float], float],
|
||||
x_range: Sequence[float] | None = None,
|
||||
use_vectorized: bool = False,
|
||||
colorscale: Union[Iterable[Color], Iterable[Color, float]] | None = None,
|
||||
colorscale_axis: int = 1,
|
||||
**kwargs: Any,
|
||||
) -> ParametricFunction:
|
||||
"""Generates a curve based on a function.
|
||||
|
|
@ -641,6 +644,12 @@ class CoordinateSystem:
|
|||
use_vectorized
|
||||
Whether to pass in the generated t value array to the function. Only use this if your function supports it.
|
||||
Output should be a numpy array of shape ``[y_0, y_1, ...]``
|
||||
colorscale
|
||||
Colors of the function. Optional parameter used when coloring a function by values. Passing a list of colors
|
||||
and a colorscale_axis will color the function by y-value. Passing a list of tuples in the form ``(color, pivot)``
|
||||
allows user-defined pivots where the color transitions.
|
||||
colorscale_axis
|
||||
Defines the axis on which the colorscale is applied (0 = x, 1 = y), default is y-axis (1).
|
||||
kwargs
|
||||
Additional parameters to be passed to :class:`~.ParametricFunction`.
|
||||
|
||||
|
|
@ -719,7 +728,57 @@ class CoordinateSystem:
|
|||
use_vectorized=use_vectorized,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
graph.underlying_function = function
|
||||
|
||||
if colorscale:
|
||||
if type(colorscale[0]) in (list, tuple):
|
||||
new_colors, pivots = [
|
||||
[i for i, j in colorscale],
|
||||
[j for i, j in colorscale],
|
||||
]
|
||||
else:
|
||||
new_colors = colorscale
|
||||
|
||||
ranges = [self.x_range, self.y_range]
|
||||
pivot_min = ranges[colorscale_axis][0]
|
||||
pivot_max = ranges[colorscale_axis][1]
|
||||
pivot_frequency = (pivot_max - pivot_min) / (len(new_colors) - 1)
|
||||
pivots = np.arange(
|
||||
start=pivot_min,
|
||||
stop=pivot_max + pivot_frequency,
|
||||
step=pivot_frequency,
|
||||
)
|
||||
|
||||
resolution = 0.01 if len(x_range) == 2 else x_range[2]
|
||||
sample_points = np.arange(x_range[0], x_range[1] + resolution, resolution)
|
||||
color_list = []
|
||||
for samp_x in sample_points:
|
||||
axis_value = (samp_x, function(samp_x))[colorscale_axis]
|
||||
if axis_value <= pivots[0]:
|
||||
color_list.append(new_colors[0])
|
||||
elif axis_value >= pivots[-1]:
|
||||
color_list.append(new_colors[-1])
|
||||
else:
|
||||
for i, pivot in enumerate(pivots):
|
||||
if pivot > axis_value:
|
||||
color_index = (axis_value - pivots[i - 1]) / (
|
||||
pivots[i] - pivots[i - 1]
|
||||
)
|
||||
color_index = min(color_index, 1)
|
||||
mob_color = interpolate_color(
|
||||
new_colors[i - 1],
|
||||
new_colors[i],
|
||||
color_index,
|
||||
)
|
||||
color_list.append(mob_color)
|
||||
break
|
||||
if config.renderer == RendererType.OPENGL:
|
||||
graph.set_color(color_list)
|
||||
else:
|
||||
graph.set_stroke(color_list)
|
||||
graph.set_sheen_direction(RIGHT)
|
||||
|
||||
return graph
|
||||
|
||||
def plot_implicit_curve(
|
||||
|
|
|
|||
|
|
@ -339,10 +339,7 @@ class BarChart(Axes):
|
|||
for i, (value, bar_name) in enumerate(zip(val_range, self.bar_names)):
|
||||
# to accommodate negative bars, the label may need to be
|
||||
# below or above the x_axis depending on the value of the bar
|
||||
if self.values[i] < 0:
|
||||
direction = UP
|
||||
else:
|
||||
direction = DOWN
|
||||
direction = UP if self.values[i] < 0 else DOWN
|
||||
bar_name_label = self.x_axis.label_constructor(bar_name)
|
||||
|
||||
bar_name_label.font_size = self.x_axis.font_size
|
||||
|
|
|
|||
|
|
@ -265,10 +265,12 @@ class Mobject:
|
|||
|
||||
>>> from manim import Square, GREEN
|
||||
>>> Square.set_default(color=GREEN, fill_opacity=0.25)
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#83C167'), 0.25)
|
||||
>>> Square.set_default()
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#FFFFFF'), 0.0)
|
||||
|
||||
.. manim:: ChangedDefaultTextcolor
|
||||
|
|
@ -674,13 +676,10 @@ class Mobject:
|
|||
# Add automatic compatibility layer
|
||||
# between properties and get_* and set_*
|
||||
# methods.
|
||||
#
|
||||
# In python 3.9+ we could change this
|
||||
# logic to use str.remove_prefix instead.
|
||||
|
||||
if attr.startswith("get_"):
|
||||
# Remove the "get_" prefix
|
||||
to_get = attr[4:]
|
||||
to_get = attr.removeprefix("get_")
|
||||
|
||||
def getter(self):
|
||||
warnings.warn(
|
||||
|
|
@ -1339,12 +1338,12 @@ class Mobject:
|
|||
# Default to applying matrix about the origin, not mobjects center
|
||||
if ("about_point" not in kwargs) and ("about_edge" not in kwargs):
|
||||
kwargs["about_point"] = ORIGIN
|
||||
full_matrix = np.identity(self.dim)
|
||||
matrix = np.array(matrix)
|
||||
full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix
|
||||
self.apply_points_function_about_point(
|
||||
lambda points: np.dot(points, full_matrix.T), **kwargs
|
||||
)
|
||||
# full_matrix = np.identity(self.dim)
|
||||
# matrix = np.array(matrix)
|
||||
# full_matrix[: matrix.shape[0], : matrix.shape[1]] = matrix
|
||||
# self.apply_points_function_about_point(
|
||||
# lambda points: np.dot(points, full_matrix.T), **kwargs
|
||||
# )
|
||||
return self
|
||||
|
||||
def apply_complex_function(
|
||||
|
|
@ -1577,9 +1576,7 @@ class Mobject:
|
|||
return True
|
||||
if self.get_bottom()[1] > config["frame_y_radius"]:
|
||||
return True
|
||||
if self.get_top()[1] < -config["frame_y_radius"]:
|
||||
return True
|
||||
return False
|
||||
return self.get_top()[1] < -config["frame_y_radius"]
|
||||
|
||||
def stretch_about_point(self, factor: float, dim: int, point: Point3D) -> Self:
|
||||
return self.stretch(factor, dim, about_point=point)
|
||||
|
|
@ -1994,7 +1991,7 @@ class Mobject:
|
|||
|
||||
# If we do not have points (but do have submobjects)
|
||||
# use only the points from those.
|
||||
if len(self.points) == 0:
|
||||
if len(self.points) == 0: # noqa: SIM108
|
||||
rv = None
|
||||
else:
|
||||
# Otherwise, be sure to include our own points
|
||||
|
|
@ -2003,10 +2000,7 @@ class Mobject:
|
|||
# smallest dimension they have and compare it to the return value.
|
||||
for mobj in self.submobjects:
|
||||
value = mobj.reduce_across_dimension(reduce_func, dim)
|
||||
if rv is None:
|
||||
rv = value
|
||||
else:
|
||||
rv = reduce_func([value, rv])
|
||||
rv = value if rv is None else reduce_func([value, rv])
|
||||
return rv
|
||||
|
||||
def nonempty_submobjects(self) -> list[Self]:
|
||||
|
|
@ -2487,10 +2481,10 @@ class Mobject:
|
|||
buff_x = buff_y = buff
|
||||
|
||||
# Initialize alignments correctly
|
||||
def init_alignments(alignments, num, mapping, name, dir):
|
||||
def init_alignments(alignments, num, mapping, name, dir_):
|
||||
if alignments is None:
|
||||
# Use cell_alignment as fallback
|
||||
return [cell_alignment * dir] * num
|
||||
return [cell_alignment * dir_] * num
|
||||
if len(alignments) != num:
|
||||
raise ValueError(f"{name}_alignments has a mismatching size.")
|
||||
alignments = list(alignments)
|
||||
|
|
|
|||
|
|
@ -463,10 +463,7 @@ class OpenGLLine(OpenGLTipableVMobject):
|
|||
if buff == 0:
|
||||
return
|
||||
#
|
||||
if self.path_arc == 0:
|
||||
length = self.get_length()
|
||||
else:
|
||||
length = self.get_arc_length()
|
||||
length = self.get_length() if self.path_arc == 0 else self.get_arc_length()
|
||||
#
|
||||
if length < 2 * buff:
|
||||
return
|
||||
|
|
|
|||
|
|
@ -14,9 +14,8 @@ from functools import partialmethod, wraps
|
|||
from math import ceil
|
||||
from typing import TYPE_CHECKING, Generic
|
||||
|
||||
import moderngl
|
||||
import numpy as np
|
||||
from typing_extensions import TypeVar
|
||||
from typing_extensions import TypedDict, TypeVar
|
||||
|
||||
from manim import config
|
||||
from manim.constants import *
|
||||
|
|
@ -43,6 +42,8 @@ from manim.utils.space_ops import (
|
|||
rotation_matrix_transpose,
|
||||
)
|
||||
|
||||
__all__ = ["OpenGLMobject", "MobjectKwargs"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
from typing import Callable
|
||||
|
|
@ -92,17 +93,6 @@ def stash_mobject_pointers(
|
|||
return wrapper
|
||||
|
||||
|
||||
def affects_shader_info_id(func):
|
||||
@wraps(func)
|
||||
def wrapper(self):
|
||||
for mob in self.get_family():
|
||||
func(mob)
|
||||
mob.refresh_shader_wrapper_id()
|
||||
return self
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
@dataclass
|
||||
class MobjectStatus:
|
||||
color_changed: bool = False
|
||||
|
|
@ -112,9 +102,19 @@ class MobjectStatus:
|
|||
points_changed: bool = False
|
||||
|
||||
|
||||
# it's generic in its renderer, which is a little bit cursed
|
||||
# In the future, it should be replaced with a RendererData protocol
|
||||
class OpenGLMobject(Generic[R]):
|
||||
# TODO: add this to the **kwargs of all mobjects that use OpenGLMobject
|
||||
class MobjectKwargs(TypedDict, total=False):
|
||||
opacity: float
|
||||
reflectiveness: float
|
||||
shadow: float
|
||||
gloss: float
|
||||
is_fixed_in_frame: bool
|
||||
is_fixed_orientation: bool
|
||||
depth_test: bool
|
||||
name: str
|
||||
|
||||
|
||||
class OpenGLMobject:
|
||||
"""Mathematical Object: base class for objects that can be displayed on screen.
|
||||
|
||||
Attributes
|
||||
|
|
@ -131,12 +131,9 @@ class OpenGLMobject(Generic[R]):
|
|||
"""
|
||||
|
||||
dim: int = 3
|
||||
shader_folder: str = ""
|
||||
render_primitive: int = moderngl.TRIANGLE_STRIP
|
||||
shader_dtype: Sequence[tuple[str, type, tuple[int]]] = [
|
||||
("point", np.float32, (3,)),
|
||||
]
|
||||
|
||||
# WARNING: when changing a parameter here, be sure to update the
|
||||
# TypedDict above so that autocomplete works for users
|
||||
def __init__(
|
||||
self,
|
||||
color=WHITE,
|
||||
|
|
@ -144,19 +141,16 @@ class OpenGLMobject(Generic[R]):
|
|||
reflectiveness: float = 0.0,
|
||||
shadow: float = 0.0,
|
||||
gloss: float = 0.0,
|
||||
texture_paths: dict[str, str] | None = None,
|
||||
is_fixed_in_frame: bool = False,
|
||||
is_fixed_orientation: bool = False,
|
||||
depth_test: bool = True,
|
||||
name: str | None = None,
|
||||
**kwargs,
|
||||
):
|
||||
self.color = color
|
||||
self.opacity = opacity
|
||||
self.reflectiveness = reflectiveness
|
||||
self.shadow = shadow
|
||||
self.gloss = gloss
|
||||
self.texture_paths = texture_paths
|
||||
self.is_fixed_in_frame = is_fixed_in_frame
|
||||
self.is_fixed_orientation = is_fixed_orientation
|
||||
self.depth_test = depth_test
|
||||
|
|
@ -174,7 +168,7 @@ class OpenGLMobject(Generic[R]):
|
|||
self.target: OpenGLMobject | None = None
|
||||
|
||||
# TODO replace with protocol
|
||||
self.renderer_data: R | None = None
|
||||
self.renderer_data: RendererData | None = None
|
||||
|
||||
# currently does nothing
|
||||
self.status = MobjectStatus()
|
||||
|
|
@ -237,10 +231,12 @@ class OpenGLMobject(Generic[R]):
|
|||
|
||||
>>> from manim import Square, GREEN
|
||||
>>> Square.set_default(color=GREEN, fill_opacity=0.25)
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#83C167'), 0.25)
|
||||
>>> Square.set_default()
|
||||
>>> s = Square(); s.color, s.fill_opacity
|
||||
>>> s = Square()
|
||||
>>> s.color, s.fill_opacity
|
||||
(ManimColor('#FFFFFF'), 0.0)
|
||||
|
||||
.. manim:: ChangedDefaultTextcolor
|
||||
|
|
@ -1080,10 +1076,10 @@ class OpenGLMobject(Generic[R]):
|
|||
buff_x = buff_y = buff
|
||||
|
||||
# Initialize alignments correctly
|
||||
def init_alignments(alignments, num, mapping, name, dir):
|
||||
def init_alignments(alignments, num, mapping, name, dir_):
|
||||
if alignments is None:
|
||||
# Use cell_alignment as fallback
|
||||
return [cell_alignment * dir] * num
|
||||
return [cell_alignment * dir_] * num
|
||||
if len(alignments) != num:
|
||||
raise ValueError(f"{name}_alignments has a mismatching size.")
|
||||
alignments = list(alignments)
|
||||
|
|
@ -1220,8 +1216,8 @@ class OpenGLMobject(Generic[R]):
|
|||
if v_buff is None:
|
||||
v_buff = v_buff_ratio * self[0].get_height()
|
||||
|
||||
x_unit = h_buff + max([sm.get_width() for sm in submobs])
|
||||
y_unit = v_buff + max([sm.get_height() for sm in submobs])
|
||||
x_unit = h_buff + max(sm.get_width() for sm in submobs)
|
||||
y_unit = v_buff + max(sm.get_height() for sm in submobs)
|
||||
|
||||
for index, sm in enumerate(submobs):
|
||||
if fill_rows_first:
|
||||
|
|
@ -1315,6 +1311,7 @@ class OpenGLMobject(Generic[R]):
|
|||
for submob in self.submobjects:
|
||||
submob.reverse_submobjects(recursive=True)
|
||||
self.note_changed_family()
|
||||
return self
|
||||
|
||||
# Copying
|
||||
|
||||
|
|
@ -1360,7 +1357,6 @@ class OpenGLMobject(Generic[R]):
|
|||
result.parents = []
|
||||
result.target = None
|
||||
result.saved_state = None
|
||||
#
|
||||
|
||||
result.points = np.array(self.points)
|
||||
#
|
||||
|
|
@ -1372,13 +1368,16 @@ class OpenGLMobject(Generic[R]):
|
|||
sm.parents = [result]
|
||||
|
||||
result.note_changed_family()
|
||||
for current, copy_ in zip(self.get_family(), result.get_family()):
|
||||
copy_.points = np.array(current.points)
|
||||
copy_.match_color(current)
|
||||
# Similarly, instead of calling match_updaters, since we know the status
|
||||
# won't have changed, just directly match with shallow copies.
|
||||
result.non_time_updaters = self.non_time_updaters.copy()
|
||||
result.time_based_updaters = self.time_based_updaters.copy()
|
||||
|
||||
family = self.get_family()
|
||||
for attr, value in list(self.__dict__.items()):
|
||||
for attr, value in self.__dict__.items():
|
||||
if (
|
||||
isinstance(value, OpenGLMobject)
|
||||
and value is not self
|
||||
|
|
@ -1404,7 +1403,7 @@ class OpenGLMobject(Generic[R]):
|
|||
|
||||
def restore(self):
|
||||
"""Restores the state that was previously saved with :meth:`~.OpenGLMobject.save_state`."""
|
||||
if not hasattr(self, "saved_state") or self.save_state is None:
|
||||
if self.saved_state is None:
|
||||
raise Exception("Trying to restore without having saved")
|
||||
self.become(self.saved_state)
|
||||
return self
|
||||
|
|
@ -1918,9 +1917,7 @@ class OpenGLMobject(Generic[R]):
|
|||
return True
|
||||
if self.get_bottom()[1] > config.frame_y_radius:
|
||||
return True
|
||||
if self.get_top()[1] < -config.frame_y_radius:
|
||||
return True
|
||||
return False
|
||||
return self.get_top()[1] < -config.frame_y_radius
|
||||
|
||||
def stretch_about_point(self, factor, dim, point):
|
||||
return self.stretch(factor, dim, about_point=point)
|
||||
|
|
@ -2158,7 +2155,7 @@ class OpenGLMobject(Generic[R]):
|
|||
if color is not None:
|
||||
self.color: ManimColor = ManimColor.parse(color)
|
||||
if opacity is not None:
|
||||
self.color.set_opacity(opacity)
|
||||
self.color.opacity(opacity)
|
||||
if recurse:
|
||||
for submob in self.submobjects:
|
||||
submob.set_color(color, recurse=True)
|
||||
|
|
@ -2171,17 +2168,14 @@ class OpenGLMobject(Generic[R]):
|
|||
submob.set_opacity(opacity, recurse=True)
|
||||
return self
|
||||
|
||||
def get_color(self):
|
||||
return rgb_to_hex(self.rgbas[0, :3])
|
||||
def get_color(self) -> ManimColor:
|
||||
return self.color
|
||||
|
||||
def get_opacity(self):
|
||||
return self.color._internal_value[3]
|
||||
return self.color.opacity()
|
||||
|
||||
def set_color_by_gradient(self, *colors: ParsableManimColor):
|
||||
if self.has_points():
|
||||
self.set_color(colors)
|
||||
else:
|
||||
self.set_submobject_colors_by_gradient(*colors)
|
||||
self.set_submobject_colors_by_gradient(*colors)
|
||||
return self
|
||||
|
||||
def set_submobject_colors_by_gradient(self, *colors):
|
||||
|
|
@ -2293,7 +2287,7 @@ class OpenGLMobject(Generic[R]):
|
|||
return all_points[index]
|
||||
|
||||
def get_continuous_bounding_box_point(self, direction):
|
||||
dl, center, ur = self.get_bounding_box()
|
||||
_dl, center, ur = self.get_bounding_box()
|
||||
corner_vect = ur - center
|
||||
return center + direction / np.max(
|
||||
np.abs(
|
||||
|
|
@ -2570,7 +2564,6 @@ class OpenGLMobject(Generic[R]):
|
|||
|
||||
self.add(dotL, dotR, dotMiddle)
|
||||
"""
|
||||
# TODO: replace with list of attribute names with a locking system
|
||||
self.points = path_func(mobject1.points, mobject2.points, alpha)
|
||||
self.interpolate_color(mobject1, mobject2, alpha)
|
||||
return self
|
||||
|
|
@ -2652,10 +2645,7 @@ class OpenGLMobject(Generic[R]):
|
|||
family1 = self.get_family()
|
||||
family2 = mobject.get_family()
|
||||
for sm1, sm2 in zip(family1, family2):
|
||||
sm1.shader_folder = sm2.shader_folder
|
||||
sm1.texture_paths = sm2.texture_paths
|
||||
sm1.depth_test = sm2.depth_test
|
||||
sm1.render_primitive = sm2.render_primitive
|
||||
# Make sure named family members carry over
|
||||
for attr, value in list(mobject.__dict__.items()):
|
||||
if isinstance(value, OpenGLMobject) and value in family2:
|
||||
|
|
@ -2663,6 +2653,7 @@ class OpenGLMobject(Generic[R]):
|
|||
self.refresh_bounding_box(recurse_down=True)
|
||||
if match_updaters:
|
||||
self.match_updaters(mobject)
|
||||
self.note_changed_family()
|
||||
return self
|
||||
|
||||
def looks_identical(self, mobject: OpenGLMobject) -> bool:
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class OpenGLPMobject(OpenGLMobject):
|
|||
for mob in self.family_members_with_points():
|
||||
num_points = mob.get_num_points()
|
||||
|
||||
def thin_func():
|
||||
def thin_func(num_points=num_points):
|
||||
return np.arange(0, num_points, factor)
|
||||
|
||||
if len(mob.points) == len(mob.rgbas):
|
||||
|
|
|
|||
|
|
@ -388,7 +388,8 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
if isinstance(image_mode, (str, Path)):
|
||||
image_mode = [image_mode] * 2
|
||||
image_mode_light, image_mode_dark = image_mode
|
||||
texture_paths = {
|
||||
# TODO: move to renderer
|
||||
_texture_paths = {
|
||||
"LightTexture": self.get_image_from_file(
|
||||
image_file,
|
||||
image_mode_light,
|
||||
|
|
@ -407,7 +408,7 @@ class OpenGLTexturedSurface(OpenGLSurface):
|
|||
self.v_range = uv_surface.v_range
|
||||
self.resolution = uv_surface.resolution
|
||||
self.gloss = self.uv_surface.gloss
|
||||
super().__init__(texture_paths=texture_paths, **kwargs)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def get_image_from_file(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ from functools import reduce
|
|||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import numpy as np
|
||||
from typing_extensions import Unpack
|
||||
|
||||
from manim.constants import *
|
||||
from manim.mobject.opengl.opengl_mobject import (
|
||||
MobjectKwargs,
|
||||
OpenGLMobject,
|
||||
OpenGLPoint,
|
||||
)
|
||||
|
|
@ -24,6 +26,7 @@ from manim.utils.bezier import (
|
|||
proportions_along_bezier_curve_for_point,
|
||||
)
|
||||
from manim.utils.color import *
|
||||
from manim.utils.color.core import ParsableManimColor
|
||||
from manim.utils.deprecation import deprecated
|
||||
from manim.utils.iterables import (
|
||||
listify,
|
||||
|
|
@ -55,6 +58,21 @@ DEFAULT_STROKE_COLOR = GREY_A
|
|||
DEFAULT_FILL_COLOR = GREY_C
|
||||
|
||||
|
||||
# TODO: add this to the **kwargs of all mobjects that use OpenGLVMobject
|
||||
class VMobjectKwargs(MobjectKwargs, total=False):
|
||||
color: ParsableManimColor | list[ParsableManimColor]
|
||||
fill_color: ParsableManimColor | list[ParsableManimColor]
|
||||
fill_opacity: float
|
||||
stroke_color: ParsableManimColor | list[ParsableManimColor]
|
||||
stroke_opacity: float
|
||||
stroke_width: float
|
||||
draw_stroke_behind_fill: bool
|
||||
background_image_file: str
|
||||
long_lines: bool
|
||||
joint_type: LineJointType
|
||||
flat_stroke: bool
|
||||
|
||||
|
||||
class OpenGLVMobject(OpenGLMobject):
|
||||
"""A vectorized mobject."""
|
||||
|
||||
|
|
@ -63,6 +81,8 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
make_smooth_after_applying_functions: bool = False
|
||||
tolerance_for_point_equality: float = 1e-8
|
||||
|
||||
# WARNING: before updating the __init__ update the VMobjectKwargs TypedDict
|
||||
# so users can get autocomplete
|
||||
def __init__(
|
||||
self,
|
||||
color: ParsableManimColor | list[ParsableManimColor] | None = None,
|
||||
|
|
@ -76,7 +96,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
long_lines: bool = False,
|
||||
joint_type: LineJointType = LineJointType.AUTO,
|
||||
flat_stroke: bool = False,
|
||||
**kwargs,
|
||||
**kwargs: Unpack[MobjectKwargs],
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
if fill_color is None:
|
||||
|
|
@ -198,7 +218,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
if color is not None:
|
||||
self.fill_color = listify(ManimColor.parse(color))
|
||||
if opacity is not None:
|
||||
self.fill_color = [c.set_opacity(opacity) for c in self.fill_color]
|
||||
self.fill_color = [c.opacity(opacity) for c in self.fill_color]
|
||||
return self
|
||||
|
||||
def set_stroke(
|
||||
|
|
@ -213,7 +233,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
if color is not None:
|
||||
mob.stroke_color = listify(ManimColor.parse(color))
|
||||
if opacity is not None:
|
||||
mob.stroke_color = [c.set_opacity(opacity) for c in mob.stroke_color]
|
||||
mob.stroke_color = [c.opacity(opacity) for c in mob.stroke_color]
|
||||
|
||||
if width is not None:
|
||||
mob.stroke_width = listify(width)
|
||||
|
|
@ -319,7 +339,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
|
||||
# TODO, it's weird for these to return the first of various lists
|
||||
# rather than the full information
|
||||
def get_fill_color(self) -> str:
|
||||
def get_fill_color(self) -> ManimColor:
|
||||
"""
|
||||
If there are multiple colors (for gradient)
|
||||
this returns the first one
|
||||
|
|
@ -333,7 +353,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
"""
|
||||
return self.get_fill_opacities()[0]
|
||||
|
||||
def get_stroke_color(self) -> str:
|
||||
def get_stroke_color(self) -> ManimColor:
|
||||
return self.get_stroke_colors()[0]
|
||||
|
||||
def get_stroke_width(self) -> float | np.ndarray:
|
||||
|
|
@ -342,7 +362,7 @@ class OpenGLVMobject(OpenGLMobject):
|
|||
def get_stroke_opacity(self) -> float:
|
||||
return self.get_stroke_opacities()[0]
|
||||
|
||||
def get_color(self) -> str:
|
||||
def get_color(self) -> ManimColor:
|
||||
if self.has_fill():
|
||||
return self.get_fill_color()
|
||||
return self.get_stroke_color()
|
||||
|
|
@ -1655,25 +1675,26 @@ class OpenGLDashedVMobject(OpenGLVMobject):
|
|||
**kwargs,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.dashed_ratio = dashed_ratio
|
||||
self.num_dashes = num_dashes
|
||||
r = self.dashed_ratio
|
||||
n = self.num_dashes
|
||||
|
||||
if num_dashes > 0:
|
||||
# End points of the unit interval for division
|
||||
alphas = np.linspace(0, 1, num_dashes + 1)
|
||||
|
||||
# This determines the length of each "dash"
|
||||
full_d_alpha = 1.0 / num_dashes
|
||||
partial_d_alpha = full_d_alpha * positive_space_ratio
|
||||
|
||||
# Rescale so that the last point of vmobject will
|
||||
# be the end of the last dash
|
||||
alphas /= 1 - full_d_alpha + partial_d_alpha
|
||||
# Assuming total length is 1
|
||||
dash_len = r / n
|
||||
void_len = (1 - r) / n if vmobject.is_closed() else (1 - r) / (n - 1)
|
||||
|
||||
self.add(
|
||||
*[
|
||||
vmobject.get_subcurve(alpha, alpha + partial_d_alpha)
|
||||
for alpha in alphas[:-1]
|
||||
]
|
||||
*(
|
||||
vmobject.get_subcurve(
|
||||
i * (dash_len + void_len),
|
||||
i * (dash_len + void_len) + dash_len,
|
||||
)
|
||||
for i in range(n)
|
||||
)
|
||||
)
|
||||
|
||||
# Family is already taken care of by get_subcurve
|
||||
# implementation
|
||||
self.match_style(vmobject, recurse=False)
|
||||
|
|
@ -1684,7 +1705,7 @@ class VHighlight(OpenGLVGroup):
|
|||
self,
|
||||
vmobject: OpenGLVMobject,
|
||||
n_layers: int = 5,
|
||||
color_bounds: tuple[Color, Color] = (GREY_C, GREY_E),
|
||||
color_bounds: tuple[ManimColor, ManimColor] = (GREY_C, GREY_E),
|
||||
max_stroke_addition: float = 5.0,
|
||||
):
|
||||
outline = vmobject.replicate(n_layers)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import textwrap
|
||||
|
|
@ -382,10 +383,8 @@ class Shader:
|
|||
shader_program_cache[self.name] = self.shader_program
|
||||
|
||||
def set_uniform(self, name, value):
|
||||
try:
|
||||
with contextlib.suppress(KeyError):
|
||||
self.shader_program[name] = value
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class FullScreenQuad(Mesh):
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ class BraceLabel(VMobject, metaclass=ConvertToOpenGL):
|
|||
self.brace = Brace(obj, brace_direction, buff, **brace_config)
|
||||
|
||||
if isinstance(text, (tuple, list)):
|
||||
self.label = self.label_constructor(font_size=font_size, *text, **kwargs)
|
||||
self.label = self.label_constructor(*text, font_size=font_size, **kwargs)
|
||||
else:
|
||||
self.label = self.label_constructor(str(text), font_size=font_size)
|
||||
|
||||
|
|
|
|||
|
|
@ -98,27 +98,13 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
should_center: bool = True,
|
||||
height: float | None = 2,
|
||||
width: float | None = None,
|
||||
color: str | None = None,
|
||||
opacity: float | None = None,
|
||||
fill_color: str | None = None,
|
||||
fill_opacity: float | None = None,
|
||||
stroke_color: str | None = None,
|
||||
stroke_opacity: float | None = None,
|
||||
stroke_width: float | None = None,
|
||||
svg_default: dict | None = None,
|
||||
path_string_config: dict | None = None,
|
||||
use_svg_cache: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
color=color,
|
||||
stroke_color=stroke_color,
|
||||
stroke_opacity=stroke_opacity,
|
||||
stroke_width=stroke_width,
|
||||
fill_opacity=fill_opacity,
|
||||
fill_color=fill_color,
|
||||
**kwargs,
|
||||
)
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# process keyword arguments
|
||||
self.file_name = Path(file_name) if file_name is not None else None
|
||||
|
|
@ -139,9 +125,7 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
}
|
||||
self.svg_default = svg_default
|
||||
|
||||
if path_string_config is None:
|
||||
path_string_config = {}
|
||||
self.path_string_config = path_string_config
|
||||
self.path_string_config = path_string_config or {}
|
||||
|
||||
self.init_svg_mobject(use_svg_cache=use_svg_cache)
|
||||
|
||||
|
|
@ -263,7 +247,8 @@ class SVGMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
result = []
|
||||
for shape in svg.elements():
|
||||
if isinstance(shape, se.Group):
|
||||
# can we combine the two continue cases into one?
|
||||
if isinstance(shape, se.Group): # noqa: SIM114
|
||||
continue
|
||||
elif isinstance(shape, se.Path):
|
||||
mob = self.path_to_mobject(shape)
|
||||
|
|
|
|||
|
|
@ -210,10 +210,7 @@ class DecimalNumber(VMobject, metaclass=ConvertToOpenGL):
|
|||
|
||||
rounded_num = np.round(number, self.num_decimal_places)
|
||||
if num_string.startswith("-") and rounded_num == 0:
|
||||
if self.include_sign:
|
||||
num_string = "+" + num_string[1:]
|
||||
else:
|
||||
num_string = num_string[1:]
|
||||
num_string = "+" + num_string[1:] if self.include_sign else num_string[1:]
|
||||
|
||||
return num_string
|
||||
|
||||
|
|
|
|||
|
|
@ -69,8 +69,9 @@ from manimpango import MarkupUtils, PangoUtils, TextSetting
|
|||
from manim import config, logger
|
||||
from manim.constants import *
|
||||
from manim.mobject.geometry.arc import Dot
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.svg.svg_mobject import SVGMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup
|
||||
from manim.utils.color import ManimColor, ParsableManimColor, color_gradient
|
||||
from manim.utils.deprecation import deprecated
|
||||
|
||||
|
|
@ -508,7 +509,7 @@ class Text(SVGMobject):
|
|||
else:
|
||||
self.line_spacing = self._font_size + self._font_size * self.line_spacing
|
||||
|
||||
color: ManimColor = ManimColor(color) if color else VMobject().color
|
||||
color: ManimColor = ManimColor(color) if color else OpenGLVMobject().color
|
||||
file_name = self._text2svg(color.to_hex())
|
||||
PangoUtils.remove_last_M(file_name)
|
||||
super().__init__(
|
||||
|
|
@ -1452,7 +1453,9 @@ class MarkupText(SVGMobject):
|
|||
"end_offset": end_offset,
|
||||
},
|
||||
)
|
||||
self.text = re.sub("<gradient[^>]+>(.+?)</gradient>", r"\1", self.text, 0, re.S)
|
||||
self.text = re.sub(
|
||||
"<gradient[^>]+>(.+?)</gradient>", r"\1", self.text, count=0, flags=re.S
|
||||
)
|
||||
return gradientmap
|
||||
|
||||
def _parse_color(self, col):
|
||||
|
|
@ -1494,7 +1497,9 @@ class MarkupText(SVGMobject):
|
|||
"end_offset": end_offset,
|
||||
},
|
||||
)
|
||||
self.text = re.sub("<color[^>]+>(.+?)</color>", r"\1", self.text, 0, re.S)
|
||||
self.text = re.sub(
|
||||
"<color[^>]+>(.+?)</color>", r"\1", self.text, count=0, flags=re.S
|
||||
)
|
||||
return colormap
|
||||
|
||||
def __repr__(self):
|
||||
|
|
|
|||
|
|
@ -664,10 +664,7 @@ class Cone(Surface):
|
|||
x, y, z = self.direction
|
||||
|
||||
r = np.sqrt(x**2 + y**2 + z**2)
|
||||
if r > 0:
|
||||
theta = np.arccos(z / r)
|
||||
else:
|
||||
theta = 0
|
||||
theta = np.arccos(z / r) if r > 0 else 0
|
||||
|
||||
if x == 0:
|
||||
if y == 0: # along the z axis
|
||||
|
|
@ -834,10 +831,7 @@ class Cylinder(Surface):
|
|||
x, y, z = self.direction
|
||||
|
||||
r = np.sqrt(x**2 + y**2 + z**2)
|
||||
if r > 0:
|
||||
theta = np.arccos(z / r)
|
||||
else:
|
||||
theta = 0
|
||||
theta = np.arccos(z / r) if r > 0 else 0
|
||||
|
||||
if x == 0:
|
||||
if y == 0: # along the z axis
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
for mob in self.family_members_with_points():
|
||||
num_points = self.get_num_points()
|
||||
mob.apply_over_attr_arrays(
|
||||
lambda arr: arr[np.arange(0, num_points, factor)],
|
||||
lambda arr, n=num_points: arr[np.arange(0, n, factor)],
|
||||
)
|
||||
return self
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ class PMobject(Mobject, metaclass=ConvertToOpenGL):
|
|||
"""
|
||||
for mob in self.family_members_with_points():
|
||||
indices = np.argsort(np.apply_along_axis(function, 1, mob.points))
|
||||
mob.apply_over_attr_arrays(lambda arr: arr[indices])
|
||||
mob.apply_over_attr_arrays(lambda arr, idx=indices: arr[idx])
|
||||
return self
|
||||
|
||||
def fade_to(self, color, alpha, family=True):
|
||||
|
|
|
|||
|
|
@ -198,13 +198,11 @@ class VMobject(Mobject):
|
|||
def init_colors(self, propagate_colors: bool = True) -> Self:
|
||||
self.set_fill(
|
||||
color=self.fill_color,
|
||||
opacity=self.fill_opacity,
|
||||
family=propagate_colors,
|
||||
)
|
||||
self.set_stroke(
|
||||
color=self.stroke_color,
|
||||
width=self.stroke_width,
|
||||
opacity=self.stroke_opacity,
|
||||
family=propagate_colors,
|
||||
)
|
||||
self.set_background_stroke(
|
||||
|
|
@ -1216,9 +1214,7 @@ class VMobject(Mobject):
|
|||
atol = self.tolerance_for_point_equality
|
||||
if abs(p0[0] - p1[0]) > atol + rtol * abs(p1[0]):
|
||||
return False
|
||||
if abs(p0[1] - p1[1]) > atol + rtol * abs(p1[1]):
|
||||
return False
|
||||
return True
|
||||
return abs(p0[1] - p1[1]) <= atol + rtol * abs(p1[1])
|
||||
|
||||
# Information about line
|
||||
def get_cubic_bezier_tuples_from_points(
|
||||
|
|
@ -2726,15 +2722,12 @@ class DashedVMobject(VMobject, metaclass=ConvertToOpenGL):
|
|||
if vmobject.is_closed():
|
||||
void_len = (1 - r) / n
|
||||
else:
|
||||
if n == 1:
|
||||
void_len = 1 - r
|
||||
else:
|
||||
void_len = (1 - r) / (n - 1)
|
||||
void_len = 1 - r if n == 1 else (1 - r) / (n - 1)
|
||||
|
||||
period = dash_len + void_len
|
||||
phase_shift = (dash_offset % 1) * period
|
||||
|
||||
if vmobject.is_closed():
|
||||
if vmobject.is_closed(): # noqa: SIM108
|
||||
# closed curves have equal amount of dashes and voids
|
||||
pattern_len = 1
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ from __future__ import annotations
|
|||
|
||||
from manim import config, logger
|
||||
|
||||
from .plugin_config import Hooks, plugins
|
||||
from .plugins_flags import get_plugins, list_plugins
|
||||
|
||||
__all__ = [
|
||||
"plugins",
|
||||
"Hooks",
|
||||
"list_plugins",
|
||||
"get_plugins",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from manim.event_handler.window import WindowABC
|
||||
from manim.renderer.opengl_renderer import OpenGLRenderer
|
||||
from manim.renderer.opengl_renderer_window import Window
|
||||
from manim.renderer.renderer import RendererProtocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
from manim.manager import Manager
|
||||
|
||||
HookFunction: TypeAlias = Callable[[Manager], object]
|
||||
|
||||
|
||||
__all__ = (
|
||||
"plugins",
|
||||
"Hooks",
|
||||
)
|
||||
|
||||
|
||||
class Hooks(Enum):
|
||||
POST_CONSTRUCT = "post_construct"
|
||||
|
||||
|
||||
class PluginConfig(BaseModel):
|
||||
"""Plugin abilities that should be customizable by the user.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
renderer : The renderer class to use for rendering scenes.
|
||||
window: The window class to use for displaying the scene.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from manim import plugins
|
||||
>>> plugins.renderer.__name__
|
||||
'OpenGLRenderer'
|
||||
>>> class MyRenderer(OpenGLRenderer):
|
||||
... '''My custom renderer
|
||||
...
|
||||
... All this actually has to do is implement
|
||||
... the RendererProtocol.
|
||||
... '''
|
||||
>>> plugins.renderer = MyRenderer
|
||||
>>> plugins.renderer.__name__
|
||||
'MyRenderer'
|
||||
>>> plugins.renderer = 3
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
pydantic_core._pydantic_core.ValidationError: 1 validation error for PluginConfig
|
||||
renderer
|
||||
Input should be a subclass of RendererProtocol [type=is_subclass_of, input_value=3, input_type=int]
|
||||
For further information visit https://errors.pydantic.dev/2.8/v/is_subclass_of
|
||||
"""
|
||||
|
||||
class Config:
|
||||
# runtime check Protocols (must be runtime_checkable Protocols)
|
||||
allow_arbitrary_types = True
|
||||
# validate setting attributes
|
||||
validate_assignment = True
|
||||
extra = "forbid"
|
||||
|
||||
renderer: type[RendererProtocol]
|
||||
window: type[WindowABC]
|
||||
|
||||
# not included in pydantic because Manager is undefined
|
||||
# due to circular imports and __future__.annotations
|
||||
# instead we do validation manually via :meth:`.register`
|
||||
_hooks: dict[Hooks, list[HookFunction]] = {hook: [] for hook in Hooks}
|
||||
|
||||
@property
|
||||
def hooks(self) -> dict[Hooks, list[HookFunction]]:
|
||||
return self._hooks
|
||||
|
||||
def register(self, hooks: dict[Hooks, list[HookFunction]]) -> None:
|
||||
"""Register hooks to run at specific points in the program."""
|
||||
|
||||
for hook, functions in hooks.items():
|
||||
if not all(callable(func) for func in functions):
|
||||
raise ValueError("All hooks must be callables!")
|
||||
if not isinstance(hook, Hooks):
|
||||
raise ValueError(
|
||||
f"Unknown hook type {hook}, must be an instance of enum {Hooks}"
|
||||
)
|
||||
self._hooks[hook].extend(functions)
|
||||
|
||||
|
||||
plugins = PluginConfig(renderer=OpenGLRenderer, window=Window)
|
||||
|
|
@ -295,7 +295,7 @@ class OpenGLRenderer(Renderer, RendererProtocol):
|
|||
self.ctx, "render_texture"
|
||||
)
|
||||
|
||||
def use_window(self):
|
||||
def use_window(self) -> None:
|
||||
self.output_fbo.release()
|
||||
self.output_fbo = self.ctx.detect_framebuffer()
|
||||
|
||||
|
|
@ -369,10 +369,10 @@ class OpenGLRenderer(Renderer, RendererProtocol):
|
|||
)
|
||||
frame_data["uv"] = np.array([[0, 0], [0, 1], [1, 0], [1, 0], [0, 1], [1, 1]])
|
||||
vbo = self.ctx.buffer(frame_data.tobytes())
|
||||
format = gl.detect_format(self.render_texture_program, frame_data.dtype.names)
|
||||
format_ = gl.detect_format(self.render_texture_program, frame_data.dtype.names)
|
||||
vao = self.ctx.vertex_array(
|
||||
program=self.render_texture_program,
|
||||
content=[(vbo, format, *frame_data.dtype.names)], # type: ignore
|
||||
content=[(vbo, format_, *frame_data.dtype.names)], # type: ignore
|
||||
)
|
||||
self.ctx.copy_framebuffer(self.render_target_texture_fbo, self.color_buffer_fbo)
|
||||
self.render_target_texture.use(0)
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
import moderngl_window as mglw
|
||||
import numpy as np
|
||||
from moderngl_window.context.pyglet.window import Window as PygletWindow
|
||||
from moderngl_window.timers.clock import Timer
|
||||
from screeninfo import get_monitors
|
||||
|
||||
from manim import __version__, config
|
||||
from manim.event_handler.window import WindowABC
|
||||
from manim.event_handler.window import WindowProtocol
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import TypeGuard
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
__all__ = ["Window"]
|
||||
|
||||
__all__ = ["Window"]
|
||||
|
||||
|
||||
class Window(PygletWindow, WindowABC):
|
||||
class Window(PygletWindow, WindowProtocol):
|
||||
name = "Manim Community"
|
||||
fullscreen: bool = False
|
||||
resizable: bool = False
|
||||
|
|
@ -22,7 +26,7 @@ class Window(PygletWindow, WindowABC):
|
|||
vsync: bool = True
|
||||
cursor: bool = True
|
||||
|
||||
def __init__(self, size=config.window_size):
|
||||
def __init__(self, size=config.window_size) -> None:
|
||||
# TODO: remove size argument from window init,
|
||||
# move size computation below to config
|
||||
|
||||
|
|
@ -65,15 +69,20 @@ class Window(PygletWindow, WindowABC):
|
|||
self.position = initial_position
|
||||
|
||||
def find_initial_position(self, size: tuple[int, int]) -> tuple[int, int]:
|
||||
custom_position = config.window_position
|
||||
custom_position = config.window_position.replace(" ", "").upper()
|
||||
monitors = get_monitors()
|
||||
mon_index = config.window_monitor
|
||||
monitor = monitors[min(mon_index, len(monitors) - 1)]
|
||||
window_width, window_height = size
|
||||
|
||||
# Position might be specified with a string of the form
|
||||
# x,y for integers x and y
|
||||
if "," in custom_position:
|
||||
return tuple(map(int, custom_position.split(",")))
|
||||
pos = tuple(int(p) for p in custom_position.split(","))
|
||||
if tuple_len_2(pos):
|
||||
return pos
|
||||
else:
|
||||
raise ValueError("Expected position in the form x,y")
|
||||
|
||||
# Alternatively, it might be specified with a string like
|
||||
# UR, OO, DL, etc. specifying what corner it should go to
|
||||
|
|
@ -85,23 +94,6 @@ class Window(PygletWindow, WindowABC):
|
|||
-monitor.y + char_to_n[custom_position[0]] * height_diff // 2,
|
||||
)
|
||||
|
||||
# Delegate event handling to scene
|
||||
def pixel_coords_to_space_coords(
|
||||
self, px: int, py: int, relative: bool = False
|
||||
) -> np.ndarray:
|
||||
pw, ph = self.size
|
||||
# TODO
|
||||
fw, fh = (
|
||||
config.frame_width,
|
||||
config.frame_height,
|
||||
) or self.scene.camera.get_frame_shape()
|
||||
fc = (
|
||||
config.frame_width,
|
||||
config.frame_height,
|
||||
) or 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]
|
||||
)
|
||||
|
||||
def tuple_len_2(pos: tuple[T, ...]) -> TypeGuard[tuple[T, T]]:
|
||||
return len(pos) == 2
|
||||
|
|
|
|||
|
|
@ -4,14 +4,12 @@ from abc import ABC, abstractmethod
|
|||
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
||||
|
||||
from manim._config import logger
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.opengl.opengl_vectorized_mobject import OpenGLVMobject
|
||||
from manim.mobject.types.image_mobject import ImageMobject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable
|
||||
|
||||
from manim.camera.camera import Camera
|
||||
from manim.scene.scene import SceneState
|
||||
from manim.typing import PixelArray
|
||||
|
||||
|
||||
|
|
@ -32,9 +30,9 @@ class Renderer(ABC):
|
|||
(ImageMobject, self.render_image),
|
||||
]
|
||||
|
||||
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None:
|
||||
self.pre_render(camera)
|
||||
for mob in renderables:
|
||||
def render(self, state: SceneState) -> None:
|
||||
self.pre_render(state.camera)
|
||||
for mob in state.mobjects:
|
||||
for type_, render_func in self.capabilities:
|
||||
if isinstance(mob, type_):
|
||||
render_func(mob)
|
||||
|
|
@ -68,7 +66,7 @@ class Renderer(ABC):
|
|||
class RendererProtocol(Protocol):
|
||||
"""The Protocol a renderer must implement to be used in :class:`.Manager`."""
|
||||
|
||||
def render(self, camera: Camera, renderables: Iterable[OpenGLMobject]) -> None:
|
||||
def render(self, state: SceneState) -> None:
|
||||
"""Render a group of Mobjects"""
|
||||
...
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import random
|
||||
from collections import OrderedDict, deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
|
@ -17,11 +18,12 @@ from manim.event_handler.event_type import EventType
|
|||
from manim.mobject.mobject import Group, Point, _AnimationBuilder
|
||||
from manim.mobject.opengl.opengl_mobject import OpenGLMobject
|
||||
from manim.mobject.types.vectorized_mobject import VGroup, VMobject
|
||||
from manim.scene.sections import SceneSection
|
||||
from manim.utils.iterables import list_difference_update
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Reversible, Sequence
|
||||
from typing import Any, Callable
|
||||
from typing import Any, Callable, Self
|
||||
|
||||
from manim.animation.protocol import AnimationProtocol
|
||||
from manim.manager import Manager
|
||||
|
|
@ -72,7 +74,9 @@ class Scene:
|
|||
embed_exception_mode: str = ""
|
||||
embed_error_sound: bool = False
|
||||
|
||||
def __init__(self, manager: Manager):
|
||||
sections_api: bool = False
|
||||
|
||||
def __init__(self, manager: Manager[Self]):
|
||||
# Core state of the scene
|
||||
self.camera: Camera = Camera()
|
||||
self.manager = manager
|
||||
|
|
@ -131,13 +135,32 @@ class Scene:
|
|||
The entrypoint to animations in Manim.
|
||||
Should be overridden in the subclass to produce animations
|
||||
"""
|
||||
raise RuntimeError("Could not find the construct method, did you misspell it?")
|
||||
raise RuntimeError(
|
||||
"Could not find the construct method, did you misspell the name?"
|
||||
)
|
||||
|
||||
def tear_down(self) -> None:
|
||||
"""
|
||||
This method is used to clean up scenes
|
||||
"""
|
||||
|
||||
def find_sections(self) -> list[SceneSection]:
|
||||
"""Find all sections in a :class:`.Scene`"""
|
||||
|
||||
sections: list[SceneSection] = [
|
||||
bound
|
||||
for _, bound in inspect.getmembers(
|
||||
self, predicate=lambda x: isinstance(x, SceneSection)
|
||||
)
|
||||
]
|
||||
# we can't care about the actual value of the order
|
||||
# because that would break files with multiple scenes that have sections
|
||||
sections.sort(key=lambda x: x.order)
|
||||
# turn them into bound methods
|
||||
for section in sections:
|
||||
section.func = section.func.__get__(self, type(self))
|
||||
return sections
|
||||
|
||||
# Only these methods should touch the camera
|
||||
# Related to updating
|
||||
|
||||
|
|
@ -544,7 +567,9 @@ class Scene:
|
|||
|
||||
|
||||
class SceneState:
|
||||
def __init__(self, scene: Scene, ignore: list[OpenGLMobject] | None = None) -> None:
|
||||
def __init__(
|
||||
self, scene: Scene, ignore: Iterable[OpenGLMobject] | None = None
|
||||
) -> None:
|
||||
self.time = scene.time
|
||||
self.num_plays = scene.num_plays
|
||||
self.camera = scene.camera.copy()
|
||||
|
|
|
|||
135
manim/scene/sections.py
Normal file
135
manim/scene/sections.py
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import ClassVar, Generic, ParamSpec, TypeVar, final, overload
|
||||
|
||||
from typing_extensions import TypedDict, Unpack
|
||||
|
||||
from manim.file_writer.sections import DefaultSectionType
|
||||
|
||||
__all__ = ["section"]
|
||||
|
||||
|
||||
P = ParamSpec("P")
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class SceneSectionData(TypedDict, total=False):
|
||||
"""(Public) data for a :class:`.SceneSection` in a :class:`.Scene`."""
|
||||
|
||||
skip: bool
|
||||
type_: str
|
||||
name: str
|
||||
order: int
|
||||
|
||||
|
||||
# mark as final because _cls_instance_count doesn't
|
||||
# work with inheritance
|
||||
@final
|
||||
class SceneSection(Generic[P, T]):
|
||||
"""A section in a :class:`.Scene`.
|
||||
|
||||
It holds data about each subsection, and keeps track of the order
|
||||
of the sections via :attr:`~SceneSection.order`.
|
||||
|
||||
.. warning::
|
||||
|
||||
:attr:`~SceneSection.func` is effectively a function - it is not
|
||||
bound to the scene, and thus must be called with the first argument
|
||||
as an instance of :class:`.Scene`.
|
||||
"""
|
||||
|
||||
_cls_instance_count: ClassVar[int] = 0
|
||||
"""How many times the class has been instantiated.
|
||||
|
||||
This is also used for ordering sections, because of the order
|
||||
decorators are called in a class.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, func: Callable[P, T], **kwargs: Unpack[SceneSectionData]
|
||||
) -> None:
|
||||
self.func = func
|
||||
|
||||
self.skip = False
|
||||
self.type_ = DefaultSectionType.NORMAL
|
||||
self.name = func.__name__
|
||||
|
||||
# update the order for finding section orders
|
||||
self.order = self._cls_instance_count
|
||||
self.__class__._cls_instance_count += 1
|
||||
|
||||
# we assume that users have a typechecker on
|
||||
# and aren't doing any weird stuff
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
name = self.name
|
||||
skip = self.skip
|
||||
section_type = self.type_
|
||||
order = self.order
|
||||
return f"{self.__class__.__name__}({name=}, {order=}, {skip=}, {section_type=})"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
# return a slightly more verbose repr
|
||||
s = str(self).removesuffix(")")
|
||||
func = self.func
|
||||
return f"{s}, {func=})"
|
||||
|
||||
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
|
||||
return self.func(*args, **kwargs)
|
||||
|
||||
|
||||
@overload
|
||||
def section(
|
||||
func: Callable[P, T],
|
||||
**kwargs: Unpack[SceneSectionData],
|
||||
) -> SceneSection[P, T]: ...
|
||||
|
||||
|
||||
@overload
|
||||
def section(
|
||||
func: None = None,
|
||||
**kwargs: Unpack[SceneSectionData],
|
||||
) -> Callable[[Callable[P, T]], SceneSection[P, T]]: ...
|
||||
|
||||
|
||||
def section(
|
||||
func: Callable[P, T] | None = None, **kwargs: Unpack[SceneSectionData]
|
||||
) -> SceneSection[P, T] | Callable[[Callable[P, T]], SceneSection[P, T]]:
|
||||
r"""Decorator to create a section in the scene.
|
||||
|
||||
Example
|
||||
-------
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MyScene(Scene):
|
||||
sections_api = True
|
||||
|
||||
@section
|
||||
def first_section(self):
|
||||
pass
|
||||
|
||||
@section(skip=True, name="Introduce Bob")
|
||||
def second_section(self):
|
||||
pass
|
||||
|
||||
Parameters
|
||||
----------
|
||||
func : Callable
|
||||
The subsection.
|
||||
skip : bool, optional
|
||||
Whether to skip the section, by default False
|
||||
type\_ : str, optional
|
||||
The type of the section, by default :attr:`.DefaultSectionType.NORMAL`
|
||||
name : str, optional
|
||||
The name of the section, by default the name of the method.
|
||||
"""
|
||||
if func is not None:
|
||||
return SceneSection(func, **kwargs)
|
||||
|
||||
def wrapper(func: Callable[P, T]) -> SceneSection[P, T]:
|
||||
return SceneSection(func, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
|
@ -135,8 +135,8 @@ class ThreeDScene(Scene):
|
|||
}
|
||||
cam.add_updater(lambda m, dt: methods[about](rate * dt))
|
||||
self.add(self.camera)
|
||||
except Exception:
|
||||
raise ValueError("Invalid ambient rotation angle.")
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid ambient rotation angle.") from e
|
||||
|
||||
def stop_ambient_camera_rotation(self, about="theta"):
|
||||
"""
|
||||
|
|
@ -155,8 +155,8 @@ class ThreeDScene(Scene):
|
|||
self.remove(x)
|
||||
elif config.renderer == RendererType.OPENGL:
|
||||
self.camera.clear_updaters()
|
||||
except Exception:
|
||||
raise ValueError("Invalid ambient rotation angle.")
|
||||
except Exception as e:
|
||||
raise ValueError("Invalid ambient rotation angle.") from e
|
||||
|
||||
def begin_3dillusion_camera_rotation(
|
||||
self,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ __all__ = [
|
|||
"RGBA_Tuple_Int",
|
||||
"HSV_Array_Float",
|
||||
"HSV_Tuple_Float",
|
||||
"HSL_Array_Float",
|
||||
"HSL_Tuple_Float",
|
||||
"HSVA_Array_Float",
|
||||
"HSVA_Tuple_Float",
|
||||
"ManimColorInternal",
|
||||
"PointDType",
|
||||
"InternalPoint2D",
|
||||
|
|
@ -215,6 +219,46 @@ Its components describe, in order, the Hue, Saturation and Value (or
|
|||
Brightness) in the represented color.
|
||||
"""
|
||||
|
||||
HSVA_Array_Float: TypeAlias = RGBA_Array_Float
|
||||
"""``shape: (4,)``
|
||||
|
||||
A :class:`numpy.ndarray` of 4 floats between 0 and 1, representing a
|
||||
color in HSVA (or HSBA) format.
|
||||
|
||||
Its components describe, in order, the Hue, Saturation and Value (or
|
||||
Brightness) in the represented color.
|
||||
"""
|
||||
|
||||
HSVA_Tuple_Float: TypeAlias = RGBA_Tuple_Float
|
||||
"""``shape: (4,)``
|
||||
|
||||
A tuple of 4 floats between 0 and 1, representing a color in HSVA (or
|
||||
HSBA) format.
|
||||
|
||||
Its components describe, in order, the Hue, Saturation and Value (or
|
||||
Brightness) in the represented color.
|
||||
"""
|
||||
|
||||
HSL_Array_Float: TypeAlias = RGB_Array_Float
|
||||
"""``shape: (3,)``
|
||||
|
||||
A :class:`numpy.ndarray` of 3 floats between 0 and 1, representing a
|
||||
color in HSL format.
|
||||
|
||||
Its components describe, in order, the Hue, Saturation and Lightness
|
||||
in the represented color.
|
||||
"""
|
||||
|
||||
HSL_Tuple_Float: TypeAlias = RGB_Tuple_Float
|
||||
"""``shape: (3,)``
|
||||
|
||||
A :class:`numpy.ndarray` of 3 floats between 0 and 1, representing a
|
||||
color in HSL format.
|
||||
|
||||
Its components describe, in order, the Hue, Saturation and Lightness
|
||||
in the represented color.
|
||||
"""
|
||||
|
||||
ManimColorInternal: TypeAlias = RGBA_Array_Float
|
||||
"""``shape: (4,)``
|
||||
|
||||
|
|
|
|||
|
|
@ -1855,9 +1855,7 @@ def is_closed(points: Point3D_Array) -> bool:
|
|||
return False
|
||||
if abs(end[1] - start[1]) > tolerance[1]:
|
||||
return False
|
||||
if abs(end[2] - start[2]) > tolerance[2]:
|
||||
return False
|
||||
return True
|
||||
return abs(end[2] - start[2]) <= tolerance[2]
|
||||
|
||||
|
||||
def proportions_along_bezier_curve_for_point(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,27 @@ Note this way uses the name of the colors in UPPERCASE.
|
|||
|
||||
The colors of type "C" have an alias equal to the colorname without a letter,
|
||||
e.g. GREEN = GREEN_C
|
||||
|
||||
===================
|
||||
Custom Color Spaces
|
||||
===================
|
||||
|
||||
Hello dear visitor, you seem to be interested in implementing a custom color class for a color space we don't currently support.
|
||||
|
||||
The current system is using a few indirections for ensuring a consistent behavior with all other color types in manim.
|
||||
|
||||
To implement a custom color space you must subclass :class:`ManimColor` and implement three important functions
|
||||
|
||||
:attr:`~.ManimColor._internal_value` is an ``@property`` implemented on :class:`ManimColor` with the goal of keeping a consistent internal representation that can be referenced by other functions in :class:`ManimColor`.
|
||||
The getter should always return a value in the format of ``[r,g,b,a]`` as a numpy array which is in accordance with the type :class:`.ManimColorInternal`.
|
||||
The setter should always accept a value in the format ``[r,g,b,a]`` which can be converted to whatever attributes you need.
|
||||
This property acts as a proxy to whatever representation you need in your class.
|
||||
|
||||
:attr:`~ManimColor._internal_space` this is a readonly ``@property`` implemented on :class:`ManimColor` with the goal of a useful representation that can be used by operators and interpolation and color transform functions.
|
||||
The only constraints on this value are that it needs to be a numpy array and the last value must be the opacity in a range ``0.0`` to ``1.0``.
|
||||
Additionally your ``__init__`` must support this format as initialization value without additional parameters to ensure correct functionality of all other methods in :class:`ManimColor`.
|
||||
|
||||
:func:`~ManimColor._from_internal` is a ``@classmethod`` that converts an ``[r,g,b,a]`` value into suitable parameters for your ``__init__`` method and calls the cls parameter.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -32,13 +53,18 @@ from typing import Any, TypeVar, Union, overload
|
|||
|
||||
import numpy as np
|
||||
import numpy.typing as npt
|
||||
from typing_extensions import Self, TypeAlias
|
||||
from typing_extensions import Self, TypeAlias, TypeGuard, override
|
||||
|
||||
from manim.typing import (
|
||||
HSL_Array_Float,
|
||||
HSL_Tuple_Float,
|
||||
HSV_Array_Float,
|
||||
HSV_Tuple_Float,
|
||||
HSVA_Array_Float,
|
||||
HSVA_Tuple_Float,
|
||||
ManimColorDType,
|
||||
ManimColorInternal,
|
||||
ManimFloat,
|
||||
RGB_Array_Float,
|
||||
RGB_Array_Int,
|
||||
RGB_Tuple_Float,
|
||||
|
|
@ -132,7 +158,7 @@ class ManimColor:
|
|||
# This is not expected to be called on module initialization time
|
||||
# It can be horribly slow to convert a string to a color because
|
||||
# it has to access the dictionary of colors and find the right color
|
||||
self._internal_value = ManimColor._internal_from_string(value)
|
||||
self._internal_value = ManimColor._internal_from_string(value, alpha)
|
||||
elif isinstance(value, (list, tuple, np.ndarray)):
|
||||
length = len(value)
|
||||
if all(isinstance(x, float) for x in value):
|
||||
|
|
@ -147,8 +173,8 @@ class ManimColor:
|
|||
else:
|
||||
if length == 3:
|
||||
self._internal_value = ManimColor._internal_from_int_rgb(
|
||||
value,
|
||||
alpha, # type: ignore
|
||||
value, # type: ignore
|
||||
alpha,
|
||||
)
|
||||
elif length == 4:
|
||||
self._internal_value = ManimColor._internal_from_int_rgba(value) # type: ignore
|
||||
|
|
@ -160,7 +186,6 @@ class ManimColor:
|
|||
result = re_hex.search(value.get_hex())
|
||||
if result is None:
|
||||
raise ValueError(f"Failed to parse a color from {value}")
|
||||
|
||||
self._internal_value = ManimColor._internal_from_hex_string(
|
||||
result.group(), alpha
|
||||
)
|
||||
|
|
@ -172,6 +197,14 @@ class ManimColor:
|
|||
f"list[float, float, float, float], not {type(value)}"
|
||||
)
|
||||
|
||||
@property
|
||||
def _internal_space(self) -> npt.NDArray[ManimFloat]:
|
||||
"""
|
||||
This is a readonly property which is a custom representation for color space operations.
|
||||
It is used for operators and can be used when implementing a custom color space.
|
||||
"""
|
||||
return self._internal_value
|
||||
|
||||
@property
|
||||
def _internal_value(self) -> ManimColorInternal:
|
||||
"""Returns the internal value of the current Manim color [r,g,b,a] float array
|
||||
|
|
@ -203,6 +236,14 @@ class ManimColor:
|
|||
raise TypeError("Array must have 4 values exactly")
|
||||
self.__value: ManimColorInternal = value
|
||||
|
||||
@classmethod
|
||||
def _construct_from_space(cls, _space) -> Self:
|
||||
"""
|
||||
This function is used as a proxy for constructing a color with an internal value,
|
||||
this can be used by subclasses to hook into the construction of new objects using the internal value format
|
||||
"""
|
||||
return cls(_space)
|
||||
|
||||
@staticmethod
|
||||
def _internal_from_integer(value: int, alpha: float) -> ManimColorInternal:
|
||||
return np.asarray(
|
||||
|
|
@ -215,9 +256,8 @@ class ManimColor:
|
|||
dtype=ManimColorDType,
|
||||
)
|
||||
|
||||
# TODO: Maybe make 8 nibble hex also convertible ?
|
||||
@staticmethod
|
||||
def _internal_from_hex_string(hex: str, alpha: float) -> ManimColorInternal:
|
||||
def _internal_from_hex_string(hex_: str, alpha: float) -> ManimColorInternal:
|
||||
"""Internal function for converting a hex string into the internal representation of a ManimColor.
|
||||
|
||||
.. warning::
|
||||
|
|
@ -231,16 +271,22 @@ class ManimColor:
|
|||
hex : str
|
||||
hex string to be parsed
|
||||
alpha : float
|
||||
alpha value used for the color
|
||||
alpha value used for the color if the color is only 3 bytes long, if the color is 4 bytes long the parameter will not be used
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColorInternal
|
||||
Internal color representation
|
||||
"""
|
||||
if len(hex) == 6:
|
||||
hex += "00"
|
||||
tmp = int(hex, 16)
|
||||
if len(hex_) == 6:
|
||||
hex_ += "FF"
|
||||
elif len(hex_) == 8:
|
||||
alpha = (int(hex_, 16) & 0xFF) / 255
|
||||
else:
|
||||
raise ValueError(
|
||||
"Hex colors must be specified with either 0x or # as prefix and contain 6 or 8 hexadecimal numbers"
|
||||
)
|
||||
tmp = int(hex_, 16)
|
||||
return np.asarray(
|
||||
(
|
||||
((tmp >> 24) & 0xFF) / 255,
|
||||
|
|
@ -340,7 +386,7 @@ class ManimColor:
|
|||
return np.asarray(rgba, dtype=ManimColorDType)
|
||||
|
||||
@staticmethod
|
||||
def _internal_from_string(name: str) -> ManimColorInternal:
|
||||
def _internal_from_string(name: str, alpha: float) -> ManimColorInternal:
|
||||
"""Internal function for converting a string into the internal representation of a ManimColor.
|
||||
This is not used for hex strings, please refer to :meth:`_internal_from_hex` for this functionality.
|
||||
|
||||
|
|
@ -364,10 +410,9 @@ class ManimColor:
|
|||
"""
|
||||
from . import _all_color_dict
|
||||
|
||||
upper_name = name.upper()
|
||||
|
||||
if upper_name in _all_color_dict:
|
||||
return _all_color_dict[upper_name]._internal_value
|
||||
if tmp := _all_color_dict.get(name.upper()):
|
||||
tmp._internal_value[3] = alpha
|
||||
return tmp._internal_value.copy()
|
||||
else:
|
||||
raise ValueError(f"Color {name} not found")
|
||||
|
||||
|
|
@ -382,9 +427,8 @@ class ManimColor:
|
|||
.. warning::
|
||||
This will return only the rgb part of the color
|
||||
"""
|
||||
return int.from_bytes(
|
||||
(self._internal_value[:3] * 255).astype(int).tobytes(), "big"
|
||||
)
|
||||
tmp = (self._internal_value[:3] * 255).astype(dtype=np.byte).tobytes()
|
||||
return int.from_bytes(tmp, "big")
|
||||
|
||||
def to_rgb(self) -> RGB_Array_Float:
|
||||
"""Converts the current ManimColor into a rgb array of floats
|
||||
|
|
@ -498,9 +542,25 @@ class ManimColor:
|
|||
HSV_Array_Float
|
||||
A hsv array containing 3 elements of type float ranging from 0 to 1
|
||||
"""
|
||||
return colorsys.rgb_to_hsv(*self.to_rgb())
|
||||
return np.array(colorsys.rgb_to_hsv(*self.to_rgb()))
|
||||
|
||||
def invert(self, with_alpha=False) -> ManimColor:
|
||||
def to_hsl(self) -> HSL_Array_Float:
|
||||
"""Converts the Manim Color to HSL array.
|
||||
|
||||
.. note::
|
||||
Be careful this returns an array in the form `[h, s, l]` where the elements are floats.
|
||||
This might be confusing because rgb can also be an array of floats so you might want to annotate the usage
|
||||
of this function in your code by typing the variables with :class:`HSL_Array_Float` in order to differentiate
|
||||
between rgb arrays and hsl arrays
|
||||
|
||||
Returns
|
||||
-------
|
||||
HSL_Array_Float
|
||||
A hsl array containing 3 elements of type float ranging from 0 to 1
|
||||
"""
|
||||
return np.array(colorsys.rgb_to_hls(*self.to_rgb()))
|
||||
|
||||
def invert(self, with_alpha=False) -> Self:
|
||||
"""Returns an linearly inverted version of the color (no inplace changes)
|
||||
|
||||
Parameters
|
||||
|
|
@ -517,9 +577,15 @@ class ManimColor:
|
|||
ManimColor
|
||||
The linearly inverted ManimColor
|
||||
"""
|
||||
return ManimColor(1.0 - self._internal_value, with_alpha)
|
||||
if with_alpha:
|
||||
return self._construct_from_space(1.0 - self._internal_space)
|
||||
else:
|
||||
alpha = self._internal_space[3]
|
||||
new = 1.0 - self._internal_space
|
||||
new[-1] = alpha
|
||||
return self._construct_from_space(new)
|
||||
|
||||
def interpolate(self, other: ManimColor, alpha: float) -> ManimColor:
|
||||
def interpolate(self, other: ManimColor, alpha: float) -> Self:
|
||||
"""Interpolates between the current and the given ManimColor an returns the interpolated color
|
||||
|
||||
Parameters
|
||||
|
|
@ -536,31 +602,62 @@ class ManimColor:
|
|||
ManimColor
|
||||
The interpolated ManimColor
|
||||
"""
|
||||
return ManimColor(
|
||||
self._internal_value * (1 - alpha) + other._internal_value * alpha
|
||||
return self._construct_from_space(
|
||||
self._internal_space * (1 - alpha) + other._internal_space * alpha
|
||||
)
|
||||
|
||||
def set_opacity(self, opacity: float) -> ManimColor:
|
||||
"""Sets the alpha value for the current ManimColor
|
||||
@overload
|
||||
def opacity(self, opacity: float) -> Self: ...
|
||||
|
||||
@overload
|
||||
def opacity(self, opacity: None = None) -> float: ...
|
||||
|
||||
def opacity(self, opacity: float | None = None) -> float | Self:
|
||||
"""Creates a new ManimColor with the given opacity and the same color value as before, or returns opacity.
|
||||
|
||||
If no opacity is passed it will return the current opacity value. Otherwise, it will set the opacity
|
||||
to the given value, returning a new ManimColor object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
opacity : float
|
||||
|
||||
The new opacity value to be used
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColor
|
||||
The color with the changed opacity value
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Raises an exception if the opacity value is not in range 0 to 1
|
||||
The new ManimColor with the same color value but the new opacity
|
||||
"""
|
||||
if opacity < 0 or opacity > 1:
|
||||
raise ValueError(f"Alpha value is not in range 0-1 it is {opacity}")
|
||||
return ManimColor(self._internal_value[:3], opacity)
|
||||
if opacity is None:
|
||||
return self._internal_space[-1]
|
||||
tmp = self._internal_space.copy()
|
||||
tmp[-1] = opacity
|
||||
return self._construct_from_space(tmp)
|
||||
|
||||
def into(self, classtype: type[ManimColorT]) -> ManimColorT:
|
||||
"""Converts the current color into a different colorspace that is given without changing the _internal_value
|
||||
|
||||
Parameters
|
||||
----------
|
||||
classtype : type[ManimColorT]
|
||||
The class that is used for conversion, it must be a subclass of ManimColor which respects the specification
|
||||
HSV, RGBA, ...
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColorT
|
||||
Color object of the type passed into classtype with the same internal value as previously
|
||||
"""
|
||||
return classtype._from_internal(self._internal_value)
|
||||
|
||||
@classmethod
|
||||
def _from_internal(cls, value: ManimColorInternal) -> Self:
|
||||
"""This function is intended to be overwritten by custom color space classes which are subtypes of ManimColor.
|
||||
|
||||
The function constructs a new object of the given class by transforming the value in the internal format ``[r,g,b,a]``
|
||||
into a format which the constructor of the custom class can understand. Look at :class:`.HSV` for an example.
|
||||
"""
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def from_rgb(
|
||||
|
|
@ -587,7 +684,7 @@ class ManimColor:
|
|||
ManimColor
|
||||
Returns the ManimColor object
|
||||
"""
|
||||
return cls(rgb, alpha)
|
||||
return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
|
||||
|
||||
@classmethod
|
||||
def from_rgba(
|
||||
|
|
@ -612,12 +709,12 @@ class ManimColor:
|
|||
return cls(rgba)
|
||||
|
||||
@classmethod
|
||||
def from_hex(cls, hex: str, alpha: float = 1.0) -> Self:
|
||||
def from_hex(cls, hex_str: str, alpha: float = 1.0) -> Self:
|
||||
"""Creates a Manim Color from a hex string, prefixes allowed # and 0x
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hex : str
|
||||
hex_str : str
|
||||
The hex string to be converted (currently only supports 6 nibbles)
|
||||
alpha : float, optional
|
||||
alpha value to be used for the hex string, by default 1.0
|
||||
|
|
@ -627,7 +724,7 @@ class ManimColor:
|
|||
ManimColor
|
||||
The ManimColor represented by the hex string
|
||||
"""
|
||||
return cls(hex, alpha)
|
||||
return cls._from_internal(ManimColor(hex_str, alpha)._internal_value)
|
||||
|
||||
@classmethod
|
||||
def from_hsv(
|
||||
|
|
@ -648,7 +745,28 @@ class ManimColor:
|
|||
The ManimColor with the corresponding RGB values to the HSV
|
||||
"""
|
||||
rgb = colorsys.hsv_to_rgb(*hsv)
|
||||
return cls(rgb, alpha)
|
||||
return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
|
||||
|
||||
@classmethod
|
||||
def from_hsl(
|
||||
cls, hsl: HSL_Array_Float | HSL_Tuple_Float, alpha: float = 1.0
|
||||
) -> Self:
|
||||
"""Creates a ManimColor from an HSL Array
|
||||
|
||||
Parameters
|
||||
----------
|
||||
hsl : HSL_Array_Float | HSL_Tuple_Float
|
||||
Any 3 Element Iterable containing floats from 0-1
|
||||
alpha : float, optional
|
||||
the alpha value to be used, by default 1.0
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColor
|
||||
The ManimColor with the corresponding RGB values to the HSL
|
||||
"""
|
||||
rgb = colorsys.hls_to_rgb(*hsl)
|
||||
return cls._from_internal(ManimColor(rgb, alpha)._internal_value)
|
||||
|
||||
@overload
|
||||
@classmethod
|
||||
|
|
@ -669,7 +787,7 @@ class ManimColor:
|
|||
@classmethod
|
||||
def parse(
|
||||
cls,
|
||||
color: ParsableManimColor | list[ParsableManimColor] | None,
|
||||
color: ParsableManimColor | Sequence[ParsableManimColor] | None,
|
||||
alpha: float = 1.0,
|
||||
) -> Self | list[Self]:
|
||||
"""
|
||||
|
|
@ -687,9 +805,19 @@ class ManimColor:
|
|||
ManimColor
|
||||
Either a list of colors or a singular color depending on the input
|
||||
"""
|
||||
if isinstance(color, (list, tuple)):
|
||||
return [cls(c, alpha) for c in color] # type: ignore
|
||||
return cls(color, alpha) # type: ignore
|
||||
|
||||
def is_sequence(colors) -> TypeGuard[Sequence[ParsableManimColor]]:
|
||||
return isinstance(colors, (list, tuple))
|
||||
|
||||
def is_parsable(color) -> TypeGuard[ParsableManimColor]:
|
||||
return not isinstance(color, (list, tuple))
|
||||
|
||||
if is_sequence(color):
|
||||
return [
|
||||
cls._from_internal(ManimColor(c, alpha)._internal_value) for c in color
|
||||
]
|
||||
elif is_parsable(color):
|
||||
return cls._from_internal(ManimColor(color, alpha)._internal_value)
|
||||
|
||||
@staticmethod
|
||||
def gradient(colors: list[ManimColor], length: int):
|
||||
|
|
@ -710,35 +838,225 @@ class ManimColor:
|
|||
)
|
||||
return np.allclose(self._internal_value, other._internal_value)
|
||||
|
||||
def __add__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value + other._internal_value)
|
||||
def __add__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space + other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space + other._internal_space
|
||||
)
|
||||
|
||||
def __sub__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value - other._internal_value)
|
||||
def __radd__(self, other: int | float | Self) -> Self:
|
||||
return self + other
|
||||
|
||||
def __mul__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value * other._internal_value)
|
||||
def __sub__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space - other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space - other._internal_space
|
||||
)
|
||||
|
||||
def __truediv__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value / other._internal_value)
|
||||
def __rsub__(self, other: int | float | Self) -> Self:
|
||||
return self - other
|
||||
|
||||
def __floordiv__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value // other._internal_value)
|
||||
def __mul__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space * other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space * other._internal_space
|
||||
)
|
||||
|
||||
def __mod__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value % other._internal_value)
|
||||
def __rmul__(self, other: int | float | Self) -> Self:
|
||||
return self * other
|
||||
|
||||
def __pow__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self._internal_value**other._internal_value)
|
||||
def __truediv__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space / other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space / other._internal_space
|
||||
)
|
||||
|
||||
def __and__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self.to_integer() & other.to_integer())
|
||||
def __rtruediv__(self, other: int | float | Self) -> Self:
|
||||
return self / other
|
||||
|
||||
def __or__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self.to_integer() | other.to_integer())
|
||||
def __floordiv__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space // other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space // other._internal_space
|
||||
)
|
||||
|
||||
def __xor__(self, other: ManimColor) -> ManimColor:
|
||||
return ManimColor(self.to_integer() ^ other.to_integer())
|
||||
def __rfloordiv__(self, other: int | float | Self) -> Self:
|
||||
return self // other
|
||||
|
||||
def __mod__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space % other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space % other._internal_space
|
||||
)
|
||||
|
||||
def __rmod__(self, other: int | float | Self) -> Self:
|
||||
return self % other
|
||||
|
||||
def __pow__(self, other: int | float | Self) -> Self:
|
||||
if isinstance(other, (int, float)):
|
||||
return self._construct_from_space(self._internal_space**other)
|
||||
else:
|
||||
return self._construct_from_space(
|
||||
self._internal_space**other._internal_space
|
||||
)
|
||||
|
||||
def __rpow__(self, other: int | float | Self) -> Self:
|
||||
return self**other
|
||||
|
||||
def __invert__(self) -> Self:
|
||||
return self.invert()
|
||||
|
||||
def __int__(self) -> int:
|
||||
return self.to_integer()
|
||||
|
||||
def __getitem__(self, index: int) -> float:
|
||||
return self._internal_space[index]
|
||||
|
||||
def __and__(self, other: Self) -> Self:
|
||||
return self._construct_from_space(
|
||||
self._internal_from_integer(self.to_integer() & int(other), 1.0)
|
||||
)
|
||||
|
||||
def __or__(self, other: Self) -> Self:
|
||||
return self._construct_from_space(
|
||||
self._internal_from_integer(self.to_integer() | int(other), 1.0)
|
||||
)
|
||||
|
||||
def __xor__(self, other: Self) -> Self:
|
||||
return self._construct_from_space(
|
||||
self._internal_from_integer(self.to_integer() ^ int(other), 1.0)
|
||||
)
|
||||
|
||||
|
||||
RGBA = ManimColor
|
||||
"""RGBA Color Space"""
|
||||
|
||||
|
||||
class HSV(ManimColor):
|
||||
"""HSV Color Space"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hsv: HSV_Array_Float | HSV_Tuple_Float | HSVA_Array_Float | HSVA_Tuple_Float,
|
||||
alpha: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(None)
|
||||
if len(hsv) == 3:
|
||||
self.__hsv: HSVA_Array_Float = np.asarray((*hsv, alpha))
|
||||
elif len(hsv) == 4:
|
||||
self.__hsv: HSVA_Array_Float = np.asarray(hsv)
|
||||
else:
|
||||
raise ValueError("HSV Color must be an array of 3 values")
|
||||
|
||||
@classmethod
|
||||
@override
|
||||
def _from_internal(cls, value: ManimColorInternal) -> Self:
|
||||
hsv = colorsys.rgb_to_hsv(*value[:3])
|
||||
hsva = [*hsv, value[-1]]
|
||||
return cls(np.array(hsva))
|
||||
|
||||
@property
|
||||
def hue(self) -> float:
|
||||
return self.__hsv[0]
|
||||
|
||||
@property
|
||||
def saturation(self) -> float:
|
||||
return self.__hsv[1]
|
||||
|
||||
@property
|
||||
def value(self) -> float:
|
||||
return self.__hsv[2]
|
||||
|
||||
@hue.setter
|
||||
def hue(self, value: float) -> None:
|
||||
self.__hsv[0] = value
|
||||
|
||||
@saturation.setter
|
||||
def saturation(self, value: float) -> None:
|
||||
self.__hsv[1] = value
|
||||
|
||||
@value.setter
|
||||
def value(self, value: float) -> None:
|
||||
self.__hsv[2] = value
|
||||
|
||||
@property
|
||||
def h(self) -> float:
|
||||
return self.__hsv[0]
|
||||
|
||||
@property
|
||||
def s(self) -> float:
|
||||
return self.__hsv[1]
|
||||
|
||||
@property
|
||||
def v(self) -> float:
|
||||
return self.__hsv[2]
|
||||
|
||||
@h.setter
|
||||
def h(self, value: float) -> None:
|
||||
self.__hsv[0] = value
|
||||
|
||||
@s.setter
|
||||
def s(self, value: float) -> None:
|
||||
self.__hsv[1] = value
|
||||
|
||||
@v.setter
|
||||
def v(self, value: float) -> None:
|
||||
self.__hsv[2] = value
|
||||
|
||||
@property
|
||||
def _internal_space(self) -> npt.NDArray:
|
||||
return self.__hsv
|
||||
|
||||
@property
|
||||
def _internal_value(self) -> ManimColorInternal:
|
||||
"""Returns the internal value of the current Manim color [r,g,b,a] float array
|
||||
|
||||
Returns
|
||||
-------
|
||||
ManimColorInternal
|
||||
internal color representation
|
||||
"""
|
||||
return np.array(
|
||||
[
|
||||
*colorsys.hsv_to_rgb(self.__hsv[0], self.__hsv[1], self.__hsv[2]),
|
||||
self.__alpha,
|
||||
],
|
||||
dtype=ManimColorDType,
|
||||
)
|
||||
|
||||
@_internal_value.setter
|
||||
def _internal_value(self, value: ManimColorInternal) -> None:
|
||||
"""Overwrites the internal color value of the ManimColor object
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : ManimColorInternal
|
||||
The value which will overwrite the current color
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
Raises a TypeError if an invalid array is passed
|
||||
"""
|
||||
if not isinstance(value, np.ndarray):
|
||||
raise TypeError("value must be a numpy array")
|
||||
if value.shape[0] != 4:
|
||||
raise TypeError("Array must have 4 values exactly")
|
||||
tmp = colorsys.rgb_to_hsv(value[0], value[1], value[2])
|
||||
self.__hsv = np.array(tmp)
|
||||
self.__alpha = value[3]
|
||||
|
||||
|
||||
ParsableManimColor: TypeAlias = Union[
|
||||
|
|
@ -1075,4 +1393,6 @@ __all__ = [
|
|||
"random_bright_color",
|
||||
"random_color",
|
||||
"get_shaded_rgb",
|
||||
"HSV",
|
||||
"RGBA",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ from decorator import decorate, decorator
|
|||
logger = logging.getLogger("manim")
|
||||
|
||||
|
||||
def _get_callable_info(callable: Callable) -> tuple[str, str]:
|
||||
def _get_callable_info(callable_: Callable, /) -> tuple[str, str]:
|
||||
"""Returns type and name of a callable.
|
||||
|
||||
Parameters
|
||||
|
|
@ -30,8 +30,8 @@ def _get_callable_info(callable: Callable) -> tuple[str, str]:
|
|||
The type and name of the callable. Type can can be one of "class", "method" (for
|
||||
functions defined in classes) or "function"). For methods, name is Class.method.
|
||||
"""
|
||||
what = type(callable).__name__
|
||||
name = callable.__qualname__
|
||||
what = type(callable_).__name__
|
||||
name = callable_.__qualname__
|
||||
if what == "function" and "." in name:
|
||||
what = "method"
|
||||
elif what != "function":
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ ALIAS_LIST = [
|
|||
alias_name
|
||||
for module_dict in ALIAS_DOCS_DICT.values()
|
||||
for category_dict in module_dict.values()
|
||||
for alias_name in category_dict.keys()
|
||||
for alias_name in category_dict
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -69,10 +69,7 @@ class ManimColorModuleDocumenter(Directive):
|
|||
luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
# Choose the font color based on the background luminance
|
||||
if luminance > 0.5:
|
||||
font_color = "black"
|
||||
else:
|
||||
font_color = "white"
|
||||
font_color = "black" if luminance > 0.5 else "white"
|
||||
|
||||
color_elements.append((member_name, member_obj.to_hex(), font_color))
|
||||
|
||||
|
|
|
|||
|
|
@ -227,11 +227,7 @@ class ManimDirective(Directive):
|
|||
+ self.options.get("ref_functions", [])
|
||||
+ self.options.get("ref_methods", [])
|
||||
)
|
||||
if ref_content:
|
||||
ref_block = "References: " + " ".join(ref_content)
|
||||
|
||||
else:
|
||||
ref_block = ""
|
||||
ref_block = "References: " + " ".join(ref_content) if ref_content else ""
|
||||
|
||||
if "quality" in self.options:
|
||||
quality = f'{self.options["quality"]}_quality'
|
||||
|
|
|
|||
|
|
@ -304,10 +304,7 @@ def get_sorted_integer_files(
|
|||
) -> list[str]:
|
||||
indexed_files = []
|
||||
for file in os.listdir(directory):
|
||||
if "." in file:
|
||||
index_str = file[: file.index(".")]
|
||||
else:
|
||||
index_str = file
|
||||
index_str = file[: file.index(".")] if "." in file else file
|
||||
|
||||
full_path = os.path.join(directory, file)
|
||||
if index_str.isdigit():
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ class _CustomEncoder(json.JSONEncoder):
|
|||
# We return the repr and not a list to avoid the JsonEncoder to iterate over it.
|
||||
return repr(o)
|
||||
elif hasattr(o, "__dict__"):
|
||||
temp = getattr(o, "__dict__")
|
||||
temp = o.__dict__
|
||||
# MappingProxy is scene-caching nightmare. It contains all of the object methods and attributes. We skip it as the mechanism will at some point process the object, but instantiated.
|
||||
# Indeed, there is certainly no case where scene-caching will receive only a non instancied object, as this is never used in the library or encouraged to be used user-side.
|
||||
if isinstance(temp, MappingProxyType):
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ def orthographic_projection_matrix(
|
|||
height=None,
|
||||
near=1,
|
||||
far=depth + 1,
|
||||
format=True,
|
||||
format_=True,
|
||||
):
|
||||
if width is None:
|
||||
width = config["frame_width"]
|
||||
|
|
@ -45,13 +45,15 @@ def orthographic_projection_matrix(
|
|||
[0, 0, 0, 1],
|
||||
],
|
||||
)
|
||||
if format:
|
||||
if format_:
|
||||
return matrix_to_shader_input(projection_matrix)
|
||||
else:
|
||||
return projection_matrix
|
||||
|
||||
|
||||
def perspective_projection_matrix(width=None, height=None, near=2, far=50, format=True):
|
||||
def perspective_projection_matrix(
|
||||
width=None, height=None, near=2, far=50, format_=True
|
||||
):
|
||||
if width is None:
|
||||
width = config["frame_width"] / 6
|
||||
if height is None:
|
||||
|
|
|
|||
68
manim/utils/progressbar.py
Normal file
68
manim/utils/progressbar.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
"""Create an abstraction over the progress bar used."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from typing import Protocol, cast
|
||||
|
||||
from tqdm.asyncio import tqdm as asyncio_tqdm
|
||||
from tqdm.auto import tqdm as auto_tqdm
|
||||
from tqdm.rich import tqdm as rich_tqdm
|
||||
from tqdm.std import TqdmExperimentalWarning as ExperimentalProgressBarWarning
|
||||
|
||||
__all__ = [
|
||||
"ProgressBar",
|
||||
"ProgressBarProtocol",
|
||||
"NullProgressBar",
|
||||
"ExperimentalProgressBarWarning",
|
||||
]
|
||||
|
||||
|
||||
# let tqdm figure out whether we're in a notebook
|
||||
# but replace the basic tqdm with tqdm.rich.tqdm
|
||||
if auto_tqdm is asyncio_tqdm: # noqa: SIM108
|
||||
tqdm = rich_tqdm
|
||||
else:
|
||||
# we're in a notebook
|
||||
# tell typecheckers to pretend like it's tqdm.rich.tqdm
|
||||
tqdm = cast(type[rich_tqdm], auto_tqdm)
|
||||
|
||||
|
||||
class ProgressBarProtocol(Protocol):
|
||||
def update(self, n: int) -> object: ...
|
||||
|
||||
|
||||
class ProgressBar(tqdm, contextlib.AbstractContextManager[ProgressBarProtocol]):
|
||||
"""A manim progress bar.
|
||||
|
||||
This abstracts away whether a progress bar is used in a notebook, or via the terminal,
|
||||
or something else.
|
||||
|
||||
You may need to ignore warnings from ``tqdm``, due to the experimental nature of
|
||||
``tqdm.notebook.tqdm`` and ``tqdm.rich.tqdm``. This can be done with something like:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import warnings
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=ExperimentalProgressBarWarning)
|
||||
return ProgressBar(...)
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This warning filtering could have been done in the constructor, but would
|
||||
have caused the loss of autocomplete with the ``__init__`` of ``tqdm``, as
|
||||
well as possibly hide issues with the progressbar. Therefore, the warning
|
||||
filtering is left to the user.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NullProgressBar(ProgressBarProtocol):
|
||||
"""Fake progressbar."""
|
||||
|
||||
def update(self, n: int) -> None:
|
||||
"""Do nothing"""
|
||||
|
|
@ -495,10 +495,7 @@ def regular_vertices(
|
|||
"""
|
||||
|
||||
if start_angle is None:
|
||||
if n % 2 == 0:
|
||||
start_angle = 0
|
||||
else:
|
||||
start_angle = TAU / 4
|
||||
start_angle = 0 if n % 2 == 0 else TAU / 4
|
||||
|
||||
start_vector = rotate_vector(RIGHT * radius, start_angle)
|
||||
vertices = compass_directions(n, start_vector)
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ class _FramesTester:
|
|||
warnings.warn(
|
||||
f"Mismatch of {number_of_mismatches} pixel values in frame {frame_number} "
|
||||
f"against control data in {self._file_path}. Below error threshold, "
|
||||
"continuing..."
|
||||
"continuing...",
|
||||
stacklevel=1,
|
||||
)
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ def _texcode_for_environment(environment: str) -> tuple[str, str]:
|
|||
begin += "}"
|
||||
|
||||
# While the \end command terminates at the first closing brace
|
||||
split_at_brace = re.split("}", environment, 1)
|
||||
split_at_brace = re.split("}", environment, maxsplit=1)
|
||||
end = r"\end{" + split_at_brace[0] + "}"
|
||||
|
||||
return begin, end
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ classifiers= [
|
|||
"Topic :: Scientific/Engineering",
|
||||
"Topic :: Multimedia :: Video",
|
||||
"Topic :: Multimedia :: Graphics",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
|
|
@ -27,7 +25,7 @@ packages = [
|
|||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9,<3.13"
|
||||
python = ">=3.10,<3.13"
|
||||
av = ">=9.0.0"
|
||||
click = ">=8.0"
|
||||
cloup = ">=2.0.0"
|
||||
|
|
@ -64,13 +62,8 @@ jupyterlab = ["jupyterlab", "notebook"]
|
|||
gui = ["dearpygui"]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
black = ">=23.11,<25.0"
|
||||
flake8 = "^6.1.0"
|
||||
flake8-bugbear = "^23.11.28"
|
||||
flake8-builtins = "^2.2.0"
|
||||
flake8-comprehensions = "^3.7.0"
|
||||
flake8-docstrings = "^1.7.0"
|
||||
flake8-simplify = "^0.14.1"
|
||||
furo = "^2023.09.10"
|
||||
gitpython = "^3"
|
||||
isort = "^5.12.0"
|
||||
|
|
@ -137,14 +130,23 @@ fix = true
|
|||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"A",
|
||||
"B",
|
||||
"C4",
|
||||
"E",
|
||||
"F",
|
||||
"I",
|
||||
"PT",
|
||||
"SIM",
|
||||
"UP",
|
||||
]
|
||||
|
||||
ignore = [
|
||||
# mutable argument defaults (too many changes)
|
||||
"B006",
|
||||
# No function calls in defaults
|
||||
# ignored because np.array() and straight_path()
|
||||
"B008",
|
||||
# due to the import * used in manim
|
||||
"F403",
|
||||
"F405",
|
||||
|
|
@ -164,6 +166,10 @@ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
# flake8-builtins
|
||||
"A",
|
||||
# unused expression
|
||||
"B018",
|
||||
# unused variable
|
||||
"F841",
|
||||
# from __future__ import annotations
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ def process_pullrequests(lst, cur, github_repo, pr_nums):
|
|||
authors.add(pr.user)
|
||||
reviewers = reviewers.union(rev.user for rev in pr.get_reviews())
|
||||
pr_labels = [label.name for label in pr.labels]
|
||||
for label in PR_LABELS.keys():
|
||||
for label in PR_LABELS:
|
||||
if label in pr_labels:
|
||||
pr_by_labels[label].append(pr)
|
||||
break # ensure that PR is only added in one category
|
||||
|
|
@ -291,7 +291,7 @@ def main(token, prior, tag, additional, outfile):
|
|||
)
|
||||
|
||||
pr_by_labels = contributions["PRs"]
|
||||
for label in PR_LABELS.keys():
|
||||
for label in PR_LABELS:
|
||||
pr_of_label = pr_by_labels[label]
|
||||
|
||||
if pr_of_label:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import colorsys
|
||||
|
||||
import numpy as np
|
||||
import numpy.testing as nt
|
||||
|
||||
from manim.utils.color import BLACK, WHITE, ManimColor, ManimColorDType
|
||||
from manim.utils.color import (
|
||||
BLACK,
|
||||
HSV,
|
||||
RED,
|
||||
WHITE,
|
||||
YELLOW,
|
||||
ManimColor,
|
||||
ManimColorDType,
|
||||
)
|
||||
from manim.utils.color.XKCD import GREEN
|
||||
|
||||
|
||||
def test_init_with_int() -> None:
|
||||
|
|
@ -20,3 +31,145 @@ def test_init_with_int() -> None:
|
|||
nt.assert_array_equal(
|
||||
color._internal_value, np.array([1.0, 1.0, 1.0, 1.0], dtype=ManimColorDType)
|
||||
)
|
||||
|
||||
|
||||
def test_init_with_hex() -> None:
|
||||
color = ManimColor("0xFF0000")
|
||||
nt.assert_array_equal(color._internal_value, np.array([1, 0, 0, 1]))
|
||||
color = ManimColor("0xFF000000")
|
||||
nt.assert_array_equal(color._internal_value, np.array([1, 0, 0, 0]))
|
||||
|
||||
color = ManimColor("#FF0000")
|
||||
nt.assert_array_equal(color._internal_value, np.array([1, 0, 0, 1]))
|
||||
color = ManimColor("#FF000000")
|
||||
nt.assert_array_equal(color._internal_value, np.array([1, 0, 0, 0]))
|
||||
|
||||
|
||||
def test_init_with_string() -> None:
|
||||
color = ManimColor("BLACK")
|
||||
nt.assert_array_equal(color._internal_value, BLACK._internal_value)
|
||||
|
||||
|
||||
def test_init_with_tuple_int() -> None:
|
||||
color = ManimColor((50, 10, 50))
|
||||
nt.assert_array_equal(
|
||||
color._internal_value, np.array([50 / 255, 10 / 255, 50 / 255, 1.0])
|
||||
)
|
||||
|
||||
color = ManimColor((50, 10, 50, 50))
|
||||
nt.assert_array_equal(
|
||||
color._internal_value, np.array([50 / 255, 10 / 255, 50 / 255, 50 / 255])
|
||||
)
|
||||
|
||||
|
||||
def test_init_with_tuple_float() -> None:
|
||||
color = ManimColor((0.5, 0.6, 0.7))
|
||||
nt.assert_array_equal(color._internal_value, np.array([0.5, 0.6, 0.7, 1.0]))
|
||||
|
||||
color = ManimColor((0.5, 0.6, 0.7, 0.1))
|
||||
nt.assert_array_equal(color._internal_value, np.array([0.5, 0.6, 0.7, 0.1]))
|
||||
|
||||
|
||||
def test_to_integer() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_equal(color.to_integer(), 0x010203)
|
||||
|
||||
|
||||
def test_to_rgb() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_array_equal(color.to_rgb(), (0x1 / 255, 0x2 / 255, 0x3 / 255))
|
||||
nt.assert_array_equal(color.to_int_rgb(), (0x1, 0x2, 0x3))
|
||||
nt.assert_array_equal(color.to_rgba(), (0x1 / 255, 0x2 / 255, 0x3 / 255, 0x4 / 255))
|
||||
nt.assert_array_equal(color.to_int_rgba(), (0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_array_equal(
|
||||
color.to_rgba_with_alpha(0.5), (0x1 / 255, 0x2 / 255, 0x3 / 255, 0.5)
|
||||
)
|
||||
nt.assert_array_equal(
|
||||
color.to_int_rgba_with_alpha(0.5), (0x1, 0x2, 0x3, int(0.5 * 255))
|
||||
)
|
||||
|
||||
|
||||
def test_to_hex() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_equal(color.to_hex(), "#010203")
|
||||
nt.assert_equal(color.to_hex(True), "#01020304")
|
||||
|
||||
|
||||
def test_to_hsv() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_array_equal(
|
||||
color.to_hsv(), colorsys.rgb_to_hsv(0x1 / 255, 0x2 / 255, 0x3 / 255)
|
||||
)
|
||||
|
||||
|
||||
def test_to_hsl() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
nt.assert_array_equal(
|
||||
color.to_hsl(), colorsys.rgb_to_hls(0x1 / 255, 0x2 / 255, 0x3 / 255)
|
||||
)
|
||||
|
||||
|
||||
def test_invert() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
rgba = color._internal_value
|
||||
inverted = color.invert()
|
||||
nt.assert_array_equal(
|
||||
inverted._internal_value, (1 - rgba[0], 1 - rgba[1], 1 - rgba[2], rgba[3])
|
||||
)
|
||||
|
||||
|
||||
def test_invert_with_alpha() -> None:
|
||||
color = ManimColor((0x1, 0x2, 0x3, 0x4))
|
||||
rgba = color._internal_value
|
||||
inverted = color.invert(True)
|
||||
nt.assert_array_equal(
|
||||
inverted._internal_value, (1 - rgba[0], 1 - rgba[1], 1 - rgba[2], 1 - rgba[3])
|
||||
)
|
||||
|
||||
|
||||
def test_interpolate() -> None:
|
||||
r1 = RED._internal_value
|
||||
r2 = YELLOW._internal_value
|
||||
nt.assert_array_equal(
|
||||
RED.interpolate(YELLOW, 0.5)._internal_value, 0.5 * r1 + 0.5 * r2
|
||||
)
|
||||
|
||||
|
||||
def test_opacity() -> None:
|
||||
nt.assert_equal(RED.opacity(0.5)._internal_value[3], 0.5)
|
||||
|
||||
|
||||
def test_parse() -> None:
|
||||
nt.assert_equal(ManimColor.parse([RED, YELLOW]), [RED, YELLOW])
|
||||
|
||||
|
||||
def test_mc_operators() -> None:
|
||||
c1 = RED
|
||||
c2 = GREEN
|
||||
halfway1 = 0.5 * c1 + 0.5 * c2
|
||||
halfway2 = c1.interpolate(c2, 0.5)
|
||||
nt.assert_equal(halfway1, halfway2)
|
||||
nt.assert_array_equal((WHITE / 2.0)._internal_value, np.array([0.5, 0.5, 0.5, 0.5]))
|
||||
|
||||
|
||||
def test_mc_from_functions() -> None:
|
||||
color = ManimColor.from_hex("#ff00a0")
|
||||
nt.assert_equal(color.to_hex(), "#FF00A0")
|
||||
|
||||
color = ManimColor.from_rgb((1.0, 1.0, 0.0))
|
||||
nt.assert_equal(color.to_hex(), "#FFFF00")
|
||||
|
||||
color = ManimColor.from_rgba((1.0, 1.0, 0.0, 1.0))
|
||||
nt.assert_equal(color.to_hex(True), "#FFFF00FF")
|
||||
|
||||
color = ManimColor.from_hsv((1.0, 1.0, 1.0), alpha=0.0)
|
||||
nt.assert_equal(color.to_hex(True), "#FF000000")
|
||||
|
||||
|
||||
def test_hsv_init() -> None:
|
||||
color = HSV((0.25, 1, 1))
|
||||
nt.assert_array_equal(color._internal_value, np.array([0.5, 1.0, 0.0, 1.0]))
|
||||
|
||||
|
||||
def test_into_HSV() -> None:
|
||||
nt.assert_equal(RED.into(HSV).into(ManimColor), RED)
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
16
tests/test_graphical_units/test_animation.py
Normal file
16
tests/test_graphical_units/test_animation.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
from manim import *
|
||||
from manim.animation.animation import DEFAULT_ANIMATION_RUN_TIME
|
||||
|
||||
__module_test__ = "animation"
|
||||
|
||||
|
||||
def test_animation_set_default():
|
||||
s = Square()
|
||||
Rotate.set_default(run_time=100)
|
||||
anim = Rotate(s)
|
||||
assert anim.run_time == 100
|
||||
anim = Rotate(s, run_time=5)
|
||||
assert anim.run_time == 5
|
||||
Rotate.set_default()
|
||||
anim = Rotate(s)
|
||||
assert anim.run_time == DEFAULT_ANIMATION_RUN_TIME
|
||||
|
|
@ -141,3 +141,33 @@ def test_number_plane_log(scene):
|
|||
)
|
||||
|
||||
scene.add(VGroup(plane1, plane2).arrange())
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_gradient_line_graph_x_axis(scene):
|
||||
"""Test that using `colorscale` generates a line whose gradient matches the y-axis"""
|
||||
axes = Axes(x_range=[-3, 3], y_range=[-3, 3])
|
||||
|
||||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale_axis=0,
|
||||
)
|
||||
|
||||
scene.add(axes, curve)
|
||||
|
||||
|
||||
@frames_comparison
|
||||
def test_gradient_line_graph_y_axis(scene):
|
||||
"""Test that using `colorscale` generates a line whose gradient matches the y-axis"""
|
||||
axes = Axes(x_range=[-3, 3], y_range=[-3, 3])
|
||||
|
||||
curve = axes.plot(
|
||||
lambda x: 0.1 * x**3,
|
||||
x_range=(-3, 3, 0.001),
|
||||
colorscale=[BLUE, GREEN, YELLOW, ORANGE, RED],
|
||||
colorscale_axis=1,
|
||||
)
|
||||
|
||||
scene.add(axes, curve)
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ class SceneWithSections(Scene):
|
|||
)
|
||||
self.wait(2)
|
||||
|
||||
self.next_section(type=PresentationSectionType.SKIP)
|
||||
self.next_section(section_type=PresentationSectionType.SKIP)
|
||||
self.wait()
|
||||
|
||||
self.next_section(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue