Merge branch 'main' into guide

This commit is contained in:
Jason Grace 2023-12-20 19:22:02 -05:00 committed by GitHub
commit 7866e007d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 499 additions and 336 deletions

0
docs/skip-manim Normal file
View file

View file

@ -5,6 +5,24 @@ If you are adding new features to manim, you should add appropriate tests for th
manim from breaking at each change by checking that no other
feature has been broken and/or been unintentionally modified.
.. warning::
The full tests suite requires Cairo 1.18 in order to run all tests.
However, Cairo 1.18 may not be available from your package manager,
like ``apt``, and it is very likely that you have an older version installed,
e.g., 1.16. If you run tests with a version prior to 1.18,
many tests will be skipped. Those tests are not skipped in the online CI.
If you want to run all tests locally, you need to install Cairo 1.18 or above.
You can do so by compiling Cairo from source:
1. download ``cairo-1.18.0.tar.xz`` from
`here <https://www.cairographics.org/releases/>`_.
and uncompress it;
2. open the INSTALL file and follow the instructions (you might need to install
``meson`` and ``ninja``);
3. run the tests suite and verify that the Cairo version is correct.
How Manim tests
---------------

View file

@ -376,13 +376,13 @@ and :meth:`~.Mobject.get_start`. Here is an example of some important coordinate
class MobjectExample(Scene):
def construct(self):
p1= [-1,-1,0]
p2= [1,-1,0]
p3= [1,1,0]
p4= [-1,1,0]
a = Line(p1,p2).append_points(Line(p2,p3).points).append_points(Line(p3,p4).points)
point_start= a.get_start()
point_end = a.get_end()
p1 = [-1,-1, 0]
p2 = [ 1,-1, 0]
p3 = [ 1, 1, 0]
p4 = [-1, 1, 0]
a = Line(p1,p2).append_points(Line(p2,p3).points).append_points(Line(p3,p4).points)
point_start = a.get_start()
point_end = a.get_end()
point_center = a.get_center()
self.add(Text(f"a.get_start() = {np.round(point_start,2).tolist()}", font_size=24).to_edge(UR).set_color(YELLOW))
self.add(Text(f"a.get_end() = {np.round(point_end,2).tolist()}", font_size=24).next_to(self.mobjects[-1],DOWN).set_color(RED))
@ -425,8 +425,8 @@ function of numpy:
self.camera.background_color = WHITE
m1a = Square().set_color(RED).shift(LEFT)
m1b = Circle().set_color(RED).shift(LEFT)
m2a= Square().set_color(BLUE).shift(RIGHT)
m2b= Circle().set_color(BLUE).shift(RIGHT)
m2a = Square().set_color(BLUE).shift(RIGHT)
m2b = Circle().set_color(BLUE).shift(RIGHT)
points = m2a.points
points = np.roll(points, int(len(points)/4), axis=0)

View file

@ -268,11 +268,9 @@ and animating those method calls with ``.animate``.
self.play(Create(square)) # show the square on screen
self.play(square.animate.rotate(PI / 4)) # rotate the square
self.play(Transform(square, circle)) # transform the square into a circle
self.play(
ReplacementTransform(square, circle)
) # transform the square into a circle
self.play(
circle.animate.set_fill(PINK, opacity=0.5)
square.animate.set_fill(PINK, opacity=0.5)
) # color the circle on screen
2. Render ``AnimatedSquareToCircle`` by running the following command in the command line:
@ -293,8 +291,8 @@ The following animation will render:
self.play(Create(square)) # show the square on screen
self.play(square.animate.rotate(PI / 4)) # rotate the square
self.play(ReplacementTransform(square, circle)) # transform the square into a circle
self.play(circle.animate.set_fill(PINK, opacity=0.5)) # color the circle on screen
self.play(Transform(square, circle)) # transform the square into a circle
self.play(square.animate.set_fill(PINK, opacity=0.5)) # color the circle on screen
The first ``self.play`` creates the square. The second animates rotating it 45 degrees.
The third transforms the square into a circle, and the last colors the circle pink.
@ -348,6 +346,55 @@ If you find that your own usage of ``.animate`` is causing similar unwanted beha
using conventional animation methods like the right square, which uses ``Rotate``.
``Transform`` vs ``ReplacementTransform``
*****************************************
The difference between ``Transform`` and ``ReplacementTransform`` is that ``Transform(mob1, mob2)`` transforms the points
(as well as other attributes like color) of ``mob1`` into the points/attributes of ``mob2``.
``ReplacementTransform(mob1, mob2)`` on the other hand literally replaces ``mob1`` on the scene with ``mob2``.
The use of ``ReplacementTransform`` or ``Transform`` is mostly up to personal preference. They can be used to accomplish the same effect, as shown below.
.. code-block:: python
class TwoTransforms(Scene):
def transform(self):
a = Circle()
b = Square()
c = Triangle()
self.play(Transform(a, b))
self.play(Transform(a, c))
self.play(FadeOut(a))
def replacement_transform(self):
a = Circle()
b = Square()
c = Triangle()
self.play(ReplacementTransform(a, b))
self.play(ReplacementTransform(b, c))
self.play(FadeOut(c))
def construct(self):
self.transform()
self.wait(0.5) # wait for 0.5 seconds
self.replacement_transform()
However, in some cases it is more beneficial to use ``Transform``, like when you are transforming several mobjects one after the other.
The code below avoids having to keep a reference to the last mobject that was transformed.
.. manim:: TransformCycle
class TransformCycle(Scene):
def construct(self):
a = Circle()
t1 = Square()
t2 = Triangle()
self.add(a)
self.wait()
for t in [t1,t2]:
self.play(Transform(a,t))
************
You're done!
************

View file

@ -167,8 +167,8 @@ Triangle.set_default(stroke_width=20)
class LineJoints(Scene):
def construct(self):
t1 = Triangle()
t2 = Triangle(line_join=LineJointType.ROUND)
t3 = Triangle(line_join=LineJointType.BEVEL)
t2 = Triangle(joint_type=LineJointType.ROUND)
t3 = Triangle(joint_type=LineJointType.BEVEL)
grp = VGroup(t1, t2, t3).arrange(RIGHT)
grp.set(width=config.frame_width - 1)

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import sys
import click
import cloup
from . import __version__, cli_ctx_settings, console
@ -9,18 +10,20 @@ from .cli.cfg.group import cfg
from .cli.checkhealth.commands import checkhealth
from .cli.default_group import DefaultGroup
from .cli.init.commands import init
from .cli.new.group import new
from .cli.plugins.commands import plugins
from .cli.render.commands import render
from .constants import EPILOG
def exit_early(ctx, param, value):
def show_splash(ctx, param, value):
if value:
sys.exit()
console.print(f"Manim Community [green]v{__version__}[/green]\n")
console.print(f"Manim Community [green]v{__version__}[/green]\n")
def print_version_and_exit(ctx, param, value):
show_splash(ctx, param, value)
if value:
ctx.exit()
@cloup.group(
@ -38,7 +41,16 @@ console.print(f"Manim Community [green]v{__version__}[/green]\n")
"--version",
is_flag=True,
help="Show version and exit.",
callback=exit_early,
callback=print_version_and_exit,
is_eager=True,
expose_value=False,
)
@click.option(
"--show-splash/--hide-splash",
is_flag=True,
default=True,
help="Print splash message with version information.",
callback=show_splash,
is_eager=True,
expose_value=False,
)
@ -52,7 +64,6 @@ main.add_command(checkhealth)
main.add_command(cfg)
main.add_command(plugins)
main.add_command(init)
main.add_command(new)
main.add_command(render)
if __name__ == "__main__":

View file

@ -191,6 +191,11 @@ class Animation:
method.
"""
if self.run_time <= 0:
raise ValueError(
f"{self} has a run_time of <= 0 seconds, this cannot be rendered correctly. "
"Please set the run_time to be positive"
)
self.starting_mobject = self.create_starting_mobject()
if self.suspend_mobject_updating:
# All calls to self.mobject's internal updaters

View file

@ -3,11 +3,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Sequence
import types
from typing import TYPE_CHECKING, Callable, Iterable, Sequence
import numpy as np
from manim.mobject.opengl.opengl_mobject import OpenGLGroup
from manim.utils.parameter_parsing import flatten_iterable_parameters
from .._config import config
from ..animation.animation import Animation, prepare_animation
@ -54,14 +56,15 @@ class AnimationGroup(Animation):
def __init__(
self,
*animations: Animation,
*animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
group: Group | VGroup | OpenGLGroup | OpenGLVGroup = None,
run_time: float | None = None,
rate_func: Callable[[float], float] = linear,
lag_ratio: float = 0,
**kwargs,
) -> None:
self.animations = [prepare_animation(anim) for anim in animations]
arg_anim = flatten_iterable_parameters(animations)
self.animations = [prepare_animation(anim) for anim in arg_anim]
self.rate_func = rate_func
self.group = group
if self.group is None:
@ -81,6 +84,16 @@ class AnimationGroup(Animation):
return list(self.group)
def begin(self) -> None:
if self.run_time <= 0:
tmp = (
"Please set the run_time to be positive"
if len(self.animations) != 0
else "Please add at least one Animation with positive run_time"
)
raise ValueError(
f"{self} has a run_time of 0 seconds, this cannot be "
f"rendered correctly. {tmp}."
)
if self.suspend_mobject_updating:
self.group.suspend_updating()
for anim in self.animations:

View file

@ -37,6 +37,14 @@ LINE_JOIN_MAP = {
}
CAP_STYLE_MAP = {
CapStyleType.AUTO: None, # TODO: this could be improved
CapStyleType.ROUND: cairo.LineCap.ROUND,
CapStyleType.BUTT: cairo.LineCap.BUTT,
CapStyleType.SQUARE: cairo.LineCap.SQUARE,
}
class Camera:
"""Base camera class.
@ -778,11 +786,13 @@ class Camera:
ctx.set_line_width(
width
* self.cairo_line_width_multiple
# This ensures lines have constant width as you zoom in on them.
* (self.frame_width / self.frame_width),
# This ensures lines have constant width as you zoom in on them.
)
if vmobject.joint_type != LineJointType.AUTO:
ctx.set_line_join(LINE_JOIN_MAP[vmobject.joint_type])
if vmobject.cap_style != CapStyleType.AUTO:
ctx.set_line_cap(CAP_STYLE_MAP[vmobject.cap_style])
ctx.stroke_preserve()
return self

View file

@ -1,189 +0,0 @@
from __future__ import annotations
import configparser
from pathlib import Path
import click
import cloup
from ... import console
from ...constants import CONTEXT_SETTINGS, EPILOG, QUALITIES
from ...utils.file_ops import (
add_import_statement,
copy_template_files,
get_template_names,
get_template_path,
)
CFG_DEFAULTS = {
"frame_rate": 30,
"background_color": "BLACK",
"background_opacity": 1,
"scene_names": "Default",
"resolution": (854, 480),
}
def select_resolution():
"""Prompts input of type click.Choice from user. Presents options from QUALITIES constant.
Returns
-------
:class:`tuple`
Tuple containing height and width.
"""
resolution_options = []
for quality in QUALITIES.items():
resolution_options.append(
(quality[1]["pixel_height"], quality[1]["pixel_width"]),
)
resolution_options.pop()
choice = click.prompt(
"\nSelect resolution:\n",
type=cloup.Choice([f"{i[0]}p" for i in resolution_options]),
show_default=False,
default="480p",
)
return [res for res in resolution_options if f"{res[0]}p" == choice][0]
def update_cfg(cfg_dict: dict, project_cfg_path: Path):
"""Updates the manim.cfg file after reading it from the project_cfg_path.
Parameters
----------
cfg_dict
values used to update manim.cfg found project_cfg_path.
project_cfg_path
Path of manim.cfg file.
"""
config = configparser.ConfigParser()
config.read(project_cfg_path)
cli_config = config["CLI"]
for key, value in cfg_dict.items():
if key == "resolution":
cli_config["pixel_height"] = str(value[0])
cli_config["pixel_width"] = str(value[1])
else:
cli_config[key] = str(value)
with project_cfg_path.open("w") as conf:
config.write(conf)
@cloup.command(
context_settings=CONTEXT_SETTINGS,
epilog=EPILOG,
)
@cloup.argument("project_name", type=Path, required=False)
@cloup.option(
"-d",
"--default",
"default_settings",
is_flag=True,
help="Default settings for project creation.",
nargs=1,
)
def project(default_settings, **args):
"""Creates a new project.
PROJECT_NAME is the name of the folder in which the new project will be initialized.
"""
if args["project_name"]:
project_name = args["project_name"]
else:
project_name = click.prompt("Project Name", type=Path)
# in the future when implementing a full template system. Choices are going to be saved in some sort of config file for templates
template_name = click.prompt(
"Template",
type=click.Choice(get_template_names(), False),
default="Default",
)
if project_name.is_dir():
console.print(
f"\nFolder [red]{project_name}[/red] exists. Please type another name\n",
)
else:
project_name.mkdir()
new_cfg = {}
new_cfg_path = Path.resolve(project_name / "manim.cfg")
if not default_settings:
for key, value in CFG_DEFAULTS.items():
if key == "scene_names":
new_cfg[key] = template_name + "Template"
elif key == "resolution":
new_cfg[key] = select_resolution()
else:
new_cfg[key] = click.prompt(f"\n{key}", default=value)
console.print("\n", new_cfg)
if click.confirm("Do you want to continue?", default=True, abort=True):
copy_template_files(project_name, template_name)
update_cfg(new_cfg, new_cfg_path)
else:
copy_template_files(project_name, template_name)
update_cfg(CFG_DEFAULTS, new_cfg_path)
@cloup.command(
context_settings=CONTEXT_SETTINGS,
no_args_is_help=True,
epilog=EPILOG,
)
@cloup.argument("scene_name", type=str, required=True)
@cloup.argument("file_name", type=str, required=False)
def scene(**args):
"""Inserts a SCENE to an existing FILE or creates a new FILE.
SCENE is the name of the scene that will be inserted.
FILE is the name of file in which the SCENE will be inserted.
"""
if not Path("main.py").exists():
raise FileNotFoundError(f"{Path('main.py')} : Not a valid project directory.")
template_name = click.prompt(
"template",
type=click.Choice(get_template_names(), False),
default="Default",
)
scene = (get_template_path() / f"{template_name}.mtp").resolve().read_text()
scene = scene.replace(template_name + "Template", args["scene_name"], 1)
if args["file_name"]:
file_name = Path(args["file_name"] + ".py")
if file_name.is_file():
# file exists so we are going to append new scene to that file
with file_name.open("a") as f:
f.write("\n\n\n" + scene)
else:
# file does not exist so we create a new file, append the scene and prepend the import statement
file_name.write_text("\n\n\n" + scene)
add_import_statement(file_name)
else:
# file name is not provided so we assume it is main.py
# if main.py does not exist we do not continue
with Path("main.py").open("a") as f:
f.write("\n\n\n" + scene)
@cloup.group(
context_settings=CONTEXT_SETTINGS,
invoke_without_command=True,
no_args_is_help=True,
epilog=EPILOG,
help="Create a new project or insert a new scene.",
deprecated=True,
)
@cloup.pass_context
def new(ctx):
pass
new.add_command(project)
new.add_command(scene)

View file

@ -76,6 +76,7 @@ __all__ = [
"CTRL_VALUE",
"RendererType",
"LineJointType",
"CapStyleType",
]
# Messages
@ -305,3 +306,41 @@ class LineJointType(Enum):
ROUND = 1
BEVEL = 2
MITER = 3
class CapStyleType(Enum):
"""Collection of available cap styles.
See the example below for a visual illustration of the different
cap styles.
Examples
--------
.. manim:: CapStyleVariants
:save_last_frame:
class CapStyleVariants(Scene):
def construct(self):
arcs = VGroup(*[
Arc(
radius=1,
start_angle=0,
angle=TAU / 4,
stroke_width=20,
color=GREEN,
cap_style=cap_style,
)
for cap_style in CapStyleType
])
arcs.arrange(RIGHT, buff=1)
self.add(arcs)
for arc in arcs:
label = Text(arc.cap_style.name, font_size=24).next_to(arc, DOWN)
self.add(label)
"""
AUTO = 0
ROUND = 1
BUTT = 2
SQUARE = 3

View file

@ -598,8 +598,10 @@ class Rectangle(Polygon):
def construct(self):
rect1 = Rectangle(width=4.0, height=2.0, grid_xstep=1.0, grid_ystep=0.5)
rect2 = Rectangle(width=1.0, height=4.0)
rect3 = Rectangle(width=2.0, height=2.0, grid_xstep=1.0, grid_ystep=1.0)
rect3.grid_lines.set_stroke(width=1)
rects = Group(rect1,rect2).arrange(buff=1)
rects = Group(rect1, rect2, rect3).arrange(buff=1)
self.add(rects)
"""
@ -617,10 +619,16 @@ class Rectangle(Polygon):
super().__init__(UR, UL, DL, DR, color=color, **kwargs)
self.stretch_to_fit_width(width)
self.stretch_to_fit_height(height)
v = self.get_vertices()
if grid_xstep is not None:
self.grid_lines = VGroup()
if grid_xstep or grid_ystep:
from manim.mobject.geometry.line import Line
v = self.get_vertices()
if grid_xstep:
grid_xstep = abs(grid_xstep)
count = int(width / grid_xstep)
grid = VGroup(
@ -633,8 +641,9 @@ class Rectangle(Polygon):
for i in range(1, count)
)
)
self.add(grid)
if grid_ystep is not None:
self.grid_lines.add(grid)
if grid_ystep:
grid_ystep = abs(grid_ystep)
count = int(height / grid_ystep)
grid = VGroup(
@ -647,7 +656,10 @@ class Rectangle(Polygon):
for i in range(1, count)
)
)
self.add(grid)
self.grid_lines.add(grid)
if self.grid_lines:
self.add(self.grid_lines)
class Square(Rectangle):

View file

@ -968,11 +968,11 @@ class Mobject:
class DtUpdater(Scene):
def construct(self):
line = Square()
square = Square()
#Let the line rotate 90° per second
line.add_updater(lambda mobject, dt: mobject.rotate(dt*90*DEGREES))
self.add(line)
#Let the square rotate 90° per second
square.add_updater(lambda mobject, dt: mobject.rotate(dt*90*DEGREES))
self.add(square)
self.wait(2)
See also
@ -1423,11 +1423,59 @@ class Mobject:
def to_corner(
self, corner: Vector3 = DL, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
) -> Self:
"""Moves this :class:`~.Mobject` to the given corner of the screen.
Returns
-------
:class:`.Mobject`
The newly positioned mobject.
Examples
--------
.. manim:: ToCornerExample
:save_last_frame:
class ToCornerExample(Scene):
def construct(self):
c = Circle()
c.to_corner(UR)
t = Tex("To the corner!")
t2 = MathTex("x^3").shift(DOWN)
self.add(c,t,t2)
t.to_corner(DL, buff=0)
t2.to_corner(UL, buff=1.5)
"""
return self.align_on_border(corner, buff)
def to_edge(
self, edge: Vector3 = LEFT, buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER
) -> Self:
"""Moves this :class:`~.Mobject` to the given edge of the screen,
without affecting its position in the other dimension.
Returns
-------
:class:`.Mobject`
The newly positioned mobject.
Examples
--------
.. manim:: ToEdgeExample
:save_last_frame:
class ToEdgeExample(Scene):
def construct(self):
tex_top = Tex("I am at the top!")
tex_top.to_edge(UP)
tex_side = Tex("I am moving to the side!")
c = Circle().shift(2*DOWN)
self.add(tex_top, tex_side)
tex_side.to_edge(LEFT)
c.to_edge(RIGHT, buff=0)
"""
return self.align_on_border(edge, buff)
def next_to(

View file

@ -49,6 +49,8 @@ Examples
from __future__ import annotations
import functools
__all__ = ["Text", "Paragraph", "MarkupText", "register_font"]
@ -407,6 +409,11 @@ class Text(SVGMobject):
"""
@staticmethod
@functools.lru_cache(maxsize=None)
def font_list() -> list[str]:
return manimpango.list_fonts()
def __init__(
self,
text: str,
@ -431,13 +438,12 @@ class Text(SVGMobject):
width: float = None,
should_center: bool = True,
disable_ligatures: bool = False,
use_svg_cache: bool = False,
**kwargs,
) -> None:
self.line_spacing = line_spacing
if font and warn_missing_font:
fonts_list = manimpango.list_fonts()
if font not in fonts_list:
logger.warning(f"Font {font} not in {fonts_list}.")
if font and warn_missing_font and font not in Text.font_list():
logger.warning(f"Font {font} not in {Text.font_list()}.")
self.font = font
self._font_size = float(font_size)
# needs to be a float or else size is inflated when font_size = 24
@ -491,7 +497,7 @@ class Text(SVGMobject):
height=height,
width=width,
should_center=should_center,
use_svg_cache=False,
use_svg_cache=use_svg_cache,
**kwargs,
)
self.text = text
@ -1133,6 +1139,11 @@ class MarkupText(SVGMobject):
"""
@staticmethod
@functools.lru_cache(maxsize=None)
def font_list() -> list[str]:
return manimpango.list_fonts()
def __init__(
self,
text: str,
@ -1156,10 +1167,8 @@ class MarkupText(SVGMobject):
) -> None:
self.text = text
self.line_spacing = line_spacing
if font and warn_missing_font:
fonts_list = manimpango.list_fonts()
if font not in fonts_list:
logger.warning(f"Font {font} not in {fonts_list}.")
if font and warn_missing_font and font not in Text.font_list():
logger.warning(f"Font {font} not in {Text.font_list()}.")
self.font = font
self._font_size = float(font_size)
self.slant = slant

View file

@ -191,7 +191,9 @@ class ImageMobject(AbstractImageMobject):
self.pixel_array, self.pixel_array_dtype
)
if self.invert:
self.pixel_array[:, :, :3] = 255 - self.pixel_array[:, :, :3]
self.pixel_array[:, :, :3] = (
np.iinfo(self.pixel_array_dtype).max - self.pixel_array[:, :, :3]
)
super().__init__(scale_to_resolution, **kwargs)
def get_pixel_array(self):

View file

@ -123,6 +123,7 @@ class VMobject(Mobject):
# TODO, do we care about accounting for varying zoom levels?
tolerance_for_point_equality: float = 1e-6,
n_points_per_cubic_curve: int = 4,
cap_style: CapStyleType = CapStyleType.AUTO,
**kwargs,
):
self.fill_opacity = fill_opacity
@ -150,6 +151,7 @@ class VMobject(Mobject):
self.shade_in_3d: bool = shade_in_3d
self.tolerance_for_point_equality: float = tolerance_for_point_equality
self.n_points_per_cubic_curve: int = n_points_per_cubic_curve
self.cap_style: CapStyleType = cap_style
super().__init__(**kwargs)
self.submobjects: list[VMobject]
@ -340,6 +342,34 @@ class VMobject(Mobject):
self.background_stroke_color = ManimColor(color)
return self
def set_cap_style(self, cap_style: CapStyleType) -> Self:
"""
Sets the cap style of the :class:`VMobject`.
Parameters
----------
cap_style
The cap style to be set. See :class:`.CapStyleType` for options.
Returns
-------
:class:`VMobject`
``self``
Examples
--------
.. manim:: CapStyleExample
:save_last_frame:
class CapStyleExample(Scene):
def construct(self):
line = Line(LEFT, RIGHT, color=YELLOW, stroke_width=20)
line.set_cap_style(CapStyleType.ROUND)
self.add(line)
"""
self.cap_style = cap_style
return self
def set_background_stroke(self, **kwargs) -> Self:
kwargs["background"] = True
self.set_stroke(**kwargs)
@ -2458,7 +2488,7 @@ class CurvesAsSubmobjects(VGroup):
if len(self.submobjects) == 0:
caller_name = sys._getframe(1).f_code.co_name
raise Exception(
f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject with no submobjects"
f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject with no submobjects"
)
def _get_submobjects_with_points(self):
@ -2468,7 +2498,7 @@ class CurvesAsSubmobjects(VGroup):
if len(submobjs_with_pts) == 0:
caller_name = sys._getframe(1).f_code.co_name
raise Exception(
f"Cannot call CurvesAsSubmobjects.{caller_name} for a CurvesAsSubmobject whose submobjects have no points"
f"Cannot call CurvesAsSubmobjects. {caller_name} for a CurvesAsSubmobject whose submobjects have no points"
)
return submobjs_with_pts

View file

@ -829,7 +829,9 @@ class StreamLines(VectorField):
step = max(1, int(len(points) / self.max_anchors_per_line))
line.set_points_smoothly(points[::step])
if self.single_color:
line.set_stroke(self.color)
line.set_stroke(
color=self.color, width=self.stroke_width, opacity=opacity
)
else:
if config.renderer == RendererType.OPENGL:
# scaled for compatibility with cairo

View file

@ -1,7 +1,6 @@
from __future__ import annotations
import typing
from typing import Any
import numpy as np
@ -15,6 +14,10 @@ from ..utils.exceptions import EndSceneEarlyException
from ..utils.iterables import list_update
if typing.TYPE_CHECKING:
import types
from typing import Any, Iterable
from manim.animation.animation import Animation
from manim.scene.scene import Scene
@ -51,7 +54,12 @@ class CairoRenderer:
scene.__class__.__name__,
)
def play(self, scene, *args, **kwargs):
def play(
self,
scene: Scene,
*args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
**kwargs,
):
# Reset skip_animations to the original state.
# Needed when rendering only some animations, and skipping others.
self.skip_animations = self._original_skipping_status

View file

@ -2,6 +2,8 @@
from __future__ import annotations
from manim.utils.parameter_parsing import flatten_iterable_parameters
__all__ = ["Scene"]
import copy
@ -13,7 +15,6 @@ import threading
import time
import types
from queue import Queue
from typing import Callable
import srt
@ -25,6 +26,8 @@ try:
dearpygui_imported = True
except ImportError:
dearpygui_imported = False
from typing import TYPE_CHECKING
import numpy as np
from tqdm import tqdm
from watchdog.events import FileSystemEventHandler
@ -48,6 +51,9 @@ from ..utils.family_ops import restructure_list_to_exclude_certain_family_member
from ..utils.file_ops import open_media_file
from ..utils.iterables import list_difference_update, list_update
if TYPE_CHECKING:
from typing import Callable, Iterable
class RerunSceneHandler(FileSystemEventHandler):
"""A class to handle rerunning a Scene after the input file is modified."""
@ -865,7 +871,11 @@ class Scene:
)
return all_moving_mobject_families, static_mobjects
def compile_animations(self, *args: Animation, **kwargs):
def compile_animations(
self,
*args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
**kwargs,
):
"""
Creates _MethodAnimations from any _AnimationBuilders and updates animation
kwargs with kwargs passed to play().
@ -883,7 +893,9 @@ class Scene:
Animations to be played.
"""
animations = []
for arg in args:
arg_anims = flatten_iterable_parameters(args)
# Allow passing a generator to self.play instead of comma separated arguments
for arg in arg_anims:
try:
animations.append(prepare_animation(arg))
except TypeError:
@ -1027,7 +1039,7 @@ class Scene:
def play(
self,
*args,
*args: Animation | Iterable[Animation] | types.GeneratorType[Animation],
subcaption=None,
subcaption_duration=None,
subcaption_offset=0,
@ -1157,7 +1169,11 @@ class Scene:
"""
self.wait(max_time, stop_condition=stop_condition)
def compile_animation_data(self, *animations: Animation, **play_kwargs):
def compile_animation_data(
self,
*animations: Animation | Iterable[Animation] | types.GeneratorType[Animation],
**play_kwargs,
):
"""Given a list of animations, compile the corresponding
static and moving mobjects, and gather the animation durations.

View file

@ -191,7 +191,7 @@ class SceneFileWriter:
and not skip_animations
):
# relative to index file
section_video = f"{self.output_name}_{len(self.sections):04}{config.movie_file_extension}"
section_video = f"{self.output_name}_{len(self.sections):04}_{name}{config.movie_file_extension}"
self.sections.append(
Section(

View file

@ -0,0 +1,31 @@
from __future__ import annotations
from types import GeneratorType
from typing import Iterable, TypeVar
T = TypeVar("T")
def flatten_iterable_parameters(
args: Iterable[T | Iterable[T] | GeneratorType],
) -> list[T]:
"""Flattens an iterable of parameters into a list of parameters.
Parameters
----------
args
The iterable of parameters to flatten.
[(generator), [], (), ...]
Returns
-------
:class:`list`
The flattened list of parameters.
"""
flattened_parameters = []
for arg in args:
if isinstance(arg, (Iterable, GeneratorType)):
flattened_parameters.extend(arg)
else:
flattened_parameters.append(arg)
return flattened_parameters

104
poetry.lock generated
View file

@ -15,7 +15,7 @@ files = [
name = "anyio"
version = "4.1.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "anyio-4.1.0-py3-none-any.whl", hash = "sha256:56a415fbc462291813a94528a779597226619c8e78af7de0507333f700011e5f"},
@ -47,7 +47,7 @@ files = [
name = "argon2-cffi"
version = "23.1.0"
description = "Argon2 for Python"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "argon2_cffi-23.1.0-py3-none-any.whl", hash = "sha256:c670642b78ba29641818ab2e68bd4e6a78ba53b7eff7b4c3815ae16abf91c7ea"},
@ -67,7 +67,7 @@ typing = ["mypy"]
name = "argon2-cffi-bindings"
version = "21.2.0"
description = "Low-level CFFI bindings for Argon2"
optional = true
optional = false
python-versions = ">=3.6"
files = [
{file = "argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3"},
@ -104,7 +104,7 @@ tests = ["pytest"]
name = "arrow"
version = "1.3.0"
description = "Better dates & times for Python"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"},
@ -261,7 +261,7 @@ uvloop = ["uvloop (>=0.15.2)"]
name = "bleach"
version = "6.1.0"
description = "An easy safelist-based HTML-sanitizing tool."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "bleach-6.1.0-py3-none-any.whl", hash = "sha256:3225f354cfc436b9789c66c4ee030194bee0568fbf9cbdad3bc8b5c26c5f12b6"},
@ -842,7 +842,7 @@ files = [
name = "defusedxml"
version = "0.7.1"
description = "XML bomb protection for Python stdlib modules"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
@ -934,7 +934,7 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth
name = "fastjsonschema"
version = "2.19.0"
description = "Fastest Python implementation of JSON schema"
optional = true
optional = false
python-versions = "*"
files = [
{file = "fastjsonschema-2.19.0-py3-none-any.whl", hash = "sha256:b9fd1a2dd6971dbc7fee280a95bd199ae0dd9ce22beb91cc75e9c1c528a5170e"},
@ -1168,7 +1168,7 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
name = "fqdn"
version = "1.5.1"
description = "Validates fully-qualified domain names against RFC 1123, so that they are acceptable to modern bowsers"
optional = true
optional = false
python-versions = ">=2.7, !=3.0, !=3.1, !=3.2, !=3.3, !=3.4, <4"
files = [
{file = "fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014"},
@ -1456,7 +1456,7 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.22)", "pa
name = "isoduration"
version = "20.11.0"
description = "Operations with ISO 8601 durations"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042"},
@ -1551,7 +1551,7 @@ dev = ["hypothesis"]
name = "jsonpointer"
version = "2.4"
description = "Identify specific nodes in a JSON document (RFC 6901)"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*"
files = [
{file = "jsonpointer-2.4-py2.py3-none-any.whl", hash = "sha256:15d51bba20eea3165644553647711d150376234112651b4f1811022aecad7d7a"},
@ -1562,7 +1562,7 @@ files = [
name = "jsonschema"
version = "4.20.0"
description = "An implementation of JSON Schema validation for Python"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"},
@ -1591,7 +1591,7 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-
name = "jsonschema-specifications"
version = "2023.11.2"
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jsonschema_specifications-2023.11.2-py3-none-any.whl", hash = "sha256:e74ba7c0a65e8cb49dc26837d6cfe576557084a8b423ed16a420984228104f93"},
@ -1605,7 +1605,7 @@ referencing = ">=0.31.0"
name = "jupyter-client"
version = "8.6.0"
description = "Jupyter protocol implementation and client libraries"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyter_client-8.6.0-py3-none-any.whl", hash = "sha256:909c474dbe62582ae62b758bca86d6518c85234bdee2d908c778db6d72f39d99"},
@ -1628,7 +1628,7 @@ test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-commit", "pyt
name = "jupyter-core"
version = "5.5.0"
description = "Jupyter core package. A base package on which Jupyter projects rely."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyter_core-5.5.0-py3-none-any.whl", hash = "sha256:e11e02cd8ae0a9de5c6c44abf5727df9f2581055afe00b22183f621ba3585805"},
@ -1648,7 +1648,7 @@ test = ["ipykernel", "pre-commit", "pytest", "pytest-cov", "pytest-timeout"]
name = "jupyter-events"
version = "0.9.0"
description = "Jupyter Event System library"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyter_events-0.9.0-py3-none-any.whl", hash = "sha256:d853b3c10273ff9bc8bb8b30076d65e2c9685579db736873de6c2232dde148bf"},
@ -1686,13 +1686,13 @@ jupyter-server = ">=1.1.2"
[[package]]
name = "jupyter-server"
version = "2.11.1"
version = "2.11.2"
description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyter_server-2.11.1-py3-none-any.whl", hash = "sha256:4b3a16e3ed16fd202588890f10b8ca589bd3e29405d128beb95935f059441373"},
{file = "jupyter_server-2.11.1.tar.gz", hash = "sha256:fe80bab96493acf5f7d6cd9a1575af8fbd253dc2591aa4d015131a1e03b5799a"},
{file = "jupyter_server-2.11.2-py3-none-any.whl", hash = "sha256:0c548151b54bcb516ca466ec628f7f021545be137d01b5467877e87f6fff4374"},
{file = "jupyter_server-2.11.2.tar.gz", hash = "sha256:0c99f9367b0f24141e527544522430176613f9249849be80504c6d2b955004bb"},
]
[package.dependencies]
@ -1724,7 +1724,7 @@ test = ["flaky", "ipykernel", "pre-commit", "pytest (>=7.0)", "pytest-console-sc
name = "jupyter-server-terminals"
version = "0.4.4"
description = "A Jupyter Server Extension Providing Terminals."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyter_server_terminals-0.4.4-py3-none-any.whl", hash = "sha256:75779164661cec02a8758a5311e18bb8eb70c4e86c6b699403100f1585a12a36"},
@ -1775,7 +1775,7 @@ test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-cons
name = "jupyterlab-pygments"
version = "0.3.0"
description = "Pygments theme using JupyterLab CSS variables"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780"},
@ -1969,6 +1969,14 @@ files = [
{file = "mapbox_earcut-1.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9af9369266bf0ca32f4d401152217c46c699392513f22639c6b1be32bde9c1cc"},
{file = "mapbox_earcut-1.0.1-cp311-cp311-win32.whl", hash = "sha256:ff9a13be4364625697b0e0e04ba6a0f77300148b871bba0a85bfa67e972e85c4"},
{file = "mapbox_earcut-1.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e736557539c74fa969e866889c2b0149fc12668f35e3ae33667d837ff2880d3"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:4fe92174410e4120022393013705d77cb856ead5bdf6c81bec614a70df4feb5d"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:082f70a865c6164a60af039aa1c377073901cf1f94fd37b1c5610dfbae2a7369"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43d268ece49d0c9e22cb4f92cd54c2cc64f71bf1c5e10800c189880d923e1292"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7748f1730fd36dd1fcf0809d8f872d7e1ddaa945f66a6a466ad37ef3c552ae93"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:5a82d10c8dec2a0bd9a6a6c90aca7044017c8dad79f7e209fd0667826f842325"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:01b292588cd3f6bad7d76ee31c004ed1b557a92bbd9602a72d2be15513b755be"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-win32.whl", hash = "sha256:fce236ddc3a56ea7260acc94601a832c260e6ac5619374bb2cec2e73e7414ff0"},
{file = "mapbox_earcut-1.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1ce86407353b4f09f5778c436518bbbc6f258f46c5736446f25074fe3d3a3bd8"},
{file = "mapbox_earcut-1.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:aa6111a18efacb79c081f3d3cdd7d25d0585bb0e9f28896b207ebe1d56efa40e"},
{file = "mapbox_earcut-1.0.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2911829d1e6e5e1282fbe2840fadf578f606580f02ed436346c2d51c92f810b"},
{file = "mapbox_earcut-1.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ff909a7b8405a923abedd701b53633c997cc2b5dc9d5b78462f51c25ec2c33"},
@ -2221,7 +2229,7 @@ files = [
name = "mistune"
version = "3.0.2"
description = "A sane and fast Markdown parser with useful plugins and renderers"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"},
@ -2383,7 +2391,7 @@ testing = ["beautifulsoup4", "coverage", "docutils (>=0.17.0,<0.18.0)", "pytest
name = "nbclient"
version = "0.9.0"
description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor."
optional = true
optional = false
python-versions = ">=3.8.0"
files = [
{file = "nbclient-0.9.0-py3-none-any.whl", hash = "sha256:a3a1ddfb34d4a9d17fc744d655962714a866639acd30130e9be84191cd97cd15"},
@ -2405,7 +2413,7 @@ test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=
name = "nbconvert"
version = "7.11.0"
description = "Converting Jupyter Notebooks"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "nbconvert-7.11.0-py3-none-any.whl", hash = "sha256:d1d417b7f34a4e38887f8da5bdfd12372adf3b80f995d57556cb0972c68909fe"},
@ -2443,7 +2451,7 @@ webpdf = ["playwright"]
name = "nbformat"
version = "5.9.2"
description = "The Jupyter Notebook format"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "nbformat-5.9.2-py3-none-any.whl", hash = "sha256:1c5172d786a41b82bcfd0c23f9e6b6f072e8fb49c39250219e4acfff1efe89e9"},
@ -2592,7 +2600,7 @@ files = [
name = "overrides"
version = "7.4.0"
description = "A decorator to automatically detect mismatch when overriding a method."
optional = true
optional = false
python-versions = ">=3.6"
files = [
{file = "overrides-7.4.0-py3-none-any.whl", hash = "sha256:3ad24583f86d6d7a49049695efe9933e67ba62f0c7625d53c59fa832ce4b8b7d"},
@ -2614,7 +2622,7 @@ files = [
name = "pandocfilters"
version = "1.5.0"
description = "Utilities for writing pandoc filters in python"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "pandocfilters-1.5.0-py2.py3-none-any.whl", hash = "sha256:33aae3f25fd1a026079f5d27bdd52496f0e0803b3469282162bafdcbdf6ef14f"},
@ -2780,7 +2788,7 @@ virtualenv = ">=20.10.0"
name = "prometheus-client"
version = "0.19.0"
description = "Python client for the Prometheus monitoring system."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "prometheus_client-0.19.0-py3-none-any.whl", hash = "sha256:c88b1e6ecf6b41cd8fb5731c7ae919bf66df6ec6fafa555cd6c0e16ca169ae92"},
@ -2861,7 +2869,7 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "unittest2", "wmi"]
name = "ptyprocess"
version = "0.7.0"
description = "Run a subprocess in a pseudo terminal"
optional = true
optional = false
python-versions = "*"
files = [
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
@ -3228,7 +3236,7 @@ six = ">=1.5"
name = "python-json-logger"
version = "2.0.7"
description = "A python library adding a json log formatter"
optional = true
optional = false
python-versions = ">=3.6"
files = [
{file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"},
@ -3239,7 +3247,7 @@ files = [
name = "pywin32"
version = "306"
description = "Python for Window Extensions"
optional = true
optional = false
python-versions = "*"
files = [
{file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"},
@ -3262,7 +3270,7 @@ files = [
name = "pywinpty"
version = "2.0.12"
description = "Pseudo terminal support for Windows from Python."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "pywinpty-2.0.12-cp310-none-win_amd64.whl", hash = "sha256:21319cd1d7c8844fb2c970fb3a55a3db5543f112ff9cfcd623746b9c47501575"},
@ -3336,7 +3344,7 @@ files = [
name = "pyzmq"
version = "25.1.1"
description = "Python bindings for 0MQ"
optional = true
optional = false
python-versions = ">=3.6"
files = [
{file = "pyzmq-25.1.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:381469297409c5adf9a0e884c5eb5186ed33137badcbbb0560b86e910a2f1e76"},
@ -3441,7 +3449,7 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""}
name = "referencing"
version = "0.31.1"
description = "JSON Referencing + Python"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "referencing-0.31.1-py3-none-any.whl", hash = "sha256:c19c4d006f1757e3dd75c4f784d38f8698d87b649c54f9ace14e5e8c9667c01d"},
@ -3490,7 +3498,7 @@ docutils = ">=0.11,<1.0"
name = "rfc3339-validator"
version = "0.1.4"
description = "A pure python RFC3339 validator"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa"},
@ -3504,7 +3512,7 @@ six = "*"
name = "rfc3986-validator"
version = "0.1.1"
description = "Pure python rfc3986 validator"
optional = true
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9"},
@ -3533,7 +3541,7 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
name = "rpds-py"
version = "0.13.2"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "rpds_py-0.13.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:1ceebd0ae4f3e9b2b6b553b51971921853ae4eebf3f54086be0565d59291e53d"},
@ -3698,7 +3706,7 @@ pyobjc-framework-Cocoa = {version = "*", markers = "sys_platform == \"darwin\""}
name = "send2trash"
version = "1.8.2"
description = "Send file to trash natively under Mac OS X, Windows and Linux"
optional = true
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
files = [
{file = "Send2Trash-1.8.2-py3-none-any.whl", hash = "sha256:a384719d99c07ce1eefd6905d2decb6f8b7ed054025bb0e618919f945de4f679"},
@ -3803,7 +3811,7 @@ files = [
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
@ -4063,7 +4071,7 @@ files = [
name = "terminado"
version = "0.18.0"
description = "Tornado websocket backend for the Xterm.js Javascript terminal emulator library."
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "terminado-0.18.0-py3-none-any.whl", hash = "sha256:87b0d96642d0fe5f5abd7783857b9cab167f221a39ff98e3b9619a788a3c0f2e"},
@ -4084,7 +4092,7 @@ typing = ["mypy (>=1.6,<2.0)", "traitlets (>=5.11.1)"]
name = "tinycss2"
version = "1.2.1"
description = "A tiny CSS parser"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "tinycss2-1.2.1-py3-none-any.whl", hash = "sha256:2b80a96d41e7c3914b8cda8bc7f705a4d9c49275616e886103dd839dfc847847"},
@ -4113,7 +4121,7 @@ files = [
name = "tornado"
version = "6.4"
description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed."
optional = true
optional = false
python-versions = ">= 3.8"
files = [
{file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"},
@ -4153,7 +4161,7 @@ telegram = ["requests"]
name = "traitlets"
version = "5.14.0"
description = "Traitlets Python configuration system"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "traitlets-5.14.0-py3-none-any.whl", hash = "sha256:f14949d23829023013c47df20b4a76ccd1a85effb786dc060f34de7948361b33"},
@ -4216,7 +4224,7 @@ types-setuptools = "*"
name = "types-python-dateutil"
version = "2.8.19.14"
description = "Typing stubs for python-dateutil"
optional = true
optional = false
python-versions = "*"
files = [
{file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"},
@ -4249,7 +4257,7 @@ files = [
name = "uri-template"
version = "1.3.0"
description = "RFC 6570 URI Template Processor"
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7"},
@ -4349,7 +4357,7 @@ files = [
name = "webcolors"
version = "1.13"
description = "A library for working with the color formats defined by HTML and CSS."
optional = true
optional = false
python-versions = ">=3.7"
files = [
{file = "webcolors-1.13-py3-none-any.whl", hash = "sha256:29bc7e8752c0a1bd4a1f03c14d6e6a72e93d82193738fa860cbff59d0fcc11bf"},
@ -4364,7 +4372,7 @@ tests = ["pytest", "pytest-cov"]
name = "webencodings"
version = "0.5.1"
description = "Character encoding aliases for legacy web content"
optional = true
optional = false
python-versions = "*"
files = [
{file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
@ -4375,7 +4383,7 @@ files = [
name = "websocket-client"
version = "1.6.4"
description = "WebSocket client for Python with low level API options"
optional = true
optional = false
python-versions = ">=3.8"
files = [
{file = "websocket-client-1.6.4.tar.gz", hash = "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df"},

View file

@ -10,14 +10,14 @@
},
"section_dir_layout": [
"SquareToCircle.json",
"SquareToCircle_0000.mp4",
"SquareToCircle_0000_autocreated.mp4",
"."
],
"section_index": [
{
"name": "autocreated",
"type": "default.normal",
"video": "SquareToCircle_0000.mp4",
"video": "SquareToCircle_0000_autocreated.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,

View file

@ -10,18 +10,18 @@
},
"section_dir_layout": [
"SceneWithSections.json",
"SceneWithSections_0004.mp4",
"SceneWithSections_0003.mp4",
"SceneWithSections_0002.mp4",
"SceneWithSections_0001.mp4",
"SceneWithSections_0000.mp4",
"SceneWithSections_0004_unnamed.mp4",
"SceneWithSections_0003_Prepare For Unforeseen Consequences..mp4",
"SceneWithSections_0002_test.mp4",
"SceneWithSections_0001_unnamed.mp4",
"SceneWithSections_0000_autocreated.mp4",
"."
],
"section_index": [
{
"name": "autocreated",
"type": "default.normal",
"video": "SceneWithSections_0000.mp4",
"video": "SceneWithSections_0000_autocreated.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -32,7 +32,7 @@
{
"name": "unnamed",
"type": "default.normal",
"video": "SceneWithSections_0001.mp4",
"video": "SceneWithSections_0001_unnamed.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -43,7 +43,7 @@
{
"name": "test",
"type": "default.normal",
"video": "SceneWithSections_0002.mp4",
"video": "SceneWithSections_0002_test.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -54,7 +54,7 @@
{
"name": "Prepare For Unforeseen Consequences.",
"type": "default.normal",
"video": "SceneWithSections_0003.mp4",
"video": "SceneWithSections_0003_Prepare For Unforeseen Consequences..mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -65,7 +65,7 @@
{
"name": "unnamed",
"type": "presentation.skip",
"video": "SceneWithSections_0004.mp4",
"video": "SceneWithSections_0004_unnamed.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,

View file

@ -10,16 +10,16 @@
},
"section_dir_layout": [
"ElaborateSceneWithSections.json",
"ElaborateSceneWithSections_0003.mp4",
"ElaborateSceneWithSections_0001.mp4",
"ElaborateSceneWithSections_0000.mp4",
"ElaborateSceneWithSections_0003_fade out.mp4",
"ElaborateSceneWithSections_0001_transform to circle.mp4",
"ElaborateSceneWithSections_0000_create square.mp4",
"."
],
"section_index": [
{
"name": "create square",
"type": "default.normal",
"video": "ElaborateSceneWithSections_0000.mp4",
"video": "ElaborateSceneWithSections_0000_create square.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -30,7 +30,7 @@
{
"name": "transform to circle",
"type": "default.normal",
"video": "ElaborateSceneWithSections_0001.mp4",
"video": "ElaborateSceneWithSections_0001_transform to circle.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,
@ -41,7 +41,7 @@
{
"name": "fade out",
"type": "default.normal",
"video": "ElaborateSceneWithSections_0003.mp4",
"video": "ElaborateSceneWithSections_0003_fade out.mp4",
"codec_name": "h264",
"width": 854,
"height": 480,

View file

@ -28,7 +28,9 @@ def test_manim_cfg_subcommand():
command = ["cfg"]
runner = CliRunner()
result = runner.invoke(main, command, prog_name="manim")
expected_output = """\
expected_output = f"""\
Manim Community v{__version__}
Usage: manim cfg [OPTIONS] COMMAND [ARGS]...
Manages Manim configuration files.
@ -50,7 +52,9 @@ def test_manim_plugins_subcommand():
command = ["plugins"]
runner = CliRunner()
result = runner.invoke(main, command, prog_name="manim")
expected_output = """\
expected_output = f"""\
Manim Community v{__version__}
Usage: manim plugins [OPTIONS]
Manages Manim plugins.
@ -90,7 +94,9 @@ def test_manim_init_subcommand():
command = ["init"]
runner = CliRunner()
result = runner.invoke(main, command, prog_name="manim")
expected_output = """\
expected_output = f"""\
Manim Community v{__version__}
Usage: manim init [OPTIONS] COMMAND [ARGS]...
Create a new project or insert a new scene.
@ -135,24 +141,3 @@ def test_manim_init_scene(tmp_path):
assert (Path(tmp_dir) / "main.py").exists()
file_content = (Path(tmp_dir) / "main.py").read_text()
assert "DefaultFileTestScene(Scene):" in file_content
def test_manim_new_command():
command = ["new"]
runner = CliRunner()
result = runner.invoke(main, command, prog_name="manim")
expected_output = """\
Usage: manim new [OPTIONS] COMMAND [ARGS]...
(Deprecated) Create a new project or insert a new scene.
Options:
--help Show this message and exit.
Commands:
project Creates a new project.
scene Inserts a SCENE to an existing FILE or creates a new FILE.
Made with <3 by Manim Community developers.
"""
assert dedent(expected_output) == result.output

View file

@ -169,3 +169,13 @@ def test_animationgroup_is_passing_remover_to_nested_animationgroups():
assert sqr_animation.remover
assert circ_animation.remover
assert polygon_animation.remover
def test_empty_animation_group_fails():
with pytest.raises(ValueError, match="Please add at least one Animation"):
AnimationGroup().begin()
def test_empty_animation_fails():
with pytest.raises(ValueError, match="Please set the run_time to be positive"):
FadeIn(None, run_time=0).begin()

View file

@ -0,0 +1,14 @@
import numpy as np
import pytest
from manim import ImageMobject
@pytest.mark.parametrize("dtype", [np.uint8, np.uint16])
def test_invert_image(dtype):
array = (255 * np.random.rand(10, 10, 4)).astype(dtype)
image = ImageMobject(array, pixel_array_dtype=dtype, invert=True)
assert image.pixel_array.dtype == dtype
array[:, :, :3] = np.iinfo(dtype).max - array[:, :, :3]
assert np.allclose(array, image.pixel_array)

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import manim.utils.color as C
from manim import VMobject
from manim.mobject.vector_field import StreamLines
def test_stroke_props_in_ctor():
@ -24,3 +25,17 @@ def test_set_background_stroke():
assert m.background_stroke_width == 2
assert m.background_stroke_opacity == 0.8
assert m.background_stroke_color.to_hex() == C.ORANGE.to_hex()
def test_streamline_attributes_for_single_color():
vector_field = StreamLines(
lambda x: x, # It is not important what this function is.
x_range=[-1, 1, 0.1],
y_range=[-1, 1, 0.1],
padding=0.1,
stroke_width=1.0,
opacity=0.2,
color=C.BLUE_D,
)
assert vector_field[0].stroke_width == 1.0
assert vector_field[0].stroke_opacity == 0.2

View file

@ -53,3 +53,22 @@ def test_vmobject_joint_types(scene):
lines.arrange(RIGHT, buff=1)
scene.add(lines)
@frames_comparison
def test_vmobject_cap_styles(scene):
arcs = VGroup(
*[
Arc(
radius=1,
start_angle=0,
angle=TAU / 4,
stroke_width=20,
color=GREEN,
cap_style=cap_style,
)
for cap_style in CapStyleType
]
)
arcs.arrange(RIGHT, buff=1)
scene.add(arcs)